Files
tech-log-frontend/scripts/lib/provider-cgroup.ts
T

170 lines
5.5 KiB
TypeScript

export type ProviderKind = "vulnerability" | "provenance";
const MEMORY_MAX = 1_073_741_824;
const TASKS_MAX = 64;
const STOP_TIMEOUT_MS = 5_000;
const RUNTIME_GRACE_MS = 10_000;
const UNIT_NAME = /^ca-provider-(?:vulnerability|provenance)-[1-9][0-9]*-[0-9a-f]{24}\.scope$/u;
const UNIT_NONCE = /^[0-9a-f]{24}$/u;
const ENVIRONMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
const ENFORCEMENT_GATE = [
'cgroup_path=""',
"while IFS=: read -r hierarchy controllers candidate; do",
' if [ "$hierarchy" = 0 ] && [ -z "$controllers" ]; then cgroup_path=$candidate; fi',
"done < /proc/self/cgroup",
'if [ -z "$cgroup_path" ]; then',
" printf '%s\\n' 'provider cgroup enforcement failed: unified cgroup v2 membership is required' >&2",
" exit 125",
"fi",
'case "$cgroup_path" in',
' */"$0") ;;',
" *)",
" printf '%s\\n' 'provider cgroup enforcement failed: unit membership is invalid' >&2",
" exit 125",
" ;;",
"esac",
"cgroup_root=/sys/fs/cgroup$cgroup_path",
"require_cgroup_value() {",
' actual=$(/bin/cat "$cgroup_root/$1") || {',
" printf 'provider cgroup enforcement failed: cannot read %s\\n' \"$1\" >&2",
" exit 125",
" }",
' if [ "$actual" != "$2" ]; then',
" printf 'provider cgroup enforcement failed: %s is %s, expected %s\\n' \"$1\" \"$actual\" \"$2\" >&2",
" exit 125",
" fi",
"}",
`require_cgroup_value memory.max ${MEMORY_MAX}`,
"require_cgroup_value memory.swap.max 0",
`require_cgroup_value pids.max ${TASKS_MAX}`,
"require_cgroup_value cpu.max '100000 100000'",
'exec "$@"',
].join("\n");
export function formatProviderCgroupUnitName(
kind: ProviderKind,
supervisorPid: number,
nonce: string,
): string {
if (!Number.isSafeInteger(supervisorPid) || supervisorPid <= 0 || !UNIT_NONCE.test(nonce)) {
throw new TypeError("provider cgroup unit identity is invalid");
}
const unit = `ca-provider-${kind}-${supervisorPid}-${nonce}.scope`;
assertUnit(unit);
return unit;
}
export function systemdRunProviderArguments(
unit: string,
timeoutMs: number,
cpuSeconds: number,
nodeExecutable: string,
wrapperScript: string,
reportPath: string,
reportDev: number,
reportIno: number,
): string[] {
assertUnit(unit);
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > Number.MAX_SAFE_INTEGER - RUNTIME_GRACE_MS) {
throw new TypeError("provider cgroup runtime is invalid");
}
if (!Number.isSafeInteger(cpuSeconds) || cpuSeconds <= 0) {
throw new TypeError("provider cgroup CPU limit is invalid");
}
if (
!nodeExecutable.startsWith("/") || !wrapperScript.startsWith("/") ||
!reportPath.startsWith("/") || reportPath.includes("\0") ||
!Number.isSafeInteger(reportDev) || reportDev <= 0 ||
!Number.isSafeInteger(reportIno) || reportIno <= 0
) {
throw new TypeError("provider scope wrapper path is invalid");
}
return [
"--user",
"--scope",
"--collect",
"--quiet",
"--expand-environment=no",
`--unit=${unit}`,
`--property=MemoryMax=${MEMORY_MAX}`,
"--property=MemorySwapMax=0",
`--property=TasksMax=${TASKS_MAX}`,
"--property=CPUQuota=100%",
"--property=CPUQuotaPeriodSec=100ms",
"--property=KillMode=control-group",
"--property=SendSIGKILL=yes",
`--property=TimeoutStopSec=${STOP_TIMEOUT_MS}ms`,
`--property=RuntimeMaxSec=${timeoutMs + RUNTIME_GRACE_MS}ms`,
"--",
"/bin/sh",
"-eu",
"-c",
ENFORCEMENT_GATE,
unit,
nodeExecutable,
wrapperScript,
String(cpuSeconds),
reportPath,
String(reportDev),
String(reportIno),
];
}
export type ProviderScopeFrame = Readonly<{
bwrapInput: Buffer;
reportPath: string;
reportDev: number;
reportIno: number;
}>;
export function encodeProviderScopeFrame(input: ProviderScopeFrame): Buffer {
if (
!Buffer.isBuffer(input.bwrapInput) || input.bwrapInput.byteLength === 0 ||
!input.reportPath.startsWith("/") || input.reportPath.includes("\0") ||
!Number.isSafeInteger(input.reportDev) || input.reportDev <= 0 ||
!Number.isSafeInteger(input.reportIno) || input.reportIno <= 0
) {
throw new TypeError("provider scope frame is invalid");
}
const payload = Buffer.from(JSON.stringify({
bwrapInputBase64: input.bwrapInput.toString("base64"),
reportPath: input.reportPath,
reportDev: input.reportDev,
reportIno: input.reportIno,
}));
const frame = Buffer.allocUnsafe(4 + payload.byteLength);
frame.writeUInt32BE(payload.byteLength, 0);
payload.copy(frame, 4);
return frame;
}
export function encodeProviderBwrapInput(
arguments_: readonly string[],
environment: Readonly<Record<string, string | undefined>>,
): Buffer {
if (arguments_.some((argument) => argument.includes("\0"))) {
throw new TypeError("provider bwrap argument is invalid");
}
const entries = Object.entries(environment).sort(([left], [right]) =>
left < right ? -1 : left > right ? 1 : 0,
);
if (entries.some(([name, value]) =>
!ENVIRONMENT_NAME.test(name) || value === undefined || value.includes("\0")
)) {
throw new TypeError("provider bwrap environment is invalid");
}
const input = ["--clearenv"];
for (const [name, value] of entries) input.push("--setenv", name, value ?? "");
input.push(...arguments_);
return Buffer.from(`${input.join("\0")}\0`);
}
export function systemctlKillProviderArguments(unit: string): string[] {
assertUnit(unit);
return ["--user", "kill", "--kill-whom=all", "--signal=SIGKILL", unit];
}
function assertUnit(unit: string): void {
if (!UNIT_NAME.test(unit)) throw new TypeError("provider cgroup unit name is invalid");
}