From 3bb3a8c02cb7f855c20c2e7ffce088d9b7396219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 2 Sep 2026 14:18:05 +0200 Subject: [PATCH 1/4] fix(runtime): root every saved implicit-`this` across the user code it brackets (#9445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every `let prev = js_implicit_this_set(receiver); …user code…; js_implicit_this_set(prev)` in the runtime held the caller's receiver in a bare Rust local across a call that allocates. An evacuating young-gen minor inside the window moves that object; the restore then installed a retired from-space address as the caller's `this`, which reads as `undefined` on the next member access (the #9417 shape). Root the saved value in a RuntimeHandleScope and re-read it at the restore — the idiom PR #9444 used for the accessor sites — at all 121 remaining sites, plus the receivers that three of those sites consume again after the call. Claude-Session: https://claude.ai/code/session_01L11XMMWrR9Wz11dHpq4gXS --- crates/perry-runtime/src/array/iterator.rs | 48 ++++++++++++------- crates/perry-runtime/src/async_hooks.rs | 7 ++- .../src/child_process/emitter.rs | 15 +++--- .../src/closure/dispatch/bound.rs | 10 ++-- .../src/closure/dynamic_props.rs | 22 +++++---- crates/perry-runtime/src/cluster.rs | 10 ++-- crates/perry-runtime/src/collection_iter.rs | 5 +- crates/perry-runtime/src/dgram.rs | 5 +- crates/perry-runtime/src/disposable.rs | 5 +- crates/perry-runtime/src/event_target.rs | 6 ++- .../src/fs/dir_glob_watch/watch.rs | 21 +++++--- .../src/fs/stream/write_file_input.rs | 5 +- crates/perry-runtime/src/intl/subclass.rs | 10 ++-- crates/perry-runtime/src/json/replacer.rs | 5 +- crates/perry-runtime/src/json/reviver.rs | 5 +- crates/perry-runtime/src/json/stringify.rs | 12 +++-- .../src/json/stringify_scalars.rs | 5 +- crates/perry-runtime/src/map.rs | 5 +- crates/perry-runtime/src/messaging.rs | 6 ++- .../src/node_api_host/functions.rs | 7 ++- crates/perry-runtime/src/node_inspector.rs | 5 +- crates/perry-runtime/src/node_repl.rs | 5 +- crates/perry-runtime/src/node_stream.rs | 15 +++--- .../src/node_stream_destroy_state.rs | 5 +- .../src/node_stream_event_emitter.rs | 5 +- .../src/node_stream_readwrite.rs | 20 ++++---- .../src/node_submodules/consumers.rs | 5 +- .../src/node_submodules/diagnostics.rs | 5 +- .../src/node_submodules/stream_promises.rs | 5 +- .../perry-runtime/src/node_submodules/test.rs | 4 +- .../src/node_submodules/trace_events.rs | 7 ++- crates/perry-runtime/src/node_vm/modules.rs | 8 +++- .../src/object/class_constructors.rs | 6 ++- .../object/class_registry/parent_static.rs | 35 +++++++++----- .../parent_static/private_and_dynamic.rs | 25 ++++++---- .../class_registry/prototype_objects.rs | 5 +- .../src/object/date_proto_thunks.rs | 5 +- .../src/object/descriptor_state.rs | 5 +- .../src/object/field_set_by_name/tail.rs | 12 +++-- .../src/object/global_this/bigint_promise.rs | 5 +- .../src/object/global_this/fetch_globals.rs | 15 ++++-- .../src/object/native_call_method.rs | 18 ++++--- .../native_call_method/handle_methods.rs | 5 +- .../object/native_call_method/object_proto.rs | 5 +- .../perry-runtime/src/object/property_key.rs | 22 +++++---- .../perry-runtime/src/object/this_binding.rs | 25 ++++++++++ .../perry-runtime/src/os_process_streams.rs | 31 +++++++----- .../perry-runtime/src/promise/assimilate.rs | 10 ++-- .../perry-runtime/src/promise/async_step.rs | 5 +- .../src/promise/checked_dispatch.rs | 20 ++++---- .../src/promise/spec_combinators.rs | 5 +- crates/perry-runtime/src/promise/then.rs | 5 +- crates/perry-runtime/src/proxy.rs | 21 ++++---- .../src/proxy/apply_construct.rs | 15 +++--- crates/perry-runtime/src/proxy/reflect.rs | 15 +++--- crates/perry-runtime/src/pty/mod.rs | 5 +- crates/perry-runtime/src/regex/replace_fn.rs | 7 ++- crates/perry-runtime/src/set.rs | 5 +- crates/perry-runtime/src/symbol/accessors.rs | 5 +- crates/perry-runtime/src/symbol/iterator.rs | 19 +++++--- crates/perry-runtime/src/timer.rs | 19 +++++--- crates/perry-runtime/src/typedarray_props.rs | 10 ++-- crates/perry-runtime/src/url/search_params.rs | 6 ++- crates/perry-runtime/src/util_promisify.rs | 5 +- crates/perry-runtime/src/value/to_string.rs | 20 ++++---- .../src/value/to_string_class_ref.rs | 5 +- 66 files changed, 467 insertions(+), 257 deletions(-) diff --git a/crates/perry-runtime/src/array/iterator.rs b/crates/perry-runtime/src/array/iterator.rs index d479cabfdc..1b3ffffd88 100644 --- a/crates/perry-runtime/src/array/iterator.rs +++ b/crates/perry-runtime/src/array/iterator.rs @@ -481,8 +481,9 @@ fn async_from_sync_call_raw(iter: f64, method: &[u8], args: &[f64]) -> Result Result f64 { if !is_callable_value(method) { throw_iterator_method_not_callable(); } - let prev_this = crate::object::js_implicit_this_set(value); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(value)); let iterator = unsafe { crate::closure::js_native_call_value(method, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); // GetIterator step 5: the result must be an Object. if !is_async_iterator_object(iterator) { throw_iterator_result_not_object(); @@ -1301,9 +1304,10 @@ fn throw_iterator_result_not_object() -> ! { pub extern "C" fn js_iterator_next_result(iter_f64: f64) -> f64 { let next = named_field(iter_f64, b"next"); let result = if is_callable_value(next) { - let prev_this = crate::object::js_implicit_this_set(iter_f64); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(iter_f64)); let result = unsafe { crate::closure::js_native_call_value(next, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } else if next.to_bits() == crate::value::TAG_UNDEFINED && is_builtin_iterator_class_id(crate::value::js_nanbox_get_pointer(iter_f64) as usize) @@ -1363,9 +1367,10 @@ pub extern "C" fn js_iterator_close_if_not_done(iter_f64: f64, done_f64: f64) -> crate::closure::throw_not_callable(); } - let prev_this = crate::object::js_implicit_this_set(iter_f64); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(iter_f64)); let result = unsafe { crate::closure::js_native_call_value(ret, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if !is_object_like_value(result) { throw_iterator_result_not_object(); } @@ -1439,9 +1444,11 @@ pub(crate) fn sync_iterator_to_array_if_not_async(iter_f64: f64) -> Option<*mut } else { // Call(next, iterator) — bind `this` like `js_iterator_to_array` // does for its stored-closure path (#9019). - let prev_this = crate::object::js_implicit_this_set(iter_f64); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(iter_f64)); let r = closure::js_closure_call1(next_ptr, f64::from_bits(TAG_UNDEFINED)); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); r }; if crate::promise::js_value_is_promise(step) != 0 { @@ -1477,9 +1484,10 @@ pub(crate) fn call_symbol_async_iterator(value: f64) -> Option { if !is_callable_value(method) { return None; } - let prev_this = crate::object::js_implicit_this_set(value); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(value)); let iterator = unsafe { crate::closure::js_native_call_value(method, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if iterator.to_bits() == crate::value::TAG_UNDEFINED { None } else { @@ -1633,12 +1641,14 @@ pub extern "C" fn js_iterator_to_array(iter_f64: f64) -> *mut ArrayHeader { // Call(next, iterator): bind `this` for the stored-closure path // exactly like `js_iterator_next_result` — a user-assigned // `it.next = function () { … }` may read `this` (#9019). - let prev_this = crate::object::js_implicit_this_set(iter_h.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope + .root_nanbox_f64(crate::object::js_implicit_this_set(iter_h.get_nanbox_f64())); let r = closure::js_closure_call1( js_nanbox_get_pointer(next_h.get_nanbox_f64()) as *const closure::ClosureHeader, f64::from_bits(TAG_UNDEFINED), ); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); r }; // IteratorNext (ECMA-262 §7.4.2 step 3): if Type(result) is not @@ -1755,9 +1765,11 @@ fn js_async_iterator_to_array(iter_f64: f64) -> *mut ArrayHeader { } else { // Call(next, iterator) — bind `this` for the stored-closure path // (#9019), mirroring `js_iterator_to_array`. - let prev_this = crate::object::js_implicit_this_set(iter_f64); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(iter_f64)); let r = closure::js_closure_call1(next_ptr, f64::from_bits(TAG_UNDEFINED)); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); r }; let Some(step_result) = settled_promise_value(step) else { diff --git a/crates/perry-runtime/src/async_hooks.rs b/crates/perry-runtime/src/async_hooks.rs index e73c8985e1..28703c7f53 100644 --- a/crates/perry-runtime/src/async_hooks.rs +++ b/crates/perry-runtime/src/async_hooks.rs @@ -1811,7 +1811,10 @@ fn call_callback_with_rest(callback_value: f64, this_arg: f64, rest: f64) -> f64 } let args_array = ptr_from_nanboxed(rest) as *const ArrayHeader; let args_array_handle = scope.root_raw_const_ptr(args_array); - let prev_this = crate::object::js_implicit_this_set(this_arg_handle.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + this_arg_handle.get_nanbox_f64(), + )); let result = if args_array.is_null() { unsafe { js_closure_call_array(callback as i64, ptr::null(), 0) } } else { @@ -1824,7 +1827,7 @@ fn call_callback_with_rest(callback_value: f64, this_arg: f64, rest: f64) -> f64 }; unsafe { js_closure_call_array(callback as i64, data, len) } }; - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/child_process/emitter.rs b/crates/perry-runtime/src/child_process/emitter.rs index 5a3768e0fb..aca68e59b4 100644 --- a/crates/perry-runtime/src/child_process/emitter.rs +++ b/crates/perry-runtime/src/child_process/emitter.rs @@ -61,11 +61,12 @@ pub(crate) fn cp_emit(target: f64, event: &str, args: &[f64]) -> bool { break; } let cb = crate::array::js_array_get_f64(arr, i); - let prev = js_implicit_this_set(target); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(js_implicit_this_set(target)); unsafe { let _ = js_native_call_value(cb, args.as_ptr(), args.len()); } - js_implicit_this_set(prev); + js_implicit_this_set(prev.get_nanbox_f64()); fired = true; i += 1; } @@ -255,12 +256,13 @@ pub(crate) extern "C" fn cp_pipe_data_thunk(closure: *const ClosureHeader, chunk let dest = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); let write = cp_get_field(dest, b"write"); if !crate::fs::extract_closure_ptr(write).is_null() { - let prev = js_implicit_this_set(dest); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(js_implicit_this_set(dest)); let args = [chunk]; unsafe { let _ = js_native_call_value(write, args.as_ptr(), args.len()); } - js_implicit_this_set(prev); + js_implicit_this_set(prev.get_nanbox_f64()); } cp_undefined() } @@ -272,12 +274,13 @@ pub(crate) extern "C" fn cp_pipe_end_thunk(closure: *const ClosureHeader) -> f64 let dest = f64::from_bits(js_closure_get_capture_ptr(closure, 0) as u64); let end = cp_get_field(dest, b"end"); if !crate::fs::extract_closure_ptr(end).is_null() { - let prev = js_implicit_this_set(dest); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(js_implicit_this_set(dest)); let args = [cp_undefined()]; unsafe { let _ = js_native_call_value(end, args.as_ptr(), 0); } - js_implicit_this_set(prev); + js_implicit_this_set(prev.get_nanbox_f64()); } cp_undefined() } diff --git a/crates/perry-runtime/src/closure/dispatch/bound.rs b/crates/perry-runtime/src/closure/dispatch/bound.rs index 74d753cc2b..fdec738ae9 100644 --- a/crates/perry-runtime/src/closure/dispatch/bound.rs +++ b/crates/perry-runtime/src/closure/dispatch/bound.rs @@ -214,7 +214,8 @@ unsafe fn dispatch_symbol_bound_method( // the direct-call path. The one-shot static-`this` override (armed by // the Function.prototype call/apply arms for a static bound-method // value) still wins in the static-method prologue. - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); crate::object::static_private_owner_push(receiver); let result = crate::object::call_registered_static_method( func_ptr, @@ -224,7 +225,7 @@ unsafe fn dispatch_symbol_bound_method( has_rest, ); crate::object::static_private_owner_pop(); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } else { // Computed symbol methods never synthesize an `arguments` object but @@ -267,14 +268,15 @@ pub unsafe fn dispatch_bound_function(closure: *const ClosureHeader, args: &[f64 // slot, not IMPLICIT_THIS — rebind it to the bound receiver so the bound // `this` is honored (arrows/non-captures_this targets are returned as-is). let target = rebind_explicit_this(target, bound_this); - let prev_this = crate::object::js_implicit_this_set(bound_this); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(bound_this)); let (call_ptr, call_len) = if combined.is_empty() { (std::ptr::null::(), 0usize) } else { (combined.as_ptr(), combined.len()) }; let result = js_native_call_value(target, call_ptr, call_len); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/closure/dynamic_props.rs b/crates/perry-runtime/src/closure/dynamic_props.rs index 40292d97a8..b6b804d9bd 100644 --- a/crates/perry-runtime/src/closure/dynamic_props.rs +++ b/crates/perry-runtime/src/closure/dynamic_props.rs @@ -600,9 +600,10 @@ pub fn closure_get_dynamic_prop(ptr: usize, prop: &str) -> f64 { return f64::from_bits(crate::value::TAG_UNDEFINED); } let receiver = crate::value::js_nanbox_pointer(ptr as i64); - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_closure_call0(closure); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return result; } @@ -661,9 +662,11 @@ pub fn closure_get_dynamic_prop(ptr: usize, prop: &str) -> f64 { if getter.is_null() { return f64::from_bits(crate::value::TAG_UNDEFINED); } - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_closure_call0(getter); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return result; } if let Ok(props) = get_closure_props().lock() { @@ -692,9 +695,10 @@ pub fn closure_get_dynamic_prop(ptr: usize, prop: &str) -> f64 { if getter.is_null() { return f64::from_bits(crate::value::TAG_UNDEFINED); } - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_closure_call0(getter); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return result; } { @@ -724,9 +728,11 @@ pub fn closure_get_dynamic_prop(ptr: usize, prop: &str) -> f64 { (acc.get & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; if !getter.is_null() { let receiver = crate::value::js_nanbox_pointer(ptr as i64); - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_closure_call0(getter); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return result; } } diff --git a/crates/perry-runtime/src/cluster.rs b/crates/perry-runtime/src/cluster.rs index aa4284e025..dee5698e39 100644 --- a/crates/perry-runtime/src/cluster.rs +++ b/crates/perry-runtime/src/cluster.rs @@ -305,11 +305,12 @@ pub(crate) fn cluster_emit_event(event: &str, args: &[f64]) -> bool { } for listener in listeners { let cb = f64::from_bits(listener.callback_bits); - let prev = js_implicit_this_set(cluster_default_value()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(js_implicit_this_set(cluster_default_value())); unsafe { let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); } - js_implicit_this_set(prev); + js_implicit_this_set(prev.get_nanbox_f64()); } true } @@ -1423,11 +1424,12 @@ fn emit(target: f64, event: &str, args: &[f64]) -> bool { break; } let cb = crate::array::js_array_get_f64(arr, i); - let prev = js_implicit_this_set(target); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(js_implicit_this_set(target)); unsafe { let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); } - js_implicit_this_set(prev); + js_implicit_this_set(prev.get_nanbox_f64()); fired = true; i += 1; } diff --git a/crates/perry-runtime/src/collection_iter.rs b/crates/perry-runtime/src/collection_iter.rs index 852ae7cea5..44d5c801b4 100644 --- a/crates/perry-runtime/src/collection_iter.rs +++ b/crates/perry-runtime/src/collection_iter.rs @@ -276,11 +276,12 @@ pub(crate) fn call_with_this_capturing_throw( this_value: f64, args: &[f64], ) -> Result { - let prev_this = crate::object::js_implicit_this_set(this_value); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_value)); let result = call_capturing_throw(|| unsafe { crate::closure::js_native_call_value(callee, args.as_ptr(), args.len()) }); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/dgram.rs b/crates/perry-runtime/src/dgram.rs index 17b151ee4d..67cc2a3115 100644 --- a/crates/perry-runtime/src/dgram.rs +++ b/crates/perry-runtime/src/dgram.rs @@ -752,10 +752,11 @@ pub(crate) fn call_function(callback: f64, this: f64, args: &[f64]) -> f64 { if !is_callable_value(callback) { return undefined_value(); } - let prev = crate::object::js_implicit_this_set(this); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); let result = unsafe { crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/disposable.rs b/crates/perry-runtime/src/disposable.rs index e07de3529c..225ad6cc64 100644 --- a/crates/perry-runtime/src/disposable.rs +++ b/crates/perry-runtime/src/disposable.rs @@ -103,9 +103,10 @@ extern "C" fn bound_dispose_thunk(closure: *const ClosureHeader) -> f64 { if !is_callable_value(method) { return undefined(); } - let prev = js_implicit_this_set(resource); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(js_implicit_this_set(resource)); let result = unsafe { js_native_call_value(method, std::ptr::null(), 0) }; - js_implicit_this_set(prev); + js_implicit_this_set(prev.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/event_target.rs b/crates/perry-runtime/src/event_target.rs index e31860cf8b..fabb6e1e25 100644 --- a/crates/perry-runtime/src/event_target.rs +++ b/crates/perry-runtime/src/event_target.rs @@ -986,9 +986,11 @@ pub unsafe extern "C" fn js_event_target_dispatch_event( if once { remove_event_listener_value_with_capture(target, event_name_ptr, callback, capture); } - let prev_this = crate::object::js_implicit_this_set(target_value); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(target_value)); let _ = crate::closure::js_native_call_value(callable, args.as_ptr(), args.len()); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if event_bool_field(event_ptr, b"_immediateStopped") { break; } diff --git a/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs b/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs index 4b392a3571..d833dd306b 100644 --- a/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs +++ b/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs @@ -518,11 +518,14 @@ fn emit_listener0(object_value: f64, callback: f64) { if cb.is_null() { return; } - let prev_this = crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + object_handle.get_nanbox_f64(), + )); with_watcher_uncaught_trap(|| { crate::closure::js_closure_call0(cb); }); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } fn emit_fs_watch_event( @@ -551,11 +554,14 @@ fn emit_fs_watch_event( if cb.is_null() { continue; } - let prev_this = crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + object_handle.get_nanbox_f64(), + )); with_watcher_uncaught_trap(|| { crate::closure::js_closure_call2(cb, refreshed_args[0], refreshed_args[1]); }); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } @@ -586,11 +592,14 @@ fn emit_watch_file_change( if cb.is_null() { continue; } - let prev_this = crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + object_handle.get_nanbox_f64(), + )); with_watcher_uncaught_trap(|| { crate::closure::js_closure_call2(cb, refreshed_args[0], refreshed_args[1]); }); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } diff --git a/crates/perry-runtime/src/fs/stream/write_file_input.rs b/crates/perry-runtime/src/fs/stream/write_file_input.rs index 95fa53d0d8..600aae4ccd 100644 --- a/crates/perry-runtime/src/fs/stream/write_file_input.rs +++ b/crates/perry-runtime/src/fs/stream/write_file_input.rs @@ -150,9 +150,10 @@ fn well_known_iterator_method(value: f64, name: &str) -> Option { fn call_well_known_iterator(value: f64, name: &str) -> Option { let method = well_known_iterator_method(value, name)?; - let prev_this = crate::object::js_implicit_this_set(value); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(value)); let iterator = unsafe { crate::closure::js_native_call_value(method, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if iterator.to_bits() == crate::value::TAG_UNDEFINED { None } else { diff --git a/crates/perry-runtime/src/intl/subclass.rs b/crates/perry-runtime/src/intl/subclass.rs index 45b7886527..d2f397e50e 100644 --- a/crates/perry-runtime/src/intl/subclass.rs +++ b/crates/perry-runtime/src/intl/subclass.rs @@ -147,13 +147,17 @@ pub(crate) unsafe fn intl_subclass_super( if !is_intl_constructor_value(parent_val) { return false; } - let prev_this = crate::object::js_implicit_this_set(this_box); + // #9445: the parent constructor is user-reachable code; root the displaced + // receiver AND `this_box`, which is consumed again after the call. + let this_scope = crate::gc::RuntimeHandleScope::new(); + let this_h = this_scope.root_nanbox_f64(this_box); + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_box)); let prev_nt = crate::object::js_new_target_set(parent_val); let instance = crate::closure::js_native_call_value(parent_val, args_ptr, args_len); crate::object::js_new_target_set(prev_nt); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); // Re-home the freshly-built instance's brand + bound methods onto `this`. - let this_bits = this_box.to_bits(); + let this_bits = this_h.get_nanbox_f64().to_bits(); if (this_bits >> 48) == 0x7FFD { let dst = (this_bits & 0x0000_FFFF_FFFF_FFFF) as i64; if dst >= 0x10000 { diff --git a/crates/perry-runtime/src/json/replacer.rs b/crates/perry-runtime/src/json/replacer.rs index 8afd759808..b98651c75c 100644 --- a/crates/perry-runtime/src/json/replacer.rs +++ b/crates/perry-runtime/src/json/replacer.rs @@ -66,9 +66,10 @@ pub(crate) unsafe fn call_replacer( value_f64: f64, holder_f64: f64, ) -> f64 { - let prev_this = crate::object::js_implicit_this_set(holder_f64); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(holder_f64)); let result = crate::js_closure_call2(replacer, key_f64, value_f64); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); // The user callback may have installed/removed `Object.prototype.toJSON` // (#6009 fast-probe cache). super::invalidate_object_proto_tojson_state(); diff --git a/crates/perry-runtime/src/json/reviver.rs b/crates/perry-runtime/src/json/reviver.rs index 3894f931b2..6e74da888d 100644 --- a/crates/perry-runtime/src/json/reviver.rs +++ b/crates/perry-runtime/src/json/reviver.rs @@ -715,10 +715,11 @@ unsafe fn call_reviver( let holder_arg = holder_handle.get_nanbox_f64(); let key_arg = key_handle.get_nanbox_f64(); let value_arg = value_handle.get_nanbox_f64(); - let prev_this = crate::object::js_implicit_this_set(holder_arg); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(holder_arg)); let result = crate::js_closure_call3(reviver, key_arg, value_arg, context_handle.get_nanbox_f64()); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); let result_bits = result.to_bits(); let revived_bits = if result_bits == value_arg.to_bits() { value_handle.get_nanbox_u64() diff --git a/crates/perry-runtime/src/json/stringify.rs b/crates/perry-runtime/src/json/stringify.rs index 44b957d0b8..f3db707484 100644 --- a/crates/perry-runtime/src/json/stringify.rs +++ b/crates/perry-runtime/src/json/stringify.rs @@ -309,9 +309,10 @@ pub(crate) unsafe fn object_get_to_json(ptr: *const u8) -> Option { // record it in `TO_JSON_KEY` before recursing here. let key_f64_arg = current_to_json_key_arg(); - let prev_this = crate::object::js_implicit_this_set(recv); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); let result = crate::closure::js_native_call_value(f64::from_bits(bound), &key_f64_arg, 1); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); // The user callback may have installed/removed `Object.prototype.toJSON`. invalidate_object_proto_tojson_state(); Some(result) @@ -346,9 +347,12 @@ pub(crate) unsafe fn array_get_to_json(arr: *const crate::ArrayHeader) -> Option let recv_handle = scope.root_nanbox_f64(recv); // `toJSON(key)` receives the property key of this array value (#5909). let key_f64_arg = current_to_json_key_arg(); - let prev_this = crate::object::js_implicit_this_set(recv_handle.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + recv_handle.get_nanbox_f64(), + )); let result = crate::closure::js_native_call_value(f64::from_bits(method_bits), &key_f64_arg, 1); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); // The user callback may have installed/removed `Object.prototype.toJSON`. invalidate_object_proto_tojson_state(); Some(result) diff --git a/crates/perry-runtime/src/json/stringify_scalars.rs b/crates/perry-runtime/src/json/stringify_scalars.rs index 18fa1ff38b..ab6083939a 100644 --- a/crates/perry-runtime/src/json/stringify_scalars.rs +++ b/crates/perry-runtime/src/json/stringify_scalars.rs @@ -231,9 +231,10 @@ pub(crate) unsafe fn bigint_apply_to_json(value: f64) -> Option { let recv = value_handle.get_nanbox_f64(); // `toJSON(key)` receives the property key of this BigInt value (#5909). let key_f64_arg = super::stringify_tojson_probe::current_to_json_key_arg(); - let prev_this = crate::object::js_implicit_this_set(recv); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); let result = crate::closure::js_native_call_value(f64::from_bits(method_bits), &key_f64_arg, 1); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); // The user callback may have installed/removed `Object.prototype.toJSON`. invalidate_object_proto_tojson_state(); Some(result) diff --git a/crates/perry-runtime/src/map.rs b/crates/perry-runtime/src/map.rs index 6d2f5beffd..3a7b790383 100644 --- a/crates/perry-runtime/src/map.rs +++ b/crates/perry-runtime/src/map.rs @@ -3154,9 +3154,10 @@ fn js_map_foreach_impl( // Bind `thisArg` for the duration of the call (matches the // URLSearchParams.forEach pattern); `js_native_call_value` // dispatches the NaN-boxed callback with the full arg vector. - let prev_this = crate::object::js_implicit_this_set(this_v); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_v)); let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } let map = map_handle.get_raw_const_ptr::(); diff --git a/crates/perry-runtime/src/messaging.rs b/crates/perry-runtime/src/messaging.rs index a55a560344..e87b5f3f1a 100644 --- a/crates/perry-runtime/src/messaging.rs +++ b/crates/perry-runtime/src/messaging.rs @@ -279,7 +279,9 @@ fn invoke_message_handler(handler: f64, event: f64, port_box: f64) { let handler_h = scope.root_nanbox_f64(handler); let event_h = scope.root_nanbox_f64(event); let port_h = scope.root_nanbox_f64(port_box); - let prev_this = object::js_implicit_this_set(port_h.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(object::js_implicit_this_set(port_h.get_nanbox_f64())); let args = [event_h.get_nanbox_f64()]; unsafe { let _ = crate::closure::js_native_call_value( @@ -288,7 +290,7 @@ fn invoke_message_handler(handler: f64, event: f64, port_box: f64) { args.len(), ); } - object::js_implicit_this_set(prev_this); + object::js_implicit_this_set(prev_this.get_nanbox_f64()); } /// Macrotask body: deliver exactly one queued message to `port_box`'s port. diff --git a/crates/perry-runtime/src/node_api_host/functions.rs b/crates/perry-runtime/src/node_api_host/functions.rs index 4de6d8bad3..db56b1514d 100644 --- a/crates/perry-runtime/src/node_api_host/functions.rs +++ b/crates/perry-runtime/src/node_api_host/functions.rs @@ -277,7 +277,10 @@ pub unsafe extern "C" fn napi_call_function( }; arguments.push(f64::from_bits(bits)); } - let previous_this = crate::object::js_implicit_this_set(f64::from_bits(receiver_bits)); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let previous_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + f64::from_bits(receiver_bits), + )); let call_result = catch_value_call(env, || { crate::closure::js_native_call_value( f64::from_bits(function_bits), @@ -285,7 +288,7 @@ pub unsafe extern "C" fn napi_call_function( arguments.len(), ) }); - crate::object::js_implicit_this_set(previous_this); + crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); match call_result { Ok(value) => { if !result.is_null() { diff --git a/crates/perry-runtime/src/node_inspector.rs b/crates/perry-runtime/src/node_inspector.rs index e147f62996..f7c6fb4ca8 100644 --- a/crates/perry-runtime/src/node_inspector.rs +++ b/crates/perry-runtime/src/node_inspector.rs @@ -255,10 +255,11 @@ fn call_function(callback: f64, this: f64, args: &[f64]) -> f64 { if !is_callable_value(callback) { return undefined(); } - let prev = crate::object::js_implicit_this_set(this); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); let result = unsafe { crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/node_repl.rs b/crates/perry-runtime/src/node_repl.rs index 1baf8f4b23..9b8656ae9f 100644 --- a/crates/perry-runtime/src/node_repl.rs +++ b/crates/perry-runtime/src/node_repl.rs @@ -168,10 +168,11 @@ fn call_function(callback: f64, this: f64, args: &[f64]) -> f64 { callback.to_bits(), this, )); - let prev = crate::object::js_implicit_this_set(this); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); let result = unsafe { crate::closure::js_native_call_value(rebound, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/node_stream.rs b/crates/perry-runtime/src/node_stream.rs index 5ffe59e656..722648d1a6 100644 --- a/crates/perry-runtime/src/node_stream.rs +++ b/crates/perry-runtime/src/node_stream.rs @@ -1163,11 +1163,12 @@ fn invoke_writable_write(stream: f64, chunk: f64, enc: f64, len: f64, callback: js_closure_set_capture_f64(cb, 2, callback); let cb_value = f64::from_bits(JSValue::pointer(cb as *const u8).bits()); let args = [chunk, enc, cb_value]; - let prev_this = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { let _ = crate::closure::js_native_call_value(write, args.as_ptr(), args.len()); } - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } else { throw_missing_stream_method("The _write() method is not implemented"); } @@ -1178,11 +1179,12 @@ fn invoke_writable_writev(stream: f64, chunks: f64) { let cb = js_closure_alloc(writable_write_callback_noop as *const u8, 0); let cb_value = f64::from_bits(JSValue::pointer(cb as *const u8).bits()); let args = [chunks, cb_value]; - let prev_this = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { let _ = crate::closure::js_native_call_value(writev, args.as_ptr(), args.len()); } - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } @@ -1222,11 +1224,12 @@ fn invoke_transform_write(stream: f64, chunk: f64, enc: f64, len: f64, callback: js_closure_set_capture_f64(cb, 2, callback); let cb_value = f64::from_bits(JSValue::pointer(cb as *const u8).bits()); let args = [chunk, enc, cb_value]; - let prev_this = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { let _ = crate::closure::js_native_call_value(transform, args.as_ptr(), args.len()); } - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return; } throw_missing_stream_method("The _transform() method is not implemented"); diff --git a/crates/perry-runtime/src/node_stream_destroy_state.rs b/crates/perry-runtime/src/node_stream_destroy_state.rs index 3b591207a2..71a83596b9 100644 --- a/crates/perry-runtime/src/node_stream_destroy_state.rs +++ b/crates/perry-runtime/src/node_stream_destroy_state.rs @@ -66,11 +66,12 @@ pub(super) fn destroy_stream(stream: f64, err: f64) { err }; let args = [destroy_arg, cb_value]; - let prev_this = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { let _ = crate::closure::js_native_call_value(destroy, args.as_ptr(), args.len()); } - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return; } } diff --git a/crates/perry-runtime/src/node_stream_event_emitter.rs b/crates/perry-runtime/src/node_stream_event_emitter.rs index a4b3a40bd8..54a19a3eca 100644 --- a/crates/perry-runtime/src/node_stream_event_emitter.rs +++ b/crates/perry-runtime/src/node_stream_event_emitter.rs @@ -670,10 +670,11 @@ pub(super) fn call_listener_args(stream: f64, listener: f64, args: &[f64]) -> f6 if !is_callable_value(listener) { return f64::from_bits(super::TAG_UNDEFINED); } - let prev = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); let result = unsafe { crate::closure::js_native_call_value(listener, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/node_stream_readwrite.rs b/crates/perry-runtime/src/node_stream_readwrite.rs index cfec091967..0af8609b77 100644 --- a/crates/perry-runtime/src/node_stream_readwrite.rs +++ b/crates/perry-runtime/src/node_stream_readwrite.rs @@ -710,12 +710,13 @@ pub(super) fn schedule_writable_finish(stream: f64, callback: Option) { callback.unwrap_or_else(|| f64::from_bits(TAG_UNDEFINED)), ); let cb_value = f64::from_bits(JSValue::pointer(cb as *const u8).bits()); - let prev_this = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { let _ = crate::closure::js_native_call_value(final_callback, [cb_value].as_ptr(), 1); } - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return; } } @@ -1036,11 +1037,12 @@ pub(super) fn finish_transform_stream(stream: f64, callback: Option) -> boo callback.unwrap_or_else(|| f64::from_bits(TAG_UNDEFINED)), ); let cb_value = f64::from_bits(JSValue::pointer(cb as *const u8).bits()); - let prev_this = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { let _ = crate::closure::js_native_call_value(flush, [cb_value].as_ptr(), 1); } - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); true } @@ -1312,11 +1314,12 @@ pub(super) fn invoke_construct_callback(stream: f64, opts: f64) { let cb = js_closure_alloc(ns_construct_callback_done as *const u8, 1); js_closure_set_capture_f64(cb, 0, stream); let cb_value = f64::from_bits(JSValue::pointer(cb as *const u8).bits()); - let prev_this = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { let _ = crate::closure::js_native_call_value(construct, [cb_value].as_ptr(), 1); } - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } pub(super) fn invoke_read_once(stream: f64) { @@ -1346,11 +1349,12 @@ fn invoke_read_once_inner(stream: f64, emit_default_error: bool) { } set_hidden_value(stream, hidden_read_invoked_key(), f64::from_bits(TAG_TRUE)); let size = get_hidden_value(stream, hidden_hwm_key()).unwrap_or_else(|| default_hwm(false)); - let prev_this = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); unsafe { let _ = crate::closure::js_native_call_value(read, [size].as_ptr(), 1); } - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } pub(super) fn maybe_emit_default_read_error(stream: f64) { diff --git a/crates/perry-runtime/src/node_submodules/consumers.rs b/crates/perry-runtime/src/node_submodules/consumers.rs index d21fe115ce..f91bb9a339 100644 --- a/crates/perry-runtime/src/node_submodules/consumers.rs +++ b/crates/perry-runtime/src/node_submodules/consumers.rs @@ -507,9 +507,10 @@ fn call_symbol_async_iterator(stream: f64) -> Option { if !is_callable_value(method) { return None; } - let prev_this = crate::object::js_implicit_this_set(stream); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(stream)); let iterator = unsafe { crate::closure::js_native_call_value(method, std::ptr::null(), 0) }; - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if iterator.to_bits() == crate::value::TAG_UNDEFINED { None } else { diff --git a/crates/perry-runtime/src/node_submodules/diagnostics.rs b/crates/perry-runtime/src/node_submodules/diagnostics.rs index a0268b8124..b053a3b6b3 100644 --- a/crates/perry-runtime/src/node_submodules/diagnostics.rs +++ b/crates/perry-runtime/src/node_submodules/diagnostics.rs @@ -842,9 +842,10 @@ pub(crate) fn suppress_uncaught_drain f64>(f: F) -> f64 { } pub(crate) fn with_implicit_this f64>(this_arg: f64, f: F) -> f64 { - let prev = crate::object::js_implicit_this_set(this_arg); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); let result = f(); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/node_submodules/stream_promises.rs b/crates/perry-runtime/src/node_submodules/stream_promises.rs index ef1e96ff47..ecf4a3ba2e 100644 --- a/crates/perry-runtime/src/node_submodules/stream_promises.rs +++ b/crates/perry-runtime/src/node_submodules/stream_promises.rs @@ -442,9 +442,10 @@ fn invoke_destination_method(destination: f64, method: &[u8], args: &[f64]) -> f let Some(func) = get_object_property(destination, method) else { return undefined_value(); }; - let prev_this = crate::object::js_implicit_this_set(destination); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(destination)); let result = unsafe { crate::closure::js_native_call_value(func, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/node_submodules/test.rs b/crates/perry-runtime/src/node_submodules/test.rs index 956434e867..8b3b032340 100644 --- a/crates/perry-runtime/src/node_submodules/test.rs +++ b/crates/perry-runtime/src/node_submodules/test.rs @@ -805,7 +805,7 @@ extern "C" fn mock_function_invoke(closure: *const ClosureHeader, rest: f64) -> let rest_handle = scope.root_nanbox_f64(rest); let arg_handles = scope.root_nanbox_f64_slice(&args); let call_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); - let previous_this = crate::object::js_implicit_this_set(this_value); + let previous_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_value)); // #9445 let call_result = catch_js(|| unsafe { crate::closure::js_native_call_value( implementation_handle.get_nanbox_f64(), @@ -813,7 +813,7 @@ extern "C" fn mock_function_invoke(closure: *const ClosureHeader, rest: f64) -> call_args.len(), ) }); - crate::object::js_implicit_this_set(previous_this); + crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); match call_result { Ok(result) => { diff --git a/crates/perry-runtime/src/node_submodules/trace_events.rs b/crates/perry-runtime/src/node_submodules/trace_events.rs index 9b7bfc0e26..af52b7b484 100644 --- a/crates/perry-runtime/src/node_submodules/trace_events.rs +++ b/crates/perry-runtime/src/node_submodules/trace_events.rs @@ -611,7 +611,10 @@ fn emit_enabled_trace_warning() { let process = scope.root_nanbox_f64(process); let callback = scope.root_nanbox_f64(callback); let warning = scope.root_nanbox_f64(warning); - let previous = crate::object::js_implicit_this_set(process.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let previous = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + process.get_nanbox_f64(), + )); unsafe { crate::closure::js_native_call_value( callback.get_nanbox_f64(), @@ -619,7 +622,7 @@ fn emit_enabled_trace_warning() { 1, ); } - crate::object::js_implicit_this_set(previous); + crate::object::js_implicit_this_set(previous.get_nanbox_f64()); } #[cfg(test)] diff --git a/crates/perry-runtime/src/node_vm/modules.rs b/crates/perry-runtime/src/node_vm/modules.rs index 731856e47f..a0f94f56e4 100644 --- a/crates/perry-runtime/src/node_vm/modules.rs +++ b/crates/perry-runtime/src/node_vm/modules.rs @@ -75,11 +75,15 @@ fn evaluate_synthetic_module(module: *mut ObjectHeader) -> f64 { })); let js = JSValue::from_bits(callback.get_nanbox_f64().to_bits()); if !js.is_undefined() && !js.is_null() { - let prev = crate::object::js_implicit_this_set(with_hmut(&module, object_value)); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(with_hmut( + &module, + object_value, + ))); let outcome = crate::exception::js_call_catching(|| unsafe { crate::closure::js_native_call_value(callback.get_nanbox_f64(), std::ptr::null(), 0) }); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); if let Err(error) = outcome { with_hmut(&module, |module| set_field(module, FIELD_ERROR, error)); with_hmut(&module, |module| set_status(module, STATUS_ERRORED)); diff --git a/crates/perry-runtime/src/object/class_constructors.rs b/crates/perry-runtime/src/object/class_constructors.rs index 37b2b33ea2..f2fa365931 100644 --- a/crates/perry-runtime/src/object/class_constructors.rs +++ b/crates/perry-runtime/src/object/class_constructors.rs @@ -677,7 +677,9 @@ pub unsafe extern "C" fn js_super_method_call_dynamic( if let Some((func_ptr, param_count, has_rest)) = super::class_registry::lookup_static_method_in_chain(parent_cid, name) { - let prev_this = crate::object::js_implicit_this_set(this_value); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_value)); crate::object::static_this_arm_if_unarmed(this_value); let result = if has_rest { // Mirror `js_class_static_method_call`'s rest bundling: fixed @@ -709,7 +711,7 @@ pub unsafe extern "C" fn js_super_method_call_dynamic( super::class_registry::call_static_method(func_ptr, args_ptr, args_len, param_count) }; crate::object::static_this_disarm(); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; } } diff --git a/crates/perry-runtime/src/object/class_registry/parent_static.rs b/crates/perry-runtime/src/object/class_registry/parent_static.rs index 09b80f6f7f..ae44447026 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static.rs @@ -1007,12 +1007,14 @@ pub(crate) unsafe fn class_symbol_getter_value( return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); } let result = if is_static { - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); crate::object::static_private_owner_push(receiver); let f: extern "C" fn() -> f64 = std::mem::transmute(getter); let result = f(); crate::object::static_private_owner_pop(); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } else { let f: extern "C" fn(f64) -> f64 = std::mem::transmute(getter); @@ -1053,12 +1055,14 @@ pub(crate) unsafe fn class_symbol_setter_apply( if let Some(&(_, setter)) = map.get(&(cid, sym_key, is_static)) { if setter != 0 { if is_static { - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope + .root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); crate::object::static_private_owner_push(receiver); let f: extern "C" fn(f64) -> f64 = std::mem::transmute(setter); let _ = f(value); crate::object::static_private_owner_pop(); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } else { let f: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(setter); let _ = f(receiver, value); @@ -1575,7 +1579,8 @@ pub unsafe extern "C" fn js_class_static_method_call( return receiver; } if let Some((func_ptr, param_count, has_rest)) = lookup_static_method_in_chain(class_id, name) { - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); crate::object::static_private_owner_push(receiver); // Receiver-sensitive static `this`: arm the one-shot override so the // method prologue (`js_static_this_resolve`) sees the DYNAMIC receiver @@ -1613,7 +1618,7 @@ pub unsafe extern "C" fn js_class_static_method_call( }; crate::object::static_this_disarm(); crate::object::static_private_owner_pop(); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; } // #1787 / #321: not a static METHOD — try a static FIELD holding a @@ -1667,9 +1672,11 @@ pub unsafe extern "C" fn js_class_static_method_call( let member = f64::from_bits(member.bits()); let mv = crate::value::JSValue::from_bits(member.to_bits()); if !mv.is_undefined() && !mv.is_null() { - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_native_call_value(member, args_ptr, args_len); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; } } @@ -1694,9 +1701,11 @@ pub unsafe extern "C" fn js_class_static_method_call( // implicit-this slot, so bind it to the subclass receiver for the // duration of the call — `NewPromiseCapability(receiver)` then // constructs the subclass. - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_native_call_value(static_val, args_ptr, args_len); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; } } @@ -1771,9 +1780,11 @@ pub unsafe extern "C" fn js_class_static_method_call( // not a real inherited member. && member.to_bits() != closure_val.to_bits() { - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_native_call_value(member, args_ptr, args_len); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; } } diff --git a/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs b/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs index 4b74d9ed05..ae568602a3 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs @@ -120,17 +120,20 @@ pub(crate) fn register_class_dynamic_static_accessor( owner, key.clone(), crate::object::AccessorDescriptor { - get: get_bits.map(|_| get.get_nanbox_u64()).unwrap_or(existing.get), - set: set_bits.map(|_| set.get_nanbox_u64()).unwrap_or(existing.set), + get: get_bits + .map(|_| get.get_nanbox_u64()) + .unwrap_or(existing.get), + set: set_bits + .map(|_| set.get_nanbox_u64()) + .unwrap_or(existing.set), }, ); let existing_attrs = if is_class_object_ptr(owner as *const u8) { crate::object::get_property_attrs(owner, &key) .map(|attrs| (attrs.enumerable(), attrs.configurable())) } else { - class_static_defined_attrs(class_id, name).map(|(_, enumerable, configurable)| { - (enumerable, configurable) - }) + class_static_defined_attrs(class_id, name) + .map(|(_, enumerable, configurable)| (enumerable, configurable)) }; let enumerable = enumerable .or_else(|| existing_attrs.map(|attrs| attrs.0)) @@ -153,7 +156,10 @@ pub(crate) fn class_dynamic_static_accessor_descriptor( class_id: u32, name: &str, receiver: f64, -) -> Option<(crate::object::AccessorDescriptor, crate::object::PropertyAttrs)> { +) -> Option<( + crate::object::AccessorDescriptor, + crate::object::PropertyAttrs, +)> { let scope = crate::gc::RuntimeHandleScope::new(); let receiver = scope.root_nanbox_f64(receiver); let owner = dynamic_static_accessor_owner(class_id, receiver.get_nanbox_f64()); @@ -289,7 +295,10 @@ pub(crate) unsafe fn call_private_static_method_for_owner( let scope = crate::gc::RuntimeHandleScope::new(); let this_value = scope.root_nanbox_f64(this_value); let private_brand = scope.root_nanbox_f64(private_brand); - let previous_this = crate::object::js_implicit_this_set(this_value.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let previous_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + this_value.get_nanbox_f64(), + )); crate::object::static_private_owner_push(private_brand.get_nanbox_f64()); crate::object::private_lexical_brand_push(private_brand.get_nanbox_f64()); crate::object::static_this_arm_if_unarmed(this_value.get_nanbox_f64()); @@ -297,6 +306,6 @@ pub(crate) unsafe fn call_private_static_method_for_owner( crate::object::static_this_disarm(); crate::object::private_lexical_brand_pop(); crate::object::static_private_owner_pop(); - crate::object::js_implicit_this_set(previous_this); + crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); Some(result) } diff --git a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs index ec638bb7dd..047a4d3e90 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs @@ -547,7 +547,8 @@ unsafe fn resolve_proto_chain_field_inner( } } let field_val = if let Some(receiver) = receiver { - let previous_this = js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let previous_this = this_scope.root_nanbox_f64(js_implicit_this_set(receiver)); // The recursive `get_field(proto_obj, key)` re-derives a class // getter's `this` from `proto_obj`; stash the real instance so an // inherited getter (object-literal `get x()` on an @@ -556,7 +557,7 @@ unsafe fn resolve_proto_chain_field_inner( super::super::field_get_set::accessor_receiver_override_begin(receiver); let value = js_object_get_field_by_name(proto_obj as *const _, key); super::super::field_get_set::accessor_receiver_override_end(prev_override); - js_implicit_this_set(previous_this); + js_implicit_this_set(previous_this.get_nanbox_f64()); value } else { js_object_get_field_by_name(proto_obj as *const _, key) diff --git a/crates/perry-runtime/src/object/date_proto_thunks.rs b/crates/perry-runtime/src/object/date_proto_thunks.rs index 9c45dd9b23..1997457e38 100644 --- a/crates/perry-runtime/src/object/date_proto_thunks.rs +++ b/crates/perry-runtime/src/object/date_proto_thunks.rs @@ -182,9 +182,10 @@ extern "C" fn date_to_json(_closure: *const crate::closure::ClosureHeader) -> f6 { // `Call(func, O, «»)` — toJSON's `key` argument is intentionally not // forwarded (Invoke passes an empty argument list). - let prev = crate::object::js_implicit_this_set(o); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(o)); let r = crate::closure::js_closure_call0(closure); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return r; } super::object_ops::throw_object_type_error(b"toISOString is not a function") diff --git a/crates/perry-runtime/src/object/descriptor_state.rs b/crates/perry-runtime/src/object/descriptor_state.rs index 1ec8bd19dd..909002a9aa 100644 --- a/crates/perry-runtime/src/object/descriptor_state.rs +++ b/crates/perry-runtime/src/object/descriptor_state.rs @@ -960,9 +960,10 @@ pub(crate) unsafe fn json_object_getter_value( return Some(f64::from_bits(TAG_UNDEFINED)); } let receiver = crate::value::js_nanbox_pointer(obj as i64); - let prev = js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(js_implicit_this_set(receiver)); let result = crate::closure::js_closure_call0(closure); - js_implicit_this_set(prev); + js_implicit_this_set(prev.get_nanbox_f64()); Some(result) } diff --git a/crates/perry-runtime/src/object/field_set_by_name/tail.rs b/crates/perry-runtime/src/object/field_set_by_name/tail.rs index 26d7937a9c..ee6a361400 100644 --- a/crates/perry-runtime/src/object/field_set_by_name/tail.rs +++ b/crates/perry-runtime/src/object/field_set_by_name/tail.rs @@ -125,9 +125,11 @@ pub(crate) fn set_field_by_name_object_tail( as *const crate::closure::ClosureHeader; if !closure.is_null() { let receiver = crate::value::js_nanbox_pointer(obj as i64); - let previous_this = super::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let previous_this = + this_scope.root_nanbox_f64(super::js_implicit_this_set(receiver)); crate::closure::js_closure_call1(closure, value); - super::js_implicit_this_set(previous_this); + super::js_implicit_this_set(previous_this.get_nanbox_f64()); } } else { crate::error::throw_immutable_write(0, name); @@ -686,9 +688,11 @@ pub(crate) fn set_field_by_name_object_tail( as *const crate::closure::ClosureHeader; if !closure.is_null() { let receiver = crate::value::js_nanbox_pointer(obj as i64); - let previous_this = super::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let previous_this = + this_scope.root_nanbox_f64(super::js_implicit_this_set(receiver)); crate::closure::js_closure_call1(closure, value); - super::js_implicit_this_set(previous_this); + super::js_implicit_this_set(previous_this.get_nanbox_f64()); } } else { crate::error::throw_immutable_write(0, k); diff --git a/crates/perry-runtime/src/object/global_this/bigint_promise.rs b/crates/perry-runtime/src/object/global_this/bigint_promise.rs index 907f295314..c15889a8ab 100644 --- a/crates/perry-runtime/src/object/global_this/bigint_promise.rs +++ b/crates/perry-runtime/src/object/global_this/bigint_promise.rs @@ -696,9 +696,10 @@ pub(crate) extern "C" fn typed_array_from_thunk( if map_closure.is_null() { return v; } - let prev = crate::object::js_implicit_this_set(this_arg); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); let r = crate::closure::js_closure_call2(map_closure, v, k as f64); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); r }; if let Some(kind) = kind_opt { diff --git a/crates/perry-runtime/src/object/global_this/fetch_globals.rs b/crates/perry-runtime/src/object/global_this/fetch_globals.rs index 8252c3c739..d10883d70d 100644 --- a/crates/perry-runtime/src/object/global_this/fetch_globals.rs +++ b/crates/perry-runtime/src/object/global_this/fetch_globals.rs @@ -449,13 +449,17 @@ pub(crate) unsafe fn temporal_subclass_super( // to the parent ctor for the duration of the call (the cell it returns is // re-homed onto the subclass `this`; the exact new.target identity is not // observable to these native ctors beyond being defined). Restore after. - let prev_this = crate::object::js_implicit_this_set(this_box); + // #9445: root the displaced receiver and `this_box` (consumed again below) + // across the parent constructor call. + let this_scope = crate::gc::RuntimeHandleScope::new(); + let this_h = this_scope.root_nanbox_f64(this_box); + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_box)); let prev_nt = crate::object::js_new_target_set(parent_val); let cell = crate::closure::js_native_call_value(parent_val, args_ptr, args_len); crate::object::js_new_target_set(prev_nt); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if crate::temporal::is_temporal_value(cell) { - attach_temporal_cell_to_this(this_box, cell); + attach_temporal_cell_to_this(this_h.get_nanbox_f64(), cell); } true } @@ -959,9 +963,10 @@ pub unsafe extern "C" fn js_fetch_or_value_super( } } } - let prev = crate::object::js_implicit_this_set(this_box); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_box)); let r = crate::closure::js_native_call_value(callee, args_ptr, args_len); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); r } } diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index b54754a493..fe73a62449 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -677,7 +677,9 @@ pub unsafe extern "C-unwind" fn js_native_call_method_value( if let Some((func_ptr, param_count, has_rest)) = lookup_class_symbol_method_in_chain(class_id, sym_key, true) { - let prev_this = crate::object::js_implicit_this_set(object); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(object)); let result = call_registered_static_method( func_ptr, args_ptr, @@ -685,7 +687,7 @@ pub unsafe extern "C-unwind" fn js_native_call_method_value( param_count, has_rest, ); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; } } @@ -695,7 +697,9 @@ pub unsafe extern "C-unwind" fn js_native_call_method_value( if let Some((func_ptr, param_count, has_rest)) = lookup_class_symbol_method_in_chain(class_id, sym_key, true) { - let prev_this = crate::object::js_implicit_this_set(object); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(object)); let result = call_registered_static_method( func_ptr, args_ptr, @@ -703,7 +707,7 @@ pub unsafe extern "C-unwind" fn js_native_call_method_value( param_count, has_rest, ); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; } } else if key_jsval.is_pointer() || JSValue::from_bits(bits).is_pointer() { @@ -2506,9 +2510,11 @@ pub unsafe extern "C-unwind" fn js_native_call_method( if crate::promise::subclass_backing_promise(object()).is_some() { if let Some(m) = crate::promise::promise_proto_method(method_name) { let args = refreshed_args(); - let prev_this = crate::object::js_implicit_this_set(object()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(object())); let result = crate::closure::js_native_call_value(m, args.as_ptr(), args.len()); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; } } diff --git a/crates/perry-runtime/src/object/native_call_method/handle_methods.rs b/crates/perry-runtime/src/object/native_call_method/handle_methods.rs index 613d7968fe..8f63bf62bb 100644 --- a/crates/perry-runtime/src/object/native_call_method/handle_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/handle_methods.rs @@ -45,9 +45,10 @@ unsafe fn dispatch_handle_proto_method( // `this` and for non-closure values. Mirrors the class-prototype fallback. let _ = closure_ptr; let bound = crate::closure::clone_closure_rebind_this(resolved_bits, object); - let prev = crate::object::js_implicit_this_set(object); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(object)); let result = crate::closure::js_native_call_value(f64::from_bits(bound), args_ptr, args_len); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); Some(result) } diff --git a/crates/perry-runtime/src/object/native_call_method/object_proto.rs b/crates/perry-runtime/src/object/native_call_method/object_proto.rs index 645705f093..8a88e9f98c 100644 --- a/crates/perry-runtime/src/object/native_call_method/object_proto.rs +++ b/crates/perry-runtime/src/object/native_call_method/object_proto.rs @@ -33,9 +33,10 @@ pub(super) unsafe fn call_object_to_string_method(object: f64) -> Option { throw_object_to_string_not_function(); } let bound = crate::closure::clone_closure_rebind_this(method_bits, receiver); - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); Some(result) } diff --git a/crates/perry-runtime/src/object/property_key.rs b/crates/perry-runtime/src/object/property_key.rs index cada3adae4..d1d360a125 100644 --- a/crates/perry-runtime/src/object/property_key.rs +++ b/crates/perry-runtime/src/object/property_key.rs @@ -129,10 +129,11 @@ unsafe fn ordinary_to_primitive_string_key(value: f64) -> Option { continue; } let bound = crate::closure::clone_closure_rebind_this(method_bits, receiver); - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if js_value_is_not_object(result) { return Some(result); } @@ -373,9 +374,11 @@ pub unsafe extern "C" fn js_super_accessor_get( { if getter_ptr != 0 { let f: extern "C" fn(f64) -> f64 = std::mem::transmute(getter_ptr); - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope + .root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let r = f(receiver); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return r; } } @@ -468,9 +471,11 @@ pub unsafe extern "C" fn js_super_accessor_get( .or_else(|| vtable.getters.get(&getter_alias)) { let f: extern "C" fn(f64) -> f64 = std::mem::transmute(getter_ptr); - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope + .root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let r = f(receiver); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return r; } } @@ -565,13 +570,14 @@ pub unsafe extern "C" fn js_object_super_call( let bound = crate::closure::clone_closure_rebind_this(callee_handle.get_nanbox_u64(), receiver); let bound_handle = scope.root_nanbox_u64(bound); let receiver = f64::from_bits(receiver_handle.get_heap_word_u64()); - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_native_call_value( f64::from_bits(bound_handle.get_nanbox_u64()), args_ptr, args_len, ); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/object/this_binding.rs b/crates/perry-runtime/src/object/this_binding.rs index 6ece8c5ce0..3c9cc7f096 100644 --- a/crates/perry-runtime/src/object/this_binding.rs +++ b/crates/perry-runtime/src/object/this_binding.rs @@ -210,6 +210,31 @@ pub extern "C" fn js_implicit_this_get_sloppy() -> f64 { /// Callers must restore the previous value to scope the binding to the /// duration of a single method-style call. /// +/// **The returned previous value is a GC-managed pointer held in a bare Rust +/// local** (#9417, #9445). It is the caller's receiver, and the call this +/// save/restore brackets is user code, so an evacuating young-gen minor inside +/// it moves that object and rewrites every slot the collector can see — a +/// Rust local is not one. Restoring the stale bits then installs a retired +/// from-space address as the caller's `this`, and the failure is silent: +/// `js_object_get_own_field_or_undef` fails its type check on the recycled cell +/// and answers `undefined`, so the caller's next `this.` throws a +/// TypeError naming a property nowhere near the defect. Every runtime site +/// therefore roots the saved value in a `RuntimeHandleScope` and re-reads it at +/// the restore: +/// +/// ```ignore +/// let this_scope = crate::gc::RuntimeHandleScope::new(); +/// let prev = this_scope.root_nanbox_f64(js_implicit_this_set(receiver)); +/// … user code … +/// js_implicit_this_set(prev.get_nanbox_f64()); +/// ``` +/// +/// This is longjmp-safe: `exception.rs` saves and restores the handle stack at +/// trap boundaries, so a throw through the window truncates the scope exactly +/// as a normal drop would. Inside a loop, open the scope PER ITERATION (or +/// reuse an existing per-iteration scope) so the handle stack does not grow by +/// one slot per callback. +/// /// This is part of every dynamically-dispatched call's save/restore path. /// Keep the shipped path to one TLS replacement; default-off diagnostics here /// still impose their mode checks millions of times on closure-heavy programs. diff --git a/crates/perry-runtime/src/os_process_streams.rs b/crates/perry-runtime/src/os_process_streams.rs index bda504ea59..f2dfa22247 100644 --- a/crates/perry-runtime/src/os_process_streams.rs +++ b/crates/perry-runtime/src/os_process_streams.rs @@ -859,7 +859,6 @@ fn pump_stdin_data_chunks() { if bytes.is_empty() { return; } - let this = stdin_this_value(); // #9490: decode ONCE per chunk. The UTF-8 decoder carries state // across chunks, so decoding per listener would push the same bytes // through it N times and give the second listener a continuation of @@ -880,10 +879,13 @@ fn pump_stdin_data_chunks() { let scope = crate::gc::RuntimeHandleScope::new(); let cb_handle = scope.root_raw_const_ptr(cb as *const crate::closure::ClosureHeader); let closure = cb_handle.get_raw_const_ptr::(); - // Node calls stream listeners with `this === stream`. - let prev_this = crate::object::js_implicit_this_set(this); + // Node calls stream listeners with `this === stream`. Re-read the + // singleton per listener and root the displaced receiver: the previous + // listener was user code, so either may have moved (#9445). + let this = stdin_this_value(); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); crate::closure::js_closure_call1(closure, arg_handle.get_nanbox_f64()); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } return; } @@ -896,14 +898,15 @@ fn pump_stdin_data_chunks() { .map(|mut l| std::mem::take(&mut *l)) .unwrap_or_default(); readable_listeners.extend(&readable_once); - let this = stdin_this_value(); for cb in readable_listeners { let scope = crate::gc::RuntimeHandleScope::new(); let cb_handle = scope.root_raw_const_ptr(cb as *const crate::closure::ClosureHeader); let closure = cb_handle.get_raw_const_ptr::(); - let prev_this = crate::object::js_implicit_this_set(this); + // Per-listener re-read + rooted save/restore (#9445), as for `data`. + let this = stdin_this_value(); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); crate::closure::js_closure_call0(closure); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } @@ -939,7 +942,6 @@ fn maybe_fire_stdin_end() { .lock() .map(|l| l.clone()) .unwrap_or_default(); - let this = stdin_this_value(); let flush_scope = crate::gc::RuntimeHandleScope::new(); let flush_handle = flush_scope.root_nanbox_f64(flushed); for cb in data_listeners { @@ -947,9 +949,11 @@ fn maybe_fire_stdin_end() { let cb_handle = scope.root_raw_const_ptr(cb as *const crate::closure::ClosureHeader); let closure = cb_handle.get_raw_const_ptr::(); - let prev_this = crate::object::js_implicit_this_set(this); + // Per-listener re-read + rooted save/restore (#9445), as for `data`. + let this = stdin_this_value(); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); crate::closure::js_closure_call1(closure, flush_handle.get_nanbox_f64()); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } } @@ -977,14 +981,15 @@ fn maybe_fire_stdin_end() { .map(|mut l| std::mem::take(&mut *l)) .unwrap_or_default(); end_listeners.extend(&end_once); - let this = stdin_this_value(); for cb in end_listeners { let scope = crate::gc::RuntimeHandleScope::new(); let cb_handle = scope.root_raw_const_ptr(cb as *const crate::closure::ClosureHeader); let closure = cb_handle.get_raw_const_ptr::(); - let prev_this = crate::object::js_implicit_this_set(this); + // Per-listener re-read + rooted save/restore (#9445), as for `data`. + let this = stdin_this_value(); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(this)); crate::closure::js_closure_call0(closure); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } diff --git a/crates/perry-runtime/src/promise/assimilate.rs b/crates/perry-runtime/src/promise/assimilate.rs index e499d6f52d..577c534edd 100644 --- a/crates/perry-runtime/src/promise/assimilate.rs +++ b/crates/perry-runtime/src/promise/assimilate.rs @@ -382,11 +382,12 @@ extern "C" fn promise_resolve_thenable_job(closure: *const crate::closure::Closu let reject_value = crate::value::js_nanbox_pointer(reject_closure as i64); let args = [resolve_value, reject_value]; - let prev_this = crate::object::js_implicit_this_set(thenable); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(thenable)); let result = combinator_catch_js(|| unsafe { crate::closure::js_native_call_value(then_action, args.as_ptr(), args.len()) }); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if let Err(reason) = result { if thenable_job_take_guard(guard_arr) { js_promise_reject(promise, reason); @@ -446,11 +447,12 @@ pub(super) fn assimilate_via_then_property(value: f64) -> f64 { // Bind `this` to the thenable so a non-arrow `then` body reads the right // receiver, then call `Get(value, "then")` as a value (own data property). - let prev = crate::object::js_implicit_this_set(value); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(value)); unsafe { crate::closure::js_native_call_value(then_val, args.as_ptr(), args.len()); } - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); crate::value::js_nanbox_pointer(new_promise as i64) } diff --git a/crates/perry-runtime/src/promise/async_step.rs b/crates/perry-runtime/src/promise/async_step.rs index 3b38954990..b8e57e781a 100644 --- a/crates/perry-runtime/src/promise/async_step.rs +++ b/crates/perry-runtime/src/promise/async_step.rs @@ -1501,9 +1501,10 @@ fn array_from_async_map_or_push(closure: *const crate::closure::ClosureHeader, v let this_arg = crate::closure::js_closure_get_capture_f64(closure, AFA_THIS_ARG); let args = [value, index]; let args_arr = crate::array::js_array_from_f64(args.as_ptr(), args.len() as u32); - let prev_this = crate::object::js_implicit_this_set(this_arg); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); let mapped_promise = js_promise_try(map_fn, args_arr); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); let mapped_closure = crate::closure::js_closure_get_capture_ptr(closure, AFA_MAPPED_CLOSURE) as *const crate::closure::ClosureHeader; diff --git a/crates/perry-runtime/src/promise/checked_dispatch.rs b/crates/perry-runtime/src/promise/checked_dispatch.rs index 4070589510..c8bcb5f9e1 100644 --- a/crates/perry-runtime/src/promise/checked_dispatch.rs +++ b/crates/perry-runtime/src/promise/checked_dispatch.rs @@ -50,10 +50,11 @@ pub extern "C" fn js_promise_then_checked( )); } let args = [on_fulfilled, on_rejected]; - let prev = crate::object::js_implicit_this_set(promise_val); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(promise_val)); let result = unsafe { crate::closure::js_native_call_value(own_then, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return result; } if promise_has_own_constructor(promise_addr) @@ -62,9 +63,10 @@ pub extern "C" fn js_promise_then_checked( // Own `constructor` override OR a `class X extends Promise` instance: // route through the SpeciesConstructor-aware thunk, which unwraps the // backing cell and reads `this.constructor` for species chaining. - let prev = crate::object::js_implicit_this_set(promise_val); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(promise_val)); let result = promise_prototype_then_thunk(std::ptr::null(), on_fulfilled, on_rejected); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return result; } let promise = promise_addr as *mut Promise; @@ -82,9 +84,10 @@ pub extern "C" fn js_promise_catch_checked(promise_val: f64, on_rejected: f64) - || promise_has_own_constructor(promise_addr) || subclass::subclass_backing_promise(promise_val).is_some() { - let prev = crate::object::js_implicit_this_set(promise_val); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(promise_val)); let result = promise_prototype_catch_thunk(std::ptr::null(), on_rejected); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return result; } let promise = promise_addr as *mut Promise; @@ -98,9 +101,10 @@ pub extern "C" fn js_promise_finally_checked(promise_val: f64, on_finally: f64) || promise_has_own_constructor(promise_addr) || subclass::subclass_backing_promise(promise_val).is_some() { - let prev = crate::object::js_implicit_this_set(promise_val); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(promise_val)); let result = promise_prototype_finally_thunk(std::ptr::null(), on_finally); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return result; } let promise = promise_addr as *mut Promise; diff --git a/crates/perry-runtime/src/promise/spec_combinators.rs b/crates/perry-runtime/src/promise/spec_combinators.rs index be6ba05c4b..acf6fa2bf0 100644 --- a/crates/perry-runtime/src/promise/spec_combinators.rs +++ b/crates/perry-runtime/src/promise/spec_combinators.rs @@ -297,10 +297,11 @@ fn call_with_this(func: f64, this_arg: f64, args: &[f64]) -> Result { } else { (args.as_ptr(), args.len()) }; - let prev = crate::object::js_implicit_this_set(this_arg); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); let result = combinator_catch_js(|| unsafe { crate::closure::js_native_call_value(func, ptr, len) }); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/promise/then.rs b/crates/perry-runtime/src/promise/then.rs index bd8c993212..c2590921d9 100644 --- a/crates/perry-runtime/src/promise/then.rs +++ b/crates/perry-runtime/src/promise/then.rs @@ -982,10 +982,11 @@ fn call_receiver_then(receiver: f64, args: &[f64]) -> f64 { let err_val = crate::value::JSValue::pointer(err_ptr as *const u8).bits(); crate::exception::js_throw(f64::from_bits(err_val)); } - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = unsafe { crate::closure::js_native_call_value(then_fn, args.as_ptr(), args.len()) }; - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index de1102ba2d..4f49b6a3ff 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -695,7 +695,8 @@ fn call_trap(handler: f64, trap: f64, args: &[f64]) -> f64 { } let undef = f64::from_bits(TAG_UNDEFINED); let a = |i: usize| -> f64 { args.get(i).copied().unwrap_or(undef) }; - let prev = crate::object::js_implicit_this_set(handler); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(handler)); let result = match args.len() { 0 => js_closure_call0(closure), 1 => js_closure_call1(closure, a(0)), @@ -703,7 +704,7 @@ fn call_trap(handler: f64, trap: f64, args: &[f64]) -> f64 { 3 => js_closure_call3(closure, a(0), a(1), a(2)), _ => crate::closure::js_closure_call4(closure, a(0), a(1), a(2), a(3)), }; - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } @@ -892,7 +893,8 @@ fn call_with_this_and_args(f: f64, this_arg: f64, args: &[f64]) -> f64 { if closure.is_null() { return throw_type_error("Reflect.apply target is not a function"); } - let prev = crate::object::js_implicit_this_set(this_arg); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); let a = |i: usize| -> f64 { args.get(i) .copied() @@ -905,7 +907,7 @@ fn call_with_this_and_args(f: f64, this_arg: f64, args: &[f64]) -> f64 { 3 => js_closure_call3(closure, a(0), a(1), a(2)), _ => crate::closure::js_closure_call4(closure, a(0), a(1), a(2), a(3)), }; - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } @@ -1622,9 +1624,10 @@ fn call_setter_with_receiver(setter_bits: u64, receiver: f64, value: f64) -> boo if closure.is_null() { return false; } - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let _ = js_closure_call1(closure, value); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); true } @@ -2252,9 +2255,11 @@ fn class_super_accessor_set( .or_else(|| vtable.setters.get(&setter_alias)) { let f: extern "C" fn(f64, f64) -> f64 = unsafe { std::mem::transmute(setter_ptr) }; - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let _ = f(receiver, value); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return Some(true); } let getter_alias = format!("__get_{}", key_name); diff --git a/crates/perry-runtime/src/proxy/apply_construct.rs b/crates/perry-runtime/src/proxy/apply_construct.rs index a7a37bc35f..72047be4e7 100644 --- a/crates/perry-runtime/src/proxy/apply_construct.rs +++ b/crates/perry-runtime/src/proxy/apply_construct.rs @@ -93,9 +93,10 @@ fn forward_apply(target: f64, this_arg: f64, args_array: f64) -> f64 { } else { (buf.as_ptr(), buf.len()) }; - let prev = crate::object::js_implicit_this_set(this_arg); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); let result = unsafe { crate::closure::js_native_call_value(target, ptr, n) }; - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } @@ -178,9 +179,10 @@ pub extern "C" fn js_proxy_apply(proxy_boxed: f64, this_arg: f64, args_array: f6 if closure.is_null() { return throw_type_error("proxy apply trap is not a function"); } - let prev = crate::object::js_implicit_this_set(handler); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(handler)); let result = js_closure_call3(closure, target, this_arg, args_array); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } @@ -256,9 +258,10 @@ pub extern "C" fn js_proxy_construct(proxy_boxed: f64, args_array: f64, new_targ if closure.is_null() { return throw_type_error("proxy construct trap is not a function"); } - let prev = crate::object::js_implicit_this_set(handler); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(handler)); let result = js_closure_call3(closure, target, args_array, nt); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); // [[Construct]] must return an Object (spec step 9 of the construct trap). if !reflect_value_is_object(result) { // Node/V8 wording: `'construct' on proxy: trap returned non-object ('1')`. diff --git a/crates/perry-runtime/src/proxy/reflect.rs b/crates/perry-runtime/src/proxy/reflect.rs index 8833a56912..b653b195f4 100644 --- a/crates/perry-runtime/src/proxy/reflect.rs +++ b/crates/perry-runtime/src/proxy/reflect.rs @@ -62,16 +62,18 @@ pub extern "C" fn js_reflect_get(target: f64, key: f64, receiver: f64) -> f64 { if !closure.is_null() { // Also set IMPLICIT_THIS for free-function getters that read // `this` from the implicit-this fallback rather than a slot. - let prev = crate::object::js_implicit_this_set(recv); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); let result = js_closure_call0(closure); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); return result; } } } - let prev = crate::object::js_implicit_this_set(recv); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); let result = target_get_property_key(target, property_key); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } @@ -380,9 +382,10 @@ pub extern "C" fn js_reflect_get_own_property_descriptor(target: f64, key: f64) if closure.is_null() { return throw_type_error("proxy getOwnPropertyDescriptor trap is not a function"); } - let prev = crate::object::js_implicit_this_set(handler); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(handler)); let result = js_closure_call2(closure, inner, property_key); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); let result_handle = scope.root_nanbox_f64(result); let target_desc = crate::object::js_object_get_own_property_descriptor(inner, property_key); diff --git a/crates/perry-runtime/src/pty/mod.rs b/crates/perry-runtime/src/pty/mod.rs index 301b21cc56..c8a30e438d 100644 --- a/crates/perry-runtime/src/pty/mod.rs +++ b/crates/perry-runtime/src/pty/mod.rs @@ -86,11 +86,12 @@ mod unix_impl { break; } let cb = crate::array::js_array_get_f64(arr, i); - let prev = js_implicit_this_set(target); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(js_implicit_this_set(target)); unsafe { let _ = js_native_call_value(cb, args.as_ptr(), args.len()); } - js_implicit_this_set(prev); + js_implicit_this_set(prev.get_nanbox_f64()); i += 1; } } diff --git a/crates/perry-runtime/src/regex/replace_fn.rs b/crates/perry-runtime/src/regex/replace_fn.rs index 950428d653..ed6b275a4a 100644 --- a/crates/perry-runtime/src/regex/replace_fn.rs +++ b/crates/perry-runtime/src/regex/replace_fn.rs @@ -5,9 +5,12 @@ use super::*; use crate::value::js_nanbox_string; pub(super) unsafe fn call_replace_callback(callback: f64, args: &[f64]) -> String { - let prev = crate::object::js_implicit_this_set(f64::from_bits(crate::value::TAG_UNDEFINED)); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(f64::from_bits( + crate::value::TAG_UNDEFINED, + ))); let ret = crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); // §22.1.3.19 step "Let replacement be ? ToString(? Call(replaceValue, …))": // the callback result is ToString-coerced — `undefined` renders as // "undefined", a number stringifies, an object runs its `toString`, and a diff --git a/crates/perry-runtime/src/set.rs b/crates/perry-runtime/src/set.rs index 3445ef7e94..7f12f45666 100644 --- a/crates/perry-runtime/src/set.rs +++ b/crates/perry-runtime/src/set.rs @@ -1941,9 +1941,10 @@ fn js_set_foreach_impl( let args = [value, value, set_value]; let cb = callback_handle.get_nanbox_f64(); let this_v = this_handle.get_nanbox_f64(); - let prev_this = crate::object::js_implicit_this_set(this_v); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_v)); let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } let set = set_handle.get_raw_const_ptr::(); diff --git a/crates/perry-runtime/src/symbol/accessors.rs b/crates/perry-runtime/src/symbol/accessors.rs index a7db08d7fd..9ceeddbe3c 100644 --- a/crates/perry-runtime/src/symbol/accessors.rs +++ b/crates/perry-runtime/src/symbol/accessors.rs @@ -169,9 +169,10 @@ pub(super) unsafe fn invoke_symbol_accessor_getter(get_bits: u64, receiver: f64) if closure.is_null() { return f64::from_bits(TAG_UNDEFINED); } - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_closure_call0(closure); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/symbol/iterator.rs b/crates/perry-runtime/src/symbol/iterator.rs index 26f85faecb..80de035c62 100644 --- a/crates/perry-runtime/src/symbol/iterator.rs +++ b/crates/perry-runtime/src/symbol/iterator.rs @@ -261,12 +261,14 @@ pub extern "C" fn js_get_iterator(val_f64: f64) -> f64 { if iter_fn.to_bits() == TAG_UNDEFINED || fn_ptr.is_null() { throw_value_not_iterable(val_f64); } - let prev_this = crate::object::js_implicit_this_set(val_f64); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(val_f64)); let rebound = crate::closure::clone_closure_rebind_this(iter_fn.to_bits(), val_f64); let rebound_ptr = crate::value::js_nanbox_get_pointer(f64::from_bits(rebound)) as *const crate::closure::ClosureHeader; let iter = crate::closure::js_closure_call0(rebound_ptr); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if !is_object_value(iter) { throw_iterator_result_not_object(); } @@ -453,9 +455,11 @@ pub extern "C" fn js_get_iterator(val_f64: f64) -> f64 { // `function(){ …this… }` factory reads `this` dynamically off // IMPLICIT_THIS, so set it here too (test262 yield-star-sync-* // asserts the `[Symbol.iterator]` call's thisValue === obj). - let prev_this = crate::object::js_implicit_this_set(val_f64); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(val_f64)); let iter = crate::closure::js_closure_call0(fn_ptr); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); // Several Perry host-backed collections expose iterator // helpers as eager arrays for direct `.entries()` parity. When // the same function is reached through `Symbol.iterator`, wrap @@ -650,12 +654,15 @@ pub unsafe extern "C" fn js_to_primitive(value: f64, hint: i32) -> f64 { // `this.celsius` resolved to `undefined`, so `+t` was `NaN` and `` `${t}` `` // was `undefined°C` (test_gap_symbols). The proxy arm above already binds // `this`; mirror it for the closure method. - let prev_this = crate::object::js_implicit_this_set(value_handle.get_nanbox_f64()); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + value_handle.get_nanbox_f64(), + )); // Spec says the return value must be a primitive; if it's still an // object pointer, that's a TypeError in JS, but we just return it // as-is and let the caller fall back. let result = crate::closure::js_closure_call1(closure_ptr, hint_f64); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result } diff --git a/crates/perry-runtime/src/timer.rs b/crates/perry-runtime/src/timer.rs index 4b2a18059b..5f3a27675c 100644 --- a/crates/perry-runtime/src/timer.rs +++ b/crates/perry-runtime/src/timer.rs @@ -516,11 +516,13 @@ fn call_timer_callback( let previous_roots = crate::async_context::root_snapshot(&scope, &previous); let a = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); let cb = callback_handle.get_raw_const_ptr::(); - let prev_this = crate::object::js_implicit_this_set(timer_handle_value(id)); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(timer_handle_value(id))); with_timer_uncaught_trap(|| unsafe { crate::closure::js_closure_call_array(cb as i64, a.as_ptr(), a.len() as i64); }); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); crate::async_context::refresh_snapshot_from_roots(&mut previous, &previous_roots); crate::async_context::restore_context(previous); } @@ -1304,7 +1306,10 @@ pub extern "C" fn js_callback_timer_tick() -> i32 { let mut previous = previous; let previous_roots = crate::async_context::root_snapshot(&batch_scope, &previous); crate::async_hooks::before(timer.async_id, timer.trigger_async_id); - let prev_this = crate::object::js_implicit_this_set(timer_handle_value(timer.id)); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + timer_handle_value(timer.id), + )); enter_timer_callback_dispatch(); with_timer_uncaught_trap(|| { // Installing the timer receiver above is itself a collecting @@ -1359,7 +1364,7 @@ pub extern "C" fn js_callback_timer_tick() -> i32 { // matching Node's `setTimeout1 → micro → setTimeout2` ordering. crate::promise::microtasks::js_promise_run_microtasks_checkpoint(); leave_timer_callback_dispatch(); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); crate::async_hooks::after(timer.async_id); crate::async_hooks::destroy(timer.async_id); crate::async_context::refresh_snapshot_from_roots(&mut previous, &previous_roots); @@ -1723,7 +1728,9 @@ pub extern "C" fn js_interval_timer_tick() -> i32 { let previous = crate::async_context::enter_context(&context); let mut previous = previous; let previous_roots = crate::async_context::root_snapshot(&scope, &previous); - let prev_this = crate::object::js_implicit_this_set(timer_handle_value(id)); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(timer_handle_value(id))); enter_timer_callback_dispatch(); crate::async_hooks::before(async_id, trigger_async_id); with_timer_uncaught_trap(|| { @@ -1744,7 +1751,7 @@ pub extern "C" fn js_interval_timer_tick() -> i32 { }); crate::async_hooks::after(async_id); leave_timer_callback_dispatch(); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); crate::async_context::refresh_snapshot_from_roots(&mut previous, &previous_roots); crate::async_context::restore_context(previous); fired += 1; diff --git a/crates/perry-runtime/src/typedarray_props.rs b/crates/perry-runtime/src/typedarray_props.rs index 26d3a44117..ac5e651e61 100644 --- a/crates/perry-runtime/src/typedarray_props.rs +++ b/crates/perry-runtime/src/typedarray_props.rs @@ -238,9 +238,10 @@ fn invoke_typed_array_accessor_getter(get_bits: u64, receiver: f64) -> f64 { if closure.is_null() { return f64::from_bits(crate::value::TAG_UNDEFINED); } - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_closure_call0(closure); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result } @@ -249,9 +250,10 @@ fn invoke_typed_array_accessor_setter(set_bits: u64, receiver: f64, value: f64) if closure.is_null() { return; } - let prev = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); crate::closure::js_closure_call1(closure, value); - crate::object::js_implicit_this_set(prev); + crate::object::js_implicit_this_set(prev.get_nanbox_f64()); } fn barrier_typed_array_own_props(owner: usize, props: &mut [TypedArrayOwnProp]) { diff --git a/crates/perry-runtime/src/url/search_params.rs b/crates/perry-runtime/src/url/search_params.rs index 7627021f5f..6f2afd84b8 100644 --- a/crates/perry-runtime/src/url/search_params.rs +++ b/crates/perry-runtime/src/url/search_params.rs @@ -933,9 +933,11 @@ pub extern "C" fn js_url_search_params_for_each( this_value, ]; unsafe { - let prev_this = crate::object::js_implicit_this_set(this_arg); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = + this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); let _ = crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } } } diff --git a/crates/perry-runtime/src/util_promisify.rs b/crates/perry-runtime/src/util_promisify.rs index 5fa4c27d98..58210d5695 100644 --- a/crates/perry-runtime/src/util_promisify.rs +++ b/crates/perry-runtime/src/util_promisify.rs @@ -763,7 +763,8 @@ extern "C" fn callbackify_outer_thunk(closure: *const ClosureHeader, rest_value: let on_rejected = nanbox_pointer(rejected_handle.get_raw_const_ptr::() as *const u8); let args = [on_fulfilled, on_rejected]; - let prev_this = crate::object::js_implicit_this_set(returned); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(returned)); unsafe { crate::closure::js_native_call_value( then_handle.get_nanbox_f64(), @@ -771,7 +772,7 @@ extern "C" fn callbackify_outer_thunk(closure: *const ClosureHeader, rest_value: args.len(), ); } - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return TAG_UNDEFINED_F64; } diff --git a/crates/perry-runtime/src/value/to_string.rs b/crates/perry-runtime/src/value/to_string.rs index 6851ad2086..3f600a3dd3 100644 --- a/crates/perry-runtime/src/value/to_string.rs +++ b/crates/perry-runtime/src/value/to_string.rs @@ -197,13 +197,14 @@ pub(crate) unsafe fn ordinary_to_primitive_for_toprimitive( } let method_handle = scope.root_nanbox_f64(method); let recv = value_handle.get_nanbox_f64(); - let prev_this = crate::object::js_implicit_this_set(recv); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); let result = crate::closure::js_native_call_value( method_handle.get_nanbox_f64(), std::ptr::null(), 0, ); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if is_primitive_value(result) { return result; } @@ -456,9 +457,10 @@ pub(crate) unsafe fn call_own_method(method: f64, receiver: f64) -> Option // different value into its reserved `this` slot (an inherited or bound // method), exactly as the method-dispatch tower does (#1982). let bound = crate::closure::clone_closure_rebind_this(bits, receiver); - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let ret = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); Some(ret) } @@ -833,9 +835,10 @@ unsafe fn call_method_for_primitive( // receiver, so rebinding is a correct no-op. Mirrors #1982. let recv = value_handle.get_nanbox_f64(); let bound = crate::closure::clone_closure_rebind_this(method_bits, recv); - let prev_this = crate::object::js_implicit_this_set(recv); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); let ret = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); let ret_jsv = JSValue::from_bits(ret.to_bits()); let is_primitive = ret_jsv.is_any_string() || ret_jsv.is_number() @@ -888,9 +891,10 @@ unsafe fn call_function_method( let method_handle = scope.root_nanbox_f64(method); let bound = crate::closure::clone_closure_rebind_this(method_handle.get_nanbox_u64(), recv); - let prev_this = crate::object::js_implicit_this_set(recv); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); let ret = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); FunctionMethodOutcome::Value(ret) } diff --git a/crates/perry-runtime/src/value/to_string_class_ref.rs b/crates/perry-runtime/src/value/to_string_class_ref.rs index 19b9700700..83fea2e15a 100644 --- a/crates/perry-runtime/src/value/to_string_class_ref.rs +++ b/crates/perry-runtime/src/value/to_string_class_ref.rs @@ -51,9 +51,10 @@ pub(crate) unsafe fn custom_to_primitive(value: f64, hint: &[u8]) -> CustomToPri if !crate::closure::is_closure_ptr(method_ptr) { return CustomToPrimitiveOutcome::TypeError; } - let prev_this = crate::object::js_implicit_this_set(receiver); + let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_native_call_value(method, &hint, 1); - crate::object::js_implicit_this_set(prev_this); + crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); result }; From fbd753bf6bba4f3858f1b3ac6861d8040a907a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 2 Sep 2026 15:15:58 +0200 Subject: [PATCH 2/4] fix(json): root the replacer closure and root key across the root-level toJSON/replacer calls (#9445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found by the #9445 fixture: with the saved implicit-`this` rooted, an allocating replacer still SIGSEGV'd in js_closure_call2. js_json_stringify_full and js_json_stringify_with_replacer run the root toJSON and the root replacer call — both user code — and then handed the walk the raw closure pointer and the "" key. Root both and re-read at each use. --- crates/perry-runtime/src/json/replacer.rs | 40 +++++++++++++++++------ 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/crates/perry-runtime/src/json/replacer.rs b/crates/perry-runtime/src/json/replacer.rs index b98651c75c..48117f34bc 100644 --- a/crates/perry-runtime/src/json/replacer.rs +++ b/crates/perry-runtime/src/json/replacer.rs @@ -685,20 +685,28 @@ pub unsafe extern "C" fn js_json_stringify_with_replacer( // Fall back to normal stringify if replacer is null return js_json_stringify(value, type_hint); } + // #9445: the root `toJSON` and the root replacer call below both run user + // code. The replacer closure and the `""` key are young heap cells held in + // bare locals across them and are consumed AFTER (the key by the replacer + // call, the closure by the walk) — an evacuating minor inside either + // callback left the walk calling a retired closure (SIGSEGV in + // `js_closure_call2`). Root both and re-read at each use. + let root_scope = crate::gc::RuntimeHandleScope::new(); + let replacer_root = root_scope.root_raw_const_ptr(replacer); // Per JSON spec, the initial call to the replacer is with key="" and the // root value — but toJSON runs FIRST (SerializeJSONProperty step 2). let empty_str = js_string_from_bytes(b"".as_ptr(), 0); - let empty_key_f64 = nanbox_string_f64(empty_str); - let value_after_to_json = apply_to_json_keyed(value, empty_key_f64); + let empty_key_root = root_scope.root_nanbox_f64(nanbox_string_f64(empty_str)); + let value_after_to_json = apply_to_json_keyed(value, empty_key_root.get_nanbox_f64()); // Call replacer with ("", root_value), `this` = the `{ "": value }` wrapper. // Per spec the holder wraps the ORIGINAL root value (so a root replacer's // `this[""]` observes the pre-`toJSON` value); only the replacer's value // argument is post-`toJSON`. CodeRabbit (PR #5438). let replaced_root = call_replacer( - replacer, - empty_key_f64, + replacer_root.get_raw_const_ptr::(), + empty_key_root.get_nanbox_f64(), value_after_to_json, root_holder(value), ); @@ -739,7 +747,14 @@ pub unsafe extern "C" fn js_json_stringify_with_replacer( // inline, pointers via the GC-tag dispatch (compact, no indent). if !write_replaced_scalar(&mut buf, replaced_root) { let ptr = extract_pointer(replaced_bits).unwrap(); - dispatch_pointer_with_replacer(ptr, replaced_root, replacer, &mut buf, "", 0); + dispatch_pointer_with_replacer( + ptr, + replaced_root, + replacer_root.get_raw_const_ptr::(), + &mut buf, + "", + 0, + ); } let result = js_string_from_bytes(buf.as_ptr(), buf.len() as u32); @@ -1743,12 +1758,17 @@ pub unsafe extern "C" fn js_json_stringify_full( // Function replacer. Per spec SerializeJSONProperty: toJSON FIRST, then // the replacer, then serialize — threading `indent_str` so the 3-arg // form (replacer + space) pretty-prints, matching Node. + // + // #9445: see `js_json_stringify_with_replacer` — the closure and the + // `""` key live across two user callbacks and are consumed after them. + let root_scope = crate::gc::RuntimeHandleScope::new(); + let replacer_root = root_scope.root_raw_const_ptr(closure_ptr); let empty_str = js_string_from_bytes(b"".as_ptr(), 0); - let empty_key_f64 = nanbox_string_f64(empty_str); - let value_after_to_json = apply_to_json_keyed(value, empty_key_f64); + let empty_key_root = root_scope.root_nanbox_f64(nanbox_string_f64(empty_str)); + let value_after_to_json = apply_to_json_keyed(value, empty_key_root.get_nanbox_f64()); let replaced_root = call_replacer( - closure_ptr, - empty_key_f64, + replacer_root.get_raw_const_ptr::(), + empty_key_root.get_nanbox_f64(), value_after_to_json, root_holder(value_after_to_json), ); @@ -1772,7 +1792,7 @@ pub unsafe extern "C" fn js_json_stringify_full( dispatch_pointer_with_replacer( ptr, replaced_root, - closure_ptr, + replacer_root.get_raw_const_ptr::(), &mut buf, &indent_str, 0, From d65ee8765b5b961ab14dd78cc38cb8adf6596391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 2 Sep 2026 15:15:58 +0200 Subject: [PATCH 3/4] fix(runtime): root the displaced receiver once per callback loop; reuse existing handle scopes (#9445) Callback loops (Map/Set/URLSearchParams forEach, EventTarget dispatch, the emitters, fs.watch fan-out, the timer batch, TypedArray.from's map callback) root the caller's receiver once before the loop and restore from that handle each iteration, instead of opening a scope per callback. Single-call sites that already own a RuntimeHandleScope push onto it. Three bare-name callers of js_implicit_this_get are fully qualified. --- crates/perry-runtime/src/array/iterator.rs | 8 +++----- crates/perry-runtime/src/async_hooks.rs | 3 +-- .../perry-runtime/src/child_process/emitter.rs | 6 ++++-- crates/perry-runtime/src/cluster.rs | 12 ++++++++---- crates/perry-runtime/src/event_target.rs | 7 ++++--- .../src/fs/dir_glob_watch/watch.rs | 17 +++++++---------- crates/perry-runtime/src/json/reviver.rs | 3 +-- crates/perry-runtime/src/json/stringify.rs | 3 +-- crates/perry-runtime/src/map.rs | 5 +++-- crates/perry-runtime/src/messaging.rs | 4 +--- .../src/node_submodules/trace_events.rs | 3 +-- crates/perry-runtime/src/node_vm/modules.rs | 3 +-- .../parent_static/private_and_dynamic.rs | 3 +-- .../src/object/global_this/bigint_promise.rs | 6 ++++-- .../src/object/native_call_method.rs | 3 +-- crates/perry-runtime/src/object/property_key.rs | 6 ++---- crates/perry-runtime/src/pty/mod.rs | 6 ++++-- crates/perry-runtime/src/set.rs | 5 +++-- crates/perry-runtime/src/symbol/iterator.rs | 3 +-- crates/perry-runtime/src/timer.rs | 13 +++++-------- crates/perry-runtime/src/url/search_params.rs | 7 ++++--- crates/perry-runtime/src/util_promisify.rs | 3 +-- crates/perry-runtime/src/value/to_string.rs | 11 +++++------ 23 files changed, 66 insertions(+), 74 deletions(-) diff --git a/crates/perry-runtime/src/array/iterator.rs b/crates/perry-runtime/src/array/iterator.rs index 1b3ffffd88..7b1e352868 100644 --- a/crates/perry-runtime/src/array/iterator.rs +++ b/crates/perry-runtime/src/array/iterator.rs @@ -481,9 +481,8 @@ fn async_from_sync_call_raw(iter: f64, method: &[u8], args: &[f64]) -> Result *mut ArrayHeader { // Call(next, iterator): bind `this` for the stored-closure path // exactly like `js_iterator_next_result` — a user-assigned // `it.next = function () { … }` may read `this` (#9019). - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope - .root_nanbox_f64(crate::object::js_implicit_this_set(iter_h.get_nanbox_f64())); + let prev_this = + scope.root_nanbox_f64(crate::object::js_implicit_this_set(iter_h.get_nanbox_f64())); let r = closure::js_closure_call1( js_nanbox_get_pointer(next_h.get_nanbox_f64()) as *const closure::ClosureHeader, f64::from_bits(TAG_UNDEFINED), diff --git a/crates/perry-runtime/src/async_hooks.rs b/crates/perry-runtime/src/async_hooks.rs index 28703c7f53..05490c2f1d 100644 --- a/crates/perry-runtime/src/async_hooks.rs +++ b/crates/perry-runtime/src/async_hooks.rs @@ -1811,8 +1811,7 @@ fn call_callback_with_rest(callback_value: f64, this_arg: f64, rest: f64) -> f64 } let args_array = ptr_from_nanboxed(rest) as *const ArrayHeader; let args_array_handle = scope.root_raw_const_ptr(args_array); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( this_arg_handle.get_nanbox_f64(), )); let result = if args_array.is_null() { diff --git a/crates/perry-runtime/src/child_process/emitter.rs b/crates/perry-runtime/src/child_process/emitter.rs index aca68e59b4..d816ea5618 100644 --- a/crates/perry-runtime/src/child_process/emitter.rs +++ b/crates/perry-runtime/src/child_process/emitter.rs @@ -52,6 +52,9 @@ pub(crate) fn cp_emit(target: f64, event: &str, args: &[f64]) -> bool { let key = cp_listener_key(event); let mut i: u32 = 0; let mut fired = false; + let this_scope = crate::gc::RuntimeHandleScope::new(); + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); loop { let arr = match cp_array_ptr(cp_get_field(target, &key)) { Some(a) => a, @@ -61,8 +64,7 @@ pub(crate) fn cp_emit(target: f64, event: &str, args: &[f64]) -> bool { break; } let cb = crate::array::js_array_get_f64(arr, i); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(js_implicit_this_set(target)); + js_implicit_this_set(target); unsafe { let _ = js_native_call_value(cb, args.as_ptr(), args.len()); } diff --git a/crates/perry-runtime/src/cluster.rs b/crates/perry-runtime/src/cluster.rs index dee5698e39..4dfcc26d8e 100644 --- a/crates/perry-runtime/src/cluster.rs +++ b/crates/perry-runtime/src/cluster.rs @@ -303,10 +303,12 @@ pub(crate) fn cluster_emit_event(event: &str, args: &[f64]) -> bool { if listeners.is_empty() { return false; } + let this_scope = crate::gc::RuntimeHandleScope::new(); + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for listener in listeners { let cb = f64::from_bits(listener.callback_bits); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(js_implicit_this_set(cluster_default_value())); + js_implicit_this_set(cluster_default_value()); unsafe { let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); } @@ -1416,6 +1418,9 @@ fn emit(target: f64, event: &str, args: &[f64]) -> bool { let key = listener_key(event); let mut i = 0; let mut fired = false; + let this_scope = crate::gc::RuntimeHandleScope::new(); + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); loop { let Some(arr) = array_ptr(get_field(target, &key)) else { break; @@ -1424,8 +1429,7 @@ fn emit(target: f64, event: &str, args: &[f64]) -> bool { break; } let cb = crate::array::js_array_get_f64(arr, i); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(js_implicit_this_set(target)); + js_implicit_this_set(target); unsafe { let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); } diff --git a/crates/perry-runtime/src/event_target.rs b/crates/perry-runtime/src/event_target.rs index fabb6e1e25..31d5c4b99d 100644 --- a/crates/perry-runtime/src/event_target.rs +++ b/crates/perry-runtime/src/event_target.rs @@ -979,6 +979,9 @@ pub unsafe extern "C" fn js_event_target_dispatch_event( .unwrap_or_default(); let args = [event]; + let this_scope = crate::gc::RuntimeHandleScope::new(); + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for (callback, capture, once) in callbacks { let Some(callable) = closure_value_from_listener(callback) else { continue; @@ -986,9 +989,7 @@ pub unsafe extern "C" fn js_event_target_dispatch_event( if once { remove_event_listener_value_with_capture(target, event_name_ptr, callback, capture); } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(target_value)); + crate::object::js_implicit_this_set(target_value); let _ = crate::closure::js_native_call_value(callable, args.as_ptr(), args.len()); crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); if event_bool_field(event_ptr, b"_immediateStopped") { diff --git a/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs b/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs index d833dd306b..3502a4755d 100644 --- a/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs +++ b/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs @@ -518,8 +518,7 @@ fn emit_listener0(object_value: f64, callback: f64) { if cb.is_null() { return; } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( object_handle.get_nanbox_f64(), )); with_watcher_uncaught_trap(|| { @@ -549,15 +548,14 @@ fn emit_fs_watch_event( let refreshed_callbacks = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&callback_handles); let refreshed_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for callback in refreshed_callbacks { let cb = extract_closure_ptr(callback); if cb.is_null() { continue; } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( - object_handle.get_nanbox_f64(), - )); + crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); with_watcher_uncaught_trap(|| { crate::closure::js_closure_call2(cb, refreshed_args[0], refreshed_args[1]); }); @@ -587,15 +585,14 @@ fn emit_watch_file_change( let refreshed_callbacks = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&callback_handles); let refreshed_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for callback in refreshed_callbacks { let cb = extract_closure_ptr(callback); if cb.is_null() { continue; } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( - object_handle.get_nanbox_f64(), - )); + crate::object::js_implicit_this_set(object_handle.get_nanbox_f64()); with_watcher_uncaught_trap(|| { crate::closure::js_closure_call2(cb, refreshed_args[0], refreshed_args[1]); }); diff --git a/crates/perry-runtime/src/json/reviver.rs b/crates/perry-runtime/src/json/reviver.rs index 6e74da888d..8280dfe10b 100644 --- a/crates/perry-runtime/src/json/reviver.rs +++ b/crates/perry-runtime/src/json/reviver.rs @@ -715,8 +715,7 @@ unsafe fn call_reviver( let holder_arg = holder_handle.get_nanbox_f64(); let key_arg = key_handle.get_nanbox_f64(); let value_arg = value_handle.get_nanbox_f64(); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(holder_arg)); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(holder_arg)); let result = crate::js_closure_call3(reviver, key_arg, value_arg, context_handle.get_nanbox_f64()); crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); diff --git a/crates/perry-runtime/src/json/stringify.rs b/crates/perry-runtime/src/json/stringify.rs index f3db707484..fc54cecbc3 100644 --- a/crates/perry-runtime/src/json/stringify.rs +++ b/crates/perry-runtime/src/json/stringify.rs @@ -347,8 +347,7 @@ pub(crate) unsafe fn array_get_to_json(arr: *const crate::ArrayHeader) -> Option let recv_handle = scope.root_nanbox_f64(recv); // `toJSON(key)` receives the property key of this array value (#5909). let key_f64_arg = current_to_json_key_arg(); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( recv_handle.get_nanbox_f64(), )); let result = crate::closure::js_native_call_value(f64::from_bits(method_bits), &key_f64_arg, 1); diff --git a/crates/perry-runtime/src/map.rs b/crates/perry-runtime/src/map.rs index 3a7b790383..27fc9474c2 100644 --- a/crates/perry-runtime/src/map.rs +++ b/crates/perry-runtime/src/map.rs @@ -3128,6 +3128,8 @@ fn js_map_foreach_impl( // appends are visited too. Bounding the walk by `size` exposed holes // and truncated later entries (#9072). let mut i = 0usize; + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); loop { let map = map_handle.get_raw_const_ptr::(); if i >= (*map).used as usize { @@ -3154,8 +3156,7 @@ fn js_map_foreach_impl( // Bind `thisArg` for the duration of the call (matches the // URLSearchParams.forEach pattern); `js_native_call_value` // dispatches the NaN-boxed callback with the full arg vector. - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_v)); + crate::object::js_implicit_this_set(this_v); let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } diff --git a/crates/perry-runtime/src/messaging.rs b/crates/perry-runtime/src/messaging.rs index e87b5f3f1a..c16f9bec78 100644 --- a/crates/perry-runtime/src/messaging.rs +++ b/crates/perry-runtime/src/messaging.rs @@ -279,9 +279,7 @@ fn invoke_message_handler(handler: f64, event: f64, port_box: f64) { let handler_h = scope.root_nanbox_f64(handler); let event_h = scope.root_nanbox_f64(event); let port_h = scope.root_nanbox_f64(port_box); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = - this_scope.root_nanbox_f64(object::js_implicit_this_set(port_h.get_nanbox_f64())); + let prev_this = scope.root_nanbox_f64(object::js_implicit_this_set(port_h.get_nanbox_f64())); let args = [event_h.get_nanbox_f64()]; unsafe { let _ = crate::closure::js_native_call_value( diff --git a/crates/perry-runtime/src/node_submodules/trace_events.rs b/crates/perry-runtime/src/node_submodules/trace_events.rs index af52b7b484..540ea0f4ac 100644 --- a/crates/perry-runtime/src/node_submodules/trace_events.rs +++ b/crates/perry-runtime/src/node_submodules/trace_events.rs @@ -611,8 +611,7 @@ fn emit_enabled_trace_warning() { let process = scope.root_nanbox_f64(process); let callback = scope.root_nanbox_f64(callback); let warning = scope.root_nanbox_f64(warning); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let previous = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + let previous = scope.root_nanbox_f64(crate::object::js_implicit_this_set( process.get_nanbox_f64(), )); unsafe { diff --git a/crates/perry-runtime/src/node_vm/modules.rs b/crates/perry-runtime/src/node_vm/modules.rs index a0f94f56e4..9d27ad49fd 100644 --- a/crates/perry-runtime/src/node_vm/modules.rs +++ b/crates/perry-runtime/src/node_vm/modules.rs @@ -75,8 +75,7 @@ fn evaluate_synthetic_module(module: *mut ObjectHeader) -> f64 { })); let js = JSValue::from_bits(callback.get_nanbox_f64().to_bits()); if !js.is_undefined() && !js.is_null() { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(with_hmut( + let prev = scope.root_nanbox_f64(crate::object::js_implicit_this_set(with_hmut( &module, object_value, ))); diff --git a/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs b/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs index ae568602a3..eff6bec662 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs @@ -295,8 +295,7 @@ pub(crate) unsafe fn call_private_static_method_for_owner( let scope = crate::gc::RuntimeHandleScope::new(); let this_value = scope.root_nanbox_f64(this_value); let private_brand = scope.root_nanbox_f64(private_brand); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let previous_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + let previous_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( this_value.get_nanbox_f64(), )); crate::object::static_private_owner_push(private_brand.get_nanbox_f64()); diff --git a/crates/perry-runtime/src/object/global_this/bigint_promise.rs b/crates/perry-runtime/src/object/global_this/bigint_promise.rs index c15889a8ab..d03cb60106 100644 --- a/crates/perry-runtime/src/object/global_this/bigint_promise.rs +++ b/crates/perry-runtime/src/object/global_this/bigint_promise.rs @@ -692,12 +692,14 @@ pub(crate) extern "C" fn typed_array_from_thunk( // possibly throwing) element coercion INTERLEAVE per spec, so an abrupt // coercion at element k means the map callback never ran for k+1 // (test262 from/set-value-abrupt-completion). + let this_scope = crate::gc::RuntimeHandleScope::new(); + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); let map_at = |k: usize, v: f64| -> f64 { if map_closure.is_null() { return v; } - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); + crate::object::js_implicit_this_set(this_arg); let r = crate::closure::js_closure_call2(map_closure, v, k as f64); crate::object::js_implicit_this_set(prev.get_nanbox_f64()); r diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index fe73a62449..6f2301c9d0 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -2510,9 +2510,8 @@ pub unsafe extern "C-unwind" fn js_native_call_method( if crate::promise::subclass_backing_promise(object()).is_some() { if let Some(m) = crate::promise::promise_proto_method(method_name) { let args = refreshed_args(); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(object())); + root_scope.root_nanbox_f64(crate::object::js_implicit_this_set(object())); let result = crate::closure::js_native_call_value(m, args.as_ptr(), args.len()); crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); return result; diff --git a/crates/perry-runtime/src/object/property_key.rs b/crates/perry-runtime/src/object/property_key.rs index d1d360a125..116377a03d 100644 --- a/crates/perry-runtime/src/object/property_key.rs +++ b/crates/perry-runtime/src/object/property_key.rs @@ -129,8 +129,7 @@ unsafe fn ordinary_to_primitive_string_key(value: f64) -> Option { continue; } let bound = crate::closure::clone_closure_rebind_this(method_bits, receiver); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); @@ -570,8 +569,7 @@ pub unsafe extern "C" fn js_object_super_call( let bound = crate::closure::clone_closure_rebind_this(callee_handle.get_nanbox_u64(), receiver); let bound_handle = scope.root_nanbox_u64(bound); let receiver = f64::from_bits(receiver_handle.get_heap_word_u64()); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); let result = crate::closure::js_native_call_value( f64::from_bits(bound_handle.get_nanbox_u64()), args_ptr, diff --git a/crates/perry-runtime/src/pty/mod.rs b/crates/perry-runtime/src/pty/mod.rs index c8a30e438d..439be74300 100644 --- a/crates/perry-runtime/src/pty/mod.rs +++ b/crates/perry-runtime/src/pty/mod.rs @@ -77,6 +77,9 @@ mod unix_impl { pub(crate) fn pty_emit(target: f64, event: &str, args: &[f64]) { let key = pty_listener_key(event); let mut i: u32 = 0; + let this_scope = crate::gc::RuntimeHandleScope::new(); + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); loop { let arr = match cp_array_ptr(cp_get_field(target, &key)) { Some(a) => a, @@ -86,8 +89,7 @@ mod unix_impl { break; } let cb = crate::array::js_array_get_f64(arr, i); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope.root_nanbox_f64(js_implicit_this_set(target)); + js_implicit_this_set(target); unsafe { let _ = js_native_call_value(cb, args.as_ptr(), args.len()); } diff --git a/crates/perry-runtime/src/set.rs b/crates/perry-runtime/src/set.rs index 7f12f45666..0b4b7a4768 100644 --- a/crates/perry-runtime/src/set.rs +++ b/crates/perry-runtime/src/set.rs @@ -1919,6 +1919,8 @@ fn js_set_foreach_impl( // during the callback are visited too. Bounding this by the live count // surfaced holes and stopped before later live values (#9072). let mut i = 0usize; + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); loop { let set = set_handle.get_raw_const_ptr::(); if i >= (*set).used as usize { @@ -1941,8 +1943,7 @@ fn js_set_foreach_impl( let args = [value, value, set_value]; let cb = callback_handle.get_nanbox_f64(); let this_v = this_handle.get_nanbox_f64(); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_v)); + crate::object::js_implicit_this_set(this_v); let _ = crate::closure::js_native_call_value(cb, args.as_ptr(), args.len()); crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } diff --git a/crates/perry-runtime/src/symbol/iterator.rs b/crates/perry-runtime/src/symbol/iterator.rs index 80de035c62..2c732a05d3 100644 --- a/crates/perry-runtime/src/symbol/iterator.rs +++ b/crates/perry-runtime/src/symbol/iterator.rs @@ -654,8 +654,7 @@ pub unsafe extern "C" fn js_to_primitive(value: f64, hint: i32) -> f64 { // `this.celsius` resolved to `undefined`, so `+t` was `NaN` and `` `${t}` `` // was `undefined°C` (test_gap_symbols). The proxy arm above already binds // `this`; mirror it for the closure method. - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( value_handle.get_nanbox_f64(), )); // Spec says the return value must be a primitive; if it's still an diff --git a/crates/perry-runtime/src/timer.rs b/crates/perry-runtime/src/timer.rs index 5f3a27675c..c09f2cac66 100644 --- a/crates/perry-runtime/src/timer.rs +++ b/crates/perry-runtime/src/timer.rs @@ -516,9 +516,8 @@ fn call_timer_callback( let previous_roots = crate::async_context::root_snapshot(&scope, &previous); let a = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); let cb = callback_handle.get_raw_const_ptr::(); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(timer_handle_value(id))); + scope.root_nanbox_f64(crate::object::js_implicit_this_set(timer_handle_value(id))); with_timer_uncaught_trap(|| unsafe { crate::closure::js_closure_call_array(cb as i64, a.as_ptr(), a.len() as i64); }); @@ -1296,6 +1295,8 @@ pub extern "C" fn js_callback_timer_tick() -> i32 { let mut fired = 0; // Call the callbacks, forwarding any trailing args captured at // `setTimeout(fn, delay, ...args)` time. Refs #665. + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev_this = batch_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for (index, mut timer) in expired.into_iter().enumerate() { if !timer.cleared { crate::async_context::refresh_snapshot_from_roots( @@ -1306,10 +1307,7 @@ pub extern "C" fn js_callback_timer_tick() -> i32 { let mut previous = previous; let previous_roots = crate::async_context::root_snapshot(&batch_scope, &previous); crate::async_hooks::before(timer.async_id, timer.trigger_async_id); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set( - timer_handle_value(timer.id), - )); + crate::object::js_implicit_this_set(timer_handle_value(timer.id)); enter_timer_callback_dispatch(); with_timer_uncaught_trap(|| { // Installing the timer receiver above is itself a collecting @@ -1728,9 +1726,8 @@ pub extern "C" fn js_interval_timer_tick() -> i32 { let previous = crate::async_context::enter_context(&context); let mut previous = previous; let previous_roots = crate::async_context::root_snapshot(&scope, &previous); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(timer_handle_value(id))); + scope.root_nanbox_f64(crate::object::js_implicit_this_set(timer_handle_value(id))); enter_timer_callback_dispatch(); crate::async_hooks::before(async_id, trigger_async_id); with_timer_uncaught_trap(|| { diff --git a/crates/perry-runtime/src/url/search_params.rs b/crates/perry-runtime/src/url/search_params.rs index 6f2afd84b8..d43d066cad 100644 --- a/crates/perry-runtime/src/url/search_params.rs +++ b/crates/perry-runtime/src/url/search_params.rs @@ -926,6 +926,9 @@ pub extern "C" fn js_url_search_params_for_each( crate::fs::validate::validate_function("callback", callback); let entries = get_url_search_params_entries(params); let this_value = crate::value::js_nanbox_pointer(params as i64); + let this_scope = crate::gc::RuntimeHandleScope::new(); + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for (key, value) in entries { let args = [ create_string_f64(&value), @@ -933,9 +936,7 @@ pub extern "C" fn js_url_search_params_for_each( this_value, ]; unsafe { - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = - this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(this_arg)); + crate::object::js_implicit_this_set(this_arg); let _ = crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()); crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); } diff --git a/crates/perry-runtime/src/util_promisify.rs b/crates/perry-runtime/src/util_promisify.rs index 58210d5695..262d6bbcd1 100644 --- a/crates/perry-runtime/src/util_promisify.rs +++ b/crates/perry-runtime/src/util_promisify.rs @@ -763,8 +763,7 @@ extern "C" fn callbackify_outer_thunk(closure: *const ClosureHeader, rest_value: let on_rejected = nanbox_pointer(rejected_handle.get_raw_const_ptr::() as *const u8); let args = [on_fulfilled, on_rejected]; - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(returned)); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(returned)); unsafe { crate::closure::js_native_call_value( then_handle.get_nanbox_f64(), diff --git a/crates/perry-runtime/src/value/to_string.rs b/crates/perry-runtime/src/value/to_string.rs index 3f600a3dd3..12bbd5ec1f 100644 --- a/crates/perry-runtime/src/value/to_string.rs +++ b/crates/perry-runtime/src/value/to_string.rs @@ -183,6 +183,8 @@ pub(crate) unsafe fn ordinary_to_primitive_for_toprimitive( } else { [b"valueOf", b"toString"] }; + // #9445: the displaced receiver is rooted ONCE here, not once per callback. + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_get()); for name in order { let recv = value_handle.get_nanbox_f64(); let key_ptr = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); @@ -197,8 +199,7 @@ pub(crate) unsafe fn ordinary_to_primitive_for_toprimitive( } let method_handle = scope.root_nanbox_f64(method); let recv = value_handle.get_nanbox_f64(); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); + crate::object::js_implicit_this_set(recv); let result = crate::closure::js_native_call_value( method_handle.get_nanbox_f64(), std::ptr::null(), @@ -835,8 +836,7 @@ unsafe fn call_method_for_primitive( // receiver, so rebinding is a correct no-op. Mirrors #1982. let recv = value_handle.get_nanbox_f64(); let bound = crate::closure::clone_closure_rebind_this(method_bits, recv); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); let ret = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); let ret_jsv = JSValue::from_bits(ret.to_bits()); @@ -891,8 +891,7 @@ unsafe fn call_function_method( let method_handle = scope.root_nanbox_f64(method); let bound = crate::closure::clone_closure_rebind_this(method_handle.get_nanbox_u64(), recv); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev_this = this_scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); + let prev_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set(recv)); let ret = crate::closure::js_native_call_value(f64::from_bits(bound), std::ptr::null(), 0); crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); From 125df982aadb1802f86b3fa47087c9917a6e8df9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 2 Sep 2026 15:29:08 +0200 Subject: [PATCH 4/4] =?UTF-8?q?test:=20#9445=20fixture=20=E2=80=94=2034=20?= =?UTF-8?q?witnesses=20for=20the=20implicit-this=20restore=20across=20a=20?= =?UTF-8?q?moving=20minor,=20plus=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../9445-implicit-this-restore-sweep.md | 62 ++ ...st_gap_9445_implicit_this_restore_sweep.ts | 569 ++++++++++++++++++ 2 files changed, 631 insertions(+) create mode 100644 changelog.d/9445-implicit-this-restore-sweep.md create mode 100644 test-files/test_gap_9445_implicit_this_restore_sweep.ts diff --git a/changelog.d/9445-implicit-this-restore-sweep.md b/changelog.d/9445-implicit-this-restore-sweep.md new file mode 100644 index 0000000000..d485226def --- /dev/null +++ b/changelog.d/9445-implicit-this-restore-sweep.md @@ -0,0 +1,62 @@ +**Every runtime callback site now restores the caller's `this` through a GC +root** (#9445) — the sweep PR #9444 asked for after fixing four accessor sites +for #9417. + +The runtime binds a callback's receiver by writing the GC-rooted +`IMPLICIT_THIS` cell and keeping the previous occupant in a **bare Rust local** +for the duration of the callback: + +```rust +let prev = js_implicit_this_set(receiver); +… user code, which allocates … +js_implicit_this_set(prev); // pre-collection address +``` + +That local is the caller's receiver, and the collector cannot see or rewrite +it. An evacuating young-gen minor inside the window — perry's default GC since +PR #7019 — relocates the caller's object, and the restore reinstalls a retired +from-space address as the caller's `this`. Nothing faults: the caller's next +`this.` fails the object-type check on the recycled cell and answers +`undefined`, so the member access after it throws a TypeError naming a +property nowhere near the defect (#9417's `Cannot read properties of +undefined (reading 'def')`). + +The issue counted ~20 sites; a grep of the whole runtime finds **122** +unrooted save/restores in 65 files (one of them landed with #9518 while this +sweep was in flight) (timers, node streams, dgram, cluster, +`fs.watch`, `EventTarget`, `Map`/`Set`/`URLSearchParams.forEach`, promisify, +JSON `toJSON`/replacer/reviver, ToPrimitive and ToPropertyKey, the iterator +protocol, Proxy traps and `Reflect`, bound functions, `super.x`, static +dispatch, …). Every one is now the idiom `prototype_chain.rs` and PR #9444 +already use: root the saved value in a `RuntimeHandleScope` and re-read it at +the restore. Nine sites were already rooted; `dyn_eval/bridge.rs` roots +through its own stack. None of the 122 could be left alone — every one calls +user code (a closure, a class accessor or static method, a Proxy trap, a +`then`), which can allocate. Callback loops (`Map`/`Set`/`URLSearchParams.forEach`, +`EventTarget` dispatch, the emitters, watchers and timer batches) root the +displaced receiver **once per loop** rather than once per callback, and sites +that already own a `RuntimeHandleScope` reuse it, so the hot per-callback cost +is one handle read. Three sites also consumed their receiver again *after* the +call (`intl_subclass_super`, `temporal_subclass_super`, and the `process.stdin` +listener loops); those re-read it through a root too. + +**Also fixed, same family, found by the fixture:** `JSON.stringify(value, +replacerFn)` handed the walk a **raw replacer closure pointer** after the +root-level replacer call (and the root `toJSON`) had run user code +(`json/replacer.rs`, both the pretty and the compact entry points). With an +allocating replacer this was a SIGSEGV in `js_closure_call2` — the walk called +a retired closure — and it survived the `prev` rooting alone. The closure and +the `""` key are now rooted across those calls. + +**Test.** `test-files/test_gap_9445_implicit_this_restore_sweep.ts` — 34 +cases, one per synchronously reachable site family, each a `function`-method +on a fresh young object that drives the site with an allocating callback and +then reads `this`. Deterministic, no GC env knobs; the PR description records +which cases print a non-zero `bad=` count on unfixed `main`. Event-loop-driven +sites (timers, `process.stdin`, dgram, cluster, `fs.watch`, pty, child +process) only see a heap `prev` from a nested pump and have no synchronous +reproduction; they carry the same mechanical fix. Two further candidate cases +(a `defineProperty` accessor on a typed array, a `toISOString` override on a +`Date`) diverge from node for a reason unrelated to rooting and are filed as +#9529; a `util.callbackify` case crashed the microtask pump at exit on every +build (a promise-side rooting bug, filed separately) and is not in the file. diff --git a/test-files/test_gap_9445_implicit_this_restore_sweep.ts b/test-files/test_gap_9445_implicit_this_restore_sweep.ts new file mode 100644 index 0000000000..3205cfd48a --- /dev/null +++ b/test-files/test_gap_9445_implicit_this_restore_sweep.ts @@ -0,0 +1,569 @@ +// #9445: every runtime site that binds `this` for a callback must not corrupt +// the CALLER's `this` across an evacuating young-gen minor. +// +// The shape (#9417 / PR #9444 fixed it for the accessor sites; this is the +// sweep of the ~120 others): +// +// let prev = js_implicit_this_set(receiver); +// … user code, which allocates … +// js_implicit_this_set(prev); // pre-collection address +// +// `prev` is the caller's receiver in a bare Rust local. A copying minor inside +// the window relocates that object and rewrites every slot the collector can +// see — a Rust local is not one — so the restore reinstalls a retired +// from-space address as the caller's `this`. Nothing faults: the caller's next +// `this.` reads `undefined` off the recycled cell and the member access +// after it throws `Cannot read properties of undefined (reading 'def')`. +// +// Each case below is a factory returning a fresh (young, escaping) object whose +// `run` is a `function`-expression method — one that reads `this` dynamically +// off the implicit-`this` cell, not from a captured slot. `run` drives ONE +// runtime site with a callback that allocates past the nursery, then reads +// `this.inner.def`. The callback is reached through a syntax or a typed +// builtin that lowers to a direct runtime call, so no compiled-code +// save/restore sits between the runtime site and `run`'s next `this` read. +// +// Pre-fix each case prints a non-zero `bad=` count, deterministically and with +// no GC env knobs. Node prints 0 for every line. + +import { Writable, Readable, Transform } from "node:stream"; + +const N = 1500; + +function churn(): number { + const tmp: any[] = []; + for (let k = 0; k < 480; k++) tmp.push({ k: k, s: "t" + k, pad: [k, k + 1] }); + return tmp.length; +} + +function check(name: string, factory: (i: number) => any): void { + let bad = 0; + const notes: string[] = []; + for (let i = 0; i < N; i++) { + const c: any = factory(i); + const want = "c" + i + ":480"; + let got: any; + try { + got = c.run(); + } catch (e: any) { + got = "THREW:" + (e && e.message); + } + if (got !== want) { + bad++; + if (bad <= 2) notes.push("[" + i + " got=" + String(got) + "]"); + } + } + console.log(name + " bad=" + bad + notes.join("")); +} + +function host(i: number, run: (this: any) => string): any { + return { id: i, inner: { def: "c" + i }, run: run }; +} + +function userIterable(onNext: () => void): any { + return { + [Symbol.iterator]: function () { + let done = false; + return { + next: function () { + if (done) return { done: true, value: undefined }; + done = true; + onNext(); + return { done: false, value: 1 }; + }, + }; + }, + }; +} + +// --- collections --------------------------------------------------------- + +check("map_forEach", function (i) { + const m = new Map([[1, 1]]); + return host(i, function (this: any) { + let n = 0; + m.forEach(function () { + n = churn(); + }); + return this.inner.def + ":" + n; + }); +}); + +check("set_forEach", function (i) { + const s = new Set([1]); + return host(i, function (this: any) { + let n = 0; + s.forEach(function () { + n = churn(); + }); + return this.inner.def + ":" + n; + }); +}); + +check("urlsearchparams_forEach", function (i) { + const p = new URLSearchParams("a=1"); + return host(i, function (this: any) { + let n = 0; + p.forEach(function () { + n = churn(); + }); + return this.inner.def + ":" + n; + }); +}); + +// --- events -------------------------------------------------------------- + +check("event_target_dispatch", function (i) { + const et = new EventTarget(); + let n = 0; + et.addEventListener("x", function () { + n = churn(); + }); + return host(i, function (this: any) { + et.dispatchEvent(new Event("x")); + return this.inner.def + ":" + n; + }); +}); + +// --- property keys, accessors, ToPrimitive -------------------------------- + +check("to_property_key_toString", function (i) { + const target: any = { k: 1 }; + return host(i, function (this: any) { + let n = 0; + const key: any = { + toString: function () { + n = churn(); + return "k"; + }, + }; + const v = target[key]; + return this.inner.def + ":" + (v === 1 ? n : -1); + }); +}); + +check("defineProperty_setter", function (i) { + return host(i, function (this: any) { + let n = 0; + const o: any = {}; + Object.defineProperty(o, "s", { + set: function (_v: any) { + n = churn(); + }, + configurable: true, + }); + o.s = 1; + return this.inner.def + ":" + n; + }); +}); + +check("function_object_getter", function (i) { + return host(i, function (this: any) { + let n = 0; + const f: any = function () {}; + Object.defineProperty(f, "g", { + get: function () { + n = churn(); + return 1; + }, + configurable: true, + }); + const v = f.g; + return this.inner.def + ":" + (v === 1 ? n : -1); + }); +}); + +check("valueOf_to_primitive", function (i) { + return host(i, function (this: any) { + let n = 0; + const o: any = { + valueOf: function () { + n = churn(); + return 1; + }, + }; + const v = o + 1; + return this.inner.def + ":" + (v === 2 ? n : -1); + }); +}); + +check("toString_template", function (i) { + return host(i, function (this: any) { + let n = 0; + const o: any = { + toString: function () { + n = churn(); + return "s"; + }, + }; + const v = `${o}`; + return this.inner.def + ":" + (v === "s" ? n : -1); + }); +}); + +check("symbol_toPrimitive", function (i) { + return host(i, function (this: any) { + let n = 0; + const o: any = { + [Symbol.toPrimitive]: function (_hint: string) { + n = churn(); + return 1; + }, + }; + const v = +o; + return this.inner.def + ":" + (v === 1 ? n : -1); + }); +}); + +// --- JSON ---------------------------------------------------------------- + +check("json_stringify_getter", function (i) { + return host(i, function (this: any) { + let n = 0; + const o: any = {}; + Object.defineProperty(o, "g", { + get: function () { + n = churn(); + return 1; + }, + enumerable: true, + configurable: true, + }); + const s = JSON.stringify(o); + return this.inner.def + ":" + (s === '{"g":1}' ? n : -1); + }); +}); + +check("json_stringify_toJSON", function (i) { + return host(i, function (this: any) { + let n = 0; + const o: any = { + toJSON: function () { + n = churn(); + return 1; + }, + }; + const s = JSON.stringify({ o: o }); + return this.inner.def + ":" + (s === '{"o":1}' ? n : -1); + }); +}); + +check("json_stringify_replacer", function (i) { + return host(i, function (this: any) { + let n = 0; + const s = JSON.stringify({ a: 1 }, function (_k: string, v: any) { + n = churn(); + return v; + }); + return this.inner.def + ":" + (s === '{"a":1}' ? n : -1); + }); +}); + +check("json_parse_reviver", function (i) { + return host(i, function (this: any) { + let n = 0; + const o = JSON.parse('{"a":1}', function (_k: string, v: any) { + n = churn(); + return v; + }); + return this.inner.def + ":" + (o.a === 1 ? n : -1); + }); +}); + +// --- iteration protocol -------------------------------------------------- + +check("for_of_user_iterator", function (i) { + return host(i, function (this: any) { + let n = 0; + let sum = 0; + for (const v of userIterable(function () { + n = churn(); + })) { + sum += v; + } + return this.inner.def + ":" + (sum === 1 ? n : -1); + }); +}); + +check("spread_user_iterator", function (i) { + return host(i, function (this: any) { + let n = 0; + const arr = [ + ...userIterable(function () { + n = churn(); + }), + ]; + return this.inner.def + ":" + (arr.length === 1 ? n : -1); + }); +}); + +check("destructure_user_iterator", function (i) { + return host(i, function (this: any) { + let n = 0; + const [first] = userIterable(function () { + n = churn(); + }); + return this.inner.def + ":" + (first === 1 ? n : -1); + }); +}); + +check("array_from_user_iterator", function (i) { + return host(i, function (this: any) { + let n = 0; + const arr = Array.from( + userIterable(function () { + n = churn(); + }), + ); + return this.inner.def + ":" + (arr.length === 1 ? n : -1); + }); +}); + +// --- Proxy / Reflect ----------------------------------------------------- + +check("proxy_get_trap", function (i) { + return host(i, function (this: any) { + let n = 0; + const p: any = new Proxy( + {}, + { + get: function (_t: any, _k: any) { + n = churn(); + return 1; + }, + }, + ); + const v = p.x; + return this.inner.def + ":" + (v === 1 ? n : -1); + }); +}); + +check("proxy_set_trap", function (i) { + return host(i, function (this: any) { + let n = 0; + const p: any = new Proxy( + {}, + { + set: function (_t: any, _k: any, _v: any) { + n = churn(); + return true; + }, + }, + ); + p.x = 1; + return this.inner.def + ":" + n; + }); +}); + +check("proxy_apply_trap", function (i) { + return host(i, function (this: any) { + let n = 0; + const p: any = new Proxy(function () {}, { + apply: function () { + n = churn(); + return 1; + }, + }); + const v = p(); + return this.inner.def + ":" + (v === 1 ? n : -1); + }); +}); + +check("proxy_construct_trap", function (i) { + return host(i, function (this: any) { + let n = 0; + const p: any = new Proxy(function () {}, { + construct: function () { + n = churn(); + return { ok: 1 }; + }, + }); + const v = new p(); + return this.inner.def + ":" + (v.ok === 1 ? n : -1); + }); +}); + +check("reflect_apply", function (i) { + return host(i, function (this: any) { + let n = 0; + const v = Reflect.apply( + function () { + n = churn(); + return 1; + }, + null, + [], + ); + return this.inner.def + ":" + (v === 1 ? n : -1); + }); +}); + +check("reflect_get_receiver_getter", function (i) { + return host(i, function (this: any) { + let n = 0; + const o: any = { + get g() { + n = churn(); + return 1; + }, + }; + const v = Reflect.get(o, "g", o); + return this.inner.def + ":" + (v === 1 ? n : -1); + }); +}); + +// --- functions ----------------------------------------------------------- + +check("bound_function_call", function (i) { + return host(i, function (this: any) { + let n = 0; + const b = function (this: any) { + n = churn(); + return 1; + }.bind({}); + const v = b(); + return this.inner.def + ":" + (v === 1 ? n : -1); + }); +}); + +check("string_replace_callback", function (i) { + return host(i, function (this: any) { + let n = 0; + const s = "abc".replace(/b/, function () { + n = churn(); + return "x"; + }); + return this.inner.def + ":" + (s === "axc" ? n : -1); + }); +}); + +check("using_dispose", function (i) { + return host(i, function (this: any) { + let n = 0; + { + using _r = { + [Symbol.dispose]: function () { + n = churn(); + }, + }; + } + return this.inner.def + ":" + n; + }); +}); + +// --- node:stream user hooks ---------------------------------------------- + +check("writable_write", function (i) { + let n = 0; + const w = new Writable({ + write(_chunk, _enc, cb) { + n = churn(); + cb(); + }, + }); + return host(i, function (this: any) { + w.write("x"); + return this.inner.def + ":" + n; + }); +}); + +check("writable_writev", function (i) { + let n = 0; + const w = new Writable({ + write(_chunk, _enc, cb) { + cb(); + }, + writev(_chunks, cb) { + n = churn(); + cb(); + }, + }); + return host(i, function (this: any) { + w.cork(); + w.write("a"); + w.write("b"); + w.uncork(); + return this.inner.def + ":" + n; + }); +}); + +check("transform_transform", function (i) { + let n = 0; + const t = new Transform({ + transform(chunk, _enc, cb) { + n = churn(); + cb(null, chunk); + }, + }); + t.on("data", function () {}); + return host(i, function (this: any) { + t.write("x"); + return this.inner.def + ":" + n; + }); +}); + +check("writable_construct", function (i) { + return host(i, function (this: any) { + let n = 0; + new Writable({ + construct(cb) { + n = churn(); + cb(); + }, + write(_chunk, _enc, cb) { + cb(); + }, + }); + // node defers `_construct` to nextTick; only the caller's `this` matters. + return this.inner.def + ":" + (n === 0 || n === 480 ? 480 : -1); + }); +}); + +check("readable_read", function (i) { + let n = 0; + const r = new Readable({ + read() { + n = churn(); + this.push(null); + }, + }); + return host(i, function (this: any) { + r.read(); + return this.inner.def + ":" + n; + }); +}); + +check("writable_final", function (i) { + let n = 0; + const w = new Writable({ + write(_chunk, _enc, cb) { + cb(); + }, + final(cb) { + n = churn(); + cb(); + }, + }); + return host(i, function (this: any) { + w.end(); + return this.inner.def + ":" + n; + }); +}); + +check("transform_flush", function (i) { + let n = 0; + const t = new Transform({ + transform(chunk, _enc, cb) { + cb(null, chunk); + }, + flush(cb) { + n = churn(); + cb(); + }, + }); + t.on("data", function () {}); + return host(i, function (this: any) { + t.end(); + return this.inner.def + ":" + n; + }); +});