test(sandbox): pin runner argv forms
This commit is contained in:
@@ -124,6 +124,8 @@ export class SandboxBashExecutor extends LocalBashExecutor {
|
|||||||
try {
|
try {
|
||||||
proc = this.startArgv(spec, confined.argv)
|
proc = this.startArgv(spec, confined.argv)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// LocalSubprocessService reports provenanced ENOENT/EACCES through async
|
||||||
|
// `done` rejection; this covers alternatives that throw that shape synchronously.
|
||||||
if (isRunnerSpawnFailure(error, confined.argv[0], spec.workdir)) {
|
if (isRunnerSpawnFailure(error, confined.argv[0], spec.workdir)) {
|
||||||
throw new SandboxUnavailableError(mode, String(error))
|
throw new SandboxUnavailableError(mode, String(error))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,29 +108,38 @@ describe('partial Landlock runner-failure classification', () => {
|
|||||||
expect(accounting.size).toBe(0)
|
expect(accounting.size).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('classifies a bare-name runner whose shebang interpreter is missing', async () => {
|
it.each(['bare-name', 'relative'] as const)(
|
||||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-bare-sandbox-runner-'))
|
'classifies a %s runner whose shebang interpreter is missing',
|
||||||
tempDirs.push(dir)
|
async (form) => {
|
||||||
const runner = 'bare-missing-interpreter-runner'
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-argv-form-sandbox-runner-'))
|
||||||
await writeFile(join(dir, runner), '#!/dsh-definitely-missing-sandbox-interpreter\nexit 0\n', { mode: 0o755 })
|
tempDirs.push(dir)
|
||||||
const bash = await setupConfiguredRunner(runner)
|
const filename = 'missing-interpreter-runner'
|
||||||
const request = { command: 'true', env: { PATH: dir } }
|
const runner = form === 'bare-name' ? filename : `./${filename}`
|
||||||
|
await writeFile(join(dir, filename), '#!/dsh-definitely-missing-sandbox-interpreter\nexit 0\n', { mode: 0o755 })
|
||||||
|
const bash = await setupConfiguredRunner(runner)
|
||||||
|
const request = form === 'bare-name'
|
||||||
|
? { command: 'true', env: { PATH: dir } }
|
||||||
|
: { command: 'true', workdir: dir }
|
||||||
|
|
||||||
const error = await bash.run(bash.resolve(request)).catch((value: unknown) => value)
|
const error = await bash.run(bash.resolve(request)).catch((value: unknown) => value)
|
||||||
expect(error).toMatchObject({ name: 'SandboxUnavailableError', code: SANDBOX_UNAVAILABLE })
|
expect(error).toMatchObject({ name: 'SandboxUnavailableError', code: SANDBOX_UNAVAILABLE })
|
||||||
expect(error).toBeInstanceOf(Error)
|
expect(error).toBeInstanceOf(Error)
|
||||||
expect((error as Error).message).toContain(`spawn ${runner} ENOENT`)
|
// Empirically, Darwin and Linux Node 24 preserve the passed bare/relative
|
||||||
|
// argv[0] in this spawn error rather than resolving it to an absolute path.
|
||||||
|
expect((error as Error).message).toContain(`spawn ${runner} ENOENT`)
|
||||||
|
|
||||||
const task = bash.start(bash.resolve(request))
|
const task = bash.start(bash.resolve(request))
|
||||||
await task.done
|
await task.done
|
||||||
expect(task.readOutput().delta).toContain(`spawn failed: Error: spawn ${runner} ENOENT`)
|
expect(task.status).toBe('killed')
|
||||||
expect(task.sandbox).toEqual({
|
expect(task.readOutput().delta).toContain(`spawn failed: Error: spawn ${runner} ENOENT`)
|
||||||
mode: 'read-only',
|
expect(task.sandbox).toEqual({
|
||||||
denied: false,
|
mode: 'read-only',
|
||||||
enforcement: 'full',
|
denied: false,
|
||||||
runnerFailed: true,
|
enforcement: 'full',
|
||||||
})
|
runnerFailed: true,
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
it('keeps a real malformed executable ordinary across no-shebang spawn behavior', async () => {
|
it('keeps a real malformed executable ordinary across no-shebang spawn behavior', async () => {
|
||||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-malformed-sandbox-runner-'))
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-malformed-sandbox-runner-'))
|
||||||
|
|||||||
@@ -248,6 +248,8 @@ describe('fail closed', () => {
|
|||||||
denialSignatures: UNIX_SIGNATURES,
|
denialSignatures: UNIX_SIGNATURES,
|
||||||
runnerFailureRules: RUNNER_FAILURE,
|
runnerFailureRules: RUNNER_FAILURE,
|
||||||
}))
|
}))
|
||||||
|
// This pins an alternative SubprocessService's synchronous seam, not the
|
||||||
|
// shipped local behavior.
|
||||||
vi.spyOn(ctx.subprocess, 'spawn').mockImplementation(() => {
|
vi.spyOn(ctx.subprocess, 'spawn').mockImplementation(() => {
|
||||||
throw Object.assign(new Error('spawn EACCES'), { code: 'EACCES', syscall: 'spawn', path: runner })
|
throw Object.assign(new Error('spawn EACCES'), { code: 'EACCES', syscall: 'spawn', path: runner })
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user