Skip to content

Commit

Permalink
buffer: make methods work on Uint8Array instances
Browse files Browse the repository at this point in the history
Removes the reliance on prototype bound methods internally so that Uint8Arrays can be set as the bound `this` value when calling the various Buffer methods. Introduces some additional tamper protection by removing internal reliance on writable properties.

Fixes: nodejs#56577
  • Loading branch information
nbbeeken committed Jan 13, 2025
1 parent f4fcf0e commit c73de47
Show file tree
Hide file tree
Showing 4 changed files with 1,187 additions and 35 deletions.
15 changes: 15 additions & 0 deletions doc/api/buffer.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,21 @@ function:
* [`Buffer.from(arrayBuffer[, byteOffset[, length]])`][`Buffer.from(arrayBuf)`]
* [`Buffer.from(string[, encoding])`][`Buffer.from(string)`]

### Buffer methods are callable with `Uint8Array` instances

All methods on the Buffer prototype are callable with a `Uint8Array` instance.

```js
const { toString, write } = Buffer.prototype;

const uint8array = new Uint8Array(5);

write.call(uint8array, 'hello', 0, 5, 'utf8'); // 5
// <Uint8Array 68 65 6c 6c 6f>

toString.call(uint8array, 'utf8'); // 'hello'
```

## Buffers and iteration

