From fe818862e1649ab45fbf0a8de9a879b1db21f9ac Mon Sep 17 00:00:00 2001 From: Hypatia May Date: Tue, 4 Aug 2026 16:20:04 +0800 Subject: [PATCH] fix(sandbox): round 6 bind spawn provenance --- packages/bash/bash-sandbox/src/helpers.ts | 17 ++++++++++------- .../bash/bash-sandbox/tests/sandbox.spec.ts | 18 ++++++++++++++++-- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/bash/bash-sandbox/src/helpers.ts b/packages/bash/bash-sandbox/src/helpers.ts index 1c3ea82eb2..d4949a93be 100644 --- a/packages/bash/bash-sandbox/src/helpers.ts +++ b/packages/bash/bash-sandbox/src/helpers.ts @@ -23,10 +23,11 @@ function isUsableWorkdir(path: string): boolean { } /** - * Attribute only executable-class failures with Node spawn provenance after - * independently ruling out the caller-owned cwd. With a usable cwd, these - * codes describe resolution, permissions, or loading of the provider's - * argv[0], including a script whose shebang interpreter is unavailable. + * Attribute only executable-class failures with positive Node argv[0] + * provenance after independently ruling out the caller-owned cwd. A supplied + * error path must exactly identify the runner; without one, the syscall must. + * With a usable cwd, these codes describe resolution, permissions, or loading + * of that argv[0], including a script whose shebang interpreter is unavailable. * @param error - the original spawn rejection. * @param runnerProgram - provider argv[0], the executable that establishes confinement. * @param workdir - the caller-owned spawn cwd, checked independently for usability. @@ -41,9 +42,11 @@ export function isRunnerSpawnFailure( if (typeof error !== 'object' || error === null) return false const { code, path, syscall } = error as { code?: unknown; path?: unknown; syscall?: unknown } if (typeof code !== 'string' || !EXECUTABLE_SPAWN_CODES.has(code)) return false - if (typeof syscall !== 'string' || (syscall !== 'spawn' && !syscall.startsWith('spawn '))) return false - if (path !== undefined && (typeof path !== 'string' || (path.length > 0 && path !== runnerProgram))) return false - return true + if (typeof syscall !== 'string') return false + const exactSyscall = `spawn ${runnerProgram}` + if (path === undefined) return syscall === exactSyscall + if (typeof path !== 'string' || path.length === 0 || path !== runnerProgram) return false + return syscall === 'spawn' || syscall === exactSyscall } /** Fatal runner evidence retained for infrastructure-error detail. */ diff --git a/packages/bash/bash-sandbox/tests/sandbox.spec.ts b/packages/bash/bash-sandbox/tests/sandbox.spec.ts index fa3c70caa9..cafd2f53e2 100644 --- a/packages/bash/bash-sandbox/tests/sandbox.spec.ts +++ b/packages/bash/bash-sandbox/tests/sandbox.spec.ts @@ -384,6 +384,8 @@ describe('isRunnerSpawnFailure', () => { const missingRunner = join(spillDir, 'definitely-missing-runner') const spawnError = (code: unknown, syscall: unknown = `spawn ${missingRunner}`, path: unknown = missingRunner) => Object.assign(new Error('spawn failed'), { code, syscall, path }) + const spawnErrorWithoutPath = (syscall: string) => + Object.assign(new Error('spawn failed'), { code: 'ENOENT', syscall }) expect(isRunnerSpawnFailure(spawnError('EMFILE'), missingRunner, process.cwd())).toBe(false) expect(isRunnerSpawnFailure(spawnError('ENOMEM'), missingRunner, process.cwd())).toBe(false) @@ -392,12 +394,24 @@ describe('isRunnerSpawnFailure', () => { expect(isRunnerSpawnFailure(spawnError('ENOENT', 1), missingRunner, process.cwd())).toBe(false) expect(isRunnerSpawnFailure(spawnError('ENOENT', 'spawn', process.execPath), missingRunner, process.cwd())).toBe(false) expect(isRunnerSpawnFailure(spawnError('ENOENT', 'spawn', 1), missingRunner, process.cwd())).toBe(false) - expect(isRunnerSpawnFailure(spawnError('ENOENT', 'spawn', ''), missingRunner, process.cwd())).toBe(true) - expect(isRunnerSpawnFailure(spawnError('ENOENT', 'spawn', undefined), missingRunner, process.cwd())).toBe(true) + expect(isRunnerSpawnFailure(spawnError('ENOENT', 'spawn', ''), missingRunner, process.cwd())).toBe(false) + expect(isRunnerSpawnFailure(spawnErrorWithoutPath('spawn'), missingRunner, process.cwd())).toBe(false) + expect(isRunnerSpawnFailure(spawnErrorWithoutPath('spawn other-runner'), missingRunner, process.cwd())).toBe(false) expect(isRunnerSpawnFailure(undefined, missingRunner, process.cwd())).toBe(false) expect(isRunnerSpawnFailure(null, missingRunner, process.cwd())).toBe(false) expect(isRunnerSpawnFailure(spawnError('ENOENT'), undefined, process.cwd())).toBe(false) }) + + it('accepts only syscall provenance compatible with the exact runner program', () => { + const runner = join(spillDir, 'runner with spaces') + const spawnError = (syscall: string, path?: string) => + Object.assign(new Error('spawn failed'), { code: 'ENOEXEC', syscall, path }) + + expect(isRunnerSpawnFailure(spawnError('spawn', runner), runner, process.cwd())).toBe(true) + expect(isRunnerSpawnFailure(spawnError(`spawn ${runner}`, runner), runner, process.cwd())).toBe(true) + expect(isRunnerSpawnFailure(spawnError(`spawn ${runner}`), runner, process.cwd())).toBe(true) + expect(isRunnerSpawnFailure(spawnError('spawn other-runner', runner), runner, process.cwd())).toBe(false) + }) }) describe('classifyRunnerFailure', () => {