179 lines
7.1 KiB
C
179 lines
7.1 KiB
C
// Regression test for oven-sh/bun#30286.
|
|||
|
|
//
|
||
|
|
// Previously, if a NAPI finalizer left a pending JSC-VM exception behind
|
||
|
|
// (which happens in the wild when a finalizer calls a napi function
|
||
|
|
// that throws -- tree-sitter's FinalizeNode path is one example), the
|
||
|
|
// NEXT finalizer in the LIFO chain that called napi_create_error would
|
||
|
|
// fail with napi_pending_exception. When the caller was node-addon-api's
|
||
|
|
// Napi::Error::New(env) helper, that failure status fed
|
||
|
|
// NAPI_FATAL_IF_FAILED -> napi_fatal_error -> the reporter's panic:
|
||
|
|
// "NAPI FATAL ERROR: Error::New napi_create_error"
|
||
|
|
//
|
||
|
|
// Root cause (two layers):
|
||
|
|
// - NapiEnv::cleanup() did not clear pending exceptions between
|
||
|
|
// finalizers. A throw from one finalizer bled into the next -- even
|
||
|
|
// though finalizers run without a JS frame that could catch, so
|
||
|
|
// there is no point propagating.
|
||
|
|
// - Separately, createErrorWithNapiValues had its own
|
||
|
|
// DECLARE_THROW_SCOPE + RETURN_IF_EXCEPTION at entry that returned
|
||
|
|
// napi_pending_exception whenever a VM exception was live. Node.js
|
||
|
|
// makes napi_create_error a pure value-producing call, so this was
|
||
|
|
// a compatibility bug on its own (#22259).
|
||
|
|
//
|
||
|
|
// Reproduces the sequence without tree-sitter:
|
||
|
|
// - Wrap two JS objects. Finalizers run LIFO during env teardown.
|
||
|
|
// - The one that runs FIRST calls a JS function via napi_call_function
|
||
|
|
// that throws; the throw reaches the VM throw scope and we return
|
||
|
|
// without clearing it.
|
||
|
|
// - The one that runs SECOND calls napi_create_error.
|
||
|
|
// * Before the fix: returns napi_pending_exception (10).
|
||
|
|
// * After the fix: returns napi_ok (0).
|
||
|
|
//
|
||
|
|
// The test driver spawns bun, waits for it to exit, and asserts the
|
||
|
|
// finalizer printed "create_error_status=0".
|
||
|
|
|
||
|
|
#include <node_api.h>
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <stdlib.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
// Strong ref (refcount 1) so the throwing function the finalizer calls
|
||
|
|
// survives until env cleanup.
|
||
|
|
static napi_ref throw_fn_ref = NULL;
|
||
|
|
static int create_error_status = -999;
|
||
|
|
|
||
|
|
// Runs SECOND during LIFO cleanup (wrapped FIRST). At this point the
|
||
|
|
// other finalizer has already left an Error on the JSC VM scope.
|
||
|
|
static void finalize_create_error(napi_env env, void *data, void *hint) {
|
||
|
|
(void)data;
|
||
|
|
(void)hint;
|
||
|
|
|
||
|
|
napi_value msg;
|
||
|
|
napi_status s = napi_create_string_utf8(env, "finalizer-error", NAPI_AUTO_LENGTH, &msg);
|
||
|
|
if (s != napi_ok) {
|
||
|
|
create_error_status = -(100 + s);
|
||
|
|
printf("create_error_status=%d\n", create_error_status);
|
||
|
|
fflush(stdout);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
napi_value err;
|
||
|
|
// Before the fix: returns napi_pending_exception because
|
||
|
|
// createErrorWithNapiValues's throw-scope check saw the prior
|
||
|
|
// finalizer's VM exception.
|
||
|
|
// After the fix: returns napi_ok.
|
||
|
|
create_error_status = napi_create_error(env, NULL, msg, &err);
|
||
|
|
|
||
|
|
// Emit directly from the finalizer -- cleanup hooks run BEFORE
|
||
|
|
// finalizers, so we can't rely on a post-finalize hook.
|
||
|
|
printf("create_error_status=%d\n", create_error_status);
|
||
|
|
fflush(stdout);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Runs FIRST during LIFO cleanup (wrapped SECOND). Calls a throwing JS
|
||
|
|
// function via napi_call_function. The throw propagates to the VM scope;
|
||
|
|
// we return without clearing it, leaving the exception pending for the
|
||
|
|
// next finalizer.
|
||
|
|
static void finalize_leak_exception(napi_env env, void *data, void *hint) {
|
||
|
|
(void)data;
|
||
|
|
(void)hint;
|
||
|
|
|
||
|
|
if (!throw_fn_ref) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
napi_value fn;
|
||
|
|
napi_status s = napi_get_reference_value(env, throw_fn_ref, &fn);
|
||
|
|
if (s != napi_ok || !fn) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
napi_value undef;
|
||
|
|
napi_get_undefined(env, &undef);
|
||
|
|
|
||
|
|
// Invoke the JS function; it throws. napi_call_function returns
|
||
|
|
// napi_pending_exception without clearing the VM scope, so the
|
||
|
|
// exception stays live for the next finalizer's entry.
|
||
|
|
(void)napi_call_function(env, undef, fn, 0, NULL, NULL);
|
||
|
|
}
|
||
|
|
|
||
|
|
// setup(fnThatThrows: () => never): { outer: object, inner: object }
|
||
|
|
// fnThatThrows must be a JS function that throws when called. We hold
|
||
|
|
// a strong ref to it so it survives to cleanup, then wrap two fresh
|
||
|
|
// objects with the two finalizers above.
|
||
|
|
static napi_value setup(napi_env env, napi_callback_info info) {
|
||
|
|
size_t argc = 1;
|
||
|
|
napi_value args[1];
|
||
|
|
napi_get_cb_info(env, info, &argc, args, NULL, NULL);
|
||
|
|
if (argc < 1) {
|
||
|
|
napi_throw_error(env, NULL, "setup needs throwing fn");
|
||
|
|
return NULL;
|
||
|
|
}
|
||
|
|
|
||
|
|
napi_create_reference(env, args[0], 1, &throw_fn_ref);
|
||
|
|
|
||
|
|
// Outer is wrapped FIRST -> appears earlier in the finalizer list ->
|
||
|
|
// cleanup iterates in reverse (LIFO) -> its finalizer runs SECOND,
|
||
|
|
// after the exception has been leaked.
|
||
|
|
napi_value outer;
|
||
|
|
napi_create_object(env, &outer);
|
||
|
|
napi_wrap(env, outer, NULL, finalize_create_error, NULL, NULL);
|
||
|
|
|
||
|
|
// Inner is wrapped SECOND -> its finalizer runs FIRST and leaks the
|
||
|
|
// pending VM exception the next finalizer will inherit.
|
||
|
|
napi_value inner;
|
||
|
|
napi_create_object(env, &inner);
|
||
|
|
napi_wrap(env, inner, NULL, finalize_leak_exception, NULL, NULL);
|
||
|
|
|
||
|
|
napi_value result;
|
||
|
|
napi_create_object(env, &result);
|
||
|
|
napi_set_named_property(env, result, "outer", outer);
|
||
|
|
napi_set_named_property(env, result, "inner", inner);
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Env cleanup hook that leaves a pending JSC VM exception behind:
|
||
|
|
// napi_throw_error schedules an env-level exception (always allowed),
|
||
|
|
// and napi_call_function's prologue (env->throwPendingException())
|
||
|
|
// promotes it to a VM-level exception before validating any argument,
|
||
|
|
// then returns napi_pending_exception with the exception still set.
|
||
|
|
// This is the same state node-canvas reaches on Worker.terminate():
|
||
|
|
// a finalizer/hook fails internally, the scheduled exception gets
|
||
|
|
// promoted to the VM, and it is still pending when NapiEnv::cleanup()
|
||
|
|
// reaches the first wrap finalizer. Without the clearing between the
|
||
|
|
// hook phase and the finalizer phase, that first finalizer's first
|
||
|
|
// napi call fails with napi_pending_exception and node-addon-api
|
||
|
|
// escalates to napi_fatal_error ("Error::Error napi_create_object").
|
||
|
|
static void leak_exception_cleanup_hook(void *arg) {
|
||
|
|
napi_env env = (napi_env)arg;
|
||
|
|
napi_throw_error(env, NULL, "leaked from cleanup hook");
|
||
|
|
// Promotes the scheduled exception onto the VM and leaves it there.
|
||
|
|
(void)napi_call_function(env, NULL, NULL, 0, NULL, NULL);
|
||
|
|
}
|
||
|
|
|
||
|
|
// setupSingle(): object
|
||
|
|
// Registers the exception-leaking cleanup hook and wraps ONE object
|
||
|
|
// whose finalizer immediately calls napi_create_string_utf8 +
|
||
|
|
// napi_create_error and prints the resulting status. Hooks run before
|
||
|
|
// wrap finalizers, so the finalizer only succeeds if cleanup clears
|
||
|
|
// pending exceptions before entering the finalizer phase.
|
||
|
|
static napi_value setup_single(napi_env env, napi_callback_info info) {
|
||
|
|
(void)info;
|
||
|
|
|
||
|
|
napi_add_env_cleanup_hook(env, leak_exception_cleanup_hook, env);
|
||
|
|
|
||
|
|
napi_value obj;
|
||
|
|
napi_create_object(env, &obj);
|
||
|
|
napi_wrap(env, obj, NULL, finalize_create_error, NULL, NULL);
|
||
|
|
return obj;
|
||
|
|
}
|
||
|
|
|
||
|
|
NAPI_MODULE_INIT(/* napi_env env, napi_value exports */) {
|
||
|
|
napi_property_descriptor props[] = {
|
||
|
|
{"setup", NULL, setup, NULL, NULL, NULL, napi_default, NULL},
|
||
|
|
{"setupSingle", NULL, setup_single, NULL, NULL, NULL, napi_default, NULL},
|
||
|
|
};
|
||
|
|
napi_define_properties(env, exports, sizeof(props) / sizeof(props[0]), props);
|
||
|
|
return exports;
|
||
|
|
}
|