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
9 changes: 6 additions & 3 deletions crates/perry-codegen/src/expr/arrays_finds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1193,21 +1193,24 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
Expr::ArrayEntries(arr) => {
let arr_box = lower_expr(ctx, arr)?;
let blk = ctx.block();
let arr_handle = unbox_to_i64(blk, &arr_box);
// Full bits, not the 48-bit mask: the runtime router classifies
// non-pointer receivers (Web Streams handle ids are plain
// doubles whose masked bits look like heap addresses).
let arr_handle = blk.bitcast_double_to_i64(&arr_box);
let result = blk.call(I64, "js_array_entries_iter_obj", &[(I64, &arr_handle)]);
Ok(nanbox_pointer_inline(blk, &result))
}
Expr::ArrayKeys(arr) => {
let arr_box = lower_expr(ctx, arr)?;
let blk = ctx.block();
let arr_handle = unbox_to_i64(blk, &arr_box);
let arr_handle = blk.bitcast_double_to_i64(&arr_box);
let result = blk.call(I64, "js_array_keys_iter_obj", &[(I64, &arr_handle)]);
Ok(nanbox_pointer_inline(blk, &result))
}
Expr::ArrayValues(arr) => {
let arr_box = lower_expr(ctx, arr)?;
let blk = ctx.block();
let arr_handle = unbox_to_i64(blk, &arr_box);
let arr_handle = blk.bitcast_double_to_i64(&arr_box);
let result = blk.call(I64, "js_array_values_iter_obj", &[(I64, &arr_handle)]);
Ok(nanbox_pointer_inline(blk, &result))
}
Expand Down
9 changes: 6 additions & 3 deletions crates/perry-codegen/src/lower_array_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -997,7 +997,10 @@ pub(crate) fn lower_array_method(
let _ = lower_expr(ctx, a)?;
}
let blk = ctx.block();
let recv_handle = unbox_to_i64(blk, &recv_box);
// Full bits, not the 48-bit mask: the runtime router classifies
// non-pointer receivers (Web Streams handle ids are plain
// doubles whose masked bits look like heap addresses).
let recv_handle = blk.bitcast_double_to_i64(&recv_box);
let result = blk.call(I64, "js_array_entries_iter_obj", &[(I64, &recv_handle)]);
Ok(nanbox_pointer_inline(blk, &result))
}
Expand All @@ -1006,7 +1009,7 @@ pub(crate) fn lower_array_method(
let _ = lower_expr(ctx, a)?;
}
let blk = ctx.block();
let recv_handle = unbox_to_i64(blk, &recv_box);
let recv_handle = blk.bitcast_double_to_i64(&recv_box);
let result = blk.call(I64, "js_array_keys_iter_obj", &[(I64, &recv_handle)]);
Ok(nanbox_pointer_inline(blk, &result))
}
Expand All @@ -1015,7 +1018,7 @@ pub(crate) fn lower_array_method(
let _ = lower_expr(ctx, a)?;
}
let blk = ctx.block();
let recv_handle = unbox_to_i64(blk, &recv_box);
let recv_handle = blk.bitcast_double_to_i64(&recv_box);
let result = blk.call(I64, "js_array_values_iter_obj", &[(I64, &recv_handle)]);
Ok(nanbox_pointer_inline(blk, &result))
}
Expand Down
105 changes: 105 additions & 0 deletions crates/perry-runtime/src/array/iter_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,9 +358,106 @@ unsafe fn collection_iter_obj_for_receiver(arr: *const ArrayHeader, kind: u8) ->
}
}

/// Receiver router for the any-typed `.values()/.keys()/.entries()` fold
/// (#597). Codegen passes the receiver's FULL NaN-box bits (bitcast, no
/// 48-bit mask) so a non-pointer receiver stays distinguishable from a heap
/// address. Before this, a Web Streams handle id (a raw numeric f64, band
/// `0x100000+`, #1545) was masked to `(id - 2^20) * 2^32`; once the id
/// offset crossed 512 that "address" passed the macOS 2 TB heap floor and
/// the registry shape probes dereferenced unmapped memory (the gscmaster
/// request-12 SIGSEGV family; on Linux the 0x1000 floor makes low ids probe
/// low memory immediately). Raw heap pointers — runtime-internal callers
/// and objects compiled before the codegen change — arrive with top16 == 0
/// and keep the legacy path bit-for-bit.
enum IterReceiver {
Ptr(*const ArrayHeader),
Done(i64),
}

