-
Notifications
You must be signed in to change notification settings - Fork 4.7k
napi: clear pending exceptions between finalizers during env cleanup #30291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
robobun
wants to merge
15
commits into
main
Choose a base branch
from
farm/61ff913a/fix-napi-create-error-during-finalizer-cleanup
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
5ca6b15
napi: clear pending exceptions between finalizers during env cleanup
robobun a2d6053
test: drop stderr-panic checks in favor of positive assertions
robobun 0229b3b
retry CI: buildkite agent expiration wave (build 51854)
robobun 27e34f3
napi: move clearExceptionsBetweenFinalizers out of the header
robobun 40351ce
retry CI: darwin agent fleet expiration (build 51894)
robobun ce118aa
retry CI: 1-shard asan flake (build 51921)
robobun 8fa4c6c
retry CI: shard-level flake on debian-13-x64-asan (build 51937)
robobun 9027c86
retry CI: one-shard asan flake (build 51947)
robobun 1288d1e
retry CI: darwin aarch64 agent expiration wave (build 51956)
robobun 720f084
retry CI: darwin-aarch64 link step exit 128 (build 52007)
robobun 7dcc2f1
napi: clear pending exceptions before the finalizer phase of cleanup too
robobun 48fcf3f
napi: soften getString comment to note the rope-OOM caveat is intenti…
robobun 1ded3d6
revert stray configVersion churn in napi-app bun.lock
robobun 97fee1c
napi: clear leaked exceptions between cleanup hooks too
robobun 4612807
Merge branch 'main' into farm/61ff913a/fix-napi-create-error-during-f…
robobun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| // 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; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.