Skip to content

Commit 4e88506

Browse files
addaleaxGabriel Schulhof
and
Gabriel Schulhof
committed
src: call napi_remove_wrap() in ObjectWrap dtor
Currently, when the `ObjectWrap` constructor runs, it calls `napi_wrap()`, adding a finalize callback to the freshly created JS object. However, if the `ObjectWrap` instance is prematurely deleted, for example because a subclass constructor throws – which seems like a reasonable scenario – that finalize callback was not removed, possibly leading to a use-after-free crash. This commit adds a call `napi_remove_wrap()` from the `ObjectWrap` destructor, and a test for that scenario. This also changes the code to use the correct pointer type in `FinalizeCallback`, which may not match the incorretct one in cases of multiple inheritance. Fixes: node-ffi-napi/weak-napi#16 PR-URL: #475 Reviewed-By: Hitesh Kanwathirtha <[email protected]> Reviewed-By: Gabriel Schulhof <[email protected]> Reviewed-By: Tobias Nießen <[email protected]> Reviewed-By: Michael Dawson <[email protected]> Co-authored-by: Gabriel Schulhof <[email protected]>
1 parent 2fde5c3 commit 4e88506

7 files changed

+84
-45
lines changed

napi-inl.h

Lines changed: 15 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2702,37 +2702,6 @@ inline Object FunctionReference::New(const std::vector<napi_value>& args) const
27022702
// CallbackInfo class
27032703
////////////////////////////////////////////////////////////////////////////////
27042704

