From b8e26f101eefc623b9860330a6906be26a1159ea Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 2 Sep 2026 19:12:41 -0500 Subject: [PATCH] fix(runner): preserve verified runtime for provider descendants --- .../runner-core/src/process_supervisor.rs | 11 +++++ .../runner-core/tests/process_supervisor.rs | 10 +++- .../src/drivers/acpx/codex-credentials.ts | 7 +-- .../drivers/acpx/installation-integrity.ts | 16 +++++-- .../acpx/verified-runtime-executable.test.ts | 47 +++++++++++++++++++ .../acpx/verified-runtime-executable.ts | 41 ++++++++++++++++ 6 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 packages/paperclip-runner/src/drivers/acpx/verified-runtime-executable.test.ts create mode 100644 packages/paperclip-runner/src/drivers/acpx/verified-runtime-executable.ts diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs b/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs index 4bf21c4d46..d2ad17c50b 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs @@ -26,6 +26,7 @@ use sha2::{Digest, Sha256}; use crate::local_runner::LocalRunnerError; const PROCESS_OUTPUT_QUEUE_CAPACITY: usize = 256; +const VERIFIED_RUNTIME_EXECUTABLE_ENV: &str = "PAPERCLIP_VERIFIED_RUNTIME_EXECUTABLE"; const VERIFIED_COMMONJS_ARTIFACT_LOADER: &str = r#"const fs=require("node:fs");const Module=require("node:module");const filename=process.argv[1];const source=fs.readFileSync(filename,"utf8").replace(/^#![^\r\n]*(?:\r?\n|$)/,"");const artifact=new Module(filename);artifact.filename=filename;artifact.paths=[];artifact._compile(source,filename);"#; #[derive(Clone, Debug)] @@ -604,6 +605,7 @@ impl SupervisedProcess { shutdown_grace, max_line_bytes, additional_environment_keys, + None, ) } @@ -622,6 +624,7 @@ impl SupervisedProcess { shutdown_grace, max_line_bytes, additional_environment_keys, + Some(&inherited.program), ); #[cfg(target_os = "macos")] if let Ok(process) = result.as_mut() { @@ -651,6 +654,7 @@ impl SupervisedProcess { shutdown_grace: Duration, max_line_bytes: usize, additional_environment_keys: &[&str], + verified_runtime_executable: Option<&Path>, ) -> Result { let mut command = Command::new(program); command @@ -680,6 +684,13 @@ impl SupervisedProcess { command.env(key, value); } } + if let Some(executable) = verified_runtime_executable { + // Node reports a sealed memfd launch as `/memfd:... (deleted)` via + // process.execPath. Give descriptor-loaded runtimes the inherited, + // authenticated executable path so their governed child launches + // can reopen the same immutable image instead of that dead alias. + command.env(VERIFIED_RUNTIME_EXECUTABLE_ENV, executable); + } #[cfg(unix)] command.process_group(0); diff --git a/packages/paperclip-runner/runner/crates/runner-core/tests/process_supervisor.rs b/packages/paperclip-runner/runner/crates/runner-core/tests/process_supervisor.rs index d3671ecccf..cbaffd4ae8 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/tests/process_supervisor.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/tests/process_supervisor.rs @@ -48,7 +48,7 @@ fn verified_launch_uses_open_command_and_script_after_atomic_path_replacement() fs::create_dir(&directory).unwrap(); let command = directory.join("command"); let script = directory.join("script"); - let original_command = "#!/bin/sh\nprintf '%s\\n' old-command\nexec /bin/sh \"$1\"\n"; + let original_command = "#!/bin/sh\nprintf '%s\\n' old-command\nprintf '%s\\n' \"$PAPERCLIP_VERIFIED_RUNTIME_EXECUTABLE\"\nexec /bin/sh \"$1\"\n"; let original_script = "#!/bin/sh\nprintf '%s\\n' old-script\n"; write_executable(&command, original_command); write_executable(&script, original_script); @@ -97,6 +97,14 @@ fn verified_launch_uses_open_command_and_script_after_atomic_path_replacement() .as_deref(), Some("old-command") ); + let inherited_runtime = process + .receive_stdout_line(Duration::from_secs(1)) + .unwrap() + .expect("verified launch should identify its inherited runtime"); + #[cfg(target_os = "linux")] + assert!(inherited_runtime.starts_with("/proc/self/fd/")); + #[cfg(target_os = "macos")] + assert!(inherited_runtime.contains(".paperclip-verified-launch-")); assert_eq!( process .receive_stdout_line(Duration::from_secs(1)) diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-credentials.ts b/packages/paperclip-runner/src/drivers/acpx/codex-credentials.ts index 452ecd7352..012b1e88e9 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-credentials.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-credentials.ts @@ -12,6 +12,8 @@ import { import { createServer, type Server } from "node:net"; import { isAbsolute, join, resolve } from "node:path"; +import { verifiedRuntimeExecutable } from "./verified-runtime-executable.js"; + const MAX_CODEX_CREDENTIAL_BYTES = 256 * 1024; const PRIVATE_FILE_MODE = 0o600; const MAX_DIRECTORY_SYNC_ATTEMPTS = 8; @@ -119,8 +121,7 @@ export interface AcpxProviderLifetimeLease { close(): Promise; } -export interface ManagedCodexCredentialLease - extends AcpxProviderLifetimeLease { +export interface ManagedCodexCredentialLease extends AcpxProviderLifetimeLease { readonly path: string; readonly mode: ManagedCodexCredentialMode; } @@ -1129,7 +1130,7 @@ async function runDirectorySyncHelper(directory: string): Promise { let child: ChildProcess; try { child = spawn( - process.execPath, + verifiedRuntimeExecutable(), [ "--input-type=module", "--eval", diff --git a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts index 9d4d255c7f..7c07a96c20 100644 --- a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts +++ b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts @@ -25,6 +25,10 @@ import { import type { Readable, Writable } from "node:stream"; import type { QualifiedAcpxProfile } from "./qualified-profiles.js"; +import { + VERIFIED_RUNTIME_EXECUTABLE_ENV, + verifiedRuntimeExecutable, +} from "./verified-runtime-executable.js"; const MAX_PACKAGE_JSON_BYTES = 256 * 1024; const MAX_AGENT_COMMAND_BYTES = 16 * 1024 * 1024; @@ -68,6 +72,7 @@ export const PROVIDER_LIFETIME_GUARDIAN_SOURCE = ` const fs = require("node:fs"); const { spawn } = require("node:child_process"); const WATCHDOG_SOURCE = ${JSON.stringify(PROVIDER_LIFETIME_WATCHDOG_SOURCE)}; +const runtimeExecutable = process.env.${VERIFIED_RUNTIME_EXECUTABLE_ENV} || process.execPath; const dependencyAncestorCount = Number.parseInt(process.argv[4], 10); if (!Number.isSafeInteger(dependencyAncestorCount) || dependencyAncestorCount < 0 || dependencyAncestorCount > ${MAX_DEPENDENCY_ANCESTORS}) throw new Error("ACPX provider dependency ancestry is invalid"); const OWNER_FD = ${DEPENDENCY_ANCESTOR_FD_START} + dependencyAncestorCount; @@ -112,7 +117,7 @@ const startProvider = () => { if (provider || reaped || shutdownStarted) return; try { provider = spawn( - process.execPath, + runtimeExecutable, ["--eval", process.argv[1], ...process.argv.slice(2)], { cwd: process.cwd(), @@ -152,7 +157,7 @@ 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. - watchdog = spawn(process.execPath, ["--eval", WATCHDOG_SOURCE], { + watchdog = spawn(runtimeExecutable, ["--eval", WATCHDOG_SOURCE], { cwd: process.cwd(), detached: false, env: {}, @@ -843,8 +848,11 @@ function commandLease( ) { throw new Error("ACPX provider credential fence is invalid"); } + const runtimeExecutable = verifiedRuntimeExecutable(); + const environment = sanitizedNodeEnvironment(options.env); + environment[VERIFIED_RUNTIME_EXECUTABLE_ENV] = runtimeExecutable; child = spawnChildProcess( - process.execPath, + runtimeExecutable, guarded ? [ // Keep resolved module URLs on the retained descriptor paths @@ -880,7 +888,7 @@ function commandLease( // both credential quorum listeners inherited, and pins the PGID // until its single whole-group reap. detached: process.platform !== "win32", - env: sanitizedNodeEnvironment(options.env), + env: environment, shell: false, stdio: guarded ? [ 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 new file mode 100644 index 0000000000..2ffa2e1c5d --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/verified-runtime-executable.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; + +import { + VERIFIED_RUNTIME_EXECUTABLE_ENV, + verifiedRuntimeExecutable, +} from "./verified-runtime-executable.js"; + +describe("verified runtime executable", () => { + it("anchors an inherited Linux descriptor at its current owner", () => { + expect( + verifiedRuntimeExecutable( + { [VERIFIED_RUNTIME_EXECUTABLE_ENV]: "/proc/self/fd/17" }, + "linux", + 4321, + "/usr/bin/node", + ), + ).toBe("/proc/4321/fd/17"); + }); + + it("preserves an already anchored descendant runtime", () => { + expect( + verifiedRuntimeExecutable( + { [VERIFIED_RUNTIME_EXECUTABLE_ENV]: "/proc/4321/fd/17" }, + "linux", + 8765, + "/usr/bin/node", + ), + ).toBe("/proc/4321/fd/17"); + }); + + it("rejects mutable Linux paths at the verified boundary", () => { + expect(() => + verifiedRuntimeExecutable( + { [VERIFIED_RUNTIME_EXECUTABLE_ENV]: "/usr/bin/node" }, + "linux", + 4321, + "/usr/bin/node", + ), + ).toThrow("descriptor is invalid"); + }); + + it("uses process identity only when no verified runtime was supplied", () => { + expect(verifiedRuntimeExecutable({}, "linux", 4321, "/usr/bin/node")).toBe( + "/usr/bin/node", + ); + }); +}); diff --git a/packages/paperclip-runner/src/drivers/acpx/verified-runtime-executable.ts b/packages/paperclip-runner/src/drivers/acpx/verified-runtime-executable.ts new file mode 100644 index 0000000000..5da0d67e84 --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/verified-runtime-executable.ts @@ -0,0 +1,41 @@ +import { isAbsolute, resolve } from "node:path"; + +export const VERIFIED_RUNTIME_EXECUTABLE_ENV = + "PAPERCLIP_VERIFIED_RUNTIME_EXECUTABLE"; + +/** + * 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. Anchor the descriptor at the current + * owner process before launching a descendant, whose own `/proc/self` would + * otherwise name the wrong descriptor table. + */ +export function verifiedRuntimeExecutable( + environment: NodeJS.ProcessEnv = process.env, + platform: NodeJS.Platform = process.platform, + currentPid: number = process.pid, + fallback: string = process.execPath, +): string { + const configured = environment[VERIFIED_RUNTIME_EXECUTABLE_ENV]; + if (configured === undefined) return fallback; + + if (platform === "linux") { + const match = /^\/proc\/self\/fd\/([0-9]+)$/.exec(configured); + if (match) return `/proc/${currentPid}/fd/${match[1]}`; + if (/^\/proc\/[1-9][0-9]*\/fd\/[0-9]+$/.test(configured)) { + return configured; + } + throw new Error("Verified runtime executable descriptor is invalid"); + } + + if (platform === "darwin") { + if (!isAbsolute(configured) || resolve(configured) !== configured) { + throw new Error("Verified runtime executable path is invalid"); + } + return configured; + } + + throw new Error( + "Verified runtime executable is unsupported on this platform", + ); +}