fix(runtime): SIGPIPE and non-UTF-8 argv (from #9418) - #9419
Merged
Conversation
added 2 commits
September 1, 2026 18:36
… program (#9402) `claude auto-mode defaults | head -2` exited 141 (128 + SIGPIPE) under Perry and 0 under Node, deterministically. Every pipeline that stops reading early hit it: `| head`, `| grep -q`, `| less` then `q`, a client that closed its socket. A Perry program has its own C `main`, emitted by codegen, so it never runs Rust's `std::rt` startup — and that startup is where an ordinary Rust binary gets SIGPIPE set to SIG_IGN. The compiled program therefore inherited the signal's default disposition and died mid-write, with no JS-visible event and nothing to catch. Node ignores the signal and lets the failing write(2) return EPIPE to the writer instead. `ignore_sigpipe_at_startup()` installs SIG_IGN once per process and only over SIG_DFL, so an embedder's own disposition and a later `process.on('SIGPIPE')` are both untouched. It is called from `js_gc_init`, the first runtime call of every `main` / `perry_module_init`, so every compiled program gets it before a byte can be written. Unix only: Windows has no SIGPIPE. Ignoring the signal alone would have traded exit 141 for exit 134 — `std`'s `println!` turns the resulting EPIPE into a panic and Perry builds with `panic = "abort"`. Node's console is specified never to throw, so the `console.*` family's print macros are shadowed with writers that drop the write error. The shadowing is confined to the `builtins` tree, alongside the existing harmonyos hilog override; diagnostics elsewhere keep `std`'s macros. `fs.writeSync(1, …)` to a closed pipe now throws EPIPE, matching Node — write errors still reach JavaScript rather than being swallowed. test-files/test_gap_9402_sigpipe_truncating_consumer.ts re-runs itself through bash, pipes 50000 lines into `head -2` and reports the WRITER's status. Byte-compared against node 26.5.1: node `writer-status=0`; a compiler built from unfixed origin/main reports `writer-status=141`; with this change, identical to node.
`claude -p $'\xff\xfe\x80abc\xc3\x28'` died with SIGABRT and a raw Rust
backtrace — "panicked at library/std/src/env.rs:878:51: called
`Result::unwrap()` on an `Err` value" — where Node prints the program's own
output. `std::env::args()` panics on an argument that is not valid Unicode, and
non-UTF-8 filenames are ordinary on Linux, so anything that passes a path
through reached it.
Node decodes argv leniently: every invalid byte becomes U+FFFD. Verified
against node 26.5.1 — `$'\xff\xfe\x80abc\xc3\x28'` arrives as the eight code
points fffd fffd fffd 61 62 63 fffd 28, byte-for-byte `String::from_utf8_lossy`.
One `process_args_lossy()` over `std::env::args_os()` now backs every argv
reader in the runtime, so a single bad byte cannot resurrect the abort in a
path nobody thought to check. There were NINE, all reachable, and the panic was
not confined to `process.argv`:
- os.rs `js_process_argv` — `process.argv`
- node_submodules/trace_events.rs — reads argv from `js_gc_init`, so the
process died before a line of JavaScript ran, whatever the program did
- process/permission.rs (x3) — the permission-model flag scan
- process/report.rs (x2) — `process.report`
- process/attributes.rs — `process.title`
- cluster.rs (x2) — cluster exec-path defaulting
- child_process/options.rs — self-launch detection in `spawn`
- process.rs `process_argv0_string` — `process.argv0` / `execPath`
Three more outside the runtime, same shape, same fix: perry-stdlib and
perry-ext-commander (`program.parse()` with no explicit argv), and the compiler
CLI's own arguments in perry/src/{main,update_policy}.rs, so `perry compile` on
a non-UTF-8 path reports a diagnostic instead of a backtrace.
`std::env::var()` needs no equivalent change: it returns Err for a non-Unicode
value rather than panicking, and the runtime has no `env::var(..).unwrap()`.
test-files/test_gap_9401_non_utf8_argv.ts re-runs itself through `sh` (which is
byte-oriented, so it can build an argument the source file cannot contain) and
prints the decoded length, code points and UTF-8 bytes. Byte-compared against
node 26.5.1: a compiler built from unfixed origin/main reports
`child-status: null / child-signal: SIGABRT`; with this change, identical to
node.
Not touched, same shape, reported rather than changed: perry-ui-gtk4
src/tray.rs, perry-ui-macos src/app.rs, perry-ui src/bin/styling-matrix.rs.
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.
Lands two of #9418's three commits. The third (#9394, sloppy-mode array store strictness) is held back on the raw-handle ratchet — details on #9418.
claude … | headexits 141 and dies mid-write #9402 — ignore SIGPIPE so a truncating consumer cannot kill the program.Both are separable commits touching different files from the strictness fix, so splitting them costs nothing and unblocks two real defects now.
Verified the SIGPIPE fix behaves as claimed, rather than only that it builds: a 200k-line writer piped into
head -3exits 0 under Perry, matching node. That is the case where SIGPIPE previously killed the process.Validation:
perry-runtime8 suites green underRUST_TEST_THREADS=1; release build clean with no warnings; file-size, raw-handle (bare and--no-raise-vs), root-holder, thread-local, addr-class, census and fmt gates all pass.