Skip to content

Commit a552a38

Browse files
addaleaxGabriel Schulhof
andcommitted
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 1a51067 commit a552a38

File tree

7 files changed

+84
-45
lines changed

7 files changed

+84
-45
lines changed

napi-inl.h

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

2669-
class ObjectWrapConstructionContext {
2670-
public:
2671-
ObjectWrapConstructionContext(CallbackInfo* info) {
2672-
info->_objectWrapConstructionContext = this;
2673-
}
2674-
2675-
static inline void SetObjectWrapped(const CallbackInfo& info) {
2676-
if (info._objectWrapConstructionContext == nullptr) {
2677-
Napi::Error::Fatal("ObjectWrapConstructionContext::SetObjectWrapped",
2678-
"_objectWrapConstructionContext is NULL");
2679-
}
2680-
info._objectWrapConstructionContext->_objectWrapped = true;
2681-
}
2682-
2683-
inline void Cleanup(const CallbackInfo& info) {
2684-
if (_objectWrapped) {
2685-
napi_status status = napi_remove_wrap(info.Env(), info.This(), nullptr);
2686-
2687-
// There's already a pending exception if we are at this point, so we have
2688-
// no choice but to fatally fail here.
2689-
NAPI_FATAL_IF_FAILED(status,
2690-
"ObjectWrapConstructionContext::Cleanup",
2691-
"Failed to remove wrap from unsuccessfully "
2692-
"constructed ObjectWrap instance");
2693-
}
2694-
}
2695-
2696-
private:
2697-
bool _objectWrapped = false;
2698-
};
2699-
27002669
inline CallbackInfo::CallbackInfo(napi_env env, napi_callback_info info)
27012670
: _env(env), _info(info), _this(nullptr), _dynamicArgs(nullptr), _data(nullptr) {
27022671
_argc = _staticArgCount;
@@ -3002,13 +2971,22 @@ inline ObjectWrap<T>::ObjectWrap(const Napi::CallbackInfo& callbackInfo) {
30022971
status = napi_wrap(env, wrapper, this, FinalizeCallback, nullptr, &ref);
30032972
NAPI_THROW_IF_FAILED_VOID(env, status);
30042973

3005-
ObjectWrapConstructionContext::SetObjectWrapped(callbackInfo);
30062974
Reference<Object>* instanceRef = this;
30072975
*instanceRef = Reference<Object>(env, ref);
30082976
}
30092977

3010-
template<typename T>
3011-
inline ObjectWrap<T>::~ObjectWrap() {}
2978+
template <typename T>
2979+
inline ObjectWrap<T>::~ObjectWrap() {
2980+
// If the JS object still exists at this point, remove the finalizer added
2981+
// through `napi_wrap()`.
2982+
if (!IsEmpty()) {
2983+
Object object = Value();
2984+
// It is not valid to call `napi_remove_wrap()` with an empty `object`.
2985+
// This happens e.g. during garbage collection.
2986+
if (!object.IsEmpty())
2987+
napi_remove_wrap(Env(), object, nullptr);
2988+
}
2989+
}
30122990

30132991
template<typename T>
30142992
inline T* ObjectWrap<T>::Unwrap(Object wrapper) {
@@ -3402,23 +3380,15 @@ inline napi_value ObjectWrap<T>::ConstructorCallbackWrapper(
34023380

34033381
napi_value wrapper = details::WrapCallback([&] {
34043382
CallbackInfo callbackInfo(env, info);
3405-
ObjectWrapConstructionContext constructionContext(&callbackInfo);
34063383
#ifdef NAPI_CPP_EXCEPTIONS
3407-
try {
3408-
new T(callbackInfo);
3409-
} catch (const Error& e) {
3410-
// Re-throw the error after removing the failed wrap.
3411-
constructionContext.Cleanup(callbackInfo);
3412-
throw e;
3413-
}
3384+
new T(callbackInfo);
34143385
#else
34153386
T* instance = new T(callbackInfo);
34163387
if (callbackInfo.Env().IsExceptionPending()) {
34173388
// We need to clear the exception so that removing the wrap might work.
34183389
Error e = callbackInfo.Env().GetAndClearPendingException();
3419-
constructionContext.Cleanup(callbackInfo);
3420-
e.ThrowAsJavaScriptException();
34213390
delete instance;
3391+
e.ThrowAsJavaScriptException();
34223392
}
34233393
# endif // NAPI_CPP_EXCEPTIONS
34243394
return callbackInfo.This();
@@ -3545,7 +3515,7 @@ inline napi_value ObjectWrap<T>::InstanceSetterCallbackWrapper(
35453515

35463516
template <typename T>
35473517
inline void ObjectWrap<T>::FinalizeCallback(napi_env env, void* data, void* /*hint*/) {
3548-
T* instance = reinterpret_cast<T*>(data);
3518+
ObjectWrap<T>* instance = static_cast<ObjectWrap<T>*>(data);
35493519
instance->Finalize(Napi::Env(env));
35503520
delete instance;
35513521
}

test/binding.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Object InitThreadSafeFunction(Env env);
5050
Object InitTypedArray(Env env);
5151
Object InitObjectWrap(Env env);
5252
Object InitObjectWrapConstructorException(Env env);
53+
Object InitObjectWrapRemoveWrap(Env env);
5354
Object InitObjectReference(Env env);
5455
Object InitVersionManagement(Env env);
5556
Object InitThunkingManual(Env env);
@@ -105,6 +106,7 @@ Object Init(Env env, Object exports) {
105106
exports.Set("objectwrap", InitObjectWrap(env));
106107
exports.Set("objectwrapConstructorException",
107108
InitObjectWrapConstructorException(env));
109+
exports.Set("objectwrap_removewrap", InitObjectWrapRemoveWrap(env));
108110
exports.Set("objectreference", InitObjectReference(env));
109111
exports.Set("version_management", InitVersionManagement(env));
110112
exports.Set("thunking_manual", InitThunkingManual(env));

test/binding.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
'typedarray.cc',
4646
'objectwrap.cc',
4747
'objectwrap_constructor_exception.cc',
48+
'objectwrap-removewrap.cc',
4849
'objectreference.cc',
4950
'version_management.cc',
5051
'thunking_manual.cc',

test/index.js

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

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
@@ -222,6 +222,9 @@ const test = (binding) => {
222222
// `Test` is needed for accessing exposed symbols
223223
testObj(new Test(), Test);
224224
testClass(Test);
225+
226+
// Make sure the C++ object can be garbage collected without issues.
227+
setImmediate(global.gc);
225228
}
226229

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

0 commit comments

Comments
 (0)