diff --git a/napi/src/promise.rs b/napi/src/promise.rs index 2ca5c7a4d5..4ca3bc0448 100644 --- a/napi/src/promise.rs +++ b/napi/src/promise.rs @@ -1,6 +1,8 @@ use std::future::Future; use std::os::raw::{c_char, c_void}; use std::ptr; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use crate::{check_status, sys, Env, JsError, NapiValue, Result}; @@ -10,10 +12,27 @@ pub struct FuturePromise { tsfn: sys::napi_threadsafe_function, async_resource_name: sys::napi_value, resolver: Box Result>, + /// Set to `true` when the N-API environment begins teardown. When this is + /// true, calling `napi_resolve_deferred` / `napi_reject_deferred` can crash + /// in Node.js >= 22 because the V8 `Persistent` backing the `Deferred` may + /// already be invalidated (SIGSEGV in `GlobalHandles::Destroy`). + env_tearing_down: Arc, + /// Raw pointer to the cleanup hook data, needed to unregister the hook on + /// normal completion. Null if the hook has already been unregistered. + cleanup_hook_data: *mut c_void, } unsafe impl Send for FuturePromise {} +struct CleanupHookData { + flag: Arc, +} + +unsafe extern "C" fn env_teardown_cleanup(data: *mut c_void) { + let hook_data = Box::from_raw(data as *mut CleanupHookData); + hook_data.flag.store(true, Ordering::Release); +} + impl FuturePromise { #[inline] pub fn create( @@ -32,12 +51,22 @@ impl FuturePromise { ) })?; + let env_tearing_down = Arc::new(AtomicBool::new(false)); + let hook_data = Box::into_raw(Box::new(CleanupHookData { + flag: Arc::clone(&env_tearing_down), + })); + unsafe { + sys::napi_add_env_cleanup_hook(env, Some(env_teardown_cleanup), hook_data as *mut c_void); + } + Ok(FuturePromise { deferred: raw_deferred, resolver, env, tsfn: ptr::null_mut(), async_resource_name, + env_tearing_down, + cleanup_hook_data: hook_data as *mut c_void, }) } @@ -100,8 +129,31 @@ unsafe extern "C" fn call_js_cb( context: *mut c_void, data: *mut c_void, ) { - let mut env = Env::from_raw(raw_env); let future_promise = Box::from_raw(context as *mut FuturePromise); + let env_tearing_down = future_promise.env_tearing_down.load(Ordering::Acquire); + let cleanup_hook_data = future_promise.cleanup_hook_data; + + if env_tearing_down { + // The environment is tearing down. Calling `napi_resolve_deferred` / + // `napi_reject_deferred` would crash inside V8's `GlobalHandles::Destroy` + // because the `Persistent` backing the `Deferred` may already be invalidated. + // + // Drop the future_promise and value to free Rust-side state, but skip every + // N-API cleanup call that would touch the partially-torn-down environment. + // The cleanup hook data is freed by the hook itself during teardown. + let value: Result = ptr::read(data as *const _); + drop(value); + drop(future_promise); + return; + } + + // Normal completion: unregister the cleanup hook since we no longer need it. + if !cleanup_hook_data.is_null() { + sys::napi_remove_env_cleanup_hook(raw_env, Some(env_teardown_cleanup), cleanup_hook_data); + drop(Box::from_raw(cleanup_hook_data as *mut CleanupHookData)); + } + + let mut env = Env::from_raw(raw_env); let value: Result = ptr::read(data as *const _); let resolver = future_promise.resolver; let deferred = future_promise.deferred;