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", 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`. 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.") 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" ); } diff --git a/crates/perry-runtime/src/closure/dynamic_props.rs b/crates/perry-runtime/src/closure/dynamic_props.rs index 4fe2f096de..93059bf44f 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,36 @@ 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); + // 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, + ); + + 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",