Repository navigation
#667 [security][tools] OS の隔離の中から制御端末へ TIOCSTI で入力を注入でき、/dev/pts の他の端末へ書ける - #674
Merged
Kewton merged 8 commits intoOct 14, 2026
Merged
Conversation
Issue #667 (F1/F2). A bounded child used `process_group(0)`, so it stayed in the caller's session and could inject into the controlling terminal with `TIOCSTI`, and Landlock allowed writes to the whole `/dev/pts` directory, so it could write to another terminal's slave. - `bounded_process`: start each child with `setsid()` instead of `process_group(0)`. The child keeps a process group whose id equals its pid, so `terminate_process_group*` still signals exactly its own group, and it now has no controlling terminal, so the kernel refuses `TIOCSTI`. - `landlock`: drop `/dev/tty` and `/dev/pts` from the write allow list. On ABI5+ this also leaves them out of the handled `IoctlDev` access. `/dev/ptmx` stays. - `seatbelt`: keep the `/dev` write allow and re-deny `/dev/tty*` last, so `/dev/null` and friends still work but terminal slaves do not. - Tests: session/process-group detachment, a pty-based `TIOCSTI` probe with a positive and negative control, a terminal-slave write denial on both backends, and a `git` pager case; plus a Seatbelt profile-order unit test. - SECURITY.md documents the change and the visible difference for a program that opens `/dev/tty` directly. 判断: removed `/dev/tty` from the Linux write allow as well as `/dev/pts`, so `TIOCSTI` is closed even below Landlock ABI5 where device ioctls cannot be filtered; the child has no controlling terminal, so it has no use for `/dev/tty`. 判断: kept `/dev/ptmx` writable and, on macOS, kept the broad `/dev` allow with a last-match `/dev/tty*` deny instead of enumerating device literals, to keep the change minimal and avoid breaking the required-tool cases. 判断: relied on `setsid()` for `TIOCSTI` rather than adding a dedicated ABI5 `IoctlDev` rule; a `TIOCSTI` attempt also needs the terminal to be the process's controlling terminal, which the child no longer has. 本文に無い指摘: an interactive pager that reads keypresses from `/dev/tty` directly now sees no controlling terminal; standard streams and the captured (non-interactive) pager path are unchanged. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Issue #667 follow-up. `setsid()` removes the controlling terminal, so the job-control check no longer applied and a child that inherited the caller's terminal on fd 0 could read the keys the user types during a run (previously a background process group was refused with `SIGTTIN`/`EIO`). - `bounded_process`: in the same `pre_exec`, after `setsid()`, replace a terminal fd 0 with `/dev/null` (`isatty`/`open`/`dup2`/`close` only). A `Stdio::null()`, piped, or file stdin is unchanged, as are stdout and stderr. - Tests: a probe that makes its own pty the controlling terminal and fd 0, then starts a child with stdin left unset through `bounded_process` in both the isolated and unisolated launches, and asserts `isatty(0)` is false and the read returns EOF at once. - `SECURITY.md`: correct the stdin statement and list the launch points that inherited a terminal stdin. 判断: only a terminal stdin is replaced; pipes, files, `/dev/null`, and every standard output and error are left alone, so a caller that pipes input keeps working. 判断: the probe asserts the unisolated launch always and the isolated launch only when the sandbox backend probes as available, printing which legs it measured. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Issue #667 follow-up. To replace a terminal stdin the launcher opens `/dev/null` in the forked child and duplicates it onto fd 0, then closes the descriptor it opened. No test pinned that `close`, so mutating the guard `if devnull != 0` to `devnull == 0` left the descriptor open without failing anything, and the exec'd command inherited an extra `/dev/null` descriptor. - Tests: a probe that makes its own pty the controlling terminal and fd 0, then starts a child with stdin left unset through `bounded_process` in both the isolated and unisolated launches. The child reports every descriptor above the standard three whose device is `/dev/null`, and the probe asserts that list is empty. A positive control opens an extra `/dev/null` in the child first, so the scan is shown to see a retained descriptor. - The probe runs in a fresh process, like the existing stdin probe, and only ever uses the pty the probe created. 判断: only the test file changes; the product guard in `bounded_process` is unchanged. The `devnull == 0` mutation now fails the new test (`DEVNULL_FDS 9`), while the unmutated build passes it. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Issue #667 (H-32). `setsid()` closes `TIOCSTI`, but a confined child can still re-open the run's terminal by path (`/dev/pts/N`, `/dev/ttys*`, `/dev/tty`) with `O_RDONLY|O_NOCTTY` and read the keys the user types. Base refused that read only through job control (`SIGTTIN`/`EIO`), which `setsid()` removed, so `SECURITY.md` claimed the read was impossible. Keep `setsid()` (it is what closes `TIOCSTI`) and close the path instead. - `seatbelt`: re-deny `^/dev/tty` for `file-read*` after the terminal write re-deny, so a confined command cannot re-open a terminal slave for reading; `/dev/null`, `/dev/urandom`, `/dev/ptmx`, and the rest of `/dev` are unchanged, and the already-open standard descriptors are not re-checked. - `landlock`: add `/dev/pts`, `/dev/tty`, and every existing `/dev/tty*` and `/dev/console` to the read-deny list, so the expansion leaves them (and their subtrees) out of the read allow list. `/dev` is an ancestor of a denied location, so it is granted `ReadDir` alone through a new `list_only` rule: the listing keeps working (`ls /dev`) while `ReadFile` on the terminal children stays denied. The read layer still needs only ABI1. - Tests: a pty probe that makes its own pty the controlling terminal and fd 0, writes one typed line to the master, and starts a child through `bounded_process` that re-opens the slave by path and reads it; `reopen` and `setsid_reopen` must fail (`ERR`) inside the isolation and must read outside it as the positive control, with `UNVERIFIED` when the backend is unavailable. Unit tests pin the seatbelt rule and its order and the landlock read allow. - `SECURITY.md`: a confined command cannot open a terminal device by path for reading or writing; a terminal stdin becomes `/dev/null`; an inherited stdout/stderr descriptor is not re-checked, but the confined launches set both to pipes; an unconfined launch is not a boundary. 判断: the read deny is by path on the device, so it closes the keystroke read without touching the inherited stdio; the product's confined launches pipe stdout/stderr, so no confined command holds a terminal descriptor. 判断: landlock keeps `/dev` listable with a `ReadDir`-only grant instead of allowing the whole tree, so the terminal children stay unreadable, and `read_isolation_gaps` keeps reporting the same meaning. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Issue #667 follow-up. `terminal_read_deny_paths` selects the terminal devices with `name == "console" || name.starts_with("tty")`, but no unit test pinned the selection, so the mutants `||` to `&&` and `==` to `!=` were not detected; both widen the read allow list and make the physical console and serial terminals readable inside the isolation. - Split the reader into `terminal_read_deny_paths_in(dev: &Path)`, which builds the hardcoded `pts` and `tty` entries relative to `dev` and enumerates `dev`, so a test can drive it against a tempdir; `terminal_read_deny_paths()` now returns `terminal_read_deny_paths_in(Path::new("/dev"))` and is unchanged for the real `/dev`. - Tests: a tempdir with `pts/`, `shm/`, and `tty`, `console`, `tty0`, `ttyS0`, `ttyUSB0`, `null`, `urandom`, `ptmx` must yield exactly the six terminal entries, so the `||`/`&&` and `==`/`!=` mutants change the set and fail it. The existing `terminal_read_deny_paths_names_the_pty_directory_and_the_controlling_tty` test stays. 判断: only the read-deny selection is refactored; `terminal_read_deny_paths()` returns the same list for `/dev`, so the product behavior is unchanged. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Issue #667 follow-up (H-33). The comments and SECURITY.md said `/dev/ptmx` stays usable "so allocating a pty still works". Inside the isolation `/dev/ptmx` is openable but the pty slave it creates is refused (Linux: `/dev/pts` is neither readable nor writable; macOS: `/dev/tty*` is denied), so `openpty`, `script`, `expect`, and `pexpect` cannot use a pty. That pty loss is the cost of closing terminal reads and writes; it worked before #667. - `landlock.rs`: correct the `DEVICE_PATHS` comment; the `/dev/pts` denial already documents why the slave is refused. - `seatbelt.rs`: correct the `WRITE_PROFILE` comment the same way. - `SECURITY.md`: state that `/dev/ptmx` is openable but the pty slave is refused, so a confined command cannot allocate a pty, and that this is a #667 cost. 判断: comments and one documentation sentence only; no code line or test changes, so the behavior is unchanged. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Issue #667 (PR #674 CI). On a runner with `legacy_tiocsti=0` the positive control cannot inject, so the probe correctly reports UNVERIFIED — but `record_unverified` wrote it with `eprintln!`, and the outer test `isolated_child_cannot_inject_into_the_controlling_terminal` looks for the verdict only on stdout, so it failed as if no verdict were reported. The stdin, devnull-fd, and terminal-read probes already print their UNVERIFIED verdict on stdout. - Print the verdict with `println!`; the `$GITHUB_STEP_SUMMARY` line is still recorded, so an unmeasurable environment is still not a silent pass. 判断: the verdict line only moves from stderr to stdout; no expectation or other line changes. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…security-tools-os-tiocsti-dev-pts
This was referenced Oct 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概要
OS の隔離(landlock・Seatbelt)の中のコマンドが、次のことをできた問題を閉じます。
TIOCSTIで入力を注入する。/dev/ptsの他の端末へ書き込む。Refs #667
変更(develop 比 5 ファイル、+1384 −16、6 commit)
src/bounded_process.rs)pre_execのsetsid()で新しい session に入り、制御端末を持たなくなります。これまでのprocess_group(0)の代わりで、PID = PGID は保つので、終了処理(-pidだけに送る)は変わりません。TIOCSTIを呼んだ側の制御端末にしか許さないので、注入は拒否されます。pre_execで/dev/nullに付け替えます。setsidの後は job control による読み取りの拒否が効かないためです。端末でない stdin はそのままです。setsidが入るのはspawn_childの 1 箇所だけで、run_with_timeout*もそこを通ります。src/tools/os_sandbox/landlock.rs)IoctlDevの許可から/dev/pts・/dev/ttyを外しました。/dev/pts・/dev/tty・/dev/console・/dev/tty*を読み取りの拒否に加え、expand_read_allowの補集合から外しました。/devにはReadDirだけを与えます(ls /devは動き、端末の子はReadFileされません)。必要な ABI は ABI1 のままです。src/tools/os_sandbox/seatbelt.rs):profile の最後で/dev/tty*の書き込みと読み取りを再び拒否します(/dev/nullなどは今どおり使えます)。tests/issue502_os_sandbox_write.rs、試験が自分で作った pty だけを使います):/dev/nullの fd が残らないこと。reopen・setsid_reopen)が、隔離の中で ERR になること(隔離の外の正の対照では読める)。利用者に見える挙動の変化
/dev/ptmxは開けますが、slave(Linux の/dev/pts/*、macOS の/dev/ttys*)が拒否されるため、openpty・script・expect・pexpectは失敗します。[security][tools] OS の隔離の中から制御端末へ TIOCSTI で入力を注入でき、/dev/pts の他の端末へ書ける #667 の前は使えました。端末の読み書きを閉じるための代償で、SECURITY.md に書いています。/dev/ttyを直接開いてキーを待つ対話の経路(たとえば対話の pager)は、制御端末がないと見えるようになります。setsidが効く起動点は、bounded_process を通る 22 箇所です(製品コード。Bash、verifier、依存の導入、git の状態の確認、各 probe、dev server など)。いずれも stdin は null・pipe・継承のどれかで、出力を捕まえています。検証(head
56c67689)cargo-zigbuild clippy --all-targets --target x86_64-unknown-linux-gnu)ok。--include-ignored):write 29/29、read 10/10、network 1+9。--network none、mount なし):write 28/28、read 12/12、network 7/7。端末の試験(TIOCSTI・stdin・/dev/nullの fd・パスでの読み取り)はすべて BLOCKED、UNVERIFIED 0。legacy_tiocsti=0で、TIOCSTI の正の対照も EIO になるため、TIOCSTI の試験は測れず UNVERIFIED になります($GITHUB_STEP_SUMMARYに記録)。TIOCSTI の拒否は、Docker(legacy_tiocsti=1)と macOS の正の対照つきの BLOCKED で確かめています。/dev/nullの fd の試験は、変異を手で入れた状態で FAILED、HEAD で pass。569e85deで FAILED(隔離の中でREAD)、HEAD で pass(Docker・macOS の両方)。env -i・一時 HOME)と、Docker の中(uid 1000、--network none)だけで流しました。signalable_process_group・group_is_signalableは除外しています([tests][mutants] 変異試験が防御を外したまま kill(-1) などを実行し、利用者の全プロセスを止める #673)。configure_child_session・detach_terminal_stdin)12 本:caught 8、missed 4。missed 4 は、fork した子では失敗しないsetsid・dup2の失敗の経路と、等価なopenの判定で、受け入れました。profile2 本:caught 2。create_ruleset → Ok(Default)、OwnedFdにDefaultがない)。レビュー
EIO)。端末デバイスの読み取りの拒否と試験・文書で直しました。後続の論点
src/bin/gui_server/delegate.rsの、bounded_process を通らない子の stdin。issue458_finish_terminal_observation_and_transaction_matrixが失敗する件(権限による拒否が root では起きない。[security][tools] OS の隔離の中から制御端末へ TIOCSTI で入力を注入でき、/dev/pts の他の端末へ書ける #667 とは無関係)。legacy_tiocsti付き)の記録を一度確かめること。stat /dev/ttys*も拒否される点(記録のみ)。🤖 Generated with Claude Code