Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various fixes for readable byte streams #1123

Merged
merged 44 commits into from May 26, 2021

Conversation

MattiasBuelens
Copy link
Collaborator

@MattiasBuelens MattiasBuelens commented Apr 16, 2021

This started as a few fixes that we encountered while working on #1114, but it ended up being... a lot of fixes. 😅

  • .respondWithNewView(newView) can now be called with a newView that is smaller than the BYOB request's view. This aligns it with .respond(bytesWritten), which also allows bytesWritten <= byobRequest.view.byteLength.
  • .respondWithNewView() must now be called with an empty view when the stream is closed. This aligns it with .respond(bytesWritten), which requires bytesWritten to be 0 when closed.
  • .respondWithNewView(newView) must now be called with a view whose view.buffer.byteLength matches that of the BYOB request. Ideally, we would like to assert that the new view's buffer is the "transferred version" of the BYOB request's buffer, but that's not possible yet with the tools currently provided by the ECMAScript specification.
  • .enqueue(chunk) and .respondWithNewView(newView) now check that the given view's buffer is actually transferable. Previously, we only checked whether the buffer is not yet detached, but this is insufficient: a WebAssembly.Memory's buffer is never transferable. We also make sure to not transfer the given buffer until after we've checked all other preconditions, so the buffer is still intact if these methods were to throw an error.
  • .enqueue() and .respond() now check that the BYOB request's view has not been transferred, since otherwise it's not possible to copy bytes into its buffer and/or transfer the buffer when committing. This could crash Chrome, but has already been fixed in https://crbug.com/1200302. The reproduction case (see below) is now covered by a WPT test.
  • .enqueue(), .respond() and .respondWithNewView() immediately invalidate the BYOB request. Previously, we only did this if we actually filled the first pull-into descriptor, which doesn't always happen. (For example: if the pull-into descriptor's element size is 4, but we only have filled 1 or 2 bytes.)
  • We now always transfer the pull-into descriptor's buffer when committing it (to fulfill a read request or read-into request). This is mainly a sanity check: the stream should never use this buffer after it has been committed.
Original description

While working on #1114, we found that byobRequest.respondWithNewView(newView) didn't support all the same use cases as a regular .respond(bytesWritten) call. In particular:

  • newView was required to be the same length as the BYOB request's view. This would have required the underlying byte source to always fill the entire view. On the other hand, .respond() allows responding with fewer bytes up to the view's length, which is the desired behavior.
  • When the stream is closed, .respond() can only be called with bytesWritten set to 0. To match this, .respondWithNewView() should only be called with an empty view in the closed state.

We now also require the view passed to .respondWithNewView(newView) to have the same capacity as the BYOB request's original buffer. Ideally, we would like to assert that the new view's buffer is the "transferred version" of the BYOB request's buffer, but that's not possible yet with the tools currently provided by the ECMAScript specification.

I also found cases where the stream would try to transfer an already transferred buffer. For example, this code crashes the tab in Chrome 92 (error code: STATUS_ACCESS_VIOLATION):

let rs = new ReadableStream({
    type: 'bytes',
    pull(c) {
        let buffer = c.byobRequest.view.buffer;
        // Detach the buffer.
        postMessage(buffer, '*', [buffer]);
        // Try to enqueue with a new buffer.
        // The stream will try to copy to the pull-into request's view, but it can't because it has lost the buffer.
        c.enqueue(new Uint8Array([42]));
    }
});
let reader = rs.getReader({ mode: 'byob' });
await reader.read(new Uint8Array(1));

I fixed this by requiring the BYOB request's buffer to not be detached when calling byteStreamController.enqueue() or .close().

(See WHATWG Working Mode: Changes for more details.)


Preview | Diff

@MattiasBuelens
Copy link
Collaborator Author

I also noticed that ReadableByteStreamControllerRespondWithNewView can still throw an error after we've already updated the first pull-into descriptor's buffer to the new view's buffer. Specifically, ReadableByteStreamControllerRespondInternal may still throw a TypeError if bytesWritten is or is not zero (depending on whether we're in the readable or closed state).

I think we will need to turn these checks into asserts inside RespondInternal(), and throw errors in Respond() and RespondWithNewView()?

index.bs Outdated Show resolved Hide resolved
@MattiasBuelens
Copy link
Collaborator Author

Hmm, we should probably also tackle #1001 while we're at it. It's often not enough to check if IsDetachedBuffer returns false, we should actually try to transfer any user-provided ArrayBuffer as soon as possible and propagate any errors before we're adding/changing pull-into descriptors deep inside our abstract ops. 🤔

Copy link
Member

@domenic domenic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we will need to turn these checks into asserts inside RespondInternal(), and throw errors in Respond() and RespondWithNewView()?

That does sound nicer. It's possible our original reasoning was that in error states like these, there are no guarantees, but we might as well make it better.

index.bs Outdated Show resolved Hide resolved
@ricea
Copy link
Collaborator

ricea commented Apr 19, 2021

I'm working on a fix for the Chrome crash that Mattias found, which I will land before the rest of these changes.

@ricea
Copy link
Collaborator

ricea commented May 21, 2021

I am interested in this on behalf of Chrome.

Do you want me to file an issue for Chrome?

@MattiasBuelens
Copy link
Collaborator Author

Sure, go ahead. 🙂

Who do we need to ping for interest from other browsers?

Do we need to file bugs for other browsers? AFAIK readable byte streams are only implemented in Chrome at the moment, other browsers haven't implemented it yet.

@domenic
Copy link
Member

domenic commented May 21, 2021

