From 2ebf6c16b68ee4757a784e30e96c03cf89c25dc2 Mon Sep 17 00:00:00 2001 From: robobun Date: Sat, 18 Oct 2025 16:52:07 -0700 Subject: [PATCH] Fix bounds check in Buffer writeBigInt64/writeBigUInt64 methods (#23781) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 Co-authored-by: Claude Co-authored-by: Jarred Sumner --- src/bun.js/bindings/JSBuffer.cpp | 114 ++++++++++++++++++------------- test/js/node/buffer.test.js | 66 ++++++++++++++++++ 2 files changed, 132 insertions(+), 48 deletions(-) diff --git a/src/bun.js/bindings/JSBuffer.cpp b/src/bun.js/bindings/JSBuffer.cpp index 9e10a88399..2cfe9247be 100644 --- a/src/bun.js/bindings/JSBuffer.cpp +++ b/src/bun.js/bindings/JSBuffer.cpp @@ -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(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(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(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(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(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(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(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(castedThis->vector()) + offset, value); return JSValue::encode(jsNumber(offset + 8)); } diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index 814ba2153c..ba74951816 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -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", + }), + ); + }); + } + } +});