Skip to content

Commit 9a9246a

Browse files
jasnelladuh95
authored andcommitted
stream: defend against re-entrancy in writev
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode PR-URL: #65652 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 2b5e062 commit 9a9246a

4 files changed

Lines changed: 85 additions & 24 deletions

File tree

lib/internal/streams/iter/broadcast.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -610,9 +610,11 @@ class BroadcastWriter {
610610
if (this.#canUseWriteFastPath(signal)) {
611611
const converted = convertChunks(chunks);
612612
const batch = createBatchEntry(converted);
613-
this.#broadcast[kWrite](batch);
614-
this.#totalBytes += batch.byteLength;
615-
return kResolvedPromise;
613+
if (this.#state === 'open' && this.#broadcast[kWrite](batch)) {
614+
this.#totalBytes += batch.byteLength;
615+
return kResolvedPromise;
616+
}
617+
return this.#writeBatchSlow(batch, signal);
616618
}
617619
return this.#writevSlow(chunks, signal);
618620
}
@@ -629,6 +631,19 @@ class BroadcastWriter {
629631

630632
const batch = createBatchEntry(convertChunks(chunks));
631633

634+
return this.#writeBatchSlow(batch, signal);
635+
}
636+
637+
async #writeBatchSlow(batch, signal) {
638+
if (this.#state === 'errored') {
639+
throw this.#error;
640+
}
641+
if (this.#state !== 'open') {
642+
throw new ERR_INVALID_STATE.TypeError('Writer is closed');
643+
}
644+
645+
signal?.throwIfAborted();
646+
632647
if (this.#broadcast[kWrite](batch)) {
633648
this.#totalBytes += batch.byteLength;
634649
return;

lib/internal/streams/iter/push.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -657,12 +657,10 @@ class PushWriter {
657657
writev(chunks, options) {
658658
validateArray(chunks, 'chunks');
659659
const signal = getWriterSignal(options);
660-
if (!signal && this.#queue.canWriteSync()) {
661-
const bytes = convertChunks(chunks);
662-
this.#queue.writeSync(bytes);
660+
const bytes = convertChunks(chunks);
661+
if (!signal && this.#queue.writeSync(bytes)) {
663662
return kResolvedPromise;
664663
}
665-
const bytes = convertChunks(chunks);
666664
return this.#queue.writeAsync(bytes, signal);
667665
}
668666

lib/internal/streams/iter/utils.js

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -185,19 +185,6 @@ function toUint8Array(chunk) {
185185
return chunk;
186186
}
187187

188-
/**
189-
* Check if all chunks in an array are already Uint8Array.
190-
* Short-circuits on the first non-Uint8Array chunk found.
191-
* @param {Array<Uint8Array|string>} chunks
192-
* @returns {boolean}
193-
*/
194-
function allUint8Array(chunks) {
195-
for (let i = 0; i < chunks.length; i++) {
196-
if (!isUint8Array(chunks[i])) return false;
197-
}
198-
return true;
199-
}
200-
201188
function snapshotByteView(value) {
202189
const buffer = TypedArrayPrototypeGetBuffer(value);
203190
const sharedBufferView = isSharedArrayBuffer(buffer) ?
@@ -317,9 +304,6 @@ function concatBytes(chunks) {
317304
* @returns {Uint8Array[]}
318305
*/
319306
function convertChunks(chunks) {
320-
if (allUint8Array(chunks)) {
321-
return ArrayPrototypeSlice(chunks);
322-
}
323307
const len = chunks.length;
324308
const result = new Array(len);
325309
for (let i = 0; i < len; i++) {
@@ -446,7 +430,6 @@ module.exports = {
446430
kMultiConsumerDefaultBudget,
447431
kPushDefaultBudget,
448432
kResolvedPromise,
449-
allUint8Array,
450433
concatBytes,
451434
convertChunks,
452435
createBatchEntry,
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Flags: --experimental-stream-iter
2+
'use strict';
3+
4+
const common = require('../common');
5+
const assert = require('assert');
6+
const { broadcast, push } = require('stream/iter');
7+
8+
const factories = [
9+
() => push({ budget: 16384 }),
10+
() => {
11+
const { writer, broadcast: bc } = broadcast({ budget: 16384 });
12+
return { __proto__: null, writer, readable: bc.push() };
13+
},
14+
];
15+
16+
async function testWritevReentrancy() {
17+
for (const factory of factories) {
18+
const { writer, readable } = factory();
19+
const chunks = [];
20+
Object.defineProperty(chunks, 0, {
21+
__proto__: null,
22+
enumerable: true,
23+
get: common.mustCall(() => {
24+
assert.strictEqual(
25+
writer.writeSync(new Uint8Array(16384)), true);
26+
return Uint8Array.of(42);
27+
}),
28+
});
29+
30+
let resolved = false;
31+
const write = writer.writev(chunks).then(common.mustCall(() => {
32+
resolved = true;
33+
}));
34+
await new Promise(setImmediate);
35+
assert.strictEqual(resolved, false);
36+
37+
const iterator = readable[Symbol.asyncIterator]();
38+
assert.strictEqual((await iterator.next()).value[0].byteLength, 16384);
39+
await write;
40+
assert.strictEqual((await iterator.next()).value[0][0], 42);
41+
writer.endSync();
42+
assert.strictEqual((await iterator.next()).done, true);
43+
}
44+
45+
for (const factory of factories) {
46+
const { writer, readable } = factory();
47+
const chunks = [];
48+
Object.defineProperty(chunks, 0, {
49+
__proto__: null,
50+
enumerable: true,
51+
get: common.mustCall(() => {
52+
writer.endSync();
53+
return Uint8Array.of(42);
54+
}),
55+
});
56+
57+
await assert.rejects(writer.writev(chunks), {
58+
code: 'ERR_INVALID_STATE',
59+
});
60+
assert.strictEqual(
61+
(await readable[Symbol.asyncIterator]().next()).done, true);
62+
}
63+
}
64+
65+
testWritevReentrancy().then(common.mustCall());

0 commit comments

Comments
 (0)