Skip to content

Commit 01c4725

Browse files
committed
buffer: make methods work on Uint8Array instances
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: #56577
1 parent 6f946c9 commit 01c4725

File tree

5 files changed

+1212
-41
lines changed

5 files changed

+1212
-41
lines changed

doc/api/buffer.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,21 @@ function:
415415
* [`Buffer.from(arrayBuffer[, byteOffset[, length]])`][`Buffer.from(arrayBuf)`]
416416
* [`Buffer.from(string[, encoding])`][`Buffer.from(string)`]
417417

418+
### Buffer methods are callable with `Uint8Array` instances
419+
420+
All methods on the Buffer prototype are callable with a `Uint8Array` instance.
421+
422+
```js
423+
const { toString, write } = Buffer.prototype;
424+
425+
const uint8array = new Uint8Array(5);
426+
427+
write.call(uint8array, 'hello', 0, 5, 'utf8'); // 5
428+
// <Uint8Array 68 65 6c 6c 6f>
429+
430+
toString.call(uint8array, 'utf8'); // 'hello'
431+
```
432+
418433
## Buffers and iteration
419434

420435
`Buffer` instances can be iterated over using `for..of` syntax:
@@ -2059,6 +2074,10 @@ console.log(buf.fill('zz', 'hex'));
20592074

20602075
<!-- YAML
20612076
added: v5.3.0
2077+
changes:
2078+
- version: REPLACEME
2079+
pr-url: https://github.com/nodejs/node/pull/56578
2080+
description: supports Uint8Array as `this` value
20622081
-->
20632082

20642083
* `value` {string|Buffer|Uint8Array|integer} What to search for.
@@ -2950,6 +2969,9 @@ changes:
29502969
pr-url: https://github.com/nodejs/node/pull/18395
29512970
description: Removed `noAssert` and no implicit coercion of the offset
29522971
and `byteLength` to `uint32` anymore.
2972+
- version: REPLACEME
2973+
pr-url: https://github.com/nodejs/node/pull/56578
2974+
description: supports Uint8Array as `this` value
29532975
-->
29542976

29552977
* `offset` {integer} Number of bytes to skip before starting to read. Must
@@ -2997,6 +3019,9 @@ changes:
29973019
pr-url: https://github.com/nodejs/node/pull/18395
29983020
description: Removed `noAssert` and no implicit coercion of the offset
29993021
and `byteLength` to `uint32` anymore.
3022+
- version: REPLACEME
3023+
pr-url: https://github.com/nodejs/node/pull/56578
3024+
description: supports Uint8Array as `this` value
30003025
-->
30013026

30023027
* `offset` {integer} Number of bytes to skip before starting to read. Must
@@ -3279,6 +3304,9 @@ changes:
32793304
pr-url: https://github.com/nodejs/node/pull/18395
32803305
description: Removed `noAssert` and no implicit coercion of the offset
32813306
and `byteLength` to `uint32` anymore.
3307+
- version: REPLACEME
3308+
pr-url: https://github.com/nodejs/node/pull/56578
3309+
description: supports Uint8Array as `this` value
32823310
-->
32833311

32843312
* `offset` {integer} Number of bytes to skip before starting to read. Must
@@ -3329,6 +3357,9 @@ changes:
33293357
pr-url: https://github.com/nodejs/node/pull/18395
33303358
description: Removed `noAssert` and no implicit coercion of the offset
33313359
and `byteLength` to `uint32` anymore.
3360+
- version: REPLACEME
3361+
pr-url: https://github.com/nodejs/node/pull/56578
3362+
description: supports Uint8Array as `this` value
33323363
-->
33333364

33343365
* `offset` {integer} Number of bytes to skip before starting to read. Must
@@ -3772,6 +3803,10 @@ console.log(copy);
37723803

37733804
<!-- YAML
37743805
added: v0.1.90
3806+
changes:
3807+
- version: REPLACEME
3808+
pr-url: https://github.com/nodejs/node/pull/56578
3809+
description: supports Uint8Array as `this` value
37753810
-->
37763811