unsafe fn route_iter_obj_receiver(arr: *const ArrayHeader, kind: u8) -> IterReceiver {
let bits = arr as u64;
let top16 = bits >> 48;
if top16 == 0 {
// Runtime-internal callers (and objects from pre-change codegen)
// pass raw heap pointers here. `0.0` and denormal-range doubles
// share the untagged shape — only a plausible heap address may be
// treated as a pointer, so `(0 as any).entries()` reaches the
// TypeError below instead of dereferencing null (#6599 review).
if crate::value::addr_class::is_plausible_heap_addr(bits as usize) {
return IterReceiver::Ptr(arr);
}
}
let masked = (bits & 0x0000_FFFF_FFFF_FFFF) as *const ArrayHeader;
if top16 == 0x7FFD {
return IterReceiver::Ptr(masked);
}
if top16 == 0x7FFC {
// undefined (1) / null (2): keep the small payload so
// `guard_coercible_this` renders its coercibility TypeError.
// Booleans (3/4) are ordinary non-iterable primitives — they used
// to fall through to the junk-pointer deref path; route them to
// the TypeError below instead (#6599 review).
let payload = masked as usize;
if payload == 1 || payload == 2 {
return IterReceiver::Ptr(masked);
}
}
let method: &[u8] = match kind {
1 => b"keys",
2 => b"entries",
_ => b"values",
};
// A Web Streams handle is a plain finite whole-number f64 that owns the
// requested method — route it through the dynamic dispatch that reaches
// the stdlib stream arms (mirrors the fetch-band block in
// `collection_iter_obj_for_receiver`; `js_readable_stream_values`
// returns a heap iterator object, so the pointer extraction below is
// sound).
let value = f64::from_bits(bits);
if value.is_finite() && value > 0.0 && value.fract() == 0.0 {
if let Some(probe) = crate::object::stream_handle_probe() {
if probe(value as usize) {
let result = crate::object::js_native_call_method(
value,
method.as_ptr() as *const i8,
method.len(),
std::ptr::null(),
0,
);
let rv = JSValue::from_bits(result.to_bits());
if !rv.is_undefined() && !rv.is_null() {
let ptr = (result.to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64;
if ptr != 0 {
return IterReceiver::Done(ptr);
}
}
}
}
}
// Any other primitive receiver (number, string, int32, bigint) does not
// own these methods — spec TypeError, as Node throws. The old masked
// path either dereferenced the primitive's bits as an ArrayHeader (UB)
// or iterated garbage.
let method_str = match kind {
1 => "keys",
2 => "entries",
_ => "values",
};
crate::error::js_throw_type_error_not_a_function(
std::ptr::null(),
0,
method_str.as_ptr(),
method_str.len(),
);
}

#[no_mangle]
pub extern "C" fn js_array_values_iter_obj(arr: *const ArrayHeader) -> i64 {
unsafe {
let arr = match route_iter_obj_receiver(arr, 0) {
IterReceiver::Ptr(p) => p,
IterReceiver::Done(it) => return it,
};
if let Some(it) = collection_iter_obj_for_receiver(arr, 0) {
return it;
}
Expand All @@ -373,6 +470,10 @@ pub extern "C" fn js_array_values_iter_obj(arr: *const ArrayHeader) -> i64 {
#[no_mangle]
pub extern "C" fn js_array_keys_iter_obj(arr: *const ArrayHeader) -> i64 {
unsafe {
let arr = match route_iter_obj_receiver(arr, 1) {
IterReceiver::Ptr(p) => p,
IterReceiver::Done(it) => return it,
};
if let Some(it) = collection_iter_obj_for_receiver(arr, 1) {
return it;
}
Expand All @@ -385,6 +486,10 @@ pub extern "C" fn js_array_keys_iter_obj(arr: *const ArrayHeader) -> i64 {
#[no_mangle]
pub extern "C" fn js_array_entries_iter_obj(arr: *const ArrayHeader) -> i64 {
unsafe {
let arr = match route_iter_obj_receiver(arr, 2) {
IterReceiver::Ptr(p) => p,
IterReceiver::Done(it) => return it,
};
if let Some(it) = collection_iter_obj_for_receiver(arr, 2) {
return it;
}
Expand Down
37 changes: 28 additions & 9 deletions crates/perry-runtime/src/object/native_call_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -875,15 +875,34 @@ pub unsafe extern "C" fn js_native_call_method(
| "sort"
| "forEach"
) {
let recv_ptr = (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as *mut ObjectHeader;
if crate::url::search_params::shape_is_url_search_params(recv_ptr) {
if let Some(result) = crate::url::search_params::url_search_params_dynamic_call(
recv_ptr,
method_name,
args_ptr,
args_len,
) {
return result;
// Only a pointer-shaped receiver (NaN-boxed pointer above the handle
// band, or a raw untagged heap address) may be shape-probed. A plain
// double must NOT have its low 48 bits read as an address: a Web
// Streams handle id (`1049102.0`) extracts to `(id - 2^20) * 2^32`,
// which passes the macOS 2 TB heap floor once ~512 stream ids are
// live and the probe then dereferences unmapped memory — the
// gscmaster request-12 SIGSEGV (`for await` resolves @@asyncIterator
// to a bound `values` re-dispatch landing here with the numeric
// handle as receiver; Linux's 0x1000 floor probes low memory from
// id 1). Numeric stream receivers fall through to the
// primitive-methods stream dispatch that owns them.
let bits = object.to_bits();
let top16 = bits >> 48;
let payload = (bits & 0x0000_FFFF_FFFF_FFFF) as usize;
let pointer_shaped = (top16 == 0x7FFD
&& crate::value::addr_class::is_above_handle_band(payload))
|| (top16 == 0 && payload >= 0x10000);
if pointer_shaped {
let recv_ptr = payload as *mut ObjectHeader;
if crate::url::search_params::shape_is_url_search_params(recv_ptr) {
if let Some(result) = crate::url::search_params::url_search_params_dynamic_call(
recv_ptr,
method_name,
args_ptr,
args_len,
) {
return result;
}
}
}
}
Expand Down
85 changes: 85 additions & 0 deletions test-files/test_gap_stream_id_band_dynamic_dispatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// gscmaster request-12 SIGSEGV: Web Streams handle ids are raw numeric f64s
// allocated from 0x100000 (one shared counter across the five stream
// registries). A dynamic method call on a stream handle — e.g. the
// `@@asyncIterator` -> bound `values` re-dispatch a `for await` performs —
// reached the URLSearchParams fast-path in `js_native_call_method`, which
// reinterpreted the receiver double's low 48 bits as a heap address. For a
// stream id `0x100000 + k` those bits decode to `k * 2^32`; once k >= 512
// the address crosses the macOS 2 TB heap floor, passes the plausibility
// check, and the shape probe dereferences unmapped memory (on Linux the
// floor is 0x1000, so low ids probe low memory immediately). Requests 1-11
// of a Next.js app each burn ~48 ids; request 12's render stream was the
// first with k >= 512.

async function main() {
const decoder = new TextDecoder();

// A plain number receiver with a URLSearchParams-list method name must
// throw TypeError, not have its double bits probed as a pointer.
// 1049102.0 = bits 0x4130_020E_0000_0000; low 48 bits = 0x20E00000000
// (2.26 TB) — the exact crashing "address". Run before any stream exists
// so the value cannot alias a live stream id.
const n: any = 1049102;
try {
n.entries();
console.log("number entries: no throw");
} catch (e) {
console.log("number entries throws TypeError:", e instanceof TypeError);
}

// Zero and booleans share dangerous bit shapes (top16 == 0 → "raw
// pointer" null; 0x7FFC payloads 3/4 → tiny "pointers") — all three
// iterator names must throw TypeError, matching Node (#6599 review).
// Direct `.entries()` syntax so the #597 any-typed fold fires.
const zero: any = 0;
const t: any = true;
const f: any = false;
const check = (label: string, fn: () => void) => {
try {
fn();
console.log(`${label}: no throw`);
} catch (e) {
console.log(`${label} throws TypeError:`, e instanceof TypeError);
}
};
check("0.entries()", () => zero.entries());
check("0.keys()", () => zero.keys());
check("0.values()", () => zero.values());
check("true.entries()", () => t.entries());
check("true.keys()", () => t.keys());
check("true.values()", () => t.values());
check("false.entries()", () => f.entries());
check("false.keys()", () => f.keys());
check("false.values()", () => f.values());

function makeStream(i: number): ReadableStream {
return new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode("chunk-" + i + ";"));
controller.close();
},
});
}

// Burn stream-family ids well past the k = 512 threshold.
const burned: ReadableStream[] = [];
for (let i = 0; i < 700; i++) burned.push(makeStream(i));

// Type-erased for-await: resolves @@asyncIterator to a bound `values`
// dynamic dispatch with the numeric handle as receiver.
const erased: any = makeStream(9999);
let text = "";
for await (const chunk of erased) text += decoder.decode(chunk);
console.log("for-await:", text);

// Direct dynamic `.values()` — the method name that collided with the
// URLSearchParams fast-path list.
const erased2: any = makeStream(4242);
const iter = erased2.values();
const first = await iter.next();
console.log("values():", decoder.decode(first.value), first.done);
const last = await iter.next();
console.log("done:", last.done, last.value === undefined);
}

main();
Loading