Fix FFI segfault when passing ArrayBuffer as pointer argument (#22225)

The FFI `JSVALUE_TO_PTR` function was missing support for ArrayBuffer objects,
causing segfaults when ArrayBuffers were passed as pointer arguments to FFI
functions. TypedArrays and DataViews worked correctly, but ArrayBuffers fell
through to number-as-pointer conversion logic, resulting in invalid pointers.

Changes:
- Add `JSC__JSValue__toArrayBufferPtr` C++ helper function to extract ArrayBuffer data pointer
- Add `JSCELL_IS_ARRAY_BUFFER` helper function for type detection
- Update `JSVALUE_TO_PTR` in FFI.h to handle ArrayBuffer case
- Add JSTypeArrayBuffer constant (value 38) for type detection
- Integrate ArrayBuffer support into FFI symbol table
- Add regression test to prevent future issues
- Add test/js/bun/ffi/*.so to .gitignore to prevent committing compiled FFI test binaries

Fixes: ArrayBuffer, TypedArray, and DataView now all work identically when
passed as pointer arguments to FFI functions.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2025-08-29 09:42:04 +00:00
parent 3545cca8cc
commit 18b5301a7f
9 changed files with 110 additions and 11 deletions

2
.gitignore vendored
View File

@@ -186,4 +186,4 @@ scratch*.{js,ts,tsx,cjs,mjs}
*.bun-build
scripts/lldb-inline
scripts/lldb-inline*.so

View File

@@ -180,8 +180,10 @@ static bool JSVALUE_TO_BOOL(EncodedJSValue val) __attribute__((__always_inline__
static uint8_t GET_JSTYPE(EncodedJSValue val) __attribute__((__always_inline__));
static bool JSTYPE_IS_TYPED_ARRAY(uint8_t type) __attribute__((__always_inline__));
static bool JSCELL_IS_TYPED_ARRAY(EncodedJSValue val) __attribute__((__always_inline__));
static bool JSCELL_IS_ARRAY_BUFFER(EncodedJSValue val) __attribute__((__always_inline__));
static void* JSVALUE_TO_TYPED_ARRAY_VECTOR(EncodedJSValue val) __attribute__((__always_inline__));
static uint64_t JSVALUE_TO_TYPED_ARRAY_LENGTH(EncodedJSValue val) __attribute__((__always_inline__));
void* JSVALUE_TO_ARRAYBUFFER_PTR(EncodedJSValue val);
static bool JSVALUE_IS_CELL(EncodedJSValue val) {
return !(val.asInt64 & NotCellMask);
@@ -207,6 +209,10 @@ static bool JSCELL_IS_TYPED_ARRAY(EncodedJSValue val) {
return JSVALUE_IS_CELL(val) && JSTYPE_IS_TYPED_ARRAY(GET_JSTYPE(val));
}
static bool JSCELL_IS_ARRAY_BUFFER(EncodedJSValue val) {
return JSVALUE_IS_CELL(val) && GET_JSTYPE(val) == JSTypeArrayBuffer;
}
static void* JSVALUE_TO_TYPED_ARRAY_VECTOR(EncodedJSValue val) {
return *(void**)((char*)val.asPtr + JSArrayBufferView__offsetOfVector);
}
@@ -228,6 +234,10 @@ static void* JSVALUE_TO_PTR(EncodedJSValue val) {
return JSVALUE_TO_TYPED_ARRAY_VECTOR(val);
}
if (JSCELL_IS_ARRAY_BUFFER(val)) {
return JSVALUE_TO_ARRAYBUFFER_PTR(val);
}
val.asInt64 -= DoubleEncodeOffset;
size_t ptr = (size_t)val.asDouble;
return (void*)ptr;

View File

@@ -2323,6 +2323,7 @@ const CompilerRT = struct {
JSVALUE_TO_UINT64: *const fn (JSValue0: jsc.JSValue) callconv(.C) u64,
INT64_TO_JSVALUE: *const fn (arg0: *jsc.JSGlobalObject, arg1: i64) callconv(.C) jsc.JSValue,
UINT64_TO_JSVALUE: *const fn (arg0: *jsc.JSGlobalObject, arg1: u64) callconv(.C) jsc.JSValue,
JSVALUE_TO_ARRAYBUFFER_PTR: *const fn (JSValue0: jsc.JSValue) callconv(.C) ?*anyopaque,
bun_call: *const @TypeOf(jsc.C.JSObjectCallAsFunction),
};
const headers = JSValue.exposed_to_ffi;
@@ -2331,6 +2332,7 @@ const CompilerRT = struct {
.JSVALUE_TO_UINT64 = headers.JSVALUE_TO_UINT64,
.INT64_TO_JSVALUE = headers.INT64_TO_JSVALUE,
.UINT64_TO_JSVALUE = headers.UINT64_TO_JSVALUE,
.JSVALUE_TO_ARRAYBUFFER_PTR = headers.JSVALUE_TO_ARRAYBUFFER_PTR,
.bun_call = &jsc.C.JSObjectCallAsFunction,
};
@@ -2369,6 +2371,7 @@ const CompilerRT = struct {
.JSCell__offsetOfType = offsets.JSCell__offsetOfType,
.JSTypeArrayBufferViewMin = @intFromEnum(jsc.JSValue.JSType.min_typed_array),
.JSTypeArrayBufferViewMax = @intFromEnum(jsc.JSValue.JSType.max_typed_array),
.JSTypeArrayBuffer = @intFromEnum(jsc.JSValue.JSType.ArrayBuffer),
});
}
@@ -2382,6 +2385,7 @@ const CompilerRT = struct {
state.addSymbol("JSVALUE_TO_UINT64_SLOW", workaround.JSVALUE_TO_UINT64) catch unreachable;
state.addSymbol("INT64_TO_JSVALUE_SLOW", workaround.INT64_TO_JSVALUE) catch unreachable;
state.addSymbol("UINT64_TO_JSVALUE_SLOW", workaround.UINT64_TO_JSVALUE) catch unreachable;
state.addSymbol("JSVALUE_TO_ARRAYBUFFER_PTR", workaround.JSVALUE_TO_ARRAYBUFFER_PTR) catch unreachable;
}
};

View File

@@ -1124,6 +1124,8 @@ pub const JSValue = enum(i64) {
return null;
}
extern fn JSC__JSValue__toArrayBufferPtr(value: JSValue) callconv(jsc.conv) ?*anyopaque;
extern fn JSC__JSValue__fromInt64NoTruncate(globalObject: *JSGlobalObject, i: i64) JSValue;
/// This always returns a JS BigInt
pub fn fromInt64NoTruncate(globalObject: *JSGlobalObject, i: i64) JSValue {
@@ -2359,6 +2361,7 @@ pub const JSValue = enum(i64) {
pub const JSVALUE_TO_UINT64 = JSValue.JSC__JSValue__toUInt64NoTruncate;
pub const INT64_TO_JSVALUE = JSValue.JSC__JSValue__fromInt64NoTruncate;
pub const UINT64_TO_JSVALUE = JSValue.JSC__JSValue__fromUInt64NoTruncate;
pub const JSVALUE_TO_ARRAYBUFFER_PTR = JSC__JSValue__toArrayBufferPtr;
};
pub const backing_int = @typeInfo(JSValue).@"enum".tag_type;

View File

@@ -4297,6 +4297,15 @@ JSC::JSString* JSC__JSValue__toStringOrNull(JSC::EncodedJSValue JSValue0, JSC::J
return value.toStringOrNull(arg1);
}
void* JSC__JSValue__toArrayBufferPtr(JSC::EncodedJSValue JSValue0)
{
JSC::JSValue value = JSC::JSValue::decode(JSValue0);
if (auto* arrayBuffer = JSC::jsDynamicCast<JSC::JSArrayBuffer*>(value)) {
return arrayBuffer->impl()->data();
}
return nullptr;
}
bool JSC__JSValue__toMatch(JSC::EncodedJSValue regexValue, JSC::JSGlobalObject* global, JSC::EncodedJSValue value)
{
ASSERT_NO_PENDING_EXCEPTION(global);

View File

@@ -289,6 +289,7 @@ CPP_DECL bool JSC__JSValue__toMatch(JSC::EncodedJSValue JSValue0, JSC::JSGlobalO
CPP_DECL JSC::JSObject* JSC__JSValue__toObject(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1);
CPP_DECL JSC::JSString* JSC__JSValue__toString(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1);
CPP_DECL JSC::JSString* JSC__JSValue__toStringOrNull(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1);
CPP_DECL void* JSC__JSValue__toArrayBufferPtr(JSC::EncodedJSValue JSValue0);
CPP_DECL uint64_t JSC__JSValue__toUInt64NoTruncate(JSC::EncodedJSValue JSValue0);
CPP_DECL void JSC__JSValue__toZigException(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, ZigException* arg2);
CPP_DECL void JSC__JSValue__toZigString(JSC::EncodedJSValue JSValue0, ZigString* arg1, JSC::JSGlobalObject* arg2);

View File

@@ -17,6 +17,11 @@
#define ZIG_REPR_TYPE int64_t
#ifdef _WIN32
#define BUN_FFI_IMPORT __declspec(dllimport)
#else
#define BUN_FFI_IMPORT
#endif
// /* 7.18.1.1 Exact-width integer types */
typedef unsigned char uint8_t;
@@ -62,9 +67,9 @@ typedef enum {
napi_detachable_arraybuffer_expected,
napi_would_deadlock // unused
} napi_status;
void* NapiHandleScope__open(void* napi_env, bool detached);
void NapiHandleScope__close(void* napi_env, void* handleScope);
extern struct napi_env__ Bun__thisFFIModuleNapiEnv;
BUN_FFI_IMPORT void* NapiHandleScope__open(void* napi_env, bool detached);
BUN_FFI_IMPORT void NapiHandleScope__close(void* napi_env, void* handleScope);
BUN_FFI_IMPORT extern struct napi_env__ Bun__thisFFIModuleNapiEnv;
#endif
@@ -138,7 +143,7 @@ typedef void* JSContext;
#ifdef IS_CALLBACK
void* callback_ctx;
ZIG_REPR_TYPE FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args);
BUN_FFI_IMPORT ZIG_REPR_TYPE FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args);
// We wrap
static EncodedJSValue _FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args) __attribute__((__always_inline__));
static EncodedJSValue _FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args) {
@@ -177,8 +182,10 @@ static bool JSVALUE_TO_BOOL(EncodedJSValue val) __attribute__((__always_inline__
static uint8_t GET_JSTYPE(EncodedJSValue val) __attribute__((__always_inline__));
static bool JSTYPE_IS_TYPED_ARRAY(uint8_t type) __attribute__((__always_inline__));
static bool JSCELL_IS_TYPED_ARRAY(EncodedJSValue val) __attribute__((__always_inline__));
static bool JSCELL_IS_ARRAY_BUFFER(EncodedJSValue val) __attribute__((__always_inline__));
static void* JSVALUE_TO_TYPED_ARRAY_VECTOR(EncodedJSValue val) __attribute__((__always_inline__));
static uint64_t JSVALUE_TO_TYPED_ARRAY_LENGTH(EncodedJSValue val) __attribute__((__always_inline__));
void* JSVALUE_TO_ARRAYBUFFER_PTR(EncodedJSValue val);
static bool JSVALUE_IS_CELL(EncodedJSValue val) {
return !(val.asInt64 & NotCellMask);
@@ -204,6 +211,10 @@ static bool JSCELL_IS_TYPED_ARRAY(EncodedJSValue val) {
return JSVALUE_IS_CELL(val) && JSTYPE_IS_TYPED_ARRAY(GET_JSTYPE(val));
}
static bool JSCELL_IS_ARRAY_BUFFER(EncodedJSValue val) {
return JSVALUE_IS_CELL(val) && GET_JSTYPE(val) == JSTypeArrayBuffer;
}
static void* JSVALUE_TO_TYPED_ARRAY_VECTOR(EncodedJSValue val) {
return *(void**)((char*)val.asPtr + JSArrayBufferView__offsetOfVector);
}
@@ -225,6 +236,10 @@ static void* JSVALUE_TO_PTR(EncodedJSValue val) {
return JSVALUE_TO_TYPED_ARRAY_VECTOR(val);
}
if (JSCELL_IS_ARRAY_BUFFER(val)) {
return JSVALUE_TO_ARRAYBUFFER_PTR(val);
}
val.asInt64 -= DoubleEncodeOffset;
size_t ptr = (size_t)val.asDouble;
return (void*)ptr;
@@ -350,7 +365,7 @@ static EncodedJSValue INT64_TO_JSVALUE(void* jsGlobalObject, int64_t val) {
}
#ifndef IS_CALLBACK
ZIG_REPR_TYPE JSFunctionCall(void* jsGlobalObject, void* callFrame);
BUN_FFI_IMPORT ZIG_REPR_TYPE JSFunctionCall(void* jsGlobalObject, void* callFrame);
#endif

View File

@@ -17,6 +17,11 @@
#define ZIG_REPR_TYPE int64_t
#ifdef _WIN32
#define BUN_FFI_IMPORT __declspec(dllimport)
#else
#define BUN_FFI_IMPORT
#endif
// /* 7.18.1.1 Exact-width integer types */
typedef unsigned char uint8_t;
@@ -62,9 +67,9 @@ typedef enum {
napi_detachable_arraybuffer_expected,
napi_would_deadlock // unused
} napi_status;
void* NapiHandleScope__open(void* napi_env, bool detached);
void NapiHandleScope__close(void* napi_env, void* handleScope);
extern struct napi_env__ Bun__thisFFIModuleNapiEnv;
BUN_FFI_IMPORT void* NapiHandleScope__open(void* napi_env, bool detached);
BUN_FFI_IMPORT void NapiHandleScope__close(void* napi_env, void* handleScope);
BUN_FFI_IMPORT extern struct napi_env__ Bun__thisFFIModuleNapiEnv;
#endif
@@ -138,7 +143,7 @@ typedef void* JSContext;
#ifdef IS_CALLBACK
void* callback_ctx;
ZIG_REPR_TYPE FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args);
BUN_FFI_IMPORT ZIG_REPR_TYPE FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args);
// We wrap
static EncodedJSValue _FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args) __attribute__((__always_inline__));
static EncodedJSValue _FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args) {
@@ -177,8 +182,10 @@ static bool JSVALUE_TO_BOOL(EncodedJSValue val) __attribute__((__always_inline__
static uint8_t GET_JSTYPE(EncodedJSValue val) __attribute__((__always_inline__));
static bool JSTYPE_IS_TYPED_ARRAY(uint8_t type) __attribute__((__always_inline__));
static bool JSCELL_IS_TYPED_ARRAY(EncodedJSValue val) __attribute__((__always_inline__));
static bool JSCELL_IS_ARRAY_BUFFER(EncodedJSValue val) __attribute__((__always_inline__));
static void* JSVALUE_TO_TYPED_ARRAY_VECTOR(EncodedJSValue val) __attribute__((__always_inline__));
static uint64_t JSVALUE_TO_TYPED_ARRAY_LENGTH(EncodedJSValue val) __attribute__((__always_inline__));
void* JSVALUE_TO_ARRAYBUFFER_PTR(EncodedJSValue val);
static bool JSVALUE_IS_CELL(EncodedJSValue val) {
return !(val.asInt64 & NotCellMask);
@@ -204,6 +211,10 @@ static bool JSCELL_IS_TYPED_ARRAY(EncodedJSValue val) {
return JSVALUE_IS_CELL(val) && JSTYPE_IS_TYPED_ARRAY(GET_JSTYPE(val));
}
static bool JSCELL_IS_ARRAY_BUFFER(EncodedJSValue val) {
return JSVALUE_IS_CELL(val) && GET_JSTYPE(val) == JSTypeArrayBuffer;
}
static void* JSVALUE_TO_TYPED_ARRAY_VECTOR(EncodedJSValue val) {
return *(void**)((char*)val.asPtr + JSArrayBufferView__offsetOfVector);
}
@@ -225,6 +236,10 @@ static void* JSVALUE_TO_PTR(EncodedJSValue val) {
return JSVALUE_TO_TYPED_ARRAY_VECTOR(val);
}
if (JSCELL_IS_ARRAY_BUFFER(val)) {
return JSVALUE_TO_ARRAYBUFFER_PTR(val);
}
val.asInt64 -= DoubleEncodeOffset;
size_t ptr = (size_t)val.asDouble;
return (void*)ptr;
@@ -350,7 +365,7 @@ static EncodedJSValue INT64_TO_JSVALUE(void* jsGlobalObject, int64_t val) {
}
#ifndef IS_CALLBACK
ZIG_REPR_TYPE JSFunctionCall(void* jsGlobalObject, void* callFrame);
BUN_FFI_IMPORT ZIG_REPR_TYPE JSFunctionCall(void* jsGlobalObject, void* callFrame);
#endif

View File

@@ -0,0 +1,42 @@
import { test, expect } from "bun:test";
import { tempDirWithFiles } from "harness";
import { endianness } from "os";
import { FFIType, cc } from "bun:ffi";
test("FFI ArrayBuffer should work as pointer without segfault (issue #22225)", async () => {
const LE = endianness() === "LE";
// Create temp directory with test C code
const dir = tempDirWithFiles("test-ffi-arraybuffer", {
"test.c": `
#include <stdint.h>
uint32_t get(uint32_t* value) {
return *value;
}
`,
});
// Compile C code and get FFI function
const { symbols: { get } } = cc({
source: `${dir}/test.c`,
symbols: {
get: {
args: [FFIType.ptr],
returns: FFIType.u32,
},
},
});
// Create test buffers
const buff = new ArrayBuffer(4);
const tarr = new Uint32Array(buff);
const view = new DataView(buff);
// Set test value
view.setUint32(0, 420, LE);
// Test that all three work correctly
expect(get(view)).toBe(420);
expect(get(tarr)).toBe(420);
expect(get(buff)).toBe(420); // This should not segfault anymore
});