IMO it's OK to treat bugfixes in readable byte streams as falling under the general precedent that other browsers have already expressed interest in readable byte streams; we don't need to block on them reviewing the fixes. (Although they're welcome to comment!) It's kind of a judgment call what's a bugfix vs. a normative change that would rise to the level of needing confirmation, but IMO these are all fine.

domenic pushed a commit to web-platform-tests/wpt that referenced this pull request May 26, 2021
@domenic domenic merged commit 033c6d9 into whatwg:main May 26, 2021
@MattiasBuelens MattiasBuelens deleted the byte-stream-fixes branch May 26, 2021 20:36
moz-v2v-gh pushed a commit to mozilla/gecko-dev that referenced this pull request May 27, 2021
…fixes, a=testonly

Automatic update from web-platform-tests
Streams: tests for readable byte stream fixes

Follows whatwg/streams#1123.
--

wpt-commits: 7b29ee36cc22bdad06b4f98df73358ca959fe0a7
wpt-pr: 28557
moz-v2v-gh pushed a commit to mozilla/gecko-dev that referenced this pull request May 27, 2021
…fixes, a=testonly

Automatic update from web-platform-tests
Streams: tests for readable byte stream fixes

Follows whatwg/streams#1123.
--

wpt-commits: 7b29ee36cc22bdad06b4f98df73358ca959fe0a7
wpt-pr: 28557
moz-v2v-gh pushed a commit to mozilla/gecko-dev that referenced this pull request May 29, 2021
…fixes, a=testonly

Automatic update from web-platform-tests
Streams: tests for readable byte stream fixes

Follows whatwg/streams#1123.
--

wpt-commits: 7b29ee36cc22bdad06b4f98df73358ca959fe0a7
wpt-pr: 28557
domenic pushed a commit that referenced this pull request Jun 2, 2021
Previously, if you cancel the stream while a BYOB read is pending, the stream has to wait for the underlying byte source to call respond(0) before it can return the BYOB request's buffer to the caller. This makes underlying byte sources difficult to write in a robust way. After this change, the contract changes so that canceling a stream while a BYOB read is pending will always lead to the stream not giving you back your buffer. This means that cancel() can immediately resolve all pending BYOB reads with the classic { done: true, value: undefined }, without having to wait for the underlying byte source. This solves the problem, and would make it easier to implement an underlying byte source.

To make this work, an additional change was required: when the stream is canceled, any pending BYOB request is now immediately invalidated, so the underlying byte source doesn't erroneously think that it still needs to provide a response. (This aligns with the behavior of controller.enqueue(), which throws if the stream is already closed.)

See #1114 (comment) and #1123 (comment) for some discussion and background.
yutakahirano pushed a commit to yutakahirano/streams that referenced this pull request Jun 3, 2021
Previously, if you cancel the stream while a BYOB read is pending, the stream has to wait for the underlying byte source to call respond(0) before it can return the BYOB request's buffer to the caller. This makes underlying byte sources difficult to write in a robust way. After this change, the contract changes so that canceling a stream while a BYOB read is pending will always lead to the stream not giving you back your buffer. This means that cancel() can immediately resolve all pending BYOB reads with the classic { done: true, value: undefined }, without having to wait for the underlying byte source. This solves the problem, and would make it easier to implement an underlying byte source.

To make this work, an additional change was required: when the stream is canceled, any pending BYOB request is now immediately invalidated, so the underlying byte source doesn't erroneously think that it still needs to provide a response. (This aligns with the behavior of controller.enqueue(), which throws if the stream is already closed.)

See whatwg#1114 (comment) and whatwg#1123 (comment) for some discussion and background.
@MattiasBuelens
Copy link
Collaborator Author

FYI since I couldn't find a Chromium issue for this spec change yet, I've filed one myself: https://crbug.com/1223565

@ricea
Copy link
Collaborator

ricea commented Jun 24, 2021

FYI since I couldn't find a Chromium issue for this spec change yet, I've filed one myself: https://crbug.com/1223565

Thanks! I dropped the ball there.

pull bot pushed a commit to Alan-love/chromium that referenced this pull request Oct 13, 2021
This CL is based on spec changes made in whatwg/streams#1123.

Bug: 1223565
Change-Id: I7235817f6f18e161d721212971c042624df059d2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3216570
Commit-Queue: Nidhi Jaju <nidhijaju@chromium.org>
Reviewed-by: Adam Rice <ricea@chromium.org>
Cr-Commit-Position: refs/heads/main@{#930929}
aarongable pushed a commit to chromium/chromium that referenced this pull request Dec 12, 2021
Based on changes made in whatwg/streams#1123,
we should check that the buffer is actually "transferable" in
enqueue(chunk) and respondWithNewView(newView). However, the
TypeError exceptions resulting from this check were not properly
propagated in the implementation until now.

This CL fixes this issue to prevent crashes while dealing with
readable byte streams with non-transferable buffers.

Bug: 1274019
Change-Id: I20e61fa7c2724a1ac958dbf40199d095fe320707
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3327856
Reviewed-by: Yutaka Hirano <yhirano@chromium.org>
Commit-Queue: Nidhi Jaju <nidhijaju@chromium.org>
Cr-Commit-Position: refs/heads/main@{#950863}
mjfroman pushed a commit to mjfroman/moz-libwebrtc-third-party that referenced this pull request Oct 14, 2022
This CL is based on spec changes made in whatwg/streams#1123.

Bug: 1223565
Change-Id: I7235817f6f18e161d721212971c042624df059d2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3216570
Commit-Queue: Nidhi Jaju <nidhijaju@chromium.org>
Reviewed-by: Adam Rice <ricea@chromium.org>
Cr-Commit-Position: refs/heads/main@{#930929}
NOKEYCHECK=True
GitOrigin-RevId: 3280161d249fef949e4901799827ad81f5df082c
mjfroman pushed a commit to mjfroman/moz-libwebrtc-third-party that referenced this pull request Oct 14, 2022
Based on changes made in whatwg/streams#1123,
we should check that the buffer is actually "transferable" in
enqueue(chunk) and respondWithNewView(newView). However, the
TypeError exceptions resulting from this check were not properly
propagated in the implementation until now.

This CL fixes this issue to prevent crashes while dealing with
readable byte streams with non-transferable buffers.

Bug: 1274019
Change-Id: I20e61fa7c2724a1ac958dbf40199d095fe320707
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3327856
Reviewed-by: Yutaka Hirano <yhirano@chromium.org>
Commit-Queue: Nidhi Jaju <nidhijaju@chromium.org>
Cr-Commit-Position: refs/heads/main@{#950863}
NOKEYCHECK=True
GitOrigin-RevId: 958bd007b37f4b16daf4802b79d5e87070370a9f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

None yet

3 participants