diff --git a/crates/perry-codegen/src/expr/static_field_meta.rs b/crates/perry-codegen/src/expr/static_field_meta.rs index 14528e70a5..8024cf15ea 100644 --- a/crates/perry-codegen/src/expr/static_field_meta.rs +++ b/crates/perry-codegen/src/expr/static_field_meta.rs @@ -135,10 +135,21 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { class_name, captures, } => { + // #6052: the snapshot refresh emitted after EACH captured var's + // assignment (#6037) can legally read a SIBLING capture whose + // `let`/`const` has not run yet (`const _fs = ..; ; + // const _path = ..` — the SWC CJS interop shape). Those loads are + // Perry-internal materialization, not user reads: bracket them in + // a TDZ-suppression window so a dead-zone box snapshots as + // `undefined` (a later refresh fixes it up) instead of throwing + // the #6044 ReferenceError. The window holds only these + // side-effect-free capture loads — no user code runs inside it. + ctx.block().call_void("js_tdz_suppress_begin", &[]); let mut lowered: Vec = Vec::with_capacity(captures.len()); for c in captures { lowered.push(lower_expr(ctx, c)?); } + ctx.block().call_void("js_tdz_suppress_end", &[]); if let Some(&class_id) = ctx.class_ids.get(class_name) { if class_id != 0 && !lowered.is_empty() { let n = lowered.len(); diff --git a/crates/perry-codegen/src/runtime_decls/strings.rs b/crates/perry-codegen/src/runtime_decls/strings.rs index 08194a7e84..63d03ad5ad 100644 --- a/crates/perry-codegen/src/runtime_decls/strings.rs +++ b/crates/perry-codegen/src/runtime_decls/strings.rs @@ -1213,6 +1213,12 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { // Decl-site snapshot of a function-nested class's captured locals — // consumed by the dynamic-construction replay (`new mod.C()`). module.declare_function("js_class_register_capture_values", VOID, &[I32, PTR, I64]); + // #6052: TDZ-suppression window around the snapshot's capture loads — a + // refresh emitted between two `let`/`const` initializers (the #6037 + // refresh-after-each-assignment strategy) legally reads a sibling capture + // still in its dead zone; it must snapshot `undefined`, not throw. + module.declare_function("js_tdz_suppress_begin", VOID, &[]); + module.declare_function("js_tdz_suppress_end", VOID, &[]); // Static-method prologue read of one decl-site capture snapshot slot. module.declare_function("js_class_capture_value", DOUBLE, &[I32, I32]); // #5437: snapshot slot read with a `new`-site appended cap-arg fallback diff --git a/crates/perry-runtime/src/box.rs b/crates/perry-runtime/src/box.rs index 0058a34ff5..a7386928db 100644 --- a/crates/perry-runtime/src/box.rs +++ b/crates/perry-runtime/src/box.rs @@ -208,12 +208,54 @@ pub extern "C" fn js_box_get_bits(ptr: *mut Box) -> i64 { // closure-captured, and compound reads alike); the resulting message is // the spec-generic form. if bits == crate::value::TAG_TDZ { + // #6044 regression (#6052): Perry-internal materialization reads — + // the class-capture decl-site snapshot refreshes emitted after EACH + // captured var's assignment (`RegisterClassCaptures`, the #6037 + // refresh strategy) — legally observe sibling captures that are + // still in their dead zone (`const _fs = ..; ; + // const _path = ..`, the SWC CJS interop shape). Those are not user + // reads: pre-TDZ they snapshotted `undefined` and the next refresh + // fixed the value up. Inside the codegen-bracketed suppression + // window, keep exactly that behavior instead of throwing. + if TDZ_SUPPRESS_DEPTH.with(|d| d.get()) > 0 { + return crate::value::TAG_UNDEFINED as i64; + } crate::error::js_throw_reference_error_tdz(f64::from_bits(crate::value::TAG_UNDEFINED)); } bits as i64 } } +thread_local! { + /// #6052: >0 while codegen-emitted Perry-internal materialization reads + /// (the `RegisterClassCaptures` decl-site snapshot refresh) are running — + /// a dead-zone box then reads as `undefined` (pre-#6044 behavior) instead + /// of throwing. Never spans user code: the bracketed window contains only + /// side-effect-free capture loads. + static TDZ_SUPPRESS_DEPTH: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +/// Enter a TDZ-suppression window (see `TDZ_SUPPRESS_DEPTH`). Emitted by +/// codegen immediately before a `RegisterClassCaptures` snapshot's capture +/// loads; paired with `js_tdz_suppress_end`. +#[no_mangle] +pub extern "C" fn js_tdz_suppress_begin() { + TDZ_SUPPRESS_DEPTH.with(|d| d.set(d.get().saturating_add(1))); +} + +/// Leave the TDZ-suppression window opened by `js_tdz_suppress_begin`. +#[no_mangle] +pub extern "C" fn js_tdz_suppress_end() { + TDZ_SUPPRESS_DEPTH.with(|d| d.set(d.get().saturating_sub(1))); +} + +/// Keepalive anchors for the auto-optimize whole-program build (generated-code- +/// only callees — without these the symbols dead-strip and the app link fails). +#[used] +static KEEP_JS_TDZ_SUPPRESS_BEGIN: extern "C" fn() = js_tdz_suppress_begin; +#[used] +static KEEP_JS_TDZ_SUPPRESS_END: extern "C" fn() = js_tdz_suppress_end; + /// Compatibility wrapper for legacy f64-lowered boxed locals. #[no_mangle] pub extern "C" fn js_box_get(ptr: *mut Box) -> f64 { diff --git a/crates/perry-runtime/src/json/replacer.rs b/crates/perry-runtime/src/json/replacer.rs index e4e28f7107..3759aaa2ea 100644 --- a/crates/perry-runtime/src/json/replacer.rs +++ b/crates/perry-runtime/src/json/replacer.rs @@ -72,6 +72,15 @@ unsafe fn root_holder(value_f64: f64) -> f64 { /// fires when the object actually has a closure-typed `toJSON` field. Returns /// the (possibly substituted) value. #[inline] +/// #5989: a real GC heap object pointer is in the low canonical VA range +/// (top 16 bits 0 or 1) and 8-byte aligned. A value whose extracted +/// "pointer" fails this is a corrupted / mis-encoded pointer, never a +/// dereferenceable `GcHeader` — feeding it to `gc_obj_type` SIGBUSes. +#[inline] +fn ptr_derefable(ptr: usize) -> bool { + (ptr >> 48) <= 1 && ptr >= 0x10000 && (ptr & 0x7) == 0 +} + unsafe fn apply_to_json(value: f64) -> f64 { let bits = value.to_bits(); // A BigInt is a primitive, not a POINTER_TAG value — `extract_pointer` @@ -91,6 +100,13 @@ unsafe fn apply_to_json(value: f64) -> f64 { if crate::value::addr_class::is_handle_band(ptr as usize) { return value; } + // #5989: a mis-aligned or out-of-range pointer is a corrupted value, not + // a real GC object; `gc_obj_type` below would deref its `GcHeader` and + // SIGBUS. Guard by magnitude + 8-byte alignment (mirrors + // `is_object_pointer`'s pre-load sanity) — skip the toJSON probe. + if !ptr_derefable(ptr as usize) { + return value; + } // An array can carry an own `toJSON` expando too (test262 // JSON/stringify/value-tojson-result) — checked via the array-named- // property side table, not `object_get_to_json` (arrays have no @@ -201,6 +217,14 @@ unsafe fn dispatch_pointer_with_replacer( buf.push_str("null"); return; } + // #5989: a mis-aligned / out-of-range pointer is a corrupted value, not a + // GC object — `gc_obj_type` (and the buffer/typed-array registry probes) + // would deref its header and SIGBUS. Emit "null" (unserializable), matching + // the handle-band fallback above, rather than crash the render. + if !ptr_derefable(ptr as usize) { + buf.push_str("null"); + return; + } // #3857 follow-up: a boxed primitive wrapper returned by a replacer function // (`new Boolean(true)`, `new Number(n)`, `new String(s)`) must serialize as // its underlying primitive, not as `{}`. Must run before the GC-type dispatch @@ -237,7 +261,17 @@ unsafe fn dispatch_pointer_with_replacer( } match gc_obj_type(ptr) { crate::gc::GC_TYPE_ARRAY => { - stringify_array_with_replacer_pretty(ptr, replacer, buf, indent, depth) + // #5989: `gc_obj_type` can mis-read a corrupted / mis-classified + // structure as an array — its `length` field then reads as garbage + // (e.g. ~2.7e9) and `stringify_array`'s `0..len` walk runs OOB and + // SIGBUSes. Sanity-cap the length (mirrors `is_object_pointer`'s 10M + // cap): beyond that it is not a real array — emit "null", not a crash. + let len = (*(ptr as *const crate::ArrayHeader)).length; + if len > 10_000_000 { + buf.push_str("null"); + } else { + stringify_array_with_replacer_pretty(ptr, replacer, buf, indent, depth) + } } crate::gc::GC_TYPE_OBJECT => { if is_object_pointer(ptr) {