fix(runner): preserve verified runtime for provider descendants

This commit is contained in:
Dotta 2026-09-02 19:12:41 -05:00
parent 325e1e813d
commit b8e26f101e
6 changed files with 124 additions and 8 deletions

View File

@ -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<Self, LocalRunnerError> {
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);

View File

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

View File

@ -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<void>;
}
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<void> {
let child: ChildProcess;
try {
child = spawn(
process.execPath,
verifiedRuntimeExecutable(),
[
"--input-type=module",
"--eval",

View File

@ -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
? [

View File

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

View File

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