Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions Core/Node-API/Source/js_native_api_chakra.cc
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,27 @@ class ExternalCallback {

napi_value result = externalCallback->_cb(
externalCallback->_env, reinterpret_cast<napi_callback_info>(&cbInfo));

// If a constructor (construct call) left a pending JS exception, the C++
// ObjectWrap instance was already destroyed by stack unwinding inside the
// callback, but the wrap finalizer registered by napi_wrap() is still
// attached to `this`. The addon-api ~ObjectWrap() cannot detach it here
// because napi_get_reference_value() returns null for the wrap's weak
// (refcount 0) reference. Detach the wrap now so a later GC does not run
// the finalizer on the freed native instance (use-after-free / heap
// corruption). The pending exception is preserved across the cleanup.
if (isConstructCall) {
bool hasException = false;
if (JsHasException(&hasException) == JsNoError && hasException) {
JsValueRef exception = JS_INVALID_REFERENCE;
if (JsGetAndClearException(&exception) == JsNoError) {
napi_remove_wrap(externalCallback->_env,
reinterpret_cast<napi_value>(arguments[0]), nullptr);
JsSetException(exception);
}
}
}

return reinterpret_cast<JsValueRef>(result);
}

Expand Down Expand Up @@ -1748,9 +1769,11 @@ napi_status napi_remove_wrap(napi_env env, napi_value js_object, void** result)
CHECK_JSRT(env, JsSetExternalData(wrapper, nullptr));

if (externalData != nullptr) {
*result = externalData->Data();
if (result != nullptr) {
*result = externalData->Data();
}
delete externalData;
} else {
} else if (result != nullptr) {
*result = nullptr;
}

Expand Down
14 changes: 14 additions & 0 deletions Tests/UnitTests/Scripts/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1490,6 +1490,20 @@ describe("TextDecoder", function () {
expect(result).to.equal("H\0i");
expect(result.length).to.equal(3);
});

it("throwing from the constructor repeatedly does not corrupt native state", function () {
// Regression for a Chakra N-API ObjectWrap bug: when a wrapped
// constructor throws, the native instance is destroyed during stack
// unwinding but the wrap finalizer stayed attached to `this`, so a
// later GC ran the finalizer on freed memory (heap corruption). Throw
// many times to create many dangling wraps, then allocate/decode to
// exercise the heap and surface any corruption within this test run.
for (let i = 0; i < 100; ++i) {
expect(() => new TextDecoder("utf-16")).to.throw();
}
const decoder = new TextDecoder("utf-8");
expect(decoder.decode(new Uint8Array([79, 75]))).to.equal("OK");
});
});

describe("TextEncoder", function () {
Expand Down
Loading