Repository navigation
fix(runtime): Map/WeakMap.prototype.set reflective brand-check crash → TypeError (#4099) - #4113
Merged
Merged
Conversation
…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.
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.
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 specTypeError. The crash also masked the rest of the#3662collection brand-check gap test, which aborted at line 26.Root cause
It wasn't the thunk — it was that reading
Map.prototype.setreturned a raw, untagged non-pointer value (typeof Map.prototype.set === "number"), which the reflective.calldispatch 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) treatsindex < field_countas an inline slot and everything else as the overflow map. A builtin collection prototype has 9 string-keyed methods, sovalues(index 8) spills to the overflow map. Installing theMap.prototype.sizeaccessor went throughensure_key_in_keys_array, which unconditionally bumpedfield_countto the new key's index + 1 (8 → 11). That pushed the inline/overflow boundary past the proto's 8 physical slots, reclassifying the overflowedvaluesslot and the last inlinesetslot as inline reads that dereference past the allocation into adjacent-heap garbage.Fix
In
ensure_key_in_keys_array, only bumpfield_countwhen the new key lands in an in-bounds inline slot (new_index < max(field_count, 8)). A keys-only entry — a built-in accessor likeMap.prototype.size, or a key whose data spilled to overflow — must not move the inline/overflow boundary. This mirrors the capjs_object_set_field_by_namealready uses. One-line behavioral change; the rest of the diff is the explanatory comment.Validation
TypeError: Method Map.prototype.set called on incompatible receiver, byte-identical tonode --experimental-strip-types.test_gap_3662_collection_brand_check.tspasses byte-identical to Node and is no longer truncated.set/add/values/getwork on validMap/WeakMap/Setreceivers;typeofof every collection prototype method is now"function".Object.definePropertyaccessor-interleaved-with-15-data-fields, TypedArraylength/byteLength/byteOffsetaccessors, andMap/Set.size+ enumeration all match Node byte-for-byte.cargo test -p perry-runtime object→ 87 passed, 0 failed.cargo fmt --all --checkclean.array_iterator_manual_next) fails identically on a clean origin/main baseline build.