Skip to content

Commit ab2bf53

Browse files
committed
ffi: preserve strings during reentrant calls
Cache temporary string conversion buffers by wrapper and active call depth. This prevents nested FFI calls from overwriting or replacing buffers still in use by an outer native call. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.6-sol
1 parent b8f81e9 commit ab2bf53

3 files changed

Lines changed: 96 additions & 25 deletions

File tree

lib/internal/ffi/fast-api.js

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ const {
2626
} = internalBinding('ffi');
2727

2828
const kFastBuffer = Symbol('kFastBuffer');
29-
const kStringConversionBuffer = Symbol('kStringConversionBuffer');
3029

3130
function throwFFIArgError(msg) {
3231
// eslint-disable-next-line no-restricted-syntax
@@ -79,19 +78,20 @@ function hasPointerMemoryArg(type, value) {
7978
(isArrayBufferView(value) || isAnyArrayBuffer(value));
8079
}
8180

82-
function getStringConversionPointer(owner, value, index) {
83-
const size = value.length * 3 + 1;
84-
let buffers = owner[kStringConversionBuffer];
85-
if (buffers === undefined) {
86-
buffers = [];
87-
ObjectDefineProperty(owner, kStringConversionBuffer, {
88-
__proto__: null,
89-
configurable: false,
90-
enumerable: false,
91-
writable: false,
92-
value: buffers,
93-
});
81+
function enterStringConversion(state) {
82+
if (state.buffers[state.depth] === undefined) {
83+
state.buffers[state.depth] = [];
9484
}
85+
state.depth++;
86+
}
87+
88+
function exitStringConversion(state) {
89+
state.depth--;
90+
}
91+
92+
function getStringConversionPointer(state, value, index) {
93+
const size = value.length * 3 + 1;
94+
const buffers = state.buffers[state.depth - 1];
9595
let entry = buffers[index];
9696
if (entry !== undefined && entry.string === value) {
9797
return entry.pointer;
@@ -117,13 +117,13 @@ function getStringConversionPointer(owner, value, index) {
117117
return entry.pointer;
118118
}
119119

120-
function convertPointerArg(type, value, owner, index) {
120+
function convertPointerArg(type, value, stringState, index) {
121121
if (needsNullPointerConversion(type) &&
122122
(value === null || value === undefined)) {
123123
return 0n;
124124
}
125125
if (hasStringPointerArg(type, value)) {
126-
return getStringConversionPointer(owner, value, index);
126+
return getStringConversionPointer(stringState, value, index);
127127
}
128128
if (hasPointerMemoryArg(type, value)) {
129129
return getRawPointer(value);
@@ -181,7 +181,7 @@ function inheritMetadata(wrapper, rawFn, nargs) {
181181
return wrapper;
182182
}
183183

184-
function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) {
184+
function wrapWithRawPointerConversions(rawFn, argumentTypes, _owner) {
185185
if (rawFn === undefined || rawFn === null) {
186186
return rawFn;
187187
}
@@ -197,6 +197,12 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) {
197197
return rawFn;
198198
}
199199

200+
const stringState = {
201+
__proto__: null,
202+
buffers: [],
203+
depth: 0,
204+
};
205+
200206
const nargs = argumentTypes.length;
201207
let wrapper;
202208
if (nargs === 1 && indexes.length === 1 && indexes[0] === 0) {
@@ -214,7 +220,12 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) {
214220
(arg === null || arg === undefined)) {
215221
arg = 0n;
216222
} else if (string0 && typeof arg === 'string') {
217-
arg = getStringConversionPointer(owner, arg, 0);
223+
enterStringConversion(stringState);
224+
try {
225+
return rawFn(getStringConversionPointer(stringState, arg, 0));
226+
} finally {
227+
exitStringConversion(stringState);
228+
}
218229
} else if (memory0 && (isArrayBufferView(arg) || isAnyArrayBuffer(arg))) {
219230
if (fastBufferInvoke !== undefined) {
220231
return fastBufferInvoke(arg);
@@ -232,8 +243,15 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) {
232243
if (arguments.length !== 2) {
233244
throwFFIArgCountError(2, arguments.length);
234245
}
235-
return rawFn(c0 ? convertPointerArg(t0, a0, owner, 0) : a0,
236-
c1 ? convertPointerArg(t1, a1, owner, 1) : a1);
246+
const stringCall = (c0 && hasStringPointerArg(t0, a0)) ||
247+
(c1 && hasStringPointerArg(t1, a1));
248+
if (stringCall) enterStringConversion(stringState);
249+
try {
250+
return rawFn(c0 ? convertPointerArg(t0, a0, stringState, 0) : a0,
251+
c1 ? convertPointerArg(t1, a1, stringState, 1) : a1);
252+
} finally {
253+
if (stringCall) exitStringConversion(stringState);
254+
}
237255
};
238256
} else if (nargs === 3) {
239257
const c0 = ArrayPrototypeIncludes(indexes, 0);
@@ -246,21 +264,42 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) {
246264
if (arguments.length !== 3) {
247265
throwFFIArgCountError(3, arguments.length);
248266
}
249-
return rawFn(c0 ? convertPointerArg(t0, a0, owner, 0) : a0,
250-
c1 ? convertPointerArg(t1, a1, owner, 1) : a1,
251-
c2 ? convertPointerArg(t2, a2, owner, 2) : a2);
267+
const stringCall = (c0 && hasStringPointerArg(t0, a0)) ||
268+
(c1 && hasStringPointerArg(t1, a1)) ||
269+
(c2 && hasStringPointerArg(t2, a2));
270+
if (stringCall) enterStringConversion(stringState);
271+
try {
272+
return rawFn(c0 ? convertPointerArg(t0, a0, stringState, 0) : a0,
273+
c1 ? convertPointerArg(t1, a1, stringState, 1) : a1,
274+
c2 ? convertPointerArg(t2, a2, stringState, 2) : a2);
275+
} finally {
276+
if (stringCall) exitStringConversion(stringState);
277+
}
252278
};
253279
} else {
254280
wrapper = function(...args) {
255281
if (args.length !== nargs) {
256282
throwFFIArgCountError(nargs, args.length);
257283
}
284+
let stringCall = false;
258285
for (let i = 0; i < indexes.length; i++) {
259286
const index = indexes[i];
260-
args[index] = convertPointerArg(
261-
argumentTypes[index], args[index], owner, index);
287+
if (hasStringPointerArg(argumentTypes[index], args[index])) {
288+
stringCall = true;
289+
break;
290+
}
291+
}
292+
if (stringCall) enterStringConversion(stringState);
293+
try {
294+
for (let i = 0; i < indexes.length; i++) {
295+
const index = indexes[i];
296+
args[index] = convertPointerArg(
297+
argumentTypes[index], args[index], stringState, index);
298+
}
299+
return ReflectApply(rawFn, undefined, args);
300+
} finally {
301+
if (stringCall) exitStringConversion(stringState);
262302
}
263-
return ReflectApply(rawFn, undefined, args);
264303
};
265304
}
266305

test/ffi/fixture_library/ffi_test_library.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,15 @@ FFI_EXPORT void call_void_callback(VoidCallback callback) {
333333
}
334334
}
335335

336+
FFI_EXPORT int32_t string_survives_callback(const char* str,
337+
VoidCallback callback) {
338+
if (callback) {
339+
callback();
340+
}
341+
342+
return str && strcmp(str, "outer string") == 0;
343+
}
344+
336345
FFI_EXPORT void call_string_callback(StringCallback callback, const char* str) {
337346
if (callback) {
338347
callback(str);

test/ffi/test-ffi-fast-buffer.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,26 @@ test('fast FFI buffer arguments reject invalid values', () => {
6969
lib.close();
7070
}
7171
});
72+
73+
test('fast FFI string buffers survive reentrant callbacks', () => {
74+
const { lib, functions } = ffi.dlopen(libraryPath, {
75+
safe_strlen: { arguments: ['string'], return: 'i32' },
76+
string_survives_callback: {
77+
arguments: ['string', 'pointer'],
78+
return: 'i32',
79+
},
80+
});
81+
let nestedLength;
82+
const callback = lib.registerCallback(() => {
83+
nestedLength = functions.safe_strlen('inner string');
84+
});
85+
86+
try {
87+
assert.strictEqual(
88+
functions.string_survives_callback('outer string', callback), 1);
89+
assert.strictEqual(nestedLength, 12);
90+
} finally {
91+
lib.unregisterCallback(callback);
92+
lib.close();
93+
}
94+
});

0 commit comments

Comments
 (0)