From cbf51b9da805d9723bd996741fe9f7d18abf682e Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 3 Sep 2026 11:59:27 -0500 Subject: [PATCH] fix(runner): inherit verified node descriptor across sidecars --- .../src/drivers/acpx/codex-credentials.ts | 10 +- .../drivers/acpx/installation-integrity.ts | 39 ++++++- .../acpx/verified-runtime-executable.test.ts | 105 +++++++++++++++++- .../acpx/verified-runtime-executable.ts | 61 ++++++++-- 4 files changed, 194 insertions(+), 21 deletions(-) diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-credentials.ts b/packages/paperclip-runner/src/drivers/acpx/codex-credentials.ts index 8205608940..a7daf19b5a 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-credentials.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-credentials.ts @@ -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 { } 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 { // 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, }, ); diff --git a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts index 27b4420302..becb0b496c 100644 --- a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts +++ b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts @@ -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]), ], }, ); diff --git a/packages/paperclip-runner/src/drivers/acpx/verified-runtime-executable.test.ts b/packages/paperclip-runner/src/drivers/acpx/verified-runtime-executable.test.ts index 13d351e933..2c8eea22ea 100644 --- a/packages/paperclip-runner/src/drivers/acpx/verified-runtime-executable.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/verified-runtime-executable.test.ts @@ -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( diff --git a/packages/paperclip-runner/src/drivers/acpx/verified-runtime-executable.ts b/packages/paperclip-runner/src/drivers/acpx/verified-runtime-executable.ts index e51fe7625d..aff593281e 100644 --- a/packages/paperclip-runner/src/drivers/acpx/verified-runtime-executable.ts +++ b/packages/paperclip-runner/src/drivers/acpx/verified-runtime-executable.ts @@ -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, + }; +}