Skip to content

Commit 9013bc4

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 646e19e commit 9013bc4

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,
@@ -630,44 +648,44 @@ const encodingOps = {
630648
encoding: 'utf8',
631649
encodingVal: encodingsMap.utf8,
632650
byteLength: byteLengthUtf8,
633-
write: (buf, string, offset, len) => buf.utf8Write(string, offset, len),
634-
slice: (buf, start, end) => buf.utf8Slice(start, end),
651+
write: (buf, string, offset, len) => FunctionPrototypeCall(utf8Write, buf, string, offset, len),
652+
slice: (buf, start, end) => FunctionPrototypeCall(utf8Slice, buf, start, end),
635653
indexOf: (buf, val, byteOffset, dir) =>
636654
indexOfString(buf, val, byteOffset, encodingsMap.utf8, dir),
637655
},
638656
ucs2: {
639657
encoding: 'ucs2',
640658
encodingVal: encodingsMap.utf16le,
641659
byteLength: (string) => string.length * 2,
642-
write: (buf, string, offset, len) => buf.ucs2Write(string, offset, len),
643-
slice: (buf, start, end) => buf.ucs2Slice(start, end),
660+
write: (buf, string, offset, len) => FunctionPrototypeCall(ucs2Write, buf, string, offset, len),
661+
slice: (buf, start, end) => FunctionPrototypeCall(ucs2Slice, buf, start, end),
644662
indexOf: (buf, val, byteOffset, dir) =>
645663
indexOfString(buf, val, byteOffset, encodingsMap.utf16le, dir),
646664
},
647665
utf16le: {
648666
encoding: 'utf16le',
649667
encodingVal: encodingsMap.utf16le,
650668
byteLength: (string) => string.length * 2,
651-
write: (buf, string, offset, len) => buf.ucs2Write(string, offset, len),
652-
slice: (buf, start, end) => buf.ucs2Slice(start, end),
669+
write: (buf, string, offset, len) => FunctionPrototypeCall(ucs2Write, buf, string, offset, len),
670+
slice: (buf, start, end) => FunctionPrototypeCall(ucs2Slice, buf, start, end),
653671
indexOf: (buf, val, byteOffset, dir) =>
654672
indexOfString(buf, val, byteOffset, encodingsMap.utf16le, dir),
655673
},
656674
latin1: {
657675
encoding: 'latin1',
658676
encodingVal: encodingsMap.latin1,
659677
byteLength: (string) => string.length,
660-
write: (buf, string, offset, len) => buf.latin1Write(string, offset, len),
661-
slice: (buf, start, end) => buf.latin1Slice(start, end),
678+
write: (buf, string, offset, len) => FunctionPrototypeCall(latin1Write, buf, string, offset, len),
679+
slice: (buf, start, end) => FunctionPrototypeCall(latin1Slice, buf, start, end),
662680
indexOf: (buf, val, byteOffset, dir) =>
663681
indexOfString(buf, val, byteOffset, encodingsMap.latin1, dir),
664682
},
665683
ascii: {
666684
encoding: 'ascii',
667685
encodingVal: encodingsMap.ascii,
668686
byteLength: (string) => string.length,
669-
write: (buf, string, offset, len) => buf.asciiWrite(string, offset, len),
670-
slice: (buf, start, end) => buf.asciiSlice(start, end),
687+
write: (buf, string, offset, len) => FunctionPrototypeCall(asciiWrite, buf, string, offset, len),
688+
slice: (buf, start, end) => FunctionPrototypeCall(asciiSlice, buf, start, end),
671689
indexOf: (buf, val, byteOffset, dir) =>
672690
indexOfBuffer(buf,
673691
fromStringFast(val, encodingOps.ascii),
@@ -679,8 +697,8 @@ const encodingOps = {
679697
encoding: 'base64',
680698
encodingVal: encodingsMap.base64,
681699
byteLength: (string) => base64ByteLength(string, string.length),
682-
write: (buf, string, offset, len) => buf.base64Write(string, offset, len),
683-
slice: (buf, start, end) => buf.base64Slice(start, end),
700+
write: (buf, string, offset, len) => FunctionPrototypeCall(base64Write, buf, string, offset, len),
701+
slice: (buf, start, end) => FunctionPrototypeCall(base64Slice, buf, start, end),
684702
indexOf: (buf, val, byteOffset, dir) =>
685703
indexOfBuffer(buf,
686704
fromStringFast(val, encodingOps.base64),
@@ -693,8 +711,8 @@ const encodingOps = {
693711
encodingVal: encodingsMap.base64url,
694712
byteLength: (string) => base64ByteLength(string, string.length),
695713
write: (buf, string, offset, len) =>
696-
buf.base64urlWrite(string, offset, len),
697-
slice: (buf, start, end) => buf.base64urlSlice(start, end),
714+
FunctionPrototypeCall(base64urlWrite, buf, string, offset, len),
715+
slice: (buf, start, end) => FunctionPrototypeCall(base64urlSlice, buf, start, end),
698716
indexOf: (buf, val, byteOffset, dir) =>
699717
indexOfBuffer(buf,
700718
fromStringFast(val, encodingOps.base64url),
@@ -706,8 +724,8 @@ const encodingOps = {
706724
encoding: 'hex',
707725
encodingVal: encodingsMap.hex,
708726
byteLength: (string) => string.length >>> 1,
709-
write: (buf, string, offset, len) => buf.hexWrite(string, offset, len),
710-
slice: (buf, start, end) => buf.hexSlice(start, end),
727+
write: (buf, string, offset, len) => FunctionPrototypeCall(hexWrite, buf, string, offset, len),
728+
slice: (buf, start, end) => FunctionPrototypeCall(hexSlice, buf, start, end),
711729
indexOf: (buf, val, byteOffset, dir) =>
712730
indexOfBuffer(buf,
713731
fromStringFast(val, encodingOps.hex),
@@ -832,7 +850,7 @@ Buffer.prototype.copy =
832850
// to their upper/lower bounds if the value passed is out of range.
833851
Buffer.prototype.toString = function toString(encoding, start, end) {
834852
if (arguments.length === 0) {
835-
return this.utf8Slice(0, this.length);
853+
return FunctionPrototypeCall(utf8Slice, this, 0, this.length);
836854
}
837855

838856
const len = this.length;
@@ -853,7 +871,7 @@ Buffer.prototype.toString = function toString(encoding, start, end) {
853871
return '';
854872

855873
if (encoding === undefined)
856-
return this.utf8Slice(start, end);
874+
return FunctionPrototypeCall(utf8Slice, this, start, end);
857875

858876
const ops = getEncodingOps(encoding);
859877
if (ops === undefined)
@@ -884,7 +902,7 @@ Buffer.prototype[customInspectSymbol] = function inspect(recurseTimes, ctx) {
884902
const actualMax = MathMin(max, this.length);
885903
const remaining = this.length - max;
886904
let str = StringPrototypeTrim(RegExpPrototypeSymbolReplace(
887-
/(.{2})/g, this.hexSlice(0, actualMax), '$1 '));
905+
/(.{2})/g, FunctionPrototypeCall(hexSlice, this, 0, actualMax), '$1 '));
888906
if (remaining > 0)
889907
str += ` ... ${remaining} more byte${remaining > 1 ? 's' : ''}`;
890908
// Inspect special properties as well, if possible.
@@ -1023,7 +1041,7 @@ Buffer.prototype.lastIndexOf = function lastIndexOf(val, byteOffset, encoding) {
10231041
};
10241042

10251043
Buffer.prototype.includes = function includes(val, byteOffset, encoding) {
1026-
return this.indexOf(val, byteOffset, encoding) !== -1;
1044+
return bidirectionalIndexOf(this, val, byteOffset, encoding, true) !== -1;
10271045
};
10281046

10291047
// Usage:
@@ -1108,7 +1126,7 @@ function _fill(buf, value, offset, end, encoding) {
11081126
Buffer.prototype.write = function write(string, offset, length, encoding) {
11091127
// Buffer#write(string);
11101128
if (offset === undefined) {
1111-
return this.utf8Write(string, 0, this.length);
1129+
return FunctionPrototypeCall(utf8Write, this, string, 0, this.length);
11121130
}
11131131
// Buffer#write(string, encoding)
11141132
if (length === undefined && typeof offset === 'string') {
@@ -1135,9 +1153,9 @@ Buffer.prototype.write = function write(string, offset, length, encoding) {
11351153
}
11361154

11371155
if (!encoding || encoding === 'utf8')
1138-
return this.utf8Write(string, offset, length);
1156+
return FunctionPrototypeCall(utf8Write, this, string, offset, length);
11391157
if (encoding === 'ascii')
1140-
return this.asciiWrite(string, offset, length);
1158+
return FunctionPrototypeCall(asciiWrite, this, string, offset, length);
11411159

11421160
const ops = getEncodingOps(encoding);
11431161
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)