`Buffer` instances can be iterated over using `for..of` syntax:
Expand Down
64 changes: 41 additions & 23 deletions lib/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const {
ArrayBufferIsView,
ArrayIsArray,
ArrayPrototypeForEach,
FunctionPrototypeCall,
MathFloor,
MathMin,
MathTrunc,
Expand Down Expand Up @@ -135,6 +136,23 @@ FastBuffer.prototype.constructor = Buffer;
Buffer.prototype = FastBuffer.prototype;
addBufferPrototypeMethods(Buffer.prototype);

const {
asciiWrite,
latin1Write,
utf8Write,
asciiSlice,
base64Slice,
base64urlSlice,
latin1Slice,
hexSlice,
ucs2Slice,
utf8Slice,
base64Write,
base64urlWrite,
hexWrite,
ucs2Write,
} = Buffer.prototype;

const constants = ObjectDefineProperties({}, {
MAX_LENGTH: {
__proto__: null,
Expand Down Expand Up @@ -633,44 +651,44 @@ const encodingOps = {
encoding: 'utf8',
encodingVal: encodingsMap.utf8,
byteLength: byteLengthUtf8,
write: (buf, string, offset, len) => buf.utf8Write(string, offset, len),
slice: (buf, start, end) => buf.utf8Slice(start, end),
write: (buf, string, offset, len) => FunctionPrototypeCall(utf8Write, buf, string, offset, len),
slice: (buf, start, end) => FunctionPrototypeCall(utf8Slice, buf, start, end),
indexOf: (buf, val, byteOffset, dir) =>
indexOfString(buf, val, byteOffset, encodingsMap.utf8, dir),
},
ucs2: {
encoding: 'ucs2',
encodingVal: encodingsMap.utf16le,
byteLength: (string) => string.length * 2,
write: (buf, string, offset, len) => buf.ucs2Write(string, offset, len),
slice: (buf, start, end) => buf.ucs2Slice(start, end),
write: (buf, string, offset, len) => FunctionPrototypeCall(ucs2Write, buf, string, offset, len),
slice: (buf, start, end) => FunctionPrototypeCall(ucs2Slice, buf, start, end),
indexOf: (buf, val, byteOffset, dir) =>
indexOfString(buf, val, byteOffset, encodingsMap.utf16le, dir),
},
utf16le: {
encoding: 'utf16le',
encodingVal: encodingsMap.utf16le,
byteLength: (string) => string.length * 2,
write: (buf, string, offset, len) => buf.ucs2Write(string, offset, len),
slice: (buf, start, end) => buf.ucs2Slice(start, end),
write: (buf, string, offset, len) => FunctionPrototypeCall(ucs2Write, buf, string, offset, len),
slice: (buf, start, end) => FunctionPrototypeCall(ucs2Slice, buf, start, end),
indexOf: (buf, val, byteOffset, dir) =>
indexOfString(buf, val, byteOffset, encodingsMap.utf16le, dir),
},
latin1: {
encoding: 'latin1',
encodingVal: encodingsMap.latin1,
byteLength: (string) => string.length,
write: (buf, string, offset, len) => buf.latin1Write(string, offset, len),
slice: (buf, start, end) => buf.latin1Slice(start, end),
write: (buf, string, offset, len) => FunctionPrototypeCall(latin1Write, buf, string, offset, len),
slice: (buf, start, end) => FunctionPrototypeCall(latin1Slice, buf, start, end),
indexOf: (buf, val, byteOffset, dir) =>
indexOfString(buf, val, byteOffset, encodingsMap.latin1, dir),
},
ascii: {
encoding: 'ascii',
encodingVal: encodingsMap.ascii,
byteLength: (string) => string.length,
write: (buf, string, offset, len) => buf.asciiWrite(string, offset, len),
slice: (buf, start, end) => buf.asciiSlice(start, end),
write: (buf, string, offset, len) => FunctionPrototypeCall(asciiWrite, buf, string, offset, len),
slice: (buf, start, end) => FunctionPrototypeCall(asciiSlice, buf, start, end),
indexOf: (buf, val, byteOffset, dir) =>
indexOfBuffer(buf,
fromStringFast(val, encodingOps.ascii),
Expand All @@ -682,8 +700,8 @@ const encodingOps = {
encoding: 'base64',
encodingVal: encodingsMap.base64,
byteLength: (string) => base64ByteLength(string, string.length),
write: (buf, string, offset, len) => buf.base64Write(string, offset, len),
slice: (buf, start, end) => buf.base64Slice(start, end),
write: (buf, string, offset, len) => FunctionPrototypeCall(base64Write, buf, string, offset, len),
slice: (buf, start, end) => FunctionPrototypeCall(base64Slice, buf, start, end),
indexOf: (buf, val, byteOffset, dir) =>
indexOfBuffer(buf,
fromStringFast(val, encodingOps.base64),
Expand All @@ -696,8 +714,8 @@ const encodingOps = {
encodingVal: encodingsMap.base64url,
byteLength: (string) => base64ByteLength(string, string.length),
write: (buf, string, offset, len) =>
buf.base64urlWrite(string, offset, len),
slice: (buf, start, end) => buf.base64urlSlice(start, end),
FunctionPrototypeCall(base64urlWrite, buf, string, offset, len),
slice: (buf, start, end) => FunctionPrototypeCall(base64urlSlice, buf, start, end),
indexOf: (buf, val, byteOffset, dir) =>
indexOfBuffer(buf,
fromStringFast(val, encodingOps.base64url),
Expand All @@ -709,8 +727,8 @@ const encodingOps = {
encoding: 'hex',
encodingVal: encodingsMap.hex,
byteLength: (string) => string.length >>> 1,
write: (buf, string, offset, len) => buf.hexWrite(string, offset, len),
slice: (buf, start, end) => buf.hexSlice(start, end),
write: (buf, string, offset, len) => FunctionPrototypeCall(hexWrite, buf, string, offset, len),
slice: (buf, start, end) => FunctionPrototypeCall(hexSlice, buf, start, end),
indexOf: (buf, val, byteOffset, dir) =>
indexOfBuffer(buf,
fromStringFast(val, encodingOps.hex),
Expand Down Expand Up @@ -835,7 +853,7 @@ Buffer.prototype.copy =
// to their upper/lower bounds if the value passed is out of range.
Buffer.prototype.toString = function toString(encoding, start, end) {
if (arguments.length === 0) {
return this.utf8Slice(0, this.length);
return FunctionPrototypeCall(utf8Slice, this, 0, this.length);
}

const len = this.length;
Expand All @@ -856,7 +874,7 @@ Buffer.prototype.toString = function toString(encoding, start, end) {
return '';

if (encoding === undefined)
return this.utf8Slice(start, end);
return FunctionPrototypeCall(utf8Slice, this, start, end);

const ops = getEncodingOps(encoding);
if (ops === undefined)
Expand Down Expand Up @@ -887,7 +905,7 @@ Buffer.prototype[customInspectSymbol] = function inspect(recurseTimes, ctx) {
const actualMax = MathMin(max, this.length);
const remaining = this.length - max;
let str = StringPrototypeTrim(RegExpPrototypeSymbolReplace(
/(.{2})/g, this.hexSlice(0, actualMax), '$1 '));
/(.{2})/g, FunctionPrototypeCall(hexSlice, this, 0, actualMax), '$1 '));
if (remaining > 0)
str += ` ... ${remaining} more byte${remaining > 1 ? 's' : ''}`;
// Inspect special properties as well, if possible.
Expand Down Expand Up @@ -1026,7 +1044,7 @@ Buffer.prototype.lastIndexOf = function lastIndexOf(val, byteOffset, encoding) {
};

Buffer.prototype.includes = function includes(val, byteOffset, encoding) {
return this.indexOf(val, byteOffset, encoding) !== -1;
return bidirectionalIndexOf(this, val, byteOffset, encoding) !== -1;
};

// Usage:
Expand Down Expand Up @@ -1111,7 +1129,7 @@ function _fill(buf, value, offset, end, encoding) {
Buffer.prototype.write = function write(string, offset, length, encoding) {
// Buffer#write(string);
if (offset === undefined) {
return this.utf8Write(string, 0, this.length);
return FunctionPrototypeCall(utf8Write, this, string, 0, this.length);
}
// Buffer#write(string, encoding)
if (length === undefined && typeof offset === 'string') {
Expand All @@ -1138,9 +1156,9 @@ Buffer.prototype.write = function write(string, offset, length, encoding) {
}

if (!encoding || encoding === 'utf8')
return this.utf8Write(string, offset, length);
return FunctionPrototypeCall(utf8Write, this, string, offset, length);
if (encoding === 'ascii')
return this.asciiWrite(string, offset, length);
return FunctionPrototypeCall(asciiWrite, this, string, offset, length);

const ops = getEncodingOps(encoding);
if (ops === undefined)
Expand Down
25 changes: 13 additions & 12 deletions lib/internal/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const {
BigInt,
Float32Array,
Float64Array,
FunctionPrototypeCall,
MathFloor,
Number,
Uint8Array,
Expand Down Expand Up @@ -177,11 +178,11 @@ function readUIntLE(offset, byteLength) {
if (byteLength === 3)
return readUInt24LE(this, offset);
if (byteLength === 4)
return this.readUInt32LE(offset);
return FunctionPrototypeCall(readUInt32LE, this, offset);
if (byteLength === 2)
return this.readUInt16LE(offset);
return FunctionPrototypeCall(readUInt16LE, this, offset);
if (byteLength === 1)
return this.readUInt8(offset);
return FunctionPrototypeCall(readUInt8, this, offset);

boundsError(byteLength, 6, 'byteLength');
}
Expand Down Expand Up @@ -266,11 +267,11 @@ function readUIntBE(offset, byteLength) {
if (byteLength === 3)
return readUInt24BE(this, offset);
if (byteLength === 4)
return this.readUInt32BE(offset);
return FunctionPrototypeCall(readUInt32BE, this, offset);
if (byteLength === 2)
return this.readUInt16BE(offset);
return FunctionPrototypeCall(readUInt16BE, this, offset);
if (byteLength === 1)
return this.readUInt8(offset);
return FunctionPrototypeCall(readUInt8, this, offset);

boundsError(byteLength, 6, 'byteLength');
}
Expand Down Expand Up @@ -346,11 +347,11 @@ function readIntLE(offset, byteLength) {
if (byteLength === 3)
return readInt24LE(this, offset);
if (byteLength === 4)
return this.readInt32LE(offset);
return FunctionPrototypeCall(readInt32LE, this, offset);
if (byteLength === 2)
return this.readInt16LE(offset);
return FunctionPrototypeCall(readInt16LE, this, offset);
if (byteLength === 1)
return this.readInt8(offset);
return FunctionPrototypeCall(readInt8, this, offset);

boundsError(byteLength, 6, 'byteLength');
}
Expand Down Expand Up @@ -438,11 +439,11 @@ function readIntBE(offset, byteLength) {
if (byteLength === 3)
return readInt24BE(this, offset);
if (byteLength === 4)
return this.readInt32BE(offset);
return FunctionPrototypeCall(readInt32BE, this, offset);
if (byteLength === 2)
return this.readInt16BE(offset);
return FunctionPrototypeCall(readInt16BE, this, offset);
if (byteLength === 1)
return this.readInt8(offset);
return FunctionPrototypeCall(readInt8, this, offset);

boundsError(byteLength, 6, 'byteLength');
}
Expand Down
Loading

0 comments on commit c73de47

Please sign in to comment.