Skip to content

Commit 7e3be29

Browse files
committed
lib: short-circuit WebIDL BufferSource SAB check
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
1 parent a79e224 commit 7e3be29

File tree

3 files changed

+283
-7
lines changed

3 files changed

+283
-7
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
5+
const bench = common.createBenchmark(main, {
6+
input: [
7+
'arraybuffer',
8+
'buffer',
9+
'dataview',
10+
'uint8array',
11+
'uint8clampedarray',
12+
'int8array',
13+
'uint16array',
14+
'int16array',
15+
'uint32array',
16+
'int32array',
17+
'float16array',
18+
'float32array',
19+
'float64array',
20+
'bigint64array',
21+
'biguint64array',
22+
],
23+
n: [1e6],
24+
}, { flags: ['--expose-internals'] });
25+
26+
function main({ n, input }) {
27+
const { converters } = require('internal/webidl');
28+
29+
let value;
30+
switch (input) {
31+
case 'arraybuffer': value = new ArrayBuffer(32); break;
32+
case 'buffer': value = Buffer.alloc(32); break;
33+
case 'dataview': value = new DataView(new ArrayBuffer(32)); break;
34+
case 'uint8array': value = new Uint8Array(32); break;
35+
case 'uint8clampedarray': value = new Uint8ClampedArray(32); break;
36+
case 'int8array': value = new Int8Array(32); break;
37+
case 'uint16array': value = new Uint16Array(16); break;
38+
case 'int16array': value = new Int16Array(16); break;
39+
case 'uint32array': value = new Uint32Array(8); break;
40+
case 'int32array': value = new Int32Array(8); break;
41+
case 'float16array': value = new Float16Array(16); break;
42+
case 'float32array': value = new Float32Array(8); break;
43+
case 'float64array': value = new Float64Array(4); break;
44+
case 'bigint64array': value = new BigInt64Array(4); break;
45+
case 'biguint64array': value = new BigUint64Array(4); break;
46+
}
47+
48+
const opts = { prefix: 'test', context: 'test' };
49+
50+
bench.start();
51+
for (let i = 0; i < n; i++)
52+
converters.BufferSource(value, opts);
53+
bench.end(n);
54+
}

