167 lines
5.4 KiB
TypeScript
167 lines
5.4 KiB
TypeScript
import { spawn } from "node:child_process";
|
|
import { closeSync, createReadStream, writeSync } from "node:fs";
|
|
|
|
import { cleanupOwnedProviderReport } from "./provider-raw-cleanup.ts";
|
|
|
|
const MAX_FRAME_BYTES = 16_777_216;
|
|
const reportIdentity = parseReportIdentity(process.argv.slice(2));
|
|
let pending = Buffer.alloc(0);
|
|
let expectedBytes: number | undefined;
|
|
let provider: ReturnType<typeof spawn> | undefined;
|
|
let providerClosed = false;
|
|
let livenessLost = false;
|
|
const liveness = createReadStream("", { fd: 0, autoClose: false });
|
|
|
|
liveness.on("data", (chunk: Buffer | string) => {
|
|
if (provider) {
|
|
terminateForProtocolFailure("provider scope received trailing protocol bytes");
|
|
return;
|
|
}
|
|
pending = Buffer.concat([pending, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
|
if (expectedBytes === undefined && pending.byteLength >= 4) {
|
|
expectedBytes = pending.readUInt32BE(0);
|
|
if (expectedBytes <= 0 || expectedBytes > MAX_FRAME_BYTES) {
|
|
terminateForProtocolFailure("provider scope frame length is invalid");
|
|
return;
|
|
}
|
|
}
|
|
if (expectedBytes !== undefined && pending.byteLength === expectedBytes + 4) {
|
|
launchProvider(pending.subarray(4));
|
|
pending = Buffer.alloc(0);
|
|
} else if (expectedBytes !== undefined && pending.byteLength > expectedBytes + 4) {
|
|
terminateForProtocolFailure("provider scope frame has trailing bytes");
|
|
}
|
|
});
|
|
|
|
liveness.once("end", () => terminateForParentLoss());
|
|
liveness.once("error", () => terminateForParentLoss());
|
|
|
|
function launchProvider(payload: Buffer): void {
|
|
const frame = parseFrame(payload);
|
|
if (
|
|
frame.reportPath !== reportIdentity.reportPath ||
|
|
frame.reportDev !== reportIdentity.reportDev ||
|
|
frame.reportIno !== reportIdentity.reportIno
|
|
) {
|
|
throw new TypeError("provider scope frame identity does not match its launch identity");
|
|
}
|
|
const bwrapInput = Buffer.from(frame.bwrapInputBase64, "base64");
|
|
provider = spawn("/usr/bin/bwrap", ["--args", "0"], {
|
|
detached: true,
|
|
stdio: ["pipe", "inherit", "inherit"],
|
|
});
|
|
provider.stdin?.end(bwrapInput);
|
|
provider.once("error", (error) => finishProvider(frame, null, null, error));
|
|
provider.once("close", (code, signal) => finishProvider(frame, code, signal));
|
|
}
|
|
|
|
async function finishProvider(
|
|
frame: ReturnType<typeof parseFrame>,
|
|
code: number | null,
|
|
signal: NodeJS.Signals | null,
|
|
error?: Error,
|
|
): Promise<void> {
|
|
if (providerClosed) return;
|
|
providerClosed = true;
|
|
if (livenessLost) await cleanupOwnedProviderReport(frame);
|
|
closeLivenessInput();
|
|
if (error) {
|
|
writeSync(2, `${error.message}\n`);
|
|
process.exit(1);
|
|
}
|
|
if (signal) process.exit(128 + signalNumber(signal));
|
|
process.exit(code ?? 1);
|
|
}
|
|
|
|
function terminateForParentLoss(): void {
|
|
if (livenessLost) return;
|
|
livenessLost = true;
|
|
if (!provider || providerClosed) {
|
|
void cleanupAfterParentLossAndExit();
|
|
return;
|
|
}
|
|
try {
|
|
process.kill(-provider.pid!, "SIGKILL");
|
|
} catch (error) {
|
|
if (!hasErrorCode(error, "ESRCH")) throw error;
|
|
}
|
|
}
|
|
|
|
async function cleanupAfterParentLossAndExit(): Promise<void> {
|
|
try {
|
|
await cleanupOwnedProviderReport(reportIdentity);
|
|
} catch (error) {
|
|
writeSync(2, `${error instanceof Error ? error.message : String(error)}\n`);
|
|
}
|
|
closeLivenessInput();
|
|
process.exit(125);
|
|
}
|
|
|
|
function terminateForProtocolFailure(message: string): void {
|
|
writeSync(2, `${message}\n`);
|
|
terminateForParentLoss();
|
|
}
|
|
|
|
function closeLivenessInput(): void {
|
|
liveness.removeAllListeners();
|
|
liveness.destroy();
|
|
try {
|
|
closeSync(0);
|
|
} catch (error) {
|
|
if (!hasErrorCode(error, "EBADF")) throw error;
|
|
}
|
|
}
|
|
|
|
function parseFrame(payload: Buffer): Readonly<{
|
|
bwrapInputBase64: string;
|
|
reportPath: string;
|
|
reportDev: number;
|
|
reportIno: number;
|
|
}> {
|
|
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(payload)) as Record<string, unknown>;
|
|
if (
|
|
typeof value.bwrapInputBase64 !== "string" ||
|
|
typeof value.reportPath !== "string" || !value.reportPath.startsWith("/") ||
|
|
!Number.isSafeInteger(value.reportDev) || Number(value.reportDev) <= 0 ||
|
|
!Number.isSafeInteger(value.reportIno) || Number(value.reportIno) <= 0
|
|
) {
|
|
throw new TypeError("provider scope frame payload is invalid");
|
|
}
|
|
return {
|
|
bwrapInputBase64: value.bwrapInputBase64,
|
|
reportPath: value.reportPath,
|
|
reportDev: Number(value.reportDev),
|
|
reportIno: Number(value.reportIno),
|
|
};
|
|
}
|
|
|
|
function parseReportIdentity(arguments_: readonly string[]): Readonly<{
|
|
cpuSeconds: number;
|
|
reportPath: string;
|
|
reportDev: number;
|
|
reportIno: number;
|
|
}> {
|
|
const [cpuValue, reportPath, devValue, inoValue, ...trailing] = arguments_;
|
|
const cpuSeconds = Number(cpuValue);
|
|
const reportDev = Number(devValue);
|
|
const reportIno = Number(inoValue);
|
|
if (
|
|
trailing.length > 0 ||
|
|
!Number.isSafeInteger(cpuSeconds) || cpuSeconds <= 0 ||
|
|
typeof reportPath !== "string" || !reportPath.startsWith("/") || reportPath.includes("\0") ||
|
|
!Number.isSafeInteger(reportDev) || reportDev <= 0 ||
|
|
!Number.isSafeInteger(reportIno) || reportIno <= 0
|
|
) {
|
|
throw new TypeError("provider scope launch identity is invalid");
|
|
}
|
|
return { cpuSeconds, reportPath, reportDev, reportIno };
|
|
}
|
|
|
|
function signalNumber(signal: NodeJS.Signals): number {
|
|
return signal === "SIGKILL" ? 9 : signal === "SIGXCPU" ? 24 : 1;
|
|
}
|
|
|
|
function hasErrorCode(error: unknown, code: string): boolean {
|
|
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
|
|
}
|