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
38 changes: 28 additions & 10 deletions crates/perry-codegen/src/expr/property_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,16 +424,34 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// `.length` hot path). Tag check is platform-independent.
let recv_tag = blk.lshr(I64, &recv_bits, "48");
let recv_tag_masked = blk.and(I64, &recv_tag, "65533"); // 0xFFFD
let handle_ok = blk.icmp_eq(I64, &recv_tag_masked, "32765"); // 0x7FFD
// SSO receivers fail this guard → route to slow path
// `js_value_length_f64` which has an SSO branch (reads
// length from the tag byte, no heap access). Accepting
// SSO here is safe because the fast path's
// `safe_load_i32_from_ptr(&recv_handle)` would read
// arbitrary bytes at the SSO "pointer" address, but
// the subsequent phi feeds the slow-path result when
// handle_ok is false — so SSO flow is correct via the
// slow path already, no widening needed.
let tag_ok = blk.icmp_eq(I64, &recv_tag_masked, "32765"); // 0x7FFD
// The tag check alone admits POINTER_TAG-boxed *handle-band*
// values — Web Fetch handles (Headers/Request/Response/Blob, id
// in [0x40000, 0xE0000)), net/http small handles, revocable-proxy
// ids — which are NaN-boxed registry ids, NOT heap pointers. A
// value statically typed Array/String/Named that actually holds
// such a handle at runtime (e.g. a `Response`/`Headers` reaching a
// `.length` site) would then `inttoptr` the bare id and load the
// GC-type byte at `id-8` and the length u32 at `id` — both
// unmapped low addresses → SIGSEGV (observed: doctor / mcp list
// crashing at the exact fetch-handle address). The IC-miss path
// (`js_object_get_field_ic_miss`) and the inline class-field guard
// already gate on `> HANDLE_BAND_TOP`; mirror that here so any
// handle-band receiver routes to the `js_value_length_f64` slow
// path, which classifies it by registry without dereferencing the
// raw id. `HANDLE_BAND_TOP` = 0xFFFFF (addr_class::HANDLE_BAND_MAX
// - 1).
let above_band = blk.icmp_ugt(I64, &recv_handle, "1048575"); // 0xFFFFF
let handle_ok = blk.and(I1, &tag_ok, &above_band);
// SSO receivers fail this guard → route to slow path
// `js_value_length_f64` which has an SSO branch (reads
// length from the tag byte, no heap access). Accepting
// SSO here is safe because the fast path's
// `safe_load_i32_from_ptr(&recv_handle)` would read
// arbitrary bytes at the SSO "pointer" address, but
// the subsequent phi feeds the slow-path result when
// handle_ok is false — so SSO flow is correct via the
// slow path already, no widening needed.

let check_gc_idx = ctx.new_block("plen.check_gc");
let fast_idx = ctx.new_block("plen.fast");
Expand Down
50 changes: 50 additions & 0 deletions crates/perry-runtime/src/value/dynamic_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ pub extern "C" fn js_value_length_f64(value: f64) -> f64 {
// nonsense.
if top16 == 0x7FFD {
let handle = (bits & POINTER_MASK) as usize;
// A POINTER_TAG value in a handle band (Web Fetch
// Headers/Request/Response/Blob, net/http small handles, zlib stream
// ids, revocable-proxy ids — all `< 0x100000`) is a registry id, not a
// heap pointer. None of them carry a `.length`, and dereferencing the
// raw id (`handle - 8` GcHeader read, or `*handle` u32 below) hits
// unmapped low memory → SIGSEGV. The macOS 2 TB `heap_min` floor below
// masks this, but the Linux/Android/iOS `0x1000` floor does not, so
// reject the band explicitly here. Matches the band gating the inline
// `.length` codegen fast path now applies before falling in here.
if crate::value::addr_class::is_handle_band(handle) {
return 0.0;
}
// Heap window: macOS mimalloc lands in 3-5 TB, but Android scudo,
// Linux glibc, Windows mimalloc, and iOS-family device
// libsystem_malloc all allocate much lower (often hundreds of GB
Expand Down Expand Up @@ -616,3 +628,41 @@ pub unsafe extern "C" fn js_dynamic_object_keys(ptr: i64) -> *mut crate::array::
pub unsafe extern "C" fn js_get_property(object: f64, name_ptr: i64, name_len: i64) -> f64 {
js_dynamic_object_get_property(object, name_ptr as *const i8, name_len as usize)
}

#[cfg(test)]
mod length_handle_band_tests {
use super::*;
use crate::value::addr_class;

/// A POINTER_TAG-boxed *handle-band* value (Web Fetch
/// Headers/Request/Response/Blob ids, net/http handles, zlib/proxy ids —
/// all `< 0x100000`) is a registry id, not a heap pointer. Reaching the
/// `.length` slow path with such a value must return `0.0` WITHOUT
/// dereferencing the raw id (which would SIGSEGV at the unmapped low
/// address). Regression for the doctor / mcp-list startup crash where a
/// Fetch handle (e.g. `0x40005`) flowed into a `.length` site; the inline
/// codegen fast path now band-gates before falling in here, and this slow
/// path rejects the band explicitly so it is safe on every platform.
#[test]
fn fetch_band_handle_length_is_zero_not_a_deref() {
// Sample one id from each handle sub-band, including the exact band
// boundaries the bug touched.
for &id in &[
1usize, // common native handle
addr_class::FETCH_HANDLE_BAND_START, // 0x40000
addr_class::FETCH_HANDLE_BAND_START + 5, // 0x40005 — the crash addr
addr_class::FETCH_HANDLE_BAND_END - 1, // top of fetch band
addr_class::PROXY_ID_BAND_START, // revocable-proxy id
addr_class::HANDLE_BAND_MAX - 1, // 0xFFFFF — last handle-band id
] {
assert!(addr_class::is_handle_band(id));
let boxed = crate::value::js_nanbox_pointer(id as i64);
// Must not crash and must report no length.
assert_eq!(
js_value_length_f64(boxed),
0.0,
"handle-band id {id:#x} must yield length 0 without dereferencing"
);
}
}
}
Loading