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
11 changes: 11 additions & 0 deletions crates/perry-codegen/src/expr/static_field_meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,21 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
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 = ..; <refresh>;
// 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<String> = 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();
Expand Down
6 changes: 6 additions & 0 deletions crates/perry-codegen/src/runtime_decls/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions crates/perry-runtime/src/box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ..; <refresh reads _path>;
// 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<u32> = 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 {
Expand Down
36 changes: 35 additions & 1 deletion crates/perry-runtime/src/json/replacer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Loading