Skip to content

[tests][mutants] 変異試験が防御を外したまま kill(-1) などを実行し、利用者の全プロセスを止める #673

Closed

Description

@Kewton

背景

#667 の merge 前の検証(L-46、2026-10-14 02:54〜)で、leader が macOS 上で cargo-mutants を流しました。02:56:57 に変異 src/bounded_process.rs: replace signalable_process_group -> Option<i32> with Some(1) の試験 binary が kill(-1, SIGTERM) を実行し、利用者のプロセスすべて(leader・worker・CommandMate・Docker・Finder など、launchd の記録で SIGTERM 102 件)が終了しました。mac は利用者の再起動で復旧しています。

  • unified log:exited due to SIGTERM | sent by commandagent-d4ecb0d8a0f17cf6[86533]
  • mutants の出力:/Volumes/SSD_NX/tmp/wave1-20261007/mutants-667-l46/mutants.out/(Terminated: 15 で中断)

製品の防御は正しく働いています。問題は、変異が防御を外したまま、本物の kill を実行できる環境で試験が走ったことです。
関連:#667

問題

  • P1:signal の宛先を守る関数を変異させると、kill(-1)(同じ利用者の全プロセス)や kill(0)(自分のグループ)が実際に実行されます。
    • src/bounded_process.rs:405 signalable_process_group は宛先の値そのものを返し、呼び出し側は kill(-pgid, …) を送ります。Some(1) → kill(-1, …)(今回の原因)、Some(0) → kill(0, …)(自分のグループ)、Some(-1) → kill(+1, …)(init。利用者の権限では EPERM)。
    • src/bin/gui_server/trial_process.rs:430 group_is_signalable は、変異で true になると防御が外れます。
  • P2:危険な値をわざと渡す試験は、防御が変異すると本物の危険な命令になります。
  • P3:再帰削除(remove_dir_all)9 箇所は、変異でパスが変わったときの被害をまだ確かめていません。
    • src/bin/gui_server/session_paths.rs:300、src/model_probe.rs:234、src/planner/pack/supply.rs:714・:717、src/tools/dir_fd.rs:414・:523、src/tools/edit.rs:308、src/tools/os_sandbox.rs:612、src/tools/write.rs:129
  • P4:危険な命令の呼び出しが新しく増えても、検知する仕組みがありません。
  • P5(H-31):signal の宛先が広がらない変異でも、試験の外に影響が残ります。
    • プロセスの回収漏れ:子の session を作る処理([security][tools] OS の隔離の中から制御端末へ TIOCSTI で入力を注入でき、/dev/pts の他の端末へ書ける #667 の configure_child_session)を空にする変異では、子が PID = PGID のグループを作らず、終了処理の kill(-child.id()) が空振りします。TUI の試験(src/tui/profile_transport_tests.rs:223)が起動する終わらない試験用サーバーが残ります。cargo-mutants の --timeout は残ったプロセスの回収を保証しません。
    • 実 HOME の継承:pager の試験(tests/issue502_os_sandbox_write.rs:1727 付近)の git init・git commit は隔離の外で動き、利用者の HOME と Git の設定(hook・template・署名)を引き継ぎます。

根拠(develop 72807847)

signal を送る箇所:

  • src/bounded_process.rs:422・:427・:482・:502(kill(-pgid, …)、防御 :405)
  • src/bin/gui_server/trial_process.rs:446・:469(防御 :430)
  • src/planner/runner/acceptance.rs:1327・:1368(pid > 1 の確認なし。pid は子プロセスから得た値で、実害は確認していない)
  • src/preflight.rs:175(kill(pid, SIGTERM)、lsof で得た正の pid)
  • src/planner/runner/tests/support/process.rs:97(kill(-pgid, 0)、試験の補助)

cargo-mutants 27.1.0 の挙動(scratchpad の小さな crate で cargo mutants --list を出して確認。試験は流していない):

  • assert!(pgid > 1) の中の比較は変異されない(マクロの中は対象外)。
  • 戻り値が io::Result<()> で、assert! と kill を同じ関数に置くと、作られる変異は Ok(()) に置き換える・- を消す・== を != にする、の 3 つだけで、どれも危険な宛先にならない。
  • 戻り値が値の関数(-> i32 など)では、関数全体を -1 などに置き換える変異で assert! ごと飛ばされる。
  • この確認は小さな crate の 3 本についてのもので、製品の全関数の安全の証明ではない。版・対象 OS・features が変わったら確かめ直す。
  • .cargo/mutants.toml の exclude_re は、コマンドの --re では解除されない(H-31。cargo-mutants の src/options.rs)。

対処の方向

  1. 除外リスト(すぐ効く応急処置):.cargo/mutants.toml の exclude_re に signalable_process_group と group_is_signalable を入れる。理由と障害の日付をコメントに書く。
  2. 変異されない最後の防御(signal):
    • kill を呼ぶ関数そのもの(戻り値が () か io::Result<()>)で、syscall に渡す値を変換した後に検査する。既存の Err・bail! の経路は残し、assert! は変異されない最後の砦として足す。
    • 複数の kill、正の pid に送る preflight.rs:175、signal 0 の生存確認(acceptance.rs:1368、trial_process.rs:469、試験の補助)は別々に分類する。pgid > 1 は、無関係な正の pid でないこと(所有)の確認にはならない。
    • 各関数と、値を決める上流の関数の変異を cargo mutants --list で全件確かめる。
  3. 変異されない最後の防御(再帰削除):P3 の 9 箇所を、本体の cleanup と試験の補助に分け、到達経路と変異で起こりうる最悪の結果で分類する。防御は path.starts_with(temp_root) では足りない(..、祖先の symlink、root そのものの削除、root を作る上流の変異)。所有する run・staging の厳密な子であることを実パスと所有の証跡で確かめ、確かめた値をそのまま削除する。pack の staging など製品で必要な削除先は、一律に temp へ変えない。
  4. 隔離した場所での確認:
    • 隔離環境の中だけで、確認用の設定(除外を外したもの)を明示して流す。危険な関数の変異が実際に列挙・実行されたことを確かめる(通常の設定のまま --re を付けても 0 本になる)。GUI の試験には gui feature が要る。
    • 合格の条件は caught だけではない。0・1・負の値などを実際に syscall の直前の防御まで届け、そこで拒否され、危険な syscall が出ていない証拠を取る(今の危険値の試験は、上流の selector の assert が先に失敗し、最後の防御まで届かない変異がある)。
    • 番兵のプロセスは同じ UID・同じ PID namespace の別のグループに置く。正当な対象のグループと孫は終了し、番兵は生き残ることを確かめる。
    • Docker は host の PID namespace・--privileged・host の HOME や端末・Docker socket を共有しない。
    • 確かめた関数から、通常の設定の除外を外す。
  5. 危険な命令の一覧と検知:
    • 対象の種類:signal(libc::kill・killpg、Rust・nix の wrapper、外部の kill)、削除(再帰削除と単一ファイルの削除)、上書き・truncate・rename、権限の変更、端末の ioctl・termios・共有 fd の flags、外部の shell・interpreter(sh -c、Python の中の命令)。
    • 範囲:src だけでなく tests・build.rs・実行スクリプト、および防御を外しうる上流の関数。
    • 一覧(関数、防御の有無、除外の理由)をリポジトリに置き、grep の結果と食い違ったら失敗する試験を加える。直接の API 名の grep では alias・wrapper・sh -c の中を落とすので、検出の限界を文書に書く。一覧の照合は検知であって、実行の隔離ではない。
  6. 開発ハーネスの規則:docs/dev/parallel-dev-harness.md に次を加える。
    • mutants の対象は brief に書いた関数だけにする。広げるときは PM に確認する。
    • run ごとに新しい実行スクリプトと出力先を用意する。流す前に cargo mutants --list を出し、PM が変異の名前と置換の中身を全件照合する(件数だけでは足りない)。
    • signal・削除・子の session を変える変異は、最後の防御を置くまで隔離環境(Docker、macOS だけの試験は使い捨ての VM や専用アカウント)で流す。
    • host で流す試験は、試験名の絞り込みを最小にし、秘密のない専用の HOME・Git 設定・既知の実行ファイルで動かす。GITHUB_STEP_SUMMARY は未設定か新しい run の所有ファイルに限る。外側の GIT_* は bounded child の許可リストで落ちるので、実際の子の環境まで確かめる。
    • --jobs 1・heavy-lock・利用者の在席は、隔離や回収の代わりにならない。

受け入れ条件

  • .cargo/mutants.toml があり、cargo mutants --list に signalable_process_group と group_is_signalable の変異が出ない(4 の確認が済んで外した場合は、その証跡がある)
  • signal を送るすべての箇所を分類し、kill を呼ぶ関数そのものに、変換後の値を検査する変異されない防御がある。各関数と上流の関数の変異を cargo mutants --list で全件確かめた
  • remove_dir_all の 9 箇所を分類し、必要な箇所に「所有する root の厳密な子」を実パスで確かめる防御を置いた(不要とした箇所は理由を書いた)
  • 隔離環境の中で、確認用の設定で危険な関数の変異が実際に実行され、危険な値が最後の防御で拒否され、危険な syscall が出ていない証跡がある。番兵(同じ UID・同じ PID namespace・別のグループ)が生き残り、正当な対象のグループと孫は終了した
  • 子の session を変える変異で、試験が起動したプロセスが残らないことを隔離環境で確かめた(または回収の仕組みを入れた)
  • host で流す試験が、利用者の HOME・Git の設定・hook に届かないことを、実際の子の環境で確かめた
  • 危険な命令の一覧と grep の結果の食い違いで失敗する試験がある(二点測定:一覧に無い kill を足すと失敗する)。検出の限界を文書に書いた
  • docs/dev/parallel-dev-harness.md に mutants の規則を書いた
  • 製品の挙動(timeout・中断でのプロセスグループの終了、preflight の確認、既存の Err・bail!)は変わっていない。既存の試験が通る
  • 検証・受入・証跡のゲートを緩めていない。成長上限の baseline を上げていない

対象ファイル

  • .cargo/mutants.toml(新規)
  • src/bounded_process.rs
  • src/bin/gui_server/trial_process.rs
  • src/planner/runner/acceptance.rs
  • src/preflight.rs
  • P3 の分類で防御を置くファイル(remove_dir_all の 9 箇所のうち必要なもの)
  • docs/dev/dangerous-commands.md(新規。危険な命令の一覧:関数、防御の有無、除外の理由、検出の限界)
  • tests/issue673_dangerous_command_inventory.rs(新規。一覧と grep の結果の食い違いで失敗する試験)
  • docs/dev/parallel-dev-harness.md

参照

  • PM の ledger(ローカル):workspace/tmp/1007/wave1/ledger.md(2026-10-14 08:20 以降の行)
  • L-46 の mutants の記録(ローカル):workspace/tmp/1007/wave1/results/L-46/mutants-macos.txt
  • H-31 の手順の安全性レビュー(ローカル):workspace/tmp/1007/wave1/results/H-31/report.md(表の 1〜10 を本文の P5・対処の方向 2〜6 に反映)

Activity

  1. added a commit that references this issue on Oct 14, 2026
  2. Kewton commented on Oct 14, 2026

    @Kewton
    OwnerAuthor

    PR #678 で develop に merge しました(merge commit 3d4144db6952a6c6546ee8a22e31fc823365e4b1)。merge 後の develop の CI・acceptance・Next.js domain oracles は success です。

    • 入ったもの:.cargo/mutants.toml の除外、signal を送る関数の kill の直前の変異できない assert!、再帰削除を確かめた実パスだけにする形、危険な命令の一覧 docs/dev/dangerous-commands.md と grep との食い違いで落ちる試験、harness §3a(mutants の規則)。
    • 検証:mutants は Docker の中だけで流し、危険な宛先への kill は全組 0 件(strace)、canary 生存。merge 前レビューは blocker 1(harness §3a の文言)を直して、再確認で blocker 0。
    • 後続の論点(未着手):
      • install_for_test の root が削除先から独立していない(試験だけの経路)
      • 一覧の照合の単位(関数名+件数)と tests/**/*.sh の読み込み
      • 確かめてから削除するまでの祖先の symlink の差し替え(TOCTOU)
      • session_paths.rs:314 の NotFound の判定を確かめる試験(missed 3 本)
      • macOS の dedicated account で流す場合の後片付けの方法
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions