-
Notifications
You must be signed in to change notification settings - Fork 29.6k
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
n-api: emit uncaught-exception on calling into modules #36510
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
#include "node_buffer.h" | ||
#include "node_errors.h" | ||
#include "node_internals.h" | ||
#include "node_process.h" | ||
#include "node_url.h" | ||
#include "threadpoolwork-inl.h" | ||
#include "tracing/traced_value.h" | ||
|
@@ -23,6 +24,11 @@ node_napi_env__::node_napi_env__(v8::Local<v8::Context> context, | |
CHECK_NOT_NULL(node_env()); | ||
} | ||
|
||
node_napi_env__::~node_napi_env__() { | ||
destructing = true; | ||
FinalizeAll(); | ||
} | ||
|
||
bool node_napi_env__::can_call_into_js() const { | ||
return node_env()->can_call_into_js(); | ||
} | ||
|
@@ -35,19 +41,64 @@ v8::Maybe<bool> node_napi_env__::mark_arraybuffer_as_untransferable( | |
} | ||
|
||
void node_napi_env__::CallFinalizer(napi_finalize cb, void* data, void* hint) { | ||
CallFinalizer<true>(cb, data, hint); | ||
} | ||
|
||
template <bool enforceUncaughtExceptionPolicy> | ||
void node_napi_env__::CallFinalizer(napi_finalize cb, void* data, void* hint) { | ||
if (destructing) { | ||
// we can not defer finalizers when the environment is being destructed. | ||
v8::HandleScope handle_scope(isolate); | ||
v8::Context::Scope context_scope(context()); | ||
CallbackIntoModule<enforceUncaughtExceptionPolicy>( | ||
[&](napi_env env) { cb(env, data, hint); }); | ||
return; | ||
} | ||
// we need to keep the env live until the finalizer has been run | ||
// EnvRefHolder provides an exception safe wrapper to Ref and then | ||
// Unref once the lambda is freed | ||
EnvRefHolder liveEnv(static_cast<napi_env>(this)); | ||
node_env()->SetImmediate( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I find that we already have a outer SetImmediate in BufferFinalizer: https://github.com/nodejs/node/blob/master/src/node_api.cc#L65 Anyway, this is not related to the intention of this PR. I'm going to revert this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I merged the setimmediate in bufferfinalizer to this one since they are doing the same thing. |
||
[=, liveEnv = std::move(liveEnv)](node::Environment* node_env) { | ||
napi_env env = liveEnv.env(); | ||
node_napi_env__* env = static_cast<node_napi_env__*>(liveEnv.env()); | ||
v8::HandleScope handle_scope(env->isolate); | ||
v8::Context::Scope context_scope(env->context()); | ||
env->CallIntoModule([&](napi_env env) { cb(env, data, hint); }); | ||
env->CallbackIntoModule<enforceUncaughtExceptionPolicy>( | ||
[&](napi_env env) { cb(env, data, hint); }); | ||
}); | ||
} | ||
|
||
void node_napi_env__::trigger_fatal_exception(v8::Local<v8::Value> local_err) { | ||
v8::Local<v8::Message> local_msg = | ||
v8::Exception::CreateMessage(isolate, local_err); | ||
node::errors::TriggerUncaughtException(isolate, local_err, local_msg); | ||
} | ||
|
||
// option enforceUncaughtExceptionPolicy is added for not breaking existing | ||
// running n-api add-ons, and should be deprecated in the next major Node.js | ||
// release. | ||
template <bool enforceUncaughtExceptionPolicy, typename T> | ||
void node_napi_env__::CallbackIntoModule(T&& call) { | ||
CallIntoModule(call, [](napi_env env_, v8::Local<v8::Value> local_err) { | ||
node_napi_env__* env = static_cast<node_napi_env__*>(env_); | ||
node::Environment* node_env = env->node_env(); | ||
if (!node_env->options()->force_node_api_uncaught_exceptions_policy && | ||
!enforceUncaughtExceptionPolicy) { | ||
ProcessEmitDeprecationWarning( | ||
node_env, | ||
"Uncaught N-API callback exception detected, please run node " | ||
"with option --force-node-api-uncaught-exceptions-policy=true" | ||
"to handle those exceptions properly.", | ||
"DEP0XXX"); | ||
return; | ||
} | ||
// If there was an unhandled exception in the complete callback, | ||
// report it as a fatal exception. (There is no JavaScript on the | ||
// callstack that can possibly handle it.) | ||
env->trigger_fatal_exception(local_err); | ||
}); | ||
} | ||
|
||
namespace v8impl { | ||
|
||
namespace { | ||
|
@@ -60,20 +111,10 @@ class BufferFinalizer : private Finalizer { | |
static_cast<BufferFinalizer*>(hint)}; | ||
finalizer->_finalize_data = data; | ||
|
||
node::Environment* node_env = | ||
static_cast<node_napi_env>(finalizer->_env)->node_env(); | ||
node_env->SetImmediate( | ||
[finalizer = std::move(finalizer)](node::Environment* env) { | ||
if (finalizer->_finalize_callback == nullptr) return; | ||
|
||
v8::HandleScope handle_scope(finalizer->_env->isolate); | ||
v8::Context::Scope context_scope(finalizer->_env->context()); | ||
|
||
finalizer->_env->CallIntoModule([&](napi_env env) { | ||
finalizer->_finalize_callback( | ||
env, finalizer->_finalize_data, finalizer->_finalize_hint); | ||
}); | ||
}); | ||
if (finalizer->_finalize_callback == nullptr) return; | ||
finalizer->_env->CallFinalizer(finalizer->_finalize_callback, | ||
finalizer->_finalize_data, | ||
finalizer->_finalize_hint); | ||
} | ||
|
||
struct Deleter { | ||
|
@@ -102,13 +143,6 @@ static inline napi_env NewEnv(v8::Local<v8::Context> context, | |
return result; | ||
} | ||
|
||
static inline void trigger_fatal_exception(napi_env env, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: It might reduce the diff if you moved trigger_fatal_exception back here. |
||
v8::Local<v8::Value> local_err) { | ||
v8::Local<v8::Message> local_msg = | ||
v8::Exception::CreateMessage(env->isolate, local_err); | ||
node::errors::TriggerUncaughtException(env->isolate, local_err, local_msg); | ||
} | ||
|
||
class ThreadSafeFunction : public node::AsyncResource { | ||
public: | ||
ThreadSafeFunction(v8::Local<v8::Function> func, | ||
|
@@ -325,7 +359,7 @@ class ThreadSafeFunction : public node::AsyncResource { | |
v8::Local<v8::Function>::New(env->isolate, ref); | ||
js_callback = v8impl::JsValueFromV8LocalValue(js_cb); | ||
} | ||
env->CallIntoModule( | ||
env->CallbackIntoModule<false>( | ||
[&](napi_env env) { call_js_cb(env, js_callback, context, data); }); | ||
} | ||
|
||
|
@@ -336,7 +370,9 @@ class ThreadSafeFunction : public node::AsyncResource { | |
v8::HandleScope scope(env->isolate); | ||
if (finalize_cb) { | ||
CallbackScope cb_scope(this); | ||
env->CallIntoModule( | ||
// Do not use CallFinalizer since it will defer the invocation, which | ||
// would lead to accessing a deleted ThreadSafeFunction. | ||
env->CallbackIntoModule<false>( | ||
[&](napi_env env) { finalize_cb(env, finalize_data, context); }); | ||
} | ||
EmptyQueueAndDelete(); | ||
|
@@ -719,7 +755,7 @@ napi_status NAPI_CDECL napi_fatal_exception(napi_env env, napi_value err) { | |
CHECK_ARG(env, err); | ||
|
||
v8::Local<v8::Value> local_err = v8impl::V8LocalValueFromJsValue(err); | ||
v8impl::trigger_fatal_exception(env, local_err); | ||
static_cast<node_napi_env>(env)->trigger_fatal_exception(local_err); | ||
|
||
return napi_clear_last_error(env); | ||
} | ||
|
@@ -1064,16 +1100,9 @@ class Work : public node::AsyncResource, public node::ThreadPoolWork { | |
|
||
CallbackScope callback_scope(this); | ||
|
||
_env->CallIntoModule( | ||
[&](napi_env env) { | ||
_complete(env, ConvertUVErrorCode(status), _data); | ||
}, | ||
[](napi_env env, v8::Local<v8::Value> local_err) { | ||
// If there was an unhandled exception in the complete callback, | ||
// report it as a fatal exception. (There is no JavaScript on the | ||
// callstack that can possibly handle it.) | ||
v8impl::trigger_fatal_exception(env, local_err); | ||
}); | ||
_env->CallbackIntoModule<true>([&](napi_env env) { | ||
_complete(env, ConvertUVErrorCode(status), _data); | ||
}); | ||
|
||
// Note: Don't access `work` after this point because it was | ||
// likely deleted by the complete callback. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -432,6 +432,12 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { | |
&EnvironmentOptions::force_async_hooks_checks, | ||
kAllowedInEnvironment, | ||
true); | ||
AddOption( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm sorry I did not pick up on this earlier. At some point last year we added easier handling for boolean options. This is an example of another one AddOption("--force-async-hooks-checks",
"disable checks for async_hooks",
&EnvironmentOptions::force_async_hooks_checks,
kAllowedInEnvironment,
true); That adds 2 possible flags --force-async-hooks-checks
--no-force-async-hooks-checks I think we should use the same technique to add the boolean option we are adding. In that context what likely makes sense is AddOption("--node-api-uncaught-exceptions-policy",
"disable checks for async_hooks",
&EnvironmentOptions::force_async_hooks_checks,
kAllowedInEnvironment,
true); This would make the option on by default and provide the following flags
We can switch the default by just changing the true/false in the part that adds the option in we are forced to fall back to older behavior. |
||
"--force-node-api-uncaught-exceptions-policy", | ||
"enforces 'uncaughtException' event on Node API asynchronous callbacks", | ||
&EnvironmentOptions::force_node_api_uncaught_exceptions_policy, | ||
kAllowedInEnvironment, | ||
false); | ||
AddOption("--addons", | ||
"disable loading native addons", | ||
&EnvironmentOptions::allow_native_addons, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
'use strict'; | ||
// Flags: --expose-gc --force-node-api-uncaught-exceptions-policy | ||
|
||
const common = require('../../common'); | ||
const test_reference = require(`./build/${common.buildType}/test_reference`); | ||
const assert = require('assert'); | ||
|
||
process.on('uncaughtException', common.mustCall((err) => { | ||
assert.throws(() => { throw err; }, /finalizer error/); | ||
})); | ||
|
||
(async function() { | ||
{ | ||
test_reference.createExternalWithJsFinalize( | ||
common.mustCall(() => { | ||
throw new Error('finalizer error'); | ||
})); | ||
} | ||
global.gc(); | ||
})().then(common.mustCall()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already part of
~napi_env__()
– is there a reason for that?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CallFinalizer
is a virtual method and should not be called from the base constructor's destructor. I'd think I need to remove theFinalizeAll
in~napi_env__()
.