From ae2700169f2a0b53e0a6a56a34456d9643b27584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 29 Aug 2026 01:49:57 +0200 Subject: [PATCH 1/6] fix(codegen): guard Array-subclass meta probe by GC kind --- .../src/expr/index_get/inline_dyn_typed_array.rs | 8 +++++++- .../perry-codegen/src/expr/index_get_claim_tests.rs | 13 +++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs b/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs index 5f1f8b93e8..2113d0370b 100644 --- a/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs +++ b/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs @@ -444,11 +444,13 @@ pub(super) fn lower_inline_dyn_typed_array_get( // of this probe (no meta, no store) is the shape-carried form and keeps // the IC below; an out-of-bounds index or a hole goes to the complete // dispatcher (prototype chain). + let elem_kind_idx = ctx.new_block("arrlike.elem.kind"); let elem_meta_idx = ctx.new_block("arrlike.elem.meta"); let elem_store_idx = ctx.new_block("arrlike.elem.store"); let elem_bounds_idx = ctx.new_block("arrlike.elem.bounds"); let elem_load_idx = ctx.new_block("arrlike.elem.load"); let elem_value_idx = ctx.new_block("arrlike.elem.value"); + let elem_kind_label = ctx.block_label(elem_kind_idx); let elem_meta_label = ctx.block_label(elem_meta_idx); let elem_store_label = ctx.block_label(elem_store_idx); let elem_bounds_label = ctx.block_label(elem_bounds_idx); @@ -456,7 +458,11 @@ pub(super) fn lower_inline_dyn_typed_array_get( let elem_value_label = ctx.block_label(elem_value_idx); ctx.current_block = object_brand_idx; ctx.block() - .cond_br(&is_array, &object_array_guard_label, &elem_meta_label); + .cond_br(&is_array, &object_array_guard_label, &elem_kind_label); + ctx.current_block = elem_kind_idx; + let elem_is_object = ctx.block().icmp_eq(I8, &gc_type, "2"); + ctx.block() + .cond_br(&elem_is_object, &elem_meta_label, &object_miss_label); ctx.current_block = elem_meta_idx; let elem_meta_addr = ctx.block().add(I64, &object_raw, &meta_offset); let elem_meta_slot_ptr = ctx.block().inttoptr(I64, &elem_meta_addr); diff --git a/crates/perry-codegen/src/expr/index_get_claim_tests.rs b/crates/perry-codegen/src/expr/index_get_claim_tests.rs index 6b0faf81d4..f3ace585ac 100644 --- a/crates/perry-codegen/src/expr/index_get_claim_tests.rs +++ b/crates/perry-codegen/src/expr/index_get_claim_tests.rs @@ -409,6 +409,19 @@ fn any_typed_dynamic_key_takes_the_numeric_tiers_when_it_is_an_array_index() { ir.contains("tav.get.brand") && ir.contains("arrlike.ic.family_token"), "an integer key must reach the inline typed-array and dense-subclass tiers:\n{ir}" ); + // Only an ordinary ObjectHeader has the `meta` slot used by the + // elements-backed Array-subclass probe. Native Buffers and other exotic + // managed cells must leave through the complete dispatcher before that + // load; interpreting their header word at offset 8 as ObjectMeta crashes. + let kind = super::class_field_barrier_tests::block_body(&ir, "arrlike.elem.kind.") + .expect("the elements-store object-kind guard exists"); + assert!( + kind.contains("icmp eq i8") + && kind.contains(", 2") + && kind.contains("arrlike.elem.meta") + && kind.contains("arrlike.ic.miss"), + "only GC_TYPE_OBJECT may reach the ObjectMeta.elements load:\n{kind}" + ); // The elements-backed subclass probe sits ahead of the shape IC: meta // word → `ObjectMeta.elements` (word 12) → inner-array bounds → slot. let store = super::class_field_barrier_tests::block_body(&ir, "arrlike.elem.store.") From 2fd645273889a0666884354feaf4fafdc2cf2ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 29 Aug 2026 03:07:07 +0200 Subject: [PATCH 2/6] fix(runtime): trust GC kind over closure payload magic --- .../src/closure/dynamic_props.rs | 44 +++++++++++++++++++ test-parity/known_failures.json | 9 ---- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/crates/perry-runtime/src/closure/dynamic_props.rs b/crates/perry-runtime/src/closure/dynamic_props.rs index 4fe2f096de..4383d8c5bb 100644 --- a/crates/perry-runtime/src/closure/dynamic_props.rs +++ b/crates/perry-runtime/src/closure/dynamic_props.rs @@ -419,6 +419,24 @@ pub fn is_closure_ptr(ptr: usize) -> bool { if !ptr.is_multiple_of(std::mem::align_of::()) { return false; } + // Arena ownership gives us an authoritative discriminator. Do not let a + // coincidental CLOSURE_MAGIC in another managed cell's payload win: in + // particular, ErrorHeader has padding at the closure tag offset and an + // arena slot reused after a closure can retain "CLOS" in those bytes. + // Headerless/external allocations remain on the exact-magic fallback. + if !matches!( + crate::arena::classify_heap_generation(ptr), + crate::arena::HeapGeneration::Unknown + ) { + let Some(header) = (unsafe { crate::value::addr_class::try_read_gc_header(ptr) }) else { + return false; + }; + if header.obj_type != crate::gc::GC_TYPE_CLOSURE + || header.gc_flags & crate::gc::GC_FLAG_FORWARDED != 0 + { + return false; + } + } unsafe { let type_tag = *((ptr as *const u8).add(CLOSURE_TYPE_TAG_OFFSET) as *const u32); type_tag == CLOSURE_MAGIC @@ -1012,6 +1030,32 @@ mod tests_1802 { ); } } + + /// A managed cell's GC kind must outrank bytes that merely look like a + /// closure tag. ErrorHeader's bytes 12..16 are padding on 64-bit targets; + /// reused arena storage can therefore retain CLOSURE_MAGIC there. + #[test] + fn managed_error_with_closure_magic_in_padding_is_not_a_closure() { + unsafe { + let message = crate::string::js_string_from_bytes(b"survives".as_ptr(), 8); + let error = crate::error::js_error_new_with_message(message); + std::ptr::write_unaligned( + (error as *mut u8).add(CLOSURE_TYPE_TAG_OFFSET) as *mut u32, + CLOSURE_MAGIC, + ); + + assert!(!is_closure_ptr(error as usize)); + assert_eq!((*error).message, message); + + let key = crate::string::js_string_from_bytes(b"message".as_ptr(), 7); + let value = crate::object::js_object_get_field_by_name(error.cast(), key); + assert_eq!( + value.bits() & crate::value::POINTER_MASK, + message as usize as u64, + "property lookup must reach Error handling, not the closure path", + ); + } + } } /// Issue #450: clone an accessor closure (from `Object.defineProperty(obj, k, { get, set })`) diff --git a/test-parity/known_failures.json b/test-parity/known_failures.json index b3cb0c8233..202e64d354 100644 --- a/test-parity/known_failures.json +++ b/test-parity/known_failures.json @@ -37,15 +37,6 @@ "linux" ] }, - "test_class_field_layout": { - "issue": "8841", - "added": "2026-08-25", - "category": "bug-open", - "reason": "Reproduces 5/5 on pre-#8835 r13 and in r14 Full CI: both runtimes exit 0 but their class-field layout output differs. Tracked in #8841.", - "platforms": [ - "linux" - ] - }, "test_date_fns_format": { "issue": "8271", "added": "2026-08-17", From d4ce6553fec18eb665deefa50b28bef1c9625b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 29 Aug 2026 03:10:57 +0200 Subject: [PATCH 3/6] fix(build): lock the intl timezone provider dependency --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index 32127e3e0a..92538a7d4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6348,6 +6348,7 @@ dependencies = [ "taffy", "temporal_rs", "thiserror 1.0.69", + "timezone_provider", "unicode-normalization", "unicode-segmentation", "url", From 4999b126c89102c566d54c52081f01b44dc71f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 29 Aug 2026 03:12:04 +0200 Subject: [PATCH 4/6] docs(changelog): record release receiver probe fixes --- changelog.d/8999-release-receiver-probe-kinds.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelog.d/8999-release-receiver-probe-kinds.md diff --git a/changelog.d/8999-release-receiver-probe-kinds.md b/changelog.d/8999-release-receiver-probe-kinds.md new file mode 100644 index 0000000000..9bb5eaf413 --- /dev/null +++ b/changelog.d/8999-release-receiver-probe-kinds.md @@ -0,0 +1,5 @@ +### Fixed + +- Dynamic indexed reads now inspect `ObjectMeta.elements` only for receivers whose GC kind is an object. Array, string, Error, and other layouts can no longer be interpreted as an object metadata pointer, eliminating the release parity crashes introduced with Array-subclass elements storage. +- Managed heap cells are classified as closures only when their authoritative GC kind is `GC_TYPE_CLOSURE`. Reused `Error` storage whose padding retained the closure magic marker no longer loses `.message` or custom fields during property lookup. +- Removed the stale Linux parity allowance for `test_class_field_layout`, which now matches Node, and recorded the timezone-provider dependency added to the runtime in `Cargo.lock`. From 74f2d49df0497e7afc819d8accdf407e6d145b09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 29 Aug 2026 03:34:00 +0200 Subject: [PATCH 5/6] test(codegen): follow boxed string concat ABI --- .../perry-codegen/tests/temp_root_operand_temporaries.rs | 7 ++++--- crates/perry-codegen/tests/typed_shape_descriptors.rs | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/perry-codegen/tests/temp_root_operand_temporaries.rs b/crates/perry-codegen/tests/temp_root_operand_temporaries.rs index f8ad2febe6..999057cf53 100644 --- a/crates/perry-codegen/tests/temp_root_operand_temporaries.rs +++ b/crates/perry-codegen/tests/temp_root_operand_temporaries.rs @@ -530,7 +530,7 @@ fn registered_root_operands_are_reloaded_rather_than_rooted() { // concat read the string's pre-move address: an empty line, exit code 0. /// An operand that is statically NUMERIC *and* can collect, so `"lit" + it` -/// takes the fused `js_string_concat_value` path — the exact expression form +/// takes the fused `js_string_concat_value_box` path — the exact expression form /// #7114 was reported against. /// /// A non-`Add` `Expr::Binary` is numeric by construction (`is_numeric_expr`), @@ -641,7 +641,8 @@ fn string_literal_concat_operand_is_re_derived_below_the_allocating_sibling() { let f = init_ir(&ir); let handle = "load double, ptr @concat_reload_ts_.str."; assert_eq!( - f.matches("call i64 @js_string_concat_value(").count(), + f.matches("call double @js_string_concat_value_box(") + .count(), 1, "exactly one fused string+value concat in @main:\n{f}" ); @@ -659,7 +660,7 @@ fn string_literal_concat_operand_is_re_derived_below_the_allocating_sibling() { it:\n{f}" ); - let concat = f.find("call i64 @js_string_concat_value(").unwrap(); + let concat = f.find("call double @js_string_concat_value_box(").unwrap(); let alloc = f[..concat] .rfind("call i64 @js_object_alloc(") .unwrap_or_else(|| panic!("the sibling must allocate before the concat:\n{f}")); diff --git a/crates/perry-codegen/tests/typed_shape_descriptors.rs b/crates/perry-codegen/tests/typed_shape_descriptors.rs index 3fbcef7e4b..28f05d52b8 100644 --- a/crates/perry-codegen/tests/typed_shape_descriptors.rs +++ b/crates/perry-codegen/tests/typed_shape_descriptors.rs @@ -187,7 +187,7 @@ fn scalar_object_literal_skips_pure_unobserved_initializers() { "non-escaping object literal should stay scalar-replaced" ); assert!( - !ir.contains("call i64 @js_string_concat"), + !ir.contains("call double @js_string_concat_value_box("), "pure unobserved field initializer should not be lowered" ); } @@ -226,7 +226,7 @@ fn scalar_array_literal_skips_pure_unobserved_initializers() { "non-escaping array literal should stay scalar-replaced" ); assert!( - !ir.contains("call i64 @js_string_concat"), + !ir.contains("call double @js_string_concat_value_box("), "pure unobserved array initializer should not be lowered" ); } @@ -294,7 +294,7 @@ fn scalar_object_literal_keeps_initializers_read_by_update() { let ir = ir_for(module); assert!( - ir.contains("call i64 @js_string_concat"), + ir.contains("call double @js_string_concat_value_box("), "field initializer read by obj.field++ must still be lowered" ); } From e8bde8d8500a704f4d2755f1740b15b5604eb445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 29 Aug 2026 03:54:13 +0200 Subject: [PATCH 6/6] chore: GC_STORE_AUDIT marker --- crates/perry-runtime/src/closure/dynamic_props.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/perry-runtime/src/closure/dynamic_props.rs b/crates/perry-runtime/src/closure/dynamic_props.rs index 4383d8c5bb..93059bf44f 100644 --- a/crates/perry-runtime/src/closure/dynamic_props.rs +++ b/crates/perry-runtime/src/closure/dynamic_props.rs @@ -1039,6 +1039,10 @@ mod tests_1802 { unsafe { let message = crate::string::js_string_from_bytes(b"survives".as_ptr(), 8); let error = crate::error::js_error_new_with_message(message); + // GC_STORE_AUDIT(POINTER_FREE): writes the u32 magic constant into + // an ErrorHeader's padding on purpose, so the assertion below proves + // the GC kind outranks look-alike bytes. No heap pointer is stored, + // so there is nothing for a barrier to track. std::ptr::write_unaligned( (error as *mut u8).add(CLOSURE_TYPE_TAG_OFFSET) as *mut u32, CLOSURE_MAGIC,