GeezerVison is showing github.com — node-api: emit uncaught-exception on unhandled tsfn callbacks · nodejs/node@73f852e
File treeExpand file treeCollapse file tree Open diff view settings js-native-api/test_reference
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/cli.md +13Lines changed: 13 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -419,6 +419,18 @@ under this flag. | 419 | 419 |
| 420 | 420 | To allow polyfills to be added, `--require` runs before freezing intrinsics. | 421 | 421 |
| | 422 | +### `--force-node-api-uncaught-exceptions-policy` | | 423 | + | | 424 | +<!-- YAML | | 425 | +added: REPLACEME | | 426 | +--> | | 427 | + | | 428 | +Enforces `uncaughtException` event on Node-API asynchronous callbacks. | | 429 | + | | 430 | +To prevent from an existing add-on from crashing the process, this flag is not | | 431 | +enabled by default. In the future, this flag will be enabled by default to | | 432 | +enforce the correct behavior. | | 433 | + | 422 | 434 | ### `--heapsnapshot-near-heap-limit=max_count` | 423 | 435 |
| 424 | 436 | <!-- YAML | @@ -1576,6 +1588,7 @@ Node.js options that are allowed are: | 1576 | 1588 | * `--experimental-wasm-modules` | 1577 | 1589 | * `--force-context-aware` | 1578 | 1590 | * `--force-fips` | | 1591 | +* `--force-node-api-uncaught-exceptions-policy` | 1579 | 1592 | * `--frozen-intrinsics` | 1580 | 1593 | * `--heapsnapshot-near-heap-limit` | 1581 | 1594 | * `--heapsnapshot-signal` | |
Collapse file Copy file name to clipboardExpand all lines: src/js_native_api_v8.h +13-9Lines changed: 13 additions & 9 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -57,15 +57,7 @@ struct napi_env__ { | 57 | 57 | CHECK_EQ(isolate, context->GetIsolate()); | 58 | 58 | napi_clear_last_error(this); | 59 | 59 | } | 60 | | - virtual ~napi_env__() { | 61 | | - // First we must finalize those references that have `napi_finalizer` | 62 | | - // callbacks. The reason is that addons might store other references which | 63 | | - // they delete during their `napi_finalizer` callbacks. If we deleted such | 64 | | - // references here first, they would be doubly deleted when the | 65 | | - // `napi_finalizer` deleted them subsequently. | 66 | | - v8impl::RefTracker::FinalizeAll(&finalizing_reflist); | 67 | | - v8impl::RefTracker::FinalizeAll(&reflist); | 68 | | - } | | 60 | + virtual ~napi_env__() { FinalizeAll(); } | 69 | 61 | v8::Isolate* const isolate; // Shortcut for context()->GetIsolate() | 70 | 62 | v8impl::Persistent<v8::Context> context_persistent; | 71 | 63 |
| @@ -102,11 +94,23 @@ struct napi_env__ { | 102 | 94 | } | 103 | 95 | } | 104 | 96 |
| | 97 | + // This should be overridden to schedule the finalization to a properiate | | 98 | + // timing, like next tick of the event loop. | 105 | 99 | virtual void CallFinalizer(napi_finalize cb, void* data, void* hint) { | 106 | 100 | v8::HandleScope handle_scope(isolate); | 107 | 101 | CallIntoModule([&](napi_env env) { cb(env, data, hint); }); | 108 | 102 | } | 109 | 103 |
| | 104 | + void FinalizeAll() { | | 105 | + // First we must finalize those references that have `napi_finalizer` | | 106 | + // callbacks. The reason is that addons might store other references which | | 107 | + // they delete during their `napi_finalizer` callbacks. If we deleted such | | 108 | + // references here first, they would be doubly deleted when the | | 109 | + // `napi_finalizer` deleted them subsequently. | | 110 | + v8impl::RefTracker::FinalizeAll(&finalizing_reflist); | | 111 | + v8impl::RefTracker::FinalizeAll(&reflist); | | 112 | + } | | 113 | + | 110 | 114 | v8impl::Persistent<v8::Value> last_exception; | 111 | 115 |
| 112 | 116 | // We store references in two different lists, depending on whether they have | |
Collapse file Copy file name to clipboardExpand all lines: src/node_api.cc +65-36Lines changed: 65 additions & 36 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 9 | 9 | #include "node_buffer.h" | 10 | 10 | #include "node_errors.h" | 11 | 11 | #include "node_internals.h" | | 12 | +#include "node_process.h" | 12 | 13 | #include "node_url.h" | 13 | 14 | #include "threadpoolwork-inl.h" | 14 | 15 | #include "tracing/traced_value.h" | @@ -23,6 +24,11 @@ node_napi_env__::node_napi_env__(v8::Local<v8::Context> context, | 23 | 24 | CHECK_NOT_NULL(node_env()); | 24 | 25 | } | 25 | 26 |
| | 27 | +node_napi_env__::~node_napi_env__() { | | 28 | + destructing = true; | | 29 | + FinalizeAll(); | | 30 | +} | | 31 | + | 26 | 32 | bool node_napi_env__::can_call_into_js() const { | 27 | 33 | return node_env()->can_call_into_js(); | 28 | 34 | } | @@ -35,19 +41,64 @@ v8::Maybe<bool> node_napi_env__::mark_arraybuffer_as_untransferable( | 35 | 41 | } | 36 | 42 |
| 37 | 43 | void node_napi_env__::CallFinalizer(napi_finalize cb, void* data, void* hint) { | | 44 | + CallFinalizer<true>(cb, data, hint); | | 45 | +} | | 46 | + | | 47 | +template <bool enforceUncaughtExceptionPolicy> | | 48 | +void node_napi_env__::CallFinalizer(napi_finalize cb, void* data, void* hint) { | | 49 | + if (destructing) { | | 50 | + // we can not defer finalizers when the environment is being destructed. | | 51 | + v8::HandleScope handle_scope(isolate); | | 52 | + v8::Context::Scope context_scope(context()); | | 53 | + CallbackIntoModule<enforceUncaughtExceptionPolicy>( | | 54 | + [&](napi_env env) { cb(env, data, hint); }); | | 55 | + return; | | 56 | + } | 38 | 57 | // we need to keep the env live until the finalizer has been run | 39 | 58 | // EnvRefHolder provides an exception safe wrapper to Ref and then | 40 | 59 | // Unref once the lambda is freed | 41 | 60 | EnvRefHolder liveEnv(static_cast<napi_env>(this)); | 42 | 61 | node_env()->SetImmediate( | 43 | 62 | [=, liveEnv = std::move(liveEnv)](node::Environment* node_env) { | 44 | | - napi_env env = liveEnv.env(); | | 63 | + node_napi_env__* env = static_cast<node_napi_env__*>(liveEnv.env()); | 45 | 64 | v8::HandleScope handle_scope(env->isolate); | 46 | 65 | v8::Context::Scope context_scope(env->context()); | 47 | | - env->CallIntoModule([&](napi_env env) { cb(env, data, hint); }); | | 66 | + env->CallbackIntoModule<enforceUncaughtExceptionPolicy>( | | 67 | + [&](napi_env env) { cb(env, data, hint); }); | 48 | 68 | }); | 49 | 69 | } | 50 | 70 |
| | 71 | +void node_napi_env__::trigger_fatal_exception(v8::Local<v8::Value> local_err) { | | 72 | + v8::Local<v8::Message> local_msg = | | 73 | + v8::Exception::CreateMessage(isolate, local_err); | | 74 | + node::errors::TriggerUncaughtException(isolate, local_err, local_msg); | | 75 | +} | | 76 | + | | 77 | +// option enforceUncaughtExceptionPolicy is added for not breaking existing | | 78 | +// running n-api add-ons, and should be deprecated in the next major Node.js | | 79 | +// release. | | 80 | +template <bool enforceUncaughtExceptionPolicy, typename T> | | 81 | +void node_napi_env__::CallbackIntoModule(T&& call) { | | 82 | + CallIntoModule(call, [](napi_env env_, v8::Local<v8::Value> local_err) { | | 83 | + node_napi_env__* env = static_cast<node_napi_env__*>(env_); | | 84 | + node::Environment* node_env = env->node_env(); | | 85 | + if (!node_env->options()->force_node_api_uncaught_exceptions_policy && | | 86 | + !enforceUncaughtExceptionPolicy) { | | 87 | + ProcessEmitDeprecationWarning( | | 88 | + node_env, | | 89 | + "Uncaught N-API callback exception detected, please run node " | | 90 | + "with option --force-node-api-uncaught-exceptions-policy=true" | | 91 | + "to handle those exceptions properly.", | | 92 | + "DEP0XXX"); | | 93 | + return; | | 94 | + } | | 95 | + // If there was an unhandled exception in the complete callback, | | 96 | + // report it as a fatal exception. (There is no JavaScript on the | | 97 | + // callstack that can possibly handle it.) | | 98 | + env->trigger_fatal_exception(local_err); | | 99 | + }); | | 100 | +} | | 101 | + | 51 | 102 | namespace v8impl { | 52 | 103 |
| 53 | 104 | namespace { | @@ -60,20 +111,10 @@ class BufferFinalizer : private Finalizer { | 60 | 111 | static_cast<BufferFinalizer*>(hint)}; | 61 | 112 | finalizer->_finalize_data = data; | 62 | 113 |
| 63 | | - node::Environment* node_env = | 64 | | - static_cast<node_napi_env>(finalizer->_env)->node_env(); | 65 | | - node_env->SetImmediate( | 66 | | - [finalizer = std::move(finalizer)](node::Environment* env) { | 67 | | - if (finalizer->_finalize_callback == nullptr) return; | 68 | | - | 69 | | - v8::HandleScope handle_scope(finalizer->_env->isolate); | 70 | | - v8::Context::Scope context_scope(finalizer->_env->context()); | 71 | | - | 72 | | - finalizer->_env->CallIntoModule([&](napi_env env) { | 73 | | - finalizer->_finalize_callback( | 74 | | - env, finalizer->_finalize_data, finalizer->_finalize_hint); | 75 | | - }); | 76 | | - }); | | 114 | + if (finalizer->_finalize_callback == nullptr) return; | | 115 | + finalizer->_env->CallFinalizer(finalizer->_finalize_callback, | | 116 | + finalizer->_finalize_data, | | 117 | + finalizer->_finalize_hint); | 77 | 118 | } | 78 | 119 |
| 79 | 120 | struct Deleter { | @@ -102,13 +143,6 @@ static inline napi_env NewEnv(v8::Local<v8::Context> context, | 102 | 143 | return result; | 103 | 144 | } | 104 | 145 |
| 105 | | -static inline void trigger_fatal_exception(napi_env env, | 106 | | - v8::Local<v8::Value> local_err) { | 107 | | - v8::Local<v8::Message> local_msg = | 108 | | - v8::Exception::CreateMessage(env->isolate, local_err); | 109 | | - node::errors::TriggerUncaughtException(env->isolate, local_err, local_msg); | 110 | | -} | 111 | | - | 112 | 146 | class ThreadSafeFunction : public node::AsyncResource { | 113 | 147 | public: | 114 | 148 | ThreadSafeFunction(v8::Local<v8::Function> func, | @@ -325,7 +359,7 @@ class ThreadSafeFunction : public node::AsyncResource { | 325 | 359 | v8::Local<v8::Function>::New(env->isolate, ref); | 326 | 360 | js_callback = v8impl::JsValueFromV8LocalValue(js_cb); | 327 | 361 | } | 328 | | - env->CallIntoModule( | | 362 | + env->CallbackIntoModule<false>( | 329 | 363 | [&](napi_env env) { call_js_cb(env, js_callback, context, data); }); | 330 | 364 | } | 331 | 365 |
| @@ -336,7 +370,9 @@ class ThreadSafeFunction : public node::AsyncResource { | 336 | 370 | v8::HandleScope scope(env->isolate); | 337 | 371 | if (finalize_cb) { | 338 | 372 | CallbackScope cb_scope(this); | 339 | | - env->CallIntoModule( | | 373 | + // Do not use CallFinalizer since it will defer the invocation, which | | 374 | + // would lead to accessing a deleted ThreadSafeFunction. | | 375 | + env->CallbackIntoModule<false>( | 340 | 376 | [&](napi_env env) { finalize_cb(env, finalize_data, context); }); | 341 | 377 | } | 342 | 378 | EmptyQueueAndDelete(); | @@ -721,7 +757,7 @@ napi_status napi_fatal_exception(napi_env env, napi_value err) { | 721 | 757 | CHECK_ARG(env, err); | 722 | 758 |
| 723 | 759 | v8::Local<v8::Value> local_err = v8impl::V8LocalValueFromJsValue(err); | 724 | | - v8impl::trigger_fatal_exception(env, local_err); | | 760 | + static_cast<node_napi_env>(env)->trigger_fatal_exception(local_err); | 725 | 761 |
| 726 | 762 | return napi_clear_last_error(env); | 727 | 763 | } | @@ -1061,16 +1097,9 @@ class Work : public node::AsyncResource, public node::ThreadPoolWork { | 1061 | 1097 |
| 1062 | 1098 | CallbackScope callback_scope(this); | 1063 | 1099 |
| 1064 | | - _env->CallIntoModule( | 1065 | | - [&](napi_env env) { | 1066 | | - _complete(env, ConvertUVErrorCode(status), _data); | 1067 | | - }, | 1068 | | - [](napi_env env, v8::Local<v8::Value> local_err) { | 1069 | | - // If there was an unhandled exception in the complete callback, | 1070 | | - // report it as a fatal exception. (There is no JavaScript on the | 1071 | | - // callstack that can possibly handle it.) | 1072 | | - v8impl::trigger_fatal_exception(env, local_err); | 1073 | | - }); | | 1100 | + _env->CallbackIntoModule<true>([&](napi_env env) { | | 1101 | + _complete(env, ConvertUVErrorCode(status), _data); | | 1102 | + }); | 1074 | 1103 |
| 1075 | 1104 | // Note: Don't access `work` after this point because it was | 1076 | 1105 | // likely deleted by the complete callback. | |
Collapse file Copy file name to clipboardExpand all lines: src/node_api_internals.h +8Lines changed: 8 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 11 | 11 | struct node_napi_env__ : public napi_env__ { | 12 | 12 | node_napi_env__(v8::Local<v8::Context> context, | 13 | 13 | const std::string& module_filename); | | 14 | + ~node_napi_env__(); | 14 | 15 |
| 15 | 16 | bool can_call_into_js() const override; | 16 | 17 | v8::Maybe<bool> mark_arraybuffer_as_untransferable( | 17 | 18 | v8::Local<v8::ArrayBuffer> ab) const override; | 18 | 19 | void CallFinalizer(napi_finalize cb, void* data, void* hint) override; | | 20 | + template <bool enforceUncaughtExceptionPolicy> | | 21 | + void CallFinalizer(napi_finalize cb, void* data, void* hint); | | 22 | + | | 23 | + void trigger_fatal_exception(v8::Local<v8::Value> local_err); | | 24 | + template <bool enforceUncaughtExceptionPolicy, typename T> | | 25 | + void CallbackIntoModule(T&& call); | 19 | 26 |
| 20 | 27 | inline node::Environment* node_env() const { | 21 | 28 | return node::Environment::GetCurrent(context()); | 22 | 29 | } | 23 | 30 | inline const char* GetFilename() const { return filename.c_str(); } | 24 | 31 |
| 25 | 32 | std::string filename; | | 33 | + bool destructing = false; | 26 | 34 | }; | 27 | 35 |
| 28 | 36 | using node_napi_env = node_napi_env__*; | |
Collapse file Copy file name to clipboardExpand all lines: src/node_options.cc +6Lines changed: 6 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -410,6 +410,12 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { | 410 | 410 | &EnvironmentOptions::force_async_hooks_checks, | 411 | 411 | kAllowedInEnvironment, | 412 | 412 | true); | | 413 | + AddOption( | | 414 | + "--force-node-api-uncaught-exceptions-policy", | | 415 | + "enforces 'uncaughtException' event on Node API asynchronous callbacks", | | 416 | + &EnvironmentOptions::force_node_api_uncaught_exceptions_policy, | | 417 | + kAllowedInEnvironment, | | 418 | + false); | 413 | 419 | AddOption("--addons", | 414 | 420 | "disable loading native addons", | 415 | 421 | &EnvironmentOptions::allow_native_addons, | |
Collapse file Copy file name to clipboardExpand all lines: src/node_options.h +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -116,6 +116,7 @@ class EnvironmentOptions : public Options { | 116 | 116 | bool experimental_repl_await = true; | 117 | 117 | bool experimental_vm_modules = false; | 118 | 118 | bool expose_internals = false; | | 119 | + bool force_node_api_uncaught_exceptions_policy = false; | 119 | 120 | bool frozen_intrinsics = false; | 120 | 121 | int64_t heap_snapshot_near_heap_limit = 0; | 121 | 122 | std::string heap_snapshot_signal; | |
Collapse file Copy file name to clipboard +20Lines changed: 20 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | +// Flags: --expose-gc --force-node-api-uncaught-exceptions-policy | | 3 | + | | 4 | +const common = require('../../common'); | | 5 | +const test_reference = require(`./build/${common.buildType}/test_reference`); | | 6 | +const assert = require('assert'); | | 7 | + | | 8 | +process.on('uncaughtException', common.mustCall((err) => { | | 9 | + assert.throws(() => { throw err; }, /finalizer error/); | | 10 | +})); | | 11 | + | | 12 | +(async function() { | | 13 | + { | | 14 | + test_reference.createExternalWithJsFinalize( | | 15 | + common.mustCall(() => { | | 16 | + throw new Error('finalizer error'); | | 17 | + })); | | 18 | + } | | 19 | + global.gc(); | | 20 | +})().then(common.mustCall()); |
Collapse file Copy file name to clipboardExpand all lines: test/js-native-api/test_reference/test_reference.c +42-1Lines changed: 42 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -21,6 +21,20 @@ static void FinalizeExternal(napi_env env, void* data, void* hint) { | 21 | 21 | finalize_count++; | 22 | 22 | } | 23 | 23 |
| | 24 | +static void FinalizeExternalCallJs(napi_env env, void* data, void* hint) { | | 25 | + int *actual_value = data; | | 26 | + NODE_API_ASSERT_RETURN_VOID(env, actual_value == &test_value, | | 27 | + "The correct pointer was passed to the finalizer"); | | 28 | + | | 29 | + napi_ref finalizer_ref = (napi_ref)hint; | | 30 | + napi_value js_finalizer; | | 31 | + napi_value recv; | | 32 | + NODE_API_CALL_RETURN_VOID(env, napi_get_reference_value(env, finalizer_ref, &js_finalizer)); | | 33 | + NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &recv)); | | 34 | + NODE_API_CALL_RETURN_VOID(env, napi_call_function(env, recv, js_finalizer, 0, NULL, NULL)); | | 35 | + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, finalizer_ref)); | | 36 | +} | | 37 | + | 24 | 38 | static napi_value CreateExternal(napi_env env, napi_callback_info info) { | 25 | 39 | int* data = &test_value; | 26 | 40 |
| @@ -99,6 +113,31 @@ CreateExternalWithFinalize(napi_env env, napi_callback_info info) { | 99 | 113 | return result; | 100 | 114 | } | 101 | 115 |
| | 116 | +static napi_value | | 117 | +CreateExternalWithJsFinalize(napi_env env, napi_callback_info info) { | | 118 | + size_t argc = 1; | | 119 | + napi_value args[1]; | | 120 | + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); | | 121 | + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); | | 122 | + napi_value finalizer = args[0]; | | 123 | + napi_valuetype finalizer_valuetype; | | 124 | + NODE_API_CALL(env, napi_typeof(env, finalizer, &finalizer_valuetype)); | | 125 | + NODE_API_ASSERT(env, finalizer_valuetype == napi_function, "Wrong type of first argument"); | | 126 | + napi_ref finalizer_ref; | | 127 | + NODE_API_CALL(env, napi_create_reference(env, finalizer, 1, &finalizer_ref)); | | 128 | + | | 129 | + napi_value result; | | 130 | + NODE_API_CALL(env, | | 131 | + napi_create_external(env, | | 132 | + &test_value, | | 133 | + FinalizeExternalCallJs, | | 134 | + finalizer_ref, /* finalize_hint */ | | 135 | + &result)); | | 136 | + | | 137 | + finalize_count = 0; | | 138 | + return result; | | 139 | +} | | 140 | + | 102 | 141 | static napi_value CheckExternal(napi_env env, napi_callback_info info) { | 103 | 142 | size_t argc = 1; | 104 | 143 | napi_value arg; | @@ -223,6 +262,8 @@ napi_value Init(napi_env env, napi_value exports) { | 223 | 262 | DECLARE_NODE_API_PROPERTY("createExternal", CreateExternal), | 224 | 263 | DECLARE_NODE_API_PROPERTY("createExternalWithFinalize", | 225 | 264 | CreateExternalWithFinalize), | | 265 | + DECLARE_NODE_API_PROPERTY("createExternalWithJsFinalize", | | 266 | + CreateExternalWithJsFinalize), | 226 | 267 | DECLARE_NODE_API_PROPERTY("checkExternal", CheckExternal), | 227 | 268 | DECLARE_NODE_API_PROPERTY("createReference", CreateReference), | 228 | 269 | DECLARE_NODE_API_PROPERTY("createSymbol", CreateSymbol), | @@ -234,7 +275,7 @@ napi_value Init(napi_env env, napi_value exports) { | 234 | 275 | DECLARE_NODE_API_PROPERTY("decrementRefcount", DecrementRefcount), | 235 | 276 | DECLARE_NODE_API_GETTER("referenceValue", GetReferenceValue), | 236 | 277 | DECLARE_NODE_API_PROPERTY("validateDeleteBeforeFinalize", | 237 | | - ValidateDeleteBeforeFinalize), | | 278 | + ValidateDeleteBeforeFinalize), | 238 | 279 | }; | 239 | 280 |
| 240 | 281 | NODE_API_CALL(env, napi_define_properties( | |
|