diff --git a/crates/coop-worker/src/plugin_host.rs b/crates/coop-worker/src/plugin_host.rs index 38e2e3a..340be2b 100644 --- a/crates/coop-worker/src/plugin_host.rs +++ b/crates/coop-worker/src/plugin_host.rs @@ -316,8 +316,9 @@ impl LoadedPlugin { /// wake primitive until I/O or the next timer is ready. There is no fixed /// polling quantum on the request path. fn await_promise(&mut self, promise_value: f64) -> Result { - // Extract the raw Promise pointer from the NaN-boxed value. - let promise_ptr = { + // Validate the shape up front; the pointer itself is re-derived from + // the root on every loop turn (see below). + { let bits = promise_value.to_bits(); if (bits & !POINTER_MASK) != POINTER_TAG { return Err(anyhow!( @@ -325,16 +326,41 @@ impl LoadedPlugin { bits )); } - (bits & POINTER_MASK) as usize as *mut u8 - }; + } let api = crate::runtime_libraries::runtime_api(); + + // ROOT the Promise for the whole await. `perry_poll` runs JS, which + // reaches GC safepoints, and an evacuating minor MOVES the Promise -- + // after which the cached pointer is a stale from-space address and + // `js_promise_state` reads recycled memory. + // + // PERRY_GC_PROTECT_FROMSPACE faults precisely here: + // + // [gc-fromspace-protect] FAULT: signal 10 + // This address is RETIRED FROM-SPACE. The evacuating minor moved or + // freed the object here and the holder kept the pre-collection address. + // last-known object: obj_type=5 size=72 (5 = GC_TYPE_PROMISE) + // + // Re-deriving the pointer from `promise_value` each turn would NOT + // help: the NaN-box holds the same pre-collection address. Only a root + // the collector rewrites is stable, so re-read the slot every turn. + let root_base = unsafe { (api.js_ffi_root_scope_enter)() }; + let root_slot = unsafe { (api.js_ffi_root_push_nanbox)(promise_value.to_bits()) }; + let _root_scope = FfiRootScope { base: root_base }; + let start = Instant::now(); loop { unsafe { let _ = (api.perry_poll)(); } + // Re-read through the root: the collector rewrote it if it moved. + let promise_ptr = { + let bits = unsafe { (api.js_ffi_root_get_nanbox)(root_slot) }; + (bits & POINTER_MASK) as usize as *mut u8 + }; + let state = unsafe { (api.js_promise_state)(promise_ptr) }; match state { 1 => { @@ -656,6 +682,18 @@ fn make_perry_buffer(bytes: &[u8]) -> Result { Ok(value) } +/// Pops Perry's FFI root scope on every exit path, including the `?` and +/// timeout returns out of the await loop. +struct FfiRootScope { + base: usize, +} + +impl Drop for FfiRootScope { + fn drop(&mut self) { + unsafe { (crate::runtime_libraries::runtime_api().js_ffi_root_scope_exit)(self.base) }; + } +} + /// NaN-box tags for the two string representations. See Perry's /// `crates/perry-runtime/src/value/nanbox.rs`. const STRING_TAG: u64 = 0x7fff; diff --git a/crates/coop-worker/src/runtime_libraries.rs b/crates/coop-worker/src/runtime_libraries.rs index 4e2d1f5..ade1db1 100644 --- a/crates/coop-worker/src/runtime_libraries.rs +++ b/crates/coop-worker/src/runtime_libraries.rs @@ -15,6 +15,13 @@ use std::time::Instant; pub(crate) type JsGcInit = unsafe extern "C" fn(); pub(crate) type JsPromiseState = unsafe extern "C" fn(*mut u8) -> i32; pub(crate) type JsPromiseValue = unsafe extern "C" fn(*mut u8) -> f64; +/// Perry's FFI root scope. A host that holds a JS value across anything that +/// can run JS must root it: an evacuating minor MOVES the object, and a cached +/// raw address becomes a stale from-space pointer. +pub(crate) type JsFfiRootScopeEnter = unsafe extern "C" fn() -> usize; +pub(crate) type JsFfiRootScopeExit = unsafe extern "C" fn(usize); +pub(crate) type JsFfiRootPushNanbox = unsafe extern "C" fn(u64) -> usize; +pub(crate) type JsFfiRootGetNanbox = unsafe extern "C" fn(usize) -> u64; /// `js_jsvalue_to_string(value) -> *mut StringHeader`: stringify ANY JS value, /// so a non-string rejection (an `Error` object) can still be reported. pub(crate) type JsValueToString = unsafe extern "C" fn(f64) -> *const u8; @@ -40,6 +47,10 @@ pub(crate) struct RuntimeApi { pub js_promise_state: JsPromiseState, pub js_promise_value: JsPromiseValue, pub js_promise_reason: JsPromiseValue, + pub js_ffi_root_scope_enter: JsFfiRootScopeEnter, + pub js_ffi_root_scope_exit: JsFfiRootScopeExit, + pub js_ffi_root_push_nanbox: JsFfiRootPushNanbox, + pub js_ffi_root_get_nanbox: JsFfiRootGetNanbox, pub js_jsvalue_to_string: JsValueToString, pub perry_poll: PerryPoll, pub js_wait_for_event: JsWaitForEvent, @@ -185,6 +196,10 @@ pub fn initialize_runtime_libraries_with_verification( js_promise_state: load_symbol(runtime_handle, "js_promise_state")?, js_promise_value: load_symbol(runtime_handle, "js_promise_value")?, js_promise_reason: load_symbol(runtime_handle, "js_promise_reason")?, + js_ffi_root_scope_enter: load_symbol(runtime_handle, "js_ffi_root_scope_enter")?, + js_ffi_root_scope_exit: load_symbol(runtime_handle, "js_ffi_root_scope_exit")?, + js_ffi_root_push_nanbox: load_symbol(runtime_handle, "js_ffi_root_push_nanbox")?, + js_ffi_root_get_nanbox: load_symbol(runtime_handle, "js_ffi_root_get_nanbox")?, js_jsvalue_to_string: load_symbol(runtime_handle, "js_jsvalue_to_string")?, perry_poll: load_symbol(runtime_handle, "perry_poll")?, js_wait_for_event: load_symbol(runtime_handle, "js_wait_for_event")?,