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
5 changes: 5 additions & 0 deletions changelog.d/9283-preinstalled-shape-facts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Fixed

- Validate the logical key count before birth-stamping a preinstalled object
shape, falling back to an exact descriptor when module keys and ShapeId facts
diverge.
6 changes: 3 additions & 3 deletions crates/perry-runtime/src/object/alloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,9 @@ fn object_alloc_class_inline_keys_impl(
(*ptr).meta = ptr::null_mut();
// The compiled entry point passes the ShapeId installed beside this
// canonical keys global at module initialization. Reuse that immutable
// descriptor directly when its live-slot bound still matches; learned
// instance widening and worker-local first installs retain the exact
// mint-and-validate fallback.
// descriptor directly when its keys facts and live-slot bound still
// match; learned instance widening, key-count drift, and worker-local
// first installs retain the exact mint-and-validate fallback.
let used_preinstalled_shape = preinstalled_shape_id != 0
&& crate::object::shapes::try_birth_stamp_preinstalled_shape(
ptr,
Expand Down
18 changes: 12 additions & 6 deletions crates/perry-runtime/src/object/shapes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1064,12 +1064,12 @@ pub(crate) unsafe fn birth_stamp_object_shape(
/// Stamp a newborn compiled-class allocation from the ShapeId installed at
/// module initialization, without re-canonicalizing the same shape facts.
///
/// A hit proves the immutable ordered-keys edge and live inline-slot bound
/// directly from the agent-local descriptor. Canonical class keys never mutate
/// in place: structural growth forks a new keys array and mints a new ShapeId,
/// so exact `(ShapeId, keys pointer)` identity also carries the descriptor's
/// logical key count. Missing worker-local ids and learned-width mismatches
/// return `false` for the existing mint-and-validate path to handle.
/// A hit proves the immutable ordered-keys edge, logical key count, and live
/// inline-slot bound directly from the agent-local descriptor. The id and keys
/// pointer arrive through separate module globals, so every structural fact is
/// checked before the single stamp store. Missing worker-local ids, key-count
/// drift, and learned-width mismatches return `false` for the existing
/// mint-and-validate path to handle.
///
/// # Safety
///
Expand All @@ -1090,7 +1090,13 @@ pub(crate) unsafe fn try_birth_stamp_preinstalled_shape(
let Some(descriptor) = shape_descriptor_by_id(runtime_shape_id) else {
return false;
};
let logical_key_count = if keys.is_null() {
0
} else {
crate::array::keys_array_len_capped_to_capacity(keys) as u32
};
if descriptor.keys != keys as u64
|| descriptor.logical_key_count != logical_key_count
|| descriptor.live_inline_slot_count != live_inline_slot_count
|| descriptor.semantic_generation != 0
|| descriptor.object_kind != ShapeObjectKind::Ordinary
Expand Down
34 changes: 34 additions & 0 deletions crates/perry-runtime/src/object/shapes_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,40 @@ mod c3c_tests {
unsafe { debug_assert_object_shape_parity(obj) };
}

/// A preinstalled id is valid only while the canonical keys edge still
/// carries the logical count it was minted for. If those facts diverge,
/// the allocator must decline the direct stamp and publish an exact local
/// descriptor through its existing fallback.
#[test]
fn preinstalled_shape_key_count_mismatch_uses_exact_fallback() {
let _lock = crate::gc::global_side_table_test_lock();
const CID: u32 = 0x0C3C_7926;
let packed = b"count_mismatch";
let keys =
crate::object::js_build_class_keys_array(CID, 1, packed.as_ptr(), packed.len() as u32);
let stale_id = js_object_shape_id_for_keys(keys as usize as u64, 1);

unsafe {
// Model a module keys global whose current array facts no longer
// match the ShapeId installed beside it. This is the state that
// fast-json-stringify reached through AJV's resolve module.
(*keys).length = 0;
}
let obj =
crate::object::js_object_alloc_class_inline_keys_stamped(CID, 0, 1, keys, stale_id);
let actual_id = unsafe { (*obj).parent_class_id };
assert_ne!(
actual_id, stale_id,
"a stale logical key count must not be published on the newborn"
);
let descriptor = shape_descriptor_by_id(actual_id)
.expect("the count-mismatch fallback must publish an exact descriptor");
assert_eq!(descriptor.keys, keys as u64);
assert_eq!(descriptor.logical_key_count, 0);
assert_eq!(descriptor.live_inline_slot_count, 1);
unsafe { debug_assert_object_shape_parity(obj) };
}

/// #6759 C3c stamp invariant on a REAL object through the real
/// write/read paths: a read resolution stamps a shape id into the
/// plain object's `parent_class_id`; after further appends any surviving
Expand Down
Loading