Skip to content

gc from napi_call_threadsafe_function with pending exception crashes #67421

Open

Description

@laverdet

Version

v26.13.1

Platform

Linux 12cc91c12230 7.0.14-linuxkit #1 SMP PREEMPT Sun Sep 20 16:14:09 UTC 2026 aarch64 GNU/Linux

Subsystem

napi

What steps will reproduce the bug?

addon.c

#define NAPI_EXPERIMENTAL
#define NODE_API_EXPERIMENTAL_NO_WARNING
#include <node_api.h>
#undef NDEBUG
#include <assert.h>

static int dummy;

// Does nothing at all. It never touches the environment.
static void finalize(node_api_basic_env /*env*/, void* /*data*/, void* /*hint*/) {}

// wrap(object): attach the no-op finalizer to `object`
static napi_value wrap(napi_env env, napi_callback_info info) {
  size_t argc = 1;
  napi_value object;
  assert(napi_get_cb_info(env, info, &argc, &object, NULL, NULL) == napi_ok);
  assert(napi_wrap(env, object, &dummy, finalize, NULL, NULL) == napi_ok);
  return NULL;
}

// Throws. The documentation says this is reported through `uncaughtException`.
static void call_js(napi_env env, napi_value /*js_callback*/, void* /*context*/, void* /*data*/) {
  assert(napi_throw_error(env, NULL, "thrown from call_js_cb") == napi_ok);
}

// start(): invoke `call_js` once through a threadsafe function
static napi_value start(napi_env env, napi_callback_info /*info*/) {
  napi_value name;
  assert(napi_create_string_utf8(env, "repro", NAPI_AUTO_LENGTH, &name) == napi_ok);
  napi_threadsafe_function tsfn;
  assert(napi_create_threadsafe_function(env, NULL, NULL, name, 0, 1, NULL, NULL, NULL, call_js, &tsfn) == napi_ok);
  assert(napi_call_threadsafe_function(tsfn, NULL, napi_tsfn_blocking) == napi_ok);
  assert(napi_release_threadsafe_function(tsfn, napi_tsfn_release) == napi_ok);
  return NULL;
}

NAPI_MODULE_INIT() {
  napi_property_descriptor properties[] = {
      {"wrap", NULL, wrap, NULL, NULL, NULL, napi_default, NULL},
      {"start", NULL, start, NULL, NULL, NULL, napi_default, NULL},
  };
  assert(napi_define_properties(env, exports, 2, properties) == napi_ok);
  return exports;
}

test.cjs

// Run with: node --expose-gc test.cjs
const addon = require("./build/Release/addon.node");

process.on("uncaughtException", error => {
	console.log("uncaughtException:", error.message);
	gc();
	console.log("handler finished");
});

// Wrapped objects with a no-op finalizer, immediately unreachable
for (let ii = 0; ii < 100; ++ii) {
	addon.wrap({});
}

addon.start();

CMakeLists.txt

cmake_minimum_required(VERSION 3.21)
project(addon LANGUAGES C)

add_library(addon SHARED addon.c ${CMAKE_JS_SRC})
set_target_properties(addon PROPERTIES PREFIX "" SUFFIX ".node" C_STANDARD 23)
target_include_directories(addon PRIVATE ${CMAKE_JS_INC})
target_link_libraries(addon PRIVATE ${CMAKE_JS_LIB})

$ cmake-js build
// [...]

$ ~/node-v26.13.1-linux-arm64/bin/node --expose-gc test.cjs
uncaughtException: thrown from call_js_cb
FATAL ERROR: Re-entrant uncaught exception detected.
The exception handler threw an error while processing a
previous uncaught exception, likely due to an inspector
protocol issue. Aborting to prevent infinite recursion.
/workspace/napi-stale-exception/test.cjs:6
  gc();
  ^

Error: thrown from call_js_cb

----- Native stack trace -----

 1: 0x840124  [/root/node-v26.13.1-linux-arm64/bin/node]
 2: 0x9cd0f8 node::errors::TriggerUncaughtException(v8::Isolate*, v8::Local<v8::Value>, v8::Local<v8::Message>, bool) [/root/node-v26.13.1-linux-arm64/bin/node]
 3: 0x97bf38 void napi_env__::CallIntoModule<node_napi_env__::CallFinalizer<true>(void (*)(napi_env__*, void*, void*), void*, void*)::{lambda(napi_env__*)#1}&, node_napi_env__::CallbackIntoModule<true, node_napi_env__::CallFinalizer<true>(void (*)(napi_env__*, void*, void*), void*, void*)::{lambda(napi_env__*)#1}>(node_napi_env__::CallFinalizer<true>(void (*)(napi_env__*, void*, void*), void*, void*)::{lambda(napi_env__*)#1}&&)::{lambda(napi_env__*, v8::Local<v8::Value>)#1}>(node_napi_env__::CallFinalizer<true>(void (*)(napi_env__*, void*, void*), void*, void*)::{lambda(napi_env__*)#1}&, node_napi_env__::CallFinalizer<true>(void (*)(napi_env__*, void*, void*), void*, void*)::{lambda(napi_env__*)#1}&&) [/root/node-v26.13.1-linux-arm64/bin/node]
 4: 0x97aeb4 void node_napi_env__::CallFinalizer<true>(void (*)(napi_env__*, void*, void*), void*, void*) [/root/node-v26.13.1-linux-arm64/bin/node]
 5: 0x9544ec v8impl::Reference::Finalize() [/root/node-v26.13.1-linux-arm64/bin/node]
 6: 0x9548c8 v8impl::ReferenceWithFinalizer::InvokeFinalizerFromGC() [/root/node-v26.13.1-linux-arm64/bin/node]
 7: 0xe76f50  [/root/node-v26.13.1-linux-arm64/bin/node]
 8: 0xf0509c  [/root/node-v26.13.1-linux-arm64/bin/node]
 9: 0xf15108  [/root/node-v26.13.1-linux-arm64/bin/node]
10: 0xf14d6c  [/root/node-v26.13.1-linux-arm64/bin/node]
11: 0x19820b4  [/root/node-v26.13.1-linux-arm64/bin/node]
12: 0xf01598  [/root/node-v26.13.1-linux-arm64/bin/node]
13: 0xe59ce0  [/root/node-v26.13.1-linux-arm64/bin/node]
14: 0xe58f20  [/root/node-v26.13.1-linux-arm64/bin/node]
15: 0x19df34c  [/root/node-v26.13.1-linux-arm64/bin/node]

----- JavaScript stack trace -----

1: /workspace/napi-stale-exception/test.cjs:6:2
2: emit (node:events:514:20)
3: node:internal/process/execution:192:32


Aborted                    ~/node-v26.13.1-linux-arm64/bin/node --expose-gc test.cjs

$ bun --expose-gc test.cjs
uncaughtException: thrown from call_js_cb
handler finished

How often does it reproduce? Is there a required condition?

It reproduces reliably

What is the expected behavior? Why is that the expected behavior?

It should not crash

What do you see instead?

It crashes

Additional information

Works on v18.18.2
Crashes on v18.19.0
Works on v20.9.0
Crashes on v20.10.0 onward

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions