Skip to content

Commit 1de47f0

Browse files
Commit pull-into descriptors after filling from queue
In https://issues.chromium.org/issues/339877167, it was discovered that a user could run JavaScript code *synchronously* during ReadableStreamFulfillReadIntoRequest by patching Object.prototype.then, and use this gadget to break some invariants within ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue. To prevent this, we now postpone all calls to ReadableByteStreamControllerCommitPullIntoDescriptor until *after* all pull-into descriptors have been filled up by ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue. This way, we won't trigger any patched then() method until the stream is in a stable state. Fixes GHSA-p5g2-876g-95h9.
1 parent 86d07e9 commit 1de47f0

File tree

4 files changed

+106
-23
lines changed

4 files changed

+106
-23
lines changed

index.bs

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3348,7 +3348,12 @@ The following abstract operations support the implementation of the
33483348
1. Otherwise, if ! [$ReadableStreamHasBYOBReader$](|stream|) is true,
33493349
1. Perform ! [$ReadableByteStreamControllerEnqueueChunkToQueue$](|controller|,
33503350
|transferredBuffer|, |byteOffset|, |byteLength|).
3351-
1. Perform ! [$ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue$](|controller|).
3351+
1. Let |filledPullIntos| be the result of performing
3352+
! [$ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue$](|controller|).
3353+
1. [=list/For each=] |filledPullInto| of |filledPullIntos|,
3354+
1. Perform !
3355+
[$ReadableByteStreamControllerCommitPullIntoDescriptor$](|controller|.[=ReadableByteStreamController/[[stream]]=],
3356+
|filledPullInto|).
33523357
1. Otherwise,
33533358
1. Assert: ! [$IsReadableStreamLocked$](|stream|) is false.
33543359
1. Perform ! [$ReadableByteStreamControllerEnqueueChunkToQueue$](|controller|,
@@ -3433,6 +3438,7 @@ The following abstract operations support the implementation of the
34333438
|maxBytesToCopy|.
34343439
1. Let |totalBytesToCopyRemaining| be |maxBytesToCopy|.
34353440
1. Let |ready| be false.
3441+
1. Assert: ! [$IsDetachedBuffer$](|pullIntoDescriptor|'s [=pull-into descriptor/buffer=]) is false.
34363442
1. Assert: |pullIntoDescriptor|'s [=pull-into descriptor/bytes filled=] < |pullIntoDescriptor|'s
34373443
[=pull-into descriptor/minimum fill=].
34383444
1. Let |remainderBytes| be the remainder after dividing |maxBytesFilled| by |pullIntoDescriptor|'s
@@ -3452,6 +3458,14 @@ The following abstract operations support the implementation of the
34523458
queue entry/byte length=]).
34533459
1. Let |destStart| be |pullIntoDescriptor|'s [=pull-into descriptor/byte offset=] +
34543460
|pullIntoDescriptor|'s [=pull-into descriptor/bytes filled=].
3461+
1. Assert: ! [$CanCopyDataBlockBytes$](|pullIntoDescriptor|'s [=pull-into descriptor/buffer=],
3462+
|destStart|, |headOfQueue|'s [=readable byte stream queue entry/buffer=],
3463+
|headOfQueue|'s [=readable byte stream queue entry/byte offset=], |bytesToCopy|) is true.
3464+
<p class="warning">If this assertion were to fail (due to a bug in this specification or
3465+
its implementation), then the next step may read from or write to potentially invalid memory.
3466+
The user agent should always check this assertion, and stop in an [=implementation-defined=]
3467+
manner if it fails (e.g. by crashing the process, or by
3468+
<a abstract-op lt="ReadableByteStreamControllerError">erroring the stream</a>).
34553469
1. Perform ! [$CopyDataBlockBytes$](|pullIntoDescriptor|'s [=pull-into
34563470
descriptor/buffer=].\[[ArrayBufferData]], |destStart|,
34573471
|headOfQueue|'s [=readable byte stream queue entry/buffer=].\[[ArrayBufferData]],
@@ -3561,17 +3575,17 @@ The following abstract operations support the implementation of the
35613575
performs the following steps:
35623576

35633577
1. Assert: |controller|.[=ReadableByteStreamController/[[closeRequested]]=] is false.
3578+
1. Let |filledPullIntos| be a new empty [=list=].
35643579
1. [=While=] |controller|.[=ReadableByteStreamController/[[pendingPullIntos]]=] is not
35653580
[=list/is empty|empty=],
3566-
1. If |controller|.[=ReadableByteStreamController/[[queueTotalSize]]=] is 0, return.
3581+
1. If |controller|.[=ReadableByteStreamController/[[queueTotalSize]]=] is 0, then [=break=].
35673582
1. Let |pullIntoDescriptor| be
35683583
|controller|.[=ReadableByteStreamController/[[pendingPullIntos]]=][0].
35693584
1. If ! [$ReadableByteStreamControllerFillPullIntoDescriptorFromQueue$](|controller|,
35703585
|pullIntoDescriptor|) is true,
35713586
1. Perform ! [$ReadableByteStreamControllerShiftPendingPullInto$](|controller|).
3572-
1. Perform !
3573-
[$ReadableByteStreamControllerCommitPullIntoDescriptor$](|controller|.[=ReadableByteStreamController/[[stream]]=],
3574-
|pullIntoDescriptor|).
3587+
1. [=list/Append=] |pullIntoDescriptor| to |filledPullIntos|.
3588+
1. Return |filledPullIntos|.
35753589
</div>
35763590

35773591
<div algorithm>
@@ -3701,11 +3715,16 @@ The following abstract operations support the implementation of the
37013715
perform ! [$ReadableByteStreamControllerShiftPendingPullInto$](|controller|).
37023716
1. Let |stream| be |controller|.[=ReadableByteStreamController/[[stream]]=].
37033717
1. If ! [$ReadableStreamHasBYOBReader$](|stream|) is true,
3704-
1. [=While=] ! [$ReadableStreamGetNumReadIntoRequests$](|stream|) > 0,
3718+
1. Let |filledPullIntos| be a new empty [=list=].
3719+
1. Let |i| be 0.
3720+
1. [=While=] |i| < ! [$ReadableStreamGetNumReadIntoRequests$](|stream|),
37053721
1. Let |pullIntoDescriptor| be !
37063722
[$ReadableByteStreamControllerShiftPendingPullInto$](|controller|).
3723+
1. [=list/Append=] |pullIntoDescriptor| to |filledPullIntos|.
3724+
1. Set |i| to |i| + 1.
3725+
1. [=list/For each=] |filledPullInto| of |filledPullIntos|,
37073726
1. Perform ! [$ReadableByteStreamControllerCommitPullIntoDescriptor$](|stream|,
3708-
|pullIntoDescriptor|).
3727+
|filledPullInto|).
37093728
</div>
37103729

37113730
<div algorithm>
@@ -3720,7 +3739,12 @@ The following abstract operations support the implementation of the
37203739
1. If |pullIntoDescriptor|'s [=pull-into descriptor/reader type=] is "`none`",
37213740
1. Perform ? [$ReadableByteStreamControllerEnqueueDetachedPullIntoToQueue$](|controller|,
37223741
|pullIntoDescriptor|).
3723-
1. Perform ! [$ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue$](|controller|).
3742+
1. Let |filledPullIntos| be the result of performing
3743+
! [$ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue$](|controller|).
3744+
1. [=list/For each=] |filledPullInto| of |filledPullIntos|,
3745+
1. Perform !
3746+
[$ReadableByteStreamControllerCommitPullIntoDescriptor$](|controller|.[=ReadableByteStreamController/[[stream]]=],
3747+
|filledPullInto|).
37243748
1. Return.
37253749
1. If |pullIntoDescriptor|'s [=pull-into descriptor/bytes filled=] &lt; |pullIntoDescriptor|'s
37263750
[=pull-into descriptor/minimum fill=], return.
@@ -3738,10 +3762,15 @@ The following abstract operations support the implementation of the
37383762
|remainderSize|).
37393763
1. Set |pullIntoDescriptor|'s [=pull-into descriptor/bytes filled=] to |pullIntoDescriptor|'s
37403764
[=pull-into descriptor/bytes filled=] − |remainderSize|.
3765+
1. Let |filledPullIntos| be the result of performing
3766+
! [$ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue$](|controller|).
37413767
1. Perform !
37423768
[$ReadableByteStreamControllerCommitPullIntoDescriptor$](|controller|.[=ReadableByteStreamController/[[stream]]=],
37433769
|pullIntoDescriptor|).
3744-
1. Perform ! [$ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue$](|controller|).
3770+
1. [=list/For each=] |filledPullInto| of |filledPullIntos|,
3771+
1. Perform !
3772+
[$ReadableByteStreamControllerCommitPullIntoDescriptor$](|controller|.[=ReadableByteStreamController/[[stream]]=],
3773+
|filledPullInto|).
37453774
</div>
37463775

37473776
<div algorithm>
@@ -6848,6 +6877,22 @@ The following abstract operations are a grab-bag of utilities.
68486877
1. Return ? [$StructuredDeserialize$](|serialized|, [=the current Realm=]).
68496878
</div>
68506879

6880+
<div algorithm>
6881+
<dfn abstract-op lt="CanCopyDataBlockBytes">CanCopyDataBlockBytes(|toBuffer|, |toIndex|,
6882+
|fromBuffer|, |fromIndex|, |count|)</dfn> performs the following steps:
6883+
6884+
1. Assert: |toBuffer| [=is an Object=].
6885+
1. Assert: |toBuffer| has an \[[ArrayBufferData]] internal slot.
6886+
1. Assert: |fromBuffer| [=is an Object=].
6887+
1. Assert: |fromBuffer| has an \[[ArrayBufferData]] internal slot.
6888+
1. If |toBuffer| is |fromBuffer|, return false.
6889+
1. If ! [$IsDetachedBuffer$](|toBuffer|) is true, return false.
6890+
1. If ! [$IsDetachedBuffer$](|fromBuffer|) is true, return false.
6891+
1. If |toIndex| + |count| > |toBuffer|.\[[ArrayBufferByteLength]], return false.
6892+
1. If |fromIndex| + |count| > |fromBuffer|.\[[ArrayBufferByteLength]], return false.
6893+
1. Return true.
6894+
</div>
6895+
68516896
<h2 id="other-specs">Using streams in other specifications</h2>
68526897

68536898
Much of this standard concerns itself with the internal machinery of streams. Other specifications

reference-implementation/lib/abstract-ops/miscellaneous.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use strict';
2+
const { IsDetachedBuffer } = require('./ecmascript');
23

34
exports.IsNonNegativeNumber = v => {
45
if (typeof v !== 'number') {
@@ -20,3 +21,22 @@ exports.CloneAsUint8Array = O => {
2021
const buffer = O.buffer.slice(O.byteOffset, O.byteOffset + O.byteLength);
2122
return new Uint8Array(buffer);
2223
};
24+
25+
exports.CanCopyDataBlockBytes = (toBuffer, toIndex, fromBuffer, fromIndex, count) => {
26+
if (toBuffer === fromBuffer) {
27+
return false;
28+
}
29+
if (IsDetachedBuffer(toBuffer) === true) {
30+
return false;
31+
}
32+
if (IsDetachedBuffer(fromBuffer) === true) {
33+
return false;
34+
}
35+
if (toIndex + count > toBuffer.byteLength) {
36+
return false;
37+
}
38+
if (fromIndex + count > fromBuffer.byteLength) {
39+
return false;
40+
}
41+
return true;
42+
};

reference-implementation/lib/abstract-ops/readable-streams.js

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const { promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, re
66
require('../helpers/webidl.js');
77
const { CanTransferArrayBuffer, Call, CopyDataBlockBytes, CreateArrayFromList, GetIterator, GetMethod, IsDetachedBuffer,
88
IteratorComplete, IteratorNext, IteratorValue, TransferArrayBuffer, typeIsObject } = require('./ecmascript.js');
9-
const { CloneAsUint8Array, IsNonNegativeNumber } = require('./miscellaneous.js');
9+
const { CanCopyDataBlockBytes, CloneAsUint8Array, IsNonNegativeNumber } = require('./miscellaneous.js');
1010
const { EnqueueValueWithSize, ResetQueue } = require('./queue-with-sizes.js');
1111
const { AcquireWritableStreamDefaultWriter, IsWritableStreamLocked, WritableStreamAbort,
1212
WritableStreamDefaultWriterCloseWithErrorPropagation, WritableStreamDefaultWriterRelease,
@@ -1360,7 +1360,10 @@ function ReadableByteStreamControllerEnqueue(controller, chunk) {
13601360
} else if (ReadableStreamHasBYOBReader(stream) === true) {
13611361
// TODO: Ideally in this branch detaching should happen only if the buffer is not consumed fully.
13621362
ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength);
1363-
ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller);
1363+
const filledPullIntos = ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller);
1364+
for (const filledPullInto of filledPullIntos) {
1365+
ReadableByteStreamControllerCommitPullIntoDescriptor(controller._stream, filledPullInto);
1366+
}
13641367
} else {
13651368
assert(IsReadableStreamLocked(stream) === false);
13661369
ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength);
@@ -1425,6 +1428,7 @@ function ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller,
14251428

14261429
let totalBytesToCopyRemaining = maxBytesToCopy;
14271430
let ready = false;
1431+
assert(!IsDetachedBuffer(pullIntoDescriptor.buffer));
14281432
assert(pullIntoDescriptor.bytesFilled < pullIntoDescriptor.minimumFill);
14291433
const remainderBytes = maxBytesFilled % pullIntoDescriptor.elementSize;
14301434
const maxAlignedBytes = maxBytesFilled - remainderBytes;
@@ -1443,6 +1447,9 @@ function ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller,
14431447
const bytesToCopy = Math.min(totalBytesToCopyRemaining, headOfQueue.byteLength);
14441448

14451449
const destStart = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled;
1450+
1451+
assert(CanCopyDataBlockBytes(pullIntoDescriptor.buffer, destStart, headOfQueue.buffer, headOfQueue.byteOffset,
1452+
bytesToCopy));
14461453
CopyDataBlockBytes(pullIntoDescriptor.buffer, destStart, headOfQueue.buffer, headOfQueue.byteOffset, bytesToCopy);
14471454

14481455
if (headOfQueue.byteLength === bytesToCopy) {
@@ -1532,23 +1539,22 @@ function ReadableByteStreamControllerInvalidateBYOBRequest(controller) {
15321539
function ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller) {
15331540
assert(controller._closeRequested === false);
15341541

1542+
const filledPullIntos = [];
15351543
while (controller._pendingPullIntos.length > 0) {
15361544
if (controller._queueTotalSize === 0) {
1537-
return;
1545+
break;
15381546
}
15391547

15401548
const pullIntoDescriptor = controller._pendingPullIntos[0];
15411549
assert(pullIntoDescriptor.readerType !== 'none');
15421550

15431551
if (ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor) === true) {
15441552
ReadableByteStreamControllerShiftPendingPullInto(controller);
1545-
1546-
ReadableByteStreamControllerCommitPullIntoDescriptor(
1547-
controller._stream,
1548-
pullIntoDescriptor
1549-
);
1553+
filledPullIntos.push(pullIntoDescriptor);
15501554
}
15511555
}
1556+
1557+
return filledPullIntos;
15521558
}
15531559

15541560
function ReadableByteStreamControllerProcessReadRequestsUsingQueue(controller) {
@@ -1673,9 +1679,15 @@ function ReadableByteStreamControllerRespondInClosedState(controller, firstDescr
16731679

16741680
const stream = controller._stream;
16751681
if (ReadableStreamHasBYOBReader(stream) === true) {
1676-
while (ReadableStreamGetNumReadIntoRequests(stream) > 0) {
1682+
const filledPullIntos = [];
1683+
let i = 0;
1684+
while (i < ReadableStreamGetNumReadIntoRequests(stream)) {
16771685
const pullIntoDescriptor = ReadableByteStreamControllerShiftPendingPullInto(controller);
1678-
ReadableByteStreamControllerCommitPullIntoDescriptor(stream, pullIntoDescriptor);
1686+
filledPullIntos.push(pullIntoDescriptor);
1687+
++i;
1688+
}
1689+
for (const filledPullInto of filledPullIntos) {
1690+
ReadableByteStreamControllerCommitPullIntoDescriptor(stream, filledPullInto);
16791691
}
16801692
}
16811693
}
@@ -1687,7 +1699,10 @@ function ReadableByteStreamControllerRespondInReadableState(controller, bytesWri
16871699

16881700
if (pullIntoDescriptor.readerType === 'none') {
16891701
ReadableByteStreamControllerEnqueueDetachedPullIntoToQueue(controller, pullIntoDescriptor);
1690-
ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller);
1702+
const filledPullIntos = ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller);
1703+
for (const filledPullInto of filledPullIntos) {
1704+
ReadableByteStreamControllerCommitPullIntoDescriptor(controller._stream, filledPullInto);
1705+
}
16911706
return;
16921707
}
16931708

@@ -1711,9 +1726,12 @@ function ReadableByteStreamControllerRespondInReadableState(controller, bytesWri
17111726
}
17121727

17131728
pullIntoDescriptor.bytesFilled -= remainderSize;
1714-
ReadableByteStreamControllerCommitPullIntoDescriptor(controller._stream, pullIntoDescriptor);
1729+
const filledPullIntos = ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller);
17151730

1716-
ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller);
1731+
ReadableByteStreamControllerCommitPullIntoDescriptor(controller._stream, pullIntoDescriptor);
1732+
for (const filledPullInto of filledPullIntos) {
1733+
ReadableByteStreamControllerCommitPullIntoDescriptor(controller._stream, filledPullInto);
1734+
}
17171735
}
17181736

17191737
function ReadableByteStreamControllerRespondInternal(controller, bytesWritten) {

0 commit comments

Comments
 (0)