fix(runner): preserve verified runtime for provider descendants
This commit is contained in:
parent
325e1e813d
commit
b8e26f101e
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
? [
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue