You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
An exhaustive audit of the if tag == A || tag == B { … } else { treat as C } fall-through shape (run for PR #9461) found six more live sites. Each was reproduced, not inferred. They are one family: a TAG_HOLE that escapes translation at its source, plus downstream ladders with no hole arm.
The leak source (fix this first)
array/indexing.rs:549 — the Set/Map arm of js_array_get_f64 returns a raw slot with no hole translation, unlike the array arm at :440. This is what makes the downstream sites reachable at all.
Downstream ladders with no hole arm
builtins/arithmetic.rs:778 — classify_value_typeof has no TAG_HOLE arm: typeof hole === "number" (node: "undefined").
value/to_string.rs:1357 — js_jsvalue_to_string renders a hole as "NaN" (node: "undefined").
builtins/table.rs:444,456 — console.table([[1,,3]]) renders the hole cell as NaN. The sibling primitives-only branch at :467 already has the TAG_HOLE skip; the two array-of-arrays branches were missed. Note a cell-only fix still will not match node — node omits the hole's column entirely, so the header derivation needs the same treatment.
builtins/table.rs:546 — const o={a:1,b:2}; delete o.a; console.table(o) prints b | NaN. Different mechanism: object_key_names drops tombstoned keys from a Vecwithout preserving slot indices, then indexes fields by compacted position. format_object_as_json and both console.rs sites iterate 0..key_count and continue on a non-string key — table.rs is the outlier.
Why this family keeps producing bugs
Tag ambiguity in perry is by design (0x7FFC is both undefined and TAG_HOLE; 0x7FFE both INT32 and class-ref), and tombstone deletes (#9331, default-on) made TAG_HOLE common in live data. Every value-decoding ladder written before that is a candidate. This family has now produced #9398 (SIGSEGV), #9415 (NaN holes / integer classes), #9409 (split("")), and these six.
Fix the leak source first, then check which downstream symptoms become unreachable — the typeof/String sites may still be reachable through other hole producers, so each needs its own repro: a .ts fixture byte-compared to node --experimental-strip-types, demonstrated failing on unfixed origin/main, covering typeof, String(), template interpolation, console.table on holey rows and tombstoned objects, and a Map-typed function parameter that keeps its fast path after .delete() (assert via the deopt counter, not timing).
An exhaustive audit of the
if tag == A || tag == B { … } else { treat as C }fall-through shape (run for PR #9461) found six more live sites. Each was reproduced, not inferred. They are one family: aTAG_HOLEthat escapes translation at its source, plus downstream ladders with no hole arm.The leak source (fix this first)
array/indexing.rs:549— the Set/Map arm ofjs_array_get_f64returns a raw slot with no hole translation, unlike the array arm at:440. This is what makes the downstream sites reachable at all.Downstream ladders with no hole arm
builtins/arithmetic.rs:778—classify_value_typeofhas noTAG_HOLEarm:typeof hole === "number"(node:"undefined").value/to_string.rs:1357—js_jsvalue_to_stringrenders a hole as"NaN"(node:"undefined").param_type_guard.rs:658,684— theOP_MAP/OP_SETarms have the samesize-vs-usedbound PR fix(runtime): inspect decodes classes, holes and promise state (#9415); Array.from(str) keeps lone surrogates (#9431) #9461 fixed in the formatter, and no hole arm, whereOP_ARRAY/OP_TUPLEare correct. Not unsafe — it silently deopts a legitimateMap<string,number>parameter after any.delete(), which is a performance leak from a correctness-shaped hole.console.table (two distinct mechanisms)
builtins/table.rs:444,456—console.table([[1,,3]])renders the hole cell asNaN. The sibling primitives-only branch at:467already has theTAG_HOLEskip; the two array-of-arrays branches were missed. Note a cell-only fix still will not match node — node omits the hole's column entirely, so the header derivation needs the same treatment.builtins/table.rs:546—const o={a:1,b:2}; delete o.a; console.table(o)printsb | NaN. Different mechanism:object_key_namesdrops tombstoned keys from aVecwithout preserving slot indices, then indexes fields by compacted position.format_object_as_jsonand bothconsole.rssites iterate0..key_countandcontinueon a non-string key —table.rsis the outlier.Why this family keeps producing bugs
Tag ambiguity in perry is by design (
0x7FFCis both undefined andTAG_HOLE;0x7FFEboth INT32 and class-ref), and tombstone deletes (#9331, default-on) madeTAG_HOLEcommon in live data. Every value-decoding ladder written before that is a candidate. This family has now produced #9398 (SIGSEGV), #9415 (NaN holes / integer classes), #9409 (split("")), and these six.Verified clean — do not re-audit
value/truthy.rs(hole → falsy),json/stringify.rs(hole →null, correct per §25.5.2),child_process/v8_serde.rs, structured clone,thread.rs,napi_typeof, array sort/join/iter paths.Verification bar
Fix the leak source first, then check which downstream symptoms become unreachable — the
typeof/Stringsites may still be reachable through other hole producers, so each needs its own repro: a.tsfixture byte-compared tonode --experimental-strip-types, demonstrated failing on unfixedorigin/main, coveringtypeof,String(), template interpolation,console.tableon holey rows and tombstoned objects, and aMap-typed function parameter that keeps its fast path after.delete()(assert via the deopt counter, not timing).Found during the #9415 audit (PR #9461).