From 122294a2fb892e4f35e7371e85f383890fd179c4 Mon Sep 17 00:00:00 2001 From: Ali Hassan Date: Wed, 8 Apr 2026 14:37:47 +0500 Subject: [PATCH 1/2] lib: reject SharedArrayBuffer in web APIs per spec Signed-off-by: Ali Hassan --- lib/internal/blob.js | 16 ++ lib/internal/crypto/webidl.js | 26 +- lib/internal/webidl.js | 49 ++++ lib/internal/webstreams/compression.js | 10 +- lib/internal/webstreams/readablestream.js | 19 ++ ...test-webapi-sharedarraybuffer-rejection.js | 227 ++++++++++++++++++ .../test-webstreams-compression-bad-chunks.js | 2 +- 7 files changed, 324 insertions(+), 25 deletions(-) create mode 100644 test/parallel/test-webapi-sharedarraybuffer-rejection.js diff --git a/lib/internal/blob.js b/lib/internal/blob.js index e1b1dceabd629d..4b58c4df25ca32 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -45,6 +45,7 @@ const { const { isAnyArrayBuffer, isArrayBufferView, + isSharedArrayBuffer, } = require('internal/util/types'); const { @@ -109,6 +110,21 @@ function getSource(source, endings) { if (isBlob(source)) return [source.size, source[kHandle]]; + if (isSharedArrayBuffer(source)) { + throw new ERR_INVALID_ARG_TYPE( + 'source', + ['ArrayBuffer', 'TypedArray', 'DataView', 'Blob', 'string'], + source, + ); + } + if (isArrayBufferView(source) && isSharedArrayBuffer(source.buffer)) { + throw new ERR_INVALID_ARG_VALUE( + 'source', + source, + 'must not be backed by a SharedArrayBuffer', + ); + } + if (isAnyArrayBuffer(source)) { source = new Uint8Array(source); } else if (!isArrayBufferView(source)) { diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 4f371955a73bfd..0c14bd22d6a9ad 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -31,6 +31,7 @@ const { } = primordials; const { + converters: sharedConverters, makeException, createEnumConverter, createSequenceConverter, @@ -43,11 +44,10 @@ const { } = require('internal/util'); const { CryptoKey } = require('internal/crypto/webcrypto'); const { - getDataViewOrTypedArrayBuffer, validateMaxBufferLength, kNamedCurveAliases, } = require('internal/crypto/util'); -const { isArrayBuffer, isSharedArrayBuffer } = require('internal/util/types'); +const { isSharedArrayBuffer } = require('internal/util/types'); // https://tc39.es/ecma262/#sec-tonumber function toNumber(value, opts = kEmptyObject) { @@ -193,8 +193,6 @@ converters.object = (V, opts) => { return V; }; -const isNonSharedArrayBuffer = isArrayBuffer; - /** * @param {string | object} V - The hash algorithm identifier (string or object). * @param {string} label - The dictionary name for the error message. @@ -223,25 +221,7 @@ converters.Uint8Array = (V, opts = kEmptyObject) => { return V; }; -converters.BufferSource = (V, opts = kEmptyObject) => { - if (ArrayBufferIsView(V)) { - if (isSharedArrayBuffer(getDataViewOrTypedArrayBuffer(V))) { - throw makeException( - 'is a view on a SharedArrayBuffer, which is not allowed.', - opts); - } - - return V; - } - - if (!isNonSharedArrayBuffer(V)) { - throw makeException( - 'is not instance of ArrayBuffer, Buffer, TypedArray, or DataView.', - opts); - } - - return V; -}; +converters.BufferSource = sharedConverters.BufferSource; converters['sequence'] = createSequenceConverter( converters.DOMString); diff --git a/lib/internal/webidl.js b/lib/internal/webidl.js index 4af564dad752e6..d6bfea8e8479fa 100644 --- a/lib/internal/webidl.js +++ b/lib/internal/webidl.js @@ -1,8 +1,10 @@ 'use strict'; const { + ArrayBufferIsView, ArrayPrototypePush, ArrayPrototypeToSorted, + DataViewPrototypeGetBuffer, MathAbs, MathMax, MathMin, @@ -19,6 +21,7 @@ const { Symbol, SymbolIterator, TypeError, + TypedArrayPrototypeGetBuffer, } = primordials; const { @@ -28,6 +31,11 @@ const { }, } = require('internal/errors'); const { kEmptyObject } = require('internal/util'); +const { + isArrayBuffer, + isDataView, + isSharedArrayBuffer, +} = require('internal/util/types'); const converters = { __proto__: null }; @@ -382,6 +390,47 @@ function createInterfaceConverter(name, I) { }; } +function getDataViewOrTypedArrayBuffer(V) { + return isDataView(V) ? + DataViewPrototypeGetBuffer(V) : TypedArrayPrototypeGetBuffer(V); +} + +// https://webidl.spec.whatwg.org/#ArrayBufferView +converters.ArrayBufferView = (V, opts = kEmptyObject) => { + if (!ArrayBufferIsView(V)) { + throw makeException( + 'is not an ArrayBufferView.', + opts); + } + if (isSharedArrayBuffer(getDataViewOrTypedArrayBuffer(V))) { + throw makeException( + 'is a view on a SharedArrayBuffer, which is not allowed.', + opts); + } + + return V; +}; + +// https://webidl.spec.whatwg.org/#BufferSource +converters.BufferSource = (V, opts = kEmptyObject) => { + if (ArrayBufferIsView(V)) { + if (isSharedArrayBuffer(getDataViewOrTypedArrayBuffer(V))) { + throw makeException( + 'is a view on a SharedArrayBuffer, which is not allowed.', + opts); + } + + return V; + } + + if (!isArrayBuffer(V)) { + throw makeException( + 'is not instance of ArrayBuffer, Buffer, TypedArray, or DataView.', + opts); + } + + return V; +}; module.exports = { type, diff --git a/lib/internal/webstreams/compression.js b/lib/internal/webstreams/compression.js index 7ec929d94a0da0..a2be93027b635c 100644 --- a/lib/internal/webstreams/compression.js +++ b/lib/internal/webstreams/compression.js @@ -26,6 +26,7 @@ const { const { codes: { ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, }, } = require('internal/errors'); @@ -40,13 +41,20 @@ function lazyZlib() { // Per the Compression Streams spec, chunks must be BufferSource // (ArrayBuffer or ArrayBufferView not backed by SharedArrayBuffer). function validateBufferSourceChunk(chunk) { - if (isArrayBufferView(chunk) && isSharedArrayBuffer(chunk.buffer)) { + if (isSharedArrayBuffer(chunk)) { throw new ERR_INVALID_ARG_TYPE( 'chunk', ['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'], chunk, ); } + if (isArrayBufferView(chunk) && isSharedArrayBuffer(chunk.buffer)) { + throw new ERR_INVALID_ARG_VALUE( + 'chunk', + chunk, + 'must not be backed by a SharedArrayBuffer', + ); + } } const formatConverter = createEnumConverter('CompressionFormat', [ diff --git a/lib/internal/webstreams/readablestream.js b/lib/internal/webstreams/readablestream.js index 94592cdf01258c..e72b4b2f11d6c6 100644 --- a/lib/internal/webstreams/readablestream.js +++ b/lib/internal/webstreams/readablestream.js @@ -48,6 +48,7 @@ const { const { isArrayBufferView, isDataView, + isSharedArrayBuffer, } = require('internal/util/types'); const { @@ -988,6 +989,15 @@ class ReadableStreamBYOBReader { const viewByteLength = ArrayBufferViewGetByteLength(view); const viewBuffer = ArrayBufferViewGetBuffer(view); + + if (isSharedArrayBuffer(viewBuffer)) { + throw new ERR_INVALID_ARG_VALUE( + 'view', + view, + 'must not be backed by a SharedArrayBuffer', + ); + } + const viewBufferByteLength = ArrayBufferPrototypeGetByteLength(viewBuffer); if (viewByteLength === 0 || viewBufferByteLength === 0) { @@ -1197,6 +1207,15 @@ class ReadableByteStreamController { validateBuffer(chunk); const chunkByteLength = ArrayBufferViewGetByteLength(chunk); const chunkBuffer = ArrayBufferViewGetBuffer(chunk); + + if (isSharedArrayBuffer(chunkBuffer)) { + throw new ERR_INVALID_ARG_VALUE( + 'chunk', + chunk, + 'must not be backed by a SharedArrayBuffer', + ); + } + const chunkBufferByteLength = ArrayBufferPrototypeGetByteLength(chunkBuffer); if (chunkByteLength === 0 || chunkBufferByteLength === 0) { throw new ERR_INVALID_STATE.TypeError( diff --git a/test/parallel/test-webapi-sharedarraybuffer-rejection.js b/test/parallel/test-webapi-sharedarraybuffer-rejection.js new file mode 100644 index 00000000000000..ef80ca062476dd --- /dev/null +++ b/test/parallel/test-webapi-sharedarraybuffer-rejection.js @@ -0,0 +1,227 @@ +'use strict'; +// Flags: --expose-internals +const common = require('../common'); +const assert = require('assert'); +const test = require('node:test'); +const { Blob } = require('buffer'); +const { ReadableStream, CompressionStream, DecompressionStream } = require('stream/web'); + +const sab = new SharedArrayBuffer(8); +const sabView = new Uint8Array(sab); +const sabDataView = new DataView(sab); + +// -- Blob constructor -- + +test('Blob rejects raw SharedArrayBuffer', () => { + assert.throws( + () => new Blob([sab]), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +}); + +test('Blob rejects Uint8Array backed by SharedArrayBuffer', () => { + assert.throws( + () => new Blob([sabView]), + { code: 'ERR_INVALID_ARG_VALUE' }, + ); +}); + +test('Blob rejects DataView backed by SharedArrayBuffer', () => { + assert.throws( + () => new Blob([sabDataView]), + { code: 'ERR_INVALID_ARG_VALUE' }, + ); +}); + +test('Blob still accepts regular ArrayBuffer', () => { + const blob = new Blob([new ArrayBuffer(4)]); + assert.strictEqual(blob.size, 4); +}); + +test('Blob still accepts regular Uint8Array', () => { + const blob = new Blob([new Uint8Array(4)]); + assert.strictEqual(blob.size, 4); +}); + +// -- ReadableStreamBYOBReader.read() -- + +test('ReadableStreamBYOBReader.read() rejects SAB-backed Uint8Array', async () => { + const rs = new ReadableStream({ + type: 'bytes', + pull(controller) { + controller.enqueue(new Uint8Array([1, 2, 3])); + }, + }); + const reader = rs.getReader({ mode: 'byob' }); + await assert.rejects( + reader.read(new Uint8Array(sab)), + { code: 'ERR_INVALID_ARG_VALUE' }, + ); + reader.releaseLock(); +}); + +test('ReadableStreamBYOBReader.read() rejects SAB-backed DataView', async () => { + const rs = new ReadableStream({ + type: 'bytes', + pull(controller) { + controller.enqueue(new Uint8Array([1, 2, 3])); + }, + }); + const reader = rs.getReader({ mode: 'byob' }); + await assert.rejects( + reader.read(sabDataView), + { code: 'ERR_INVALID_ARG_VALUE' }, + ); + reader.releaseLock(); +}); + +test('ReadableStreamBYOBReader.read() accepts regular view', async () => { + const rs = new ReadableStream({ + type: 'bytes', + pull(controller) { + controller.enqueue(new Uint8Array([1, 2, 3])); + }, + }); + const reader = rs.getReader({ mode: 'byob' }); + const { value, done } = await reader.read(new Uint8Array(3)); + assert.strictEqual(done, false); + assert.deepStrictEqual(value, new Uint8Array([1, 2, 3])); + reader.releaseLock(); +}); + +// -- ReadableByteStreamController.enqueue() -- + +test('ReadableByteStreamController.enqueue() rejects SAB-backed Uint8Array', async () => { + const sabForEnqueue = new SharedArrayBuffer(4); + const sabViewForEnqueue = new Uint8Array(sabForEnqueue); + sabViewForEnqueue[0] = 42; + + const rs = new ReadableStream({ + type: 'bytes', + pull: common.mustCall((controller) => { + assert.throws( + () => controller.enqueue(sabViewForEnqueue), + { code: 'ERR_INVALID_ARG_VALUE' }, + ); + controller.enqueue(new Uint8Array([1])); + }), + }); + const reader = rs.getReader(); + const { value } = await reader.read(); + assert.deepStrictEqual(value, new Uint8Array([1])); + reader.releaseLock(); +}); + +test('ReadableByteStreamController.enqueue() rejects SAB-backed DataView', async () => { + const sabForDv = new SharedArrayBuffer(4); + const dvForEnqueue = new DataView(sabForDv); + + const rs = new ReadableStream({ + type: 'bytes', + pull: common.mustCall((controller) => { + assert.throws( + () => controller.enqueue(dvForEnqueue), + { code: 'ERR_INVALID_ARG_VALUE' }, + ); + controller.enqueue(new Uint8Array([2])); + }), + }); + const reader = rs.getReader(); + const { value } = await reader.read(); + assert.deepStrictEqual(value, new Uint8Array([2])); + reader.releaseLock(); +}); + +// -- Compression/Decompression streams reject raw SharedArrayBuffer -- + +for (const format of ['deflate', 'gzip', 'deflate-raw', 'brotli']) { + test(`CompressionStream rejects raw SharedArrayBuffer for ${format}`, async () => { + const cs = new CompressionStream(format); + const writer = cs.writable.getWriter(); + const reader = cs.readable.getReader(); + + const writePromise = writer.write(sab); + const readPromise = reader.read(); + + await assert.rejects(writePromise, { code: 'ERR_INVALID_ARG_TYPE' }); + await assert.rejects(readPromise, { code: 'ERR_INVALID_ARG_TYPE' }); + }); + + test(`DecompressionStream rejects raw SharedArrayBuffer for ${format}`, async () => { + const ds = new DecompressionStream(format); + const writer = ds.writable.getWriter(); + const reader = ds.readable.getReader(); + + const writePromise = writer.write(sab); + const readPromise = reader.read(); + + await assert.rejects(writePromise, { code: 'ERR_INVALID_ARG_TYPE' }); + await assert.rejects(readPromise, { code: 'ERR_INVALID_ARG_TYPE' }); + }); +} + +// -- SharedWebIDL converters -- + +const { converters } = require('internal/webidl'); + +test('webidl converters.BufferSource rejects SharedArrayBuffer', () => { + assert.throws( + () => converters.BufferSource(sab), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +}); + +test('webidl converters.BufferSource rejects SAB-backed Uint8Array', () => { + assert.throws( + () => converters.BufferSource(sabView), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +}); + +test('webidl converters.BufferSource rejects SAB-backed DataView', () => { + assert.throws( + () => converters.BufferSource(sabDataView), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +}); + +test('webidl converters.BufferSource accepts ArrayBuffer', () => { + const ab = new ArrayBuffer(4); + assert.strictEqual(converters.BufferSource(ab), ab); +}); + +test('webidl converters.BufferSource accepts regular TypedArray', () => { + const ta = new Uint8Array(4); + assert.strictEqual(converters.BufferSource(ta), ta); +}); + +test('webidl converters.ArrayBufferView rejects SAB-backed Uint8Array', () => { + assert.throws( + () => converters.ArrayBufferView(sabView), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +}); + +test('webidl converters.ArrayBufferView rejects SAB-backed DataView', () => { + assert.throws( + () => converters.ArrayBufferView(sabDataView), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +}); + +test('webidl converters.ArrayBufferView rejects non-view', () => { + assert.throws( + () => converters.ArrayBufferView('not a view'), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +}); + +test('webidl converters.ArrayBufferView accepts regular Uint8Array', () => { + const ta = new Uint8Array(4); + assert.strictEqual(converters.ArrayBufferView(ta), ta); +}); + +test('webidl converters.ArrayBufferView accepts regular DataView', () => { + const dv = new DataView(new ArrayBuffer(4)); + assert.strictEqual(converters.ArrayBufferView(dv), dv); +}); diff --git a/test/parallel/test-webstreams-compression-bad-chunks.js b/test/parallel/test-webstreams-compression-bad-chunks.js index 4a8ca3cff8a27f..ce0f8d75b19918 100644 --- a/test/parallel/test-webstreams-compression-bad-chunks.js +++ b/test/parallel/test-webstreams-compression-bad-chunks.js @@ -22,7 +22,7 @@ const badChunks = [ { name: 'Uint8Array backed by SharedArrayBuffer', value: new Uint8Array(new SharedArrayBuffer(1)), - code: 'ERR_INVALID_ARG_TYPE', + code: 'ERR_INVALID_ARG_VALUE', }, ]; From cec023c771b23c375e952a7360151f90d46f7ff9 Mon Sep 17 00:00:00 2001 From: Ali Hassan Date: Wed, 8 Apr 2026 15:56:11 +0500 Subject: [PATCH 2/2] lib: revert compression changes and drop semver-major changes Signed-off-by: Ali Hassan --- lib/internal/blob.js | 16 ----- lib/internal/webstreams/compression.js | 10 +-- ...test-webapi-sharedarraybuffer-rejection.js | 64 +------------------ .../test-webstreams-compression-bad-chunks.js | 2 +- 4 files changed, 3 insertions(+), 89 deletions(-) diff --git a/lib/internal/blob.js b/lib/internal/blob.js index 4b58c4df25ca32..e1b1dceabd629d 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -45,7 +45,6 @@ const { const { isAnyArrayBuffer, isArrayBufferView, - isSharedArrayBuffer, } = require('internal/util/types'); const { @@ -110,21 +109,6 @@ function getSource(source, endings) { if (isBlob(source)) return [source.size, source[kHandle]]; - if (isSharedArrayBuffer(source)) { - throw new ERR_INVALID_ARG_TYPE( - 'source', - ['ArrayBuffer', 'TypedArray', 'DataView', 'Blob', 'string'], - source, - ); - } - if (isArrayBufferView(source) && isSharedArrayBuffer(source.buffer)) { - throw new ERR_INVALID_ARG_VALUE( - 'source', - source, - 'must not be backed by a SharedArrayBuffer', - ); - } - if (isAnyArrayBuffer(source)) { source = new Uint8Array(source); } else if (!isArrayBufferView(source)) { diff --git a/lib/internal/webstreams/compression.js b/lib/internal/webstreams/compression.js index a2be93027b635c..7ec929d94a0da0 100644 --- a/lib/internal/webstreams/compression.js +++ b/lib/internal/webstreams/compression.js @@ -26,7 +26,6 @@ const { const { codes: { ERR_INVALID_ARG_TYPE, - ERR_INVALID_ARG_VALUE, }, } = require('internal/errors'); @@ -41,20 +40,13 @@ function lazyZlib() { // Per the Compression Streams spec, chunks must be BufferSource // (ArrayBuffer or ArrayBufferView not backed by SharedArrayBuffer). function validateBufferSourceChunk(chunk) { - if (isSharedArrayBuffer(chunk)) { + if (isArrayBufferView(chunk) && isSharedArrayBuffer(chunk.buffer)) { throw new ERR_INVALID_ARG_TYPE( 'chunk', ['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'], chunk, ); } - if (isArrayBufferView(chunk) && isSharedArrayBuffer(chunk.buffer)) { - throw new ERR_INVALID_ARG_VALUE( - 'chunk', - chunk, - 'must not be backed by a SharedArrayBuffer', - ); - } } const formatConverter = createEnumConverter('CompressionFormat', [ diff --git a/test/parallel/test-webapi-sharedarraybuffer-rejection.js b/test/parallel/test-webapi-sharedarraybuffer-rejection.js index ef80ca062476dd..c5503dfc0a1b2d 100644 --- a/test/parallel/test-webapi-sharedarraybuffer-rejection.js +++ b/test/parallel/test-webapi-sharedarraybuffer-rejection.js @@ -3,46 +3,12 @@ const common = require('../common'); const assert = require('assert'); const test = require('node:test'); -const { Blob } = require('buffer'); -const { ReadableStream, CompressionStream, DecompressionStream } = require('stream/web'); +const { ReadableStream } = require('stream/web'); const sab = new SharedArrayBuffer(8); const sabView = new Uint8Array(sab); const sabDataView = new DataView(sab); -// -- Blob constructor -- - -test('Blob rejects raw SharedArrayBuffer', () => { - assert.throws( - () => new Blob([sab]), - { code: 'ERR_INVALID_ARG_TYPE' }, - ); -}); - -test('Blob rejects Uint8Array backed by SharedArrayBuffer', () => { - assert.throws( - () => new Blob([sabView]), - { code: 'ERR_INVALID_ARG_VALUE' }, - ); -}); - -test('Blob rejects DataView backed by SharedArrayBuffer', () => { - assert.throws( - () => new Blob([sabDataView]), - { code: 'ERR_INVALID_ARG_VALUE' }, - ); -}); - -test('Blob still accepts regular ArrayBuffer', () => { - const blob = new Blob([new ArrayBuffer(4)]); - assert.strictEqual(blob.size, 4); -}); - -test('Blob still accepts regular Uint8Array', () => { - const blob = new Blob([new Uint8Array(4)]); - assert.strictEqual(blob.size, 4); -}); - // -- ReadableStreamBYOBReader.read() -- test('ReadableStreamBYOBReader.read() rejects SAB-backed Uint8Array', async () => { @@ -132,34 +98,6 @@ test('ReadableByteStreamController.enqueue() rejects SAB-backed DataView', async reader.releaseLock(); }); -// -- Compression/Decompression streams reject raw SharedArrayBuffer -- - -for (const format of ['deflate', 'gzip', 'deflate-raw', 'brotli']) { - test(`CompressionStream rejects raw SharedArrayBuffer for ${format}`, async () => { - const cs = new CompressionStream(format); - const writer = cs.writable.getWriter(); - const reader = cs.readable.getReader(); - - const writePromise = writer.write(sab); - const readPromise = reader.read(); - - await assert.rejects(writePromise, { code: 'ERR_INVALID_ARG_TYPE' }); - await assert.rejects(readPromise, { code: 'ERR_INVALID_ARG_TYPE' }); - }); - - test(`DecompressionStream rejects raw SharedArrayBuffer for ${format}`, async () => { - const ds = new DecompressionStream(format); - const writer = ds.writable.getWriter(); - const reader = ds.readable.getReader(); - - const writePromise = writer.write(sab); - const readPromise = reader.read(); - - await assert.rejects(writePromise, { code: 'ERR_INVALID_ARG_TYPE' }); - await assert.rejects(readPromise, { code: 'ERR_INVALID_ARG_TYPE' }); - }); -} - // -- SharedWebIDL converters -- const { converters } = require('internal/webidl'); diff --git a/test/parallel/test-webstreams-compression-bad-chunks.js b/test/parallel/test-webstreams-compression-bad-chunks.js index ce0f8d75b19918..4a8ca3cff8a27f 100644 --- a/test/parallel/test-webstreams-compression-bad-chunks.js +++ b/test/parallel/test-webstreams-compression-bad-chunks.js @@ -22,7 +22,7 @@ const badChunks = [ { name: 'Uint8Array backed by SharedArrayBuffer', value: new Uint8Array(new SharedArrayBuffer(1)), - code: 'ERR_INVALID_ARG_VALUE', + code: 'ERR_INVALID_ARG_TYPE', }, ];