37773812
* `encoding` {string} The character encoding to use. **Default:** `'utf8'`.
@@ -3910,6 +3945,10 @@ for (const value of buf) {
39103945

39113946
<!-- YAML
39123947
added: v0.1.90
3948+
changes:
3949+
- version: REPLACEME
3950+
pr-url: https://github.com/nodejs/node/pull/56578
3951+
description: supports Uint8Array as `this` value
39133952
-->
39143953

39153954
* `string` {string} String to write to `buf`.

lib/buffer.js

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const {
2626
ArrayBufferIsView,
2727
ArrayIsArray,
2828
ArrayPrototypeForEach,
29+
FunctionPrototypeCall,
2930
MathFloor,
3031
MathMin,
3132
MathTrunc,
@@ -135,6 +136,23 @@ FastBuffer.prototype.constructor = Buffer;
135136
Buffer.prototype = FastBuffer.prototype;
136137
addBufferPrototypeMethods(Buffer.prototype);
137138

139+
const {
140+
asciiWrite,
141+
latin1Write,
142+
utf8Write,
143+
asciiSlice,
144+
base64Slice,
145+
base64urlSlice,
146+
latin1Slice,
147+
hexSlice,
148+
ucs2Slice,
149+
utf8Slice,
150+
base64Write,
151+
base64urlWrite,
152+
hexWrite,
153+
ucs2Write,
154+
} = Buffer.prototype;
155+
138156
const constants = ObjectDefineProperties({}, {
139157
MAX_LENGTH: {
140158
__proto__: null,
@@ -633,44 +651,44 @@ const encodingOps = {
633651
encoding: 'utf8',
634652
encodingVal: encodingsMap.utf8,
635653
byteLength: byteLengthUtf8,
636-
write: (buf, string, offset, len) => buf.utf8Write(string, offset, len),
637-
slice: (buf, start, end) => buf.utf8Slice(start, end),
654+
write: (buf, string, offset, len) => FunctionPrototypeCall(utf8Write, buf, string, offset, len),
655+
slice: (buf, start, end) => FunctionPrototypeCall(utf8Slice, buf, start, end),
638656
indexOf: (buf, val, byteOffset, dir) =>
639657
indexOfString(buf, val, byteOffset, encodingsMap.utf8, dir),
640658
},
641659
ucs2: {
642660
encoding: 'ucs2',
643661
encodingVal: encodingsMap.utf16le,
644662
byteLength: (string) => string.length * 2,
645-
write: (buf, string, offset, len) => buf.ucs2Write(string, offset, len),
646-
slice: (buf, start, end) => buf.ucs2Slice(start, end),
663+
write: (buf, string, offset, len) => FunctionPrototypeCall(ucs2Write, buf, string, offset, len),
664+
slice: (buf, start, end) => FunctionPrototypeCall(ucs2Slice, buf, start, end),
647665
indexOf: (buf, val, byteOffset, dir) =>
648666
indexOfString(buf, val, byteOffset, encodingsMap.utf16le, dir),
649667
},
650668
utf16le: {
651669
encoding: 'utf16le',
652670
encodingVal: encodingsMap.utf16le,
653671
byteLength: (string) => string.length * 2,
654-
write: (buf, string, offset, len) => buf.ucs2Write(string, offset, len),
655-
slice: (buf, start, end) => buf.ucs2Slice(start, end),
672+
write: (buf, string, offset, len) => FunctionPrototypeCall(ucs2Write, buf, string, offset, len),
673+
slice: (buf, start, end) => FunctionPrototypeCall(ucs2Slice, buf, start, end),
656674
indexOf: (buf, val, byteOffset, dir) =>
657675
indexOfString(buf, val, byteOffset, encodingsMap.utf16le, dir),
658676
},
659677
latin1: {
660678
encoding: 'latin1',
661679
encodingVal: encodingsMap.latin1,
662680
byteLength: (string) => string.length,
663-
write: (buf, string, offset, len) => buf.latin1Write(string, offset, len),
664-
slice: (buf, start, end) => buf.latin1Slice(start, end),
681+
write: (buf, string, offset, len) => FunctionPrototypeCall(latin1Write, buf, string, offset, len),
682+
slice: (buf, start, end) => FunctionPrototypeCall(latin1Slice, buf, start, end),
665683
indexOf: (buf, val, byteOffset, dir) =>
666684
indexOfString(buf, val, byteOffset, encodingsMap.latin1, dir),
667685
},
668686
ascii: {
669687
encoding: 'ascii',
670688
encodingVal: encodingsMap.ascii,
671689
byteLength: (string) => string.length,
672-
write: (buf, string, offset, len) => buf.asciiWrite(string, offset, len),
673-
slice: (buf, start, end) => buf.asciiSlice(start, end),
690+
write: (buf, string, offset, len) => FunctionPrototypeCall(asciiWrite, buf, string, offset, len),
691+
slice: (buf, start, end) => FunctionPrototypeCall(asciiSlice, buf, start, end),
674692
indexOf: (buf, val, byteOffset, dir) =>
675693
indexOfBuffer(buf,
676694
fromStringFast(val, encodingOps.ascii),
@@ -682,8 +700,8 @@ const encodingOps = {
682700
encoding: 'base64',
683701
encodingVal: encodingsMap.base64,
684702
byteLength: (string) => base64ByteLength(string, string.length),
685-
write: (buf, string, offset, len) => buf.base64Write(string, offset, len),
686-
slice: (buf, start, end) => buf.base64Slice(start, end),
703+
write: (buf, string, offset, len) => FunctionPrototypeCall(base64Write, buf, string, offset, len),
704+
slice: (buf, start, end) => FunctionPrototypeCall(base64Slice, buf, start, end),
687705
indexOf: (buf, val, byteOffset, dir) =>
688706
indexOfBuffer(buf,
689707
fromStringFast(val, encodingOps.base64),
@@ -696,8 +714,8 @@ const encodingOps = {
696714
encodingVal: encodingsMap.base64url,
697715
byteLength: (string) => base64ByteLength(string, string.length),
698716
write: (buf, string, offset, len) =>
699-
buf.base64urlWrite(string, offset, len),
700-
slice: (buf, start, end) => buf.base64urlSlice(start, end),
717+
FunctionPrototypeCall(base64urlWrite, buf, string, offset, len),
718+
slice: (buf, start, end) => FunctionPrototypeCall(base64urlSlice, buf, start, end),
701719
indexOf: (buf, val, byteOffset, dir) =>
702720
indexOfBuffer(buf,
703721
fromStringFast(val, encodingOps.base64url),
@@ -709,8 +727,8 @@ const encodingOps = {
709727
encoding: 'hex',
710728
encodingVal: encodingsMap.hex,
711729
byteLength: (string) => string.length >>> 1,
712-
write: (buf, string, offset, len) => buf.hexWrite(string, offset, len),
713-
slice: (buf, start, end) => buf.hexSlice(start, end),
730+
write: (buf, string, offset, len) => FunctionPrototypeCall(hexWrite, buf, string, offset, len),
731+
slice: (buf, start, end) => FunctionPrototypeCall(hexSlice, buf, start, end),
714732
indexOf: (buf, val, byteOffset, dir) =>
715733
indexOfBuffer(buf,
716734
fromStringFast(val, encodingOps.hex),
@@ -835,7 +853,7 @@ Buffer.prototype.copy =
835853
// to their upper/lower bounds if the value passed is out of range.
836854
Buffer.prototype.toString = function toString(encoding, start, end) {
837855
if (arguments.length === 0) {
838-
return this.utf8Slice(0, this.length);
856+
return FunctionPrototypeCall(utf8Slice, this, 0, this.length);
839857
}
840858

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

858876
if (encoding === undefined)
859-
return this.utf8Slice(start, end);
877+
return FunctionPrototypeCall(utf8Slice, this, start, end);
860878

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

10281046
Buffer.prototype.includes = function includes(val, byteOffset, encoding) {
1029-
return this.indexOf(val, byteOffset, encoding) !== -1;
1047+
return bidirectionalIndexOf(this, val, byteOffset, encoding, true) !== -1;
10301048
};
10311049

10321050
// Usage:
@@ -1111,7 +1129,7 @@ function _fill(buf, value, offset, end, encoding) {
11111129
Buffer.prototype.write = function write(string, offset, length, encoding) {
11121130
// Buffer#write(string);
11131131
if (offset === undefined) {
1114-
return this.utf8Write(string, 0, this.length);
1132+
return FunctionPrototypeCall(utf8Write, this, string, 0, this.length);
11151133
}
11161134
// Buffer#write(string, encoding)
11171135
if (length === undefined && typeof offset === 'string') {
@@ -1138,9 +1156,9 @@ Buffer.prototype.write = function write(string, offset, length, encoding) {
11381156
}
11391157

11401158
if (!encoding || encoding === 'utf8')
1141-
return this.utf8Write(string, offset, length);
1159+
return FunctionPrototypeCall(utf8Write, this, string, offset, length);
11421160
if (encoding === 'ascii')
1143-
return this.asciiWrite(string, offset, length);
1161+
return FunctionPrototypeCall(asciiWrite, this, string, offset, length);
11441162

11451163
const ops = getEncodingOps(encoding);
11461164
if (ops === undefined)

lib/internal/buffer.js

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const {
44
BigInt,
55
Float32Array,
66
Float64Array,
7+
FunctionPrototypeCall,
78
MathFloor,
89
Number,
910
Uint8Array,
@@ -177,11 +178,11 @@ function readUIntLE(offset, byteLength) {
177178
if (byteLength === 3)
178179
return readUInt24LE(this, offset);
179180
if (byteLength === 4)
180-
return this.readUInt32LE(offset);
181+
return FunctionPrototypeCall(readUInt32LE, this, offset);
181182
if (byteLength === 2)
182-
return this.readUInt16LE(offset);
183+
return FunctionPrototypeCall(readUInt16LE, this, offset);
183184
if (byteLength === 1)
184-
return this.readUInt8(offset);
185+
return FunctionPrototypeCall(readUInt8, this, offset);
185186

186187
boundsError(byteLength, 6, 'byteLength');
187188
}
@@ -266,11 +267,11 @@ function readUIntBE(offset, byteLength) {
266267
if (byteLength === 3)
267268
return readUInt24BE(this, offset);
268269
if (byteLength === 4)
269-
return this.readUInt32BE(offset);
270+
return FunctionPrototypeCall(readUInt32BE, this, offset);
270271
if (byteLength === 2)
271-
return this.readUInt16BE(offset);
272+
return FunctionPrototypeCall(readUInt16BE, this, offset);
272273
if (byteLength === 1)
273-
return this.readUInt8(offset);
274+
return FunctionPrototypeCall(readUInt8, this, offset);
274275

275276
boundsError(byteLength, 6, 'byteLength');
276277
}
@@ -346,11 +347,11 @@ function readIntLE(offset, byteLength) {
346347
if (byteLength === 3)
347348
return readInt24LE(this, offset);
348349
if (byteLength === 4)
349-
return this.readInt32LE(offset);
350+
return FunctionPrototypeCall(readInt32LE, this, offset);
350351
if (byteLength === 2)
351-
return this.readInt16LE(offset);
352+
return FunctionPrototypeCall(readInt16LE, this, offset);
352353
if (byteLength === 1)
353-
return this.readInt8(offset);
354+
return FunctionPrototypeCall(readInt8, this, offset);
354355

355356
boundsError(byteLength, 6, 'byteLength');
356357
}
@@ -438,11 +439,11 @@ function readIntBE(offset, byteLength) {
438439
if (byteLength === 3)
439440
return readInt24BE(this, offset);
440441
if (byteLength === 4)
441-
return this.readInt32BE(offset);
442+
return FunctionPrototypeCall(readInt32BE, this, offset);
442443
if (byteLength === 2)
443-
return this.readInt16BE(offset);
444+
return FunctionPrototypeCall(readInt16BE, this, offset);
444445
if (byteLength === 1)
445-
return this.readInt8(offset);
446+
return FunctionPrototypeCall(readInt8, this, offset);
446447

447448
boundsError(byteLength, 6, 'byteLength');
448449
}

test/fixtures/permission/fs-traversal.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,6 @@ const uint8ArrayTraversalPath = new TextEncoder().encode(traversalPath);
9898
return w.apply(this, [traversalPath, ...args]);
9999
})(Buffer.prototype.utf8Write);
100100

101-
// Sanity check (remove if the internals of Buffer.from change):
102-
// The custom implementation of utf8Write should cause Buffer.from() to encode
103-
// traversalPath instead of the sanitized output of resolve().
104-
assert.strictEqual(Buffer.from(resolve(traversalPathWithExtraChars)).toString(), traversalPath);
105-
106101
assert.throws(() => {
107102
fs.readFileSync(traversalPathWithExtraBytes);
108103
}, common.expectsError({
@@ -125,4 +120,4 @@ const uint8ArrayTraversalPath = new TextEncoder().encode(traversalPath);
125120
assert.ok(!process.permission.has('fs.write', traversalPath));
126121
assert.ok(!process.permission.has('fs.read', traversalFolderPath));
127122
assert.ok(!process.permission.has('fs.write', traversalFolderPath));
128-
}
123+
}

0 commit comments

Comments
 (0)