Skip to content

Commit 4f1aeb3

Browse files
committed
n-api: handle reference delete before finalize
Crashes were reported during finalization due to the memory for a reference being deleted and the finalizer running after the deletion. This change ensures the deletion of the memory for the reference only occurs after the finalizer has run. Fixes: nodejs/node-addon-api#393 Backport-PR-URL: nodejs#25572 PR-URL: nodejs#24494 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com> Reviewed-By: Refael Ackermann <refack@gmail.com>
1 parent b44bd88 commit 4f1aeb3

3 files changed

Lines changed: 92 additions & 6 deletions

File tree

src/node_api.cc

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ class Finalizer {
356356
napi_finalize _finalize_callback;
357357
void* _finalize_data;
358358
void* _finalize_hint;
359+
bool _finalize_ran = false;
359360
};
360361

361362
// Wrapper around v8::Persistent that implements reference counting.
@@ -410,8 +411,29 @@ class Reference : private Finalizer {
410411
finalize_hint);
411412
}
412413

414+
// Delete is called in 2 ways. Either from the finalizer or
415+
// from one of Unwrap or napi_delete_reference.
416+
//
417+
// When it is called from Unwrap or napi_delete_reference we only
418+
// want to do the delete if the finalizer has already run,
419+
// otherwise we may crash when the finalizer does run.
420+
// If the finalizer has not already run delay the delete until
421+
// the finalizer runs by not doing the delete
422+
// and setting _delete_self to true so that the finalizer will
423+
// delete it when it runs.
424+
//
425+
// The second way this is called is from
426+
// the finalizer and _delete_self is set. In this case we
427+
// know we need to do the deletion so just do it.
413428
static void Delete(Reference* reference) {
414-
delete reference;
429+
if ((reference->_delete_self) || (reference->_finalize_ran)) {
430+
delete reference;
431+
} else {
432+
// reduce the reference count to 0 and defer until
433+
// finalizer runs
434+
reference->_delete_self = true;
435+
while (reference->Unref() != 0) {}
436+
}
415437
}
416438

417439
uint32_t Ref() {
@@ -452,9 +474,6 @@ class Reference : private Finalizer {
452474
Reference* reference = data.GetParameter();
453475
reference->_persistent.Reset();
454476

455-
// Check before calling the finalize callback, because the callback might
456-
// delete it.
457-
bool delete_self = reference->_delete_self;
458477
napi_env env = reference->_env;
459478

460479
if (reference->_finalize_callback != nullptr) {
@@ -465,8 +484,13 @@ class Reference : private Finalizer {
465484
reference->_finalize_hint));
466485
}
467486

468-
if (delete_self) {
487+
// this is safe because if a request to delete the reference
488+
// is made in the finalize_callback it will defer deletion
489+
// to this block and set _delete_self to true
490+
if (reference->_delete_self) {
469491
Delete(reference);
492+
} else {
493+
reference->_finalize_ran = true;
470494
}
471495
}
472496

test/addons-napi/test_reference/test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,29 @@ runTests(0, undefined, [
118118
assert.strictEqual(test_reference.finalizeCount, 1);
119119
},
120120
]);
121+
122+
// This test creates a napi_ref on an object that has
123+
// been wrapped by napi_wrap and for which the finalizer
124+
// for the wrap calls napi_delete_ref on that napi_ref.
125+
//
126+
// Since both the wrap and the reference use the same
127+
// object the finalizer for the wrap and reference
128+
// may run in the same gc and in any order.
129+
//
130+
// It does that to validate that napi_delete_ref can be
131+
// called before the finalizer has been run for the
132+
// reference (there is a finalizer behind the scenes even
133+
// though it cannot be passed to napi_create_reference).
134+
//
135+
// Since the order is not guarranteed, run the
136+
// test a number of times maximize the chance that we
137+
// get a run with the desired order for the test.
138+
//
139+
// 1000 reliably recreated the problem without the fix
140+
// required to ensure delete could be called before
141+
// the finalizer in manual testing.
142+
for (let i = 0; i < 1000; i++) {
143+
const wrapObject = new Object();
144+
test_reference.validateDeleteBeforeFinalize(wrapObject);
145+
global.gc();
146+
}

test/addons-napi/test_reference/test_reference.c

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#include <stdlib.h>
12
#include <node_api.h>
23
#include "../common.h"
34

@@ -130,7 +131,40 @@ napi_value GetReferenceValue(napi_env env, napi_callback_info info) {
130131
return result;
131132
}
132133

133-
napi_value Init(napi_env env, napi_value exports) {
134+
static void DeleteBeforeFinalizeFinalizer(
135+
napi_env env, void* finalize_data, void* finalize_hint) {
136+
napi_ref* ref = (napi_ref*)finalize_data;
137+
napi_delete_reference(env, *ref);
138+
free(ref);
139+
}
140+
141+
static napi_value ValidateDeleteBeforeFinalize(napi_env env, napi_callback_info info) {
142+
napi_value wrapObject;
143+
size_t argc = 1;
144+
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, &wrapObject, NULL, NULL));
145+
146+
napi_ref* ref_t = malloc(sizeof(napi_ref));
147+
NAPI_CALL(env, napi_wrap(env,
148+
wrapObject,
149+
ref_t,
150+
DeleteBeforeFinalizeFinalizer,
151+
NULL,
152+
NULL));
153+
154+
// Create a reference that will be eligible for collection at the same
155+
// time as the wrapped object by passing in the same wrapObject.
156+
// This means that the FinalizeOrderValidation callback may be run
157+
// before the finalizer for the newly created reference (there is a finalizer
158+
// behind the scenes even though it cannot be passed to napi_create_reference)
159+
// The Finalizer for the wrap (which is different than the finalizer
160+
// for the reference) calls napi_delete_reference validating that
161+
// napi_delete_reference can be called before the finalizer for the
162+
// reference runs.
163+
NAPI_CALL(env, napi_create_reference(env, wrapObject, 0, ref_t));
164+
return wrapObject;
165+
}
166+
167+
static napi_value Init(napi_env env, napi_value exports) {
134168
napi_property_descriptor descriptors[] = {
135169
DECLARE_NAPI_GETTER("finalizeCount", GetFinalizeCount),
136170
DECLARE_NAPI_PROPERTY("createExternal", CreateExternal),
@@ -142,6 +176,8 @@ napi_value Init(napi_env env, napi_value exports) {
142176
DECLARE_NAPI_PROPERTY("incrementRefcount", IncrementRefcount),
143177
DECLARE_NAPI_PROPERTY("decrementRefcount", DecrementRefcount),
144178
DECLARE_NAPI_GETTER("referenceValue", GetReferenceValue),
179+
DECLARE_NAPI_PROPERTY("validateDeleteBeforeFinalize",
180+
ValidateDeleteBeforeFinalize),
145181
};
146182

147183
NAPI_CALL(env, napi_define_properties(

0 commit comments

Comments
 (0)