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
18 changes: 15 additions & 3 deletions crates/perry-ext-mysql2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,11 @@ unsafe fn run_connection_query(
}
});
match outcome {
Ok(out) => promise.resolve(outcome_to_jsvalue(&out)),
// #1824: build the JS result on the MAIN thread. outcome_to_jsvalue
// allocates arrays/objects/strings, which is UB on this blocking-pool
// thread (worker thread-local arena → dangling on the main thread once
// the pooled thread idles out). `out` is plain Send Rust data.
Ok(out) => promise.resolve_with(move || outcome_to_jsvalue(&out)),
Err(e) => promise.reject_string(&e),
}
});
Expand Down Expand Up @@ -749,7 +753,11 @@ unsafe fn run_pool_query(pool_handle: Handle, sql_ptr: *const u8, params_f: f64)
}
});
match outcome {
Ok(out) => promise.resolve(outcome_to_jsvalue(&out)),
// #1824: build the JS result on the MAIN thread. outcome_to_jsvalue
// allocates arrays/objects/strings, which is UB on this blocking-pool
// thread (worker thread-local arena → dangling on the main thread once
// the pooled thread idles out). `out` is plain Send Rust data.
Ok(out) => promise.resolve_with(move || outcome_to_jsvalue(&out)),
Err(e) => promise.reject_string(&e),
}
});
Expand Down Expand Up @@ -867,7 +875,11 @@ unsafe fn run_pool_conn_query(
}
});
match outcome {
Ok(out) => promise.resolve(outcome_to_jsvalue(&out)),
// #1824: build the JS result on the MAIN thread. outcome_to_jsvalue
// allocates arrays/objects/strings, which is UB on this blocking-pool
// thread (worker thread-local arena → dangling on the main thread once
// the pooled thread idles out). `out` is plain Send Rust data.
Ok(out) => promise.resolve_with(move || outcome_to_jsvalue(&out)),
Err(e) => promise.reject_string(&e),
}
});
Expand Down
43 changes: 43 additions & 0 deletions crates/perry-ffi/src/async_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ extern "C" {
fn perry_ffi_promise_new() -> *mut Promise;
fn perry_ffi_promise_resolve_bits(promise: *mut Promise, bits: u64);
fn perry_ffi_promise_reject_bits(promise: *mut Promise, bits: u64);
// Resolve by running `invoke(ctx)` on the MAIN thread (during the
// resolution pump) to produce the result bits. For results that require
// constructing JSValues (objects/arrays/strings) — which is UB on a
// blocking-pool thread. See `JsPromise::resolve_with`.
fn perry_ffi_promise_resolve_deferred(
promise: *mut Promise,
ctx: *mut c_void,
invoke: extern "C" fn(*mut c_void) -> u64,
);
fn perry_ffi_spawn_blocking(ctx: *mut c_void, invoke: extern "C" fn(*mut c_void));
fn perry_ffi_spawn_blocking_with_reactor(ctx: *mut c_void, invoke: extern "C" fn(*mut c_void));
}
Expand Down Expand Up @@ -139,6 +148,40 @@ impl JsPromise {
unsafe { perry_ffi_promise_resolve_bits(self.0, value.bits()) };
}

/// Resolve by building the result JSValue on the **main thread**.
///
/// `spawn_blocking` closures run on a tokio blocking-pool thread, where
/// perry-runtime's thread-local arena makes constructing JSValues
/// (objects / arrays / hand-built strings) undefined behaviour — the
/// objects land in a worker arena that is freed when the pooled thread
/// idles out, leaving the main thread with dangling pointers (issue
/// #1824). Bindings that return complex values (DB row sets, parsed
/// payloads, …) must therefore defer the JS construction: do the Rust
/// work on the worker, then hand a `Send` closure here. It runs once, on
/// the main thread, during the resolution pump.
///
/// `f` must be `Send` (it crosses to the main thread) and `'static`.
pub fn resolve_with<F>(self, f: F)
where
F: FnOnce() -> crate::JsValue + Send + 'static,
{
// Box twice for a thin pointer across the FFI boundary, mirroring
// `spawn_blocking`. The trampoline runs the closure on the main
// thread and returns the NaN-boxed result bits.
let boxed: Box<dyn FnOnce() -> u64 + Send> = Box::new(move || f().bits());
let thin: Box<Box<dyn FnOnce() -> u64 + Send>> = Box::new(boxed);
let ctx = Box::into_raw(thin) as *mut c_void;

extern "C" fn invoke(ctx: *mut c_void) -> u64 {
let thin: Box<Box<dyn FnOnce() -> u64 + Send>> =
unsafe { Box::from_raw(ctx as *mut Box<dyn FnOnce() -> u64 + Send>) };
let f: Box<dyn FnOnce() -> u64 + Send> = *thin;
f()
}

unsafe { perry_ffi_promise_resolve_deferred(self.0, ctx, invoke) };
}

/// Reject with an arbitrary [`crate::JsValue`]. Mirror of
/// [`Self::resolve`].
pub fn reject(self, value: crate::JsValue) {
Expand Down
26 changes: 26 additions & 0 deletions crates/perry-stdlib/src/perry_ffi_async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,32 @@ pub extern "C" fn perry_ffi_promise_reject_bits(promise: *mut perry_runtime::Pro
async_bridge::queue_promise_resolution(promise as usize, false, bits);
}

/// `perry_ffi_promise_resolve_deferred(promise, ctx, invoke)` — resolve the
/// promise by running `invoke(ctx)` on the MAIN thread during the resolution
/// pump (`js_stdlib_process_pending`), using its return value as the result
/// bits. This is the safe path for native bindings that need to *construct*
/// JSValues (objects/arrays/strings) for the result: doing that on a tokio
/// blocking-pool thread allocates from a worker thread-local arena that is
/// freed when the pooled thread idles out, leaving dangling objects on the
/// main thread (issue #1824). The worker keeps the JS-building closure boxed
/// (carrying only `Send` Rust data) and this defers its execution to the main
/// thread via the existing deferred-resolution queue. perry-ffi's
/// `JsPromise::resolve_with` builds the `ctx`/`invoke` pair.
#[no_mangle]
pub extern "C" fn perry_ffi_promise_resolve_deferred(
promise: *mut perry_runtime::Promise,
ctx: *mut std::ffi::c_void,
invoke: extern "C" fn(*mut std::ffi::c_void) -> u64,
) {
// `ctx` is a raw pointer (not Send); carry it as usize into the converter
// closure, which runs on the main thread where `invoke` decodes + runs the
// boxed JS-builder and returns the NaN-boxed result bits.
let ctx_addr = ctx as usize;
async_bridge::queue_deferred_resolution(promise as usize, true, move || {
invoke(ctx_addr as *mut std::ffi::c_void)
});
}

/// `perry_ffi_spawn_blocking(ctx, invoke)` — run `invoke(ctx)` on
/// the global tokio runtime's blocking pool. The caller is expected
/// to box a closure into `ctx` before calling, and write a thin
Expand Down