背景
#566 の merge 前の再確認(2026-10-02、H-05)で見つかった見逃しです。#566 の変更とは関係がなく、develop にもとからあります(Bash の書き込み検査を入れた #206 の 5239f9b から)。2026-10-03 の着手前調査(develop@1e193ae )で、原因と直し方を確かめました。判定の関数を呼んで結果を確かめ、shell の挙動は scratchpad の中で echo・printf を使って確かめただけです。
問題(背景)
Bash の書き込み検査の字句解析(shell_tokens)は、ANSI-C 引用 $'…' と locale 引用 $"…" を知りません。$ を普通の文字として残し、続く引用を普通の引用として外すので、$'tee' は $tee という語になります。bash(macOS の /bin/sh を含む)では tee に展開されるため、program・前置き・option を見逃します。
入力(例)
今の判定
bash の実際
$'tee' /tmp/f、env $'tee' /tmp/f、env $'-S' 'tee /tmp/f'
許可(bash:verify でも自動許可)
外へ書く
$'\x74ee' /tmp/f、$'t\145e' /tmp/f、$"tee" /tmp/f、x$'tee' /tmp/f
許可
外へ書く
$'tee' sub/link/f(sub/link は外を指す symlink)、env $'-C' sub tee link/f、cp $'-t' sub/link a.txt
許可
symlink の先へ書く
echo $'\'' ; tee /tmp/f
許可
$'\'' は ' 1 文字だが、字句解析は閉じない引用と読み、後ろのコマンドを丸ごと検査しない
cat $'.env'、cat $'sub/link/secret'
許可(秘密情報の検出も素通り)
秘密や外を読む
影響(背景)
ワークスペースの外への書き込みと読み取り、秘密情報の参照の検出の素通り、bash:verify の自動許可が起きます。
決めた設計(2026-10-03)
引用の外にある $' と $" を、語の位置によらず「確かめられない書式」として拒否する(正直な失敗)。 escape を解く案は、shell の版によって escape の表が違い、$"…" は locale 次第で中身が決まらないため採らない。
判定は引用の外 に限る。二重引用の中("$'x'")、単一引用の中('$')、逃がした $(\$'x')は bash でも展開されないので許可のまま。引用の状態の扱いは shell_tokens と同じにする(引用の外・'…'・"…"・\ による逃がし)。
走査は shell_tokens の前に、コマンド全体 に対して行う($'\'' の読み違えで後ろが隠れる形も塞ぐため)。新しい葉の module に置き、ShellToken の種類と shell_tokens の状態の扱いは変えない([security][tools] Bash の書き込み検査が同じコマンドの cd・pushd の後の cwd を追わず、symlink を通ってワークスペース外へ書き込める(#509 の調査で発見) #568 との衝突を小さくするため)。
拒否は env -S と同じ形にする。新しい operation(例:"ANSI-C / locale quoting")の対象を write_targets に積み、confinement_rejection で拒否する。書き込みありの判定は真になり、inspect の段では拒否、bash:verify の自動許可は外れる。拒否の理由には代わりの書き方(printf 'a\tb\n'、grep -P '\t' など)を書く。event の名前と schema は変えない(operation の値が増えるだけ)。
読み取りの候補の抽出(path_tokens.rs)、秘密情報の検出(sensitive_path.rs)、verify の分類などのほかの字句解析は変えない(1 段目で先に拒否されるため)。
誤拒否として受け入れる形:printf $'a\tb\n' > out.txt、grep $'\t' a.txt、IFS=$'\n' read -r x、read -d $'\0'、echo $"hello"。記録に残る agent の Bash コマンドで $'・$" を使うものは 0 件だった。
対象ファイル
src/tools/bash_write_guard.rs — write_targets の最初と confinement_rejection の配線(数行)
src/tools/bash_write_guard/ansi_c_quoting.rs — 新規。引用の外の $'・$" の走査と単体テスト
tests/issue575_bash_ansi_c_quoting.rs — 新規。path_confinement_rejection を呼ぶ結合テスト
受け入れ条件
テストの設計(参考)
拒否する形(/tmp/f と、fixture の sub/link → 外):
問題の表の形すべて(cat の形は判定の関数で確かめるだけで、実際には読まない)
$'\cX' /tmp/f、$''tee /tmp/f、$""tee /tmp/f、$'sudo' tee /tmp/f、$'rm' -rf sub/link/x
echo $'\'' の後に改行して tee sub/link/f、printf $'a\'b' > sub/link/f
X=$'tee' のような代入の値
許可のままの形:
echo "$'x'"、echo '$'、echo \$'x'、printf '%s\n' x > out.txt、printf 'a\tb\n' > out.txt
普段の検証コマンド(cargo test・cargo fmt --all -- --check・cargo clippy --all-targets -- -D warnings・npm test・npm run build・python3 -m pytest・timeout 600 cargo test など)
テストの関数名には、すべて ansi_c_quoting を含めます(変異テストの名前のフィルタで、単体テストと結合テストの両方が走るようにするため)。
回帰で通す既存のテスト(参考)
tests/issue566_bash_command_prefix_write_targets.rs
tests/issue567_bash_glob_write_targets.rs
tests/issue428_bash_path_tokens.rs
tests/bash_workspace_confinement.rs
作業の場所(参考)
ビルドとテストは CARGO_TARGET_DIR=/Volumes/SSD_NX/tmp/issue-575-20261003-01/target-575 と CARGO_INCREMENTAL=0 で行います。
変異テストのコマンド(--lib は付けない。付けると結合テストが走らない):
cargo mutants -f src/tools/bash_write_guard/ansi_c_quoting.rs --jobs 2 --timeout 120 -o /Volumes/SSD_NX/tmp/issue-575-20261003-01/mutants -- ansi_c_quoting
配線の行は -F で足します。ベースラインで単体テストと結合テストの両方が走ることを確かめます。打ち切った後は、ソースを touch してビルドし直します。
着手前調査の report:ローカルの workspace/tmp/1003/issue-575/results/H-01/report.md。probe:/Volumes/SSD_NX/tmp/issue-575-20261003-01/probe。
関連(参考)
背景
#566 の merge 前の再確認(2026-10-02、H-05)で見つかった見逃しです。#566 の変更とは関係がなく、develop にもとからあります(Bash の書き込み検査を入れた #206 の 5239f9b から)。2026-10-03 の着手前調査(develop@1e193ae)で、原因と直し方を確かめました。判定の関数を呼んで結果を確かめ、shell の挙動は scratchpad の中で echo・printf を使って確かめただけです。
問題(背景)
Bash の書き込み検査の字句解析(shell_tokens)は、ANSI-C 引用
$'…'と locale 引用$"…"を知りません。$を普通の文字として残し、続く引用を普通の引用として外すので、$'tee'は$teeという語になります。bash(macOS の /bin/sh を含む)ではteeに展開されるため、program・前置き・option を見逃します。$'tee' /tmp/f、env $'tee' /tmp/f、env $'-S' 'tee /tmp/f'$'\x74ee' /tmp/f、$'t\145e' /tmp/f、$"tee" /tmp/f、x$'tee' /tmp/f$'tee' sub/link/f(sub/link は外を指す symlink)、env $'-C' sub tee link/f、cp $'-t' sub/link a.txtecho $'\'' ; tee /tmp/f$'\''は'1 文字だが、字句解析は閉じない引用と読み、後ろのコマンドを丸ごと検査しないcat $'.env'、cat $'sub/link/secret'影響(背景)
ワークスペースの外への書き込みと読み取り、秘密情報の参照の検出の素通り、bash:verify の自動許可が起きます。
決めた設計(2026-10-03)
$'と$"を、語の位置によらず「確かめられない書式」として拒否する(正直な失敗)。 escape を解く案は、shell の版によって escape の表が違い、$"…"は locale 次第で中身が決まらないため採らない。"$'x'")、単一引用の中('$')、逃がした$(\$'x')は bash でも展開されないので許可のまま。引用の状態の扱いは shell_tokens と同じにする(引用の外・'…'・"…"・\による逃がし)。$'\''の読み違えで後ろが隠れる形も塞ぐため)。新しい葉の module に置き、ShellToken の種類と shell_tokens の状態の扱いは変えない([security][tools] Bash の書き込み検査が同じコマンドの cd・pushd の後の cwd を追わず、symlink を通ってワークスペース外へ書き込める(#509 の調査で発見) #568 との衝突を小さくするため)。printf 'a\tb\n'、grep -P '\t'など)を書く。event の名前と schema は変えない(operation の値が増えるだけ)。printf $'a\tb\n' > out.txt、grep $'\t' a.txt、IFS=$'\n' read -r x、read -d $'\0'、echo $"hello"。記録に残る agent の Bash コマンドで$'・$"を使うものは 0 件だった。対象ファイル
src/tools/bash_write_guard.rs— write_targets の最初と confinement_rejection の配線(数行)src/tools/bash_write_guard/ansi_c_quoting.rs— 新規。引用の外の$'・$"の走査と単体テストtests/issue575_bash_ansi_c_quoting.rs— 新規。path_confinement_rejection を呼ぶ結合テスト受け入れ条件
$'tee' a.txtが真になることをテストで固定したテストの設計(参考)
拒否する形(/tmp/f と、fixture の sub/link → 外):
$'\cX' /tmp/f、$''tee /tmp/f、$""tee /tmp/f、$'sudo' tee /tmp/f、$'rm' -rf sub/link/xecho $'\''の後に改行してtee sub/link/f、printf $'a\'b' > sub/link/fX=$'tee'のような代入の値許可のままの形:
echo "$'x'"、echo '$'、echo \$'x'、printf '%s\n' x > out.txt、printf 'a\tb\n' > out.txtテストの関数名には、すべて ansi_c_quoting を含めます(変異テストの名前のフィルタで、単体テストと結合テストの両方が走るようにするため)。
回帰で通す既存のテスト(参考)
作業の場所(参考)
cargo mutants -f src/tools/bash_write_guard/ansi_c_quoting.rs --jobs 2 --timeout 120 -o /Volumes/SSD_NX/tmp/issue-575-20261003-01/mutants -- ansi_c_quoting
配線の行は -F で足します。ベースラインで単体テストと結合テストの両方が走ることを確かめます。打ち切った後は、ソースを touch してビルドし直します。
関連(参考)
{ … }の中の書き込みを認識せず、ワークスペース外へ書き込める(#509 の調査で発見) #566(コマンドの前置き)、[security][tools] Bash の書き込み検査が同じコマンドの cd・pushd の後の cwd を追わず、symlink を通ってワークスペース外へ書き込める(#509 の調査で発見) #568(cd の後の作業ディレクトリ。同じ字句解析を変える)、[security][tools] Bash の字句検査では、プログラム経由の間接書き込みや検証コマンドの副作用を閉じ込められない #502(字句検査の限界)根拠(develop@1e193ae)