Merge pull request #1362 from deepseek-harness/fix/landlock-runner-failure-classification

Fix false sandbox failures from Landlock partial-enforcement notices
This commit is contained in:
Wenlu Wang
2026-08-05 13:59:47 +08:00
committed by GitHub
58 changed files with 1418 additions and 250 deletions

View File

@@ -228,7 +228,7 @@ export interface Config {
}
```
Source: [`packages/bash/bash-local/src/index.ts:39`](../packages/bash/bash-local/src/index.ts)
Source: [`packages/bash/bash-local/src/index.ts:40`](../packages/bash/bash-local/src/index.ts)
## `@deepseek-ai/dsh-bash-sandbox`
@@ -247,7 +247,7 @@ export type Config = LocalConfig
Depends on: [`LocalConfig`](#deepseek-aidsh-bash-local)
Source: [`packages/bash/bash-sandbox/src/index.ts:27`](../packages/bash/bash-sandbox/src/index.ts)
Source: [`packages/bash/bash-sandbox/src/index.ts:35`](../packages/bash/bash-sandbox/src/index.ts)
## `@deepseek-ai/dsh-cli-demo`
@@ -1062,17 +1062,18 @@ export interface Config {
/**
* Override the runner argv; bwrap-shaped profile arguments are appended. A
* non-empty override asserts full enforcement and skips built-in selection and
* probing; a broken runner then fails at execution and must be identifiable by
* {@link runnerFailureSignatures}.
* probing. A runner that starts but refuses its profile must be identifiable by
* {@link runnerFailureSignatures}. Consumers classify spawn rejection; only
* attributable `ENOENT` or `EACCES` with runner argv[0] provenance becomes an
* infrastructure failure.
*/
runnerCommand?: string[]
/**
* Case-insensitive stderr substrings emitted when a configured
* {@link runnerCommand} refuses its profile before executing the wrapped
* command. Required and non-empty with `runnerCommand`; rejected without
* it. Missing/unexecutable runner errors are added automatically from
* `runnerCommand[0]`, while these signatures cover an executable runner's
* own failure dialect.
* it. Each entry is a non-empty, single-line, case-insensitive substring
* covering the executable runner's own failure dialect.
*/
runnerFailureSignatures?: string[]
/** Positive timeout for each functional probe; zero would mean unbounded to Node. */
@@ -1080,7 +1081,7 @@ export interface Config {
}
```
Source: [`packages/sandbox/sandbox-local/src/index.ts:19`](../packages/sandbox/sandbox-local/src/index.ts)
Source: [`packages/sandbox/sandbox-local/src/index.ts:24`](../packages/sandbox/sandbox-local/src/index.ts)
## `@deepseek-ai/dsh-sandbox-policy`

View File

@@ -1103,7 +1103,7 @@ abstract confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv
Types: [ConfinedArgv](../core-data-structures/sandbox.md) · [SandboxPolicy](../core-data-structures/sandbox.md)
Source: [`packages/sandbox/sandbox/src/index.ts:131`](../../packages/sandbox/sandbox/src/index.ts)
Source: [`packages/sandbox/sandbox/src/index.ts:148`](../../packages/sandbox/sandbox/src/index.ts)
## `ctx.sandboxPolicy` — `SandboxPolicyService`

View File

@@ -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 docs/core-data-structures/sandbox.md
sandbox.md: 9bc05fa06f22fdc9ac9e8aacd482c1e7c2f2edec
sandbox.zh.md: ff44db13115073f3a77d2bda575cc91ec41da418
sandbox.md: b931dca54afd1fdd1dcba5b8e7778feeedb7d15d
sandbox.zh.md: a2334148e0237db2309c77437cf0681586e2fb5a

View File

