Skip to content

Commit eab819b

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 525c4fb commit eab819b

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:
@@ -2058,6 +2073,10 @@ console.log(buf.fill('zz', 'hex'));
20582073

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

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

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

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

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

33333364
* `offset` {integer} Number of bytes to skip before starting to read. Must
@@ -3771,6 +3802,10 @@ console.log(copy);
37713802

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

37763811
* `encoding` {string} The character encoding to use. **Default:** `'utf8'`.
@@ -3909,6 +3944,10 @@ for (const value of buf) {
39093944

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

39143953
* `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,
@@ -133,6 +134,23 @@ FastBuffer.prototype.constructor = Buffer;
133134
Buffer.prototype = FastBuffer.prototype;
134135
addBufferPrototypeMethods(Buffer.prototype);
135136

137+
const {
138+
asciiWrite,
139+
latin1Write,
140+
utf8Write,
141+
asciiSlice,
142+
base64Slice,
143+
base64urlSlice,
144+
latin1Slice,
145+
hexSlice,
146+
ucs2Slice,
147+
utf8Slice,
148+
base64Write,
149+
base64urlWrite,
150+
hexWrite,
151+
ucs2Write,
152+
} = Buffer.prototype;
153+
136154
const constants = ObjectDefineProperties({}, {
137155
MAX_LENGTH: {
138156
__proto__: null,
@@ -634,44 +652,44 @@ const encodingOps = {
634652
encoding: 'utf8',
635653
encodingVal: encodingsMap.utf8,
636654
byteLength: byteLengthUtf8,
637-
write: (buf, string, offset, len) => buf.utf8Write(string, offset, len),
638-
slice: (buf, start, end) => buf.utf8Slice(start, end),
655+
write: (buf, string, offset, len) => FunctionPrototypeCall(utf8Write, buf, string, offset, len),
656+
slice: (buf, start, end) => FunctionPrototypeCall(utf8Slice, buf, start, end),
639657
indexOf: (buf, val, byteOffset, dir) =>
640658
indexOfString(buf, val, byteOffset, encodingsMap.utf8, dir),
641659
},
642660
ucs2: {
643661
encoding: 'ucs2',
644662
encodingVal: encodingsMap.utf16le,
645663
byteLength: (string) => string.length * 2,
646-
write: (buf, string, offset, len) => buf.ucs2Write(string, offset, len),
647-
slice: (buf, start, end) => buf.ucs2Slice(start, end),
664+
write: (buf, string, offset, len) => FunctionPrototypeCall(ucs2Write, buf, string, offset, len),
665+
slice: (buf, start, end) => FunctionPrototypeCall(ucs2Slice, buf, start, end),
648666
indexOf: (buf, val, byteOffset, dir) =>
649667
indexOfString(buf, val, byteOffset, encodingsMap.utf16le, dir),
650668
},
651669
utf16le: {
652670
encoding: 'utf16le',
653671
encodingVal: encodingsMap.utf16le,
654672
byteLength: (string) => string.length * 2,
655-
write: (buf, string, offset, len) => buf.ucs2Write(string, offset, len),
656-
slice: (buf, start, end) => buf.ucs2Slice(start, end),
673+
write: (buf, string, offset, len) => FunctionPrototypeCall(ucs2Write, buf, string, offset, len),
674+
slice: (buf, start, end) => FunctionPrototypeCall(ucs2Slice, buf, start, end),
657675
indexOf: (buf, val, byteOffset, dir) =>
658676
indexOfString(buf, val, byteOffset, encodingsMap.utf16le, dir),
659677
},
660678
latin1: {
661679
encoding: 'latin1',
662680
encodingVal: encodingsMap.latin1,
663681
byteLength: (string) => string.length,
664-
write: (buf, string, offset, len) => buf.latin1Write(string, offset, len),
665-
slice: (buf, start, end) => buf.latin1Slice(start, end),
682+
write: (buf, string, offset, len) => FunctionPrototypeCall(latin1Write, buf, string, offset, len),
683+
slice: (buf, start, end) => FunctionPrototypeCall(latin1Slice, buf, start, end),
666684
indexOf: (buf, val, byteOffset, dir) =>
667685
indexOfString(buf, val, byteOffset, encodingsMap.latin1, dir),
668686
},
669687
ascii: {
670688
encoding: 'ascii',
671689
encodingVal: encodingsMap.ascii,
672690
byteLength: (string) => string.length,
673-
write: (buf, string, offset, len) => buf.asciiWrite(string, offset, len),
674-
slice: (buf, start, end) => buf.asciiSlice(start, end),
691+
write: (buf, string, offset, len) => FunctionPrototypeCall(asciiWrite, buf, string, offset, len),
692+
slice: (buf, start, end) => FunctionPrototypeCall(asciiSlice, buf, start, end),
675693
indexOf: (buf, val, byteOffset, dir) =>
676694
indexOfBuffer(buf,
677695
fromStringFast(val, encodingOps.ascii),
@@ -683,8 +701,8 @@ const encodingOps = {
683701
encoding: 'base64',
684702
encodingVal: encodingsMap.base64,
685703
byteLength: (string) => base64ByteLength(string, string.length),
686-
write: (buf, string, offset, len) => buf.base64Write(string, offset, len),
687-
slice: (buf, start, end) => buf.base64Slice(start, end),
704+
write: (buf, string, offset, len) => FunctionPrototypeCall(base64Write, buf, string, offset, len),
705+
slice: (buf, start, end) => FunctionPrototypeCall(base64Slice, buf, start, end),
688706
indexOf: (buf, val, byteOffset, dir) =>
689707
indexOfBuffer(buf,
690708
fromStringFast(val, encodingOps.base64),
@@ -697,8 +715,8 @@ const encodingOps = {
697715
encodingVal: encodingsMap.base64url,
698716
byteLength: (string) => base64ByteLength(string, string.length),
699717
write: (buf, string, offset, len) =>
700-
buf.base64urlWrite(string, offset, len),
701-
slice: (buf, start, end) => buf.base64urlSlice(start, end),
718+
FunctionPrototypeCall(base64urlWrite, buf, string, offset, len),
719+
slice: (buf, start, end) => FunctionPrototypeCall(base64urlSlice, buf, start, end),
702720
indexOf: (buf, val, byteOffset, dir) =>
703721
indexOfBuffer(buf,
704722
fromStringFast(val, encodingOps.base64url),
@@ -710,8 +728,8 @@ const encodingOps = {
710728
encoding: 'hex',
711729
encodingVal: encodingsMap.hex,
712730
byteLength: (string) => string.length >>> 1,
713-
write: (buf, string, offset, len) => buf.hexWrite(string, offset, len),
714-
slice: (buf, start, end) => buf.hexSlice(start, end),
731+
write: (buf, string, offset, len) => FunctionPrototypeCall(hexWrite, buf, string, offset, len),
732+
slice: (buf, start, end) => FunctionPrototypeCall(hexSlice, buf, start, end),
715733
indexOf: (buf, val, byteOffset, dir) =>
716734
indexOfBuffer(buf,
717735
fromStringFast(val, encodingOps.hex),
@@ -836,7 +854,7 @@ Buffer.prototype.copy =
836854
// to their upper/lower bounds if the value passed is out of range.
837855
Buffer.prototype.toString = function toString(encoding, start, end) {
838856
if (arguments.length === 0) {
839-
return this.utf8Slice(0, this.length);
857+
return FunctionPrototypeCall(utf8Slice, this, 0, this.length);
840858
}
841859

842860
const len = this.length;
@@ -857,7 +875,7 @@ Buffer.prototype.toString = function toString(encoding, start, end) {
857875
return '';
858876

859877
if (encoding === undefined)
860-
return this.utf8Slice(start, end);
878+
return FunctionPrototypeCall(utf8Slice, this, start, end);
861879

862880
const ops = getEncodingOps(encoding);
863881
if (ops === undefined)
@@ -888,7 +906,7 @@ Buffer.prototype[customInspectSymbol] = function inspect(recurseTimes, ctx) {
888906
const actualMax = MathMin(max, this.length);
889907
const remaining = this.length - max;
890908
let str = StringPrototypeTrim(RegExpPrototypeSymbolReplace(
891-
/(.{2})/g, this.hexSlice(0, actualMax), '$1 '));
909+
/(.{2})/g, FunctionPrototypeCall(hexSlice, this, 0, actualMax), '$1 '));
892910
if (remaining > 0)
893911
str += ` ... ${remaining} more byte${remaining > 1 ? 's' : ''}`;
894912
// Inspect special properties as well, if possible.
@@ -1027,7 +1045,7 @@ Buffer.prototype.lastIndexOf = function lastIndexOf(val, byteOffset, encoding) {
10271045
};
10281046

10291047
Buffer.prototype.includes = function includes(val, byteOffset, encoding) {
1030-
return this.indexOf(val, byteOffset, encoding) !== -1;
1048+
return bidirectionalIndexOf(this, val, byteOffset, encoding, true) !== -1;
10311049
};
10321050

10331051
// Usage:
@@ -1112,7 +1130,7 @@ function _fill(buf, value, offset, end, encoding) {
11121130
Buffer.prototype.write = function write(string, offset, length, encoding) {
11131131
// Buffer#write(string);
11141132
if (offset === undefined) {
1115-
return this.utf8Write(string, 0, this.length);
1133+
return FunctionPrototypeCall(utf8Write, this, string, 0, this.length);
11161134
}
11171135
// Buffer#write(string, encoding)
11181136
if (length === undefined && typeof offset === 'string') {
@@ -1139,9 +1157,9 @@ Buffer.prototype.write = function write(string, offset, length, encoding) {
11391157
}
11401158

11411159
if (!encoding || encoding === 'utf8')
1142-
return this.utf8Write(string, offset, length);
1160+
return FunctionPrototypeCall(utf8Write, this, string, offset, length);
11431161
if (encoding === 'ascii')
1144-
return this.asciiWrite(string, offset, length);
1162+
return FunctionPrototypeCall(asciiWrite, this, string, offset, length);
11451163

11461164
const ops = getEncodingOps(encoding);
11471165
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,
@@ -184,11 +185,11 @@ function readUIntLE(offset, byteLength) {
184185
if (byteLength === 3)
185186
return readUInt24LE(this, offset);
186187
if (byteLength === 4)
187-
return this.readUInt32LE(offset);
188+
return FunctionPrototypeCall(readUInt32LE, this, offset);
188189
if (byteLength === 2)
189-
return this.readUInt16LE(offset);
190+
return FunctionPrototypeCall(readUInt16LE, this, offset);
190191
if (byteLength === 1)
191-
return this.readUInt8(offset);
192+
return FunctionPrototypeCall(readUInt8, this, offset);
192193

193194
boundsError(byteLength, 6, 'byteLength');
194195
}
@@ -273,11 +274,11 @@ function readUIntBE(offset, byteLength) {
273274
if (byteLength === 3)
274275
return readUInt24BE(this, offset);
275276
if (byteLength === 4)
276-
return this.readUInt32BE(offset);
277+
return FunctionPrototypeCall(readUInt32BE, this, offset);
277278
if (byteLength === 2)
278-
return this.readUInt16BE(offset);
279+
return FunctionPrototypeCall(readUInt16BE, this, offset);
279280
if (byteLength === 1)
280-
return this.readUInt8(offset);
281+
return FunctionPrototypeCall(readUInt8, this, offset);
281282

282283
boundsError(byteLength, 6, 'byteLength');
283284
}
@@ -353,11 +354,11 @@ function readIntLE(offset, byteLength) {
353354
if (byteLength === 3)
354355
return readInt24LE(this, offset);
355356
if (byteLength === 4)
356-
return this.readInt32LE(offset);
357+
return FunctionPrototypeCall(readInt32LE, this, offset);
357358
if (byteLength === 2)
358-
return this.readInt16LE(offset);
359+
return FunctionPrototypeCall(readInt16LE, this, offset);
359360
if (byteLength === 1)
360-
return this.readInt8(offset);
361+
return FunctionPrototypeCall(readInt8, this, offset);
361362

362363
boundsError(byteLength, 6, 'byteLength');
363364
}
@@ -445,11 +446,11 @@ function readIntBE(offset, byteLength) {
445446
if (byteLength === 3)
446447
return readInt24BE(this, offset);
447448
if (byteLength === 4)
448-
return this.readInt32BE(offset);
449+
return FunctionPrototypeCall(readInt32BE, this, offset);
449450
if (byteLength === 2)
450-
return this.readInt16BE(offset);
451+
return FunctionPrototypeCall(readInt16BE, this, offset);
451452
if (byteLength === 1)
452-
return this.readInt8(offset);
453+
return FunctionPrototypeCall(readInt8, this, offset);
453454

454455
boundsError(byteLength, 6, 'byteLength');
455456
}

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)