fix(sandbox): spawn confined argv directly (round 1)

This commit is contained in:
Hypatia May
2026-08-04 12:04:48 +08:00
parent 2eedb54849
commit e36d040d0a
34 changed files with 392 additions and 282 deletions
@@ -12,6 +12,11 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import {
LAUNCHER_FAILURE_EXIT,
LAUNCHER_FATAL_PREFIX,
PARTIAL_ENFORCEMENT_NOTICE,
} from 'node-addon-landlock-run'
import { SANDBOX_UNAVAILABLE, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
import {
@@ -23,19 +28,6 @@ import { bwrapProfileArgs, landlockProfileArgs, seatbeltProfileArgs } from '../s
const RO: SandboxPolicy = { mode: 'read-only', workspaceRoot: '/ws' }
const WW: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: '/ws' }
/** Expected argv0-scoped rule for failures in bash's outer `exec`. */
function outerShellRule(argv0: string) {
return {
allowedExitCodes: [126, 127],
fatalSignatures: [
`exec: ${argv0}: not found`,
`${argv0}: No such file or directory`,
`${argv0}: Permission denied`,
`exec: ${argv0}: cannot execute`,
],
}
}
async function setup(config: Config = {}, internals: LocalSandboxProvider['internals'] = {}) {
const ctx = new Context()
await ctx.plugin(LocalSandboxProvider, config)
@@ -48,7 +40,7 @@ async function setup(config: Config = {}, internals: LocalSandboxProvider['inter
function fakeLauncher(report = 'landlock: fully enforced'): string {
const dir = mkdtempSync(join(tmpdir(), 'dsh-fake-landlock-'))
const launcher = join(dir, 'landlock-run')
writeFileSync(launcher, `#!/bin/sh\nif [ "$1" = "--probe" ]; then echo "${report}"; exit 0; fi\nexit 125\n`, { mode: 0o755 })
writeFileSync(launcher, `#!/bin/sh\nif [ "$1" = "--probe" ]; then echo "${report}"; exit 0; fi\nexit ${LAUNCHER_FAILURE_EXIT}\n`, { mode: 0o755 })
return launcher
}
@@ -124,14 +116,7 @@ describe('runnerCommand config', () => {
// An operator runner's kernel mechanism is unknown: both Linux
// file-denial dialects, never bare EPERM.
denialSignatures: ['read-only file system', 'permission denied'],
// The runner's own dialect is unknown, but the consumer re-joins the
// wrap through an outer `bash -c 'exec …'` — a missing or
// unexecutable runner fails with the OUTER shell's argv0-scoped
// shapes, and those classify as sandbox failures like any rung.
runnerFailureRules: [
outerShellRule('fake-runner'),
{ fatalSignatures: ['fake-runner: profile rejected'] },
],
runnerFailureRules: [{ fatalSignatures: ['fake-runner: profile rejected'] }],
})
expect(probeBwrap).not.toHaveBeenCalled()
expect(probeLandlock).not.toHaveBeenCalled()
@@ -157,11 +142,14 @@ describe('runnerCommand config', () => {
)
})
it('rejects blank configured-runner failure signatures', async () => {
await expect(setup({ runnerCommand: ['fake-runner'], runnerFailureSignatures: [' '] })).rejects.toThrow(
'runnerFailureSignatures entries must be non-empty',
)
})
it.each([' ', 'fatal\ncontinued', 'fatal\rcontinued'])(
'rejects an unusable configured-runner failure signature %j',
async (signature) => {
await expect(setup({ runnerCommand: ['fake-runner'], runnerFailureSignatures: [signature] })).rejects.toThrow(
'runnerFailureSignatures entries must be non-empty single-line strings',
)
},
)
})
describe('the platform chains', () => {
@@ -174,10 +162,7 @@ describe('the platform chains', () => {
argv: ['bwrap', ...bwrapProfileArgs(RO), '--', 'true'],
enforcement: 'full',
denialSignatures: ['read-only file system'],
runnerFailureRules: [
outerShellRule('bwrap'),
{ fatalSignatures: ['bwrap: '] },
],
runnerFailureRules: [{ fatalSignatures: ['bwrap: '] }],
})
expect(probeLandlock).not.toHaveBeenCalled()
})
@@ -192,14 +177,11 @@ describe('the platform chains', () => {
argv: [launcher, ...landlockProfileArgs(WW), '--', 'bash', '-c', 'echo hi'],
enforcement: 'full',
denialSignatures: ['permission denied'],
runnerFailureRules: [
outerShellRule(launcher),
{
allowedExitCodes: [125],
fatalSignatures: ['landlock-run: '],
informationalLines: ['landlock-run: partial enforcement (older Landlock ABI)'],
},
],
runnerFailureRules: [{
allowedExitCodes: [LAUNCHER_FAILURE_EXIT],
fatalSignatures: [LAUNCHER_FATAL_PREFIX],
informationalLines: [PARTIAL_ENFORCEMENT_NOTICE],
}],
})
expect(probeLandlock).toHaveBeenCalledWith(launcher)
})
@@ -215,10 +197,7 @@ describe('the platform chains', () => {
argv: ['sandbox-exec', ...seatbeltProfileArgs(RO), '--', 'bash', '-c', 'echo hi'],
enforcement: 'full',
denialSignatures: ['operation not permitted'],
runnerFailureRules: [
outerShellRule('sandbox-exec'),
{ fatalSignatures: ['sandbox-exec: '] },
],
runnerFailureRules: [{ fatalSignatures: ['sandbox-exec: '] }],
})
expect(probeSeatbelt).not.toHaveBeenCalled()
})
@@ -335,7 +314,7 @@ describe('the default landlock probe (launcher CLI contract)', () => {
it('reads a failing launcher as unusable: the chain ends and fails closed', async () => {
const dir = mkdtempSync(join(tmpdir(), 'dsh-fake-landlock-'))
const launcher = join(dir, 'landlock-run')
writeFileSync(launcher, '#!/bin/sh\nexit 125\n', { mode: 0o755 })
writeFileSync(launcher, `#!/bin/sh\nexit ${LAUNCHER_FAILURE_EXIT}\n`, { mode: 0o755 })
const { sandbox } = await setup({}, { platform: 'linux', probeBwrap: () => false, landlockLauncher: launcher })
expect(() => sandbox.confine(['true'], RO)).toThrow(expect.objectContaining({ code: SANDBOX_UNAVAILABLE }))
})
@@ -384,10 +363,7 @@ describe('the default seatbelt probe (sandbox-exec contract)', () => {
argv: [exec, ...seatbeltProfileArgs(RO), '--', 'true'],
enforcement: 'full',
denialSignatures: ['operation not permitted'],
runnerFailureRules: [
outerShellRule(exec),
{ fatalSignatures: ['sandbox-exec: '] },
],
runnerFailureRules: [{ fatalSignatures: ['sandbox-exec: '] }],
})
})