diff --git a/changelog.d/7455-raw-handle-debt.md b/changelog.d/7455-raw-handle-debt.md new file mode 100644 index 0000000000..9bd24f25b7 --- /dev/null +++ b/changelog.d/7455-raw-handle-debt.md @@ -0,0 +1 @@ +- **The Layer 3 raw-handle debt ratchet is green again, at a lower number.** It had been red on `main` since #7424 took it 1006 → 1008, and its baseline may only move *down*, so re-pinning was not an option. Both #7424 sites are correct — they re-derive a receiver after a proxy trap that can move it — but they spell that as a manual call/re-read pair rather than the `RuntimeHandle::across_*` combinator the RFC prescribes. Five such pairs are now converted (`array/push_pop.rs`, `promise/then.rs` ×2, `object/async_generator_queue.rs`, `string/append.rs`); each is semantically identical, but the ordering becomes structural instead of conventional — which is the point of #7341, where every fix in the family was an ordering bug rather than a missing root. 1008 → 1003, baseline locked at 1003. (#7455) diff --git a/crates/perry-runtime/src/array/push_pop.rs b/crates/perry-runtime/src/array/push_pop.rs index 801294c40a..a7b4a9649f 100644 --- a/crates/perry-runtime/src/array/push_pop.rs +++ b/crates/perry-runtime/src/array/push_pop.rs @@ -443,10 +443,14 @@ pub(super) fn proxy_array_mutator( for k in 0..actual_delete_count { let from = (actual_start + k).to_string(); if proxy_has_str_key(p(), from.as_bytes()) { - let v = proxy_get_str_key(p(), from.as_bytes()); - // Re-derive `removed` from its handle: the traps above - // run arbitrary JS, which can move it. - let removed = removed_handle.get_raw_mut_ptr::(); + // The trap runs arbitrary JS and can move `removed`, so + // its address is only valid after the call. `across_mut` + // is that pattern as one combinator: it runs the call and + // hands back the post-collection address, so a stale + // pointer is never bound in between (#7341). + let (v, removed) = removed_handle.across_mut::(|| { + proxy_get_str_key(p(), from.as_bytes()) + }); let elems = (removed as *mut u8).add(std::mem::size_of::()) as *mut f64; // GC_STORE_AUDIT(BARRIERED): note_array_slot re-stores diff --git a/crates/perry-runtime/src/object/async_generator_queue.rs b/crates/perry-runtime/src/object/async_generator_queue.rs index 96aecce872..3e53ced400 100644 --- a/crates/perry-runtime/src/object/async_generator_queue.rs +++ b/crates/perry-runtime/src/object/async_generator_queue.rs @@ -268,8 +268,10 @@ fn async_generator_request(closure: *const ClosureHeader, arg: f64, kind: Reques let scope = crate::gc::RuntimeHandleScope::new(); let original_handle = scope.root_raw_const_ptr(original); let arg_handle = scope.root_nanbox_f64(arg); - let promise = js_promise_new(); - let original = original_handle.get_raw_const_ptr::(); + // `across_const` pairs the allocating call with the re-read, so the + // closure pointer cannot be bound stale in between (#7341). + let (promise, original) = + original_handle.across_const::(|| js_promise_new()); let arg = arg_handle.get_nanbox_f64(); STATES.with(|states| { if let Some(state) = states.borrow_mut().get_mut(state_id - 1) { diff --git a/crates/perry-runtime/src/promise/then.rs b/crates/perry-runtime/src/promise/then.rs index 4a345b832c..a2659a408b 100644 --- a/crates/perry-runtime/src/promise/then.rs +++ b/crates/perry-runtime/src/promise/then.rs @@ -488,8 +488,10 @@ pub extern "C" fn js_promise_then( let promise_handle = scope.root_raw_mut_ptr(promise); let on_fulfilled_handle = scope.root_raw_const_ptr(on_fulfilled); let on_rejected_handle = scope.root_raw_const_ptr(on_rejected); - let next = js_promise_new_with_parent(promise); - let promise = promise_handle.get_raw_mut_ptr::(); + // `across_mut` runs the allocating call and returns the post-collection + // address, so the receiver is never bound stale in between (#7341). + let (next, promise) = + promise_handle.across_mut::(|| js_promise_new_with_parent(promise)); let on_fulfilled = on_fulfilled_handle.get_raw_const_ptr::(); let on_rejected = on_rejected_handle.get_raw_const_ptr::(); @@ -764,8 +766,10 @@ pub extern "C" fn js_promise_finally( let scope = crate::gc::RuntimeHandleScope::new(); let promise_handle = scope.root_raw_mut_ptr(promise); let on_finally_handle = scope.root_raw_const_ptr(on_finally); - let next = js_promise_new_with_parent(promise); - let promise = promise_handle.get_raw_mut_ptr::(); + // See the sibling in `then`: the allocating call and the re-read are one + // combinator so the order cannot drift apart (#7341). + let (next, promise) = + promise_handle.across_mut::(|| js_promise_new_with_parent(promise)); let on_finally = on_finally_handle.get_raw_const_ptr::(); let next_i64 = next as i64; diff --git a/crates/perry-runtime/src/string/append.rs b/crates/perry-runtime/src/string/append.rs index a3c15de56d..c0cde49f23 100644 --- a/crates/perry-runtime/src/string/append.rs +++ b/crates/perry-runtime/src/string/append.rs @@ -39,8 +39,11 @@ pub extern "C" fn js_string_append( let scope = crate::gc::RuntimeHandleScope::new(); let src_handle = scope.root_string_ptr(src); let src_blen = unsafe { (*src).byte_len }; - let new_ptr = js_string_from_bytes_with_capacity(ptr::null(), 0, src_blen); - let src = src_handle.get_raw_const_ptr::(); + // `across_const` pairs the allocating call with the re-read, so the + // source pointer cannot be bound stale in between (#7341). + let (new_ptr, src) = src_handle.across_const::(|| { + js_string_from_bytes_with_capacity(ptr::null(), 0, src_blen) + }); if is_valid_string_ptr(src) { unsafe { let src_data = string_data(src); diff --git a/scripts/raw_handle_debt_baseline.txt b/scripts/raw_handle_debt_baseline.txt index 9540e56f97..baccd0398f 100644 --- a/scripts/raw_handle_debt_baseline.txt +++ b/scripts/raw_handle_debt_baseline.txt @@ -1 +1 @@ -1006 +1003