Fix bounds check in Buffer writeBigInt64/writeBigUInt64 methods (#23781)

## Summary

Fixed an unsigned integer underflow in the bounds check for
`writeBigInt64LE`, `writeBigInt64BE`, `writeBigUInt64LE`, and
`writeBigUInt64BE` methods.

## Problem

When `byteLength < 8`, the bounds check `offset > byteLength - 8` would
cause unsigned integer underflow (since both are `size_t`), resulting in
a large positive number that would pass the check. This allowed
out-of-bounds writes and caused ASAN use-after-poison errors.

**Reproduction:**
```js
const buf = Buffer.from("Hello World");
const slice = buf.slice(0, 5);
slice.writeBigUInt64BE(4096n, 10000); // ASAN error!
```

## Solution

Added an explicit `byteLength < 8` check before the subtraction to
prevent the underflow. The fix is applied to all four functions:
- `writeBigInt64LE` (src/bun.js/bindings/JSBuffer.cpp:2464)
- `writeBigInt64BE` (src/bun.js/bindings/JSBuffer.cpp:2504)
- `writeBigUInt64LE` (src/bun.js/bindings/JSBuffer.cpp:2543)
- `writeBigUInt64BE` (src/bun.js/bindings/JSBuffer.cpp:2582)

## Test plan

- Added comprehensive regression tests covering all edge cases
- Verified the original reproduction case now throws a proper RangeError
instead of crashing
- All tests pass

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

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
robobun
2025-10-18 16:52:07 -07:00
committed by GitHub
parent 6ee9dac50f
commit 2ebf6c16b6
2 changed files with 132 additions and 48 deletions

View File

@@ -2218,6 +2218,64 @@ extern "C" JSC_DECLARE_JIT_OPERATION_WITHOUT_WTF_INTERNAL(jsBufferConstructorAll
extern "C" JSC_DECLARE_JIT_OPERATION_WITHOUT_WTF_INTERNAL(jsBufferConstructorAllocUnsafeWithoutTypeChecks, JSUint8Array*, (JSC::JSGlobalObject * lexicalGlobalObject, void* thisValue, int size));
extern "C" JSC_DECLARE_JIT_OPERATION_WITHOUT_WTF_INTERNAL(jsBufferConstructorAllocUnsafeSlowWithoutTypeChecks, JSUint8Array*, (JSC::JSGlobalObject * lexicalGlobalObject, void* thisValue, int size));
static size_t validateOffsetBigInt64(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSValue offsetVal, size_t byteLength)
{
if (byteLength < 8) [[unlikely]] {
auto* error = Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_BUFFER_OUT_OF_BOUNDS, "Attempt to access memory outside buffer bounds"_s);
scope.throwException(lexicalGlobalObject, error);
return 0;
}
if (offsetVal.isUndefined()) {
return 0;
}
size_t offset;
size_t maxOffset = byteLength - 8;
if (offsetVal.isInt32()) {
int32_t offsetI = offsetVal.asInt32();
if (offsetI < 0) [[unlikely]] {
Bun::ERR::BUFFER_OUT_OF_BOUNDS(scope, lexicalGlobalObject, "offset"_s);
return 0;
}
offset = static_cast<size_t>(offsetI);
if (offset > maxOffset) [[unlikely]] {
Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, 0, maxOffset, offsetVal);
return 0;
}
return offset;
}
if (!offsetVal.isNumber()) [[unlikely]] {
Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetVal);
return 0;
}
auto offsetD = offsetVal.asNumber();
if (offsetD < 0) [[unlikely]] {
Bun::ERR::BUFFER_OUT_OF_BOUNDS(scope, lexicalGlobalObject, "offset"_s);
return 0;
}
if (std::fmod(offsetD, 1.0) != 0) [[unlikely]] {
Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, "an integer"_s, offsetVal);
return 0;
}
offset = static_cast<size_t>(offsetD);
if (offset > maxOffset) [[unlikely]] {
Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, 0, maxOffset, offsetVal);
return 0;
}
return offset;
}
JSC_DEFINE_JIT_OPERATION(jsBufferConstructorAllocWithoutTypeChecks, JSUint8Array*, (JSC::JSGlobalObject * lexicalGlobalObject, void* thisValue, int byteLength))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
@@ -2452,18 +2510,8 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigInt64LE, (JSGlobalObj
if (bigint->sign() && limb - 0x8000000000000000 > 0x7fffffffffffffff) return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "value"_s, ">= -(2n ** 63n) and < 2n ** 63n"_s, valueVal);
int64_t value = static_cast<int64_t>(limb);
if (offsetVal.isUndefined()) offsetVal = jsNumber(0);
if (!offsetVal.isNumber()) [[unlikely]]
return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetVal);
auto offsetD = offsetVal.asNumber();
if (std::fmod(offsetD, 1.0) != 0) [[unlikely]]
return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, "an integer"_s, offsetVal);
size_t offset = offsetD;
if (offset < 0) [[unlikely]]
return Bun::ERR::BUFFER_OUT_OF_BOUNDS(scope, lexicalGlobalObject, "offset"_s);
if (offset > byteLength - 8) [[unlikely]]
return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, 0, byteLength - 8, offsetVal);
size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength);
RETURN_IF_EXCEPTION(scope, {});
write_int64_le(static_cast<uint8_t*>(castedThis->vector()) + offset, value);
return JSValue::encode(jsNumber(offset + 8));
}
@@ -2492,18 +2540,8 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigInt64BE, (JSGlobalObj
if (bigint->sign() && limb - 0x8000000000000000 > 0x7fffffffffffffff) return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "value"_s, ">= -(2n ** 63n) and < 2n ** 63n"_s, valueVal);
int64_t value = static_cast<int64_t>(limb);
if (offsetVal.isUndefined()) offsetVal = jsNumber(0);
if (!offsetVal.isNumber()) [[unlikely]]
return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetVal);
auto offsetD = offsetVal.asNumber();
if (std::fmod(offsetD, 1.0) != 0) [[unlikely]]
return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, "an integer"_s, offsetVal);
size_t offset = offsetD;
if (offset < 0) [[unlikely]]
return Bun::ERR::BUFFER_OUT_OF_BOUNDS(scope, lexicalGlobalObject, "offset"_s);
if (offset > byteLength - 8) [[unlikely]]
return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, 0, byteLength - 8, offsetVal);
size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength);
RETURN_IF_EXCEPTION(scope, {});
write_int64_be(static_cast<uint8_t*>(castedThis->vector()) + offset, value);
return JSValue::encode(jsNumber(offset + 8));
}
@@ -2531,18 +2569,8 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64LE, (JSGlobalOb
uint64_t value = valueVal.toBigUInt64(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
if (offsetVal.isUndefined()) offsetVal = jsNumber(0);
if (!offsetVal.isNumber()) [[unlikely]]
return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetVal);
auto offsetD = offsetVal.asNumber();
if (std::fmod(offsetD, 1.0) != 0) [[unlikely]]
return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, "an integer"_s, offsetVal);
size_t offset = offsetD;
if (offset < 0) [[unlikely]]
return Bun::ERR::BUFFER_OUT_OF_BOUNDS(scope, lexicalGlobalObject, "offset"_s);
if (offset > byteLength - 8) [[unlikely]]
return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, 0, byteLength - 8, offsetVal);
size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength);
RETURN_IF_EXCEPTION(scope, {});
write_int64_le(static_cast<uint8_t*>(castedThis->vector()) + offset, value);
return JSValue::encode(jsNumber(offset + 8));
}
@@ -2570,18 +2598,8 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64BE, (JSGlobalOb
uint64_t value = valueVal.toBigUInt64(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
if (offsetVal.isUndefined()) offsetVal = jsNumber(0);
if (!offsetVal.isNumber()) [[unlikely]]
return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetVal);
auto offsetD = offsetVal.asNumber();
if (std::fmod(offsetD, 1.0) != 0) [[unlikely]]
return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, "an integer"_s, offsetVal);
size_t offset = offsetD;
if (offset < 0) [[unlikely]]
return Bun::ERR::BUFFER_OUT_OF_BOUNDS(scope, lexicalGlobalObject, "offset"_s);
if (offset > byteLength - 8) [[unlikely]]
return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, 0, byteLength - 8, offsetVal);
size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength);
RETURN_IF_EXCEPTION(scope, {});
write_int64_be(static_cast<uint8_t*>(castedThis->vector()) + offset, value);
return JSValue::encode(jsNumber(offset + 8));
}

