mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
fix(sandbox): spawn confined argv directly (round 1)
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write native/landlock-run/README.md
|
||||
README.md: 284d5df764cf5a5205973696211aee2366d3b76e
|
||||
README.zh.md: f369799cc8dcfb6de7c4b7b8c18857693d310418
|
||||
README.md: 483b14df7cb91fc6486f69efc1ddc560ab635ed5
|
||||
README.zh.md: a239889ec68a8d56bb901b17feaac7f7acbf66fa
|
||||
|
||||
@@ -39,7 +39,7 @@ The public API is intentionally small:
|
||||
- `launcherPath()`: absolute path of this host's launcher (existence deliberately unchecked — the probe is the availability signal).
|
||||
- `probe(launcher?, { timeoutMs? })`: functional enforcement probe — `'full' | 'partial' | 'unusable'`.
|
||||
- `grantArgs({ readOnly?, readWrite? })`: the launcher's grant argv; everything not granted is denied.
|
||||
- `LAUNCHER_BIN`, `LAUNCHER_FAILURE_EXIT` (125): contract constants.
|
||||
- `LAUNCHER_BIN`, `LAUNCHER_FAILURE_EXIT` (125), `LAUNCHER_FATAL_PREFIX`, and `PARTIAL_ENFORCEMENT_NOTICE`: contract constants. Every launcher failure uses 125, but a successfully exec'd child may also return 125, so failure attribution requires the fatal prefix too.
|
||||
|
||||
The full binary contract (argv grammar, exit codes, report lines) is pinned in [docs/cli-contract.md](docs/cli-contract.md).
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ if (probe(launcher) !== 'unusable') {
|
||||
- `launcherPath()`:当前宿主启动器的绝对路径(有意不检查是否存在;探测结果才是可用性信号)。
|
||||
- `probe(launcher?, { timeoutMs? })`:功能性强制执行探测,返回 `'full' | 'partial' | 'unusable'`。
|
||||
- `grantArgs({ readOnly?, readWrite? })`:启动器的授权 argv;未授予的一切都被拒绝。
|
||||
- `LAUNCHER_BIN`、`LAUNCHER_FAILURE_EXIT`(125):契约常量。
|
||||
- `LAUNCHER_BIN`、`LAUNCHER_FAILURE_EXIT`(125)、`LAUNCHER_FATAL_PREFIX` 和 `PARTIAL_ENFORCEMENT_NOTICE`:契约常量。所有 launcher 失败的退出码都是 125,但成功完成 exec 的子进程也可能返回 125,因此还必须有致命前缀,才能将结果归因为 launcher 失败。
|
||||
|
||||
完整的二进制契约(argv 语法、退出码、报告行)锁定在 [docs/cli-contract.md](docs/cli-contract.md) 中。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CLI contract: landlock-run
|
||||
|
||||
This file pins the launcher's externally observable behavior — the cross-repo compatibility surface between the binaries and every consumer. Consumers interact with it only through the entry package (`launcherPath`/`probe`/`grantArgs`); changing anything below requires a version bump for the whole package family and a note in the release notes.
|
||||
This file pins the launcher's externally observable behavior — the cross-repo compatibility surface between the binaries and every consumer. Consumers interact with it only through the entry package (`launcherPath`/`probe`/`grantArgs` and its protocol constants); changing anything below requires a version bump for the whole package family and a note in the release notes.
|
||||
|
||||
## Invocation grammar
|
||||
|
||||
@@ -19,15 +19,15 @@ landlock-run --probe
|
||||
|
||||
## Exit codes
|
||||
|
||||
- `125` (`LAUNCHER_FAILURE_EXIT`): every launcher-level failure — usage error, kernel that cannot enforce Landlock, unopenable grant root, failed `exec`. The wrapped command was NOT run (fail-closed; the one exception is `exec` itself failing after restriction, which by definition never ran the command either).
|
||||
- Any other status: the wrapped command's own exit status, passed through unchanged.
|
||||
- `125` (`LAUNCHER_FAILURE_EXIT`): every launcher-level failure — usage error, kernel that cannot enforce Landlock, unopenable grant root, failed `exec`. The wrapped command was NOT run.
|
||||
- After a successful `exec`, every child status is passed through unchanged, including 125. Consumers therefore require both status 125 and a `LAUNCHER_FATAL_PREFIX` line to attribute launcher failure.
|
||||
- `--probe`: `0` when the kernel enforces (fully or partially), `125` otherwise.
|
||||
|
||||
## Report lines
|
||||
|
||||
- Probe success prints exactly one stdout line: `landlock: fully enforced` or `landlock: partially enforced (older ABI)`. The entry package's `probe()` maps these to `full`/`partial`; a non-zero probe exit maps to `unusable`.
|
||||
- A confined run under a partial-ABI kernel prints one stderr line `landlock-run: partial enforcement (older Landlock ABI)` and proceeds — still confined for everything the kernel supports.
|
||||
- Every fatal error prints one stderr line prefixed `landlock-run: ` before exiting `125`.
|
||||
- A confined run under a partial-ABI kernel prints one stderr line `landlock-run: partial enforcement (older Landlock ABI)` (`PARTIAL_ENFORCEMENT_NOTICE`) and proceeds — still confined for everything the kernel supports.
|
||||
- Every fatal error prints one stderr line prefixed `landlock-run: ` (`LAUNCHER_FATAL_PREFIX`) before exiting `125`.
|
||||
|
||||
## Confinement semantics
|
||||
|
||||
|
||||
@@ -21,11 +21,18 @@ import { fileURLToPath } from 'node:url'
|
||||
/** The launcher binary's file name inside each platform package's `bin/`. */
|
||||
export const LAUNCHER_BIN = 'landlock-run'
|
||||
|
||||
/** Prefix on every launcher-owned fatal stderr line. */
|
||||
export const LAUNCHER_FATAL_PREFIX = 'landlock-run: '
|
||||
|
||||
/** Informational stderr line emitted before child execution under partial enforcement. */
|
||||
export const PARTIAL_ENFORCEMENT_NOTICE = 'landlock-run: partial enforcement (older Landlock ABI)'
|
||||
|
||||
/**
|
||||
* The exit code for every launcher-level failure (usage error, unenforcing
|
||||
* kernel, unopenable grant root, failed exec) — chosen because the wrapped
|
||||
* command itself is unlikely to use it, so a consumer can tell launcher
|
||||
* failures from command failures. Part of the CLI contract.
|
||||
* kernel, unopenable grant root, failed exec). After a successful `exec`, the
|
||||
* wrapped command may also return 125, so consumers require a matching
|
||||
* {@link LAUNCHER_FATAL_PREFIX} diagnostic to attribute launcher failure.
|
||||
* Part of the CLI contract.
|
||||
*/
|
||||
export const LAUNCHER_FAILURE_EXIT = 125
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import path from 'node:path';
|
||||
import {
|
||||
LAUNCHER_BIN,
|
||||
LAUNCHER_FAILURE_EXIT,
|
||||
LAUNCHER_FATAL_PREFIX,
|
||||
PARTIAL_ENFORCEMENT_NOTICE,
|
||||
grantArgs,
|
||||
launcherPath,
|
||||
probe,
|
||||
@@ -20,6 +22,8 @@ import {
|
||||
// --- constants are part of the CLI contract ---
|
||||
assert.equal(LAUNCHER_BIN, 'landlock-run');
|
||||
assert.equal(LAUNCHER_FAILURE_EXIT, 125);
|
||||
assert.equal(LAUNCHER_FATAL_PREFIX, 'landlock-run: ');
|
||||
assert.equal(PARTIAL_ENFORCEMENT_NOTICE, 'landlock-run: partial enforcement (older Landlock ABI)');
|
||||
|
||||
// --- grantArgs: flag spelling, ordering, and empty grants ---
|
||||
assert.deepEqual(grantArgs({}), []);
|
||||
|
||||
@@ -19,6 +19,8 @@ import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import {
|
||||
LAUNCHER_FAILURE_EXIT,
|
||||
LAUNCHER_FATAL_PREFIX,
|
||||
PARTIAL_ENFORCEMENT_NOTICE,
|
||||
grantArgs,
|
||||
launcherPath,
|
||||
probe,
|
||||
@@ -43,6 +45,7 @@ const run = (args, options = {}) => spawnSync(launcher, args, { encoding: 'utf8'
|
||||
{
|
||||
const noCommand = run([]);
|
||||
assert.equal(noCommand.status, LAUNCHER_FAILURE_EXIT);
|
||||
assert.ok(noCommand.stderr.startsWith(LAUNCHER_FATAL_PREFIX));
|
||||
assert.match(noCommand.stderr, /usage error: missing `-- <argv>\.\.\.` command/);
|
||||
|
||||
const unknownFlag = run(['--bogus', '--', 'true']);
|
||||
@@ -75,6 +78,7 @@ if (enforcement === 'unusable') {
|
||||
console.log('launcher.test: SKIP enforcement half — kernel does not enforce Landlock');
|
||||
process.exit(0);
|
||||
}
|
||||
const expectedNotice = enforcement === 'partial' ? `${PARTIAL_ENFORCEMENT_NOTICE}\n` : '';
|
||||
{
|
||||
const probeRun = run(['--probe']);
|
||||
assert.equal(probeRun.status, 0);
|
||||
@@ -86,9 +90,14 @@ if (enforcement === 'unusable') {
|
||||
const echo = run([...grantArgs({ readOnly: ['/'] }), '--', '/bin/sh', '-c', 'echo confined-ok']);
|
||||
assert.equal(echo.status, 0, echo.stderr);
|
||||
assert.equal(echo.stdout, 'confined-ok\n');
|
||||
assert.equal(echo.stderr, expectedNotice);
|
||||
|
||||
const exitCode = run([...grantArgs({ readOnly: ['/'] }), '--', '/bin/sh', '-c', 'exit 7']);
|
||||
assert.equal(exitCode.status, 7, 'the wrapped command exit code must pass through unchanged');
|
||||
|
||||
const child125 = run([...grantArgs({ readOnly: ['/'] }), '--', '/bin/sh', '-c', `exit ${LAUNCHER_FAILURE_EXIT}`]);
|
||||
assert.equal(child125.status, LAUNCHER_FAILURE_EXIT, 'a wrapped child may itself return the launcher failure status');
|
||||
assert.equal(child125.stderr, expectedNotice);
|
||||
}
|
||||
|
||||
// --- world-proofs: denied writes stay off disk, grants land, inheritance crosses exec ---
|
||||
@@ -120,6 +129,7 @@ if (enforcement === 'unusable') {
|
||||
const marker = path.join(os.tmpdir(), `nalr-should-not-exist-${process.pid}`);
|
||||
const badGrant = run(['--ro', '/no/such/grant/root', '--', '/bin/sh', '-c', `echo x > ${marker}`]);
|
||||
assert.equal(badGrant.status, LAUNCHER_FAILURE_EXIT);
|
||||
assert.ok(badGrant.stderr.startsWith(LAUNCHER_FATAL_PREFIX));
|
||||
assert.match(badGrant.stderr, /cannot open rule path/);
|
||||
assert.ok(!fs.existsSync(marker), 'the command must never run when the launcher fails');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user