Skip to content

fix(runtime): Map/WeakMap.prototype.set reflective brand-check crash → TypeError (#4099) - #4113

Merged
proggeramlug merged 3 commits into
mainfrom
worktree-fix-4099-map-set-reflective
Jun 2, 2026
Merged

proggeramlug merged 3 commits into
mainfrom
worktree-fix-4099-map-set-reflective

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Fixes #4099. Map.prototype.set / WeakMap.prototype.set (and, it turns out, Map.prototype.values / Set.prototype.values) invoked reflectively on an invalid receiver crashed the process (SIGBUS, exit 138) instead of throwing the spec TypeError. The crash also masked the rest of the #3662 collection brand-check gap test, which aborted at line 26.

(Map.prototype.set as any).call(null, 1, 2);   // was SIGBUS — now throws TypeError

Root cause

It wasn't the thunk — it was that reading Map.prototype.set returned a raw, untagged non-pointer value (typeof Map.prototype.set === "number"), which the reflective .call dispatch then dereferenced as a pointer in a NaN-box-tag check, before any brand check ran.

An object's physical inline field capacity is max(field_count, 8) (js_object_alloc), and the read path (js_object_get_field) treats index < field_count as an inline slot and everything else as the overflow map. A builtin collection prototype has 9 string-keyed methods, so values (index 8) spills to the overflow map. Installing the Map.prototype.size accessor went through ensure_key_in_keys_array, which unconditionally bumped field_count to the new key's index + 1 (8 → 11). That pushed the inline/overflow boundary past the proto's 8 physical slots, reclassifying the overflowed values slot and the last inline set slot as inline reads that dereference past the allocation into adjacent-heap garbage.

Fix

In ensure_key_in_keys_array, only bump field_count when the new key lands in an in-bounds inline slot (new_index < max(field_count, 8)). A keys-only entry — a built-in accessor like Map.prototype.size, or a key whose data spilled to overflow — must not move the inline/overflow boundary. This mirrors the cap js_object_set_field_by_name already uses. One-line behavioral change; the rest of the diff is the explanatory comment.

Validation

  • Original repro now throws TypeError: Method Map.prototype.set called on incompatible receiver, byte-identical to node --experimental-strip-types.
  • test_gap_3662_collection_brand_check.ts passes byte-identical to Node and is no longer truncated.
  • Reflective set/add/values/get work on valid Map/WeakMap/Set receivers; typeof of every collection prototype method is now "function".
  • Data fields installed after a prototype accessor still read back correctly; Object.defineProperty accessor-interleaved-with-15-data-fields, TypedArray length/byteLength/byteOffset accessors, and Map/Set .size + enumeration all match Node byte-for-byte.
  • cargo test -p perry-runtime object → 87 passed, 0 failed. cargo fmt --all --check clean.
  • Confirmed no regression: the one unrelated gap-test diff in the sweep (array_iterator_manual_next) fails identically on a clean origin/main baseline build.

Ralph Küpper and others added 3 commits June 2, 2026 14:02
…s_array (#4099)

`Map.prototype.set` / `WeakMap.prototype.set` (and `Map.prototype.values`,
`Set.prototype.values`) invoked reflectively on an invalid receiver crashed
the process with SIGBUS instead of throwing the spec `TypeError`. The fault
was a type-confusion deref in `js_native_call_method`'s NaN-box-tag check,
reached before the thunk's brand check ever ran — because reading
`Map.prototype.set` returned a raw, untagged non-pointer value.

Root cause: an object's physical inline field capacity is `max(field_count, 8)`
(`js_object_alloc`), and the read path (`js_object_get_field`) treats
`index < field_count` as inline and the rest as the overflow map. A builtin
collection prototype has 9 string-keyed methods, so `values` spills to the
overflow map (index 8). Installing the `Map.prototype.size` accessor then went
through `ensure_key_in_keys_array`, which unconditionally bumped `field_count`
to the new key's index + 1 (8 → 11). That pushed the boundary past the proto's
8 physical slots, reclassifying the overflowed `values` slot and the last
inline `set` slot as inline reads that dereference past the allocation into
adjacent-heap garbage.

Fix: in `ensure_key_in_keys_array`, only bump `field_count` when the new key
lands in an in-bounds inline slot (`new_index < max(field_count, 8)`). A
keys-only entry — a built-in accessor, or a key whose data spilled to overflow
— must not move the inline/overflow boundary. This mirrors the cap already
used by `js_object_set_field_by_name`.

Verified: the original repro now throws `TypeError` (byte-identical to Node),
the #3662 collection brand-check gap test passes byte-identical and is no
longer truncated, reflective set/add/values/get work on valid receivers, and
data fields installed after a prototype accessor still read back correctly.
@proggeramlug
proggeramlug merged commit cad3cf0 into main Jun 2, 2026
11 checks passed
@proggeramlug
proggeramlug deleted the worktree-fix-4099-map-set-reflective branch June 2, 2026 12:34
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.

runtime: Map/WeakMap.prototype.set reflective on invalid receiver crashes (SIGBUS) instead of TypeError (#3662)

1 participant