Skip to content

fix(post-install): sanitize child output on the native interpreter path - #793

Open
rustytrees wants to merge 1 commit into
indaco:mainfrom
rustytrees:fix/sanitize-native-post-install-output
Open

fix(post-install): sanitize child output on the native interpreter path#793
rustytrees wants to merge 1 commit into
indaco:mainfrom
rustytrees:fix/sanitize-native-post-install-output

Conversation

@rustytrees

Copy link
Copy Markdown

Fourth slice of #789.

ui/term_sanitize.zig is 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-ruby path. The native interpreter, which is 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 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: filterLoop was split into a non-closing filterInto, because std.process.spawn's wait owns the pipe fds it handed out and closing them in the pump too would free an fd number a later open could 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 wait cannot 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-ruby path already makes, and MALT_ALLOW_RAW_POST_INSTALL=1 continues to opt out of both. --json/--ndjson stdout 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 .inherit behaviour rather than passing vacuously — it trips on exactly the \x1b]52 assertion.

  • zig build test — full suite green
  • ./scripts/lint-spawn-invariants.sh — clean
  • ./scripts/smokes/smoke_security.sh — 8/8

`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.
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