From 2511f3a5dceba15fc927ec997b74a32ffa62235c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 31 Aug 2026 14:37:50 +0200 Subject: [PATCH 1/2] fix(runtime): validate preinstalled shape key counts --- crates/perry-runtime/src/object/alloc.rs | 6 ++-- crates/perry-runtime/src/object/shapes.rs | 18 ++++++---- .../perry-runtime/src/object/shapes_tests.rs | 34 +++++++++++++++++++ 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/crates/perry-runtime/src/object/alloc.rs b/crates/perry-runtime/src/object/alloc.rs index 5c96af6529..0601ee7519 100644 --- a/crates/perry-runtime/src/object/alloc.rs +++ b/crates/perry-runtime/src/object/alloc.rs @@ -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, diff --git a/crates/perry-runtime/src/object/shapes.rs b/crates/perry-runtime/src/object/shapes.rs index 8142737dbc..648f53ce27 100644 --- a/crates/perry-runtime/src/object/shapes.rs +++ b/crates/perry-runtime/src/object/shapes.rs @@ -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 /// @@ -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 diff --git a/crates/perry-runtime/src/object/shapes_tests.rs b/crates/perry-runtime/src/object/shapes_tests.rs index 80825880dc..ac708f34d1 100644 --- a/crates/perry-runtime/src/object/shapes_tests.rs +++ b/crates/perry-runtime/src/object/shapes_tests.rs @@ -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 From bc89231c93ee46c1b8e63735beabfa1d59ac0379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 31 Aug 2026 14:39:31 +0200 Subject: [PATCH 2/2] docs(changelog): record preinstalled shape fix --- changelog.d/9283-preinstalled-shape-facts.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelog.d/9283-preinstalled-shape-facts.md diff --git a/changelog.d/9283-preinstalled-shape-facts.md b/changelog.d/9283-preinstalled-shape-facts.md new file mode 100644 index 0000000000..d73e7f951b --- /dev/null +++ b/changelog.d/9283-preinstalled-shape-facts.md @@ -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.