lib/internal/webidl.js

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const {
44
ArrayBufferIsView,
5+
ArrayBufferPrototypeGetByteLength,
56
ArrayPrototypePush,
67
ArrayPrototypeToSorted,
78
DataViewPrototypeGetBuffer,
@@ -33,8 +34,7 @@ const {
3334
const { kEmptyObject } = require('internal/util');
3435
const {
3536
isArrayBuffer,
36-
isDataView,
37-
isSharedArrayBuffer,
37+
isTypedArray,
3838
} = require('internal/util/types');
3939

4040
const converters = { __proto__: null };
@@ -390,9 +390,23 @@ function createInterfaceConverter(name, I) {
390390
};
391391
}
392392

393-
function getDataViewOrTypedArrayBuffer(V) {
394-
return isDataView(V) ?
395-
DataViewPrototypeGetBuffer(V) : TypedArrayPrototypeGetBuffer(V);
393+
// Returns the [[ViewedArrayBuffer]] of an ArrayBufferView without leaving JS.
394+
function getViewedArrayBuffer(V) {
395+
return isTypedArray(V) ?
396+
TypedArrayPrototypeGetBuffer(V) : DataViewPrototypeGetBuffer(V);
397+
}
398+
399+
// Returns `true` if `buffer` is a `SharedArrayBuffer`. Uses a brand check via
400+
// the `ArrayBuffer.prototype.byteLength` getter, which succeeds only on real
401+
// (non-shared) ArrayBuffers and throws on SharedArrayBuffers — independent
402+
// of the receiver's prototype chain.
403+
function isSharedArrayBufferBacking(buffer) {
404+
try {
405+
ArrayBufferPrototypeGetByteLength(buffer);
406+
return false;
407+
} catch {
408+
return true;
409+
}
396410
}
397411

398412
// https://webidl.spec.whatwg.org/#ArrayBufferView
@@ -402,7 +416,7 @@ converters.ArrayBufferView = (V, opts = kEmptyObject) => {
402416
'is not an ArrayBufferView.',
403417
opts);
404418
}
405-
if (isSharedArrayBuffer(getDataViewOrTypedArrayBuffer(V))) {
419+
if (isSharedArrayBufferBacking(getViewedArrayBuffer(V))) {
406420
throw makeException(
407421
'is a view on a SharedArrayBuffer, which is not allowed.',
408422
opts);
@@ -414,7 +428,7 @@ converters.ArrayBufferView = (V, opts = kEmptyObject) => {
414428
// https://webidl.spec.whatwg.org/#BufferSource
415429
converters.BufferSource = (V, opts = kEmptyObject) => {
416430
if (ArrayBufferIsView(V)) {
417-
if (isSharedArrayBuffer(getDataViewOrTypedArrayBuffer(V))) {
431+
if (isSharedArrayBufferBacking(getViewedArrayBuffer(V))) {
418432
throw makeException(
419433
'is a view on a SharedArrayBuffer, which is not allowed.',
420434
opts);
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
4+
require('../common');
5+
const assert = require('assert');
6+
const { test } = require('node:test');
7+
8+
const { converters } = require('internal/webidl');
9+
10+
const TYPED_ARRAY_CTORS = [
11+
Uint8Array, Int8Array, Uint8ClampedArray,
12+
Uint16Array, Int16Array,
13+
Uint32Array, Int32Array,
14+
Float16Array, Float32Array, Float64Array,
15+
BigInt64Array, BigUint64Array,
16+
];
17+
18+
test('BufferSource accepts ArrayBuffer', () => {
19+
const ab = new ArrayBuffer(8);
20+
assert.strictEqual(converters.BufferSource(ab), ab);
21+
});
22+
23+
test('BufferSource accepts all TypedArray kinds', () => {
24+
for (const Ctor of TYPED_ARRAY_CTORS) {
25+
const ta = new Ctor(4);
26+
assert.strictEqual(converters.BufferSource(ta), ta);
27+
}
28+
});
29+
30+
test('BufferSource accepts Buffer', () => {
31+
const buf = Buffer.alloc(8);
32+
assert.strictEqual(converters.BufferSource(buf), buf);
33+
});
34+
35+
test('BufferSource accepts DataView', () => {
36+
const dv = new DataView(new ArrayBuffer(8));
37+
assert.strictEqual(converters.BufferSource(dv), dv);
38+
});
39+
40+
test('BufferSource accepts ArrayBuffer subclass instance', () => {
41+
class MyAB extends ArrayBuffer {}
42+
const sub = new MyAB(8);
43+
assert.strictEqual(converters.BufferSource(sub), sub);
44+
});
45+
46+
test('BufferSource accepts TypedArray with null prototype', () => {
47+
const ta = new Uint8Array(4);
48+
Object.setPrototypeOf(ta, null);
49+
assert.strictEqual(converters.BufferSource(ta), ta);
50+
});
51+
52+
test('BufferSource accepts DataView with null prototype', () => {
53+
const dv = new DataView(new ArrayBuffer(4));
54+
Object.setPrototypeOf(dv, null);
55+
assert.strictEqual(converters.BufferSource(dv), dv);
56+
});
57+
58+
test('BufferSource accepts ArrayBuffer with null prototype', () => {
59+
const ab = new ArrayBuffer(4);
60+
Object.setPrototypeOf(ab, null);
61+
assert.strictEqual(converters.BufferSource(ab), ab);
62+
});
63+
64+
test('BufferSource rejects SharedArrayBuffer', () => {
65+
assert.throws(
66+
() => converters.BufferSource(new SharedArrayBuffer(4)),
67+
{ code: 'ERR_INVALID_ARG_TYPE' },
68+
);
69+
});
70+
71+
test('BufferSource rejects SAB-backed TypedArray', () => {
72+
const view = new Uint8Array(new SharedArrayBuffer(4));
73+
assert.throws(
74+
() => converters.BufferSource(view),
75+
{ code: 'ERR_INVALID_ARG_TYPE' },
76+
);
77+
});
78+
79+
test('BufferSource rejects SAB-backed DataView', () => {
80+
const dv = new DataView(new SharedArrayBuffer(4));
81+
assert.throws(
82+
() => converters.BufferSource(dv),
83+
{ code: 'ERR_INVALID_ARG_TYPE' },
84+
);
85+
});
86+
87+
test('BufferSource rejects SAB view whose buffer prototype was reassigned', () => {
88+
const sab = new SharedArrayBuffer(4);
89+
Object.setPrototypeOf(sab, ArrayBuffer.prototype);
90+
const view = new Uint8Array(sab);
91+
assert.throws(
92+
() => converters.BufferSource(view),
93+
{ code: 'ERR_INVALID_ARG_TYPE' },
94+
);
95+
});
96+
97+
test('BufferSource accepts a detached ArrayBuffer', () => {
98+
const ab = new ArrayBuffer(8);
99+
structuredClone(ab, { transfer: [ab] });
100+
assert.strictEqual(ab.byteLength, 0);
101+
assert.strictEqual(converters.BufferSource(ab), ab);
102+
});
103+
104+
test('BufferSource rejects objects with a forged @@toStringTag', () => {
105+
const fake = { [Symbol.toStringTag]: 'Uint8Array' };
106+
assert.throws(
107+
() => converters.BufferSource(fake),
108+
{ code: 'ERR_INVALID_ARG_TYPE' },
109+
);
110+
});
111+
112+
for (const value of [null, undefined, 0, 1, 1n, '', 'x', true, Symbol('s'), [],
113+
{}, () => {}]) {
114+
test(`BufferSource rejects ${typeof value} ${String(value)}`, () => {
115+
assert.throws(
116+
() => converters.BufferSource(value),
117+
{ code: 'ERR_INVALID_ARG_TYPE' },
118+
);
119+
});
120+
}
121+
122+
test('ArrayBufferView accepts all TypedArray kinds', () => {
123+
for (const Ctor of TYPED_ARRAY_CTORS) {
124+
const ta = new Ctor(4);
125+
assert.strictEqual(converters.ArrayBufferView(ta), ta);
126+
}
127+
});
128+
129+
test('ArrayBufferView accepts DataView', () => {
130+
const dv = new DataView(new ArrayBuffer(8));
131+
assert.strictEqual(converters.ArrayBufferView(dv), dv);
132+
});
133+
134+
test('ArrayBufferView accepts TypedArray subclass instance', () => {
135+
class MyU8 extends Uint8Array {}
136+
const sub = new MyU8(4);
137+
assert.strictEqual(converters.ArrayBufferView(sub), sub);
138+
});
139+
140+
test('ArrayBufferView accepts TypedArray with null prototype', () => {
141+
const ta = new Uint8Array(4);
142+
Object.setPrototypeOf(ta, null);
143+
assert.strictEqual(converters.ArrayBufferView(ta), ta);
144+
});
145+
146+
test('ArrayBufferView accepts DataView with null prototype', () => {
147+
const dv = new DataView(new ArrayBuffer(4));
148+
Object.setPrototypeOf(dv, null);
149+
assert.strictEqual(converters.ArrayBufferView(dv), dv);
150+
});
151+
152+
test('ArrayBufferView rejects raw ArrayBuffer', () => {
153+
assert.throws(
154+
() => converters.ArrayBufferView(new ArrayBuffer(4)),
155+
{ code: 'ERR_INVALID_ARG_TYPE' },
156+
);
157+
});
158+
159+
test('ArrayBufferView rejects raw SharedArrayBuffer', () => {
160+
assert.throws(
161+
() => converters.ArrayBufferView(new SharedArrayBuffer(4)),
162+
{ code: 'ERR_INVALID_ARG_TYPE' },
163+
);
164+
});
165+
166+
test('ArrayBufferView rejects SAB-backed TypedArray', () => {
167+
const view = new Uint8Array(new SharedArrayBuffer(4));
168+
assert.throws(
169+
() => converters.ArrayBufferView(view),
170+
{ code: 'ERR_INVALID_ARG_TYPE' },
171+
);
172+
});
173+
174+
test('ArrayBufferView rejects SAB-backed DataView', () => {
175+
const dv = new DataView(new SharedArrayBuffer(4));
176+
assert.throws(
177+
() => converters.ArrayBufferView(dv),
178+
{ code: 'ERR_INVALID_ARG_TYPE' },
179+
);
180+
});
181+
182+
test('ArrayBufferView rejects SAB view whose buffer prototype was reassigned', () => {
183+
const sab = new SharedArrayBuffer(4);
184+
Object.setPrototypeOf(sab, ArrayBuffer.prototype);
185+
const view = new Uint8Array(sab);
186+
assert.throws(
187+
() => converters.ArrayBufferView(view),
188+
{ code: 'ERR_INVALID_ARG_TYPE' },
189+
);
190+
});
191+
192+
test('ArrayBufferView rejects objects with a forged @@toStringTag', () => {
193+
const fake = { [Symbol.toStringTag]: 'Uint8Array' };
194+
assert.throws(
195+
() => converters.ArrayBufferView(fake),
196+
{ code: 'ERR_INVALID_ARG_TYPE' },
197+
);
198+
});
199+
200+
for (const value of [null, undefined, 0, 1, 1n, '', 'x', true, Symbol('s'), [],
201+
{}, () => {}]) {
202+
test(`ArrayBufferView rejects ${typeof value} ${String(value)}`, () => {
203+
assert.throws(
204+
() => converters.ArrayBufferView(value),
205+
{ code: 'ERR_INVALID_ARG_TYPE' },
206+
);
207+
});
208+
}

0 commit comments

Comments
 (0)