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
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/closure/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub use bound::{dispatch_bound_function, dispatch_bound_method, js_function_bind
pub(crate) use errors::reset_throw_not_callable_counter;
pub use errors::throw_not_callable;

pub use validate::{clean_closure_ptr, get_valid_func_ptr};
pub use validate::{clean_closure_ptr, dispatch_proxy_callee_or_throw, get_valid_func_ptr};

pub(crate) use calln::{
dispatch_registered_call, dispatch_rest_or_declared_arity, resolve_call2_direct,
Expand Down
79 changes: 62 additions & 17 deletions crates/perry-runtime/src/closure/dispatch/calln.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use super::*;
pub extern "C" fn js_closure_call0(closure: *const ClosureHeader) -> f64 {
let func_ptr = get_valid_func_ptr(closure);
if func_ptr.is_null() {
throw_not_callable();
return dispatch_proxy_callee_or_throw(closure, &[]);
}
match resolve_strategy(func_ptr) {
DispatchStrategy::BoundMethod => unsafe { dispatch_bound_method(closure, &[]) },
Expand All @@ -34,7 +34,7 @@ pub extern "C" fn js_closure_call0(closure: *const ClosureHeader) -> f64 {
pub extern "C" fn js_closure_call1(closure: *const ClosureHeader, arg0: f64) -> f64 {
let func_ptr = get_valid_func_ptr(closure);
if func_ptr.is_null() {
throw_not_callable();
return dispatch_proxy_callee_or_throw(closure, &[arg0]);
}
match resolve_strategy(func_ptr) {
DispatchStrategy::BoundMethod => unsafe { dispatch_bound_method(closure, &[arg0]) },
Expand Down Expand Up @@ -88,7 +88,7 @@ pub(crate) fn resolve_call2_direct(
pub extern "C" fn js_closure_call2(closure: *const ClosureHeader, arg0: f64, arg1: f64) -> f64 {
let func_ptr = get_valid_func_ptr(closure);
if func_ptr.is_null() {
throw_not_callable();
return dispatch_proxy_callee_or_throw(closure, &[arg0, arg1]);
}
match resolve_strategy(func_ptr) {
DispatchStrategy::BoundMethod => unsafe { dispatch_bound_method(closure, &[arg0, arg1]) },
Expand Down Expand Up @@ -119,7 +119,7 @@ pub extern "C" fn js_closure_call3(
) -> f64 {
let func_ptr = get_valid_func_ptr(closure);
if func_ptr.is_null() {
throw_not_callable();
return dispatch_proxy_callee_or_throw(closure, &[arg0, arg1, arg2]);
}
match resolve_strategy(func_ptr) {
DispatchStrategy::BoundMethod => unsafe {
Expand Down Expand Up @@ -153,7 +153,7 @@ pub extern "C" fn js_closure_call4(
) -> f64 {
let func_ptr = get_valid_func_ptr(closure);
if func_ptr.is_null() {
throw_not_callable();
return dispatch_proxy_callee_or_throw(closure, &[arg0, arg1, arg2, arg3]);
}
match resolve_strategy(func_ptr) {
DispatchStrategy::BoundMethod => unsafe {
Expand Down Expand Up @@ -194,7 +194,7 @@ pub extern "C" fn js_closure_call5(
) -> f64 {
let func_ptr = get_valid_func_ptr(closure);
if func_ptr.is_null() {
throw_not_callable();
return dispatch_proxy_callee_or_throw(closure, &[arg0, arg1, arg2, arg3, arg4]);
}
if func_ptr == BOUND_METHOD_FUNC_PTR {
return unsafe { dispatch_bound_method(closure, &[arg0, arg1, arg2, arg3, arg4]) };
Expand Down Expand Up @@ -238,7 +238,7 @@ pub extern "C" fn js_closure_call6(
) -> f64 {
let func_ptr = get_valid_func_ptr(closure);
if func_ptr.is_null() {
throw_not_callable();
return dispatch_proxy_callee_or_throw(closure, &[arg0, arg1, arg2, arg3, arg4, arg5]);
}
if func_ptr == BOUND_METHOD_FUNC_PTR {
return unsafe { dispatch_bound_method(closure, &[arg0, arg1, arg2, arg3, arg4, arg5]) };
Expand Down Expand Up @@ -321,7 +321,10 @@ pub extern "C" fn js_closure_call7(
) -> f64 {
let func_ptr = get_valid_func_ptr(closure);
if func_ptr.is_null() {
throw_not_callable();
return dispatch_proxy_callee_or_throw(
closure,
&[arg0, arg1, arg2, arg3, arg4, arg5, arg6],
);
}
let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6];
if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) {
Expand Down Expand Up @@ -351,7 +354,10 @@ pub extern "C" fn js_closure_call8(
) -> f64 {
let func_ptr = get_valid_func_ptr(closure);
if func_ptr.is_null() {
throw_not_callable();
return dispatch_proxy_callee_or_throw(
closure,
&[arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7],
);
}
let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7];
if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) {
Expand Down Expand Up @@ -382,7 +388,10 @@ pub extern "C" fn js_closure_call9(
) -> f64 {
let func_ptr = get_valid_func_ptr(closure);
if func_ptr.is_null() {
throw_not_callable();
return dispatch_proxy_callee_or_throw(
closure,
&[arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8],
);
}
let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8];
if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) {
Expand Down Expand Up @@ -426,7 +435,10 @@ pub extern "C" fn js_closure_call10(
) -> f64 {
let func_ptr = get_valid_func_ptr(closure);
if func_ptr.is_null() {
throw_not_callable();
return dispatch_proxy_callee_or_throw(
closure,
&[arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9],
);
}
let args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9];
if let Some(result) = dispatch_registered_call(closure, func_ptr, &args) {
Expand Down Expand Up @@ -472,7 +484,12 @@ pub extern "C" fn js_closure_call11(
) -> f64 {
let func_ptr = get_valid_func_ptr(closure);
if func_ptr.is_null() {
throw_not_callable();
return dispatch_proxy_callee_or_throw(
closure,
&[
arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10,
],
);
}
let args = [
arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10,
Expand Down Expand Up @@ -524,7 +541,12 @@ pub extern "C" fn js_closure_call12(
) -> f64 {
let func_ptr = get_valid_func_ptr(closure);
if func_ptr.is_null() {
throw_not_callable();
return dispatch_proxy_callee_or_throw(
closure,
&[
arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11,
],
);
}
let args = [
arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11,
Expand Down Expand Up @@ -578,7 +600,12 @@ pub extern "C" fn js_closure_call13(
) -> f64 {
let func_ptr = get_valid_func_ptr(closure);
if func_ptr.is_null() {
throw_not_callable();
return dispatch_proxy_callee_or_throw(
closure,
&[
arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12,
],
);
}
let args = [
arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12,
Expand Down Expand Up @@ -634,7 +661,13 @@ pub extern "C" fn js_closure_call14(
) -> f64 {
let func_ptr = get_valid_func_ptr(closure);
if func_ptr.is_null() {
throw_not_callable();
return dispatch_proxy_callee_or_throw(
closure,
&[
arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12,
arg13,
],
);
}
let args = [
arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13,
Expand Down Expand Up @@ -693,7 +726,13 @@ pub extern "C" fn js_closure_call15(
) -> f64 {
let func_ptr = get_valid_func_ptr(closure);
if func_ptr.is_null() {
throw_not_callable();
return dispatch_proxy_callee_or_throw(
closure,
&[
arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12,
arg13, arg14,
],
);
}
let args = [
arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13,
Expand Down Expand Up @@ -756,7 +795,13 @@ pub extern "C" fn js_closure_call16(
) -> f64 {
let func_ptr = get_valid_func_ptr(closure);
if func_ptr.is_null() {
throw_not_callable();
return dispatch_proxy_callee_or_throw(
closure,
&[
arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12,
arg13, arg14, arg15,
],
);
}
let args = [
arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13,
Expand Down
26 changes: 26 additions & 0 deletions crates/perry-runtime/src/closure/dispatch/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,29 @@ pub fn get_valid_func_ptr(closure: *const ClosureHeader) -> *const u8 {
}
func_ptr
}

/// Last resort for a callee that [`get_valid_func_ptr`] rejected: it may be a
/// **Proxy of a function**, not a broken pointer. Route it through the Proxy
/// `[[Call]]` (apply trap, else forwarded to the target); otherwise throw
/// `TypeError: value is not a function` exactly as before.
///
/// #6320. The `js_closure_callN` entry points take a RAW `*const ClosureHeader`
/// — codegen has already stripped the NaN-box tag (`js_closure_unbox_callee_
/// checked`) — so a proxy callee arrives here as its bare registry id
/// (`PROXY_ID_BAND_START + id`). The compiler emits a `ProxyApply` node only
/// when it can statically prove the callee is a proxy; a proxy read out of a
/// dynamically-typed slot (`const g = obj.m` / `arr[0]` / `map.get(k)`, or a
/// method detached by `js_closure_unbind_this`) reaches this generic path with
/// no static hint, and `get_valid_func_ptr` correctly refuses to dereference the
/// id (#5976/#6321) — which turned the SIGSEGV into a spurious TypeError. Node
/// calls the proxy. Re-boxing the id and asking the proxy registry costs nothing
/// on the hot path: this runs only where the code used to throw unconditionally.
pub fn dispatch_proxy_callee_or_throw(closure: *const ClosureHeader, args: &[f64]) -> f64 {
let boxed =
f64::from_bits(crate::value::POINTER_TAG | (closure as u64 & crate::value::POINTER_MASK));
if crate::proxy::js_proxy_is_proxy(boxed) == 1 {
let this_arg = f64::from_bits(crate::value::TAG_UNDEFINED);
return crate::proxy::call_proxy_value_with_this(boxed, this_arg, args);
}
throw_not_callable()
}
15 changes: 9 additions & 6 deletions crates/perry-runtime/src/closure/dynamic_props.rs
Original file line number Diff line number Diff line change
Expand Up @@ -779,15 +779,18 @@ pub extern "C" fn js_closure_unbind_this(val: f64) -> f64 {
return val;
}
let ptr = (bits & 0x0000_FFFF_FFFF_FFFF) as usize;
if ptr < 0x10000 {
// #6320: the old `< 0x10000` floor is an order of magnitude below
// `HANDLE_BAND_MAX`, so a registry handle NaN-boxed under POINTER_TAG — most
// sharply a revocable-Proxy id at `0xF0000 + id` — passed it and the
// CLOSURE_MAGIC probe below dereferenced unmapped low memory. Detaching a
// proxy-valued method (`const g = obj.m` where `obj.m = new Proxy(fn, {})`)
// reaches exactly here. `is_closure_ptr` subsumes the band, heap-range,
// alignment and magic checks; a non-closure value has no `this` slot to
// unbind, so it flows through untouched.
if !is_closure_ptr(ptr) {
return val;
}
// Check CLOSURE_MAGIC
unsafe {
let type_tag = *((ptr as *const u8).add(CLOSURE_TYPE_TAG_OFFSET) as *const u32);
if type_tag != CLOSURE_MAGIC {
return val;
}
let header = ptr as *const ClosureHeader;
let raw_count = (*header).capture_count;
// Only unbind if the closure has the CAPTURES_THIS_FLAG
Expand Down
89 changes: 78 additions & 11 deletions crates/perry-runtime/src/jsx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
//! `lower_call.rs` passes both args as `double` (NaN-boxed), so both adapters
//! are `(f64, f64) -> f64`.

use crate::closure::{js_closure_call1, ClosureHeader, CLOSURE_MAGIC};
use crate::closure::{js_closure_call1, ClosureHeader};
use crate::object::ObjectHeader;
use crate::value::{JSValue, TAG_UNDEFINED};

Expand All @@ -49,6 +49,20 @@ pub extern "C" fn js_jsxs(type_arg: f64, props: f64) -> f64 {
fn dispatch(type_arg: f64, props: f64) -> f64 {
let jsval = JSValue::from_bits(type_arg.to_bits());

// #6320: `<P />` where `P` is a `Proxy(<component fn>)`. A proxy value is a
// small registry id NaN-boxed under POINTER_TAG, not a `ClosureHeader*`;
// `is_valid_closure` used to probe `*(id + 12)` for CLOSURE_MAGIC behind an
// 0x1000 floor and SIGSEGV'd on the unmapped low address. React calls the
// component through the proxy's [[Call]] (apply trap, else forwarded to the
// target), so do the same — components take no `this`, which is exactly what
// the value-call path binds.
if crate::proxy::js_proxy_is_proxy(type_arg) == 1 {
if crate::proxy::proxy_wraps_callable(type_arg) {
return unsafe { crate::closure::js_native_call_value(type_arg, &props, 1) };
}
return f64::from_bits(crate::value::TAG_UNDEFINED);
}

// Function component: call it with props. Its return value is already a
// JSX node (or any value the component produced).
if jsval.is_pointer() {
Expand Down Expand Up @@ -302,17 +316,15 @@ fn render_props_attrs(props: f64) -> String {

/// Validate that `ptr` points at a real `ClosureHeader` (CLOSURE_MAGIC at
/// offset 12) before invoking it as a function component.
///
/// #6320: this used to hand-roll the probe behind an `0x1000` floor, which is
/// an order of magnitude below `HANDLE_BAND_MAX` — every registry handle (proxy
/// id, fetch/zlib stream, stdlib id) sailed through and the `*(addr + 12)` read
/// faulted. `closure::is_closure_ptr` is the single owner of this check: it
/// rejects the handle band, the non-heap addresses, and misaligned pointers
/// before touching memory.
fn is_valid_closure(ptr: *const ClosureHeader) -> bool {
let addr = ptr as u64;
if !(0x1000..0x0001_0000_0000_0000).contains(&addr) {
return false;
}
let tag = unsafe {
std::ptr::read_volatile(
(ptr as *const u8).add(crate::closure::CLOSURE_TYPE_TAG_OFFSET) as *const u32,
)
};
tag == CLOSURE_MAGIC
crate::closure::is_closure_ptr(ptr as usize)
}

#[cfg(test)]
Expand Down Expand Up @@ -384,4 +396,59 @@ mod tests {
let node = js_jsx(str_val("br"), f64::from_bits(TAG_UNDEFINED));
assert_eq!(node_html(node), "<br/>");
}

/// A function component: `(props) => <span>{props.children}</span>`.
extern "C" fn span_component(_closure: *const ClosureHeader, props: f64) -> f64 {
js_jsx(str_val("span"), props)
}

/// Allocate `span_component` as a real capture-free closure value.
fn span_component_value() -> f64 {
let c = crate::closure::js_closure_alloc(span_component as *const u8, 0);
f64::from_bits(JSValue::pointer(c as *const u8).bits())
}

/// Control: a plain closure component still renders.
#[test]
fn closure_function_component_renders() {
let props = obj_with(&[("children", str_val("hi"))]);
let node = js_jsx(span_component_value(), props);
assert_eq!(node_html(node), "<span>hi</span>");
}

/// #6320: `<P />` where `P = new Proxy(Component, {})`. The proxy value is a
/// small registry id, not a `ClosureHeader*`; the old `is_valid_closure`
/// probe read `*(id + 12)` behind an `0x1000` floor and SIGSEGV'd. It must
/// dispatch through the proxy's [[Call]] (here: no trap → forward to the
/// target) and render the component.
#[test]
fn proxy_wrapped_function_component_dispatches_through_call() {
let proxy = crate::proxy::js_proxy_new(span_component_value(), obj_with(&[]));
let props = obj_with(&[("children", str_val("hi"))]);
let node = js_jsx(proxy, props);
assert_eq!(node_html(node), "<span>hi</span>");
}

/// A proxy over a NON-callable target is not a component — it must return
/// undefined rather than fault or render garbage.
#[test]
fn proxy_over_non_callable_target_is_not_a_component() {
let proxy = crate::proxy::js_proxy_new(obj_with(&[]), obj_with(&[]));
let node = js_jsx(proxy, obj_with(&[]));
assert_eq!(node.to_bits(), TAG_UNDEFINED);
}

/// The validator itself must reject every small-handle id (proxy ids, fetch
/// / zlib streams, stdlib registry handles) WITHOUT dereferencing them.
#[test]
fn is_valid_closure_rejects_the_handle_band() {
for handle in [
0x1usize, 0x1000, 0x10000, 0x40000, 0xE0000, 0xF000D, 0xF_FFF8,
] {
assert!(
!is_valid_closure(handle as *const ClosureHeader),
"handle {handle:#x} must not be probed as a ClosureHeader"
);
}
}
}
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ use std::collections::HashMap;
use crate::closure::{js_closure_call0, js_closure_call1, js_closure_call2, js_closure_call3};

mod apply_construct;
pub use apply_construct::{call_proxy_value_with_this, js_proxy_apply, js_proxy_construct};
pub(crate) use apply_construct::{is_callable_function, is_constructor_function};
pub use apply_construct::{js_proxy_apply, js_proxy_construct};
mod has_delete;
pub(crate) use has_delete::reflect_ordinary_delete_property_key;
pub use has_delete::{js_proxy_delete, js_proxy_has};
Expand Down
Loading
Loading