fix(post-install): sanitize child output on the native interpreter path - #793
Open
rustytrees wants to merge 1 commit into
Open
fix(post-install): sanitize child output on the native interpreter path#793rustytrees wants to merge 1 commit into
rustytrees wants to merge 1 commit into
Conversation
`ui/term_sanitize.zig` says it exists so "a hostile formula cannot rewrite scrollback or exfiltrate via terminal extensions", and it is well built — allowlist-based, dropping OSC (including OSC 52 clipboard writes), DCS, absolute cursor positioning, and scrollback erase. It was only wired into `runRubySandboxed`, i.e. the `--use-system-ruby` path. The native interpreter — the default, and the feature the README leads with — spawned children with stdout inherited, so a formula's `system` call wrote straight to the terminal and none of that filtering applied. The module's own doc comment overstated its coverage. Both stdout and stderr now go through the same pump, reusing the ruby path's `filterInto` rather than growing a second copy of the loop. Two threads, not an inline drain, so a chatty child cannot deadlock by filling one pipe while we block on the other; `std.process.spawn`'s `wait` owns the pipe fds, which is why the non-closing variant was split out. Trade-off: piping means the child no longer sees a TTY, so colour-on-TTY heuristics turn themselves off. That is the same trade the ruby path already makes, and `MALT_ALLOW_RAW_POST_INSTALL=1` continues to opt out of both. `--json`/`--ndjson` suppression still takes precedence. The regression test captures this process's real fd 1 and asserts an OSC 52 sequence never lands on it while the surrounding text survives; it fails against the previous `.inherit` behaviour.
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.
Fourth slice of #789.
ui/term_sanitize.zigis genuinely good work — allowlist-based, dropping OSC (including OSC 52 clipboard writes), DCS, SOS/PM/APC, 8-bit C1 introducers, absolute cursor positioning, and scrollback erase. Better than most tools ship.Its module doc says it exists so "a hostile formula cannot rewrite scrollback or exfiltrate via terminal extensions". It was only wired into
runRubySandboxed— the--use-system-rubypath. The native interpreter, which is the default and the feature the README leads with, spawned children with stdout inherited, so a formula'ssystemcall wrote straight to the terminal with none of that filtering applied. The doc comment overstated the coverage.Both stdout and stderr now go through the same pump. It reuses the ruby path's loop rather than growing a second copy:
filterLoopwas split into a non-closingfilterInto, becausestd.process.spawn'swaitowns the pipe fds it handed out and closing them in the pump too would free an fd number a lateropencould already have reused.Two threads rather than an inline drain, so a chatty child cannot deadlock by filling one pipe while we block on the other. The pumps see EOF when the child's write ends close, so joining before
waitcannot hang on a live child.Trade-off
Piping means the child no longer sees a TTY, so colour-on-TTY heuristics turn themselves off. That is the same trade the
--use-system-rubypath already makes, andMALT_ALLOW_RAW_POST_INSTALL=1continues to opt out of both.--json/--ndjsonstdout suppression still takes precedence over everything.If you'd rather keep the native path raw by default and make sanitizing opt-in, that's a one-line flip of
childStdioMode— say the word. I went with sanitize-by-default because it matches what the module already claims and what the other path already does.Verification
The regression test dups this process's real fd 1 to a file, runs the builtin against a payload that carries an OSC 52 sequence, restores the fd, and asserts the escape never landed while the surrounding text survived. I checked it fails against the previous
.inheritbehaviour rather than passing vacuously — it trips on exactly the\x1b]52assertion.zig build test— full suite green./scripts/lint-spawn-invariants.sh— clean./scripts/smokes/smoke_security.sh— 8/8