Skip to content

[P0] Object.assign / object-spread of a Map or Set crashes (type confusion → SIGBUS) #6070

Closed

Description

@proggeramlug

Severity: P0 (crash / memory-unsafe on a common operation)
Found by: audit fable-audit-perry-2.md (2026-07-06), confirmed by probe (exit 138 / EXC_BAD_ACCESS).

What

Object.assign({}, someMap) and first-position object-spread {...someMap} (and the Set equivalents) crash with a SIGBUS/SIGSEGV instead of producing {}.

Object.assign({}, new Map([["a", 1]]));   // node: {}   perry: exit 138 (SIGBUS)
({ ...new Map([["a", 1]]) });             // node: {}   perry: exit 138
Object.assign({}, new Set([1, 2, 3]));    // node: {}   perry: exit 138

Spec (CopyDataProperties / [[OwnPropertyKeys]]): a Map/Set has no own enumerable string keys, so the result is {}.

Why

js_object_assign_one (crates/perry-runtime/src/object/alloc.rs:1194-1247) validates the source only as pointer/aligned/not-array, then does let src = src_raw as *const ObjectHeader; … (*src).keys_array. A MapHeader is {size@0, capacity@4, entries@8} (16 bytes) — reading keys_array at offset 16 walks past the header into adjacent arena bytes; the garbage pointer then passes the >= 0x10000 floor and js_array_length derefs it.

js_object_clone_with_extra (the {...src, k:v} first-source path, alloc.rs:634-739) is worse: it still uses the pre-audit top16 >= 0x7FF8 catch-all strip + src_raw < 0x10000 floor and reads (*src).field_count (offset 12 = high half of MapHeader.entries) → copies thousands of phantom fields.

This is a straggler the 2026-07-02 band-safety sweep missed: js_object_copy_own_fields (2nd+ spread source) was hardened; assign_one's non-array branch and the first-spread clone were not.

Fix

In both functions, classify the source via crate::value::addr_class::try_read_gc_header and require GC_TYPE_OBJECT (arrays are already special-cased; strings still owe their index props — pre-existing gap, but no longer UB). This is an exact mirror of the already-fixed js_object_copy_own_fields (alloc.rs:746-800):

match crate::value::addr_class::try_read_gc_header(src_raw) {
    Some(h) if h.obj_type == crate::gc::GC_TYPE_OBJECT => {}
    _ => return, // Map/Set/Promise/handle/etc. contribute no own enumerable string keys
}

Add a gap test (Object.assign + spread of Map/Set/Date/Promise) and confirm the test262 assign/* suite still passes.

Confidence: high (probe + source + exact fixed-twin).

Activity

  1. added 2 commits that reference this issue on Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions