Test-only, but it corrupts the heap silently and presents as a SIGSEGV somewhere unrelated. Found while building dictionary mode (PR #10938).
The bug
crates/perry-runtime/src/gc/tests/support.rs:869 (and its old-generation twin at :838):
let payload = std::mem::size_of::<crate::object::ObjectHeader>() + field_count as usize * 8;
let obj = crate::arena::arena_alloc_gc(payload, 8, GC_TYPE_OBJECT) as *mut ObjectHeader;
With field_count == 0 the allocation is exactly the header — no inline slots — and parent_class_id is deliberately left at 0 (unstamped), which the comment above it explains:
A zero-slot fixture needs no descriptor at all — the derived bound is 0 either way — and minting one would perturb the descriptor-count accounting that sibling tests assert on.
That is fine for what the fixture has been used for. Every existing caller only ever sets a [[Prototype]] on it. Nobody had written a named property to one.
A named-property write does not respect "the derived bound is 0". The inline/overflow boundary is
alloc_limit = max(object_live_slot_count(obj), INLINE_SLOT_FLOOR) // floor is 2
so the first two keys store into inline slots 0 and 1 — of an object that has none. Those two words are the next cell.
Repro
let (owner, _) = unsafe { alloc_nursery_test_object(0) };
for i in 0..6 {
let name = format!("k{i:02}");
let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32);
crate::object::js_object_set_field_by_name(owner, key, i as f64);
}
// reads come back wrong; the next collection SIGSEGVs
It presented for me as a SIGSEGV inside a URLSearchParams shape probe reached from an ordinary by-name read — nowhere near the fixture.
Suggested fix
Either is fine; the point is that the failure should not be silent.
- Allocate
max(field_count, INLINE_SLOT_FLOOR) slots, and keep publishing the bound as field_count so the descriptor-count accounting the comment protects is unchanged; or
debug_assert! in the named-store path that the write is within the allocation, so the fixture fails loudly at the store rather than at an unrelated read; or
- document on the fixture that it is prototype-only and that a named write is UB — the cheapest, and it at least stops the next person.
Workaround in the meantime, used by test_object_meta_dictionary_keys_survive_copied_minor_move (PR #10938): pass a nonzero field_count.
Test-only, but it corrupts the heap silently and presents as a SIGSEGV somewhere unrelated. Found while building dictionary mode (PR #10938).
The bug
crates/perry-runtime/src/gc/tests/support.rs:869(and its old-generation twin at:838):With
field_count == 0the allocation is exactly the header — no inline slots — andparent_class_idis deliberately left at 0 (unstamped), which the comment above it explains:That is fine for what the fixture has been used for. Every existing caller only ever sets a
[[Prototype]]on it. Nobody had written a named property to one.A named-property write does not respect "the derived bound is 0". The inline/overflow boundary is
so the first two keys store into inline slots 0 and 1 — of an object that has none. Those two words are the next cell.
Repro
It presented for me as a SIGSEGV inside a URLSearchParams shape probe reached from an ordinary by-name read — nowhere near the fixture.
Suggested fix
Either is fine; the point is that the failure should not be silent.
max(field_count, INLINE_SLOT_FLOOR)slots, and keep publishing the bound asfield_countso the descriptor-count accounting the comment protects is unchanged; ordebug_assert!in the named-store path that the write is within the allocation, so the fixture fails loudly at the store rather than at an unrelated read; orWorkaround in the meantime, used by
test_object_meta_dictionary_keys_survive_copied_minor_move(PR #10938): pass a nonzerofield_count.