Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
1b4a595ab2 fix(ffi): correct integer boundary encoding for uint32 and int64 values
Fix sign corruption where FFI functions returning uint32_t value 2147483648
(0x80000000) would produce -2147483648 in JavaScript due to incorrect
MAX_INT32 constant and missing uint32 conversion handling.

Changes:
- Fix MAX_INT32 constant from 2147483648 (INT32_MAX+1) to 2147483647 (INT32_MAX)
- Add JSVALUE_TO_UINT32() that handles both int32-tagged and double-encoded JSValues
- Update C codegen to use JSVALUE_TO_UINT32 for uint32_t arguments instead of
  JSVALUE_TO_INT32, fixing round-trip of values > INT32_MAX through FFI calls
- Fix JSVALUE_TO_UINT64 sign-extension: use (uint32_t) intermediate cast to
  prevent negative int32 values from sign-extending to 0xFFFFFFFFFFFFFFFF
- Fix UINT64_TO_JSVALUE boundary: use <= MAX_INT32 to include 2147483647 in
  the int32 fast path

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-12 04:57:38 +00:00
5 changed files with 207 additions and 46 deletions

View File

@@ -89,7 +89,7 @@ BUN_FFI_IMPORT extern struct NapiEnv Bun__thisFFIModuleNapiEnv;
#define TagValueNull (OtherTag)
#define NotCellMask (int64_t)(NumberTag | OtherTag)
#define MAX_INT32 2147483648
#define MAX_INT32 2147483647
#define MAX_INT52 9007199254740991
// If all bits in the mask are set, this indicates an integer number,
@@ -174,6 +174,7 @@ static EncodedJSValue PTR_TO_JSVALUE(void* ptr) __attribute__((__always_inline__
static void* JSVALUE_TO_PTR(EncodedJSValue val) __attribute__((__always_inline__));
static int32_t JSVALUE_TO_INT32(EncodedJSValue val) __attribute__((__always_inline__));
static uint32_t JSVALUE_TO_UINT32(EncodedJSValue val) __attribute__((__always_inline__));
static float JSVALUE_TO_FLOAT(EncodedJSValue val) __attribute__((__always_inline__));
static double JSVALUE_TO_DOUBLE(EncodedJSValue val) __attribute__((__always_inline__));
static bool JSVALUE_TO_BOOL(EncodedJSValue val) __attribute__((__always_inline__));
@@ -260,6 +261,16 @@ static int32_t JSVALUE_TO_INT32(EncodedJSValue val) {
return val.asInt64;
}
static uint32_t JSVALUE_TO_UINT32(EncodedJSValue val) {
if (JSVALUE_IS_INT32(val)) {
return (uint32_t)JSVALUE_TO_INT32(val);
}
if (JSVALUE_IS_NUMBER(val)) {
return (uint32_t)JSVALUE_TO_DOUBLE(val);
}
return 0;
}
static EncodedJSValue INT32_TO_JSVALUE(int32_t val) {
EncodedJSValue res;
res.asInt64 = NumberTag | (uint32_t)val;
@@ -306,7 +317,7 @@ static bool JSVALUE_TO_BOOL(EncodedJSValue val) {
static uint64_t JSVALUE_TO_UINT64(EncodedJSValue value) {
if (JSVALUE_IS_INT32(value)) {
return (uint64_t)JSVALUE_TO_INT32(value);
return (uint64_t)(uint32_t)JSVALUE_TO_INT32(value);
}
if (JSVALUE_IS_NUMBER(value)) {
@@ -332,11 +343,11 @@ static int64_t JSVALUE_TO_INT64(EncodedJSValue value) {
}
static EncodedJSValue UINT64_TO_JSVALUE(void* jsGlobalObject, uint64_t val) {
if (val < MAX_INT32) {
if (val <= MAX_INT32) {
return INT32_TO_JSVALUE((int32_t)val);
}
if (val < MAX_INT52) {
if (val <= MAX_INT52) {
return DOUBLE_TO_JSVALUE((double)val);
}

View File

@@ -2032,7 +2032,8 @@ pub const FFI = struct {
/// Types that we can directly pass through as an `int64_t`
pub fn needsACastInC(this: ABIType) bool {
return switch (this) {
.char, .int8_t, .uint8_t, .int16_t, .uint16_t, .int32_t, .uint32_t => false,
.char, .int8_t, .uint8_t, .int16_t, .uint16_t, .int32_t => false,
.uint32_t => true,
else => true,
};
}
@@ -2143,12 +2144,18 @@ pub const FFI = struct {
try writer.writeAll("(bool)");
try writer.writeAll("JSVALUE_TO_BOOL(");
},
.char, .int8_t, .uint8_t, .int16_t, .uint16_t, .int32_t, .uint32_t => {
.char, .int8_t, .uint8_t, .int16_t, .uint16_t, .int32_t => {
if (self.exact)
try writer.print("({s})", .{bun.asByteSlice(@tagName(self.tag))});
try writer.writeAll("JSVALUE_TO_INT32(");
},
.uint32_t => {
if (self.exact)
try writer.writeAll("(uint32_t)");
try writer.writeAll("JSVALUE_TO_UINT32(");
},
.i64_fast, .int64_t => {
if (self.exact)
try writer.writeAll("(int64_t)");
@@ -2297,10 +2304,8 @@ pub const FFI = struct {
.uint8_t => "uint8_t",
.int16_t => "int16_t",
.uint16_t => "uint16_t",
// see the comment in ffi.ts about why `uint32_t` acts as `int32_t`
.int32_t,
.uint32_t,
=> "int32_t",
.int32_t => "int32_t",
.uint32_t => "uint32_t",
.i64_fast, .int64_t => "int64_t",
.u64_fast, .uint64_t => "uint64_t",
.double => "double",

View File

@@ -0,0 +1,107 @@
import { cc } from "bun:ffi";
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import { isASAN, tempDirWithFiles } from "harness";
import path from "path";
// TinyCC's setjmp/longjmp error handling conflicts with ASan.
describe.skipIf(isASAN)("FFI integer boundary values", () => {
const source = /* c */ `
#include <stdint.h>
// 2147483648 is INT32_MAX + 1 (0x80000000)
uint32_t returns_uint32_boundary(void) {
return 2147483648u;
}
// INT32_MAX = 2147483647
uint32_t returns_uint32_max_int32(void) {
return 2147483647u;
}
// UINT32_MAX
uint32_t returns_uint32_max(void) {
return 4294967295u;
}
// Return value that's exactly at the boundary for int64 fast path
int64_t returns_int64_boundary_pos(void) {
return 2147483648ll;
}
int64_t returns_int64_boundary_neg(void) {
return -2147483649ll;
}
// Identity functions for round-tripping
uint32_t identity_u32(uint32_t val) {
return val;
}
`;
let dir: string;
beforeAll(() => {
dir = tempDirWithFiles("bun-ffi-int-boundary", {
"boundary.c": source,
});
});
describe("uint32 boundary at INT32_MAX+1", () => {
let lib: any;
beforeAll(() => {
lib = cc({
source: path.join(dir, "boundary.c"),
symbols: {
returns_uint32_boundary: { args: [], returns: "uint32_t" },
returns_uint32_max_int32: { args: [], returns: "uint32_t" },
returns_uint32_max: { args: [], returns: "uint32_t" },
returns_int64_boundary_pos: { args: [], returns: "i64_fast" },
returns_int64_boundary_neg: { args: [], returns: "i64_fast" },
identity_u32: { args: ["uint32_t"], returns: "uint32_t" },
},
});
});
afterAll(() => {
lib?.close();
});
it("uint32 value 2147483648 should not become negative", () => {
// This is the core bug: 2147483648 (0x80000000) was being routed through
// the int32 encoding path, causing sign corruption to -2147483648
const result = lib.symbols.returns_uint32_boundary();
expect(result).toBe(2147483648);
expect(result).toBeGreaterThan(0);
});
it("uint32 value 2147483647 (INT32_MAX) should work correctly", () => {
const result = lib.symbols.returns_uint32_max_int32();
expect(result).toBe(2147483647);
});
it("uint32 value 4294967295 (UINT32_MAX) should work correctly", () => {
const result = lib.symbols.returns_uint32_max();
expect(result).toBe(4294967295);
});
it("int64_fast value 2147483648 should not become negative", () => {
// INT64_TO_JSVALUE was casting 2147483648 to int32_t causing UB/sign corruption
const result = lib.symbols.returns_int64_boundary_pos();
expect(result).toBe(2147483648);
expect(result).toBeGreaterThan(0);
});
it("int64_fast value -2147483649 should be correctly negative", () => {
const result = lib.symbols.returns_int64_boundary_neg();
expect(result).toBe(-2147483649);
expect(result).toBeLessThan(-2147483648);
});
it("round-trip uint32 value 2147483648 through identity function", () => {
const result = lib.symbols.identity_u32(2147483648);
expect(result).toBe(2147483648);
expect(result).toBeGreaterThan(0);
});
});
});

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;
@@ -36,7 +41,7 @@ typedef _Bool bool;
#define false 0
#ifndef SRC_JS_NATIVE_API_TYPES_H_
typedef struct napi_env__ *napi_env;
typedef struct NapiEnv *napi_env;
typedef int64_t napi_value;
typedef enum {
napi_ok,
@@ -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 NapiEnv Bun__thisFFIModuleNapiEnv;
#endif
@@ -86,7 +91,7 @@ extern struct napi_env__ Bun__thisFFIModuleNapiEnv;
#define TagValueNull (OtherTag)
#define NotCellMask (int64_t)(NumberTag | OtherTag)
#define MAX_INT32 2147483648
#define MAX_INT32 2147483647
#define MAX_INT52 9007199254740991
// If all bits in the mask are set, this indicates an integer number,
@@ -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) {
@@ -171,6 +176,7 @@ static EncodedJSValue PTR_TO_JSVALUE(void* ptr) __attribute__((__always_inline__
static void* JSVALUE_TO_PTR(EncodedJSValue val) __attribute__((__always_inline__));
static int32_t JSVALUE_TO_INT32(EncodedJSValue val) __attribute__((__always_inline__));
static uint32_t JSVALUE_TO_UINT32(EncodedJSValue val) __attribute__((__always_inline__));
static float JSVALUE_TO_FLOAT(EncodedJSValue val) __attribute__((__always_inline__));
static double JSVALUE_TO_DOUBLE(EncodedJSValue val) __attribute__((__always_inline__));
static bool JSVALUE_TO_BOOL(EncodedJSValue val) __attribute__((__always_inline__));
@@ -222,23 +228,26 @@ static void* JSVALUE_TO_PTR(EncodedJSValue val) {
return 0;
if (JSCELL_IS_TYPED_ARRAY(val)) {
return JSVALUE_TO_TYPED_ARRAY_VECTOR(val);
return JSVALUE_TO_TYPED_ARRAY_VECTOR(val);
}
if (JSVALUE_IS_INT32(val)) {
return (void*)(uintptr_t)JSVALUE_TO_INT32(val);
}
// Assume the JSValue is a double
val.asInt64 -= DoubleEncodeOffset;
size_t ptr = (size_t)val.asDouble;
return (void*)ptr;
return (void*)(uintptr_t)val.asDouble;
}
static EncodedJSValue PTR_TO_JSVALUE(void* ptr) {
EncodedJSValue val;
if (ptr == 0)
{
val.asInt64 = TagValueNull;
return val;
if (ptr == 0) {
val.asInt64 = TagValueNull;
return val;
}
val.asDouble = (double)(size_t)ptr;
val.asDouble = (double)(uintptr_t)ptr;
val.asInt64 += DoubleEncodeOffset;
return val;
}
@@ -254,6 +263,16 @@ static int32_t JSVALUE_TO_INT32(EncodedJSValue val) {
return val.asInt64;
}
static uint32_t JSVALUE_TO_UINT32(EncodedJSValue val) {
if (JSVALUE_IS_INT32(val)) {
return (uint32_t)JSVALUE_TO_INT32(val);
}
if (JSVALUE_IS_NUMBER(val)) {
return (uint32_t)JSVALUE_TO_DOUBLE(val);
}
return 0;
}
static EncodedJSValue INT32_TO_JSVALUE(int32_t val) {
EncodedJSValue res;
res.asInt64 = NumberTag | (uint32_t)val;
@@ -300,7 +319,7 @@ static bool JSVALUE_TO_BOOL(EncodedJSValue val) {
static uint64_t JSVALUE_TO_UINT64(EncodedJSValue value) {
if (JSVALUE_IS_INT32(value)) {
return (uint64_t)JSVALUE_TO_INT32(value);
return (uint64_t)(uint32_t)JSVALUE_TO_INT32(value);
}
if (JSVALUE_IS_NUMBER(value)) {
@@ -326,11 +345,11 @@ static int64_t JSVALUE_TO_INT64(EncodedJSValue value) {
}
static EncodedJSValue UINT64_TO_JSVALUE(void* jsGlobalObject, uint64_t val) {
if (val < MAX_INT32) {
if (val <= MAX_INT32) {
return INT32_TO_JSVALUE((int32_t)val);
}
if (val < MAX_INT52) {
if (val <= MAX_INT52) {
return DOUBLE_TO_JSVALUE((double)val);
}
@@ -350,7 +369,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;
@@ -36,7 +41,7 @@ typedef _Bool bool;
#define false 0
#ifndef SRC_JS_NATIVE_API_TYPES_H_
typedef struct napi_env__ *napi_env;
typedef struct NapiEnv *napi_env;
typedef int64_t napi_value;
typedef enum {
napi_ok,
@@ -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 NapiEnv Bun__thisFFIModuleNapiEnv;
#endif
@@ -86,7 +91,7 @@ extern struct napi_env__ Bun__thisFFIModuleNapiEnv;
#define TagValueNull (OtherTag)
#define NotCellMask (int64_t)(NumberTag | OtherTag)
#define MAX_INT32 2147483648
#define MAX_INT32 2147483647
#define MAX_INT52 9007199254740991
// If all bits in the mask are set, this indicates an integer number,
@@ -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) {
@@ -171,6 +176,7 @@ static EncodedJSValue PTR_TO_JSVALUE(void* ptr) __attribute__((__always_inline__
static void* JSVALUE_TO_PTR(EncodedJSValue val) __attribute__((__always_inline__));
static int32_t JSVALUE_TO_INT32(EncodedJSValue val) __attribute__((__always_inline__));
static uint32_t JSVALUE_TO_UINT32(EncodedJSValue val) __attribute__((__always_inline__));
static float JSVALUE_TO_FLOAT(EncodedJSValue val) __attribute__((__always_inline__));
static double JSVALUE_TO_DOUBLE(EncodedJSValue val) __attribute__((__always_inline__));
static bool JSVALUE_TO_BOOL(EncodedJSValue val) __attribute__((__always_inline__));
@@ -222,23 +228,26 @@ static void* JSVALUE_TO_PTR(EncodedJSValue val) {
return 0;
if (JSCELL_IS_TYPED_ARRAY(val)) {
return JSVALUE_TO_TYPED_ARRAY_VECTOR(val);
return JSVALUE_TO_TYPED_ARRAY_VECTOR(val);
}
if (JSVALUE_IS_INT32(val)) {
return (void*)(uintptr_t)JSVALUE_TO_INT32(val);
}
// Assume the JSValue is a double
val.asInt64 -= DoubleEncodeOffset;
size_t ptr = (size_t)val.asDouble;
return (void*)ptr;
return (void*)(uintptr_t)val.asDouble;
}
static EncodedJSValue PTR_TO_JSVALUE(void* ptr) {
EncodedJSValue val;
if (ptr == 0)
{
val.asInt64 = TagValueNull;
return val;
if (ptr == 0) {
val.asInt64 = TagValueNull;
return val;
}
val.asDouble = (double)(size_t)ptr;
val.asDouble = (double)(uintptr_t)ptr;
val.asInt64 += DoubleEncodeOffset;
return val;
}
@@ -254,6 +263,16 @@ static int32_t JSVALUE_TO_INT32(EncodedJSValue val) {
return val.asInt64;
}
static uint32_t JSVALUE_TO_UINT32(EncodedJSValue val) {
if (JSVALUE_IS_INT32(val)) {
return (uint32_t)JSVALUE_TO_INT32(val);
}
if (JSVALUE_IS_NUMBER(val)) {
return (uint32_t)JSVALUE_TO_DOUBLE(val);
}
return 0;
}
static EncodedJSValue INT32_TO_JSVALUE(int32_t val) {
EncodedJSValue res;
res.asInt64 = NumberTag | (uint32_t)val;
@@ -300,7 +319,7 @@ static bool JSVALUE_TO_BOOL(EncodedJSValue val) {
static uint64_t JSVALUE_TO_UINT64(EncodedJSValue value) {
if (JSVALUE_IS_INT32(value)) {
return (uint64_t)JSVALUE_TO_INT32(value);
return (uint64_t)(uint32_t)JSVALUE_TO_INT32(value);
}
if (JSVALUE_IS_NUMBER(value)) {
@@ -326,11 +345,11 @@ static int64_t JSVALUE_TO_INT64(EncodedJSValue value) {
}
static EncodedJSValue UINT64_TO_JSVALUE(void* jsGlobalObject, uint64_t val) {
if (val < MAX_INT32) {
if (val <= MAX_INT32) {
return INT32_TO_JSVALUE((int32_t)val);
}
if (val < MAX_INT52) {
if (val <= MAX_INT52) {
return DOUBLE_TO_JSVALUE((double)val);
}
@@ -350,7 +369,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