diff --git a/Core/Node-API/Source/js_native_api_chakra.cc b/Core/Node-API/Source/js_native_api_chakra.cc index 297be2aa..67066d92 100644 --- a/Core/Node-API/Source/js_native_api_chakra.cc +++ b/Core/Node-API/Source/js_native_api_chakra.cc @@ -172,6 +172,27 @@ class ExternalCallback { napi_value result = externalCallback->_cb( externalCallback->_env, reinterpret_cast(&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(arguments[0]), nullptr); + JsSetException(exception); + } + } + } + return reinterpret_cast(result); } @@ -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; } diff --git a/Tests/UnitTests/Scripts/tests.ts b/Tests/UnitTests/Scripts/tests.ts index e0647092..ff2d7915 100644 --- a/Tests/UnitTests/Scripts/tests.ts +++ b/Tests/UnitTests/Scripts/tests.ts @@ -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 () {