mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
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:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user