Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions changelog.d/7379-assign-regexp-source.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**Fixed** `Object.assign({}, /re/)` and `{ ...\/re\/ }` type-confused the RegExp
source as a plain object, reading `ObjectHeader.keys_array` out of a
`RegExpHeader`'s fields and calling `js_array_length` on the resulting garbage
pointer.

`js_object_assign_one` skips exotic sources (Map/Set/Promise/Date/WeakMap) whose
header layout is not an `ObjectHeader`, and classifies them by GC type. A RegExp
is allocated as `gc_malloc(GC_TYPE_OBJECT)`, so it passes the `== GC_TYPE_OBJECT`
test that excludes the others and falls into the plain-object key walk. The slot
that lands at `ObjectHeader.keys_array`'s offset in a `RegExpHeader` is not a
keys array, so `js_array_length` reads a `GcHeader` at `garbage - 8`.

Unprotected this silently walks unrelated memory and usually still prints the
right answer; under `#7341`'s from-space quarantine the address is a retired
protected page and the process takes SIGBUS. This is 1 of the 8 catches that
remained open after #7373-#7376, and the only one whose root cause was a type
confusion rather than a rooting-order defect.

Per CopyDataProperties a RegExp exposes no own enumerable string keys through
this path, so skipping it matches Node: `Object.assign({}, /x/g)` is `{}`.
Verified with `test_gap_object_assign_collection`, which is now byte-identical
to Node **under quarantine**; 40 regex/assign/spread/object gap tests and 43
regex unit tests unchanged.
23 changes: 23 additions & 0 deletions crates/perry-runtime/src/object/alloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1468,6 +1468,29 @@ pub unsafe extern "C" fn js_object_assign_one(target_f64: f64, source_f64: f64)
};
let source_is_array = source_obj_type == crate::gc::GC_TYPE_ARRAY;

// #7341: a RegExp source must be skipped here, and the exotic guard above
// cannot do it. That guard classifies by GC type, and a RegExp is literally
// `gc_malloc(GC_TYPE_OBJECT)` (see `regex.rs`) — so unlike Map/Set/Date it
// passes `== GC_TYPE_OBJECT` and falls into the plain-object arm, where
// `(*src).keys_array` reads a `RegExpHeader` at `ObjectHeader`'s field
// offset. That is a type confusion: the slot it lands on is not a keys
// array, and `js_array_length` then reads a GcHeader at `garbage - 8`.
// Under from-space quarantine that address is a retired protected page and
// the process dies; unprotected it silently walks unrelated memory.
//
// Per CopyDataProperties a RegExp exposes no own enumerable string keys
// through this path (`source`/`flags`/`lastIndex` are prototype accessors
// or non-enumerable), so skipping contributes nothing and matches Node:
// `Object.assign({}, /x/g)` is `{}`. Any own expandos a user attached live
// in the exotic-expando side table, which this raw walk never read anyway.
//
// `is_regex_pointer` is the bounds-checked magic probe, safe on arbitrary
// payloads. Repro: `Object.assign({}, /x/g)` under
// PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_HEAP_LIMIT=8.
if crate::regex::is_regex_pointer(src_raw as *const u8) {
return target_f64;
}

// #7200: EVERYTHING BELOW RUNS WITH USER CODE IN THE WINDOW.
//
// Both copy loops reach a `[[Get]]` that short-circuits into
Expand Down
Loading