Skip to content

Commit

Permalink
src: call napi_remove_wrap() in ObjectWrap dtor
Browse files Browse the repository at this point in the history
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.

Fixes: node-ffi-napi/weak-napi#16
  • Loading branch information
addaleax committed Apr 29, 2019
1 parent 36863f0 commit 048a378
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 0 deletions.
6 changes: 6 additions & 0 deletions napi-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -2814,6 +2814,12 @@ inline ObjectWrap<T>::ObjectWrap(const Napi::CallbackInfo& callbackInfo) {
*instanceRef = Reference<Object>(env, ref);
}

template <typename T>
inline ObjectWrap<T>::~ObjectWrap() {
if (!IsEmpty())
napi_remove_wrap(Env(), Value(), nullptr);
}

template<typename T>
inline T* ObjectWrap<T>::Unwrap(Object wrapper) {
T* unwrapped;
Expand Down
1 change: 1 addition & 0 deletions napi.h
Original file line number Diff line number Diff line change
Expand Up @@ -1569,6 +1569,7 @@ namespace Napi {
class ObjectWrap : public Reference<Object> {
public:
ObjectWrap(const CallbackInfo& callbackInfo);
~ObjectWrap();

static T* Unwrap(Object wrapper);

Expand Down
2 changes: 2 additions & 0 deletions test/binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Object InitObjectDeprecated(Env env);
Object InitPromise(Env env);
Object InitTypedArray(Env env);
Object InitObjectWrap(Env env);
Object InitObjectWrapRemoveWrap(Env env);
Object InitObjectReference(Env env);
Object InitVersionManagement(Env env);
Object InitThunkingManual(Env env);
Expand Down Expand Up @@ -73,6 +74,7 @@ Object Init(Env env, Object exports) {
exports.Set("promise", InitPromise(env));
exports.Set("typedarray", InitTypedArray(env));
exports.Set("objectwrap", InitObjectWrap(env));
exports.Set("objectwrap_removewrap", InitObjectWrapRemoveWrap(env));
exports.Set("objectreference", InitObjectReference(env));
exports.Set("version_management", InitVersionManagement(env));
exports.Set("thunking_manual", InitThunkingManual(env));
Expand Down
1 change: 1 addition & 0 deletions test/binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
'promise.cc',
'typedarray.cc',
'objectwrap.cc',
'objectwrap-removewrap.cc',
'objectreference.cc',
'version_management.cc',
'thunking_manual.cc',
Expand Down
1 change: 1 addition & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ let testModules = [
'typedarray',
'typedarray-bigint',
'objectwrap',
'objectwrap-removewrap',
'objectreference',
'version_management'
];
Expand Down
45 changes: 45 additions & 0 deletions test/objectwrap-removewrap.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#include <napi.h>
#include <assert.h>

#ifdef NAPI_CPP_EXCEPTIONS
namespace {

static int dtor_called = 0;

class DtorCounter {
public:
~DtorCounter() {
assert(dtor_called == 0);
dtor_called++;
}
};

Napi::Value GetDtorCalled(const Napi::CallbackInfo& info) {
return Napi::Number::New(info.Env(), dtor_called);
}

class Test : public Napi::ObjectWrap<Test> {
public:
Test(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Test>(info) {
throw Napi::Error::New(Env(), "Some error");
}

static void Initialize(Napi::Env env, Napi::Object exports) {
exports.Set("Test", DefineClass(env, "Test", {}));
exports.Set("getDtorCalled", Napi::Function::New(env, GetDtorCalled));
}

private:
DtorCounter dtor_ounter_;
};

} // anonymous namespace
#endif // NAPI_CPP_EXCEPTIONS

Napi::Object InitObjectWrapRemoveWrap(Napi::Env env) {
Napi::Object exports = Napi::Object::New(env);
#ifdef NAPI_CPP_EXCEPTIONS
Test::Initialize(env, exports);
#endif
return exports;
}
17 changes: 17 additions & 0 deletions test/objectwrap-removewrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';
const buildType = process.config.target_defaults.default_configuration;
const assert = require('assert');

const test = (binding) => {
const Test = binding.objectwrap_removewrap.Test;
const getDtorCalled = binding.objectwrap_removewrap.getDtorCalled;

assert.strictEqual(getDtorCalled(), 0);
assert.throws(() => {
new Test();
});
assert.strictEqual(getDtorCalled(), 1);
global.gc(); // Does not crash.
}

test(require(`./build/${buildType}/binding.node`));
3 changes: 3 additions & 0 deletions test/objectwrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ const test = (binding) => {
// `Test` is needed for accessing exposed symbols
testObj(new Test(), Test);
testClass(Test);

// Make sure the C++ object can be garbage collected without issues.
setImmediate(global.gc);
}

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

0 comments on commit 048a378

Please sign in to comment.