fix(runner): inherit verified node descriptor across sidecars

This commit is contained in:
Dotta 2026-09-03 11:59:27 -05:00
parent d7faaafa6d
commit cbf51b9da8
4 changed files with 194 additions and 21 deletions

View File

@ -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,
},
);

View File

@ -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]),
],
},
);

View File

@ -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(

View File

@ -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,
};
}