Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions crates/coop-worker/src/plugin_host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,25 +316,51 @@ 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<f64> {
// 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!(
"handler returned non-pointer non-string value: 0x{:016x}",
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 => {
Expand Down Expand Up @@ -656,6 +682,18 @@ fn make_perry_buffer(bytes: &[u8]) -> Result<f64> {
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;
Expand Down
15 changes: 15 additions & 0 deletions crates/coop-worker/src/runtime_libraries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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")?,
Expand Down
Loading