From 9bb889734a0093ef7954fbada48d9decc2b91bec Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 23 Jul 2026 18:01:38 -0700 Subject: [PATCH 01/25] Make Buffer read*/write* native functions with a DFG/FTL intrinsic The fixed-width accessors (readInt8 ... readDoubleBE, the BigInt64 reads, writeInt8 ... writeDoubleBE) were JS builtins that lazily created a hidden DataView on each Buffer (`this.$dataView ||= new DataView(...)`) and validated through $checkBufferRead / internal/buffer.js. They are now C++ host functions in JSBuffer.cpp with the same semantics (error codes, messages and the per-function argument-validation order of lib/internal/buffer.js), registered with JSC's BufferAccessorRegistry and carrying BufferAccessorIntrinsic so the DFG/FTL compile call sites into bounds-checked loads/stores on the receiver's storage and OSR-exit back to the host function for anything they don't speculate. The existing writeBigInt64* host functions get the intrinsic too. The variable-width accessors (readIntLE etc.) are unchanged; the now-unused $checkBufferRead global is removed. Adds tier-up coverage to test/js/node/buffer.test.js and a bench/snippets/buffer-read-write.mjs benchmark. --- bench/snippets/buffer-read-write.mjs | 105 ++++++ src/js/builtins.d.ts | 1 - src/js/builtins/BunBuiltinNames.h | 1 - src/js/builtins/JSBufferPrototype.ts | 275 +--------------- src/jsc/bindings/JSBuffer.cpp | 472 ++++++++++++++++++++++++--- src/jsc/bindings/ZigGlobalObject.cpp | 35 -- test/js/node/buffer.test.js | 133 ++++++++ 7 files changed, 667 insertions(+), 355 deletions(-) create mode 100644 bench/snippets/buffer-read-write.mjs diff --git a/bench/snippets/buffer-read-write.mjs b/bench/snippets/buffer-read-write.mjs new file mode 100644 index 000000000000..7bd931daac6d --- /dev/null +++ b/bench/snippets/buffer-read-write.mjs @@ -0,0 +1,105 @@ +// Buffer.prototype.read* / write* — the fixed-width accessors that JSC JIT-compiles into +// bounds-checked loads/stores (see JSBuffer.cpp / JavaScriptCore BufferAccessorRegistry). +// +// Three shapes: +// - a tight loop over one buffer (constant offset): mostly measures call/loop overhead +// - a loop over increasing offsets on one buffer: the load/store + bounds check per iteration +// - one access on each of many distinct buffers: previously paid a hidden DataView allocation +// plus a structure transition per buffer +import { bench, group, run } from "../runner.mjs"; + +const size = 4096; +const buf = Buffer.alloc(size); +for (let i = 0; i < size; i++) buf[i] = (i * 37 + 11) & 0xff; + +const many = Array.from({ length: 1024 }, () => Buffer.alloc(64)); + +group("constant offset (10 accesses per iteration)", () => { + bench("readInt32LE(0)", () => { + let s = 0; + for (let i = 0; i < 10; i++) s += buf.readInt32LE(0); + return s; + }); + bench("writeInt32LE(v, 0)", () => { + for (let i = 0; i < 10; i++) buf.writeInt32LE(i, 0); + }); +}); + +group("varying offset over one buffer", () => { + bench("readInt8", () => { + let s = 0; + for (let i = 0; i < size; i++) s += buf.readInt8(i); + return s; + }); + bench("readUInt8", () => { + let s = 0; + for (let i = 0; i < size; i++) s += buf.readUInt8(i); + return s; + }); + bench("readInt16BE", () => { + let s = 0; + for (let i = 0; i < size; i += 2) s += buf.readInt16BE(i); + return s; + }); + bench("readInt32LE", () => { + let s = 0; + for (let i = 0; i < size; i += 4) s += buf.readInt32LE(i); + return s; + }); + bench("readUInt32BE", () => { + let s = 0; + for (let i = 0; i < size; i += 4) s += buf.readUInt32BE(i); + return s; + }); + bench("readFloatLE", () => { + let s = 0; + for (let i = 0; i < size; i += 4) s += buf.readFloatLE(i); + return s; + }); + bench("readDoubleLE", () => { + let s = 0; + for (let i = 0; i < size; i += 8) s += buf.readDoubleLE(i); + return s; + }); + bench("readBigInt64LE", () => { + let s = 0n; + for (let i = 0; i < size; i += 8) s += buf.readBigInt64LE(i); + return s; + }); + bench("writeUInt8", () => { + for (let i = 0; i < size; i++) buf.writeUInt8(i & 0xff, i); + }); + bench("writeInt16BE", () => { + for (let i = 0; i < size; i += 2) buf.writeInt16BE(i, i); + }); + bench("writeInt32LE", () => { + for (let i = 0; i < size; i += 4) buf.writeInt32LE(i, i); + }); + bench("writeUInt32BE", () => { + for (let i = 0; i < size; i += 4) buf.writeUInt32BE(i, i); + }); + bench("writeFloatLE", () => { + for (let i = 0; i < size; i += 4) buf.writeFloatLE(i + 0.5, i); + }); + bench("writeDoubleLE", () => { + for (let i = 0; i < size; i += 8) buf.writeDoubleLE(i + 0.5, i); + }); +}); + +group("one access on each of 1024 buffers", () => { + bench("readInt32LE", () => { + let s = 0; + for (let i = 0; i < many.length; i++) s += many[i].readInt32LE(0); + return s; + }); + bench("writeInt32LE", () => { + for (let i = 0; i < many.length; i++) many[i].writeInt32LE(i, 0); + }); + bench("readDoubleLE", () => { + let s = 0; + for (let i = 0; i < many.length; i++) s += many[i].readDoubleLE(0); + return s; + }); +}); + +await run(); diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index 1407381f81fd..df84758a4588 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -692,7 +692,6 @@ declare function $toClass(fn: Function, name: string, base?: Function | undefine declare function $min(a: number, b: number): number; -declare function $checkBufferRead(buf: Buffer, offset: number, byteLength: number): undefined; /** * Schedules a callback to be invoked as a microtask. diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h index b6495c2eea2b..2ddf702b46c8 100644 --- a/src/js/builtins/BunBuiltinNames.h +++ b/src/js/builtins/BunBuiltinNames.h @@ -61,7 +61,6 @@ using namespace JSC; macro(byobRequest) \ macro(bytes) \ macro(cancel) \ - macro(checkBufferRead) \ macro(checks) \ macro(cloneArrayBuffer) \ macro(close) \ diff --git a/src/js/builtins/JSBufferPrototype.ts b/src/js/builtins/JSBufferPrototype.ts index 81bb7507ef87..63bbc1f63054 100644 --- a/src/js/builtins/JSBufferPrototype.ts +++ b/src/js/builtins/JSBufferPrototype.ts @@ -1,5 +1,6 @@ -// The fastest way as of April 2022 is to use DataView. -// DataView has intrinsics that cause inlining +// The fixed-width readers/writers (readInt8 ... writeDoubleBE) are C++ host functions in +// JSBuffer.cpp with a DFG/FTL intrinsic; the variable-width ones below still go through a +// lazily-created DataView (whose accessors JSC also inlines). interface BufferExt extends Buffer { $dataView?: DataView; @@ -16,74 +17,6 @@ export function setBigUint64(this: BufferExt, offset, value, le) { ); } -export function readInt8(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined) $checkBufferRead(this, offset, 1); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getInt8(offset); -} - -export function readUInt8(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined) $checkBufferRead(this, offset, 1); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getUint8(offset); -} - -export function readInt16LE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 1] === undefined) - $checkBufferRead(this, offset, 2); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getInt16(offset, true); -} - -export function readInt16BE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 1] === undefined) - $checkBufferRead(this, offset, 2); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getInt16(offset, false); -} - -export function readUInt16LE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 1] === undefined) - $checkBufferRead(this, offset, 2); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getUint16(offset, true); -} - -export function readUInt16BE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 1] === undefined) - $checkBufferRead(this, offset, 2); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getUint16(offset, false); -} - -export function readInt32LE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 3] === undefined) - $checkBufferRead(this, offset, 4); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getInt32(offset, true); -} - -export function readInt32BE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 3] === undefined) - $checkBufferRead(this, offset, 4); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getInt32(offset, false); -} - -export function readUInt32LE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 3] === undefined) - $checkBufferRead(this, offset, 4); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getUint32(offset, true); -} - -export function readUInt32BE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 3] === undefined) - $checkBufferRead(this, offset, 4); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getUint32(offset, false); -} - export function readIntLE(this: BufferExt, offset, byteLength) { if (offset === undefined) throw $ERR_INVALID_ARG_TYPE("offset", "number", offset); if (typeof byteLength !== "number") throw $ERR_INVALID_ARG_TYPE("byteLength", "number", byteLength); @@ -262,172 +195,6 @@ export function readUIntBE(this: BufferExt, offset, byteLength) { require("internal/buffer").boundsError(byteLength, 6, "byteLength"); } -export function readFloatLE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 3] === undefined) - $checkBufferRead(this, offset, 4); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getFloat32(offset, true); -} - -export function readFloatBE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 3] === undefined) - $checkBufferRead(this, offset, 4); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getFloat32(offset, false); -} - -export function readDoubleLE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 7] === undefined) - $checkBufferRead(this, offset, 8); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getFloat64(offset, true); -} - -export function readDoubleBE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 7] === undefined) - $checkBufferRead(this, offset, 8); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getFloat64(offset, false); -} - -export function readBigInt64LE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 7] === undefined) - $checkBufferRead(this, offset, 8); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getBigInt64(offset, true); -} - -export function readBigInt64BE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 7] === undefined) - $checkBufferRead(this, offset, 8); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getBigInt64(offset, false); -} - -export function readBigUInt64LE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 7] === undefined) - $checkBufferRead(this, offset, 8); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getBigUint64(offset, true); -} - -export function readBigUInt64BE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 7] === undefined) - $checkBufferRead(this, offset, 8); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getBigUint64(offset, false); -} - -export function writeInt8(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = -0x80; - const max = 0x7f; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined) require("internal/buffer").writeU_Int8(this, value, offset, min, max); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt8(offset, value); - return offset + 1; -} - -export function writeUInt8(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = 0; - const max = 0xff; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined) require("internal/buffer").writeU_Int8(this, value, offset, min, max); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint8(offset, value); - return offset + 1; -} - -export function writeInt16LE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = -0x8000; - const max = 0x7fff; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined || this[offset + 1] === undefined) require("internal/buffer").checkInt(this, value, offset, min, max, 2); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt16(offset, value, true); - return offset + 2; -} - -export function writeInt16BE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = -0x8000; - const max = 0x7fff; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined || this[offset + 1] === undefined) require("internal/buffer").checkInt(this, value, offset, min, max, 2); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt16(offset, value, false); - return offset + 2; -} - -export function writeUInt16LE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = 0; - const max = 0xffff; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined || this[offset + 1] === undefined) require("internal/buffer").checkInt(this, value, offset, min, max, 2); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint16(offset, value, true); - return offset + 2; -} - -export function writeUInt16BE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = 0; - const max = 0xffff; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined || this[offset + 1] === undefined) require("internal/buffer").checkInt(this, value, offset, min, max, 2); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint16(offset, value, false); - return offset + 2; -} - -export function writeInt32LE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = -0x80000000; - const max = 0x7fffffff; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined || this[offset + 3] === undefined) require("internal/buffer").checkInt(this, value, offset, min, max, 4); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt32(offset, value, true); - return offset + 4; -} - -export function writeInt32BE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = -0x80000000; - const max = 0x7fffffff; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined || this[offset + 3] === undefined) require("internal/buffer").checkInt(this, value, offset, min, max, 4); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt32(offset, value, false); - return offset + 4; -} - -export function writeUInt32LE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = 0; - const max = 0xffffffff; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined || this[offset + 3] === undefined) require("internal/buffer").checkInt(this, value, offset, min, max, 4); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint32(offset, value, true); - return offset + 4; -} - -export function writeUInt32BE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = 0; - const max = 0xffffffff; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined || this[offset + 3] === undefined) require("internal/buffer").checkInt(this, value, offset, min, max, 4); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint32(offset, value, false); - return offset + 4; -} - export function writeIntLE(this: BufferExt, value, offset, byteLength) { const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); value = +value; @@ -634,42 +401,6 @@ export function writeUIntBE(this: BufferExt, value, offset, byteLength) { return offset + byteLength; } -export function writeFloatLE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - // prettier-ignore - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 3] === undefined) require("internal/buffer").checkBounds(this, offset, 4); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat32(offset, value, true); - return offset + 4; -} - -export function writeFloatBE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - // prettier-ignore - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 3] === undefined) require("internal/buffer").checkBounds(this, offset, 4); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat32(offset, value, false); - return offset + 4; -} - -export function writeDoubleLE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - // prettier-ignore - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 7] === undefined) require("internal/buffer").checkBounds(this, offset, 8); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat64(offset, value, true); - return offset + 8; -} - -export function writeDoubleBE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - // prettier-ignore - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 7] === undefined) require("internal/buffer").checkBounds(this, offset, 8); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat64(offset, value, false); - return offset + 8; -} - export function toJSON(this: BufferExt) { const type = "Buffer"; const data = Array.from(this); diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp index ffdae2557a6a..3b597cf159c6 100644 --- a/src/jsc/bindings/JSBuffer.cpp +++ b/src/jsc/bindings/JSBuffer.cpp @@ -78,7 +78,11 @@ #include // #include +#include #include +#include +#include +#include #include #include #include @@ -129,6 +133,41 @@ JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigInt64BE); JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64LE); JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64BE); +// Fixed-width readers / writers. Each is registered as a JSC "buffer accessor" (BufferAccessorIntrinsic + +// runtime/BufferAccessorRegistry.h) so DFG/FTL compile call sites into bounds-checked loads / stores. +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readInt8); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readUInt8); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readInt16LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readInt16BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readUInt16LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readUInt16BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readInt32LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readInt32BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readUInt32LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readUInt32BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readFloatLE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readFloatBE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readDoubleLE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readDoubleBE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readBigInt64LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readBigInt64BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readBigUInt64LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readBigUInt64BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeInt8); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeUInt8); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeInt16LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeInt16BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeUInt16LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeUInt16BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeInt32LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeInt32BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeUInt32LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeUInt32BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeFloatLE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeFloatBE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeDoubleLE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeDoubleBE); + extern "C" EncodedJSValue WebCore_BufferEncodingType_toJS(JSC::JSGlobalObject* lexicalGlobalObject, WebCore::BufferEncodingType encoding) { // clang-format off @@ -3025,6 +3064,287 @@ template void write_int64_be(uint8_t* buffer, I value) buffer[7] = val[0]; } +// Fixed-width read* / write* (readInt8 ... writeDoubleBE, plus the BigInt64 reads). +// +// These are ordinary host functions, but each is also registered with JSC's BufferAccessorRegistry +// and carries BufferAccessorIntrinsic (see JSBufferPrototype::finishCreation), so the DFG / FTL +// compile call sites into a bounds-checked load / store on the receiver's storage and OSR-exit back +// here for anything they do not speculate. That makes these functions the single source of truth for +// error behavior: they follow lib/internal/buffer.js's checkBounds() / checkInt() / boundsError() and +// the fast-path guards of the JS implementation they replace, so the errors (code, message and the +// order in which arguments are validated) are unchanged. +namespace { + +// The receiver check: like the old JS fast path plus $checkBufferRead(), any ArrayBufferView receiver is +// accepted (byte-length semantics); anything else is ERR_INVALID_ARG_TYPE("buf"). +static JSC::JSArrayBufferView* bufferAccessReceiver(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSValue thisValue) +{ + auto* view = dynamicDowncast(thisValue); + if (!view) [[unlikely]] { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "buf"_s, "Buffer"_s, thisValue); + return nullptr; + } + return view; +} + +// validateNumber(offset, 'offset') +static bool bufferAccessCheckOffsetType(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSValue offsetValue) +{ + if (!offsetValue.isNumber()) [[unlikely]] { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetValue); + return false; + } + return true; +} + +// boundsError(offset, byteLength - byteSize) after the fast path failed: reports the same +// ERR_OUT_OF_RANGE("offset", "an integer" / ">= 0 and <= N") / ERR_BUFFER_OUT_OF_BOUNDS as +// lib/internal/buffer.js. Returns the byte offset for a value that turns out to be in range +// (a non-int32 but integral double, e.g. a large offset into a >2GB view). +static std::optional bufferAccessCheckOffsetBounds(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSValue offsetValue, size_t byteLength, size_t byteSize) +{ + double offset = offsetValue.asNumber(); + // Math.floor(value) !== value: NaN and fractions are "an integer"; +-Infinity get the range error. + if (std::floor(offset) != offset) [[unlikely]] { + Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, "an integer"_s, offsetValue); + return std::nullopt; + } + if (byteLength < byteSize) [[unlikely]] { + Bun::ERR::BUFFER_OUT_OF_BOUNDS(scope, lexicalGlobalObject, ""_s); + return std::nullopt; + } + size_t maxOffset = byteLength - byteSize; + if (!(offset >= 0 && offset <= static_cast(maxOffset))) [[unlikely]] { + Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, makeString(">= 0 and <= "_s, maxOffset), offsetValue); + return std::nullopt; + } + return static_cast(offset); +} + +// The unsigned integer type a scalar's bytes are moved in. +template struct BufferAccessStorage { + using Type = std::make_unsigned_t; +}; +template<> struct BufferAccessStorage { + using Type = uint32_t; +}; +template<> struct BufferAccessStorage { + using Type = uint64_t; +}; + +// Reads / stores in the accessor's byte order, on an unaligned pointer. +template +static ALWAYS_INLINE Storage bufferAccessLoad(const uint8_t* address) +{ + Storage value = WTF::unalignedLoad(address); + if constexpr (!isLittleEndian && sizeof(Storage) > 1) + value = WTF::flipBytes(value); + return value; +} + +template +static ALWAYS_INLINE void bufferAccessStore(uint8_t* address, Storage value) +{ + if constexpr (!isLittleEndian && sizeof(Storage) > 1) + value = WTF::flipBytes(value); + WTF::unalignedStore(address, value); +} + +// read*(offset = 0). `T` is the scalar type (int8_t ... double, int64_t / uint64_t for the BigInt reads). +template +static JSC::EncodedJSValue bufferRead(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + using Storage = typename BufferAccessStorage::Type; + constexpr size_t byteSize = sizeof(T); + JSValue thisValue = callFrame->thisValue(); + JSValue offsetValue = callFrame->argument(0); + + auto* view = dynamicDowncast(thisValue); + size_t offset; + // The fast path: an int32 (or missing) offset inside a real ArrayBufferView -- what the JIT'd form assumes. + if (view && (offsetValue.isInt32() || offsetValue.isUndefined())) [[likely]] { + int32_t offset32 = offsetValue.isInt32() ? offsetValue.asInt32() : 0; + size_t byteLength = view->byteLength(); + if (offset32 >= 0 && static_cast(offset32) + byteSize <= byteLength) [[likely]] { + offset = offset32; + goto fastPath; + } + } + + { + // The slow path: same errors, in the same order, as the JS implementation. + if (offsetValue.isUndefined()) + offsetValue = jsNumber(0); + if (!bufferAccessCheckOffsetType(lexicalGlobalObject, scope, offsetValue)) + return {}; + if (!view) [[unlikely]] { + bufferAccessReceiver(lexicalGlobalObject, scope, thisValue); + return {}; + } + auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->byteLength(), byteSize); + if (!checkedOffset) + return {}; + offset = *checkedOffset; + } + +fastPath: + const uint8_t* address = static_cast(view->vector()) + offset; + Storage raw = bufferAccessLoad(address); + if constexpr (std::is_floating_point_v) { + // The bytes may be any NaN; a boxed JS double must be the purified one (DataView does the same). + return JSValue::encode(jsNumber(JSC::purifyNaN(static_cast(std::bit_cast(raw))))); + } else if constexpr (byteSize == 8) { + // readBigInt64* / readBigUInt64* + RELEASE_AND_RETURN(scope, JSValue::encode(JSBigInt::makeHeapBigIntOrBigInt32(lexicalGlobalObject, std::bit_cast(raw)))); + } else + return JSValue::encode(jsNumber(std::bit_cast(raw))); +} + +// write*(value, offset = 0). Ints: `value = +value` then checkInt() / writeU_Int8() (which range-check the +// number and throw ERR_OUT_OF_RANGE("value")); the stored bytes are the number truncated the way a +// DataView / typed array store truncates (ToInt32). Floats: checkBounds() only. +template +static JSC::EncodedJSValue bufferWrite(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + using Storage = typename BufferAccessStorage::Type; + constexpr size_t byteSize = sizeof(T); + constexpr bool isFloat = std::is_floating_point_v; + JSValue thisValue = callFrame->thisValue(); + JSValue valueValue = callFrame->argument(0); + JSValue offsetValue = callFrame->argument(1); + + auto* view = dynamicDowncast(thisValue); + + // `value = +value` comes first (it can run user code and throw), before any offset validation. + double number; + if (valueValue.isNumber()) [[likely]] + number = valueValue.asNumber(); + else { + number = valueValue.toNumber(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + } + + // The value range check of writeU_Int8() / checkInt() (floats and NaN are never out of range). + auto valueIsInRange = [&] { + if constexpr (isFloat) + return true; + else + return !(number < static_cast(std::numeric_limits::min()) || number > static_cast(std::numeric_limits::max())); + }; + auto throwValueOutOfRange = [&] { + return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "value"_s, static_cast(std::numeric_limits::min()), static_cast(std::numeric_limits::max()), valueValue); + }; + + size_t offset; + // The fast path: an in-range value at an int32 (or missing) offset inside a real ArrayBufferView. + if (view && (offsetValue.isInt32() || offsetValue.isUndefined())) [[likely]] { + int32_t offset32 = offsetValue.isInt32() ? offsetValue.asInt32() : 0; + size_t byteLength = view->byteLength(); + if (offset32 >= 0 && static_cast(offset32) + byteSize <= byteLength && valueIsInRange()) [[likely]] { + offset = offset32; + goto fastPath; + } + } + + { + // The slow path. The odd argument-validation order is Node's own: writeU_Int8() (the one-byte + // writers) validates the offset before the value's range, checkInt() (2 and 4 bytes) the other + // way around; the float writers have no value range at all (checkBounds()). + if (offsetValue.isUndefined()) + offsetValue = jsNumber(0); + if constexpr (byteSize == 1) { + if (!bufferAccessCheckOffsetType(lexicalGlobalObject, scope, offsetValue)) + return {}; + if (!valueIsInRange()) + return throwValueOutOfRange(); + } else if constexpr (!isFloat) { + if (!valueIsInRange()) + return throwValueOutOfRange(); + if (!bufferAccessCheckOffsetType(lexicalGlobalObject, scope, offsetValue)) + return {}; + } else { + if (!bufferAccessCheckOffsetType(lexicalGlobalObject, scope, offsetValue)) + return {}; + } + if (!view) [[unlikely]] { + bufferAccessReceiver(lexicalGlobalObject, scope, thisValue); + return {}; + } + auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->byteLength(), byteSize); + if (!checkedOffset) + return {}; + offset = *checkedOffset; + } + +fastPath: + uint8_t* address = static_cast(view->vector()) + offset; + if constexpr (isFloat) { + if constexpr (byteSize == 4) + bufferAccessStore(address, std::bit_cast(static_cast(number))); + else + bufferAccessStore(address, std::bit_cast(number)); + } else if constexpr (std::is_signed_v) + bufferAccessStore(address, static_cast(JSC::toInt32(number))); + else + bufferAccessStore(address, static_cast(JSC::toUInt32(number))); + return JSValue::encode(jsNumber(offset + byteSize)); +} + +} // namespace + +#define DEFINE_BUFFER_READ(name, T, isLittleEndian) \ + JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_##name, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) \ + { \ + return bufferRead(lexicalGlobalObject, callFrame); \ + } +#define DEFINE_BUFFER_WRITE(name, T, isLittleEndian) \ + JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_##name, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) \ + { \ + return bufferWrite(lexicalGlobalObject, callFrame); \ + } + +DEFINE_BUFFER_READ(readInt8, int8_t, true) +DEFINE_BUFFER_READ(readUInt8, uint8_t, true) +DEFINE_BUFFER_READ(readInt16LE, int16_t, true) +DEFINE_BUFFER_READ(readInt16BE, int16_t, false) +DEFINE_BUFFER_READ(readUInt16LE, uint16_t, true) +DEFINE_BUFFER_READ(readUInt16BE, uint16_t, false) +DEFINE_BUFFER_READ(readInt32LE, int32_t, true) +DEFINE_BUFFER_READ(readInt32BE, int32_t, false) +DEFINE_BUFFER_READ(readUInt32LE, uint32_t, true) +DEFINE_BUFFER_READ(readUInt32BE, uint32_t, false) +DEFINE_BUFFER_READ(readFloatLE, float, true) +DEFINE_BUFFER_READ(readFloatBE, float, false) +DEFINE_BUFFER_READ(readDoubleLE, double, true) +DEFINE_BUFFER_READ(readDoubleBE, double, false) +DEFINE_BUFFER_READ(readBigInt64LE, int64_t, true) +DEFINE_BUFFER_READ(readBigInt64BE, int64_t, false) +DEFINE_BUFFER_READ(readBigUInt64LE, uint64_t, true) +DEFINE_BUFFER_READ(readBigUInt64BE, uint64_t, false) +DEFINE_BUFFER_WRITE(writeInt8, int8_t, true) +DEFINE_BUFFER_WRITE(writeUInt8, uint8_t, true) +DEFINE_BUFFER_WRITE(writeInt16LE, int16_t, true) +DEFINE_BUFFER_WRITE(writeInt16BE, int16_t, false) +DEFINE_BUFFER_WRITE(writeUInt16LE, uint16_t, true) +DEFINE_BUFFER_WRITE(writeUInt16BE, uint16_t, false) +DEFINE_BUFFER_WRITE(writeInt32LE, int32_t, true) +DEFINE_BUFFER_WRITE(writeInt32BE, int32_t, false) +DEFINE_BUFFER_WRITE(writeUInt32LE, uint32_t, true) +DEFINE_BUFFER_WRITE(writeUInt32BE, uint32_t, false) +DEFINE_BUFFER_WRITE(writeFloatLE, float, true) +DEFINE_BUFFER_WRITE(writeFloatBE, float, false) +DEFINE_BUFFER_WRITE(writeDoubleLE, double, true) +DEFINE_BUFFER_WRITE(writeDoubleBE, double, false) + +#undef DEFINE_BUFFER_READ +#undef DEFINE_BUFFER_WRITE + JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigInt64LE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = JSC::getVM(lexicalGlobalObject); @@ -3169,32 +3489,32 @@ static const HashTableValue JSBufferPrototypeTableValues[] { "latin1Write"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_latin1Write, 3 } }, { "offset"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, jsBufferPrototypeOffsetCodeGenerator, 0 } }, { "parent"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, jsBufferPrototypeParentCodeGenerator, 0 } }, - { "readBigInt64"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadBigInt64LECodeGenerator, 1 } }, - { "readBigInt64BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadBigInt64BECodeGenerator, 1 } }, - { "readBigInt64LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadBigInt64LECodeGenerator, 1 } }, - { "readBigUInt64"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadBigUInt64LECodeGenerator, 1 } }, - { "readBigUInt64BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadBigUInt64BECodeGenerator, 1 } }, - { "readBigUInt64LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadBigUInt64LECodeGenerator, 1 } }, - { "readDouble"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadDoubleLECodeGenerator, 1 } }, - { "readDoubleBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadDoubleBECodeGenerator, 1 } }, - { "readDoubleLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadDoubleLECodeGenerator, 1 } }, - { "readFloat"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadFloatLECodeGenerator, 1 } }, - { "readFloatBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadFloatBECodeGenerator, 1 } }, - { "readFloatLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadFloatLECodeGenerator, 1 } }, - { "readInt16"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadInt16LECodeGenerator, 1 } }, - { "readInt16BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadInt16BECodeGenerator, 1 } }, - { "readInt16LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadInt16LECodeGenerator, 1 } }, - { "readInt32"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadInt32LECodeGenerator, 1 } }, - { "readInt32BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadInt32BECodeGenerator, 1 } }, - { "readInt32LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadInt32LECodeGenerator, 1 } }, - { "readInt8"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadInt8CodeGenerator, 2 } }, + { "readBigInt64"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readBigInt64LE, 1 } }, + { "readBigInt64BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readBigInt64BE, 1 } }, + { "readBigInt64LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readBigInt64LE, 1 } }, + { "readBigUInt64"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readBigUInt64LE, 1 } }, + { "readBigUInt64BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readBigUInt64BE, 1 } }, + { "readBigUInt64LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readBigUInt64LE, 1 } }, + { "readDouble"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readDoubleLE, 1 } }, + { "readDoubleBE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readDoubleBE, 1 } }, + { "readDoubleLE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readDoubleLE, 1 } }, + { "readFloat"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readFloatLE, 1 } }, + { "readFloatBE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readFloatBE, 1 } }, + { "readFloatLE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readFloatLE, 1 } }, + { "readInt16"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readInt16LE, 1 } }, + { "readInt16BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readInt16BE, 1 } }, + { "readInt16LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readInt16LE, 1 } }, + { "readInt32"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readInt32LE, 1 } }, + { "readInt32BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readInt32BE, 1 } }, + { "readInt32LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readInt32LE, 1 } }, + { "readInt8"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readInt8, 1 } }, { "readIntBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadIntBECodeGenerator, 1 } }, { "readIntLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadIntLECodeGenerator, 1 } }, - { "readUInt16BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadUInt16BECodeGenerator, 1 } }, - { "readUInt16LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadUInt16LECodeGenerator, 1 } }, - { "readUInt32BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadUInt32BECodeGenerator, 1 } }, - { "readUInt32LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadUInt32LECodeGenerator, 1 } }, - { "readUInt8"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadUInt8CodeGenerator, 1 } }, + { "readUInt16BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUInt16BE, 1 } }, + { "readUInt16LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUInt16LE, 1 } }, + { "readUInt32BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUInt32BE, 1 } }, + { "readUInt32LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUInt32LE, 1 } }, + { "readUInt8"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUInt8, 1 } }, { "readUIntBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadUIntBECodeGenerator, 1 } }, { "readUIntLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadUIntLECodeGenerator, 1 } }, @@ -3213,30 +3533,30 @@ static const HashTableValue JSBufferPrototypeTableValues[] { "utf8Slice"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_utf8Slice, 2 } }, { "utf8Write"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_utf8Write, 3 } }, { "write"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_write, 4 } }, - { "writeBigInt64BE"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeBigInt64BE, 3 } }, - { "writeBigInt64LE"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeBigInt64LE, 3 } }, - { "writeBigUInt64BE"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeBigUInt64BE, 3 } }, - { "writeBigUInt64LE"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeBigUInt64LE, 3 } }, - { "writeDouble"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteDoubleLECodeGenerator, 1 } }, - { "writeDoubleBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteDoubleBECodeGenerator, 1 } }, - { "writeDoubleLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteDoubleLECodeGenerator, 1 } }, - { "writeFloat"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteFloatLECodeGenerator, 1 } }, - { "writeFloatBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteFloatBECodeGenerator, 1 } }, - { "writeFloatLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteFloatLECodeGenerator, 1 } }, - { "writeInt16BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteInt16BECodeGenerator, 1 } }, - { "writeInt16LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteInt16LECodeGenerator, 1 } }, - { "writeInt32BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteInt32BECodeGenerator, 1 } }, - { "writeInt32LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteInt32LECodeGenerator, 1 } }, - { "writeInt8"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteInt8CodeGenerator, 1 } }, + { "writeBigInt64BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeBigInt64BE, 3 } }, + { "writeBigInt64LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeBigInt64LE, 3 } }, + { "writeBigUInt64BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeBigUInt64BE, 3 } }, + { "writeBigUInt64LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeBigUInt64LE, 3 } }, + { "writeDouble"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeDoubleLE, 2 } }, + { "writeDoubleBE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeDoubleBE, 2 } }, + { "writeDoubleLE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeDoubleLE, 2 } }, + { "writeFloat"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeFloatLE, 2 } }, + { "writeFloatBE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeFloatBE, 2 } }, + { "writeFloatLE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeFloatLE, 2 } }, + { "writeInt16BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeInt16BE, 2 } }, + { "writeInt16LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeInt16LE, 2 } }, + { "writeInt32BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeInt32BE, 2 } }, + { "writeInt32LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeInt32LE, 2 } }, + { "writeInt8"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeInt8, 2 } }, { "writeIntBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteIntBECodeGenerator, 1 } }, { "writeIntLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteIntLECodeGenerator, 1 } }, - { "writeUInt16"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUInt16LECodeGenerator, 1 } }, - { "writeUInt16BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUInt16BECodeGenerator, 1 } }, - { "writeUInt16LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUInt16LECodeGenerator, 1 } }, - { "writeUInt32"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUInt32LECodeGenerator, 1 } }, - { "writeUInt32BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUInt32BECodeGenerator, 1 } }, - { "writeUInt32LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUInt32LECodeGenerator, 1 } }, - { "writeUInt8"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUInt8CodeGenerator, 1 } }, + { "writeUInt16"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt16LE, 2 } }, + { "writeUInt16BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt16BE, 2 } }, + { "writeUInt16LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt16LE, 2 } }, + { "writeUInt32"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt32LE, 2 } }, + { "writeUInt32BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt32BE, 2 } }, + { "writeUInt32LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt32LE, 2 } }, + { "writeUInt8"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt8, 2 } }, { "writeUIntBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUIntBECodeGenerator, 1 } }, { "writeUIntLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUIntLECodeGenerator, 1 } }, }; @@ -3250,10 +3570,70 @@ static const HashTableValue JSBufferPrototypeTableValues[] this->putDirect(vm, alias_ident, original, PropertyAttribute::Builtin | 0); \ } while (false); +// Registers the fixed-width read* / write* host functions with JSC's DFG/FTL, so a call to one of +// them on a Buffer (Uint8Array) receiver compiles down to a bounds-checked load / store +// (BufferReadInt / BufferReadFloat / BufferWrite) that OSR-exits back to the host function for +// anything it does not speculate. Process-global (the descriptor is a property of the function +// pointer), so this only has to happen once, before any of the functions is reachable from JS. +static void registerBufferAccessorsWithJSC() +{ + static std::once_flag registered; + std::call_once(registered, [] { + auto registerAccessor = [](JSC::NativeFunction function, uint8_t byteSize, bool isSigned, bool isFloatingPoint, bool isLittleEndian, bool isWrite) { + JSC::DFG::DataViewData data {}; + data.byteSize = byteSize; + data.isSigned = isSigned; + data.isFloatingPoint = isFloatingPoint; + data.isResizable = false; + data.isLittleEndian = triState(isLittleEndian); + JSC::registerBufferAccessor(JSC::toTagged(function), { data, isWrite }); + }; + constexpr bool read = false; + constexpr bool write = true; + registerAccessor(jsBufferPrototypeFunction_readInt8, 1, true, false, true, read); + registerAccessor(jsBufferPrototypeFunction_readUInt8, 1, false, false, true, read); + registerAccessor(jsBufferPrototypeFunction_readInt16LE, 2, true, false, true, read); + registerAccessor(jsBufferPrototypeFunction_readInt16BE, 2, true, false, false, read); + registerAccessor(jsBufferPrototypeFunction_readUInt16LE, 2, false, false, true, read); + registerAccessor(jsBufferPrototypeFunction_readUInt16BE, 2, false, false, false, read); + registerAccessor(jsBufferPrototypeFunction_readInt32LE, 4, true, false, true, read); + registerAccessor(jsBufferPrototypeFunction_readInt32BE, 4, true, false, false, read); + registerAccessor(jsBufferPrototypeFunction_readUInt32LE, 4, false, false, true, read); + registerAccessor(jsBufferPrototypeFunction_readUInt32BE, 4, false, false, false, read); + registerAccessor(jsBufferPrototypeFunction_readFloatLE, 4, false, true, true, read); + registerAccessor(jsBufferPrototypeFunction_readFloatBE, 4, false, true, false, read); + registerAccessor(jsBufferPrototypeFunction_readDoubleLE, 8, false, true, true, read); + registerAccessor(jsBufferPrototypeFunction_readDoubleBE, 8, false, true, false, read); + registerAccessor(jsBufferPrototypeFunction_readBigInt64LE, 8, true, false, true, read); + registerAccessor(jsBufferPrototypeFunction_readBigInt64BE, 8, true, false, false, read); + registerAccessor(jsBufferPrototypeFunction_readBigUInt64LE, 8, false, false, true, read); + registerAccessor(jsBufferPrototypeFunction_readBigUInt64BE, 8, false, false, false, read); + registerAccessor(jsBufferPrototypeFunction_writeInt8, 1, true, false, true, write); + registerAccessor(jsBufferPrototypeFunction_writeUInt8, 1, false, false, true, write); + registerAccessor(jsBufferPrototypeFunction_writeInt16LE, 2, true, false, true, write); + registerAccessor(jsBufferPrototypeFunction_writeInt16BE, 2, true, false, false, write); + registerAccessor(jsBufferPrototypeFunction_writeUInt16LE, 2, false, false, true, write); + registerAccessor(jsBufferPrototypeFunction_writeUInt16BE, 2, false, false, false, write); + registerAccessor(jsBufferPrototypeFunction_writeInt32LE, 4, true, false, true, write); + registerAccessor(jsBufferPrototypeFunction_writeInt32BE, 4, true, false, false, write); + registerAccessor(jsBufferPrototypeFunction_writeUInt32LE, 4, false, false, true, write); + registerAccessor(jsBufferPrototypeFunction_writeUInt32BE, 4, false, false, false, write); + registerAccessor(jsBufferPrototypeFunction_writeFloatLE, 4, false, true, true, write); + registerAccessor(jsBufferPrototypeFunction_writeFloatBE, 4, false, true, false, write); + registerAccessor(jsBufferPrototypeFunction_writeDoubleLE, 8, false, true, true, write); + registerAccessor(jsBufferPrototypeFunction_writeDoubleBE, 8, false, true, false, write); + registerAccessor(jsBufferPrototypeFunction_writeBigInt64LE, 8, true, false, true, write); + registerAccessor(jsBufferPrototypeFunction_writeBigInt64BE, 8, true, false, false, write); + registerAccessor(jsBufferPrototypeFunction_writeBigUInt64LE, 8, false, false, true, write); + registerAccessor(jsBufferPrototypeFunction_writeBigUInt64BE, 8, false, false, false, write); + }); +} + void JSBufferPrototype::finishCreation(VM& vm, JSC::JSGlobalObject* globalThis) { Base::finishCreation(vm); JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); + registerBufferAccessorsWithJSC(); reifyStaticProperties(vm, JSBuffer::info(), JSBufferPrototypeTableValues, *this); ALIAS("toLocaleString", "toString"); diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index eebbb5cfedab..6d04a645a882 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -2957,40 +2957,6 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionToClass, (JSC::JSGlobalObject * globalObject, return JSValue::encode(jsUndefined()); } -JSC_DEFINE_HOST_FUNCTION(jsFunctionCheckBufferRead, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) -{ - auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - - auto bufVal = callFrame->argument(0); - auto offsetVal = callFrame->argument(1); - auto byteLengthVal = callFrame->argument(2); - - // Mirrors Node's read-path validation (lib/internal/buffer.js): validateNumber - // on the offset, then boundsError(). A non-integer offset (NaN, 1.01) reports - // "an integer", but a finite-floor value like Infinity or a negative integer - // reports the ">= 0 and <= N" range, matching boundsError's MathFloor check. - if (!offsetVal.isNumber()) return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "offset"_s, "number"_s, offsetVal); - double offset = offsetVal.asNumber(); - - if (!bufVal.isCell()) return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "buf"_s, "Buffer"_s, bufVal); - auto* buf = dynamicDowncast(bufVal.asCell()); - if (!buf) return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "buf"_s, "Buffer"_s, bufVal); - size_t byteLength = byteLengthVal.asNumber(); - ssize_t type = ((ssize_t)buf->length()) - byteLength; - - // A non-integer offset (NaN, 1.01) is "an integer" even when numerically - // within bounds — the caller only reaches here because buf[offset] was - // undefined, which for a fractional index happens regardless of bounds. - if (std::floor(offset) != offset) { - return Bun::ERR::OUT_OF_RANGE(scope, globalObject, "offset"_s, "an integer"_s, offsetVal); - } - if (!(offset >= 0 && offset <= (double)type)) { - if (type < 0) return Bun::ERR::BUFFER_OUT_OF_BOUNDS(scope, globalObject, ""_s); - return Bun::ERR::OUT_OF_RANGE(scope, globalObject, "offset"_s, makeString(">= 0 and <= "_s, type), offsetVal); - } - return JSValue::encode(jsUndefined()); -} EncodedJSValue GlobalObject::assignToStream(JSValue stream, JSValue controller) { auto& vm = this->vm(); @@ -3090,7 +3056,6 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm) GlobalPropertyInfo(builtinNames.toClassPrivateName(), JSFunction::create(vm, this, 1, String(), jsFunctionToClass, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), GlobalPropertyInfo(builtinNames.inheritsPrivateName(), JSFunction::create(vm, this, 1, String(), jsFunctionInherits, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), GlobalPropertyInfo(builtinNames.makeAbortErrorPrivateName(), JSFunction::create(vm, this, 1, String(), jsFunctionMakeAbortError, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), - GlobalPropertyInfo(builtinNames.checkBufferReadPrivateName(), JSFunction::create(vm, this, 1, String(), jsFunctionCheckBufferRead, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), }; addStaticGlobals(staticGlobals); diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index 8bb673e4bf9b..df84b92e0f5a 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -4645,3 +4645,136 @@ it.skipIf(os.totalmem() < 10 * 1024 ** 3)( expect(exitCode).toBe(0); }, ); + +// The fixed-width read* / write* accessors are C++ host functions that JSC's DFG/FTL compile into +// bounds-checked loads / stores (JSBuffer.cpp + JavaScriptCore's BufferAccessorRegistry). They must +// keep agreeing with a DataView reference after tier-up, and everything the JIT does not speculate +// (bad offsets, out-of-range values, other receivers) must keep throwing exactly as before. +describe("read*/write* after JIT tier-up", () => { + const buf = Buffer.alloc(64); + const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); + for (let i = 0; i < buf.length; i++) buf[i] = (i * 37 + 11) & 0xff; + + const readers = [ + ["readInt8", 1, o => dv.getInt8(o)], + ["readUInt8", 1, o => dv.getUint8(o)], + ["readInt16LE", 2, o => dv.getInt16(o, true)], + ["readInt16BE", 2, o => dv.getInt16(o, false)], + ["readUInt16LE", 2, o => dv.getUint16(o, true)], + ["readUInt16BE", 2, o => dv.getUint16(o, false)], + ["readInt32LE", 4, o => dv.getInt32(o, true)], + ["readInt32BE", 4, o => dv.getInt32(o, false)], + ["readUInt32LE", 4, o => dv.getUint32(o, true)], + ["readUInt32BE", 4, o => dv.getUint32(o, false)], + ["readFloatLE", 4, o => dv.getFloat32(o, true)], + ["readFloatBE", 4, o => dv.getFloat32(o, false)], + ["readDoubleLE", 8, o => dv.getFloat64(o, true)], + ["readDoubleBE", 8, o => dv.getFloat64(o, false)], + ["readBigInt64LE", 8, o => dv.getBigInt64(o, true)], + ["readBigInt64BE", 8, o => dv.getBigInt64(o, false)], + ["readBigUInt64LE", 8, o => dv.getBigUint64(o, true)], + ["readBigUInt64BE", 8, o => dv.getBigUint64(o, false)], + ]; + + it("reads match a DataView across many iterations, and out-of-bounds keeps throwing", () => { + for (const [name, byteSize, reference] of readers) { + const read = new Function("b", "o", `return b.${name}(o);`); + for (let i = 0; i < 5000; i++) { + const o = i & 31; + expect(read(buf, o)).toBe(reference(o)); + } + expect(read(buf, 64 - byteSize)).toBe(reference(64 - byteSize)); + for (let i = 0; i < 100; i++) { + expect(() => read(buf, 64 - byteSize + 1)).toThrow(RangeError); + expect(() => read(buf, -1)).toThrow(RangeError); + expect(() => read(buf, 1.5)).toThrow(RangeError); + expect(() => read(buf, "0")).toThrow(TypeError); + } + } + }); + + it("writes match a DataView across many iterations, and range checks keep throwing", () => { + const cases = [ + ["writeInt8", 1, o => dv.getInt8(o), i => (i & 0xff) - 128], + ["writeUInt8", 1, o => dv.getUint8(o), i => i & 0xff], + ["writeInt16LE", 2, o => dv.getInt16(o, true), i => (i & 0xffff) - 0x8000], + ["writeUInt16BE", 2, o => dv.getUint16(o, false), i => i & 0xffff], + ["writeInt32LE", 4, o => dv.getInt32(o, true), i => (-i * 1000) | 0], + ["writeUInt32BE", 4, o => dv.getUint32(o, false), i => 2147483648 + i], + ["writeFloatLE", 4, o => dv.getFloat32(o, true), i => Math.fround(i / 3)], + ["writeDoubleBE", 8, o => dv.getFloat64(o, false), i => -i - 0.5], + ]; + for (const [name, byteSize, reference, value] of cases) { + const write = new Function("b", "v", "o", `return b.${name}(v, o);`); + for (let i = 0; i < 5000; i++) { + const o = i & 31; + const v = value(i); + expect(write(buf, v, o)).toBe(o + byteSize); + expect(reference(o)).toBe(v); + } + for (let i = 0; i < 100; i++) { + expect(() => write(buf, value(i), 64)).toThrow(RangeError); + } + } + for (let i = 0; i < 5000; i++) { + expect(() => buf.writeInt8(128, 0)).toThrow(RangeError); + expect(() => buf.writeUInt16LE(65536, 0)).toThrow(RangeError); + expect(() => buf.writeUInt32BE(-1, 0)).toThrow(RangeError); + } + }); + + it("BigInt writes match a DataView across many iterations, and 64-bit range checks keep throwing", () => { + const values = [0n, 1n, -1n, 2n ** 32n + 7n, 2n ** 63n - 1n, -(2n ** 63n)]; + for (let i = 0; i < 5000; i++) { + const o = (i & 7) * 8; + const v = values[i % values.length]; + expect(buf.writeBigInt64LE(v, o)).toBe(o + 8); + expect(dv.getBigInt64(o, true)).toBe(v); + expect(buf.writeBigInt64BE(v, o)).toBe(o + 8); + expect(dv.getBigInt64(o, false)).toBe(v); + if (v >= 0n) { + expect(buf.writeBigUInt64LE(v, o)).toBe(o + 8); + expect(dv.getBigUint64(o, true)).toBe(v); + expect(buf.writeBigUInt64BE(v, o)).toBe(o + 8); + expect(dv.getBigUint64(o, false)).toBe(v); + } + } + for (let i = 0; i < 3000; i++) { + expect(buf.writeBigUInt64LE(2n ** 64n - 1n, 0)).toBe(8); + expect(dv.getBigUint64(0, true)).toBe(2n ** 64n - 1n); + expect(() => buf.writeBigUInt64LE(-1n, 0)).toThrow(RangeError); + expect(() => buf.writeBigInt64LE(2n ** 63n, 0)).toThrow(RangeError); + expect(() => buf.writeBigInt64LE(-(2n ** 63n) - 1n, 0)).toThrow(RangeError); + expect(() => buf.writeBigInt64LE(5, 0)).toThrow(TypeError); + expect(() => buf.writeBigInt64LE(0n, 57)).toThrow(RangeError); + } + }); + + it("works on many distinct buffers (no hidden per-buffer state)", () => { + const bufs = Array.from({ length: 512 }, (_, i) => { + const b = Buffer.alloc(16); + b.writeInt32LE(i * 7, 4); + return b; + }); + let sum = 0; + for (let round = 0; round < 50; round++) { + for (let i = 0; i < bufs.length; i++) sum += bufs[i].readInt32LE(4); + } + expect(sum).toBe(50 * 7 * ((511 * 512) / 2)); + // Reading/writing added no non-index own properties to the buffers. + expect(Object.getOwnPropertyNames(bufs[0]).filter(k => !/^\d+$/.test(k))).toEqual([]); + expect(Object.getOwnPropertySymbols(bufs[0])).toEqual([]); + }); + + it("keeps working with other ArrayBufferView receivers via .call", () => { + const u8 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + const dv8 = new DataView(u8.buffer); + for (let i = 0; i < 5000; i++) { + expect(Buffer.prototype.readInt32LE.call(u8, 0)).toBe(dv8.getInt32(0, true)); + expect(Buffer.prototype.readUInt16BE.call(u8, 6)).toBe(dv8.getUint16(6, false)); + expect(Buffer.prototype.writeUInt8.call(u8, i & 0xff, 7)).toBe(8); + expect(u8[7]).toBe(i & 0xff); + expect(() => Buffer.prototype.readInt32LE.call({}, 0)).toThrow(TypeError); + } + }); +}); From 43838352796cb5e33ce5051594a0433f6b674924 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 01:04:02 +0000 Subject: [PATCH 02/25] [autofix.ci] apply automated fixes --- src/js/builtins.d.ts | 1 - src/jsc/bindings/JSBuffer.cpp | 18 +++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index df84758a4588..253a3c5b1618 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -692,7 +692,6 @@ declare function $toClass(fn: Function, name: string, base?: Function | undefine declare function $min(a: number, b: number): number; - /** * Schedules a callback to be invoked as a microtask. */ diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp index 3b597cf159c6..c51d0ccfad65 100644 --- a/src/jsc/bindings/JSBuffer.cpp +++ b/src/jsc/bindings/JSBuffer.cpp @@ -3298,15 +3298,15 @@ static JSC::EncodedJSValue bufferWrite(JSC::JSGlobalObject* lexicalGlobalObject, } // namespace -#define DEFINE_BUFFER_READ(name, T, isLittleEndian) \ - JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_##name, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) \ - { \ - return bufferRead(lexicalGlobalObject, callFrame); \ - } -#define DEFINE_BUFFER_WRITE(name, T, isLittleEndian) \ - JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_##name, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) \ - { \ - return bufferWrite(lexicalGlobalObject, callFrame); \ +#define DEFINE_BUFFER_READ(name, T, isLittleEndian) \ + JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_##name, (JSGlobalObject * lexicalGlobalObject, CallFrame * callFrame)) \ + { \ + return bufferRead(lexicalGlobalObject, callFrame); \ + } +#define DEFINE_BUFFER_WRITE(name, T, isLittleEndian) \ + JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_##name, (JSGlobalObject * lexicalGlobalObject, CallFrame * callFrame)) \ + { \ + return bufferWrite(lexicalGlobalObject, callFrame); \ } DEFINE_BUFFER_READ(readInt8, int8_t, true) From 9bc68f389fd7a47cf0de7d770203c8942c72640f Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 23 Jul 2026 18:24:17 -0700 Subject: [PATCH 03/25] Report the coerced number in write* range errors; test and registration cleanups - ERR_OUT_OF_RANGE("value") for write* now formats the coerced number (like Node and the previous JS implementation), not the raw argument. - Derive each accessor's registration descriptor from the same template arguments the host function is instantiated with, instead of a hand-encoded flag list. - Tighten the new tier-up tests: assert error codes, and count mismatches instead of asserting per iteration (much faster under debug+ASAN). - Trim over-long comments. --- src/jsc/bindings/JSBuffer.cpp | 129 ++++++++++++++++------------------ test/js/node/buffer.test.js | 90 ++++++++++++++++-------- 2 files changed, 120 insertions(+), 99 deletions(-) diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp index c51d0ccfad65..13264550a7ca 100644 --- a/src/jsc/bindings/JSBuffer.cpp +++ b/src/jsc/bindings/JSBuffer.cpp @@ -3064,19 +3064,13 @@ template void write_int64_be(uint8_t* buffer, I value) buffer[7] = val[0]; } -// Fixed-width read* / write* (readInt8 ... writeDoubleBE, plus the BigInt64 reads). -// -// These are ordinary host functions, but each is also registered with JSC's BufferAccessorRegistry -// and carries BufferAccessorIntrinsic (see JSBufferPrototype::finishCreation), so the DFG / FTL -// compile call sites into a bounds-checked load / store on the receiver's storage and OSR-exit back -// here for anything they do not speculate. That makes these functions the single source of truth for -// error behavior: they follow lib/internal/buffer.js's checkBounds() / checkInt() / boundsError() and -// the fast-path guards of the JS implementation they replace, so the errors (code, message and the -// order in which arguments are validated) are unchanged. +// Fixed-width read* / write* (readInt8 ... writeDoubleBE, plus the BigInt64 reads). JSC JITs call +// sites of these (BufferAccessorIntrinsic) and OSR-exits back here for anything it doesn't speculate, +// so these functions own the error behavior: they follow lib/internal/buffer.js exactly. namespace { -// The receiver check: like the old JS fast path plus $checkBufferRead(), any ArrayBufferView receiver is -// accepted (byte-length semantics); anything else is ERR_INVALID_ARG_TYPE("buf"). +// Any ArrayBufferView receiver is accepted (byte-length semantics); anything else is +// ERR_INVALID_ARG_TYPE("buf"). static JSC::JSArrayBufferView* bufferAccessReceiver(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSValue thisValue) { auto* view = dynamicDowncast(thisValue); @@ -3097,10 +3091,8 @@ static bool bufferAccessCheckOffsetType(JSC::JSGlobalObject* lexicalGlobalObject return true; } -// boundsError(offset, byteLength - byteSize) after the fast path failed: reports the same -// ERR_OUT_OF_RANGE("offset", "an integer" / ">= 0 and <= N") / ERR_BUFFER_OUT_OF_BOUNDS as -// lib/internal/buffer.js. Returns the byte offset for a value that turns out to be in range -// (a non-int32 but integral double, e.g. a large offset into a >2GB view). +// boundsError(offset, byteLength - byteSize): the same ERR_OUT_OF_RANGE / ERR_BUFFER_OUT_OF_BOUNDS as +// lib/internal/buffer.js. Returns the offset when it is in range after all (an integral double). static std::optional bufferAccessCheckOffsetBounds(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSValue offsetValue, size_t byteLength, size_t byteSize) { double offset = offsetValue.asNumber(); @@ -3238,7 +3230,8 @@ static JSC::EncodedJSValue bufferWrite(JSC::JSGlobalObject* lexicalGlobalObject, return !(number < static_cast(std::numeric_limits::min()) || number > static_cast(std::numeric_limits::max())); }; auto throwValueOutOfRange = [&] { - return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "value"_s, static_cast(std::numeric_limits::min()), static_cast(std::numeric_limits::max()), valueValue); + // Node reports the coerced number ("Received 40000" for the string "40000"), not the argument. + return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "value"_s, static_cast(std::numeric_limits::min()), static_cast(std::numeric_limits::max()), jsNumber(number)); }; size_t offset; @@ -3570,62 +3563,62 @@ static const HashTableValue JSBufferPrototypeTableValues[] this->putDirect(vm, alias_ident, original, PropertyAttribute::Builtin | 0); \ } while (false); -// Registers the fixed-width read* / write* host functions with JSC's DFG/FTL, so a call to one of -// them on a Buffer (Uint8Array) receiver compiles down to a bounds-checked load / store -// (BufferReadInt / BufferReadFloat / BufferWrite) that OSR-exits back to the host function for -// anything it does not speculate. Process-global (the descriptor is a property of the function -// pointer), so this only has to happen once, before any of the functions is reachable from JS. +// The accessor descriptor JSC's DFG needs, derived from the same template arguments the host +// function is instantiated with so the two cannot disagree. +template +static void registerBufferAccessor(JSC::NativeFunction function) +{ + JSC::DFG::DataViewData data {}; + data.byteSize = sizeof(T); + data.isSigned = std::is_signed_v && !std::is_floating_point_v; + data.isFloatingPoint = std::is_floating_point_v; + data.isResizable = false; + data.isLittleEndian = triState(isLittleEndian); + JSC::registerBufferAccessor(JSC::toTagged(function), { data, isWrite }); +} + +// Process-global (the descriptor belongs to the function pointer): once, before the functions are +// reachable from JS. static void registerBufferAccessorsWithJSC() { static std::once_flag registered; std::call_once(registered, [] { - auto registerAccessor = [](JSC::NativeFunction function, uint8_t byteSize, bool isSigned, bool isFloatingPoint, bool isLittleEndian, bool isWrite) { - JSC::DFG::DataViewData data {}; - data.byteSize = byteSize; - data.isSigned = isSigned; - data.isFloatingPoint = isFloatingPoint; - data.isResizable = false; - data.isLittleEndian = triState(isLittleEndian); - JSC::registerBufferAccessor(JSC::toTagged(function), { data, isWrite }); - }; - constexpr bool read = false; - constexpr bool write = true; - registerAccessor(jsBufferPrototypeFunction_readInt8, 1, true, false, true, read); - registerAccessor(jsBufferPrototypeFunction_readUInt8, 1, false, false, true, read); - registerAccessor(jsBufferPrototypeFunction_readInt16LE, 2, true, false, true, read); - registerAccessor(jsBufferPrototypeFunction_readInt16BE, 2, true, false, false, read); - registerAccessor(jsBufferPrototypeFunction_readUInt16LE, 2, false, false, true, read); - registerAccessor(jsBufferPrototypeFunction_readUInt16BE, 2, false, false, false, read); - registerAccessor(jsBufferPrototypeFunction_readInt32LE, 4, true, false, true, read); - registerAccessor(jsBufferPrototypeFunction_readInt32BE, 4, true, false, false, read); - registerAccessor(jsBufferPrototypeFunction_readUInt32LE, 4, false, false, true, read); - registerAccessor(jsBufferPrototypeFunction_readUInt32BE, 4, false, false, false, read); - registerAccessor(jsBufferPrototypeFunction_readFloatLE, 4, false, true, true, read); - registerAccessor(jsBufferPrototypeFunction_readFloatBE, 4, false, true, false, read); - registerAccessor(jsBufferPrototypeFunction_readDoubleLE, 8, false, true, true, read); - registerAccessor(jsBufferPrototypeFunction_readDoubleBE, 8, false, true, false, read); - registerAccessor(jsBufferPrototypeFunction_readBigInt64LE, 8, true, false, true, read); - registerAccessor(jsBufferPrototypeFunction_readBigInt64BE, 8, true, false, false, read); - registerAccessor(jsBufferPrototypeFunction_readBigUInt64LE, 8, false, false, true, read); - registerAccessor(jsBufferPrototypeFunction_readBigUInt64BE, 8, false, false, false, read); - registerAccessor(jsBufferPrototypeFunction_writeInt8, 1, true, false, true, write); - registerAccessor(jsBufferPrototypeFunction_writeUInt8, 1, false, false, true, write); - registerAccessor(jsBufferPrototypeFunction_writeInt16LE, 2, true, false, true, write); - registerAccessor(jsBufferPrototypeFunction_writeInt16BE, 2, true, false, false, write); - registerAccessor(jsBufferPrototypeFunction_writeUInt16LE, 2, false, false, true, write); - registerAccessor(jsBufferPrototypeFunction_writeUInt16BE, 2, false, false, false, write); - registerAccessor(jsBufferPrototypeFunction_writeInt32LE, 4, true, false, true, write); - registerAccessor(jsBufferPrototypeFunction_writeInt32BE, 4, true, false, false, write); - registerAccessor(jsBufferPrototypeFunction_writeUInt32LE, 4, false, false, true, write); - registerAccessor(jsBufferPrototypeFunction_writeUInt32BE, 4, false, false, false, write); - registerAccessor(jsBufferPrototypeFunction_writeFloatLE, 4, false, true, true, write); - registerAccessor(jsBufferPrototypeFunction_writeFloatBE, 4, false, true, false, write); - registerAccessor(jsBufferPrototypeFunction_writeDoubleLE, 8, false, true, true, write); - registerAccessor(jsBufferPrototypeFunction_writeDoubleBE, 8, false, true, false, write); - registerAccessor(jsBufferPrototypeFunction_writeBigInt64LE, 8, true, false, true, write); - registerAccessor(jsBufferPrototypeFunction_writeBigInt64BE, 8, true, false, false, write); - registerAccessor(jsBufferPrototypeFunction_writeBigUInt64LE, 8, false, false, true, write); - registerAccessor(jsBufferPrototypeFunction_writeBigUInt64BE, 8, false, false, false, write); + registerBufferAccessor(jsBufferPrototypeFunction_readInt8); + registerBufferAccessor(jsBufferPrototypeFunction_readUInt8); + registerBufferAccessor(jsBufferPrototypeFunction_readInt16LE); + registerBufferAccessor(jsBufferPrototypeFunction_readInt16BE); + registerBufferAccessor(jsBufferPrototypeFunction_readUInt16LE); + registerBufferAccessor(jsBufferPrototypeFunction_readUInt16BE); + registerBufferAccessor(jsBufferPrototypeFunction_readInt32LE); + registerBufferAccessor(jsBufferPrototypeFunction_readInt32BE); + registerBufferAccessor(jsBufferPrototypeFunction_readUInt32LE); + registerBufferAccessor(jsBufferPrototypeFunction_readUInt32BE); + registerBufferAccessor(jsBufferPrototypeFunction_readFloatLE); + registerBufferAccessor(jsBufferPrototypeFunction_readFloatBE); + registerBufferAccessor(jsBufferPrototypeFunction_readDoubleLE); + registerBufferAccessor(jsBufferPrototypeFunction_readDoubleBE); + registerBufferAccessor(jsBufferPrototypeFunction_readBigInt64LE); + registerBufferAccessor(jsBufferPrototypeFunction_readBigInt64BE); + registerBufferAccessor(jsBufferPrototypeFunction_readBigUInt64LE); + registerBufferAccessor(jsBufferPrototypeFunction_readBigUInt64BE); + registerBufferAccessor(jsBufferPrototypeFunction_writeInt8); + registerBufferAccessor(jsBufferPrototypeFunction_writeUInt8); + registerBufferAccessor(jsBufferPrototypeFunction_writeInt16LE); + registerBufferAccessor(jsBufferPrototypeFunction_writeInt16BE); + registerBufferAccessor(jsBufferPrototypeFunction_writeUInt16LE); + registerBufferAccessor(jsBufferPrototypeFunction_writeUInt16BE); + registerBufferAccessor(jsBufferPrototypeFunction_writeInt32LE); + registerBufferAccessor(jsBufferPrototypeFunction_writeInt32BE); + registerBufferAccessor(jsBufferPrototypeFunction_writeUInt32LE); + registerBufferAccessor(jsBufferPrototypeFunction_writeUInt32BE); + registerBufferAccessor(jsBufferPrototypeFunction_writeFloatLE); + registerBufferAccessor(jsBufferPrototypeFunction_writeFloatBE); + registerBufferAccessor(jsBufferPrototypeFunction_writeDoubleLE); + registerBufferAccessor(jsBufferPrototypeFunction_writeDoubleBE); + registerBufferAccessor(jsBufferPrototypeFunction_writeBigInt64LE); + registerBufferAccessor(jsBufferPrototypeFunction_writeBigInt64BE); + registerBufferAccessor(jsBufferPrototypeFunction_writeBigUInt64LE); + registerBufferAccessor(jsBufferPrototypeFunction_writeBigUInt64BE); }); } diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index df84b92e0f5a..af2380136778 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -4676,20 +4676,36 @@ describe("read*/write* after JIT tier-up", () => { ["readBigUInt64BE", 8, o => dv.getBigUint64(o, false)], ]; + function codeOf(fn) { + try { + fn(); + } catch (e) { + return e.code; + } + return "no throw"; + } + it("reads match a DataView across many iterations, and out-of-bounds keeps throwing", () => { for (const [name, byteSize, reference] of readers) { const read = new Function("b", "o", `return b.${name}(o);`); + let mismatches = 0; for (let i = 0; i < 5000; i++) { const o = i & 31; - expect(read(buf, o)).toBe(reference(o)); + if (read(buf, o) !== reference(o)) mismatches++; } + expect(mismatches).toBe(0); expect(read(buf, 64 - byteSize)).toBe(reference(64 - byteSize)); - for (let i = 0; i < 100; i++) { - expect(() => read(buf, 64 - byteSize + 1)).toThrow(RangeError); - expect(() => read(buf, -1)).toThrow(RangeError); - expect(() => read(buf, 1.5)).toThrow(RangeError); - expect(() => read(buf, "0")).toThrow(TypeError); + let outOfBounds = 0, + negative = 0, + fractional = 0, + wrongType = 0; + for (let i = 0; i < 50; i++) { + if (codeOf(() => read(buf, 64 - byteSize + 1)) === "ERR_OUT_OF_RANGE") outOfBounds++; + if (codeOf(() => read(buf, -1)) === "ERR_OUT_OF_RANGE") negative++; + if (codeOf(() => read(buf, 1.5)) === "ERR_OUT_OF_RANGE") fractional++; + if (codeOf(() => read(buf, "0")) === "ERR_INVALID_ARG_TYPE") wrongType++; } + expect([outOfBounds, negative, fractional, wrongType]).toEqual([50, 50, 50, 50]); } }); @@ -4706,48 +4722,60 @@ describe("read*/write* after JIT tier-up", () => { ]; for (const [name, byteSize, reference, value] of cases) { const write = new Function("b", "v", "o", `return b.${name}(v, o);`); + let mismatches = 0; for (let i = 0; i < 5000; i++) { const o = i & 31; const v = value(i); - expect(write(buf, v, o)).toBe(o + byteSize); - expect(reference(o)).toBe(v); - } - for (let i = 0; i < 100; i++) { - expect(() => write(buf, value(i), 64)).toThrow(RangeError); + if (write(buf, v, o) !== o + byteSize || reference(o) !== v) mismatches++; } + expect(mismatches).toBe(0); + let outOfBounds = 0; + for (let i = 0; i < 50; i++) if (codeOf(() => write(buf, value(i), 64)) === "ERR_OUT_OF_RANGE") outOfBounds++; + expect(outOfBounds).toBe(50); } - for (let i = 0; i < 5000; i++) { - expect(() => buf.writeInt8(128, 0)).toThrow(RangeError); - expect(() => buf.writeUInt16LE(65536, 0)).toThrow(RangeError); - expect(() => buf.writeUInt32BE(-1, 0)).toThrow(RangeError); + let ranges = 0; + for (let i = 0; i < 2000; i++) { + if (codeOf(() => buf.writeInt8(128, 0)) === "ERR_OUT_OF_RANGE") ranges++; + if (codeOf(() => buf.writeUInt16LE(65536, 0)) === "ERR_OUT_OF_RANGE") ranges++; + if (codeOf(() => buf.writeUInt32BE(-1, 0)) === "ERR_OUT_OF_RANGE") ranges++; } + expect(ranges).toBe(6000); + expect(() => buf.writeInt16LE("40000", 0)).toThrow("Received 40000"); }); it("BigInt writes match a DataView across many iterations, and 64-bit range checks keep throwing", () => { const values = [0n, 1n, -1n, 2n ** 32n + 7n, 2n ** 63n - 1n, -(2n ** 63n)]; + let mismatches = 0; for (let i = 0; i < 5000; i++) { const o = (i & 7) * 8; const v = values[i % values.length]; - expect(buf.writeBigInt64LE(v, o)).toBe(o + 8); - expect(dv.getBigInt64(o, true)).toBe(v); - expect(buf.writeBigInt64BE(v, o)).toBe(o + 8); - expect(dv.getBigInt64(o, false)).toBe(v); + if (buf.writeBigInt64LE(v, o) !== o + 8 || dv.getBigInt64(o, true) !== v) mismatches++; + if (buf.writeBigInt64BE(v, o) !== o + 8 || dv.getBigInt64(o, false) !== v) mismatches++; if (v >= 0n) { - expect(buf.writeBigUInt64LE(v, o)).toBe(o + 8); - expect(dv.getBigUint64(o, true)).toBe(v); - expect(buf.writeBigUInt64BE(v, o)).toBe(o + 8); - expect(dv.getBigUint64(o, false)).toBe(v); + if (buf.writeBigUInt64LE(v, o) !== o + 8 || dv.getBigUint64(o, true) !== v) mismatches++; + if (buf.writeBigUInt64BE(v, o) !== o + 8 || dv.getBigUint64(o, false) !== v) mismatches++; } } - for (let i = 0; i < 3000; i++) { - expect(buf.writeBigUInt64LE(2n ** 64n - 1n, 0)).toBe(8); - expect(dv.getBigUint64(0, true)).toBe(2n ** 64n - 1n); - expect(() => buf.writeBigUInt64LE(-1n, 0)).toThrow(RangeError); - expect(() => buf.writeBigInt64LE(2n ** 63n, 0)).toThrow(RangeError); - expect(() => buf.writeBigInt64LE(-(2n ** 63n) - 1n, 0)).toThrow(RangeError); - expect(() => buf.writeBigInt64LE(5, 0)).toThrow(TypeError); - expect(() => buf.writeBigInt64LE(0n, 57)).toThrow(RangeError); + expect(mismatches).toBe(0); + expect(buf.writeBigUInt64LE(2n ** 64n - 1n, 0)).toBe(8); + expect(dv.getBigUint64(0, true)).toBe(2n ** 64n - 1n); + let codes = []; + for (let i = 0; i < 1000; i++) { + codes = [ + codeOf(() => buf.writeBigUInt64LE(-1n, 0)), + codeOf(() => buf.writeBigInt64LE(2n ** 63n, 0)), + codeOf(() => buf.writeBigInt64LE(-(2n ** 63n) - 1n, 0)), + codeOf(() => buf.writeBigInt64LE(5, 0)), + codeOf(() => buf.writeBigInt64LE(0n, 57)), + ]; } + expect(codes).toEqual([ + "ERR_OUT_OF_RANGE", + "ERR_OUT_OF_RANGE", + "ERR_OUT_OF_RANGE", + "ERR_INVALID_ARG_TYPE", + "ERR_OUT_OF_RANGE", + ]); }); it("works on many distinct buffers (no hidden per-buffer state)", () => { From 922d63bf90e66fe9548745139e11de31ccc7bd64 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 23 Jul 2026 18:52:08 -0700 Subject: [PATCH 04/25] Remove writeU_Int8 and the checkBounds export, now unused --- src/js/internal/buffer.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/js/internal/buffer.ts b/src/js/internal/buffer.ts index 691b51738f5b..544913b85cfe 100644 --- a/src/js/internal/buffer.ts +++ b/src/js/internal/buffer.ts @@ -33,18 +33,7 @@ function checkInt(buf, value, offset, min, max, byteLength) { checkBounds(buf, offset, byteLength); } -function writeU_Int8(buf, value, offset, min, max) { - // `checkInt()` can not be used here because it checks two entries. - validateNumber(offset, "offset"); - if (value > max || value < min) { - throw $ERR_OUT_OF_RANGE("value", `>= ${min} and <= ${max}`, value); - } - if (buf[offset] === undefined) boundsError(offset, buf.length - 1); -} - export default { boundsError, - checkBounds, checkInt, - writeU_Int8, }; From 28008011bac83f8bdb409bc5f1130bdf7bdcf344 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 23 Jul 2026 19:40:00 -0700 Subject: [PATCH 05/25] Move the variable-width read*/write* accessors to native functions too readIntLE/BE, readUIntLE/BE, writeIntLE/BE and writeUIntLE/BE (byteLength 1..6) become C++ host functions with the same lib/internal/buffer.js semantics, which removes the last users of the hidden `this.$dataView` and of internal/buffer.js (both deleted). They register with JSC as variable-width accessors, so a call site with a constant byteLength of 1, 2 or 4 compiles to the same bounds-checked load/store as the fixed-width methods; other widths stay on the host function. --- src/js/builtins/JSBufferPrototype.ts | 398 --------------------------- src/js/internal/buffer.ts | 39 --- src/jsc/bindings/JSBuffer.cpp | 245 ++++++++++++++++- test/js/node/buffer.test.js | 46 ++++ 4 files changed, 283 insertions(+), 445 deletions(-) delete mode 100644 src/js/internal/buffer.ts diff --git a/src/js/builtins/JSBufferPrototype.ts b/src/js/builtins/JSBufferPrototype.ts index 63bbc1f63054..f3e9edc3bb58 100644 --- a/src/js/builtins/JSBufferPrototype.ts +++ b/src/js/builtins/JSBufferPrototype.ts @@ -1,406 +1,8 @@ -// The fixed-width readers/writers (readInt8 ... writeDoubleBE) are C++ host functions in -// JSBuffer.cpp with a DFG/FTL intrinsic; the variable-width ones below still go through a -// lazily-created DataView (whose accessors JSC also inlines). - interface BufferExt extends Buffer { - $dataView?: DataView; - toString(encoding?: BufferEncoding, start?: number, end?: number): string; toString(offset: number, length: number, encoding?: BufferEncoding): string; } -export function setBigUint64(this: BufferExt, offset, value, le) { - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setBigUint64( - offset, - value, - le, - ); -} - -export function readIntLE(this: BufferExt, offset, byteLength) { - if (offset === undefined) throw $ERR_INVALID_ARG_TYPE("offset", "number", offset); - if (typeof byteLength !== "number") throw $ERR_INVALID_ARG_TYPE("byteLength", "number", byteLength); - const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); - switch (byteLength) { - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: { - // Infinity must fall through to boundsError() so it reports the - // ">= 0 and <= N" range like Node, not "an integer". - if (typeof offset !== "number" || ((offset | 0) !== offset && offset !== Infinity && offset !== -Infinity)) - require("internal/validators").validateInteger(offset, "offset"); - let thisLength; - if (!(offset >= 0 && offset <= (thisLength = this.length) - byteLength)) - require("internal/buffer").boundsError(offset, (thisLength ?? this.length) - byteLength); - } - } - switch (byteLength) { - case 1: { - return view.getInt8(offset); - } - case 2: { - return view.getInt16(offset, true); - } - case 3: { - const val = view.getUint16(offset, true) + view.getUint8(offset + 2) * 2 ** 16; - return val | ((val & (2 ** 23)) * 0x1fe); - } - case 4: { - return view.getInt32(offset, true); - } - case 5: { - const last = view.getUint8(offset + 4); - return (last | ((last & (2 ** 7)) * 0x1fffffe)) * 2 ** 32 + view.getUint32(offset, true); - } - case 6: { - const last = view.getUint16(offset + 4, true); - return (last | ((last & (2 ** 15)) * 0x1fffe)) * 2 ** 32 + view.getUint32(offset, true); - } - } - require("internal/buffer").boundsError(byteLength, 6, "byteLength"); -} - -export function readIntBE(this: BufferExt, offset, byteLength) { - if (offset === undefined) throw $ERR_INVALID_ARG_TYPE("offset", "number", offset); - if (typeof byteLength !== "number") throw $ERR_INVALID_ARG_TYPE("byteLength", "number", byteLength); - const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); - switch (byteLength) { - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: { - // Infinity must fall through to boundsError() so it reports the - // ">= 0 and <= N" range like Node, not "an integer". - if (typeof offset !== "number" || ((offset | 0) !== offset && offset !== Infinity && offset !== -Infinity)) - require("internal/validators").validateInteger(offset, "offset"); - let thisLength; - if (!(offset >= 0 && offset <= (thisLength = this.length) - byteLength)) - require("internal/buffer").boundsError(offset, (thisLength ?? this.length) - byteLength); - } - } - switch (byteLength) { - case 1: { - return view.getInt8(offset); - } - case 2: { - return view.getInt16(offset, false); - } - case 3: { - const val = view.getUint16(offset + 1, false) + view.getUint8(offset) * 2 ** 16; - return val | ((val & (2 ** 23)) * 0x1fe); - } - case 4: { - return view.getInt32(offset, false); - } - case 5: { - const last = view.getUint8(offset); - return (last | ((last & (2 ** 7)) * 0x1fffffe)) * 2 ** 32 + view.getUint32(offset + 1, false); - } - case 6: { - const last = view.getUint16(offset, false); - return (last | ((last & (2 ** 15)) * 0x1fffe)) * 2 ** 32 + view.getUint32(offset + 2, false); - } - } - require("internal/buffer").boundsError(byteLength, 6, "byteLength"); -} - -export function readUIntLE(this: BufferExt, offset, byteLength) { - if (offset === undefined) throw $ERR_INVALID_ARG_TYPE("offset", "number", offset); - if (typeof byteLength !== "number") throw $ERR_INVALID_ARG_TYPE("byteLength", "number", byteLength); - const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); - switch (byteLength) { - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: { - // Infinity must fall through to boundsError() so it reports the - // ">= 0 and <= N" range like Node, not "an integer". - if (typeof offset !== "number" || ((offset | 0) !== offset && offset !== Infinity && offset !== -Infinity)) - require("internal/validators").validateInteger(offset, "offset"); - let thisLength; - if (!(offset >= 0 && offset <= (thisLength = this.length) - byteLength)) - require("internal/buffer").boundsError(offset, (thisLength ?? this.length) - byteLength); - } - } - switch (byteLength) { - case 1: { - return view.getUint8(offset); - } - case 2: { - return view.getUint16(offset, true); - } - case 3: { - return view.getUint16(offset, true) + view.getUint8(offset + 2) * 2 ** 16; - } - case 4: { - return view.getUint32(offset, true); - } - case 5: { - return view.getUint8(offset + 4) * 2 ** 32 + view.getUint32(offset, true); - } - case 6: { - return view.getUint16(offset + 4, true) * 2 ** 32 + view.getUint32(offset, true); - } - } - require("internal/buffer").boundsError(byteLength, 6, "byteLength"); -} - -export function readUIntBE(this: BufferExt, offset, byteLength) { - if (offset === undefined) throw $ERR_INVALID_ARG_TYPE("offset", "number", offset); - if (typeof byteLength !== "number") throw $ERR_INVALID_ARG_TYPE("byteLength", "number", byteLength); - const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); - switch (byteLength) { - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: { - // Infinity must fall through to boundsError() so it reports the - // ">= 0 and <= N" range like Node, not "an integer". - if (typeof offset !== "number" || ((offset | 0) !== offset && offset !== Infinity && offset !== -Infinity)) - require("internal/validators").validateInteger(offset, "offset"); - let thisLength; - if (!(offset >= 0 && offset <= (thisLength = this.length) - byteLength)) - require("internal/buffer").boundsError(offset, (thisLength ?? this.length) - byteLength); - } - } - switch (byteLength) { - case 1: { - return view.getUint8(offset); - } - case 2: { - return view.getUint16(offset, false); - } - case 3: { - return view.getUint16(offset + 1, false) + view.getUint8(offset) * 2 ** 16; - } - case 4: { - return view.getUint32(offset, false); - } - case 5: { - return view.getUint8(offset) * 2 ** 32 + view.getUint32(offset + 1, false); - } - case 6: { - return view.getUint16(offset, false) * 2 ** 32 + view.getUint32(offset + 2, false); - } - } - require("internal/buffer").boundsError(byteLength, 6, "byteLength"); -} - -export function writeIntLE(this: BufferExt, value, offset, byteLength) { - const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); - value = +value; - - switch (byteLength) { - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: { - const max = 2 ** (8 * byteLength - 1) - 1; - require("internal/buffer").checkInt(this, value, offset, -max - 1, max, byteLength); - break; - } - default: { - require("internal/buffer").boundsError(byteLength, 6, "byteLength"); - break; - } - } - switch (byteLength) { - case 1: { - view.setInt8(offset, value); - break; - } - case 2: { - view.setInt16(offset, value, true); - break; - } - case 3: { - view.setUint16(offset, value & 0xffff, true); - view.setInt8(offset + 2, Math.floor(value * 2 ** -16)); - break; - } - case 4: { - view.setInt32(offset, value, true); - break; - } - case 5: { - view.setUint32(offset, value | 0, true); - view.setInt8(offset + 4, Math.floor(value * 2 ** -32)); - break; - } - case 6: { - view.setUint32(offset, value | 0, true); - view.setInt16(offset + 4, Math.floor(value * 2 ** -32), true); - break; - } - } - return offset + byteLength; -} - -export function writeIntBE(this: BufferExt, value, offset, byteLength) { - const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); - value = +value; - - switch (byteLength) { - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: { - const max = 2 ** (8 * byteLength - 1) - 1; - require("internal/buffer").checkInt(this, value, offset, -max - 1, max, byteLength); - break; - } - default: { - require("internal/buffer").boundsError(byteLength, 6, "byteLength"); - break; - } - } - switch (byteLength) { - case 1: { - view.setInt8(offset, value); - break; - } - case 2: { - view.setInt16(offset, value, false); - break; - } - case 3: { - view.setUint16(offset + 1, value & 0xffff, false); - view.setInt8(offset, Math.floor(value * 2 ** -16)); - break; - } - case 4: { - view.setInt32(offset, value, false); - break; - } - case 5: { - view.setUint32(offset + 1, value | 0, false); - view.setInt8(offset, Math.floor(value * 2 ** -32)); - break; - } - case 6: { - view.setUint32(offset + 2, value | 0, false); - view.setInt16(offset, Math.floor(value * 2 ** -32), false); - break; - } - } - return offset + byteLength; -} - -export function writeUIntLE(this: BufferExt, value, offset, byteLength) { - const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); - value = +value; - - switch (byteLength) { - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: { - require("internal/buffer").checkInt(this, value, offset, 0, 2 ** (8 * byteLength) - 1, byteLength); - break; - } - default: { - require("internal/buffer").boundsError(byteLength, 6, "byteLength"); - break; - } - } - switch (byteLength) { - case 1: { - view.setUint8(offset, value); - break; - } - case 2: { - view.setUint16(offset, value, true); - break; - } - case 3: { - view.setUint16(offset, value & 0xffff, true); - view.setUint8(offset + 2, Math.floor(value * 2 ** -16)); - break; - } - case 4: { - view.setUint32(offset, value, true); - break; - } - case 5: { - view.setUint32(offset, value | 0, true); - view.setUint8(offset + 4, Math.floor(value * 2 ** -32)); - break; - } - case 6: { - view.setUint32(offset, value | 0, true); - view.setUint16(offset + 4, Math.floor(value * 2 ** -32), true); - break; - } - } - return offset + byteLength; -} - -export function writeUIntBE(this: BufferExt, value, offset, byteLength) { - const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); - value = +value; - - switch (byteLength) { - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: { - require("internal/buffer").checkInt(this, value, offset, 0, 2 ** (8 * byteLength) - 1, byteLength); - break; - } - default: { - require("internal/buffer").boundsError(byteLength, 6, "byteLength"); - break; - } - } - switch (byteLength) { - case 1: { - view.setUint8(offset, value); - break; - } - case 2: { - view.setUint16(offset, value, false); - break; - } - case 3: { - view.setUint16(offset + 1, value & 0xffff, false); - view.setUint8(offset, Math.floor(value * 2 ** -16)); - break; - } - case 4: { - view.setUint32(offset, value, false); - break; - } - case 5: { - view.setUint32(offset + 1, value | 0, false); - view.setUint8(offset, Math.floor(value * 2 ** -32)); - break; - } - case 6: { - view.setUint32(offset + 2, value | 0, false); - view.setUint16(offset, Math.floor(value * 2 ** -32), false); - break; - } - } - return offset + byteLength; -} - export function toJSON(this: BufferExt) { const type = "Buffer"; const data = Array.from(this); diff --git a/src/js/internal/buffer.ts b/src/js/internal/buffer.ts deleted file mode 100644 index 544913b85cfe..000000000000 --- a/src/js/internal/buffer.ts +++ /dev/null @@ -1,39 +0,0 @@ -const { validateNumber } = require("internal/validators"); - -function boundsError(value, length, type?) { - if (Math.floor(value) !== value) { - validateNumber(value, type); - throw $ERR_OUT_OF_RANGE(type || "offset", "an integer", value); - } - if (length < 0) throw $ERR_BUFFER_OUT_OF_BOUNDS(); - throw $ERR_OUT_OF_RANGE(type || "offset", `>= ${type ? 1 : 0} and <= ${length}`, value); -} - -function checkBounds(buf, offset, byteLength) { - validateNumber(offset, "offset"); - if (buf[offset] === undefined || buf[offset + byteLength - 1] === undefined) - boundsError(offset, buf.length - byteLength); -} - -function checkInt(buf, value, offset, min, max, byteLength) { - if (value > max || value < min) { - const n = typeof min === "bigint" ? "n" : ""; - let range; - if (byteLength > 4) { - if (min === 0 || min === 0n) { - range = `>= 0${n} and < 2${n} ** ${byteLength * 8}${n}`; - } else { - range = `>= -(2${n} ** ${byteLength * 8 - 1}${n}) and ` + `< 2${n} ** ${byteLength * 8 - 1}${n}`; - } - } else { - range = `>= ${min}${n} and <= ${max}${n}`; - } - throw $ERR_OUT_OF_RANGE("value", range, value); - } - checkBounds(buf, offset, byteLength); -} - -export default { - boundsError, - checkInt, -}; diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp index 13264550a7ca..cef4595f5535 100644 --- a/src/jsc/bindings/JSBuffer.cpp +++ b/src/jsc/bindings/JSBuffer.cpp @@ -167,6 +167,15 @@ JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeFloatLE); JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeFloatBE); JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeDoubleLE); JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeDoubleBE); +// Variable-width (byteLength 1..6) readers / writers. JIT-inlined when byteLength is a constant 1, 2 or 4. +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readIntLE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readIntBE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readUIntLE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readUIntBE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeIntLE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeIntBE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeUIntLE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeUIntBE); extern "C" EncodedJSValue WebCore_BufferEncodingType_toJS(JSC::JSGlobalObject* lexicalGlobalObject, WebCore::BufferEncodingType encoding) { @@ -3338,6 +3347,204 @@ DEFINE_BUFFER_WRITE(writeDoubleBE, double, false) #undef DEFINE_BUFFER_READ #undef DEFINE_BUFFER_WRITE +// Variable-width readers/writers: read(U)Int{LE,BE}(offset, byteLength) and +// write(U)Int{LE,BE}(value, offset, byteLength) with byteLength 1..6, per lib/internal/buffer.js. +namespace { + +// The byteLength argument: the two errors boundsError(byteLength, 6, "byteLength") produces. +static EncodedJSValue throwBufferInvalidByteLength(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSValue byteLengthValue) +{ + double byteLength = byteLengthValue.asNumber(); + if (std::floor(byteLength) != byteLength) + return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "byteLength"_s, "an integer"_s, byteLengthValue); + return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "byteLength"_s, ">= 1 and <= 6"_s, byteLengthValue); +} + +// The reader's offset validation: validateInteger() for non-numbers / fractions, then the +// this.length - byteLength range (boundsError). +static std::optional bufferReadVarWidthOffset(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSArrayBufferView* view, JSValue offsetValue, size_t byteLength) +{ + double offset; + if (offsetValue.isInt32()) [[likely]] + offset = offsetValue.asInt32(); + else { + offset = offsetValue.isNumber() ? offsetValue.asNumber() : 0; + // ((offset | 0) !== offset && offset !== +-Infinity) -> validateInteger(offset, "offset") + if (!offsetValue.isNumber() || (std::floor(offset) != offset && !std::isinf(offset)) || (std::isfinite(offset) && std::abs(offset) > 9007199254740991.0)) { + int32_t unused; + Bun::V::validateInteger(scope, lexicalGlobalObject, offsetValue, "offset"_s, jsUndefined(), jsUndefined(), &unused); + RETURN_IF_EXCEPTION(scope, std::nullopt); + } + } + size_t byteLengthOfView = view->byteLength(); + if (!(offset >= 0 && offset <= static_cast(byteLengthOfView) - static_cast(byteLength))) [[unlikely]] { + // boundsError(offset, length - byteLength) + if (std::floor(offset) != offset) { + Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, "an integer"_s, offsetValue); + return std::nullopt; + } + if (byteLengthOfView < byteLength) { + Bun::ERR::BUFFER_OUT_OF_BOUNDS(scope, lexicalGlobalObject, ""_s); + return std::nullopt; + } + Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, makeString(">= 0 and <= "_s, byteLengthOfView - byteLength), offsetValue); + return std::nullopt; + } + return static_cast(offset); +} + +// The writer's offset validation is checkBounds(): validateNumber(offset) then boundsError(). +static std::optional bufferWriteVarWidthOffset(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSArrayBufferView* view, JSValue offsetValue, size_t byteLength) +{ + if (!offsetValue.isNumber()) [[unlikely]] { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetValue); + return std::nullopt; + } + return bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->byteLength(), byteLength); +} + +// checkInt()'s value range for byteLength <= 4 and the ">= -(2 ** N) and < 2 ** N" wording it uses +// for the 5- and 6-byte widths. +static bool bufferWriteVarWidthCheckValue(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSValue valueValue, double number, size_t byteLength, bool isSigned) +{ + double min = isSigned ? -std::pow(2.0, 8.0 * byteLength - 1) : 0; + double max = (isSigned ? std::pow(2.0, 8.0 * byteLength - 1) : std::pow(2.0, 8.0 * byteLength)) - 1; + if (!(number > max || number < min)) [[likely]] + return true; + if (byteLength > 4) { + if (isSigned) + Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "value"_s, makeString(">= -(2 ** "_s, 8 * byteLength - 1, ") and < 2 ** "_s, 8 * byteLength - 1), jsNumber(number)); + else + Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "value"_s, makeString(">= 0 and < 2 ** "_s, 8 * byteLength), jsNumber(number)); + } else + Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "value"_s, min, max, jsNumber(number)); + return false; +} + +template +static JSC::EncodedJSValue bufferReadVarWidth(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue offsetValue = callFrame->argument(0); + JSValue byteLengthValue = callFrame->argument(1); + + if (offsetValue.isUndefined()) [[unlikely]] + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetValue); + if (!byteLengthValue.isNumber()) [[unlikely]] + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "byteLength"_s, "number"_s, byteLengthValue); + double byteLengthNumber = byteLengthValue.asNumber(); + if (!(byteLengthNumber >= 1 && byteLengthNumber <= 6 && std::floor(byteLengthNumber) == byteLengthNumber)) [[unlikely]] + return throwBufferInvalidByteLength(lexicalGlobalObject, scope, byteLengthValue); + size_t byteLength = static_cast(byteLengthNumber); + + auto* view = dynamicDowncast(callFrame->thisValue()); + if (!view) [[unlikely]] { + bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); + return {}; + } + auto checkedOffset = bufferReadVarWidthOffset(lexicalGlobalObject, scope, view, offsetValue, byteLength); + RETURN_IF_EXCEPTION(scope, {}); + if (!checkedOffset) + return {}; + + const uint8_t* address = static_cast(view->vector()) + *checkedOffset; + // Assemble the little-endian byte sequence, then reinterpret (matches the DataView arithmetic + // in lib/internal/buffer.js for every width, including the 3/5/6-byte sign extension). + uint64_t bits = 0; + for (size_t i = 0; i < byteLength; ++i) + bits |= static_cast(address[i]) << (8 * (isLittleEndian ? i : byteLength - 1 - i)); + if constexpr (isSigned) { + unsigned shift = 64 - 8 * byteLength; + int64_t value = static_cast(bits << shift) >> shift; + return JSValue::encode(jsNumber(static_cast(value))); + } else + return JSValue::encode(jsNumber(static_cast(bits))); +} + +template +static JSC::EncodedJSValue bufferWriteVarWidth(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue valueValue = callFrame->argument(0); + JSValue offsetValue = callFrame->argument(1); + JSValue byteLengthValue = callFrame->argument(2); + + // value = +value + double number; + if (valueValue.isNumber()) [[likely]] + number = valueValue.asNumber(); + else { + number = valueValue.toNumber(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + } + + if (!byteLengthValue.isNumber()) [[unlikely]] { + // boundsError(byteLength, 6, "byteLength") -> validateNumber(byteLength, "byteLength") + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "byteLength"_s, "number"_s, byteLengthValue); + } + double byteLengthNumber = byteLengthValue.asNumber(); + if (!(byteLengthNumber >= 1 && byteLengthNumber <= 6 && std::floor(byteLengthNumber) == byteLengthNumber)) [[unlikely]] + return throwBufferInvalidByteLength(lexicalGlobalObject, scope, byteLengthValue); + size_t byteLength = static_cast(byteLengthNumber); + + if (!bufferWriteVarWidthCheckValue(lexicalGlobalObject, scope, valueValue, number, byteLength, isSigned)) [[unlikely]] + return {}; + + if (!offsetValue.isNumber()) [[unlikely]] + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetValue); + auto* view = dynamicDowncast(callFrame->thisValue()); + if (!view) [[unlikely]] { + bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); + return {}; + } + auto checkedOffset = bufferWriteVarWidthOffset(lexicalGlobalObject, scope, view, offsetValue, byteLength); + RETURN_IF_EXCEPTION(scope, {}); + if (!checkedOffset) + return {}; + + // The stored bytes are the JS arithmetic of lib/internal/buffer.js: the low 32 bits are + // ToInt32(value); the high bytes (5/6-byte widths) are ToInt32(Math.floor(value * 2 ** -32)); + // for 3 bytes the high byte is Math.floor(value * 2 ** -16). + uint8_t bytes[8] = {}; + auto putLittleEndian = [&](size_t at, uint64_t chunk, size_t width) { + for (size_t i = 0; i < width; ++i) + bytes[at + i] = static_cast(chunk >> (8 * i)); + }; + switch (byteLength) { + case 1: + case 2: + case 4: + putLittleEndian(0, static_cast(JSC::toInt32(number)), byteLength); + break; + case 3: + putLittleEndian(0, static_cast(JSC::toInt32(number)), 2); + putLittleEndian(2, static_cast(JSC::toInt32(std::floor(number / 65536.0))), 1); + break; + case 5: + case 6: + putLittleEndian(0, static_cast(JSC::toInt32(number)), 4); + putLittleEndian(4, static_cast(JSC::toInt32(std::floor(number / 4294967296.0))), byteLength - 4); + break; + } + uint8_t* address = static_cast(view->vector()) + *checkedOffset; + for (size_t i = 0; i < byteLength; ++i) + address[i] = bytes[isLittleEndian ? i : byteLength - 1 - i]; + return JSValue::encode(jsNumber(*checkedOffset + byteLength)); +} + +} // namespace + +JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_readIntLE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { return bufferReadVarWidth(lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_readIntBE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { return bufferReadVarWidth(lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_readUIntLE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { return bufferReadVarWidth(lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_readUIntBE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { return bufferReadVarWidth(lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeIntLE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { return bufferWriteVarWidth(lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeIntBE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { return bufferWriteVarWidth(lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeUIntLE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { return bufferWriteVarWidth(lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeUIntBE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { return bufferWriteVarWidth(lexicalGlobalObject, callFrame); } + JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigInt64LE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = JSC::getVM(lexicalGlobalObject); @@ -3501,15 +3708,15 @@ static const HashTableValue JSBufferPrototypeTableValues[] { "readInt32BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readInt32BE, 1 } }, { "readInt32LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readInt32LE, 1 } }, { "readInt8"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readInt8, 1 } }, - { "readIntBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadIntBECodeGenerator, 1 } }, - { "readIntLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadIntLECodeGenerator, 1 } }, + { "readIntBE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readIntBE, 2 } }, + { "readIntLE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readIntLE, 2 } }, { "readUInt16BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUInt16BE, 1 } }, { "readUInt16LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUInt16LE, 1 } }, { "readUInt32BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUInt32BE, 1 } }, { "readUInt32LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUInt32LE, 1 } }, { "readUInt8"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUInt8, 1 } }, - { "readUIntBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadUIntBECodeGenerator, 1 } }, - { "readUIntLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadUIntLECodeGenerator, 1 } }, + { "readUIntBE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUIntBE, 2 } }, + { "readUIntLE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUIntLE, 2 } }, { "slice"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_slice, 2 } }, { "subarray"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_slice, 2 } }, @@ -3541,8 +3748,8 @@ static const HashTableValue JSBufferPrototypeTableValues[] { "writeInt32BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeInt32BE, 2 } }, { "writeInt32LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeInt32LE, 2 } }, { "writeInt8"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeInt8, 2 } }, - { "writeIntBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteIntBECodeGenerator, 1 } }, - { "writeIntLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteIntLECodeGenerator, 1 } }, + { "writeIntBE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeIntBE, 3 } }, + { "writeIntLE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeIntLE, 3 } }, { "writeUInt16"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt16LE, 2 } }, { "writeUInt16BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt16BE, 2 } }, { "writeUInt16LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt16LE, 2 } }, @@ -3550,8 +3757,8 @@ static const HashTableValue JSBufferPrototypeTableValues[] { "writeUInt32BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt32BE, 2 } }, { "writeUInt32LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt32LE, 2 } }, { "writeUInt8"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt8, 2 } }, - { "writeUIntBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUIntBECodeGenerator, 1 } }, - { "writeUIntLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUIntLECodeGenerator, 1 } }, + { "writeUIntBE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUIntBE, 3 } }, + { "writeUIntLE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUIntLE, 3 } }, }; // TODO: add this as a feature to the hash table generator mechanism above so that we can avoid all the unnecessary extra calls to `Identifier::fromString` and `this->getDirect`. @@ -3577,6 +3784,20 @@ static void registerBufferAccessor(JSC::NativeFunction function) JSC::registerBufferAccessor(JSC::toTagged(function), { data, isWrite }); } +// read(U)Int{LE,BE} / write(U)Int{LE,BE}: the width comes from the byteLength argument, so the JIT +// only inlines call sites whose byteLength is a constant 1, 2 or 4. +template +static void registerBufferVarWidthAccessor(JSC::NativeFunction function) +{ + JSC::DFG::DataViewData data {}; + data.byteSize = 0; + data.isSigned = isSigned; + data.isFloatingPoint = false; + data.isResizable = false; + data.isLittleEndian = triState(isLittleEndian); + JSC::registerBufferAccessor(JSC::toTagged(function), { data, isWrite, /* byteLengthFromArgument */ true }); +} + // Process-global (the descriptor belongs to the function pointer): once, before the functions are // reachable from JS. static void registerBufferAccessorsWithJSC() @@ -3619,6 +3840,14 @@ static void registerBufferAccessorsWithJSC() registerBufferAccessor(jsBufferPrototypeFunction_writeBigInt64BE); registerBufferAccessor(jsBufferPrototypeFunction_writeBigUInt64LE); registerBufferAccessor(jsBufferPrototypeFunction_writeBigUInt64BE); + registerBufferVarWidthAccessor(jsBufferPrototypeFunction_readIntLE); + registerBufferVarWidthAccessor(jsBufferPrototypeFunction_readIntBE); + registerBufferVarWidthAccessor(jsBufferPrototypeFunction_readUIntLE); + registerBufferVarWidthAccessor(jsBufferPrototypeFunction_readUIntBE); + registerBufferVarWidthAccessor(jsBufferPrototypeFunction_writeIntLE); + registerBufferVarWidthAccessor(jsBufferPrototypeFunction_writeIntBE); + registerBufferVarWidthAccessor(jsBufferPrototypeFunction_writeUIntLE); + registerBufferVarWidthAccessor(jsBufferPrototypeFunction_writeUIntBE); }); } diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index af2380136778..e5d738d884d1 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -4805,4 +4805,50 @@ describe("read*/write* after JIT tier-up", () => { expect(() => Buffer.prototype.readInt32LE.call({}, 0)).toThrow(TypeError); } }); + + it("variable-width readers/writers match across widths after tier-up", () => { + const uint = (o, l, le) => { + let value = 0; + for (let i = 0; i < l; ++i) value = le ? value + buf[o + i] * 2 ** (8 * i) : value * 256 + buf[o + i]; + return value; + }; + const sint = (o, l, le) => { + const value = uint(o, l, le); + return value >= 2 ** (8 * l - 1) ? value - 2 ** (8 * l) : value; + }; + // A constant byteLength (JIT-inlined for 1/2/4) and a varying one (host path). + const readConst3 = (b, o) => b.readUIntBE(o, 3); + const readConst4 = (b, o) => b.readIntLE(o, 4); + let mismatches = 0; + for (let i = 0; i < 5000; i++) { + const o = i & 15; + const l = 1 + (i % 6); + if (buf.readIntLE(o, l) !== sint(o, l, true)) mismatches++; + if (buf.readIntBE(o, l) !== sint(o, l, false)) mismatches++; + if (buf.readUIntLE(o, l) !== uint(o, l, true)) mismatches++; + if (buf.readUIntBE(o, l) !== uint(o, l, false)) mismatches++; + if (readConst3(buf, o) !== uint(o, 3, false)) mismatches++; + if (readConst4(buf, o) !== dv.getInt32(o, true)) mismatches++; + } + expect(mismatches).toBe(0); + const scratch = Buffer.alloc(16); + for (let i = 0; i < 5000; i++) { + const l = 1 + (i % 6); + const v = i % 100; + expect(scratch.writeUIntLE(v, 0, l)).toBe(l); + expect(scratch.readUIntLE(0, l)).toBe(v); + expect(scratch.writeIntBE(-v, 8, l)).toBe(8 + l); + expect(scratch.readIntBE(8, l)).toBe(-v | 0); + } + let codes = []; + for (let i = 0; i < 1000; i++) { + codes = [ + codeOf(() => buf.readIntLE(0, 7)), + codeOf(() => buf.readIntLE(undefined, 4)), + codeOf(() => readConst4(buf, 61)), + codeOf(() => scratch.writeIntLE(2 ** 24, 0, 3)), + ]; + } + expect(codes).toEqual(["ERR_OUT_OF_RANGE", "ERR_INVALID_ARG_TYPE", "ERR_OUT_OF_RANGE", "ERR_OUT_OF_RANGE"]); + }); }); From f289a679f7468d5bc498a97b4d193f1cbb658aa2 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 23 Jul 2026 20:13:33 -0700 Subject: [PATCH 06/25] Remove the now-unused dataView private name and dead var-width helper params --- src/js/builtins.d.ts | 1 - src/js/builtins/BunBuiltinNames.h | 1 - src/jsc/bindings/JSBuffer.cpp | 17 +- test/js/node/buffer-jit.test.ts | 340 ++++++++++++++++++++++++++++++ 4 files changed, 344 insertions(+), 15 deletions(-) create mode 100644 test/js/node/buffer-jit.test.ts diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index 253a3c5b1618..a33ca117ddaf 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -338,7 +338,6 @@ declare function $controller(): TODO; declare function $createFIFO(): TODO; declare function $createUninitializedArrayBuffer(size: number): ArrayBuffer; declare function $data(): TODO; -declare function $dataView(): TODO; declare function $decode(): TODO; declare function $dirname(): TODO; declare function $disturbed(): TODO; diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h index 2ddf702b46c8..8fe6a5819061 100644 --- a/src/js/builtins/BunBuiltinNames.h +++ b/src/js/builtins/BunBuiltinNames.h @@ -73,7 +73,6 @@ using namespace JSC; macro(createUninitializedArrayBuffer) \ macro(ctimeMs) \ macro(data) \ - macro(dataView) \ macro(decode) \ macro(dest) \ macro(dirname) \ diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp index cef4595f5535..7bd5e8d6677b 100644 --- a/src/jsc/bindings/JSBuffer.cpp +++ b/src/jsc/bindings/JSBuffer.cpp @@ -3393,19 +3393,9 @@ static std::optional bufferReadVarWidthOffset(JSC::JSGlobalObject* lexic return static_cast(offset); } -// The writer's offset validation is checkBounds(): validateNumber(offset) then boundsError(). -static std::optional bufferWriteVarWidthOffset(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSArrayBufferView* view, JSValue offsetValue, size_t byteLength) -{ - if (!offsetValue.isNumber()) [[unlikely]] { - Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetValue); - return std::nullopt; - } - return bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->byteLength(), byteLength); -} - // checkInt()'s value range for byteLength <= 4 and the ">= -(2 ** N) and < 2 ** N" wording it uses // for the 5- and 6-byte widths. -static bool bufferWriteVarWidthCheckValue(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSValue valueValue, double number, size_t byteLength, bool isSigned) +static bool bufferWriteVarWidthCheckValue(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, double number, size_t byteLength, bool isSigned) { double min = isSigned ? -std::pow(2.0, 8.0 * byteLength - 1) : 0; double max = (isSigned ? std::pow(2.0, 8.0 * byteLength - 1) : std::pow(2.0, 8.0 * byteLength)) - 1; @@ -3489,7 +3479,7 @@ static JSC::EncodedJSValue bufferWriteVarWidth(JSC::JSGlobalObject* lexicalGloba return throwBufferInvalidByteLength(lexicalGlobalObject, scope, byteLengthValue); size_t byteLength = static_cast(byteLengthNumber); - if (!bufferWriteVarWidthCheckValue(lexicalGlobalObject, scope, valueValue, number, byteLength, isSigned)) [[unlikely]] + if (!bufferWriteVarWidthCheckValue(lexicalGlobalObject, scope, number, byteLength, isSigned)) [[unlikely]] return {}; if (!offsetValue.isNumber()) [[unlikely]] @@ -3499,7 +3489,8 @@ static JSC::EncodedJSValue bufferWriteVarWidth(JSC::JSGlobalObject* lexicalGloba bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); return {}; } - auto checkedOffset = bufferWriteVarWidthOffset(lexicalGlobalObject, scope, view, offsetValue, byteLength); + // checkBounds(): the offset type was validated above; the range check is boundsError(). + auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->byteLength(), byteLength); RETURN_IF_EXCEPTION(scope, {}); if (!checkedOffset) return {}; diff --git a/test/js/node/buffer-jit.test.ts b/test/js/node/buffer-jit.test.ts new file mode 100644 index 000000000000..12c55f726b26 --- /dev/null +++ b/test/js/node/buffer-jit.test.ts @@ -0,0 +1,340 @@ +// JIT behavior of Buffer.prototype.read* / write*: these are native functions that JSC's DFG/FTL +// compile into bounds-checked loads/stores on the receiver's storage (JSBuffer.cpp + +// JavaScriptCore's BufferAccessorRegistry). Plain Buffer semantics live in buffer.test.js; this +// file pins the *compiler* behavior: that the JIT path is really taken and converges, that every +// speculation failure lands back on the correct host behavior, that loads and stores are not +// mis-ordered or mis-CSE'd, and that swapping the method is respected. +// +// It runs each scenario in a fresh subprocess with the concurrent JIT off and a deterministic tier-up +// policy, so numberOfDFGCompiles() is meaningful. +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + +async function run(source: string) { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", source], + env: { + ...bunEnv, + BUN_JSC_useConcurrentJIT: "0", + // Tier up quickly and deterministically, but not so eagerly that profiling is skipped. + BUN_JSC_jitPolicyScale: "0.05", + }, + stderr: "pipe", + stdout: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout, stderr, exitCode }; +} + +// Shared prelude: helpers + a deterministic buffer. +const prelude = ` +const { numberOfDFGCompiles, noInline } = require("bun:jsc"); +function assert(condition, message) { if (!condition) throw new Error("Assertion failed: " + message); } +const buf = Buffer.alloc(256); +const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); +for (let i = 0; i < buf.length; i++) buf[i] = (i * 37 + 11) & 0xff; +const N = 20000; +`; + +describe("Buffer accessor JIT", () => { + test("the JIT path is actually taken, and compile counts converge", async () => { + const { stdout, stderr, exitCode } = await run( + prelude + + ` + function read(b, o) { return b.readInt32LE(o); } + function write(b, v, o) { return b.writeInt32LE(v, o); } + noInline(read); noInline(write); + for (let i = 0; i < N; i++) { + assert(read(buf, i & 63) === dv.getInt32(i & 63, true), "read"); + assert(write(buf, i, (i & 63) + 128) === (i & 63) + 132, "write"); + assert(dv.getInt32((i & 63) + 128, true) === i, "write store"); + } + const readCompiles = numberOfDFGCompiles(read); + const writeCompiles = numberOfDFGCompiles(write); + // Compiled at least once (the intrinsic did not stop the DFG from taking these), and no + // OSR-exit -> recompile storm: a well-behaved call site converges in a handful of compiles. + assert(readCompiles >= 1 && readCompiles <= 4, "read compiles: " + readCompiles); + assert(writeCompiles >= 1 && writeCompiles <= 4, "write compiles: " + writeCompiles); + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }); + + test("each speculation exit converges instead of looping", async () => { + // Warm up on the fast path, then keep triggering one exit kind at the same call site: the + // site must fall back to a stable state (bounded recompiles), not exit -> recompile forever. + const { stdout, stderr, exitCode } = await run( + prelude + + ` + function readAt(b, o) { return b.readInt32LE(o); } + noInline(readAt); + function writeAt(b, v, o) { return b.writeInt8(v, o); } + noInline(writeAt); + for (let i = 0; i < N; i++) { + readAt(buf, i & 63); + writeAt(buf, i & 127, i & 63); + } + const badReceiver = { length: 4 }; + const detached = Buffer.alloc(8); + structuredClone(detached.buffer, { transfer: [detached.buffer] }); + const exits = [ + () => readAt(buf, buf.length), // out of bounds + () => readAt(buf, -1), // negative offset + () => readAt(buf, 1.5), // fractional offset + () => readAt(buf, "4"), // wrong offset type + () => readAt.call(null, badReceiver, 0), // wrong receiver (a plain object as this-arg buffer) + () => detached.readInt32LE(0), // detached + () => writeAt(buf, 200, 0), // value out of int8 range + () => writeAt(buf, 1.5, 0), // fractional value (host truncates; no throw) + ]; + for (const trigger of exits) { + for (let i = 0; i < 2000; i++) { + try { trigger(); } catch {} + readAt(buf, i & 63); // and the fast path keeps working in between + } + } + const compiles = Math.max(numberOfDFGCompiles(readAt), numberOfDFGCompiles(writeAt)); + assert(compiles <= 8, "compile count did not converge: " + compiles); + assert(readAt(buf, 12) === dv.getInt32(12, true), "still correct after all the exits"); + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }); + + test("host semantics survive every exit path (results, not just compile counts)", async () => { + const { stdout, stderr, exitCode } = await run( + prelude + + ` + function read(b, o) { return b.readUInt16BE(o); } + function write(b, v, o) { return b.writeUInt16LE(v, o); } + noInline(read); noInline(write); + for (let i = 0; i < N; i++) { read(buf, i & 63); write(buf, i & 0xffff, (i & 63) + 128); } + // Non-int32 but valid inputs the JIT does not speculate on must produce the host result. + assert(read(buf, 4.0) === dv.getUint16(4, false), "integral double offset"); + const scratch = Buffer.alloc(8); + assert(scratch.writeUInt16LE(1.5, 0) === 2, "fractional value returns offset + 2"); + assert(scratch.readUInt16LE(0) === 1, "fractional value truncates"); + assert(scratch.writeUInt16LE(NaN, 0) === 2 && scratch.readUInt16LE(0) === 0, "NaN stores 0"); + assert(scratch.writeUInt16LE({ valueOf() { return 7; } }, 2) === 4 && scratch.readUInt16LE(2) === 7, "valueOf value"); + let coerced = 0; + const counting = { valueOf() { coerced++; return 300; } }; + for (let i = 0; i < 1000; i++) { + try { scratch.writeInt8(counting, 0); } catch (e) { assert(e.code === "ERR_OUT_OF_RANGE", "range code"); } + try { scratch.writeInt8(counting, 100); } catch (e) { assert(e.code === "ERR_OUT_OF_RANGE", "value checked before offset for 1-byte writes? no: offset first"); } + } + assert(coerced === 2000, "value coerced exactly once per call, even when it then throws: " + coerced); + // The BigInt writers: too-wide BigInts throw; the widest valid ones store. + const bb = Buffer.alloc(8); + const bd = new DataView(bb.buffer, bb.byteOffset, 8); + for (let i = 0; i < 2000; i++) { + assert(bb.writeBigInt64LE(-(2n ** 63n), 0) === 8 && bd.getBigInt64(0, true) === -(2n ** 63n), "int64 min"); + assert(bb.writeBigUInt64BE(2n ** 64n - 1n, 0) === 8 && bd.getBigUint64(0, false) === 2n ** 64n - 1n, "uint64 max"); + try { bb.writeBigInt64LE(2n ** 63n, 0); assert(false, "should throw"); } catch (e) { assert(e.code === "ERR_OUT_OF_RANGE", "int64 too big"); } + try { bb.writeBigUInt64LE(-1n, 0); assert(false, "should throw"); } catch (e) { assert(e.code === "ERR_OUT_OF_RANGE", "uint64 negative"); } + } + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }); + + test("no stale reads: a store between two loads of the same offset is observed", async () => { + const { stdout, stderr, exitCode } = await run( + prelude + + ` + function readWriteRead(b, o, v) { + const before = b.readInt32LE(o); + b.writeInt32LE(v, o); + const after = b.readInt32LE(o); // must not be CSE'd with 'before' + return before * 3 + after; // use both so neither is dead + } + noInline(readWriteRead); + for (let i = 0; i < N; i++) { + const o = (i & 31) * 4; + const before = dv.getInt32(o, true); + assert(readWriteRead(buf, o, i) === before * 3 + i, "write between reads at iteration " + i); + } + // Same, but the store goes through a plain typed-array element write and a DataView. + function readAroundOtherStores(b, o, v) { + const a = b.readUInt8(o); + b[o] = v & 0xff; + const c = b.readUInt8(o); + dv.setUint8(o, (v + 1) & 0xff); + const d = b.readUInt8(o); + return [a, c, d]; + } + noInline(readAroundOtherStores); + for (let i = 0; i < N; i++) { + const o = i & 127; + const a0 = buf[o]; + const [a, c, d] = readAroundOtherStores(buf, o, i); + assert(a === a0 && c === (i & 0xff) && d === ((i + 1) & 0xff), "typed array / DataView stores observed at " + i); + } + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }); + + test("two Buffers over the same ArrayBuffer are never mis-aliased", async () => { + const { stdout, stderr, exitCode } = await run( + prelude + + ` + const backing = new ArrayBuffer(64); + const a = Buffer.from(backing); // views over the SAME memory + const b = Buffer.from(backing, 8, 32); + const raw = new Uint8Array(backing); + function crossViewReadAfterWrite(x) { + const before = a.readInt32LE(8); // a[8..12) is b[0..4) + b.writeInt32LE(x, 0); // store through the other view + const after = a.readInt32LE(8); // must observe it: no CSE across the store + return { before, after }; + } + noInline(crossViewReadAfterWrite); + let previous = raw[8] | (raw[9] << 8) | (raw[10] << 16) | (raw[11] << 24); + for (let i = 0; i < N; i++) { + const { before, after } = crossViewReadAfterWrite(i); + assert(before === previous && after === i, "cross-view store observed at " + i + ": " + before + "/" + after); + previous = i; + } + // Overlapping views + a loop the compiler will try to hoist loads out of. + function sumWhileWriting(iters) { + let sum = 0; + for (let i = 0; i < iters; i++) { + sum += a.readUInt8(12); + b.writeUInt8((sum + i) & 0xff, 4); // b[4] is a[12]: the load above cannot be hoisted + } + return sum; + } + noInline(sumWhileWriting); + a.writeUInt8(3, 12); + let expected = 0, cell = 3; + for (let i = 0; i < 500; i++) { expected += cell; cell = (expected + i) & 0xff; } + for (let i = 0; i < 200; i++) { + a.writeUInt8(3, 12); + assert(sumWhileWriting(500) === expected, "loop with aliasing store"); + } + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }); + + test("a loop-carried load is not kept alive across a call that mutates the buffer", async () => { + const { stdout, stderr, exitCode } = await run( + prelude + + ` + let mutations = 0; + function mutate(b) { b.writeUInt8((++mutations) & 0xff, 0); } + noInline(mutate); + function readAroundCall(b, iters) { + let last = 0; + for (let i = 0; i < iters; i++) { + const v = b.readUInt8(0); // loop-invariant address, but the call below may write it + mutate(b); + last = v; + } + return last; + } + noInline(readAroundCall); + for (let i = 0; i < 300; i++) { + buf.writeUInt8(200, 0); + mutations = 0; + const last = readAroundCall(buf, 100); + assert(last === (99 & 0xff), "the read is re-done each iteration after the call: got " + last); + } + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }); + + test("replacing or shadowing the method after tier-up takes effect", async () => { + const { stdout, stderr, exitCode } = await run( + prelude + + ` + function readViaMethod(b, o) { return b.readInt32LE(o); } + noInline(readViaMethod); + for (let i = 0; i < N; i++) assert(readViaMethod(buf, i & 63) === dv.getInt32(i & 63, true), "warm"); + + // 1. Shadow on one instance: only that receiver changes behavior. + const special = Buffer.alloc(16); + special.readInt32LE = function () { return 424242; }; + for (let i = 0; i < 5000; i++) { + assert(readViaMethod(special, 0) === 424242, "instance shadow at " + i); + assert(readViaMethod(buf, 4) === dv.getInt32(4, true), "normal buffer unaffected at " + i); + } + + // 2. Replace on the prototype: every receiver changes behavior, immediately. + const original = Buffer.prototype.readInt32LE; + Buffer.prototype.readInt32LE = function (o) { return -original.call(this, o) - 1; }; + for (let i = 0; i < 5000; i++) { + assert(readViaMethod(buf, i & 63) === -dv.getInt32(i & 63, true) - 1, "prototype replaced at " + i); + } + Buffer.prototype.readInt32LE = original; + for (let i = 0; i < 5000; i++) { + assert(readViaMethod(buf, i & 63) === dv.getInt32(i & 63, true), "prototype restored at " + i); + } + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }); + + test("resizable and growable receivers keep tracking the length after tier-up", async () => { + const { stdout, stderr, exitCode } = await run( + prelude + + ` + function readEnd(b) { return b.readUInt16LE(b.length - 2); } + function readAt(b, o) { return b.readUInt16LE(o); } + noInline(readEnd); noInline(readAt); + // Warm on fixed-size buffers first so the resizable ones arrive after optimization. + for (let i = 0; i < N; i++) { readEnd(buf); readAt(buf, i & 63); } + + const rab = new ArrayBuffer(16, { maxByteLength: 128 }); + const tracking = Buffer.from(rab); // length-tracking view + tracking.writeUInt16LE(0xabcd, 14); + for (let i = 0; i < 5000; i++) assert(readEnd(tracking) === 0xabcd, "before grow"); + rab.resize(128); + tracking.writeUInt16LE(0x1234, 126); + for (let i = 0; i < 5000; i++) { + assert(readEnd(tracking) === 0x1234, "after grow"); + assert(readAt(tracking, 126) === 0x1234, "read into the grown region"); + } + rab.resize(8); + for (let i = 0; i < 2000; i++) { + try { readAt(tracking, 14); assert(false, "must throw after shrink"); } + catch (e) { assert(e.code === "ERR_OUT_OF_RANGE" || e.code === "ERR_BUFFER_OUT_OF_BOUNDS", "shrink error: " + e.code); } + } + + const gsab = new SharedArrayBuffer(16, { maxByteLength: 128 }); + const shared = Buffer.from(gsab); + shared.writeUInt16LE(0x5678, 14); + for (let i = 0; i < 5000; i++) assert(readEnd(shared) === 0x5678, "shared before grow"); + gsab.grow(128); + shared.writeUInt16LE(0x9abc, 126); + for (let i = 0; i < 5000; i++) assert(readEnd(shared) === 0x9abc, "shared after grow"); + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }); +}); From 77ba5319859dc3bec350afefa5595c2535e2671d Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 23 Jul 2026 21:07:22 -0700 Subject: [PATCH 07/25] Build against the WebKit preview build for oven-sh/WebKit#330 --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index fbb60710f023..b6ae4676da97 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -10,7 +10,7 @@ // Windows ICU data table filtered + per-item zstd compressed, and Windows // unwind info (RtlAddGrowableFunctionTable) registered for the fixed JIT // pool (LLInt pending offlineasm .seh_* emission). -export const WEBKIT_VERSION = "a40d462206e1caf8388062120acde61e37a4ae7d"; +export const WEBKIT_VERSION = "autobuild-preview-pr-330-be77ad70"; /** * WebKit (JavaScriptCore) — the JS engine. From c4040983aa6a1ba54e9947fe0593fcc6913d60cb Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 24 Jul 2026 00:04:57 -0700 Subject: [PATCH 08/25] Bump the WebKit preview build (Windows JIT handler merge, write range checks) --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index b6ae4676da97..61c3290166be 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -10,7 +10,7 @@ // Windows ICU data table filtered + per-item zstd compressed, and Windows // unwind info (RtlAddGrowableFunctionTable) registered for the fixed JIT // pool (LLInt pending offlineasm .seh_* emission). -export const WEBKIT_VERSION = "autobuild-preview-pr-330-be77ad70"; +export const WEBKIT_VERSION = "autobuild-preview-pr-330-6d8df126"; /** * WebKit (JavaScriptCore) — the JS engine. From af34232046029893be68c2b87e34477c784965aa Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 08:29:46 +0000 Subject: [PATCH 09/25] [autofix.ci] apply automated fixes --- scripts/build/deps/webkit.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index fe168f427f8d..94bc96f033bd 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -5,7 +5,6 @@ */ export const WEBKIT_VERSION = "autobuild-preview-pr-330-6d8df126"; - /** * WebKit (JavaScriptCore) — the JS engine. * From 9bb5bd79bd3d8d905e7d71b65cfd81c5f87ba67c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 24 Jul 2026 01:35:36 -0700 Subject: [PATCH 10/25] Pin the NaN / Infinity write semantics after tier-up --- test/js/node/buffer-jit.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/js/node/buffer-jit.test.ts b/test/js/node/buffer-jit.test.ts index 12c55f726b26..d96de779070b 100644 --- a/test/js/node/buffer-jit.test.ts +++ b/test/js/node/buffer-jit.test.ts @@ -130,6 +130,17 @@ describe("Buffer accessor JIT", () => { } assert(coerced === 2000, "value coerced exactly once per call, even when it then throws: " + coerced); // The BigInt writers: too-wide BigInts throw; the widest valid ones store. + // NaN and +-Infinity, which the value range check treats differently (Node stores 0 for NaN + // but throws for the infinities), must keep doing so once the write is JIT-compiled. + for (let i = 0; i < 2000; i++) { + assert(scratch.writeInt8(NaN, 0) === 1 && scratch.readInt8(0) === 0, "NaN stores 0 (int8)"); + assert(scratch.writeUInt16LE(NaN, 0) === 2 && scratch.readUInt16LE(0) === 0, "NaN stores 0 (uint16)"); + assert(scratch.writeUIntLE(NaN, 0, 3) === 3 && scratch.readUIntLE(0, 3) === 0, "NaN stores 0 (var width)"); + for (const [f, what] of [[() => scratch.writeInt8(Infinity, 0), "int8 +Inf"], [() => scratch.writeInt8(-Infinity, 0), "int8 -Inf"], [() => scratch.writeIntLE(Infinity, 0, 4), "var-width +Inf"]]) { + try { f(); assert(false, what + " should throw"); } catch (e) { assert(e.code === "ERR_OUT_OF_RANGE", what + ": " + e.code); } + } + } + const bb = Buffer.alloc(8); const bd = new DataView(bb.buffer, bb.byteOffset, 8); for (let i = 0; i < 2000; i++) { From c24eaa7fbc1ebd612147960e1ce11a4e6f2fa636 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 24 Jul 2026 02:09:24 -0700 Subject: [PATCH 11/25] Run the buffer JIT tests concurrently; make the bad-receiver case reach the accessor --- test/js/node/buffer-jit.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/js/node/buffer-jit.test.ts b/test/js/node/buffer-jit.test.ts index d96de779070b..61dcede3e7f3 100644 --- a/test/js/node/buffer-jit.test.ts +++ b/test/js/node/buffer-jit.test.ts @@ -36,7 +36,7 @@ for (let i = 0; i < buf.length; i++) buf[i] = (i * 37 + 11) & 0xff; const N = 20000; `; -describe("Buffer accessor JIT", () => { +describe.concurrent("Buffer accessor JIT", () => { test("the JIT path is actually taken, and compile counts converge", async () => { const { stdout, stderr, exitCode } = await run( prelude + @@ -77,7 +77,9 @@ describe("Buffer accessor JIT", () => { readAt(buf, i & 63); writeAt(buf, i & 127, i & 63); } - const badReceiver = { length: 4 }; + // Carries the real accessor, so the call reaches it and fails the receiver check itself + // (rather than throwing "not a function" at the property lookup). + const badReceiver = { length: 4, readInt32LE: Buffer.prototype.readInt32LE }; const detached = Buffer.alloc(8); structuredClone(detached.buffer, { transfer: [detached.buffer] }); const exits = [ @@ -85,7 +87,7 @@ describe("Buffer accessor JIT", () => { () => readAt(buf, -1), // negative offset () => readAt(buf, 1.5), // fractional offset () => readAt(buf, "4"), // wrong offset type - () => readAt.call(null, badReceiver, 0), // wrong receiver (a plain object as this-arg buffer) + () => readAt(badReceiver, 0), // wrong receiver: a plain object () => detached.readInt32LE(0), // detached () => writeAt(buf, 200, 0), // value out of int8 range () => writeAt(buf, 1.5, 0), // fractional value (host truncates; no throw) From e702b53dacf5f84813a746fdb33a387e47bd80e9 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 24 Jul 2026 02:27:39 -0700 Subject: [PATCH 12/25] Trigger the detached-receiver exit through the measured call site --- test/js/node/buffer-jit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js/node/buffer-jit.test.ts b/test/js/node/buffer-jit.test.ts index 61dcede3e7f3..e78ada49db05 100644 --- a/test/js/node/buffer-jit.test.ts +++ b/test/js/node/buffer-jit.test.ts @@ -88,7 +88,7 @@ describe.concurrent("Buffer accessor JIT", () => { () => readAt(buf, 1.5), // fractional offset () => readAt(buf, "4"), // wrong offset type () => readAt(badReceiver, 0), // wrong receiver: a plain object - () => detached.readInt32LE(0), // detached + () => readAt(detached, 0), // detached () => writeAt(buf, 200, 0), // value out of int8 range () => writeAt(buf, 1.5, 0), // fractional value (host truncates; no throw) ]; From 6382eeb7430862c57ae896537eb18ca96df6f655 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 24 Jul 2026 20:13:33 -0700 Subject: [PATCH 13/25] Bound the accessors by the receiver's element count, trim tier-up loops The bounds check used the receiver's byteLength; lib/internal/buffer.js (and the deleted builtins) bound by this.length -- the element count -- which only equals byteLength for byte-sized views. Buffer methods .call'd on a wider view now throw ERR_OUT_OF_RANGE where they used to, matching Node. Also cut the in-process tier-up loop counts in buffer.test.js. --- src/jsc/bindings/JSBuffer.cpp | 22 +++++++++++----------- test/js/node/buffer.test.js | 20 ++++++++++++++------ 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp index 7bd5e8d6677b..0f6919dbf938 100644 --- a/src/jsc/bindings/JSBuffer.cpp +++ b/src/jsc/bindings/JSBuffer.cpp @@ -3102,7 +3102,7 @@ static bool bufferAccessCheckOffsetType(JSC::JSGlobalObject* lexicalGlobalObject // boundsError(offset, byteLength - byteSize): the same ERR_OUT_OF_RANGE / ERR_BUFFER_OUT_OF_BOUNDS as // lib/internal/buffer.js. Returns the offset when it is in range after all (an integral double). -static std::optional bufferAccessCheckOffsetBounds(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSValue offsetValue, size_t byteLength, size_t byteSize) +static std::optional bufferAccessCheckOffsetBounds(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSValue offsetValue, size_t viewLength, size_t byteSize) { double offset = offsetValue.asNumber(); // Math.floor(value) !== value: NaN and fractions are "an integer"; +-Infinity get the range error. @@ -3110,11 +3110,11 @@ static std::optional bufferAccessCheckOffsetBounds(JSC::JSGlobalObject* Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, "an integer"_s, offsetValue); return std::nullopt; } - if (byteLength < byteSize) [[unlikely]] { + if (viewLength < byteSize) [[unlikely]] { Bun::ERR::BUFFER_OUT_OF_BOUNDS(scope, lexicalGlobalObject, ""_s); return std::nullopt; } - size_t maxOffset = byteLength - byteSize; + size_t maxOffset = viewLength - byteSize; if (!(offset >= 0 && offset <= static_cast(maxOffset))) [[unlikely]] { Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, makeString(">= 0 and <= "_s, maxOffset), offsetValue); return std::nullopt; @@ -3168,8 +3168,8 @@ static JSC::EncodedJSValue bufferRead(JSC::JSGlobalObject* lexicalGlobalObject, // The fast path: an int32 (or missing) offset inside a real ArrayBufferView -- what the JIT'd form assumes. if (view && (offsetValue.isInt32() || offsetValue.isUndefined())) [[likely]] { int32_t offset32 = offsetValue.isInt32() ? offsetValue.asInt32() : 0; - size_t byteLength = view->byteLength(); - if (offset32 >= 0 && static_cast(offset32) + byteSize <= byteLength) [[likely]] { + size_t viewLength = view->length(); + if (offset32 >= 0 && static_cast(offset32) + byteSize <= viewLength) [[likely]] { offset = offset32; goto fastPath; } @@ -3185,7 +3185,7 @@ static JSC::EncodedJSValue bufferRead(JSC::JSGlobalObject* lexicalGlobalObject, bufferAccessReceiver(lexicalGlobalObject, scope, thisValue); return {}; } - auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->byteLength(), byteSize); + auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->length(), byteSize); if (!checkedOffset) return {}; offset = *checkedOffset; @@ -3247,8 +3247,8 @@ static JSC::EncodedJSValue bufferWrite(JSC::JSGlobalObject* lexicalGlobalObject, // The fast path: an in-range value at an int32 (or missing) offset inside a real ArrayBufferView. if (view && (offsetValue.isInt32() || offsetValue.isUndefined())) [[likely]] { int32_t offset32 = offsetValue.isInt32() ? offsetValue.asInt32() : 0; - size_t byteLength = view->byteLength(); - if (offset32 >= 0 && static_cast(offset32) + byteSize <= byteLength && valueIsInRange()) [[likely]] { + size_t viewLength = view->length(); + if (offset32 >= 0 && static_cast(offset32) + byteSize <= viewLength && valueIsInRange()) [[likely]] { offset = offset32; goto fastPath; } @@ -3278,7 +3278,7 @@ static JSC::EncodedJSValue bufferWrite(JSC::JSGlobalObject* lexicalGlobalObject, bufferAccessReceiver(lexicalGlobalObject, scope, thisValue); return {}; } - auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->byteLength(), byteSize); + auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->length(), byteSize); if (!checkedOffset) return {}; offset = *checkedOffset; @@ -3376,7 +3376,7 @@ static std::optional bufferReadVarWidthOffset(JSC::JSGlobalObject* lexic RETURN_IF_EXCEPTION(scope, std::nullopt); } } - size_t byteLengthOfView = view->byteLength(); + size_t byteLengthOfView = view->length(); if (!(offset >= 0 && offset <= static_cast(byteLengthOfView) - static_cast(byteLength))) [[unlikely]] { // boundsError(offset, length - byteLength) if (std::floor(offset) != offset) { @@ -3490,7 +3490,7 @@ static JSC::EncodedJSValue bufferWriteVarWidth(JSC::JSGlobalObject* lexicalGloba return {}; } // checkBounds(): the offset type was validated above; the range check is boundsError(). - auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->byteLength(), byteLength); + auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->length(), byteLength); RETURN_IF_EXCEPTION(scope, {}); if (!checkedOffset) return {}; diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index e1fa57847f7f..0d3157124f7d 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -4691,7 +4691,7 @@ describe("read*/write* after JIT tier-up", () => { for (const [name, byteSize, reference] of readers) { const read = new Function("b", "o", `return b.${name}(o);`); let mismatches = 0; - for (let i = 0; i < 5000; i++) { + for (let i = 0; i < 1500; i++) { const o = i & 31; if (read(buf, o) !== reference(o)) mismatches++; } @@ -4725,7 +4725,7 @@ describe("read*/write* after JIT tier-up", () => { for (const [name, byteSize, reference, value] of cases) { const write = new Function("b", "v", "o", `return b.${name}(v, o);`); let mismatches = 0; - for (let i = 0; i < 5000; i++) { + for (let i = 0; i < 1500; i++) { const o = i & 31; const v = value(i); if (write(buf, v, o) !== o + byteSize || reference(o) !== v) mismatches++; @@ -4748,7 +4748,7 @@ describe("read*/write* after JIT tier-up", () => { it("BigInt writes match a DataView across many iterations, and 64-bit range checks keep throwing", () => { const values = [0n, 1n, -1n, 2n ** 32n + 7n, 2n ** 63n - 1n, -(2n ** 63n)]; let mismatches = 0; - for (let i = 0; i < 5000; i++) { + for (let i = 0; i < 1500; i++) { const o = (i & 7) * 8; const v = values[i % values.length]; if (buf.writeBigInt64LE(v, o) !== o + 8 || dv.getBigInt64(o, true) !== v) mismatches++; @@ -4799,13 +4799,21 @@ describe("read*/write* after JIT tier-up", () => { it("keeps working with other ArrayBufferView receivers via .call", () => { const u8 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); const dv8 = new DataView(u8.buffer); - for (let i = 0; i < 5000; i++) { + for (let i = 0; i < 1500; i++) { expect(Buffer.prototype.readInt32LE.call(u8, 0)).toBe(dv8.getInt32(0, true)); expect(Buffer.prototype.readUInt16BE.call(u8, 6)).toBe(dv8.getUint16(6, false)); expect(Buffer.prototype.writeUInt8.call(u8, i & 0xff, 7)).toBe(8); expect(u8[7]).toBe(i & 0xff); expect(() => Buffer.prototype.readInt32LE.call({}, 0)).toThrow(TypeError); } + // The bound is the receiver's element count (this.length), as in lib/internal/buffer.js, not + // its byteLength: on a wider-element view these throw even though the bytes would fit. + const u16 = new Uint16Array(4); + expect(codeOf(() => Buffer.prototype.writeUInt32BE.call(u16, 1, 3))).toBe("ERR_OUT_OF_RANGE"); + expect(codeOf(() => Buffer.prototype.readInt32LE.call(u16, 3))).toBe("ERR_OUT_OF_RANGE"); + expect(codeOf(() => Buffer.prototype.readUIntLE.call(u16, 2, 3))).toBe("ERR_OUT_OF_RANGE"); + u16[0] = 0x1234; + expect(Buffer.prototype.readInt32LE.call(u16, 0)).toBe(new DataView(u16.buffer).getInt32(0, true)); }); it("variable-width readers/writers match across widths after tier-up", () => { @@ -4822,7 +4830,7 @@ describe("read*/write* after JIT tier-up", () => { const readConst3 = (b, o) => b.readUIntBE(o, 3); const readConst4 = (b, o) => b.readIntLE(o, 4); let mismatches = 0; - for (let i = 0; i < 5000; i++) { + for (let i = 0; i < 1500; i++) { const o = i & 15; const l = 1 + (i % 6); if (buf.readIntLE(o, l) !== sint(o, l, true)) mismatches++; @@ -4834,7 +4842,7 @@ describe("read*/write* after JIT tier-up", () => { } expect(mismatches).toBe(0); const scratch = Buffer.alloc(16); - for (let i = 0; i < 5000; i++) { + for (let i = 0; i < 1500; i++) { const l = 1 + (i % 6); const v = i % 100; expect(scratch.writeUIntLE(v, 0, l)).toBe(l); From 7957227c4b18f727702880d0cd9d7e1399339859 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 24 Jul 2026 21:27:57 -0700 Subject: [PATCH 14/25] Bump the WebKit preview build (Int52 length path, review fixes) --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 94bc96f033bd..db4398167fe9 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "autobuild-preview-pr-330-6d8df126"; +export const WEBKIT_VERSION = "autobuild-preview-pr-330-93cf9dc7"; /** * WebKit (JavaScriptCore) — the JS engine. From 7d218bf00765297f782bc668e89d596c87678477 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 25 Jul 2026 02:14:04 -0700 Subject: [PATCH 15/25] Buffer accessors: Node parity for DataView receivers, var-width offsets, byteLength dispatch, and BigInt writers - A DataView receiver has no `length` in lib/internal/buffer.js, so every accessor now reports the `>= 0 and <= NaN` ERR_OUT_OF_RANGE that Node and the deleted builtins produce, instead of reading/writing through byteLength. - The variable-width readers validated offsets with a validateInteger() fallback that printed the safe-integer-range message; Node's readIntLE family dispatches to the width-specific readers, whose validation is validateNumber() + boundsError(). Reuse the shared type/bounds helpers, so out-of-range integral offsets (including |offset| > 2^53) get the bounds message. - The variable-width writers coerced the value before validating byteLength, invoking user valueOf() where Node never does; coerce after the dispatch. - The BigInt writers keep their value validation but now go through the shared receiver check and element-count bound, so writeBigInt64* and readBigInt64* agree on the receiver and throw coded errors like siblings. Also give the subprocess-based JIT tests an explicit per-test timeout. --- src/jsc/bindings/JSBuffer.cpp | 99 ++++++++++++--------------------- test/js/node/buffer-jit.test.ts | 16 +++--- test/js/node/buffer.test.js | 22 ++++++++ 3 files changed, 65 insertions(+), 72 deletions(-) diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp index 0f6919dbf938..010bbf2ae934 100644 --- a/src/jsc/bindings/JSBuffer.cpp +++ b/src/jsc/bindings/JSBuffer.cpp @@ -3102,7 +3102,7 @@ static bool bufferAccessCheckOffsetType(JSC::JSGlobalObject* lexicalGlobalObject // boundsError(offset, byteLength - byteSize): the same ERR_OUT_OF_RANGE / ERR_BUFFER_OUT_OF_BOUNDS as // lib/internal/buffer.js. Returns the offset when it is in range after all (an integral double). -static std::optional bufferAccessCheckOffsetBounds(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSValue offsetValue, size_t viewLength, size_t byteSize) +static std::optional bufferAccessCheckOffsetBounds(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSValue offsetValue, size_t viewLength, size_t byteSize, bool viewHasLength = true) { double offset = offsetValue.asNumber(); // Math.floor(value) !== value: NaN and fractions are "an integer"; +-Infinity get the range error. @@ -3110,6 +3110,11 @@ static std::optional bufferAccessCheckOffsetBounds(JSC::JSGlobalObject* Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, "an integer"_s, offsetValue); return std::nullopt; } + if (!viewHasLength) [[unlikely]] { + // A DataView receiver has no `length`, so boundsError() compares against NaN. + Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, offsetValue); + return std::nullopt; + } if (viewLength < byteSize) [[unlikely]] { Bun::ERR::BUFFER_OUT_OF_BOUNDS(scope, lexicalGlobalObject, ""_s); return std::nullopt; @@ -3166,7 +3171,7 @@ static JSC::EncodedJSValue bufferRead(JSC::JSGlobalObject* lexicalGlobalObject, auto* view = dynamicDowncast(thisValue); size_t offset; // The fast path: an int32 (or missing) offset inside a real ArrayBufferView -- what the JIT'd form assumes. - if (view && (offsetValue.isInt32() || offsetValue.isUndefined())) [[likely]] { + if (view && view->type() != JSC::DataViewType && (offsetValue.isInt32() || offsetValue.isUndefined())) [[likely]] { int32_t offset32 = offsetValue.isInt32() ? offsetValue.asInt32() : 0; size_t viewLength = view->length(); if (offset32 >= 0 && static_cast(offset32) + byteSize <= viewLength) [[likely]] { @@ -3185,7 +3190,7 @@ static JSC::EncodedJSValue bufferRead(JSC::JSGlobalObject* lexicalGlobalObject, bufferAccessReceiver(lexicalGlobalObject, scope, thisValue); return {}; } - auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->length(), byteSize); + auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->length(), byteSize, view->type() != JSC::DataViewType); if (!checkedOffset) return {}; offset = *checkedOffset; @@ -3245,7 +3250,7 @@ static JSC::EncodedJSValue bufferWrite(JSC::JSGlobalObject* lexicalGlobalObject, size_t offset; // The fast path: an in-range value at an int32 (or missing) offset inside a real ArrayBufferView. - if (view && (offsetValue.isInt32() || offsetValue.isUndefined())) [[likely]] { + if (view && view->type() != JSC::DataViewType && (offsetValue.isInt32() || offsetValue.isUndefined())) [[likely]] { int32_t offset32 = offsetValue.isInt32() ? offsetValue.asInt32() : 0; size_t viewLength = view->length(); if (offset32 >= 0 && static_cast(offset32) + byteSize <= viewLength && valueIsInRange()) [[likely]] { @@ -3278,7 +3283,7 @@ static JSC::EncodedJSValue bufferWrite(JSC::JSGlobalObject* lexicalGlobalObject, bufferAccessReceiver(lexicalGlobalObject, scope, thisValue); return {}; } - auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->length(), byteSize); + auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->length(), byteSize, view->type() != JSC::DataViewType); if (!checkedOffset) return {}; offset = *checkedOffset; @@ -3360,38 +3365,6 @@ static EncodedJSValue throwBufferInvalidByteLength(JSC::JSGlobalObject* lexicalG return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "byteLength"_s, ">= 1 and <= 6"_s, byteLengthValue); } -// The reader's offset validation: validateInteger() for non-numbers / fractions, then the -// this.length - byteLength range (boundsError). -static std::optional bufferReadVarWidthOffset(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSArrayBufferView* view, JSValue offsetValue, size_t byteLength) -{ - double offset; - if (offsetValue.isInt32()) [[likely]] - offset = offsetValue.asInt32(); - else { - offset = offsetValue.isNumber() ? offsetValue.asNumber() : 0; - // ((offset | 0) !== offset && offset !== +-Infinity) -> validateInteger(offset, "offset") - if (!offsetValue.isNumber() || (std::floor(offset) != offset && !std::isinf(offset)) || (std::isfinite(offset) && std::abs(offset) > 9007199254740991.0)) { - int32_t unused; - Bun::V::validateInteger(scope, lexicalGlobalObject, offsetValue, "offset"_s, jsUndefined(), jsUndefined(), &unused); - RETURN_IF_EXCEPTION(scope, std::nullopt); - } - } - size_t byteLengthOfView = view->length(); - if (!(offset >= 0 && offset <= static_cast(byteLengthOfView) - static_cast(byteLength))) [[unlikely]] { - // boundsError(offset, length - byteLength) - if (std::floor(offset) != offset) { - Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, "an integer"_s, offsetValue); - return std::nullopt; - } - if (byteLengthOfView < byteLength) { - Bun::ERR::BUFFER_OUT_OF_BOUNDS(scope, lexicalGlobalObject, ""_s); - return std::nullopt; - } - Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, makeString(">= 0 and <= "_s, byteLengthOfView - byteLength), offsetValue); - return std::nullopt; - } - return static_cast(offset); -} // checkInt()'s value range for byteLength <= 4 and the ">= -(2 ** N) and < 2 ** N" wording it uses // for the 5- and 6-byte widths. @@ -3433,7 +3406,9 @@ static JSC::EncodedJSValue bufferReadVarWidth(JSC::JSGlobalObject* lexicalGlobal bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); return {}; } - auto checkedOffset = bufferReadVarWidthOffset(lexicalGlobalObject, scope, view, offsetValue, byteLength); + if (!bufferAccessCheckOffsetType(lexicalGlobalObject, scope, offsetValue)) [[unlikely]] + return {}; + auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->length(), byteLength, view->type() != JSC::DataViewType); RETURN_IF_EXCEPTION(scope, {}); if (!checkedOffset) return {}; @@ -3461,15 +3436,6 @@ static JSC::EncodedJSValue bufferWriteVarWidth(JSC::JSGlobalObject* lexicalGloba JSValue offsetValue = callFrame->argument(1); JSValue byteLengthValue = callFrame->argument(2); - // value = +value - double number; - if (valueValue.isNumber()) [[likely]] - number = valueValue.asNumber(); - else { - number = valueValue.toNumber(lexicalGlobalObject); - RETURN_IF_EXCEPTION(scope, {}); - } - if (!byteLengthValue.isNumber()) [[unlikely]] { // boundsError(byteLength, 6, "byteLength") -> validateNumber(byteLength, "byteLength") return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "byteLength"_s, "number"_s, byteLengthValue); @@ -3479,6 +3445,15 @@ static JSC::EncodedJSValue bufferWriteVarWidth(JSC::JSGlobalObject* lexicalGloba return throwBufferInvalidByteLength(lexicalGlobalObject, scope, byteLengthValue); size_t byteLength = static_cast(byteLengthNumber); + // value = +value: after the byteLength dispatch, as in lib/internal/buffer.js + double number; + if (valueValue.isNumber()) [[likely]] + number = valueValue.asNumber(); + else { + number = valueValue.toNumber(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + } + if (!bufferWriteVarWidthCheckValue(lexicalGlobalObject, scope, number, byteLength, isSigned)) [[unlikely]] return {}; @@ -3490,7 +3465,7 @@ static JSC::EncodedJSValue bufferWriteVarWidth(JSC::JSGlobalObject* lexicalGloba return {}; } // checkBounds(): the offset type was validated above; the range check is boundsError(). - auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->length(), byteLength); + auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->length(), byteLength, view->type() != JSC::DataViewType); RETURN_IF_EXCEPTION(scope, {}); if (!checkedOffset) return {}; @@ -3541,10 +3516,9 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigInt64LE, (JSGlobalObj auto& vm = JSC::getVM(lexicalGlobalObject); auto scope = DECLARE_THROW_SCOPE(vm); - auto* castedThis = dynamicDowncast(callFrame->thisValue()); - if (!castedThis) [[unlikely]] - return throwVMError(lexicalGlobalObject, scope, "Expected ArrayBufferView"_s); - auto byteLength = castedThis->byteLength(); + auto* castedThis = bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); + RETURN_IF_EXCEPTION(scope, {}); + auto byteLength = castedThis->length(); auto valueVal = callFrame->argument(0); auto offsetVal = callFrame->argument(1); @@ -3571,10 +3545,9 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigInt64BE, (JSGlobalObj auto& vm = JSC::getVM(lexicalGlobalObject); auto scope = DECLARE_THROW_SCOPE(vm); - auto* castedThis = dynamicDowncast(callFrame->thisValue()); - if (!castedThis) [[unlikely]] - return throwVMError(lexicalGlobalObject, scope, "Expected ArrayBufferView"_s); - auto byteLength = castedThis->byteLength(); + auto* castedThis = bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); + RETURN_IF_EXCEPTION(scope, {}); + auto byteLength = castedThis->length(); auto valueVal = callFrame->argument(0); auto offsetVal = callFrame->argument(1); @@ -3601,10 +3574,9 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64LE, (JSGlobalOb auto& vm = JSC::getVM(lexicalGlobalObject); auto scope = DECLARE_THROW_SCOPE(vm); - auto* castedThis = dynamicDowncast(callFrame->thisValue()); - if (!castedThis) [[unlikely]] - return throwVMError(lexicalGlobalObject, scope, "Expected ArrayBufferView"_s); - auto byteLength = castedThis->byteLength(); + auto* castedThis = bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); + RETURN_IF_EXCEPTION(scope, {}); + auto byteLength = castedThis->length(); auto valueVal = callFrame->argument(0); auto offsetVal = callFrame->argument(1); @@ -3630,10 +3602,9 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64BE, (JSGlobalOb auto& vm = JSC::getVM(lexicalGlobalObject); auto scope = DECLARE_THROW_SCOPE(vm); - auto* castedThis = dynamicDowncast(callFrame->thisValue()); - if (!castedThis) [[unlikely]] - return throwVMError(lexicalGlobalObject, scope, "Expected ArrayBufferView"_s); - auto byteLength = castedThis->byteLength(); + auto* castedThis = bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); + RETURN_IF_EXCEPTION(scope, {}); + auto byteLength = castedThis->length(); auto valueVal = callFrame->argument(0); auto offsetVal = callFrame->argument(1); diff --git a/test/js/node/buffer-jit.test.ts b/test/js/node/buffer-jit.test.ts index e78ada49db05..a3a3aafc0980 100644 --- a/test/js/node/buffer-jit.test.ts +++ b/test/js/node/buffer-jit.test.ts @@ -61,7 +61,7 @@ describe.concurrent("Buffer accessor JIT", () => { expect(stderr).toBe(""); expect(stdout.trim()).toBe("OK"); expect(exitCode).toBe(0); - }); + }, 30_000); test("each speculation exit converges instead of looping", async () => { // Warm up on the fast path, then keep triggering one exit kind at the same call site: the @@ -107,7 +107,7 @@ describe.concurrent("Buffer accessor JIT", () => { expect(stderr).toBe(""); expect(stdout.trim()).toBe("OK"); expect(exitCode).toBe(0); - }); + }, 30_000); test("host semantics survive every exit path (results, not just compile counts)", async () => { const { stdout, stderr, exitCode } = await run( @@ -157,7 +157,7 @@ describe.concurrent("Buffer accessor JIT", () => { expect(stderr).toBe(""); expect(stdout.trim()).toBe("OK"); expect(exitCode).toBe(0); - }); + }, 30_000); test("no stale reads: a store between two loads of the same offset is observed", async () => { const { stdout, stderr, exitCode } = await run( @@ -197,7 +197,7 @@ describe.concurrent("Buffer accessor JIT", () => { expect(stderr).toBe(""); expect(stdout.trim()).toBe("OK"); expect(exitCode).toBe(0); - }); + }, 30_000); test("two Buffers over the same ArrayBuffer are never mis-aliased", async () => { const { stdout, stderr, exitCode } = await run( @@ -243,7 +243,7 @@ describe.concurrent("Buffer accessor JIT", () => { expect(stderr).toBe(""); expect(stdout.trim()).toBe("OK"); expect(exitCode).toBe(0); - }); + }, 30_000); test("a loop-carried load is not kept alive across a call that mutates the buffer", async () => { const { stdout, stderr, exitCode } = await run( @@ -274,7 +274,7 @@ describe.concurrent("Buffer accessor JIT", () => { expect(stderr).toBe(""); expect(stdout.trim()).toBe("OK"); expect(exitCode).toBe(0); - }); + }, 30_000); test("replacing or shadowing the method after tier-up takes effect", async () => { const { stdout, stderr, exitCode } = await run( @@ -308,7 +308,7 @@ describe.concurrent("Buffer accessor JIT", () => { expect(stderr).toBe(""); expect(stdout.trim()).toBe("OK"); expect(exitCode).toBe(0); - }); + }, 30_000); test("resizable and growable receivers keep tracking the length after tier-up", async () => { const { stdout, stderr, exitCode } = await run( @@ -349,5 +349,5 @@ describe.concurrent("Buffer accessor JIT", () => { expect(stderr).toBe(""); expect(stdout.trim()).toBe("OK"); expect(exitCode).toBe(0); - }); + }, 30_000); }); diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index 0d3157124f7d..6d31775b19c5 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -4810,6 +4810,21 @@ describe("read*/write* after JIT tier-up", () => { // its byteLength: on a wider-element view these throw even though the bytes would fit. const u16 = new Uint16Array(4); expect(codeOf(() => Buffer.prototype.writeUInt32BE.call(u16, 1, 3))).toBe("ERR_OUT_OF_RANGE"); + // The BigInt writers share the receiver / bound handling with the rest of the family. + expect(codeOf(() => Buffer.prototype.writeBigInt64LE.call(u16, 5n, 0))).toBe("ERR_BUFFER_OUT_OF_BOUNDS"); + expect(codeOf(() => Buffer.prototype.readBigInt64LE.call(u16, 0))).toBe("ERR_BUFFER_OUT_OF_BOUNDS"); + expect(codeOf(() => Buffer.prototype.writeBigInt64LE.call({}, 0n))).toBe("ERR_INVALID_ARG_TYPE"); + // A DataView has no `length`, so as in lib/internal/buffer.js every accessor reports `<= NaN`. + const dv = new DataView(new ArrayBuffer(8)); + for (const f of [ + () => Buffer.prototype.readIntLE.call(dv, 0, 3), + () => Buffer.prototype.writeIntLE.call(dv, 1, 0, 3), + () => Buffer.prototype.readInt32LE.call(dv, 0), + () => Buffer.prototype.writeInt32LE.call(dv, 1, 0), + ]) { + expect(codeOf(f)).toBe("ERR_OUT_OF_RANGE"); + expect(() => f()).toThrow("<= NaN"); + } expect(codeOf(() => Buffer.prototype.readInt32LE.call(u16, 3))).toBe("ERR_OUT_OF_RANGE"); expect(codeOf(() => Buffer.prototype.readUIntLE.call(u16, 2, 3))).toBe("ERR_OUT_OF_RANGE"); u16[0] = 0x1234; @@ -4860,5 +4875,12 @@ describe("read*/write* after JIT tier-up", () => { ]; } expect(codes).toEqual(["ERR_OUT_OF_RANGE", "ERR_INVALID_ARG_TYPE", "ERR_OUT_OF_RANGE", "ERR_OUT_OF_RANGE"]); + // The value is only coerced after the byteLength dispatch, as in lib/internal/buffer.js. + let valueOfCalls = 0; + expect(codeOf(() => scratch.writeIntLE({ valueOf() { valueOfCalls++; return 1; } }, 0, 7))).toBe("ERR_OUT_OF_RANGE"); + expect(valueOfCalls).toBe(0); + // Out-of-range integral offsets, including |offset| > 2**53, get the bounds message. + expect(() => scratch.readIntLE(2 ** 53, 2)).toThrow(">= 0 and <= 14"); + expect(() => scratch.readIntLE(1.5, 2)).toThrow("an integer"); }); }); From 28491a115e8d03aae0338ff497cfe46e33d08fbd Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 09:16:02 +0000 Subject: [PATCH 16/25] [autofix.ci] apply automated fixes --- src/jsc/bindings/JSBuffer.cpp | 1 - test/js/node/buffer.test.js | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp index 010bbf2ae934..6268126cd35d 100644 --- a/src/jsc/bindings/JSBuffer.cpp +++ b/src/jsc/bindings/JSBuffer.cpp @@ -3365,7 +3365,6 @@ static EncodedJSValue throwBufferInvalidByteLength(JSC::JSGlobalObject* lexicalG return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "byteLength"_s, ">= 1 and <= 6"_s, byteLengthValue); } - // checkInt()'s value range for byteLength <= 4 and the ">= -(2 ** N) and < 2 ** N" wording it uses // for the 5- and 6-byte widths. static bool bufferWriteVarWidthCheckValue(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, double number, size_t byteLength, bool isSigned) diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index 6d31775b19c5..354faaa4562a 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -4877,7 +4877,20 @@ describe("read*/write* after JIT tier-up", () => { expect(codes).toEqual(["ERR_OUT_OF_RANGE", "ERR_INVALID_ARG_TYPE", "ERR_OUT_OF_RANGE", "ERR_OUT_OF_RANGE"]); // The value is only coerced after the byteLength dispatch, as in lib/internal/buffer.js. let valueOfCalls = 0; - expect(codeOf(() => scratch.writeIntLE({ valueOf() { valueOfCalls++; return 1; } }, 0, 7))).toBe("ERR_OUT_OF_RANGE"); + expect( + codeOf(() => + scratch.writeIntLE( + { + valueOf() { + valueOfCalls++; + return 1; + }, + }, + 0, + 7, + ), + ), + ).toBe("ERR_OUT_OF_RANGE"); expect(valueOfCalls).toBe(0); // Out-of-range integral offsets, including |offset| > 2**53, get the bounds message. expect(() => scratch.readIntLE(2 ** 53, 2)).toThrow(">= 0 and <= 14"); From 357582d009741397f97537a2647c232d8260d0ad Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 25 Jul 2026 02:37:44 -0700 Subject: [PATCH 17/25] Bump the WebKit preview build (differential fuzzer, restored Overflow gate) --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index db4398167fe9..3d09368916ca 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "autobuild-preview-pr-330-93cf9dc7"; +export const WEBKIT_VERSION = "autobuild-preview-pr-330-8debd979"; /** * WebKit (JavaScriptCore) — the JS engine. From d807f2db371096fb6120fa2128eddd3f25392a8f Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 25 Jul 2026 02:44:40 -0700 Subject: [PATCH 18/25] Guard the BigInt writers against DataView receivers as well --- src/jsc/bindings/JSBuffer.cpp | 16 ++++++++++++++++ test/js/node/buffer.test.js | 3 +++ 2 files changed, 19 insertions(+) diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp index 6268126cd35d..3a6458b5d79e 100644 --- a/src/jsc/bindings/JSBuffer.cpp +++ b/src/jsc/bindings/JSBuffer.cpp @@ -3517,6 +3517,10 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigInt64LE, (JSGlobalObj auto* castedThis = bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); RETURN_IF_EXCEPTION(scope, {}); + if (castedThis->type() == JSC::DataViewType) [[unlikely]] { + // A DataView receiver has no `length`, so boundsError() compares against NaN. + return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, callFrame->argument(1)); + } auto byteLength = castedThis->length(); auto valueVal = callFrame->argument(0); @@ -3546,6 +3550,10 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigInt64BE, (JSGlobalObj auto* castedThis = bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); RETURN_IF_EXCEPTION(scope, {}); + if (castedThis->type() == JSC::DataViewType) [[unlikely]] { + // A DataView receiver has no `length`, so boundsError() compares against NaN. + return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, callFrame->argument(1)); + } auto byteLength = castedThis->length(); auto valueVal = callFrame->argument(0); @@ -3575,6 +3583,10 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64LE, (JSGlobalOb auto* castedThis = bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); RETURN_IF_EXCEPTION(scope, {}); + if (castedThis->type() == JSC::DataViewType) [[unlikely]] { + // A DataView receiver has no `length`, so boundsError() compares against NaN. + return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, callFrame->argument(1)); + } auto byteLength = castedThis->length(); auto valueVal = callFrame->argument(0); @@ -3603,6 +3615,10 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64BE, (JSGlobalOb auto* castedThis = bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); RETURN_IF_EXCEPTION(scope, {}); + if (castedThis->type() == JSC::DataViewType) [[unlikely]] { + // A DataView receiver has no `length`, so boundsError() compares against NaN. + return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, callFrame->argument(1)); + } auto byteLength = castedThis->length(); auto valueVal = callFrame->argument(0); diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index 354faaa4562a..3edeb78d546a 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -4821,6 +4821,9 @@ describe("read*/write* after JIT tier-up", () => { () => Buffer.prototype.writeIntLE.call(dv, 1, 0, 3), () => Buffer.prototype.readInt32LE.call(dv, 0), () => Buffer.prototype.writeInt32LE.call(dv, 1, 0), + () => Buffer.prototype.readBigInt64LE.call(dv, 0), + () => Buffer.prototype.writeBigInt64LE.call(dv, 5n, 0), + () => Buffer.prototype.writeBigUInt64BE.call(dv, 5n, 0), ]) { expect(codeOf(f)).toBe("ERR_OUT_OF_RANGE"); expect(() => f()).toThrow("<= NaN"); From c56e904b45d5b2534444941c095a656af0533ad1 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 25 Jul 2026 03:12:49 -0700 Subject: [PATCH 19/25] Check the offset before the value for one-byte var-width writes --- src/jsc/bindings/JSBuffer.cpp | 18 +++++++++++++----- test/js/node/buffer.test.js | 6 ++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp index 3a6458b5d79e..90ae6118fbcd 100644 --- a/src/jsc/bindings/JSBuffer.cpp +++ b/src/jsc/bindings/JSBuffer.cpp @@ -3453,11 +3453,19 @@ static JSC::EncodedJSValue bufferWriteVarWidth(JSC::JSGlobalObject* lexicalGloba RETURN_IF_EXCEPTION(scope, {}); } - if (!bufferWriteVarWidthCheckValue(lexicalGlobalObject, scope, number, byteLength, isSigned)) [[unlikely]] - return {}; - - if (!offsetValue.isNumber()) [[unlikely]] - return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetValue); + // The one-byte writers dispatch to writeU_Int8(), which validates the offset before the value's + // range; wider widths go through checkInt(), which is the other way around. + if (byteLength == 1) { + if (!bufferAccessCheckOffsetType(lexicalGlobalObject, scope, offsetValue)) [[unlikely]] + return {}; + if (!bufferWriteVarWidthCheckValue(lexicalGlobalObject, scope, number, byteLength, isSigned)) [[unlikely]] + return {}; + } else { + if (!bufferWriteVarWidthCheckValue(lexicalGlobalObject, scope, number, byteLength, isSigned)) [[unlikely]] + return {}; + if (!bufferAccessCheckOffsetType(lexicalGlobalObject, scope, offsetValue)) [[unlikely]] + return {}; + } auto* view = dynamicDowncast(callFrame->thisValue()); if (!view) [[unlikely]] { bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index 3edeb78d546a..190f06aea34d 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -4895,6 +4895,12 @@ describe("read*/write* after JIT tier-up", () => { ), ).toBe("ERR_OUT_OF_RANGE"); expect(valueOfCalls).toBe(0); + // With both a bad value and a bad offset: the one-byte writers (writeU_Int8) report the offset + // first, the wider ones (checkInt) the value first. + expect(codeOf(() => scratch.writeUIntLE(300, "bad", 1))).toBe("ERR_INVALID_ARG_TYPE"); + expect(codeOf(() => scratch.writeIntLE(200, "bad", 1))).toBe("ERR_INVALID_ARG_TYPE"); + expect(codeOf(() => scratch.writeUIntLE(2 ** 24, "bad", 2))).toBe("ERR_OUT_OF_RANGE"); + expect(codeOf(() => scratch.writeIntBE(2 ** 24, "bad", 3))).toBe("ERR_OUT_OF_RANGE"); // Out-of-range integral offsets, including |offset| > 2**53, get the bounds message. expect(() => scratch.readIntLE(2 ** 53, 2)).toThrow(">= 0 and <= 14"); expect(() => scratch.readIntLE(1.5, 2)).toThrow("an integer"); From 50bab583639869bfd8f96ec7d0433fec34e48621 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 25 Jul 2026 03:34:07 -0700 Subject: [PATCH 20/25] Validate the BigInt value before reporting a DataView receiver --- src/jsc/bindings/JSBuffer.cpp | 36 +++++++++++++++++++---------------- test/js/node/buffer.test.js | 8 ++++++++ 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp index 90ae6118fbcd..5ed66ed2c481 100644 --- a/src/jsc/bindings/JSBuffer.cpp +++ b/src/jsc/bindings/JSBuffer.cpp @@ -3525,10 +3525,6 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigInt64LE, (JSGlobalObj auto* castedThis = bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); RETURN_IF_EXCEPTION(scope, {}); - if (castedThis->type() == JSC::DataViewType) [[unlikely]] { - // A DataView receiver has no `length`, so boundsError() compares against NaN. - return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, callFrame->argument(1)); - } auto byteLength = castedThis->length(); auto valueVal = callFrame->argument(0); @@ -3545,6 +3541,11 @@ 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 (castedThis->type() == JSC::DataViewType) [[unlikely]] { + // A DataView receiver has no `length`, so boundsError() compares against NaN; checkInt() has + // already validated the value's range by this point. + return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, offsetVal); + } size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength); RETURN_IF_EXCEPTION(scope, {}); write_int64_le(static_cast(castedThis->vector()) + offset, value); @@ -3558,10 +3559,6 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigInt64BE, (JSGlobalObj auto* castedThis = bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); RETURN_IF_EXCEPTION(scope, {}); - if (castedThis->type() == JSC::DataViewType) [[unlikely]] { - // A DataView receiver has no `length`, so boundsError() compares against NaN. - return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, callFrame->argument(1)); - } auto byteLength = castedThis->length(); auto valueVal = callFrame->argument(0); @@ -3578,6 +3575,11 @@ 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 (castedThis->type() == JSC::DataViewType) [[unlikely]] { + // A DataView receiver has no `length`, so boundsError() compares against NaN; checkInt() has + // already validated the value's range by this point. + return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, offsetVal); + } size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength); RETURN_IF_EXCEPTION(scope, {}); write_int64_be(static_cast(castedThis->vector()) + offset, value); @@ -3591,10 +3593,6 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64LE, (JSGlobalOb auto* castedThis = bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); RETURN_IF_EXCEPTION(scope, {}); - if (castedThis->type() == JSC::DataViewType) [[unlikely]] { - // A DataView receiver has no `length`, so boundsError() compares against NaN. - return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, callFrame->argument(1)); - } auto byteLength = castedThis->length(); auto valueVal = callFrame->argument(0); @@ -3610,6 +3608,11 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64LE, (JSGlobalOb uint64_t value = valueVal.toBigUInt64(lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, {}); + if (castedThis->type() == JSC::DataViewType) [[unlikely]] { + // A DataView receiver has no `length`, so boundsError() compares against NaN; checkInt() has + // already validated the value's range by this point. + return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, offsetVal); + } size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength); RETURN_IF_EXCEPTION(scope, {}); write_int64_le(static_cast(castedThis->vector()) + offset, value); @@ -3623,10 +3626,6 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64BE, (JSGlobalOb auto* castedThis = bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); RETURN_IF_EXCEPTION(scope, {}); - if (castedThis->type() == JSC::DataViewType) [[unlikely]] { - // A DataView receiver has no `length`, so boundsError() compares against NaN. - return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, callFrame->argument(1)); - } auto byteLength = castedThis->length(); auto valueVal = callFrame->argument(0); @@ -3642,6 +3641,11 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64BE, (JSGlobalOb uint64_t value = valueVal.toBigUInt64(lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, {}); + if (castedThis->type() == JSC::DataViewType) [[unlikely]] { + // A DataView receiver has no `length`, so boundsError() compares against NaN; checkInt() has + // already validated the value's range by this point. + return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, offsetVal); + } size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength); RETURN_IF_EXCEPTION(scope, {}); write_int64_be(static_cast(castedThis->vector()) + offset, value); diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index 190f06aea34d..9a51e94cee25 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -4814,6 +4814,14 @@ describe("read*/write* after JIT tier-up", () => { expect(codeOf(() => Buffer.prototype.writeBigInt64LE.call(u16, 5n, 0))).toBe("ERR_BUFFER_OUT_OF_BOUNDS"); expect(codeOf(() => Buffer.prototype.readBigInt64LE.call(u16, 0))).toBe("ERR_BUFFER_OUT_OF_BOUNDS"); expect(codeOf(() => Buffer.prototype.writeBigInt64LE.call({}, 0n))).toBe("ERR_INVALID_ARG_TYPE"); + // For the BigInt writers checkInt() validates the value's range before checkBounds() reaches the + // receiver, so a bad value wins over the DataView receiver. + expect(codeOf(() => Buffer.prototype.writeBigInt64LE.call(new DataView(new ArrayBuffer(16)), 2n ** 64n, 0))).toBe( + "ERR_OUT_OF_RANGE", + ); + expect(() => Buffer.prototype.writeBigInt64LE.call(new DataView(new ArrayBuffer(16)), 2n ** 64n, 0)).toThrow( + 'The value of "value" is out of range', + ); // A DataView has no `length`, so as in lib/internal/buffer.js every accessor reports `<= NaN`. const dv = new DataView(new ArrayBuffer(8)); for (const f of [ From f871414a579c0437fe38b7666c961e98a5de4cf3 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 25 Jul 2026 04:17:14 -0700 Subject: [PATCH 21/25] Route the BigInt writers' DataView check through their offset validator --- src/jsc/bindings/JSBuffer.cpp | 36 +++++++++++------------------------ test/js/node/buffer.test.js | 8 ++++++++ 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp index 5ed66ed2c481..baa3a83b2b63 100644 --- a/src/jsc/bindings/JSBuffer.cpp +++ b/src/jsc/bindings/JSBuffer.cpp @@ -2807,7 +2807,7 @@ 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) +static size_t validateOffsetBigInt64(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSValue offsetVal, size_t byteLength, bool viewHasLength = true) { // Node's checkBounds/boundsError validates the offset's type and // integer-ness before reporting a too-short buffer, and reports a @@ -2830,6 +2830,12 @@ static size_t validateOffsetBigInt64(JSC::JSGlobalObject* lexicalGlobalObject, J } } + if (!viewHasLength) [[unlikely]] { + // A DataView receiver has no `length`, so boundsError() compares against NaN. + Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, offsetVal); + return 0; + } + 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); @@ -3541,12 +3547,7 @@ 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 (castedThis->type() == JSC::DataViewType) [[unlikely]] { - // A DataView receiver has no `length`, so boundsError() compares against NaN; checkInt() has - // already validated the value's range by this point. - return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, offsetVal); - } - size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength); + size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength, castedThis->type() != JSC::DataViewType); RETURN_IF_EXCEPTION(scope, {}); write_int64_le(static_cast(castedThis->vector()) + offset, value); return JSValue::encode(jsNumber(offset + 8)); @@ -3575,12 +3576,7 @@ 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 (castedThis->type() == JSC::DataViewType) [[unlikely]] { - // A DataView receiver has no `length`, so boundsError() compares against NaN; checkInt() has - // already validated the value's range by this point. - return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, offsetVal); - } - size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength); + size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength, castedThis->type() != JSC::DataViewType); RETURN_IF_EXCEPTION(scope, {}); write_int64_be(static_cast(castedThis->vector()) + offset, value); return JSValue::encode(jsNumber(offset + 8)); @@ -3608,12 +3604,7 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64LE, (JSGlobalOb uint64_t value = valueVal.toBigUInt64(lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, {}); - if (castedThis->type() == JSC::DataViewType) [[unlikely]] { - // A DataView receiver has no `length`, so boundsError() compares against NaN; checkInt() has - // already validated the value's range by this point. - return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, offsetVal); - } - size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength); + size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength, castedThis->type() != JSC::DataViewType); RETURN_IF_EXCEPTION(scope, {}); write_int64_le(static_cast(castedThis->vector()) + offset, value); return JSValue::encode(jsNumber(offset + 8)); @@ -3641,12 +3632,7 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64BE, (JSGlobalOb uint64_t value = valueVal.toBigUInt64(lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, {}); - if (castedThis->type() == JSC::DataViewType) [[unlikely]] { - // A DataView receiver has no `length`, so boundsError() compares against NaN; checkInt() has - // already validated the value's range by this point. - return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, offsetVal); - } - size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength); + size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength, castedThis->type() != JSC::DataViewType); 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 9a51e94cee25..c1972cf0e1f6 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -4822,6 +4822,14 @@ describe("read*/write* after JIT tier-up", () => { expect(() => Buffer.prototype.writeBigInt64LE.call(new DataView(new ArrayBuffer(16)), 2n ** 64n, 0)).toThrow( 'The value of "value" is out of range', ); + // The offset's type is validated before the receiver's length is consulted, matching the + // fixed-width writers, so a non-number offset still wins over the DataView receiver. + expect(codeOf(() => Buffer.prototype.writeBigInt64LE.call(new DataView(new ArrayBuffer(16)), 5n, "bad"))).toBe( + "ERR_INVALID_ARG_TYPE", + ); + expect(codeOf(() => Buffer.prototype.writeBigInt64LE.call(new DataView(new ArrayBuffer(16)), 5n, 1.5))).toBe( + "ERR_OUT_OF_RANGE", + ); // A DataView has no `length`, so as in lib/internal/buffer.js every accessor reports `<= NaN`. const dv = new DataView(new ArrayBuffer(8)); for (const f of [ From 4e3ea853d7ccb31701404b562de5fc4dbc65a21a Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 25 Jul 2026 21:20:32 -0700 Subject: [PATCH 22/25] Port the JSC stress coverage into the Bun test suite The buffer accessor stress tests in the WebKit fork's JSTests never run in CI, since oven's WebKit CI only builds. Bring the coverage that has no Bun equivalent into buffer-jit.test.ts so it runs on every PR: - A differential fuzzer: one seeded operation stream over all read*/write* accessors runs in a JIT process and a BUN_JSC_useJIT=0 process, and the digests of every return value, error and post-write buffer byte must match. The receiver pool avoids SharedArrayBuffer, which the interpreter- only mode does not expose, so both arms build the identical stream. - A >2GB receiver that must stay optimized (bounded compiles) while out-of-bounds accesses keep throwing. - Views with 2GB and ~4GB byteOffsets, cross-checked against a DataView on the raw buffer. The run() helper takes per-test env. Verified the fuzzer detects an injected JIT-arm divergence (digest mismatch) before trusting a pass. --- test/js/node/buffer-jit.test.ts | 168 +++++++++++++++++++++++++++++++- 1 file changed, 167 insertions(+), 1 deletion(-) diff --git a/test/js/node/buffer-jit.test.ts b/test/js/node/buffer-jit.test.ts index a3a3aafc0980..d31f8ae24dab 100644 --- a/test/js/node/buffer-jit.test.ts +++ b/test/js/node/buffer-jit.test.ts @@ -10,7 +10,7 @@ import { describe, expect, test } from "bun:test"; import { bunEnv, bunExe } from "harness"; -async function run(source: string) { +async function run(source: string, extraEnv: Record = {}) { await using proc = Bun.spawn({ cmd: [bunExe(), "-e", source], env: { @@ -18,6 +18,7 @@ async function run(source: string) { BUN_JSC_useConcurrentJIT: "0", // Tier up quickly and deterministically, but not so eagerly that profiling is skipped. BUN_JSC_jitPolicyScale: "0.05", + ...extraEnv, }, stderr: "pipe", stdout: "pipe", @@ -350,4 +351,169 @@ describe.concurrent("Buffer accessor JIT", () => { expect(stdout.trim()).toBe("OK"); expect(exitCode).toBe(0); }, 30_000); + + // Differential fuzzer: an identical seeded operation stream runs once with the JIT and once with + // BUN_JSC_useJIT=0; the two traces (return values, error codes/messages, and the buffer bytes + // after every write) must match. Ported from JSTests/stress/buffer-accessor-jit-differential.js. + const fuzzerSource = ` + let seed = 0x9e3779b1; + function rand() { seed ^= seed << 13; seed |= 0; seed ^= seed >>> 17; seed ^= seed << 5; seed |= 0; return (seed >>> 0) / 4294967296; } + function pick(list) { return list[(rand() * list.length) | 0]; } + function randInt(lo, hi) { return lo + ((rand() * (hi - lo + 1)) | 0); } + const names = Object.getOwnPropertyNames(Buffer.prototype).filter(n => /^(read|write)(U?Int|Float|Double|Big)/.test(n)); + const readers = names.filter(n => n.startsWith("read")), writers = names.filter(n => n.startsWith("write")); + function describeName(name) { + const isWrite = name.startsWith("write"), isFloat = /Float|Double/.test(name), isBigInt = /Big/.test(name); + const isVarWidth = /Int(LE|BE)$/.test(name) && !/(8|16|32|64)/.test(name), isSigned = !/UInt/.test(name); + const byteSize = /Double/.test(name) ? 8 : /Float/.test(name) ? 4 : isBigInt ? 8 : isVarWidth ? 0 : Number(name.match(/(8|16|32|64)/)[0]) / 8; + return { isWrite, isFloat, isBigInt, isVarWidth, isSigned, byteSize }; + } + function cleanValue(shape, size) { + if (shape.isBigInt) return pick(shape.isSigned ? [-(2n ** 63n), 2n ** 63n - 1n, 0n, -1n, BigInt(randInt(-1e6, 1e6))] : [0n, 2n ** 64n - 1n, 12345678901234567890n, BigInt(randInt(0, 1e6))]); + if (shape.isFloat) return pick([() => rand() * 1e6 - 5e5, () => Math.fround(rand() * 100), () => -0, () => Infinity, () => 2 ** -1074, () => 1e300])(); + const min = shape.isSigned ? -(2 ** (8 * size - 1)) : 0, max = shape.isSigned ? 2 ** (8 * size - 1) - 1 : 2 ** (8 * size) - 1; + return pick([() => min, () => max, () => randInt(min, max), () => randInt(min, max), () => 0])(); + } + const dirtyValue = () => pick([() => (rand() * 2 ** 32) | 0, () => -((rand() * 2 ** 31) | 0), () => 2 ** 31, () => 2 ** 32, () => -(2 ** 31) - 1, + () => rand() * 1e6 - 5e5, () => 0.5, () => -0.5, () => -0, () => NaN, () => Infinity, () => -Infinity, () => 2 ** 53 + 1, + () => "42", () => "abc", () => "", () => true, () => false, () => null, () => undefined, + () => 5n, () => 2n ** 63n, () => 2n ** 64n, () => -1n, () => -(2n ** 63n) - 1n, () => Symbol("v")])(); + const dirtyOffset = length => pick([() => randInt(0, length + 3), () => -randInt(1, 8), () => length - randInt(0, 8), () => rand() * length, + () => -0, () => 2 ** 31 + randInt(0, 8), () => 2 ** 32, () => 2 ** 53 + 2, () => NaN, () => Infinity, () => -Infinity, + () => undefined, () => null, () => String(randInt(0, length)), () => "not a number", () => true, () => Symbol("s"), () => 3n])(); + const dirtyByteLength = () => pick([() => randInt(1, 6), () => 0, () => 7, () => -1, () => 2.5, () => NaN, () => "4", () => undefined, () => 9n])(); + function makeReceiver() { + return pick([() => Buffer.alloc(32), () => Buffer.from(new ArrayBuffer(64), 8, 24), () => Buffer.alloc(7), + () => Buffer.from(new ArrayBuffer(16, { maxByteLength: 64 })), + () => Buffer.from(new ArrayBuffer(48, { maxByteLength: 64 }), 8, 16)])(); + } + function makeInvoker(name) { + if (!/^[A-Za-z0-9]+$/.test(name)) throw new Error("bad name " + name); + return new Function("return function invoke_" + name + "(receiver, args, box) { try { let result;" + + " switch (args.length) { case 0: result = receiver." + name + "(); break;" + + " case 1: result = receiver." + name + "(args[0]); break;" + + " case 2: result = receiver." + name + "(args[0], args[1]); break;" + + " default: result = receiver." + name + "(args[0], args[1], args[2]); break; }" + + " box.value = result; box.error = null; } catch (e) { box.value = undefined;" + + " box.error = e === null || typeof e !== 'object' ? 'throw:' + String(e) : 'throw:' + e.constructor.name + ':' + (e.code === undefined ? '' : e.code) + ':' + e.message; } };")(); + } + const invokers = new Map(); + const invokerFor = name => { let f = invokers.get(name); if (!f) invokers.set(name, (f = makeInvoker(name))); return f; }; + const fmt = v => typeof v === "bigint" ? v + "n" : typeof v === "symbol" ? "Symbol" : Object.is(v, -0) ? "-0" : String(v); + let digest = 0x811c9dc5; + const mix = str => { for (let i = 0; i < str.length; i++) { digest ^= str.charCodeAt(i); digest = Math.imul(digest, 0x01000193); } }; + const box = { value: undefined, error: null }; + let ops = 0; + for (let round = 0; round < 40; ++round) { + const receiver = makeReceiver(); + const name = pick(rand() < 0.5 ? readers : writers); + const shape = describeName(name), clean = rand() < 0.6, invoke = invokerFor(name); + const width = shape.isVarWidth ? randInt(1, 6) : shape.byteSize; + let resizeCountdown = clean ? Infinity : 100 + randInt(0, 400); + for (let step = 0; step < 850; ++step) { + const args = []; + const maxOffset = receiver.length - width; + if (clean) { + if (maxOffset < 0) break; + if (shape.isWrite) args.push(cleanValue(shape, width)); + args.push(randInt(0, maxOffset)); + if (shape.isVarWidth) args.push(width); + } else { + if (shape.isWrite) args.push(dirtyValue()); + if (rand() < 0.9 || shape.isVarWidth) args.push(dirtyOffset(receiver.length)); + if (shape.isVarWidth) args.push(dirtyByteLength()); + while (args.length && args[args.length - 1] === undefined && rand() < 0.3) args.pop(); + if (args.length && typeof args[args.length - 1] === "symbol" && rand() < 0.5) args[args.length - 1] = 0; + } + invoke(receiver, args, box); + ops++; + let bytes; + try { bytes = Array.prototype.join.call(receiver, ","); } catch { bytes = ""; } + mix(name + "|" + args.map(fmt).join(",") + "=>" + (box.error === null ? fmt(box.value) : box.error) + "|" + bytes); + if (--resizeCountdown === 0) { + resizeCountdown = 100 + randInt(0, 400); + const ab = receiver.buffer; + if (typeof ab.resize === "function" && ab.resizable) { try { ab.resize(randInt(0, ab.maxByteLength)); } catch {} } + } + } + } + console.log("digest=" + (digest >>> 0).toString(16) + " ops=" + ops); + `; + + test("differential fuzzer: JIT and useJIT=0 agree on every result, error and byte", async () => { + const [jit, reference] = await Promise.all([run(fuzzerSource), run(fuzzerSource, { BUN_JSC_useJIT: "0" })]); + expect(jit.stderr).toBe(""); + expect(reference.stderr).toBe(""); + expect(jit.exitCode).toBe(0); + expect(reference.exitCode).toBe(0); + const parse = (out: string) => Object.fromEntries(out.trim().split(/\s+/).map(kv => kv.split("="))); + const jitResult = parse(jit.stdout), referenceResult = parse(reference.stdout); + // A meaningful volume actually ran, and the JIT arm reproduces the interpreter's trace exactly. + expect(Number(referenceResult.ops)).toBeGreaterThan(20_000); + expect(jitResult.ops).toBe(referenceResult.ops); + expect(jitResult.digest).toBe(referenceResult.digest); + }, 120_000); + + test("a >2GB receiver stays optimized: no exit storm, and OOB still throws", async () => { + const { stdout, stderr, exitCode } = await run( + prelude + + ` + let big; + try { big = new Uint8Array(3 * 2**30); } catch { console.log("OK"); process.exit(0); } + Object.setPrototypeOf(big, Buffer.prototype); + const small = Buffer.alloc(64); + function readAt(b, o) { return b.readInt32LE(o); } + function writeAt(b, v, o) { return b.writeInt32LE(v, o); } + noInline(readAt); noInline(writeAt); + const top = 2 ** 31 - 4; + for (let i = 0; i < N * 10; i++) { + assert(writeAt(big, i, 100) === 104, "write low"); + assert(readAt(big, 100) === i, "read low"); + assert(writeAt(big, ~i, top) === top + 4, "write at the int32 offset ceiling"); + assert(readAt(big, top) === ~i, "read at the int32 offset ceiling"); + assert(writeAt(small, i, 60) === 64 && readAt(small, 60) === i, "the same site with a small receiver"); + } + const compiles = Math.max(numberOfDFGCompiles(readAt), numberOfDFGCompiles(writeAt)); + assert(compiles <= 3, "the large receiver caused recompiles: " + compiles); + let threw = 0; + for (let i = 0; i < 200; i++) { try { readAt(big, big.length - 3); } catch (e) { threw += e.code === "ERR_OUT_OF_RANGE"; } } + assert(threw === 200, "straddling the end of a >2GB view throws"); + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }, 120_000); + + test("views with 2GB and ~4GB byteOffsets read and write correctly after tier-up", async () => { + const { stdout, stderr, exitCode } = await run( + prelude + + ` + let ab; + try { ab = new ArrayBuffer(4 * 2**30); } catch { console.log("OK"); process.exit(0); } + const tailOffset = 4 * 2**30 - 64; + const tail = Buffer.from(ab, tailOffset, 64); + const wide = Buffer.from(ab, 2**31); + const raw = new DataView(ab); + function readAt(v, o) { return v.readInt32LE(o); } + function writeAt(v, x, o) { return v.writeInt32LE(x, o); } + noInline(readAt); noInline(writeAt); + for (let i = 0; i < N; i++) { + assert(writeAt(tail, i, 8) === 12 && readAt(tail, 8) === i, "~4GB byteOffset view"); + assert(writeAt(wide, ~i, wide.length - 4) === wide.length && readAt(wide, wide.length - 4) === ~i, "2GB byteOffset view"); + } + assert(raw.getInt32(tailOffset + 8, true) === N - 1, "the store landed at byteOffset + offset"); + assert(raw.getInt32(2**31 + wide.length - 4, true) === ~(N - 1), "the store landed at the 2GB byteOffset"); + let threw = false; + try { readAt(tail, 61); } catch (e) { threw = e.code === "ERR_OUT_OF_RANGE"; } + assert(threw, "straddling the end of the tiny high-offset view throws"); + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }, 120_000); }); + From 9ce177c238b8094aa6f527ad83b8a32e99d74793 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 26 Jul 2026 04:22:41 +0000 Subject: [PATCH 23/25] [autofix.ci] apply automated fixes --- test/js/node/buffer-jit.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/js/node/buffer-jit.test.ts b/test/js/node/buffer-jit.test.ts index d31f8ae24dab..c8d6ccf2c0d0 100644 --- a/test/js/node/buffer-jit.test.ts +++ b/test/js/node/buffer-jit.test.ts @@ -446,8 +446,15 @@ describe.concurrent("Buffer accessor JIT", () => { expect(reference.stderr).toBe(""); expect(jit.exitCode).toBe(0); expect(reference.exitCode).toBe(0); - const parse = (out: string) => Object.fromEntries(out.trim().split(/\s+/).map(kv => kv.split("="))); - const jitResult = parse(jit.stdout), referenceResult = parse(reference.stdout); + const parse = (out: string) => + Object.fromEntries( + out + .trim() + .split(/\s+/) + .map(kv => kv.split("=")), + ); + const jitResult = parse(jit.stdout), + referenceResult = parse(reference.stdout); // A meaningful volume actually ran, and the JIT arm reproduces the interpreter's trace exactly. expect(Number(referenceResult.ops)).toBeGreaterThan(20_000); expect(jitResult.ops).toBe(referenceResult.ops); @@ -516,4 +523,3 @@ describe.concurrent("Buffer accessor JIT", () => { expect(exitCode).toBe(0); }, 120_000); }); - From ea0cf81c20e8dc834d29b801201a6de2bdc3225a Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 25 Jul 2026 22:09:07 -0700 Subject: [PATCH 24/25] Check the offset type before the receiver in the var-width readers --- src/jsc/bindings/JSBuffer.cpp | 4 ++-- test/js/node/buffer.test.js | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp index baa3a83b2b63..dc5dfd345ed7 100644 --- a/src/jsc/bindings/JSBuffer.cpp +++ b/src/jsc/bindings/JSBuffer.cpp @@ -3406,13 +3406,13 @@ static JSC::EncodedJSValue bufferReadVarWidth(JSC::JSGlobalObject* lexicalGlobal return throwBufferInvalidByteLength(lexicalGlobalObject, scope, byteLengthValue); size_t byteLength = static_cast(byteLengthNumber); + if (!bufferAccessCheckOffsetType(lexicalGlobalObject, scope, offsetValue)) [[unlikely]] + return {}; auto* view = dynamicDowncast(callFrame->thisValue()); if (!view) [[unlikely]] { bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); return {}; } - if (!bufferAccessCheckOffsetType(lexicalGlobalObject, scope, offsetValue)) [[unlikely]] - return {}; auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->length(), byteLength, view->type() != JSC::DataViewType); RETURN_IF_EXCEPTION(scope, {}); if (!checkedOffset) diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index c1972cf0e1f6..dd70883daf27 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -4917,6 +4917,16 @@ describe("read*/write* after JIT tier-up", () => { expect(codeOf(() => scratch.writeIntLE(200, "bad", 1))).toBe("ERR_INVALID_ARG_TYPE"); expect(codeOf(() => scratch.writeUIntLE(2 ** 24, "bad", 2))).toBe("ERR_OUT_OF_RANGE"); expect(codeOf(() => scratch.writeIntBE(2 ** 24, "bad", 3))).toBe("ERR_OUT_OF_RANGE"); + // Every accessor validates the offset's type before consulting the receiver, so a bad offset + // wins over a garbage receiver across the whole family. + for (const f of [ + () => Buffer.prototype.readIntLE.call({}, "bad", 3), + () => Buffer.prototype.readInt32LE.call({}, "bad"), + () => Buffer.prototype.writeIntLE.call({}, 1, "bad", 3), + ]) { + expect(codeOf(f)).toBe("ERR_INVALID_ARG_TYPE"); + expect(() => f()).toThrow('The "offset" argument must be of type number'); + } // Out-of-range integral offsets, including |offset| > 2**53, get the bounds message. expect(() => scratch.readIntLE(2 ** 53, 2)).toThrow(">= 0 and <= 14"); expect(() => scratch.readIntLE(1.5, 2)).toThrow("an integer"); From 2a8c8377e3d4ef3abc06510f295146321554a26f Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 25 Jul 2026 22:59:05 -0700 Subject: [PATCH 25/25] BigInt writers: validate the value before the receiver, default the omitted offset in errors --- src/jsc/bindings/JSBuffer.cpp | 54 ++++++++++++++++++++++++++--------- test/js/node/buffer.test.js | 7 +++++ 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp index dc5dfd345ed7..938f4068a7d6 100644 --- a/src/jsc/bindings/JSBuffer.cpp +++ b/src/jsc/bindings/JSBuffer.cpp @@ -2832,7 +2832,7 @@ static size_t validateOffsetBigInt64(JSC::JSGlobalObject* lexicalGlobalObject, J if (!viewHasLength) [[unlikely]] { // A DataView receiver has no `length`, so boundsError() compares against NaN. - Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, offsetVal); + Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, jsNumber(offsetD)); return 0; } @@ -3529,9 +3529,7 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigInt64LE, (JSGlobalObj auto& vm = JSC::getVM(lexicalGlobalObject); auto scope = DECLARE_THROW_SCOPE(vm); - auto* castedThis = bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); - RETURN_IF_EXCEPTION(scope, {}); - auto byteLength = castedThis->length(); + auto* castedThis = dynamicDowncast(callFrame->thisValue()); auto valueVal = callFrame->argument(0); auto offsetVal = callFrame->argument(1); @@ -3547,6 +3545,15 @@ 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 (!castedThis) [[unlikely]] { + // The offset's type is validated before the receiver in every accessor, so a non-number + // offset still wins over a garbage receiver. + if (!offsetVal.isUndefined() && !offsetVal.isNumber()) + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetVal); + bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); + return {}; + } + size_t byteLength = castedThis->length(); size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength, castedThis->type() != JSC::DataViewType); RETURN_IF_EXCEPTION(scope, {}); write_int64_le(static_cast(castedThis->vector()) + offset, value); @@ -3558,9 +3565,7 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigInt64BE, (JSGlobalObj auto& vm = JSC::getVM(lexicalGlobalObject); auto scope = DECLARE_THROW_SCOPE(vm); - auto* castedThis = bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); - RETURN_IF_EXCEPTION(scope, {}); - auto byteLength = castedThis->length(); + auto* castedThis = dynamicDowncast(callFrame->thisValue()); auto valueVal = callFrame->argument(0); auto offsetVal = callFrame->argument(1); @@ -3576,6 +3581,15 @@ 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 (!castedThis) [[unlikely]] { + // The offset's type is validated before the receiver in every accessor, so a non-number + // offset still wins over a garbage receiver. + if (!offsetVal.isUndefined() && !offsetVal.isNumber()) + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetVal); + bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); + return {}; + } + size_t byteLength = castedThis->length(); size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength, castedThis->type() != JSC::DataViewType); RETURN_IF_EXCEPTION(scope, {}); write_int64_be(static_cast(castedThis->vector()) + offset, value); @@ -3587,9 +3601,7 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64LE, (JSGlobalOb auto& vm = JSC::getVM(lexicalGlobalObject); auto scope = DECLARE_THROW_SCOPE(vm); - auto* castedThis = bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); - RETURN_IF_EXCEPTION(scope, {}); - auto byteLength = castedThis->length(); + auto* castedThis = dynamicDowncast(callFrame->thisValue()); auto valueVal = callFrame->argument(0); auto offsetVal = callFrame->argument(1); @@ -3604,6 +3616,15 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64LE, (JSGlobalOb uint64_t value = valueVal.toBigUInt64(lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, {}); + if (!castedThis) [[unlikely]] { + // The offset's type is validated before the receiver in every accessor, so a non-number + // offset still wins over a garbage receiver. + if (!offsetVal.isUndefined() && !offsetVal.isNumber()) + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetVal); + bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); + return {}; + } + size_t byteLength = castedThis->length(); size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength, castedThis->type() != JSC::DataViewType); RETURN_IF_EXCEPTION(scope, {}); write_int64_le(static_cast(castedThis->vector()) + offset, value); @@ -3615,9 +3636,7 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64BE, (JSGlobalOb auto& vm = JSC::getVM(lexicalGlobalObject); auto scope = DECLARE_THROW_SCOPE(vm); - auto* castedThis = bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); - RETURN_IF_EXCEPTION(scope, {}); - auto byteLength = castedThis->length(); + auto* castedThis = dynamicDowncast(callFrame->thisValue()); auto valueVal = callFrame->argument(0); auto offsetVal = callFrame->argument(1); @@ -3632,6 +3651,15 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64BE, (JSGlobalOb uint64_t value = valueVal.toBigUInt64(lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, {}); + if (!castedThis) [[unlikely]] { + // The offset's type is validated before the receiver in every accessor, so a non-number + // offset still wins over a garbage receiver. + if (!offsetVal.isUndefined() && !offsetVal.isNumber()) + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetVal); + bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); + return {}; + } + size_t byteLength = castedThis->length(); size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength, castedThis->type() != JSC::DataViewType); RETURN_IF_EXCEPTION(scope, {}); write_int64_be(static_cast(castedThis->vector()) + offset, value); diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index dd70883daf27..18a5c8b86aaa 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -4822,6 +4822,12 @@ describe("read*/write* after JIT tier-up", () => { expect(() => Buffer.prototype.writeBigInt64LE.call(new DataView(new ArrayBuffer(16)), 2n ** 64n, 0)).toThrow( 'The value of "value" is out of range', ); + // A garbage receiver never masks the value's own error: the BigInt writers, like their + // number siblings, validate the value first. + expect(codeOf(() => Buffer.prototype.writeBigInt64LE.call({}, 2n ** 64n, 0))).toBe("ERR_OUT_OF_RANGE"); + expect(() => Buffer.prototype.writeBigInt64LE.call({}, 2n ** 64n, 0)).toThrow('The value of "value"'); + // An omitted offset defaults to 0 in the error text, on writes as on reads. + expect(() => Buffer.prototype.writeBigInt64LE.call(new DataView(new ArrayBuffer(16)), 5n)).toThrow("Received 0"); // The offset's type is validated before the receiver's length is consulted, matching the // fixed-width writers, so a non-number offset still wins over the DataView receiver. expect(codeOf(() => Buffer.prototype.writeBigInt64LE.call(new DataView(new ArrayBuffer(16)), 5n, "bad"))).toBe( @@ -4923,6 +4929,7 @@ describe("read*/write* after JIT tier-up", () => { () => Buffer.prototype.readIntLE.call({}, "bad", 3), () => Buffer.prototype.readInt32LE.call({}, "bad"), () => Buffer.prototype.writeIntLE.call({}, 1, "bad", 3), + () => Buffer.prototype.writeBigInt64LE.call({}, 5n, "bad"), ]) { expect(codeOf(f)).toBe("ERR_INVALID_ARG_TYPE"); expect(() => f()).toThrow('The "offset" argument must be of type number');