View File

@@ -331,6 +331,34 @@ for (let withOverridenBufferWrite of [false, true]) {
}
});
it("write BigInt64 with insufficient buffer space", () => {
// Test for bounds check fix - prevent unsigned integer underflow
// when byteLength < 8, the check `offset > byteLength - 8` would underflow
const buf = Buffer.from("Hello World");
const slice = buf.slice(0, 5); // 5 bytes
for (const fn of ["writeBigInt64LE", "writeBigInt64BE", "writeBigUInt64LE", "writeBigUInt64BE"]) {
// Should throw because we need 8 bytes but only have 5
expect(() => slice[fn](4096n, 0)).toThrow(RangeError);
// Should also throw with large invalid offset
expect(() => slice[fn](4096n, 10000)).toThrow(RangeError);
}
// Test exact boundary - 8 bytes should work at offset 0
const buf8 = Buffer.allocUnsafe(8);
for (const fn of ["writeBigInt64LE", "writeBigInt64BE", "writeBigUInt64LE", "writeBigUInt64BE"]) {
expect(buf8[fn](4096n, 0)).toBe(8);
// But should fail at offset 1 (not enough space)
expect(() => buf8[fn](4096n, 1)).toThrow(RangeError);
}
// Test very small buffers
const buf7 = Buffer.allocUnsafe(7);
for (const fn of ["writeBigInt64LE", "writeBigInt64BE", "writeBigUInt64LE", "writeBigUInt64BE"]) {
expect(() => buf7[fn](0n, 0)).toThrow(RangeError);
}
});
it("copy() beyond end of buffer", () => {
const b = Buffer.allocUnsafe(64);
// Try to copy 0 bytes worth of data into an empty buffer
@@ -3061,3 +3089,41 @@ it("Buffer.from(arrayBuffer, byteOffset, length)", () => {
expect(buf.byteLength).toBe(5);
expect(buf[Symbol.iterator]().toArray()).toEqual([13, 14, 15, 16, 17]);
});
describe("ERR_BUFFER_OUT_OF_BOUNDS", () => {
for (const method of ["writeBigInt64BE", "writeBigInt64LE", "writeBigUInt64BE", "writeBigUInt64LE"]) {
for (const bufferLength of [0, 1, 2, 3, 4, 5, 6]) {
const buffer = Buffer.allocUnsafe(bufferLength);
it(`Buffer(${bufferLength}).${method}`, () => {
expect(() => buffer[method](0n)).toThrow(
expect.objectContaining({
code: "ERR_BUFFER_OUT_OF_BOUNDS",
}),
);
expect(() => buffer[method](0n, 0)).toThrow(
expect.objectContaining({
code: "ERR_BUFFER_OUT_OF_BOUNDS",
}),
);
});
}
}
for (const method of ["readBigInt64BE", "readBigInt64LE", "readBigUInt64BE", "readBigUInt64LE"]) {
for (const bufferLength of [0, 1, 2, 3, 4, 5, 6]) {
const buffer = Buffer.allocUnsafe(bufferLength);
it(`Buffer(${bufferLength}).${method}`, () => {
expect(() => buffer[method]()).toThrow(
expect.objectContaining({
code: "ERR_BUFFER_OUT_OF_BOUNDS",
}),
);
expect(() => buffer[method](0)).toThrow(
expect.objectContaining({
code: "ERR_BUFFER_OUT_OF_BOUNDS",
}),
);
});
}
}
});