Skip to content
Closed
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
1 change: 1 addition & 0 deletions crates/perry-runtime/src/gc/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ mod smoke;
mod start_bitmap;
mod step_bounds;
pub(super) mod support;
mod zero_slot_fixture;
mod survival_diag;
mod sweep_described_runs;
mod sweep_hole_rebuild;
Expand Down
30 changes: 26 additions & 4 deletions crates/perry-runtime/src/gc/tests/support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -852,15 +852,26 @@ pub(super) unsafe fn alloc_old_test_object(
crate::object::shapes::shape_descriptor_ensure(std::ptr::null(), 0, field_count)
.expect("shape id range exhausted in a test fixture")
};
let payload = std::mem::size_of::<crate::object::ObjectHeader>() + field_count as usize * 8;
// #10941: a named-property write does not respect "the derived bound is
// 0". The inline/overflow boundary is
// `max(object_live_slot_count(obj), INLINE_SLOT_FLOOR)` with a floor of 2,
// so the first two keys written to a zero-slot fixture store into inline
// slots 0 and 1 of an object that has none — those two words are THE NEXT
// CELL. It presents as a wrong read now and a SIGSEGV somewhere unrelated
// during the next collection. Allocate to the floor; the PUBLISHED bound
// stays `field_count`, so the collector still traces exactly `field_count`
// slots and the descriptor-count accounting sibling tests assert on is
// unchanged.
let allocated_slots = std::cmp::max(field_count as usize, crate::object::INLINE_SLOT_FLOOR);
let payload = std::mem::size_of::<crate::object::ObjectHeader>() + allocated_slots * 8;
let obj = crate::arena::arena_alloc_gc_old(payload, 8, GC_TYPE_OBJECT)
as *mut crate::object::ObjectHeader;
(*obj).class_id = 0;
(*obj).parent_class_id = shape_id;
(*obj).meta = std::ptr::null_mut();
let fields =
(obj as *mut u8).add(std::mem::size_of::<crate::object::ObjectHeader>()) as *mut u64;
for i in 0..field_count as usize {
for i in 0..allocated_slots {
*fields.add(i) = 0;
}
(obj, fields)
Expand All @@ -880,15 +891,26 @@ pub(super) unsafe fn alloc_nursery_test_object(
crate::object::shapes::shape_descriptor_ensure(std::ptr::null(), 0, field_count)
.expect("shape id range exhausted in a test fixture")
};
let payload = std::mem::size_of::<crate::object::ObjectHeader>() + field_count as usize * 8;
// #10941: a named-property write does not respect "the derived bound is
// 0". The inline/overflow boundary is
// `max(object_live_slot_count(obj), INLINE_SLOT_FLOOR)` with a floor of 2,
// so the first two keys written to a zero-slot fixture store into inline
// slots 0 and 1 of an object that has none — those two words are THE NEXT
// CELL. It presents as a wrong read now and a SIGSEGV somewhere unrelated
// during the next collection. Allocate to the floor; the PUBLISHED bound
// stays `field_count`, so the collector still traces exactly `field_count`
// slots and the descriptor-count accounting sibling tests assert on is
// unchanged.
let allocated_slots = std::cmp::max(field_count as usize, crate::object::INLINE_SLOT_FLOOR);
let payload = std::mem::size_of::<crate::object::ObjectHeader>() + allocated_slots * 8;
let obj = crate::arena::arena_alloc_gc(payload, 8, GC_TYPE_OBJECT)
as *mut crate::object::ObjectHeader;
(*obj).class_id = 0;
(*obj).parent_class_id = shape_id;
(*obj).meta = std::ptr::null_mut();
let fields =
(obj as *mut u8).add(std::mem::size_of::<crate::object::ObjectHeader>()) as *mut u64;
for i in 0..field_count as usize {
for i in 0..allocated_slots {
*fields.add(i) = 0;
}
(obj, fields)
Expand Down
66 changes: 66 additions & 0 deletions crates/perry-runtime/src/gc/tests/zero_slot_fixture.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//! #10941: a zero-slot test fixture must still have room for a named store.
//!
//! `alloc_{nursery,old}_test_object(0)` allocated exactly an `ObjectHeader`
//! and left the receiver unstamped, on the reasoning that "the derived bound
//! is 0 either way". A named-property write does not respect that bound: the
//! inline/overflow boundary is
//! `max(object_live_slot_count(obj), INLINE_SLOT_FLOOR)` and the floor is 2,
//! so the first two keys stored into a zero-slot fixture land in inline slots
//! 0 and 1 of an object that has none — THE NEXT CELL.
//!
//! Every caller before PR #10938 only ever set a `[[Prototype]]` on one, so
//! nothing had written a named property to one and the hazard was invisible.
//! It presents as a wrong read now and a SIGSEGV inside an unrelated
//! collection later, which is why this is pinned structurally: the assertions
//! below are about the ALLOCATION, and they cannot pass vacuously.
//!
//! An end-to-end pin — write six named properties to a zero-slot fixture and
//! read them back — was written and then deliberately dropped: without the
//! fix it does not fail, it DUMPS CORE, which under `--test-threads=1` takes
//! the other ~4,200 results in the process with it. The structural assertions
//! redden with a message instead, and they redden for the same reason.

use super::support::{alloc_nursery_test_object, alloc_old_test_object};

unsafe fn payload_bytes(obj: *mut crate::object::ObjectHeader) -> usize {
let header = (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader;
(*header).size as usize - crate::gc::GC_HEADER_SIZE
}

fn floor_bytes() -> usize {
std::mem::size_of::<crate::object::ObjectHeader>() + crate::object::INLINE_SLOT_FLOOR * 8
}

#[test]
fn a_zero_slot_nursery_fixture_has_room_for_the_named_store_floor() {
let _global = crate::gc::global_side_table_test_lock();
unsafe {
let (obj, _) = alloc_nursery_test_object(0);
let payload = payload_bytes(obj);
assert!(
payload >= floor_bytes(),
"a named-property write on this fixture stores into inline slots 0 \
and 1 — the store path's floor is \
`max(object_live_slot_count(obj), INLINE_SLOT_FLOOR)` and never \
zero — but the allocation carries {payload} payload bytes against \
the {} it would need. Those words are the NEXT CELL (#10941).",
floor_bytes()
);
}
}

#[test]
fn a_zero_slot_old_fixture_has_room_for_the_named_store_floor() {
let _global = crate::gc::global_side_table_test_lock();
unsafe {
let (obj, _) = alloc_old_test_object(0);
let payload = payload_bytes(obj);
assert!(
payload >= floor_bytes(),
"the old-generation twin of the nursery fixture has the same hole: \
{payload} payload bytes against the {} a named store needs \
(#10941).",
floor_bytes()
);
}
}
Loading