fix(runner): inherit verified node descriptor across sidecars
This commit is contained in:
parent
d7faaafa6d
commit
cbf51b9da8
|
|
@ -12,7 +12,7 @@ import {
|
|||
import { createServer, type Server } from "node:net";
|
||||
import { isAbsolute, join, resolve } from "node:path";
|
||||
|
||||
import { verifiedRuntimeExecutable } from "./verified-runtime-executable.js";
|
||||
import { verifiedRuntimeExecutableHandoff } from "./verified-runtime-executable.js";
|
||||
|
||||
const MAX_CODEX_CREDENTIAL_BYTES = 256 * 1024;
|
||||
const PRIVATE_FILE_MODE = 0o600;
|
||||
|
|
@ -1140,8 +1140,9 @@ async function runDirectorySyncHelper(directory: string): Promise<void> {
|
|||
}
|
||||
let child: ChildProcess;
|
||||
try {
|
||||
const runtimeHandoff = verifiedRuntimeExecutableHandoff(3);
|
||||
child = spawn(
|
||||
verifiedRuntimeExecutable(),
|
||||
runtimeHandoff.executable,
|
||||
[
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
|
|
@ -1152,7 +1153,10 @@ async function runDirectorySyncHelper(directory: string): Promise<void> {
|
|||
// The helper imports only Node built-ins. Do not inherit loader hooks or
|
||||
// any credential-bearing process environment into the durability worker.
|
||||
env: {},
|
||||
stdio: "ignore",
|
||||
stdio:
|
||||
runtimeHandoff.sourceFd === null
|
||||
? "ignore"
|
||||
: ["ignore", "ignore", "ignore", runtimeHandoff.sourceFd],
|
||||
windowsHide: true,
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import type { Readable, Writable } from "node:stream";
|
|||
import type { QualifiedAcpxProfile } from "./qualified-profiles.js";
|
||||
import {
|
||||
VERIFIED_RUNTIME_EXECUTABLE_ENV,
|
||||
verifiedRuntimeExecutable,
|
||||
verifiedRuntimeExecutableHandoff,
|
||||
} from "./verified-runtime-executable.js";
|
||||
|
||||
const MAX_PACKAGE_JSON_BYTES = 256 * 1024;
|
||||
|
|
@ -132,7 +132,12 @@ const OWNER_FD = PROVIDER_RUNTIME_EXECUTABLE_FD + providerRuntimeExecutableCount
|
|||
const OWNERSHIP_FD = OWNER_FD + 1;
|
||||
const PROVIDER_EXIT_FD = OWNERSHIP_FD + 1;
|
||||
const CREDENTIAL_FENCE_FD_START = PROVIDER_EXIT_FD + 1;
|
||||
const VERIFIED_RUNTIME_FD = CREDENTIAL_FENCE_FD_START + 2;
|
||||
const dependencyAncestorFds = Array.from({ length: dependencyAncestorCount }, (_, index) => ${DEPENDENCY_ANCESTOR_FD_START} + index);
|
||||
const runtimeDescriptorMatch = /^\\/proc\\/self\\/fd\\/([0-9]+)$/.exec(runtimeExecutable);
|
||||
const runtimeDescriptorFd = runtimeDescriptorMatch === null ? null : Number.parseInt(runtimeDescriptorMatch[1], 10);
|
||||
if (runtimeDescriptorFd !== null && runtimeDescriptorFd !== VERIFIED_RUNTIME_FD) throw new Error("ACPX verified runtime descriptor is misplaced");
|
||||
if (runtimeDescriptorFd !== null) fs.fstatSync(runtimeDescriptorFd);
|
||||
let provider;
|
||||
let watchdog;
|
||||
let reaped = false;
|
||||
|
|
@ -179,7 +184,7 @@ const startProvider = () => {
|
|||
// The provider observes this guardian-owned pipe directly. Kernel EOF
|
||||
// therefore revokes it even when SIGKILL/OOM prevents our JS reap path.
|
||||
// It also inherits both quorum fences until that self-reap completes.
|
||||
stdio: [0, 1, 2, ${COMMAND_SOURCE_FD}, ${COMMAND_DIRECTORY_FD}, ...dependencyAncestorFds, ...(providerRuntimeExecutableCount === 1 ? [PROVIDER_RUNTIME_EXECUTABLE_FD] : []), "pipe", PROVIDER_EXIT_FD, CREDENTIAL_FENCE_FD_START, CREDENTIAL_FENCE_FD_START + 1],
|
||||
stdio: [0, 1, 2, ${COMMAND_SOURCE_FD}, ${COMMAND_DIRECTORY_FD}, ...dependencyAncestorFds, ...(providerRuntimeExecutableCount === 1 ? [PROVIDER_RUNTIME_EXECUTABLE_FD] : []), "pipe", PROVIDER_EXIT_FD, CREDENTIAL_FENCE_FD_START, CREDENTIAL_FENCE_FD_START + 1, ...(runtimeDescriptorFd === null ? [] : ["ignore", runtimeDescriptorFd])],
|
||||
windowsHide: true,
|
||||
},
|
||||
);
|
||||
|
|
@ -209,12 +214,17 @@ try {
|
|||
// its live identity if this guardian is killed before it can run its reap.
|
||||
// Its private owner pipe reaches kernel EOF on guardian death even while the
|
||||
// provider is stopped and unable to process its own guardian-loss callback.
|
||||
const watchdogStdio = ["ignore", "ignore", "ignore", "pipe", "pipe"];
|
||||
if (runtimeDescriptorFd !== null) {
|
||||
while (watchdogStdio.length < runtimeDescriptorFd) watchdogStdio.push("ignore");
|
||||
watchdogStdio.push(runtimeDescriptorFd);
|
||||
}
|
||||
watchdog = spawn(runtimeExecutable, ["--eval", WATCHDOG_SOURCE], {
|
||||
cwd: process.cwd(),
|
||||
detached: false,
|
||||
env: {},
|
||||
shell: false,
|
||||
stdio: ["ignore", "ignore", "ignore", "pipe", "pipe"],
|
||||
stdio: watchdogStdio,
|
||||
windowsHide: true,
|
||||
});
|
||||
const watchdogOwnerPipe = watchdog.stdio[3];
|
||||
|
|
@ -1299,9 +1309,20 @@ function commandLease(
|
|||
) {
|
||||
throw new Error("ACPX provider credential fence is invalid");
|
||||
}
|
||||
const runtimeExecutable = verifiedRuntimeExecutable();
|
||||
const runtimeTargetFd = guarded
|
||||
? providerExitFd + 3
|
||||
: DEPENDENCY_ANCESTOR_FD_START +
|
||||
dependencyAncestors.length +
|
||||
providerRuntimeExecutableCount;
|
||||
const runtimeHandoff =
|
||||
verifiedRuntimeExecutableHandoff(runtimeTargetFd);
|
||||
const environment = sanitizedNodeEnvironment(options.env);
|
||||
environment[VERIFIED_RUNTIME_EXECUTABLE_ENV] = runtimeExecutable;
|
||||
if (runtimeHandoff.environmentValue === undefined) {
|
||||
delete environment[VERIFIED_RUNTIME_EXECUTABLE_ENV];
|
||||
} else {
|
||||
environment[VERIFIED_RUNTIME_EXECUTABLE_ENV] =
|
||||
runtimeHandoff.environmentValue;
|
||||
}
|
||||
if (
|
||||
(providerRuntimeExecutable === null) !==
|
||||
(providerRuntimeEnvironmentVariable === null)
|
||||
|
|
@ -1315,7 +1336,7 @@ function commandLease(
|
|||
providerRuntimeEnvironmentVariable;
|
||||
}
|
||||
child = spawnChildProcess(
|
||||
runtimeExecutable,
|
||||
runtimeHandoff.executable,
|
||||
guarded
|
||||
? [
|
||||
// Keep resolved module URLs on the retained descriptor paths
|
||||
|
|
@ -1370,6 +1391,9 @@ function commandLease(
|
|||
"pipe",
|
||||
"pipe",
|
||||
...lifetime.credentialFenceFds,
|
||||
...(runtimeHandoff.sourceFd === null
|
||||
? []
|
||||
: [runtimeHandoff.sourceFd]),
|
||||
]
|
||||
: [
|
||||
"pipe",
|
||||
|
|
@ -1381,6 +1405,9 @@ function commandLease(
|
|||
...(providerRuntimeExecutable === null
|
||||
? []
|
||||
: [providerRuntimeExecutable.fd]),
|
||||
...(runtimeHandoff.sourceFd === null
|
||||
? []
|
||||
: [runtimeHandoff.sourceFd]),
|
||||
],
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import { spawnSync } from "node:child_process";
|
||||
import { closeSync, openSync } from "node:fs";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
VERIFIED_RUNTIME_EXECUTABLE_ENV,
|
||||
verifiedRuntimeExecutable,
|
||||
verifiedRuntimeExecutableHandoff,
|
||||
} from "./verified-runtime-executable.js";
|
||||
|
||||
describe("verified runtime executable", () => {
|
||||
it("projects an inherited Linux descriptor through the live process image", () => {
|
||||
it("preserves an inherited Linux descriptor for an explicit child handoff", () => {
|
||||
expect(
|
||||
verifiedRuntimeExecutable(
|
||||
{ [VERIFIED_RUNTIME_EXECUTABLE_ENV]: "/proc/self/fd/17" },
|
||||
|
|
@ -14,18 +18,18 @@ describe("verified runtime executable", () => {
|
|||
4321,
|
||||
"/usr/bin/node",
|
||||
),
|
||||
).toBe("/proc/self/exe");
|
||||
).toBe("/proc/self/fd/17");
|
||||
});
|
||||
|
||||
it("preserves the live process image for verified descendants", () => {
|
||||
expect(
|
||||
it("rejects a deleted live-process alias as a verified descendant runtime", () => {
|
||||
expect(() =>
|
||||
verifiedRuntimeExecutable(
|
||||
{ [VERIFIED_RUNTIME_EXECUTABLE_ENV]: "/proc/self/exe" },
|
||||
"linux",
|
||||
8765,
|
||||
"/usr/bin/node",
|
||||
),
|
||||
).toBe("/proc/self/exe");
|
||||
).toThrow("descriptor is invalid");
|
||||
});
|
||||
|
||||
it("rejects ancestor descriptor paths at the verified boundary", () => {
|
||||
|
|
@ -56,6 +60,97 @@ describe("verified runtime executable", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("remaps the authenticated Linux descriptor into the child stdio table", () => {
|
||||
expect(
|
||||
verifiedRuntimeExecutableHandoff(
|
||||
29,
|
||||
{ [VERIFIED_RUNTIME_EXECUTABLE_ENV]: "/proc/self/fd/17" },
|
||||
"linux",
|
||||
4321,
|
||||
"/usr/bin/node",
|
||||
),
|
||||
).toEqual({
|
||||
executable: "/proc/self/fd/29",
|
||||
environmentValue: "/proc/self/fd/29",
|
||||
sourceFd: 17,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not invent a descriptor handoff for an ambient runtime", () => {
|
||||
expect(
|
||||
verifiedRuntimeExecutableHandoff(29, {}, "linux", 4321, "/usr/bin/node"),
|
||||
).toEqual({
|
||||
executable: "/usr/bin/node",
|
||||
environmentValue: undefined,
|
||||
sourceFd: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects standard and invalid child descriptor targets", () => {
|
||||
for (const targetFd of [-1, 0, 2, 3.5, Number.MAX_SAFE_INTEGER + 1]) {
|
||||
expect(() =>
|
||||
verifiedRuntimeExecutableHandoff(
|
||||
targetFd,
|
||||
{ [VERIFIED_RUNTIME_EXECUTABLE_ENV]: "/proc/self/fd/17" },
|
||||
"linux",
|
||||
4321,
|
||||
"/usr/bin/node",
|
||||
),
|
||||
).toThrow("target descriptor is invalid");
|
||||
}
|
||||
});
|
||||
|
||||
it.runIf(process.platform === "linux")(
|
||||
"keeps the authenticated runtime executable across two child generations",
|
||||
() => {
|
||||
const sourceFd = openSync(process.execPath, "r");
|
||||
try {
|
||||
const targetFd = 10;
|
||||
const handoff = verifiedRuntimeExecutableHandoff(
|
||||
targetFd,
|
||||
{
|
||||
[VERIFIED_RUNTIME_EXECUTABLE_ENV]: `/proc/self/fd/${sourceFd}`,
|
||||
},
|
||||
"linux",
|
||||
);
|
||||
const stdio: Array<"ignore" | "pipe" | number> = [
|
||||
"ignore",
|
||||
"pipe",
|
||||
"pipe",
|
||||
];
|
||||
while (stdio.length < targetFd) stdio.push("ignore");
|
||||
stdio.push(handoff.sourceFd!);
|
||||
const child = spawnSync(
|
||||
handoff.executable,
|
||||
[
|
||||
"--eval",
|
||||
`const { spawnSync } = require("node:child_process");
|
||||
const fd = ${targetFd};
|
||||
const stdio = ["ignore", "pipe", "pipe"];
|
||||
while (stdio.length < fd) stdio.push("ignore");
|
||||
stdio.push(fd);
|
||||
const nested = spawnSync("/proc/self/fd/" + fd, ["--eval", "process.stdout.write('nested-ok')"], { stdio });
|
||||
if (nested.status !== 0) throw nested.error || new Error(nested.stderr.toString());
|
||||
process.stdout.write(nested.stdout);`,
|
||||
],
|
||||
{
|
||||
env: {
|
||||
[VERIFIED_RUNTIME_EXECUTABLE_ENV]: handoff.environmentValue!,
|
||||
},
|
||||
stdio,
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
expect(child.error).toBeUndefined();
|
||||
expect(child.status).toBe(0);
|
||||
expect(child.stderr).toBe("");
|
||||
expect(child.stdout).toBe("nested-ok");
|
||||
} finally {
|
||||
closeSync(sourceFd);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("accepts only the authenticated live process image on macOS", () => {
|
||||
expect(
|
||||
verifiedRuntimeExecutable(
|
||||
|
|
|
|||
|
|
@ -3,13 +3,17 @@ import { isAbsolute, resolve } from "node:path";
|
|||
export const VERIFIED_RUNTIME_EXECUTABLE_ENV =
|
||||
"PAPERCLIP_VERIFIED_RUNTIME_EXECUTABLE";
|
||||
|
||||
export interface VerifiedRuntimeExecutableHandoff {
|
||||
executable: string;
|
||||
environmentValue: string | undefined;
|
||||
sourceFd: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover the runner-authenticated executable inherited by a descriptor-loaded
|
||||
* sidecar. Linux children cannot use process.execPath here: Node resolves the
|
||||
* sealed image to a deleted memfd alias. Once the inherited descriptor has
|
||||
* authenticated the current image, `/proc/self/exe` keeps that exact live
|
||||
* image available to every fork/exec generation without granting a descendant
|
||||
* access to an ancestor's descriptor table.
|
||||
* sidecar. Linux descendants must explicitly inherit this descriptor: Node
|
||||
* resolves process.execPath and /proc/self/exe to a deleted memfd alias that a
|
||||
* later exec cannot reopen.
|
||||
*/
|
||||
export function verifiedRuntimeExecutable(
|
||||
environment: NodeJS.ProcessEnv = process.env,
|
||||
|
|
@ -21,8 +25,7 @@ export function verifiedRuntimeExecutable(
|
|||
if (configured === undefined) return fallback;
|
||||
|
||||
if (platform === "linux") {
|
||||
if (/^\/proc\/self\/fd\/[0-9]+$/.test(configured)) return "/proc/self/exe";
|
||||
if (configured === "/proc/self/exe") return configured;
|
||||
if (/^\/proc\/self\/fd\/[0-9]+$/.test(configured)) return configured;
|
||||
throw new Error("Verified runtime executable descriptor is invalid");
|
||||
}
|
||||
|
||||
|
|
@ -45,3 +48,47 @@ export function verifiedRuntimeExecutable(
|
|||
"Verified runtime executable is unsupported on this platform",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Project the current verified runtime into a chosen child descriptor. The
|
||||
* caller must place sourceFd at child targetFd in its stdio table.
|
||||
*/
|
||||
export function verifiedRuntimeExecutableHandoff(
|
||||
targetFd: number,
|
||||
environment: NodeJS.ProcessEnv = process.env,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
currentPid: number = process.pid,
|
||||
fallback: string = process.execPath,
|
||||
): VerifiedRuntimeExecutableHandoff {
|
||||
if (!Number.isSafeInteger(targetFd) || targetFd < 3) {
|
||||
throw new Error("Verified runtime executable target descriptor is invalid");
|
||||
}
|
||||
const executable = verifiedRuntimeExecutable(
|
||||
environment,
|
||||
platform,
|
||||
currentPid,
|
||||
fallback,
|
||||
);
|
||||
const configured = environment[VERIFIED_RUNTIME_EXECUTABLE_ENV];
|
||||
if (platform !== "linux" || configured === undefined) {
|
||||
return {
|
||||
executable,
|
||||
environmentValue: configured === undefined ? undefined : executable,
|
||||
sourceFd: null,
|
||||
};
|
||||
}
|
||||
const match = /^\/proc\/self\/fd\/([0-9]+)$/.exec(configured);
|
||||
if (match === null) {
|
||||
throw new Error("Verified runtime executable descriptor is invalid");
|
||||
}
|
||||
const sourceFd = Number.parseInt(match[1]!, 10);
|
||||
if (!Number.isSafeInteger(sourceFd) || sourceFd < 3) {
|
||||
throw new Error("Verified runtime executable descriptor is invalid");
|
||||
}
|
||||
const childExecutable = `/proc/self/fd/${targetFd}`;
|
||||
return {
|
||||
executable: childExecutable,
|
||||
environmentValue: childExecutable,
|
||||
sourceFd,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue