Skip to content

fix(runtime): node:zlib 58/58 — slab-buffer fake-GcHeader SIGBUS in brand probes#5044

Merged
proggeramlug merged 1 commit into
mainfrom
node-zlib-local-parity
Jun 12, 2026
Merged

fix(runtime): node:zlib 58/58 — slab-buffer fake-GcHeader SIGBUS in brand probes#5044
proggeramlug merged 1 commit into
mainfrom
node-zlib-local-parity

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

What

Raises node:zlib node-suite parity to 58/58 (100%), from 57/58 on current main.

Note: the tracked 70.7% (41/58, 17 crashes) snapshot was stale — those crashes were already fixed by earlier merges (e.g. #4996 zlib one-shot arity/levels). On a fresh origin/main build only one test still failed, and it was a real runtime crash.

The crash (zlib/unzip/auto-detect-deflate.ts, SIGBUS exit 138)

unzipSync(deflateSync(Buffer.from(input))) crashed for some inputs and not others — content-dependent.

Root cause is not in zlib at all:

  • Small Buffers (<256B) are allocated from the per-thread bump slab (buffer/header.rs), which carries no GcHeader.
  • String(buffer) / .toString() enters js_to_primitive, whose Temporal brand probe (is_temporal_cell_addr) runs before to_string's buffer-registry check and reads addr - GC_HEADER_SIZE as a GcHeader.
  • For a slab buffer that word is the previous slab entry's data bytes — i.e. whatever the previous compression produced. When those bytes happen to spell obj_type == GC_TYPE_TEMPORAL, the value is misrouted into the Temporal path, which dereferences the buffer's (length, capacity) words (0x2f0000002f for a 47-byte result) as a TemporalCell box pointer → SIGBUS.

Date/Map/Set/WeakRef brand probes share the same try_read_gc_header helper, so the same content-dependent misroute was latent for all of them.

Fix

Guard at the choke point: addr_class::try_read_gc_header now returns None for addresses inside small-buffer slab ranges (new is_small_buf_slab_addr, also reused by is_registered_buffer's existing fast path). One check covers every brand probe; large buffers are unaffected (they get real arena GcHeaders with GC_TYPE_BUFFER).

3 files, +26/−7. No Cargo.toml / CLAUDE.md / CHANGELOG edits (maintainer folds metadata at merge).

Verification (local macOS, node v26.3.0 oracle, print-and-diff)

suite before after
zlib 57/58 (crash) 58/58
buffer 134/134 134/134
string_decoder 36/36 36/36
util / object / events 339/348 batch identical — all 9 fails reproduce byte-for-byte on baseline (pre-existing: util deprecate/inspect/legacy-helpers, reflect-proxy-construct, events.on async-iterator)
cargo test -p perry-runtime only pre-existing flakes (date TZ flake + rotating parallel-state flakes; verified same set fails on clean baseline)

Zero regressions: every non-zlib failure was re-run against a baseline build with the fix reverted and failed identically there.

Small buffers (<256B) come from a per-thread bump slab with no GcHeader.
Brand probes (Temporal/Date/Map/Set) that read addr-8 as a GcHeader on a
slab buffer see the previous slab entry's data bytes — a content-dependent
fake header. Observed: String(buffer) on a zlib unzipSync result matched
GC_TYPE_TEMPORAL via js_to_primitive's Temporal probe (which runs before
to_string's buffer-registry check) and dereferenced the buffer's
(length,capacity) words as a TemporalCell Box pointer -> SIGBUS.

Guard at the choke point: try_read_gc_header returns None for addresses
inside small-buffer slab ranges, covering every brand probe at once.

node-suite zlib: 57/58 -> 58/58 (unzip/auto-detect-deflate crashed).
@proggeramlug proggeramlug merged commit 46c2a6c into main Jun 12, 2026
13 checks passed
@proggeramlug proggeramlug deleted the node-zlib-local-parity branch June 12, 2026 19:23
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