2705-
class ObjectWrapConstructionContext {
2706-
public:
2707-
ObjectWrapConstructionContext(CallbackInfo* info) {
2708-
info->_objectWrapConstructionContext = this;
2709-
}
2710-
2711-
static inline void SetObjectWrapped(const CallbackInfo& info) {
2712-
if (info._objectWrapConstructionContext == nullptr) {
2713-
Napi::Error::Fatal("ObjectWrapConstructionContext::SetObjectWrapped",
2714-
"_objectWrapConstructionContext is NULL");
2715-
}
2716-
info._objectWrapConstructionContext->_objectWrapped = true;
2717-
}
2718-
2719-
inline void Cleanup(const CallbackInfo& info) {
2720-
if (_objectWrapped) {
2721-
napi_status status = napi_remove_wrap(info.Env(), info.This(), nullptr);
2722-
2723-
// There's already a pending exception if we are at this point, so we have
2724-
// no choice but to fatally fail here.
2725-
NAPI_FATAL_IF_FAILED(status,
2726-
"ObjectWrapConstructionContext::Cleanup",
2727-
"Failed to remove wrap from unsuccessfully "
2728-
"constructed ObjectWrap instance");
2729-
}
2730-
}
2731-
2732-
private:
2733-
bool _objectWrapped = false;
2734-
};
2735-
27362705
inline CallbackInfo::CallbackInfo(napi_env env, napi_callback_info info)
27372706
: _env(env), _info(info), _this(nullptr), _dynamicArgs(nullptr), _data(nullptr) {
27382707
_argc = _staticArgCount;
@@ -3140,13 +3109,22 @@ inline ObjectWrap<T>::ObjectWrap(const Napi::CallbackInfo& callbackInfo) {
31403109
status = napi_wrap(env, wrapper, this, FinalizeCallback, nullptr, &ref);
31413110
NAPI_THROW_IF_FAILED_VOID(env, status);
31423111

3143-
ObjectWrapConstructionContext::SetObjectWrapped(callbackInfo);
31443112
Reference<Object>* instanceRef = this;
31453113
*instanceRef = Reference<Object>(env, ref);
31463114
}
31473115

3148-
template<typename T>
3149-
inline ObjectWrap<T>::~ObjectWrap() {}
3116+
template <typename T>
3117+
inline ObjectWrap<T>::~ObjectWrap() {
3118+
// If the JS object still exists at this point, remove the finalizer added
3119+
// through `napi_wrap()`.
3120+
if (!IsEmpty()) {
3121+
Object object = Value();
3122+
// It is not valid to call `napi_remove_wrap()` with an empty `object`.
3123+
// This happens e.g. during garbage collection.
3124+
if (!object.IsEmpty())
3125+
napi_remove_wrap(Env(), object, nullptr);
3126+
}
3127+
}
31503128

31513129
template<typename T>
31523130
inline T* ObjectWrap<T>::Unwrap(Object wrapper) {
@@ -3716,23 +3694,15 @@ inline napi_value ObjectWrap<T>::ConstructorCallbackWrapper(
37163694

37173695
napi_value wrapper = details::WrapCallback([&] {
37183696
CallbackInfo callbackInfo(env, info);
3719-
ObjectWrapConstructionContext constructionContext(&callbackInfo);
37203697
#ifdef NAPI_CPP_EXCEPTIONS
3721-
try {
3722-
new T(callbackInfo);
3723-
} catch (const Error& e) {
3724-
// Re-throw the error after removing the failed wrap.
3725-
constructionContext.Cleanup(callbackInfo);
3726-
throw e;
3727-
}
3698+
new T(callbackInfo);
37283699
#else
37293700
T* instance = new T(callbackInfo);
37303701
if (callbackInfo.Env().IsExceptionPending()) {
37313702
// We need to clear the exception so that removing the wrap might work.
37323703
Error e = callbackInfo.Env().GetAndClearPendingException();
3733-
constructionContext.Cleanup(callbackInfo);
3734-
e.ThrowAsJavaScriptException();
37353704
delete instance;
3705+
e.ThrowAsJavaScriptException();
37363706
}
37373707
# endif // NAPI_CPP_EXCEPTIONS
37383708
return callbackInfo.This();
@@ -3859,7 +3829,7 @@ inline napi_value ObjectWrap<T>::InstanceSetterCallbackWrapper(
38593829

38603830
template <typename T>
38613831
inline void ObjectWrap<T>::FinalizeCallback(napi_env env, void* data, void* /*hint*/) {
3862-
T* instance = reinterpret_cast<T*>(data);
3832+
ObjectWrap<T>* instance = static_cast<ObjectWrap<T>*>(data);
38633833
instance->Finalize(Napi::Env(env));
38643834
delete instance;
38653835
}

test/binding.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Object InitThreadSafeFunction(Env env);
5151
Object InitTypedArray(Env env);
5252
Object InitObjectWrap(Env env);
5353
Object InitObjectWrapConstructorException(Env env);
54+
Object InitObjectWrapRemoveWrap(Env env);
5455
Object InitObjectReference(Env env);
5556
Object InitVersionManagement(Env env);
5657
Object InitThunkingManual(Env env);
@@ -107,6 +108,7 @@ Object Init(Env env, Object exports) {
107108
exports.Set("objectwrap", InitObjectWrap(env));
108109
exports.Set("objectwrapConstructorException",
109110
InitObjectWrapConstructorException(env));
111+
exports.Set("objectwrap_removewrap", InitObjectWrapRemoveWrap(env));
110112
exports.Set("objectreference", InitObjectReference(env));
111113
exports.Set("version_management", InitVersionManagement(env));
112114
exports.Set("thunking_manual", InitThunkingManual(env));

test/binding.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
'typedarray.cc',
4343
'objectwrap.cc',
4444
'objectwrap_constructor_exception.cc',
45+
'objectwrap-removewrap.cc',
4546
'objectreference.cc',
4647
'version_management.cc',
4748
'thunking_manual.cc',

test/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ let testModules = [
5050
'typedarray-bigint',
5151
'objectwrap',
5252
'objectwrap_constructor_exception',
53+
'objectwrap-removewrap',
5354
'objectreference',
5455
'version_management'
5556
];

test/objectwrap-removewrap.cc

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#include <napi.h>
2+
#include <assert.h>
3+
4+
#ifdef NAPI_CPP_EXCEPTIONS
5+
namespace {
6+
7+
static int dtor_called = 0;
8+
9+
class DtorCounter {
10+
public:
11+
~DtorCounter() {
12+
assert(dtor_called == 0);
13+
dtor_called++;
14+
}
15+
};
16+
17+
Napi::Value GetDtorCalled(const Napi::CallbackInfo& info) {
18+
return Napi::Number::New(info.Env(), dtor_called);
19+
}
20+
21+
class Test : public Napi::ObjectWrap<Test> {
22+
public:
23+
Test(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Test>(info) {
24+
throw Napi::Error::New(Env(), "Some error");
25+
}
26+
27+
static void Initialize(Napi::Env env, Napi::Object exports) {
28+
exports.Set("Test", DefineClass(env, "Test", {}));
29+
exports.Set("getDtorCalled", Napi::Function::New(env, GetDtorCalled));
30+
}
31+
32+
private:
33+
DtorCounter dtor_ounter_;
34+
};
35+
36+
} // anonymous namespace
37+
#endif // NAPI_CPP_EXCEPTIONS
38+
39+
Napi::Object InitObjectWrapRemoveWrap(Napi::Env env) {
40+
Napi::Object exports = Napi::Object::New(env);
41+
#ifdef NAPI_CPP_EXCEPTIONS
42+
Test::Initialize(env, exports);
43+
#endif
44+
return exports;
45+
}

test/objectwrap-removewrap.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use strict';
2+
const buildType = process.config.target_defaults.default_configuration;
3+
const assert = require('assert');
4+
5+
const test = (binding) => {
6+
const Test = binding.objectwrap_removewrap.Test;
7+
const getDtorCalled = binding.objectwrap_removewrap.getDtorCalled;
8+
9+
assert.strictEqual(getDtorCalled(), 0);
10+
assert.throws(() => {
11+
new Test();
12+
});
13+
assert.strictEqual(getDtorCalled(), 1);
14+
global.gc(); // Does not crash.
15+
}
16+
17+
test(require(`./build/${buildType}/binding.node`));

test/objectwrap.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,9 @@ const test = (binding) => {
273273
// `Test` is needed for accessing exposed symbols
274274
testObj(new Test(), Test);
275275
testClass(Test);
276+
277+
// Make sure the C++ object can be garbage collected without issues.
278+
setImmediate(global.gc);
276279
}
277280

278281
test(require(`./build/${buildType}/binding.node`));

0 commit comments

Comments
 (0)