@@ -87,7 +87,27 @@ interface SandboxPolicy extends SandboxExecutionPolicy {
## Wrapped argv and classification dialects
`ConfinedArgv` is what the consumer spawns. Besides the replacement argv, it carries the backend's enforcement fact and two orthogonal stderr dialects. `denialSignatures` identify the confined command being blocked while the sandbox works correctly. `runnerFailureSignatures` identify the sandbox runner refusing or failing before it executes the command; consumers check these first and surface a sandbox infrastructure failure, never an ordinary task failure.
`RunnerFailureRule` combines evidence that a runner failed before executing the command. A consumer requires a nonzero exit, the optional allowed-exit-code gate, and a case-insensitive fatal signature within one remaining stderr line. Case-insensitive exact full-line informational exclusions are removed first, so a benign runner notice cannot prove failure by itself. The matched line remains available as error detail; classification does not rewrite stderr.
```ts type-equiv
/**
* Evidence that identifies a sandbox runner failing before it executes the
* wrapped command. A consumer first applies {@link allowedExitCodes} when
* present, removes {@link informationalLines} by case-insensitive exact line
* equality, then matches {@link fatalSignatures} case-insensitively within
* each remaining stderr line. Exit status alone never proves runner failure.
*/
interface RunnerFailureRule {
/** Nonzero process exit codes on which this rule may match; omitted permits any nonzero exit. */
allowedExitCodes?: readonly number[]
/** Non-empty substrings identifying a fatal runner diagnostic on one stderr line. */
fatalSignatures: readonly string[]
/** Benign stderr lines excluded by exact full-line equality before fatal matching. */
informationalLines?: readonly string[]
}
```
`ConfinedArgv` is what the consumer spawns. Besides the replacement argv, it carries the backend's enforcement fact and two orthogonal stderr classifiers. `denialSignatures` identify the confined command being blocked while the sandbox works correctly. `runnerFailureRules` identify the sandbox runner refusing or failing before it executes the command; consumers check these first and surface a sandbox infrastructure failure, never an ordinary task failure.
```ts type-equiv
/**
@@ -110,18 +130,19 @@ interface ConfinedArgv {
*/
denialSignatures: readonly string[]
/**
* Case-insensitive signatures for runner failure before command execution.
* Consumers check these before denial signatures: runner failure means the
* Structured runner-failure evidence rules. Consumers require a matching
* fatal stderr line (after informational exclusions) and any rule-specific
* exit-code gate before checking denial signatures: runner failure means the
* command never ran, while denial means confinement worked and blocked it.
*/
runnerFailureSignatures: readonly string[]
runnerFailureRules: readonly RunnerFailureRule[]
}
```
An operator-configured local runner must supply at least one `runnerFailureSignatures` entry for its own pre-exec refusal dialect; the provider adds outer-shell missing and unexecutable forms automatically. This makes an executable custom runner rejecting its profile distinguishable from the wrapped command exiting with the same status.
The operator-facing local-provider key remains `runnerFailureSignatures`: an operator-configured runner must supply at least one non-empty, single-line, case-insensitive substring for its own pre-exec refusal dialect. The provider maps those entries into one rule. Consumers directly spawn `ConfinedArgv.argv`, so a missing runner, a non-executable runner, or an executable script whose shebang interpreter is unavailable rejects through the spawn channel rather than a stderr rule when Node supplies attributable `ENOENT`/`EACCES` evidence; after a process starts, child exits such as 126 or 127 remain ordinary unless the selected runner's documented fatal signature matches.
## Provider and fail-closed errors
`ctx.sandbox.confine(argv, policy)` returns a `ConfinedArgv` or throws `SandboxUnavailableError` with code `SANDBOX_UNAVAILABLE` when no usable backend exists. A selected runner can also fail closed at execution time, in which case its failure signature carries the same infrastructure meaning. Silent unconfined passthrough is never legal for a confined policy.
`ctx.sandbox.confine(argv, policy)` returns a `ConfinedArgv` or throws `SandboxUnavailableError` with code `SANDBOX_UNAVAILABLE` when no usable backend exists. Any direct spawn rejection of the returned argv proves the confined launch never started, but only `ENOENT` or `EACCES` with positive Node provenance for provider argv[0] after the caller-owned workdir is independently verified usable carries infrastructure meaning and the original error as detail. A bare `syscall: 'spawn'` without an exact error path, any other code, an invalid or unusable workdir, a resource failure, an unrelated syscall, or an unstructured rejection retains the consumer's ordinary command-start semantics. After a process starts, a matching structured rule identifies a runner refusal. Silent unconfined passthrough is never legal for a confined policy.
Provider probing arbitrates between multiple candidates and is cached for the provider lifetime. A platform with one candidate may select it directly; execution-time refusal retains the safety property. The local provider reports bwrap and Seatbelt as full and preserves the Landlock launcher's full/partial kernel verdict.

View File

@@ -87,7 +87,27 @@ interface SandboxPolicy extends SandboxExecutionPolicy {
## 包装后的 argv 与分类方言
`ConfinedArgv` 是消费方实际 spawn 的内容。除了替换后的 argv它还携带后端的强制执行事实和两种正交的 stderr 方言。`denialSignatures` 用于识别沙箱正常工作、受限命令被阻止的情况。`runnerFailureSignatures` 用于识别沙箱运行器在执行命令之前拒绝或失败的情况;消费方应先检查后者,将其作为沙箱基础设施故障上报,而非普通任务失败
`RunnerFailureRule` 汇集用于判定 runner 在执行命令前失败的证据。消费方要求进程以非零状态退出,并同时满足可选的允许退出码门控,以及余下某一 stderr 行中不区分大小写的致命签名。系统会先按不区分大小写的整行精确匹配移除信息性排除项,因此无害的 runner 通知本身不能证明失败。匹配到的行仍可用作错误详情;分类过程不会重写 stderr
```ts type-equiv
/**
* Evidence that identifies a sandbox runner failing before it executes the
* wrapped command. A consumer first applies {@link allowedExitCodes} when
* present, removes {@link informationalLines} by case-insensitive exact line
* equality, then matches {@link fatalSignatures} case-insensitively within
* each remaining stderr line. Exit status alone never proves runner failure.
*/
interface RunnerFailureRule {
/** Nonzero process exit codes on which this rule may match; omitted permits any nonzero exit. */
allowedExitCodes?: readonly number[]
/** Non-empty substrings identifying a fatal runner diagnostic on one stderr line. */
fatalSignatures: readonly string[]
/** Benign stderr lines excluded by exact full-line equality before fatal matching. */
informationalLines?: readonly string[]
}
```
`ConfinedArgv` 是消费方实际 spawn 的内容。除了替换后的 argv它还携带后端的强制执行事实和两种正交的 stderr 分类器。`denialSignatures` 用于识别沙箱正常工作时受限命令被阻止的情况。`runnerFailureRules` 用于识别沙箱 runner 在执行命令之前拒绝或失败的情况;消费方应先检查后者,将其作为沙箱基础设施故障上报,而非普通任务失败。
```ts type-equiv
/**
@@ -110,18 +130,19 @@ interface ConfinedArgv {
*/
denialSignatures: readonly string[]
/**
* Case-insensitive signatures for runner failure before command execution.
* Consumers check these before denial signatures: runner failure means the
* Structured runner-failure evidence rules. Consumers require a matching
* fatal stderr line (after informational exclusions) and any rule-specific
* exit-code gate before checking denial signatures: runner failure means the
* command never ran, while denial means confinement worked and blocked it.
*/
runnerFailureSignatures: readonly string[]
runnerFailureRules: readonly RunnerFailureRule[]
}
```
运维人员配置的本地运行器必须为自身的 pre-exec 拒绝方言提供至少一条 `runnerFailureSignatures` 条目;提供方会自动添加外层 shell 报告的「命令不存在」和「不可执行」形式。这使得可执行的自定义运行器拒绝其 profile 的情况能够与被包装命令以相同状态码退出的情况区分开来
面向运维人员的本地提供方配置键仍为 `runnerFailureSignatures`:运维人员配置的 runner 必须为自身的 pre-exec 拒绝方言提供至少一个非空、单行、不区分大小写的子串。提供方会将这些条目映射到一条规则。消费方直接 spawn `ConfinedArgv.argv`,因此当 Node 提供可归因的 `ENOENT``EACCES` 证据时,缺失的 runner、不可执行的 runner或 shebang 解释器不可用的可执行脚本会在 spawn 通道遭拒,而不是由 stderr 规则判定进程启动后126 或 127 等子进程退出码仍按普通结果处理,除非匹配所选 runner 文档所定义的致命签名
## 提供方与 fail-closed 错误
`ctx.sandbox.confine(argv, policy)` 返回一个 `ConfinedArgv`,或在没有可用后端时抛出 `SandboxUnavailableError`(错误码 `SANDBOX_UNAVAILABLE`)。已选定的运行器也可能在执行时 fail-closed此时其失败签名承载相同的基础设施含义。对于受限策略,静默的无隔离透传永远不合法。
`ctx.sandbox.confine(argv, policy)` 返回一个 `ConfinedArgv`,或在没有可用后端时抛出 `SandboxUnavailableError`(错误码 `SANDBOX_UNAVAILABLE`)。直接 spawn 所返回的 argv 时,任何拒绝都能证明受限启动从未开始;但只有在调用方拥有的 workdir 经独立验证可用,且 `ENOENT` 或 `EACCES` 带有明确指向提供方 argv[0] 的 Node 来源信息时,该拒绝才具有基础设施含义,并以原始错误作为详细信息。没有精确错误路径的裸 `syscall: 'spawn'`、任何其他错误码、无效或不可用的 workdir、资源失败、无关 syscall 或无结构拒绝仍保留消费方的普通命令启动语义。进程启动后,匹配到的结构化规则标识 runner 拒绝。对于受限策略,静默的无隔离透传永远不合法。
提供方探测在多个候选后端之间仲裁,结果在提供方生命周期内缓存。只有一个候选后端的平台可以直接选定它;执行时拒绝仍保留安全属性。本地提供方将 bwrap 和 Seatbelt 报告为 full并保留 Landlock 启动器的 full/partial 内核裁定。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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 docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.md
0004-landlock-partial-notice-misclassified-child-failures.md: b3be42d221623c70796105a593127971660ebc2b
0004-landlock-partial-notice-misclassified-child-failures.zh.md: 7a057d4405fcc6733e9c86cbbbbfff9bf8012641

View File

@@ -0,0 +1,55 @@
# Post-mortem 0004: Landlock partial-enforcement notice misclassified child failures
English | [中文](0004-landlock-partial-notice-misclassified-child-failures.zh.md)
Status: resolved
## Executive summary
On kernels with an older Landlock ABI, the launcher prints a benign partial-enforcement notice before executing every child. The harness treated that shared `landlock-run:` prefix plus any nonzero child exit as launcher failure, so ordinary outcomes such as ripgrep's exit 1 for no matches surfaced as `SANDBOX_UNAVAILABLE`; the then-bash-backed filesystem search also hid that structured error behind `SEARCH_FAILED`. Broad signature rules and missing partial-ABI composition coverage let the defect through. Runner classification now requires status-gated fatal evidence after exact informational exclusions, and an assembled keyless scenario pins the surviving bash path. Filesystem search has since moved to packaged ripgrep through the subprocess seam and no longer crosses sandboxed bash.
## Summary
The native launcher contract distinguishes two kinds of stderr lines. A partially enforcing kernel prints exactly `landlock-run: partial enforcement (older Landlock ABI)` and continues into the child. A launcher failure prints another `landlock-run:` line and exits 125 without executing the child.
The harness represented both with one case-insensitive `landlock-run: ` substring. Its consumer classified any nonzero exit carrying that substring as runner failure. The child's status was therefore attached to the launcher's informational line: `false`, ripgrep's no-match exit 1, invalid-pattern exit 2, and even a child-selected exit 125 could be blamed on the sandbox despite successful confinement and execution.
At the time of the incident, filesystem search added a second attribution error. Its bash-backed `runRipgrep()` caught every rejected bash run that was not aborted and replaced it with a generic cwd/shell-start `SEARCH_FAILED`, including the structured `SandboxUnavailableError` produced by the sandbox executor.
## Impact
On partial-ABI Landlock hosts, legitimate nonzero child outcomes could appear as sandbox infrastructure failure. `glob` and `grep` were especially visible because ripgrep uses exit 1 as successful empty search. When a real sandbox failure did occur through filesystem search, callers lost its `SANDBOX_UNAVAILABLE` code and received an incorrect startup diagnosis.
The defect did not weaken confinement or run a command unconfined. Its security effect was availability and diagnostic integrity: a valid confined result was rejected or mislabeled.
## Timeline
- The native launcher contract defined exit 125 for launcher failures, a fatal `landlock-run:` line for every such failure, and the exact partial-enforcement notice for successful child execution.
- The sandbox provider reduced that contract to `runnerFailureSignatures: ['landlock-run: ']`; the bash consumer combined the prefix with any nonzero exit and reported stderr's first line.
- Unit tests covered clean success, denial diagnostics, and fatal runner prefixes. Real-runner tests self-skipped without a usable kernel and did not force partial enforcement followed by a nonzero child.
- A minimal POSIX wrapper that prints the notice and `exec`s its payload reproduced the failure with `false` and ripgrep no-match.
- Structured rules plus shared foreground/background classification and assembled replay coverage closed the surviving sandbox attribution gap. Before this fix was reconciled with current `master`, filesystem search moved to packaged ripgrep through `ctx.subprocess`; the obsolete bash-adapter patch and tests were dropped instead of reintroducing the old architecture.
## Root cause
The public sandbox result type could express only a bag of substrings. It could not state that Landlock failure requires exit 125, that evidence must occur within one fatal line, or that one exact line under the same prefix is informational. The boolean consumer consequently joined unrelated facts from different processes and selected the first stderr line for detail even when a later line was the fatal evidence.
The test matrix mirrored that representation. Fake providers emitted either no runner line or an unambiguously fatal prefix; they never emitted a benign runner line before a child-controlled nonzero exit. Real Landlock coverage depended on the host ABI, so full-ABI hosts could not exercise the notice. In the incident-era search implementation, filesystem-search tests modeled raw spawn errors but not a structured error thrown by the real sandboxed bash composition.
Stderr remains an in-band attribution channel. A confined child can deliberately reproduce a runner's gated fatal line and exit status, causing an availability/diagnostic false attribution. The tighter conjunction prevents the accidental collision in this incident but does not authenticate the writer; an out-of-band status protocol remains separate hardening, not a sandbox-bypass fix.
## Guardrails added
- [`RunnerFailureRule`](../core-data-structures/sandbox.md#wrapped-argv-and-classification-dialects) carries optional allowed exit codes, case-insensitive per-line fatal signatures, and case-insensitive exact informational-line exclusions.
- [`dsh-sandbox-local`](../../packages/sandbox/sandbox-local/) maps Landlock to exit 125 plus a non-notice `landlock-run:` line while bwrap, Seatbelt, and custom runners remain signature-only.
- [`dsh-bash-sandbox`](../../packages/bash/bash-sandbox/) directly spawns the provider argv, so a pre-start rejection uses the spawn-error channel instead of localized shell diagnostics. Settled foreground and background execution share one evidence-returning classifier; fatal evidence outranks denial, and foreground errors report the matched fatal line without changing captured stderr.
- Current [`dsh-tool-fs-search`](../../packages/fs/tool-fs-search/) uses packaged ripgrep through `ctx.subprocess` and no longer consumes the sandboxed bash seam; the base reconciliation keeps that architecture unchanged.
- Deterministic tests use a POSIX fake partial-Landlock launcher to cover notice-only child exits 1, 2, and 125, ordinary child exits 126 and 127, gated fatal diagnostics, permission denial, and foreground/background parity.
- The `examples/acp-agent` keyless snapshot runs direct bash `false` through a test-only partial-Landlock provider, keeping the product regression pinned independently of filesystem-search implementation choices.
## Lessons
- Process attribution requires a conjunction of independent evidence; a shared prefix is not a protocol.
- Informational and fatal diagnostics can share a namespace, so exclusions must be exact and narrow while unknown fatal lines stay fail-closed.
- An adapter must preserve structured failures owned by the seam below it instead of replacing them with its own nearest generic category.
- Platform-dependent behavior needs a deterministic fake at the native boundary plus one assembled product path; a self-skipping real-kernel test cannot carry that regression alone.

View File

@@ -0,0 +1,55 @@
# 事故复盘postmortem 0004Landlock 部分强制执行通知导致子进程失败被误归类
[English](0004-landlock-partial-notice-misclassified-child-failures.md) | 中文
Status: resolved
## 摘要
在 Landlock ABI 较旧的内核上launcher 会在执行每个子进程前打印一条无害的部分强制执行通知。harness 把共享的 `landlock-run:` 前缀与任意非零子进程退出组合起来,判定为 launcher 失败,因此 ripgrep 在没有匹配项时以 1 退出等普通结果会呈现为 `SANDBOX_UNAVAILABLE`;当时仍由 bash 支撑的文件系统搜索还会用 `SEARCH_FAILED` 遮蔽这个结构化错误。过于宽泛的签名规则,以及缺少较旧 ABI 下部分强制执行的组合测试覆盖让该缺陷得以流入。runner 分类现在会先精确排除信息性行,再要求由退出状态门控的致命证据,并由一个组装后的无密钥场景固定仍然存在的 bash 路径。文件系统搜索后来已改为通过 subprocess seam 运行打包的 ripgrep不再经过沙箱化 bash。
## 概述
原生 launcher 契约区分两类 stderr 行。内核只能部分强制执行时,会精确打印 `landlock-run: partial enforcement (older Landlock ABI)`然后继续执行子进程。launcher 失败则打印另一行 `landlock-run:` 诊断,在不执行子进程的情况下以 125 退出。
harness 用一个不区分大小写的 `landlock-run: ` 子串表示这两种情况。消费方只要发现非零退出同时携带该子串,就会归类为 runner 失败。因此,子进程的退出状态被错误地关联到 launcher 的信息性行:`false`、ripgrep 无匹配时的退出码 1、无效 pattern 的退出码 2乃至由子进程自行选择的退出码 125都可能在约束与执行均成功的情况下被错误归因为沙箱故障。
事故发生时,文件系统搜索又造成第二处归因错误。当时由 bash 支撑的 `runRipgrep()` 会捕获 bash 执行器除中止外抛出的所有错误,并将其替换为关于 cwd 或 shell 启动的通用 `SEARCH_FAILED`,其中也包括沙箱执行器产生的结构化 `SandboxUnavailableError`
## 影响
在 Landlock ABI 只能部分强制执行的主机上,合法的非零子进程结果可能表现为沙箱基础设施故障。`glob``grep` 尤其容易暴露该问题,因为 ripgrep 把退出码 1 用作成功的空搜索。当文件系统搜索中确实发生沙箱故障时,调用方也会丢失其 `SANDBOX_UNAVAILABLE` 错误码,转而收到错误的启动诊断。
该缺陷没有削弱约束,也没有让命令在无约束状态下运行。其安全影响在于可用性与诊断完整性:有效的受限结果会被拒绝或错误标记。
## 时间线
- 原生 launcher 契约规定launcher 失败使用退出码 125每次此类失败都会打印一行致命的 `landlock-run:` 诊断;成功执行子进程时则打印精确的部分强制执行通知。
- 沙箱提供方把该契约简化为 `runnerFailureSignatures: ['landlock-run: ']`bash 消费方将此前缀与任意非零退出组合,并报告 stderr 的第一行。
- 单元测试覆盖了无诊断的成功、拒绝诊断和致命 runner 前缀。真实 runner 测试在没有可用内核时会自行跳过,也没有强制构造「部分强制执行通知后跟非零子进程退出」的情况。
- 一个最小 POSIX 包装脚本会打印该通知并 `exec` 其负载;它通过 `false` 与 ripgrep 无匹配场景复现了故障。
- 结构化规则、前台与后台共享的分类逻辑和组装后的回放覆盖共同弥补了仍然存在的沙箱归因缺口。本修复与当前 `master` 对齐前,文件系统搜索已改为通过 `ctx.subprocess` 运行打包的 ripgrep合并时删除了过时的 bash 适配器补丁与测试,而没有把旧架构重新引入。
## 根因
公开的沙箱结果类型只能表达一组子字符串。它无法表示 Landlock 失败必须使用退出码 125、证据必须出现在一行致命诊断内或同一前缀下有一行精确文本属于信息性通知。消费方的布尔判定逻辑因此把来自不同进程且互不相关的事实组合在一起即便致命证据位于后续行它仍选用 stderr 的第一行作为详细信息。
测试矩阵与这种表示方式一致。模拟提供方要么不输出 runner 行,要么输出含义明确的致命前缀,从不在由子进程控制的非零退出前输出无害 runner 行。真实 Landlock 覆盖依赖主机 ABI因此使用完整 ABI 的主机无法覆盖该通知。在事故发生时的搜索实现中,文件系统搜索测试模拟了原始 spawn 错误,却没有覆盖真实沙箱化 bash 组合抛出的结构化错误。
stderr 仍是带内归因通道。受限子进程可以故意复现 runner 的门控致命诊断行与退出状态,造成可用性或诊断误归因。更严格的多项证据合取可以避免本次事故中的意外冲突,但无法验证写入者身份;带外状态协议仍属于独立的加固工作,而非沙箱绕过修复。
## 已添加的防护措施
- [`RunnerFailureRule`](../core-data-structures/sandbox.md#wrapped-argv-and-classification-dialects) 携带可选的允许退出码、不区分大小写的逐行致命签名,以及按不区分大小写的整行精确匹配排除的信息性行。
- [`dsh-sandbox-local`](../../packages/sandbox/sandbox-local/) 把 Landlock 映射为退出码 125 加一行非通知的 `landlock-run:` 诊断,而 bwrap、Seatbelt 和自定义 runner 仍仅依据签名。
- [`dsh-bash-sandbox`](../../packages/bash/bash-sandbox/) 直接 spawn 提供方 argv因此启动前遭拒时使用 spawn 错误通道,而非本地化的 shell 诊断。已结算的前台与后台执行共用一个返回证据的分类器;致命证据优先于拒绝,前台错误会报告匹配到的致命行,同时保持捕获的 stderr 不变。
- 当前 [`dsh-tool-fs-search`](../../packages/fs/tool-fs-search/) 通过 `ctx.subprocess` 运行打包的 ripgrep不再消费沙箱化 bash seam与新基线对齐时保持该架构不变。
- 确定性测试使用一个模拟 Landlock 部分强制执行行为的 POSIX launcher覆盖仅带通知的子进程退出码 1、2、125普通子进程退出码 126、127带门控的致命诊断、权限拒绝以及前台后台一致性。
- `examples/acp-agent` 的无密钥快照会通过仅用于测试的部分 Landlock 提供方直接运行 bash `false`,从而独立于文件系统搜索的实现选择固定产品层回归。
## 教训
- 进程归因需要多项独立证据同时成立;共享前缀不是协议。
- 信息性诊断与致命诊断可以共享同一命名空间,因此排除规则必须精确且范围狭窄,同时对未知的致命行保持失败关闭。
- 适配器必须保留下层 seam 所拥有的结构化失败,而不能用自身最接近的通用类别将其替换。
- 平台相关行为需要在原生边界放置确定性的模拟实现,并覆盖一条组装后的产品路径;会自行跳过的真实内核测试无法独自固定该回归。

View File

@@ -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 docs/postmortem/README.md
README.md: 4858f8841e92a895f2d1a840b59b42758e83d952
README.zh.md: 45fe23275e6236d5210ba32622b51457d170aaeb
README.md: ffde0057304856b7c7718e3dd1f4743c48ee193f
README.zh.md: 58635af16212f990d0f4e2621f4bfed437631645

View File

@@ -15,3 +15,4 @@ Every post-mortem opens with an **Executive summary**: one short paragraph a bus
| [0001](0001-acp-default-export-drops-inject.md) | ACP server crashed on connect: `export default` dropped the plugin's `inject` |
| [0002](0002-js-expression-disabled-filesystem-tools.md) | Filesystem snapshot tools were permanently disabled by a literal `!!js` object |
| [0003](0003-web-agent-gui-feedback-loop.md) | Web agent validated a replacement server instead of the GUI hosting its session |
| [0004](0004-landlock-partial-notice-misclassified-child-failures.md) | Landlock partial-enforcement notice misclassified child failures |

View File

@@ -15,3 +15,4 @@
| [0001](0001-acp-default-export-drops-inject.md) | ACPAgent Client Protocol服务器在连接时崩溃`export default` 丢失了插件的 `inject` |
| [0002](0002-js-expression-disabled-filesystem-tools.md) | 文件系统快照工具被一个字面量 `!!js` 对象永久禁用 |
| [0003](0003-web-agent-gui-feedback-loop.md) | Web agent智能体验证了替代服务器而非承载其会话的 GUI |
| [0004](0004-landlock-partial-notice-misclassified-child-failures.md) | Landlock 部分强制执行通知导致子进程失败被误归类 |