Skip to content

fix(runtime): SIGPIPE and non-UTF-8 argv (from #9418) - #9419

Merged
proggeramlug merged 2 commits into
mainfrom
fix/9418-sigpipe-argv
Sep 1, 2026
Merged

fix(runtime): SIGPIPE and non-UTF-8 argv (from #9418)#9419
proggeramlug merged 2 commits into
mainfrom
fix/9418-sigpipe-argv

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

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.

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 -3 exits 0 under Perry, matching node. That is the case where SIGPIPE previously killed the process.

Validation: perry-runtime 8 suites green under RUST_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.

Ralph Küpper 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.
@proggeramlug
proggeramlug merged commit 367f9aa into main Sep 1, 2026
16 checks passed
@proggeramlug
proggeramlug deleted the fix/9418-sigpipe-argv branch September 1, 2026 16:50
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