Skip to content

fix: guard FuturePromise resolve against env teardown crash#2

Open
cam-from-ditto wants to merge 1 commit intoditto/closure-into-jsfunctionfrom
cam/DEVX-852/fix-tsfn-promise-teardown
Open

fix: guard FuturePromise resolve against env teardown crash#2
cam-from-ditto wants to merge 1 commit intoditto/closure-into-jsfunctionfrom
cam/DEVX-852/fix-tsfn-promise-teardown

Conversation

@cam-from-ditto
Copy link
Copy Markdown

Summary

  • Mirrors #1 (the napi_delete_async_work env-teardown fix) but for the TSFN-backed FuturePromise used by Env::execute_tokio_future.
  • Registers a napi_add_env_cleanup_hook per FuturePromise to detect when the N-API environment begins tearing down. When set, the TSFN's call_js_cb skips napi_resolve_deferred / napi_reject_deferred to avoid crashing inside V8's GlobalHandles::Destroy.
  • The cleanup hook is unregistered on normal completion so it doesn't leak.

Root cause

In Node.js >= 22, the Persistent<Promise::Resolver> backing the Deferred can be invalidated during environment teardown before the TSFN's queued AsyncCb fires. When the AsyncCb then calls napi_resolve_deferred, V8's GlobalHandles::Destroy segfaults on the dangling handle.

This is the trace that DEVX-852 was originally chasing:

#0  v8::internal::GlobalHandles::Destroy(unsigned long*)         ← SIGSEGV
#1  napi_resolve_deferred()
#2  call_js_cb<...>()                napi-rs/promise.rs:111
#3  v8impl::ThreadSafeFunction::AsyncCb(uv_async_s*)
#4  uv__async_io
...

Frame 2 is the function this PR guards.

Validation

CI run 25482278460 on getditto/ditto's cam/DEVX-852/flaky-node-v24-sigsegv branch (which has both this fix and the previously-merged napi_delete_async_work fix) no longer hits the original signature. A different unrelated crash now surfaces (node::fs::FSReqPromise::~FSReqPromise from Node's own fs internals), confirming this PR closes its target trace.

Test plan

  • Sanity-check CI on the consumer (getditto/ditto) once this is merged and Cargo.lock is bumped.

In Node.js >= 22, calling napi_resolve_deferred / napi_reject_deferred
inside the threadsafe-function callback used by execute_tokio_future
crashes inside V8's GlobalHandles::Destroy when the environment is
tearing down — the Persistent backing the Deferred has already been
invalidated by the time the AsyncCb fires.

Mirror the napi_delete_async_work fix: register a
napi_add_env_cleanup_hook per FuturePromise to detect when the
environment begins tearing down, and skip the deferred resolve/reject
calls in that case. The hook is unregistered on normal completion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant