Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/bun.js/bindings/InternalForTesting.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,35 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_lsanDoLeakCheck, (JSC::JSGlobalObject * glob
return encodedJSUndefined();
}

extern "C" bool JSC__isBigIntInInt64Range(JSC::EncodedJSValue value, int64_t min, int64_t max);
extern "C" bool JSC__isBigIntInUInt64Range(JSC::EncodedJSValue value, uint64_t min, uint64_t max);

// For testing JSC__isBigIntInInt64Range / JSC__isBigIntInUInt64Range from JS.
// args: (bigint, min: number|bigint, max: number|bigint, unsigned: boolean)
JSC_DEFINE_HOST_FUNCTION(jsFunction_isBigIntInRange, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);

JSValue value = callFrame->argument(0);
JSValue minArg = callFrame->argument(1);
JSValue maxArg = callFrame->argument(2);
bool isUnsigned = callFrame->argument(3).toBoolean(globalObject);
RETURN_IF_EXCEPTION(scope, {});

if (isUnsigned) {
uint64_t min = minArg.toBigUInt64(globalObject);
RETURN_IF_EXCEPTION(scope, {});
uint64_t max = maxArg.toBigUInt64(globalObject);
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(jsBoolean(JSC__isBigIntInUInt64Range(JSValue::encode(value), min, max)));
}

int64_t min = minArg.toBigInt64(globalObject);
RETURN_IF_EXCEPTION(scope, {});
int64_t max = maxArg.toBigInt64(globalObject);
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(jsBoolean(JSC__isBigIntInInt64Range(JSValue::encode(value), min, max)));
}

}
1 change: 1 addition & 0 deletions src/bun.js/bindings/InternalForTesting.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ namespace Bun {
JSC_DECLARE_HOST_FUNCTION(jsFunction_arrayBufferViewHasBuffer);
JSC_DECLARE_HOST_FUNCTION(jsFunction_hasReifiedStatic);
JSC_DECLARE_HOST_FUNCTION(jsFunction_lsanDoLeakCheck);
JSC_DECLARE_HOST_FUNCTION(jsFunction_isBigIntInRange);

}
26 changes: 12 additions & 14 deletions src/bun.js/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5265,34 +5265,32 @@ extern "C" void JSC__JSValue__forEachPropertyNonIndexed(JSC::EncodedJSValue JSVa
JSC__JSValue__forEachPropertyImpl<true>(JSValue0, globalObject, arg2, iter);
}

extern "C" [[ZIG_EXPORT(nothrow)]] bool JSC__isBigIntInUInt64Range(JSC::EncodedJSValue value, uint64_t max, uint64_t min)
extern "C" [[ZIG_EXPORT(nothrow)]] bool JSC__isBigIntInUInt64Range(JSC::EncodedJSValue value, uint64_t min, uint64_t max)
{
JSValue jsValue = JSValue::decode(value);
if (!jsValue.isHeapBigInt())
return false;

JSC::JSBigInt* bigInt = jsValue.asHeapBigInt();
auto result = bigInt->compare(bigInt, min);
if (result == JSBigInt::ComparisonResult::GreaterThan || result == JSBigInt::ComparisonResult::Equal) {
return true;
}
result = bigInt->compare(bigInt, max);
return result == JSBigInt::ComparisonResult::LessThan || result == JSBigInt::ComparisonResult::Equal;
auto result = JSBigInt::compare(bigInt, min);
if (result == JSBigInt::ComparisonResult::LessThan)
return false;
result = JSBigInt::compare(bigInt, max);
return result != JSBigInt::ComparisonResult::GreaterThan;
}

extern "C" [[ZIG_EXPORT(nothrow)]] bool JSC__isBigIntInInt64Range(JSC::EncodedJSValue value, int64_t max, int64_t min)
extern "C" [[ZIG_EXPORT(nothrow)]] bool JSC__isBigIntInInt64Range(JSC::EncodedJSValue value, int64_t min, int64_t max)
{
JSValue jsValue = JSValue::decode(value);
if (!jsValue.isHeapBigInt())
return false;

JSC::JSBigInt* bigInt = jsValue.asHeapBigInt();
auto result = bigInt->compare(bigInt, min);
if (result == JSBigInt::ComparisonResult::GreaterThan || result == JSBigInt::ComparisonResult::Equal) {
return true;
}
result = bigInt->compare(bigInt, max);
return result == JSBigInt::ComparisonResult::LessThan || result == JSBigInt::ComparisonResult::Equal;
auto result = JSBigInt::compare(bigInt, min);
if (result == JSBigInt::ComparisonResult::LessThan)
return false;
result = JSBigInt::compare(bigInt, max);
return result != JSBigInt::ComparisonResult::GreaterThan;
}

[[ZIG_EXPORT(check_slow)]] void JSC__JSValue__forEachPropertyOrdered(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject, void* arg2, void (*iter)([[ZIG_NONNULL]] JSC::JSGlobalObject* arg0, void* ctx, [[ZIG_NONNULL]] ZigString* arg2, JSC::EncodedJSValue JSValue3, bool isSymbol, bool isPrivateSymbol))
Expand Down
6 changes: 6 additions & 0 deletions src/js/internal-for-testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@ export const getDevServerDeinitCount = $bindgenFn("DevServer.bind.ts", "getDeini
export const getCounters = $newZigFunction("Counters.zig", "createCountersObject", 0);
export const hasNonReifiedStatic = $newCppFunction("InternalForTesting.cpp", "jsFunction_hasReifiedStatic", 1);

export const isBigIntInRange: (value: bigint, min: bigint, max: bigint, unsigned: boolean) => boolean = $newCppFunction(
"InternalForTesting.cpp",
"jsFunction_isBigIntInRange",
4,
);

interface setSocketOptionsFn {
(socket: Bun.Socket, sendBuffer: 1, size: number): void;
(socket: Bun.Socket, recvBuffer: 2, size: number): void;
Expand Down
85 changes: 85 additions & 0 deletions test/js/bun/jsc/bigint-range-check.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { isBigIntInRange } from "bun:internal-for-testing";
import { describe, expect, test } from "bun:test";

// Regression test for JSC__isBigIntInInt64Range / JSC__isBigIntInUInt64Range.
// Previously the C++ implementation had swapped (min, max) parameter names AND
// used OR-short-circuit logic, causing the function to return true when the
// value was OUTSIDE the range and false when it was INSIDE.
// This affected MySQL BigInt parameter binding (all in-range values were
// rejected with ERR_OUT_OF_RANGE).

const I64_MIN = -9223372036854775808n;
const I64_MAX = 9223372036854775807n;
const U64_MAX = 18446744073709551615n;

describe("isBigIntInInt64Range (signed)", () => {
test("100n is in [i64_min, i64_max]", () => {
expect(isBigIntInRange(100n, I64_MIN, I64_MAX, false)).toBe(true);
});

test("0n is in [i64_min, i64_max]", () => {
expect(isBigIntInRange(0n, I64_MIN, I64_MAX, false)).toBe(true);
});

test("-100n is in [i64_min, i64_max]", () => {
expect(isBigIntInRange(-100n, I64_MIN, I64_MAX, false)).toBe(true);
});

test("i64_max boundary is in range", () => {
expect(isBigIntInRange(I64_MAX, I64_MIN, I64_MAX, false)).toBe(true);
});

test("i64_min boundary is in range", () => {
expect(isBigIntInRange(I64_MIN, I64_MIN, I64_MAX, false)).toBe(true);
});

test("i64_max + 1 is out of range", () => {
expect(isBigIntInRange(I64_MAX + 1n, I64_MIN, I64_MAX, false)).toBe(false);
});

test("i64_min - 1 is out of range", () => {
expect(isBigIntInRange(I64_MIN - 1n, I64_MIN, I64_MAX, false)).toBe(false);
});

test("very large positive bigint is out of range", () => {
expect(isBigIntInRange(2n ** 128n, I64_MIN, I64_MAX, false)).toBe(false);
});

test("very large negative bigint is out of range", () => {
expect(isBigIntInRange(-(2n ** 128n), I64_MIN, I64_MAX, false)).toBe(false);
});

test("narrow range [0, 100]", () => {
expect(isBigIntInRange(50n, 0n, 100n, false)).toBe(true);
expect(isBigIntInRange(0n, 0n, 100n, false)).toBe(true);
expect(isBigIntInRange(100n, 0n, 100n, false)).toBe(true);
expect(isBigIntInRange(-1n, 0n, 100n, false)).toBe(false);
expect(isBigIntInRange(101n, 0n, 100n, false)).toBe(false);
});
});

describe("isBigIntInUInt64Range (unsigned)", () => {
test("100n is in [0, u64_max]", () => {
expect(isBigIntInRange(100n, 0n, U64_MAX, true)).toBe(true);
});

test("0n is in [0, u64_max]", () => {
expect(isBigIntInRange(0n, 0n, U64_MAX, true)).toBe(true);
});

test("u64_max boundary is in range", () => {
expect(isBigIntInRange(U64_MAX, 0n, U64_MAX, true)).toBe(true);
});

test("u64_max + 1 is out of range", () => {
expect(isBigIntInRange(U64_MAX + 1n, 0n, U64_MAX, true)).toBe(false);
});

test("-1n is out of [0, u64_max]", () => {
expect(isBigIntInRange(-1n, 0n, U64_MAX, true)).toBe(false);
});

test("very large bigint is out of range", () => {
expect(isBigIntInRange(2n ** 128n, 0n, U64_MAX, true)).toBe(false);
});
});
51 changes: 47 additions & 4 deletions test/js/sql/sql-mysql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,9 +483,7 @@ if (isDockerEnabled()) {
test("Binary", async () => {
const random_name = ("t_" + Bun.randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (a binary(1), b varbinary(1), c blob)`;
const values = [
{ a: Buffer.from([1]), b: Buffer.from([2]), c: Buffer.from([3]) },
];
const values = [{ a: Buffer.from([1]), b: Buffer.from([2]), c: Buffer.from([3]) }];
await sql`INSERT INTO ${sql(random_name)} ${sql(values)}`;
const results = await sql`select * from ${sql(random_name)}`;
// return buffers
Expand All @@ -497,7 +495,7 @@ if (isDockerEnabled()) {
expect(results2[0].a).toEqual(Buffer.from([1]));
expect(results2[0].b).toEqual(Buffer.from([2]));
expect(results2[0].c).toEqual(Buffer.from([3]));
})
});

test("bulk insert nested sql()", async () => {
await using sql = new SQL({ ...getOptions(), max: 1 });
Expand Down Expand Up @@ -966,6 +964,51 @@ if (isDockerEnabled()) {
expect((await sql`select 9223372036854777 as x`)[0].x).toBe(9223372036854777n);
});

// Regression: previously any in-range BigInt parameter would throw
// ERR_OUT_OF_RANGE due to inverted logic in JSC__isBigIntInInt64Range.
describe("bigint parameter binding", () => {
test("binds BigInt values into BIGINT columns without ERR_OUT_OF_RANGE", async () => {
await using sql = new SQL({ ...getOptions(), bigint: true });
const table = "t_" + randomUUIDv7("hex").replaceAll("-", "");
await sql`CREATE TEMPORARY TABLE ${sql(table)} (s BIGINT, u BIGINT UNSIGNED)`;

// These should NOT throw ERR_OUT_OF_RANGE (the bug was that they all did)
await sql`INSERT INTO ${sql(table)} (s) VALUES (${100n})`;
await sql`INSERT INTO ${sql(table)} (s) VALUES (${0n})`;
await sql`INSERT INTO ${sql(table)} (s) VALUES (${-1n})`;
await sql`INSERT INTO ${sql(table)} (s) VALUES (${9223372036854775807n})`;
await sql`INSERT INTO ${sql(table)} (s) VALUES (${-9223372036854775808n})`;
await sql`INSERT INTO ${sql(table)} (u) VALUES (${0n})`;
await sql`INSERT INTO ${sql(table)} (u) VALUES (${9223372036854775808n})`;
await sql`INSERT INTO ${sql(table)} (u) VALUES (${18446744073709551615n})`;

// Verify i64 boundary values round-trip correctly as BigInt
const s = (await sql`SELECT s FROM ${sql(table)} WHERE s IS NOT NULL ORDER BY s`).map(r => r.s);
expect(s).toHaveLength(5);
expect(s[0]).toBe(-9223372036854775808n);
expect(s[s.length - 1]).toBe(9223372036854775807n);

// Verify u64 boundary values round-trip correctly
const u = (await sql`SELECT u FROM ${sql(table)} WHERE u IS NOT NULL ORDER BY u`).map(r => r.u);
expect(u).toHaveLength(3);
expect(u[u.length - 1]).toBe(18446744073709551615n);
});

test("throws ERR_OUT_OF_RANGE for BigInt exceeding u64", async () => {
await using sql = new SQL({ ...getOptions() });
await expect(sql`select ${18446744073709551616n} as x`).rejects.toMatchObject({
code: "ERR_OUT_OF_RANGE",
});
});

test("throws ERR_OUT_OF_RANGE for BigInt below i64 min", async () => {
await using sql = new SQL({ ...getOptions() });
await expect(sql`select ${-9223372036854775809n} as x`).rejects.toMatchObject({
code: "ERR_OUT_OF_RANGE",
});
});
});

test("int is returned as Number", async () => {
expect((await sql`select CAST(123 AS SIGNED) as x`)[0].x).toBe(123);
});
Expand Down
Loading