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", + }), + ); + }); + } + } +});