Skip to content

#659 [security][tools] workflow の子 node の隔離の照合、run の一時ディレクトリの後片付け、mktemp の案内 - #665

Merged
Kewton merged 4 commits into
developfrom
feature/issue-659-security-tools-1a-workflow-node-mktemp-run
Oct 13, 2026
Merged

Kewton merged 4 commits into
developfrom
feature/issue-659-security-tools-1a-workflow-node-mktemp-run

Conversation

@Kewton

@Kewton Kewton commented Oct 13, 2026

Copy link
Copy Markdown
Owner

概要

書き込み隔離(1a、#655)の後続です。workflow の子 node の隔離を照合して正直に記録し、run の一時ディレクトリを終了時に片付け、隔離の中で mktemp が失敗したときに案内を出します。merge 前レビュー(H-25)で見つかった P1(cleanup() の後に required でも素通しになる)も、この PR で直しています。

Refs #659

変更(develop 比 5 ファイル、+570 −27)

  • 子 node のワークスペースの照合(src/tools/os_sandbox.rs の reconcile_workspace、src/lib.rs の run_resolved_config_for_workflow)
    • 子 node のワークスペースが run のワークスペースと違う場合は、availability を Unavailable("workflow node runs in a workspace other than the run workspace; …") にし、applied=false で記録します。
    • required では、Bash と検証のコマンドを拒否します(既存の wrap_command の Unavailable の分岐)。同じワークスペースなら何もしません。
  • run の一時ディレクトリの後片付け
    • cleanup・SandboxCleanupGuard・cleanup_guard を足しました。成功と失敗は drop guard で、中断は SIGINT の finalizer(finalize_interrupted_direct_command)で、exit(130) の前に cleanup() を呼びます。
    • 読み取りだけの action(--runs・--ux-demo・--model-probe・--doctor)では、隔離の設定も一時ディレクトリの作成もしません。
  • cleanup() の後に required で素通しにしない(H-25 の P1)
    • cleanup() は、隔離が適用中なら availability を Unavailable("run finished; …") に変えてから、policy を外してディレクトリを消します。cleanup の後は is_applied() が false です。
    • wrap_command の「Available かつ policy が None」の分岐は、required では拒否します(auto は従来どおり)。availability と policy を別の lock で読む間に cleanup が入る競合も、この分岐が受けます。
    • 「policy を残す」案より小さく、auto・off に触れず、消したばかりの一時ディレクトリを指す policy を子に渡さないため、この方法を選びました。
  • mktemp の案内(src/tools/bash.rs):隔離が適用されていて、失敗が一時ディレクトリの権限エラーのときだけ、要約に mktemp -p "$TMPDIR" の案内を足します。判定と許可は変えていません。
  • SECURITY.md:「What remains lexical-only」に、依存の導入が隔離の外であること([security][tools] --offline の外向きの通信を OS の隔離で遮断し、runtime の依存の導入を隔離の中で動かす(第 3 段) #658)と、事前にある外向きの hardlink の限界を追記しました。
  • 試験:tests/issue502_os_sandbox_write.rs は 10 本から 18 本になりました。os_sandbox の単体試験も 2 本足しています。macOS 専用の helper は cfg で囲みました。allow(dead_code) は 0 行です。
  • 変えていないもの:seatbelt の profile、字句検査、event の名前と schema(新しい event・field なし。reason の文字列が 2 つ増えただけ)、成長上限のファイルと baseline。

検証(HEAD 6c4e6f91、develop 6b264fab を取り込み済み)

  • 15 ゲート:commandmate verify で 15/15 passed。
  • Linux の確認:cargo-zigbuild clippy --all-targets --target x86_64-unknown-linux-gnu は exit 0(warning・error 0)でした。
  • 隔離を適用した試験(入れ子でない macOS、--include-ignored):18/18 pass、SKIP 0。target が checkout の下にある配置でも 17/17 pass でした(P1 の修正の前、L-32 の時点)。
  • 二点測定
    • develop 936dbd37 に新しい試験を置くと compile error になります(reconcile_workspace・cleanup が無い)。
    • P1 の試験 cleanup_ends_the_run_without_leaving_the_sandbox_applied は、修正の前の 4dc21624 で落ちます(17 passed・1 failed)。
  • cargo mutants(--jobs 1、--timeout 300、RUSTFLAGS="-A unused-variables")
    • 本体(L-32):caught 23・missed 9・unviable 3。missed に許可が増える向きはありません。5 件は今回変えていない既存の build_summary の行、4 件は案内の語の || です。
    • P1 の修正(L-35):caught 4・missed 0・unviable 2。1 回目は、新しい分岐の required の判定の反転が missed だったので、単体試験を足して捕まえました。
    • unviable はどれも Default を持たない型への置き換えで、build できない変異です。

レビュー

  • H-25(merge 前レビュー):merge 可、blocker 0。P1 は、この PR で直しました。
  • H-26(P1 の修正だけの再確認):merge 可、blocker 0。
    • required は cleanup の前は sandbox-exec で包まれ、後は拒否されます。
    • auto の許可は増えていません。off は変わりません。
    • cleanup を 2 回呼んでも安全です。最上位を symlink に差し替えても、外は消えません。
  • 後続の論点(H-25)
    • a. auto で、ワークスペースの違う子 node は隔離なしで走ります。記録は正直なので、今のままとしました。node ごとに profile を作り直すかは、1b([security][tools] Bash と検証コマンドの書き込みを Linux の landlock で閉じ込める(第 1 段 b) #656)で検討します。
    • b. 一度 applied=false になると戻りません。厳しい側で正直なので、今のままとしました。
    • c. 通しの workflow 試験がありません。--origin ≠ --workspace の 2 node の workflow を mock provider で通し、子の events に applied=false があることを確かめる試験を、後続で 1 本足すことを推奨されています。
    • d. cleanup() の cfg:今のままとしました。
    • e. 案内の判定は文字列の組み合わせです。誤って出ても定数の文が足されるだけで、許可と記録は変わりません。

確かめてほしい点

  • 子 node の試験は reconcile_workspace を直接呼ぶ形です。workflow を通しで走らせる試験は、上の c のとおり後続に残しています。
  • cleanup() の後は、auto でも is_applied() が false になります。bash_sandbox_status を出すのは run の開始時だけなので、cleanup の後に出る event はありません。
  • required の「policy が None」の拒否は、ほとんどの場合 cleanup が先に Unavailable にするので届きません。lock の間の競合だけで届く防御の分岐で、単体試験で固定しています。

🤖 Generated with Claude Code

Kewton and others added 4 commits October 13, 2026 01:27
… workspace

When a workflow child node runs in a workspace other than the run's, the
installed OS write sandbox is fixed to the run's workspace. Instead of applying
it to the wrong workspace, record it as not applied for that node: `auto`
continues with lexical checks only and `required` fails the node's Bash and
verifier commands, so a node is never recorded as isolated while Bash runs in a
different workspace. When the workspace matches, nothing changes. Once a
mismatch is recorded, it does not revert when a later node runs in the parent's
workspace; the stricter record is kept.

Skip installing the sandbox (and its per-run temp directory) for read-only
actions, and remove the run temp directory on every exit path. The direct-CLI
SIGINT finalizer exits with `std::process::exit`, which skips `Drop`, so it
releases the run temp directory itself before both of its exits (the summary
path and the already-finalized path).

Append a `mktemp -p "$TMPDIR"` hint to a Bash failure summary when the sandbox
is applied and the failure is a temp-directory permission error.

Document in SECURITY.md that runtime dependency setup runs outside the sandbox
and that a workspace hardlink that pre-exists the run can be written through.

判断: Issue #659 leaves the child-node fix open (re-point the sandbox to the
child workspace, or record applied=false and fail under `required`). Chose the
second, smaller option: it needs no new temp directory and no profile rebuild,
and keeps the call site a single reconcile step. `auto` continues with lexical
checks only and `required` fails, so the record never claims isolation that was
not in force.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
`cleanup()` removed the policy but left `availability = Available`, so
`is_applied()` stayed true and `wrap_command` passed a command through unwrapped
under `required`. A child spawned between the guard drop, or the SIGINT
finalizer's cleanup and `exit(130)`, could then run unconfined while the record
still claimed the sandbox was applied. `cleanup()` now also moves an applied
sandbox to `Unavailable("run finished")`, and `wrap_command` refuses under
`required` when the backend is available but no policy is installed.

Chose "mark unavailable on cleanup" over "keep the policy": it is the smaller
change, leaves `auto`/`off` untouched, and avoids handing children a policy whose
run temp directory was just deleted. The `wrap_command` refusal hardens the
policy-missing branch so the invariant cannot silently regress.

Test: cleanup_ends_the_run_without_leaving_the_sandbox_applied (Required +
Available via install_for_test, then cleanup) fails on 4dc2162 and passes now.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cover the branch where the backend is available but the run's policy is gone:
`required` refuses with `bash_sandbox_unavailable` and `auto` continues as
before. The applied integration test cannot reach this branch because `cleanup`
also marks the sandbox unavailable, so cargo mutants' `==` -> `!=` mutation in
that branch went uncaught.

The tests set `INSTALLED` directly under the module's existing serialized test
state, so no production API or field is added.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant