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
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,14 @@ pub(crate) fn lower_generic_property_get(
let cache_slot_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "1")]);
let slot = ctx.block().load(I64, &cache_slot_ptr);
let offset = ctx.block().shl(I64, &slot, "3");
let base = ctx.block().add(I64, &obj_handle, "24");
// arm64_32 watchOS: the object fields region begins at
// `size_of::<ObjectHeader>()` past the user pointer — 24 on 64-bit, 20 on
// ILP32 (the trailing `keys_array` pointer is 4 bytes there). A hardcoded
// 24 would read every cached property 4 bytes off on a 32-bit watch. Derive
// it from the target triple (no-op on 64-bit; see `target_layout`).
let obj_header_size =
crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();
let base = ctx.block().add(I64, &obj_handle, &obj_header_size);
let field_addr = ctx.block().add(I64, &base, &offset);
let field_ptr = ctx.block().inttoptr(I64, &field_addr);
let val_hit = ctx.block().load(DOUBLE, &field_ptr);
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub mod runtime_decls;
pub(crate) mod stmt;
pub mod strings;
pub mod stubs;
pub mod target_layout;
pub(crate) mod type_analysis;
pub(crate) mod type_analysis_class_fields;
pub(crate) mod type_analysis_facts;
Expand Down
19 changes: 15 additions & 4 deletions crates/perry-codegen/src/lower_call/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -905,7 +905,12 @@ fn lower_new_impl(
} else {
// Compile-time layout constants.
const GC_HEADER_SIZE: u64 = 8;
const OBJECT_HEADER_SIZE: u64 = 24;
// arm64_32 watchOS: `size_of::<ObjectHeader>()` is 24 on 64-bit but
// 20 on ILP32 (4-byte `keys_array` pointer). Derive from the target
// triple so the inline alloc size and field-region base match the
// target-compiled runtime (no-op on 64-bit; see `target_layout`).
let object_header_size: u64 =
crate::target_layout::object_header_size_bytes(ctx.target_triple);
const FIELD_SLOT_SIZE: u64 = 8;
const MIN_FIELD_SLOTS: u64 = 8;
const GC_TYPE_OBJECT: u64 = 2;
Expand All @@ -918,8 +923,14 @@ fn lower_new_impl(
const OBJECT_TYPE_REGULAR: u64 = 1;

let alloc_field_count = std::cmp::max(field_count as u64, MIN_FIELD_SLOTS);
let payload_size = OBJECT_HEADER_SIZE + alloc_field_count * FIELD_SLOT_SIZE;
let total_size = GC_HEADER_SIZE + payload_size; // e.g. 96 for any class with ≤8 fields
let payload_size = object_header_size + alloc_field_count * FIELD_SLOT_SIZE;
// Round the whole allocation up to FIELD_SLOT_SIZE (8). The inline
// bump allocator's offset invariant (below) requires every
// allocation to be a multiple of 8; on ILP32 `object_header_size`
// is 20, so an unpadded total is 4-skewed (e.g. 92) and would
// misalign the next bump. No-op on 64-bit (8 + 24 + 8·n is already
// 8-aligned → 96 for ≤8 fields).
let total_size = (GC_HEADER_SIZE + payload_size).next_multiple_of(FIELD_SLOT_SIZE);
let total_size_str = total_size.to_string();

// Lazy: allocate the per-function arena-state slot on the
Expand Down Expand Up @@ -1057,7 +1068,7 @@ fn lower_new_impl(
// crashed with "Cannot read properties of undefined". Slots start at
// raw + GcHeader(8) + ObjectHeader(24) = raw + 32.
for i in 0..alloc_field_count {
let slot_off = GC_HEADER_SIZE + OBJECT_HEADER_SIZE + i * FIELD_SLOT_SIZE;
let slot_off = GC_HEADER_SIZE + object_header_size + i * FIELD_SLOT_SIZE;
let slot_ptr = blk.gep(I8, &raw, &[(I64, &slot_off.to_string())]);
// GC_STORE_AUDIT(INIT): freshly allocated inline object slot initialized to undefined.
blk.store(I64, crate::nanbox::TAG_UNDEFINED_I64, &slot_ptr);
Expand Down
69 changes: 69 additions & 0 deletions crates/perry-codegen/src/target_layout.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//! Target-pointer-width-dependent struct layout sizes used by inline codegen.
//!
//! Perry's codegen runs on the 64-bit host but may *emit* code for a 32-bit
//! (ILP32) target — currently `arm64_32-apple-watchos` (Apple Watch Series
//! 4–8 / SE). Any inline IR that bakes in a runtime struct's byte size MUST
//! derive it from the *target* triple, not from the host's `size_of`, or the
//! emitted offsets disagree with the target-compiled `perry-runtime` and every
//! field access reads/writes the wrong bytes (the arm64_32 watchOS class of
//! bug). These helpers are the single source of truth for those
//! target-dependent sizes.

/// True when `target_triple` names a 32-bit-pointer (ILP32) target. `arm64_32`
/// (64-bit registers, 32-bit pointers) is the live case for Perry; the other
/// 32-bit families are matched defensively so a future target is sized
/// correctly rather than silently treated as 64-bit.
pub fn target_is_ilp32(target_triple: &str) -> bool {
target_triple.starts_with("arm64_32")
|| target_triple.starts_with("armv7")
|| target_triple.starts_with("thumbv7")
|| target_triple.starts_with("wasm32")
|| target_triple.starts_with("i686")
|| target_triple.starts_with("i386")
}

/// `std::mem::size_of::<perry_runtime::object::ObjectHeader>()` for the target.
///
/// `ObjectHeader` is four `u32`s (`object_type`, `class_id`, `parent_class_id`,
/// `field_count` = 16 bytes) followed by one pointer (`keys_array`): 8 bytes +
/// 8-byte alignment → 24 on 64-bit; 4 bytes → 20 on ILP32. Inline object
/// allocation, header init, and the property inline-cache fast path all use
/// this as the field-region base (`fields = obj + object_header_size_bytes`).
/// It MUST equal the runtime's `size_of::<ObjectHeader>()`, or inline-
/// constructed objects and runtime-FFI field access diverge by 4 bytes and
/// every property read/write is corrupt. (The closure header `type_tag` offset
/// has the analogous problem; that one is handled runtime-side via
/// `perry_runtime::closure::CLOSURE_TYPE_TAG_OFFSET` / `offset_of!`.)
pub fn object_header_size_bytes(target_triple: &str) -> u64 {
if target_is_ilp32(target_triple) {
20
} else {
24
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn object_header_size_matches_pointer_width() {
// 64-bit targets: 4×u32 + 8-byte aligned pointer = 24. Must stay 24 so
// the shipping arm64 / x86_64 IR is byte-identical to before this fix.
assert_eq!(object_header_size_bytes("aarch64-apple-darwin"), 24);
assert_eq!(object_header_size_bytes("aarch64-apple-watchos"), 24);
assert_eq!(object_header_size_bytes("aarch64-apple-watchos-sim"), 24);
assert_eq!(object_header_size_bytes("x86_64-unknown-linux-gnu"), 24);
// arm64_32 watchOS (Series 4–8 / SE): 4×u32 + 4-byte pointer = 20.
assert_eq!(object_header_size_bytes("arm64_32-apple-watchos"), 20);
}

#[test]
fn ilp32_classification() {
assert!(target_is_ilp32("arm64_32-apple-watchos"));
// The 64-bit watch target must NOT be treated as ILP32.
assert!(!target_is_ilp32("aarch64-apple-watchos"));
assert!(!target_is_ilp32("aarch64-apple-darwin"));
assert!(!target_is_ilp32("x86_64-pc-windows-msvc"));
}
}
3 changes: 2 additions & 1 deletion crates/perry-runtime/src/builtins/arithmetic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,8 @@ pub extern "C" fn js_value_typeof(value: f64) -> *mut StringHeader {
get_cached(&TYPEOF_OBJECT, "object")
} else {
// ClosureHeader has type_tag at offset 12 (after func_ptr:8 + capture_count:4)
let type_tag = unsafe { *(ptr.add(12) as *const u32) };
let type_tag =
unsafe { *(ptr.add(crate::closure::CLOSURE_TYPE_TAG_OFFSET) as *const u32) };
if type_tag == crate::closure::CLOSURE_MAGIC {
get_cached(&TYPEOF_FUNCTION, "function")
} else if crate::object::is_class_object_ptr(ptr) {
Expand Down
14 changes: 14 additions & 0 deletions crates/perry-runtime/src/closure/alloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ pub struct ClosureHeader {
pub type_tag: u32,
}

/// Byte offset of `type_tag` (the `CLOSURE_MAGIC` slot) within `ClosureHeader`.
///
/// On 64-bit targets this is 12 (`func_ptr` 8 bytes + `capture_count` 4 bytes);
/// on arm64_32 / wasm32 (32-bit pointers) `func_ptr` is 4 bytes, so it is 8.
/// Every site that probes a heap pointer for `CLOSURE_MAGIC` MUST read at this
/// offset, never a hardcoded `12`: that literal was correct only for 64-bit and
/// was the arm64_32 watchOS startup-crash root cause. On a 32-bit watch every
/// real closure failed the magic probe (the read landed 4 bytes past
/// `type_tag`), so a getter/function value was judged non-callable and the
/// resulting `TypeError` value-coercion dereferenced the closure as an
/// `ObjectHeader` → `EXC_BAD_ACCESS` before the first frame rendered.
/// `offset_of!` tracks the real per-target layout, so this is a no-op on 64-bit.
pub const CLOSURE_TYPE_TAG_OFFSET: usize = std::mem::offset_of!(ClosureHeader, type_tag);

#[inline]
pub fn closure_payload_size(actual_count: usize) -> usize {
std::mem::size_of::<ClosureHeader>() + actual_count * std::mem::size_of::<u64>()
Expand Down
11 changes: 8 additions & 3 deletions crates/perry-runtime/src/closure/dispatch/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ pub fn clean_closure_ptr(mut closure: *const ClosureHeader) -> *const ClosureHea
if !(0x1000..0x0001_0000_0000_0000).contains(&addr) {
return closure;
}
let type_tag =
unsafe { std::ptr::read_volatile((closure as *const u8).add(12) as *const u32) };
let type_tag = unsafe {
std::ptr::read_volatile(
(closure as *const u8).add(CLOSURE_TYPE_TAG_OFFSET) as *const u32
)
};
if type_tag != CLOSURE_MAGIC {
return closure;
}
Expand Down Expand Up @@ -62,7 +65,9 @@ pub fn get_valid_func_ptr(closure: *const ClosureHeader) -> *const u8 {
if !(0x1000..0x0001_0000_0000_0000).contains(&addr) {
return std::ptr::null();
}
let type_tag = unsafe { std::ptr::read_volatile((closure as *const u8).add(12) as *const u32) };
let type_tag = unsafe {
std::ptr::read_volatile((closure as *const u8).add(CLOSURE_TYPE_TAG_OFFSET) as *const u32)
};
if type_tag != CLOSURE_MAGIC {
return std::ptr::null();
}
Expand Down
14 changes: 8 additions & 6 deletions crates/perry-runtime/src/closure/dynamic_props.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ pub fn is_closure_ptr(ptr: usize) -> bool {
return false;
}
unsafe {
let type_tag = *((ptr as *const u8).add(12) as *const u32);
let type_tag = *((ptr as *const u8).add(CLOSURE_TYPE_TAG_OFFSET) as *const u32);
type_tag == CLOSURE_MAGIC
}
}
Expand Down Expand Up @@ -643,7 +643,7 @@ pub extern "C" fn js_closure_unbind_this(val: f64) -> f64 {
}
// Check CLOSURE_MAGIC
unsafe {
let type_tag = *((ptr as *const u8).add(12) as *const u32);
let type_tag = *((ptr as *const u8).add(CLOSURE_TYPE_TAG_OFFSET) as *const u32);
if type_tag != CLOSURE_MAGIC {
return val;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand All @@ -664,8 +664,9 @@ pub extern "C" fn js_closure_unbind_this(val: f64) -> f64 {
let new_closure = js_closure_alloc(func_ptr, raw_count);
let source_bits = val_handle.get_nanbox_f64().to_bits();
let source_ptr = (source_bits & 0x0000_FFFF_FFFF_FFFF) as usize;
let source_type_tag =
std::ptr::read_volatile((source_ptr as *const u8).add(12) as *const u32);
let source_type_tag = std::ptr::read_volatile(
(source_ptr as *const u8).add(CLOSURE_TYPE_TAG_OFFSET) as *const u32,
);
if source_type_tag != CLOSURE_MAGIC {
return val_handle.get_nanbox_f64();
}
Expand Down Expand Up @@ -863,8 +864,9 @@ pub(crate) fn clone_closure_rebind_this(closure_bits: u64, recv_box: f64) -> u64
let new_closure = js_closure_alloc(func_ptr, raw_count);
let source_bits = closure_handle.get_nanbox_u64();
let source_ptr = (source_bits & 0x0000_FFFF_FFFF_FFFF) as usize;
let source_type_tag =
std::ptr::read_volatile((source_ptr as *const u8).add(12) as *const u32);
let source_type_tag = std::ptr::read_volatile(
(source_ptr as *const u8).add(CLOSURE_TYPE_TAG_OFFSET) as *const u32,
);
if source_type_tag != CLOSURE_MAGIC {
return source_bits;
}
Expand Down
1 change: 1 addition & 0 deletions crates/perry-runtime/src/closure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub use alloc::{
js_closure_set_capture_f64, js_closure_set_capture_ptr, note_closure_capture_slot,
rebuild_closure_layout_and_barriers, scan_singleton_closure_roots_mut, ClosureHeader,
CLOSURE_ALLOC_COUNT, CLOSURE_CAP_SINGLETON_HIT, CLOSURE_CAP_SINGLETON_MISS,
CLOSURE_TYPE_TAG_OFFSET,
};

pub use registry::{
Expand Down
6 changes: 4 additions & 2 deletions crates/perry-runtime/src/json/stringify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,8 @@ pub(crate) unsafe fn is_closure_value(bits: u64) -> bool {
return false;
}
// Check for ClosureHeader magic at offset 8 (type_tag field)
let type_tag = *((ptr as *const u8).add(12) as *const u32);
let type_tag =
*((ptr as *const u8).add(crate::closure::CLOSURE_TYPE_TAG_OFFSET) as *const u32);
type_tag == crate::closure::CLOSURE_MAGIC
} else {
false
Expand Down Expand Up @@ -1204,7 +1205,8 @@ pub(crate) unsafe fn stringify_object_inner(ptr: *const u8, buf: &mut String, de
// in a Next.js render object crashed exactly here). Real closures
// live far above the band.
if crate::value::addr_class::is_above_handle_band(ptr_candidate as usize) {
let type_tag = *(ptr_candidate.add(12) as *const u32);
let type_tag =
*(ptr_candidate.add(crate::closure::CLOSURE_TYPE_TAG_OFFSET) as *const u32);
if type_tag == crate::closure::CLOSURE_MAGIC {
found = true;
break;
Expand Down
6 changes: 5 additions & 1 deletion crates/perry-runtime/src/jsx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,11 @@ fn is_valid_closure(ptr: *const ClosureHeader) -> bool {
if !(0x1000..0x0001_0000_0000_0000).contains(&addr) {
return false;
}
let tag = unsafe { std::ptr::read_volatile((ptr as *const u8).add(12) as *const u32) };
let tag = unsafe {
std::ptr::read_volatile(
(ptr as *const u8).add(crate::closure::CLOSURE_TYPE_TAG_OFFSET) as *const u32,
)
};
tag == CLOSURE_MAGIC
}

Expand Down
3 changes: 2 additions & 1 deletion crates/perry-runtime/src/object/field_set_by_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,8 @@ pub extern "C" fn js_object_set_field_by_name(
// Check if this is a ClosureHeader — closures support dynamic props via separate storage.
// ClosureHeader has CLOSURE_MAGIC (0x434C4F53) at offset 12.
// Without this check, (*obj).keys_array reads capture[0] → corruption/crash.
let type_tag_at_12 = *((obj as *const u8).add(12) as *const u32);
let type_tag_at_12 =
*((obj as *const u8).add(crate::closure::CLOSURE_TYPE_TAG_OFFSET) as *const u32);
if type_tag_at_12 == crate::closure::CLOSURE_MAGIC {
if !key.is_null() {
let name_ptr = (key as *const u8).add(std::mem::size_of::<crate::StringHeader>());
Expand Down
3 changes: 2 additions & 1 deletion crates/perry-runtime/src/object/native_call_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -968,7 +968,8 @@ pub unsafe extern "C" fn js_native_call_method(
// the call result was an empty object stub instead of the
// dynamic-prop closure's return value.
let is_closure = gc_type == crate::gc::GC_TYPE_CLOSURE
|| *((obj as *const u8).add(12) as *const u32) == crate::closure::CLOSURE_MAGIC;
|| *((obj as *const u8).add(crate::closure::CLOSURE_TYPE_TAG_OFFSET) as *const u32)
== crate::closure::CLOSURE_MAGIC;
if is_closure {
let dyn_val = crate::closure::closure_get_dynamic_prop(obj as usize, method_name);
if dyn_val.to_bits() != crate::value::TAG_UNDEFINED {
Expand Down
3 changes: 2 additions & 1 deletion crates/perry-runtime/src/object/object_ops/accessors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ pub extern "C" fn js_object_get_own_field_or_undef(
return f64::from_bits(TAG_UNDEF);
}
// Skip closures sharing the GC_TYPE_OBJECT slot (CLOSURE_MAGIC at +12).
let type_tag_at_12 = *((obj as *const u8).add(12) as *const u32);
let type_tag_at_12 =
*((obj as *const u8).add(crate::closure::CLOSURE_TYPE_TAG_OFFSET) as *const u32);
if type_tag_at_12 == crate::closure::CLOSURE_MAGIC {
return f64::from_bits(TAG_UNDEF);
}
Expand Down
4 changes: 3 additions & 1 deletion crates/perry-runtime/src/symbol/iterator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,9 @@ pub unsafe extern "C" fn js_to_primitive(value: f64, hint: i32) -> f64 {
return value_handle.get_nanbox_f64();
}
// Validate CLOSURE_MAGIC before calling.
let type_tag = std::ptr::read_volatile((closure_ptr as *const u8).add(12) as *const u32);
let type_tag = std::ptr::read_volatile(
(closure_ptr as *const u8).add(crate::closure::CLOSURE_TYPE_TAG_OFFSET) as *const u32,
);
if type_tag != crate::closure::CLOSURE_MAGIC {
return value_handle.get_nanbox_f64();
}
Expand Down
8 changes: 6 additions & 2 deletions crates/perry-runtime/src/symbol/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,9 @@ pub unsafe extern "C" fn js_object_set_symbol_method(
let c_ptr = (c_bits & POINTER_MASK) as *mut crate::closure::ClosureHeader;
if !c_ptr.is_null() && (c_ptr as usize) >= 0x1000 {
// Read the type_tag at offset 12 (layout: func_ptr u64, capture_count u32, type_tag u32).
let type_tag = std::ptr::read_volatile((c_ptr as *const u8).add(12) as *const u32);
let type_tag = std::ptr::read_volatile(
(c_ptr as *const u8).add(crate::closure::CLOSURE_TYPE_TAG_OFFSET) as *const u32,
);
if type_tag == crate::closure::CLOSURE_MAGIC {
let raw_count = (*c_ptr).capture_count;
let real_count = crate::closure::real_capture_count(raw_count);
Expand Down Expand Up @@ -500,7 +502,9 @@ pub unsafe extern "C" fn js_object_set_method_by_name(
if c_tag == POINTER_TAG {
let c_ptr = (c_bits & POINTER_MASK) as *mut crate::closure::ClosureHeader;
if !c_ptr.is_null() && (c_ptr as usize) >= 0x1000 {
let type_tag = std::ptr::read_volatile((c_ptr as *const u8).add(12) as *const u32);
let type_tag = std::ptr::read_volatile(
(c_ptr as *const u8).add(crate::closure::CLOSURE_TYPE_TAG_OFFSET) as *const u32,
);
if type_tag == crate::closure::CLOSURE_MAGIC {
let raw_count = (*c_ptr).capture_count;
let real_count = crate::closure::real_capture_count(raw_count);
Expand Down
17 changes: 17 additions & 0 deletions crates/perry-runtime/src/url/search_params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,23 @@ pub(crate) fn try_read_as_search_params(
return None;
}
unsafe {
// arm64_32 watchOS hardening: validate `params` is a real heap
// `GC_TYPE_OBJECT` *before* dereferencing `class_id` / `keys_array`
// below. Callers guard only with the magnitude check
// `!is_handle_band(ptr)`, which on 32-bit pointers cannot distinguish a
// low heap address from a misclassified non-pointer (e.g. a closure
// whose `CLOSURE_MAGIC` probe missed — see `CLOSURE_TYPE_TAG_OFFSET`).
// Without this, the raw field reads below dereference garbage → SIGSEGV
// (the documented watchOS startup crash, stage 2). `try_read_gc_header`
// rejects the handle band and implausible addresses without touching
// memory; a genuine URLSearchParams is an ordinary `GC_TYPE_OBJECT`
// allocation, so this is a no-op for every value that legitimately
// reaches here (mirrors the guard `is_url_object_shape` already applies
// to the sibling `js_url_href_if_url` probe).
match crate::value::addr_class::try_read_gc_header(params as usize) {
Some(h) if h.obj_type == crate::gc::GC_TYPE_OBJECT => {}
_ => return None,
}
// A genuine URLSearchParams is always allocated with `class_id == 0`
// (an ordinary object, see `create_url_search_params`). Other native
// classes — notably `util.MIMEParams` — ALSO store their data in a
Expand Down
Loading
Loading