Skip to content

Commit 4812e87

Browse files
committed
ffi: fix crash in refCallback and unrefCallback
Both handlers called fn.ClearWeak() and fn.SetWeak() without checking whether the persistent handle was still populated. After the callback function is garbage collected following an earlier unrefCallback() call, the handle is empty and both V8 methods dereference a null slot. InvokeCallback() already guarded against this. Add the same check to both handlers and throw ERR_INVALID_ARG_VALUE, matching the existing behavior for a pointer that is not in the callback map. Signed-off-by: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Assisted-by: claude:opus-5
1 parent 67af6d1 commit 4812e87

3 files changed

Lines changed: 55 additions & 0 deletions

File tree

doc/api/ffi.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,10 @@ memory.
459459

460460
Keeps the callback strongly referenced by JavaScript.
461461

462+
Throws `ERR_INVALID_ARG_VALUE` if the callback function has already been
463+
garbage collected after a previous `library.unrefCallback(pointer)` call, since
464+
a collected function cannot be referenced again.
465+
462466
### `library.unrefCallback(pointer)`
463467

464468
* `pointer` {bigint}
@@ -469,6 +473,9 @@ If the callback function is later garbage collected, subsequent native
469473
invocations become a no-op. Non-void return values are zero-initialized before
470474
returning to native code.
471475

476+
Throws `ERR_INVALID_ARG_VALUE` if the callback function has already been
477+
garbage collected.
478+
472479
## Calling native functions
473480

474481
Argument conversion depends on the declared FFI type.

src/node_ffi.cc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,6 +1125,15 @@ void DynamicLibrary::RefCallback(const FunctionCallbackInfo<Value>& args) {
11251125
return;
11261126
}
11271127

1128+
// The callback function may already have been collected after a previous
1129+
// unrefCallback() call. The persistent handle is empty in that case, and
1130+
// ClearWeak() on an empty handle dereferences a null slot. There is also no
1131+
// function left to make strong again.
1132+
if (existing->second->fn.IsEmpty()) {
1133+
THROW_ERR_INVALID_ARG_VALUE(env, "Callback not found");
1134+
return;
1135+
}
1136+
11281137
existing->second->fn.ClearWeak();
11291138
}
11301139

@@ -1156,6 +1165,14 @@ void DynamicLibrary::UnrefCallback(const FunctionCallbackInfo<Value>& args) {
11561165
return;
11571166
}
11581167

1168+
// The callback function may already have been collected by a previous
1169+
// unrefCallback() call. The persistent handle is empty in that case, and
1170+
// SetWeak() on an empty handle dereferences a null slot.
1171+
if (existing->second->fn.IsEmpty()) {
1172+
THROW_ERR_INVALID_ARG_VALUE(env, "Callback not found");
1173+
return;
1174+
}
1175+
11591176
existing->second->fn.SetWeak();
11601177
}
11611178

test/ffi/test-ffi-weakref-calls.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,37 @@ test('ffi refCallback retains callback function', async (t) => {
5151
lib.unregisterCallback(pointer);
5252
});
5353

54+
test('callback ref/unref throw after callback function is collected', async (t) => {
55+
const { lib } = ffi.dlopen(libraryPath, fixtureSymbols);
56+
t.after(() => lib.close());
57+
58+
let callback = () => 1;
59+
const ref = new WeakRef(callback);
60+
const pointer = lib.registerCallback(
61+
{ arguments: ['i32'], return: 'i32' },
62+
callback,
63+
);
64+
65+
lib.unrefCallback(pointer);
66+
callback = null;
67+
68+
await gcUntil(
69+
'callback ref/unref throw after callback function is collected',
70+
() => ref.deref() === undefined,
71+
);
72+
73+
t.assert.throws(() => lib.unrefCallback(pointer), {
74+
code: 'ERR_INVALID_ARG_VALUE',
75+
message: /Callback not found/,
76+
});
77+
t.assert.throws(() => lib.refCallback(pointer), {
78+
code: 'ERR_INVALID_ARG_VALUE',
79+
message: /Callback not found/,
80+
});
81+
82+
lib.unregisterCallback(pointer);
83+
});
84+
5485
test('callback ref/unref/unregister throw when library is closed', (t) => {
5586
const { lib } = ffi.dlopen(libraryPath, fixtureSymbols);
5687
const callback = lib.registerCallback(() => {});

0 commit comments

Comments
 (0)