Skip to content

Fix BufferSource algorithms for shared and resizable buffers - #1529

Open
MattiasBuelens wants to merge 37 commits into
whatwg:mainfrom
MattiasBuelens:buffer-source-fixes
Open

Fix BufferSource algorithms for shared and resizable buffers#1529
MattiasBuelens wants to merge 37 commits into
whatwg:mainfrom
MattiasBuelens:buffer-source-fixes

Conversation

@MattiasBuelens

@MattiasBuelens MattiasBuelens commented Oct 8, 2025

Copy link
Copy Markdown
Contributor

Previously, many BufferSource algorithms were using internal slots such as [[ByteOffset]] and [[ByteLength]] directly. However, with the addition of SharedArrayBuffer (#353, #1311) and [AllowResizable] (#982), this comes with extra caveats:

"Byte length" and "byte offset" both mirror JavaScript's asymmetry between typed arrays and DataViews: an out-of-bounds typed array reports 0, whereas an out-of-bounds DataView throws a TypeError. That way, implementations can reuse their existing getters.

When reading the length of a buffer, we use SEQ-CST ordering to match ECMAScript. When reading the contents of a buffer, we keep using UNORDERED, as "get a copy of the bytes held by the buffer source" already did. See #1529 (comment).

Transferring an ArrayBuffer must also take into account whether it should remain resizable or not. Fortunately, we can use the new ArrayBufferCopyAndDetach operation for that.

  • I added an optional preserveResizability parameter to "transfer an ArrayBuffer", so specifications can choose whether to preserve resizability or not. By default, this is not preserved. I intend to use it in Streams, see Add explainer for resizable buffers for BYOB readers streams#1360.
  • I removed the realm parameter of that algorithm, since ArrayBufferCopyAndDetach doesn't accept such a parameter. According to WebDex, no specs were actually using that parameter anyway.

Normative changes, which need a review of existing callers:

  • "Byte length" now throws a TypeError for a DataView that is detached or out of bounds, where it previously returned a stale [[ByteLength]]. This is reachable without [AllowResizable], simply by detaching the underlying buffer. "Get a copy of the bytes held by the buffer source" inherits this. Both algorithms have a note documenting it, but every existing caller is currently written as an infallible read.
  • "Transfer" no longer takes a realm parameter. Per WebDex this is not a breaking change in practice, but it does change the exported signature.
  • "Transfer" on a resizable ArrayBuffer now yields a fixed-length ArrayBuffer by default, instead of reusing the original buffer's resizability.

Relates to #1312.
Fixes #1385.

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


Preview | Diff

@MattiasBuelens
MattiasBuelens marked this pull request as ready for review October 13, 2025 08:14
@MattiasBuelens

Copy link
Copy Markdown
Contributor Author

@annevk Could you assign reviewer(s) to this? Thanks! 🙏

@annevk

annevk commented Nov 24, 2025

Copy link
Copy Markdown
Member

Thanks for tackling this!

Hopefully @syg and @bakkot can help or appoint someone?

@annevk annevk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I did an editorial pass.

Comment thread index.bs Outdated
Comment thread index.bs Outdated
Comment thread index.bs Outdated
Comment thread index.bs Outdated
Comment thread index.bs Outdated
Comment thread index.bs Outdated
Comment thread index.bs Outdated
Comment thread index.bs Outdated
Comment thread index.bs Outdated
Comment thread index.bs Outdated

@MattiasBuelens MattiasBuelens left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Quick self-review.

Comment thread index.bs Outdated
Comment thread index.bs Outdated
Comment thread index.bs Outdated
Comment thread index.bs
Comment thread index.bs Outdated
1. [=/Assert=]: |arrayBuffer| is an {{ArrayBuffer}} or {{SharedArrayBuffer}} object.
1. Let |jsArrayBuffer| be the result of [=converted to a JavaScript value|converting=]
|arrayBuffer| to a JavaScript value.
1. If [$IsDetachedBuffer$](|jsArrayBuffer|) is true, then return the empty

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If you pass a TA backed by a detached buffer, then the Set |offset| to |bufferSource|'s [=ArrayBufferView/byte offset=]. step above will throw. It's weird that passing a TA backed by a detached buffer throws while passing the detached buffer itself returns the empty byte sequence (in this step). It doesn't fundamentally break anything, but it's inconsistent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right, we should check if bufferSource's underlying buffer is detached before we try to read bufferSource's byte offset.

I'll move this to the preceding "if bufferSource is a buffer view type instance" step.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've moved the detached check all the way to the top. I also check if length is zero, so reading byte offset later on won't throw.

Funnily enough, reading bufferSource's byte length itself can still throw for an out-of-bounds DataView (as you already reported)... Do we want "get a copy of the bytes held" to propagate that error? Or should we return an empty byte sequence in that case? 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I changed it so we read both byte length and byte offset before we check if length is zero. That way, any exceptions from reading those properties will be thrown first.

@MattiasBuelens
MattiasBuelens force-pushed the buffer-source-fixes branch 3 times, most recently from ce8c232 to cf28af9 Compare January 11, 2026 19:00
@syg

syg commented Jan 15, 2026

Copy link
Copy Markdown
Contributor

When dealing with views backed by a SharedArrayBuffer, what memory ordering should we use? ECMAScript seems to use SEQ-CST pretty much everywhere (e.g. %TypedArray%.prototype.byteLength), but "get a copy of the bytes" uses UNORDERED for GetValueFromBuffer.

The rules of thumb here are:

  • Explicit accesses of the length, like via .length, or a particular builtin reading the length, of a growable SAB are seq-cst (intended to be synchronizing)
  • Implicit accesses of the length for the sake of bounds checking are unordered (intended to be fast)
  • Access of SAB (growable or otherwise) contents themselves are unordered (intended to be fast)

@MattiasBuelens

Copy link
Copy Markdown
Contributor Author

@annevk @bakkot Could you re-review? Thanks. 🙏

@annevk

annevk commented Aug 28, 2026

Copy link
Copy Markdown
Member

AI found a number of substantive issues that appear to be correct: https://gist.github.com/annevk/123a3994ba82ec664b787559803cdabf (it's also incorrect about some things though; our Bikeshed setup would definitely fail on link errors).

@MattiasBuelens MattiasBuelens left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Quick self-review.

Comment thread index.bs
allocated. If they are in the same [=agent cluster=], then implementations will just
change the backing pointers to get the same observable results with better performance
and no allocations.
* Sufficient memory cannot be allocated in the [=current realm=], which is the [=realm=]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure if we have to fully spell out the meaning of "current realm" here? Happy to remove that.

Comment thread index.bs Outdated
Comment thread index.bs Outdated
Comment thread index.bs

@annevk annevk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks okay to me now, but how do we want to handle tests and such?

@MattiasBuelens

Copy link
Copy Markdown
Contributor Author

We'll need some APIs that already have [AllowResizable] and/or [AllowShared] attributes. Looking around a bit, I found:

So I think the best way forward right now is to use the WebAssembly methods for testing. It'll be a bit awkward since the test inputs will have to be valid WASM modules, but I think I can make that work. I'll take a look. 🙂

@MattiasBuelens

MattiasBuelens commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

I've opened web-platform-tests/wpt#62301, which tests "get a copy of the bytes held by the buffer source" across all combinations of resizable/shared buffers and fixed-length/length-tracking/out-of-bounds views.

MattiasBuelens and others added 25 commits August 31, 2026 11:10
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
%TypedArray%.prototype.byteOffset returns +0 for an out-of-bounds typed
array; only DataView.prototype.byteOffset throws. Mirror that split,
like byte length already does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Matches how IsArrayBufferViewOutOfBounds and DataView.prototype.byteLength
dispatch, and removes two asserts that only restated the branch condition.
Also consolidates the three per-step equivalence notes into one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reading |view|'s byte length or byte offset throws for an out-of-bounds
DataView, so the existing asserts could throw. Assert the precondition
first instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bind the resizability once instead of duplicating the
ArrayBufferCopyAndDetach call, drop the redundant detached check (step 5
of that operation already throws), and use <emu-val>undefined</emu-val>.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The realm parameter was removed. ArrayBufferCopyAndDetach allocates with
%ArrayBuffer%, so the new buffer is always allocated in the current realm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both algorithms now throw for an out-of-bounds DataView, where byte
length previously returned a stale [[ByteLength]]. Call that out with
algorithm-level notes, like the existing detach and transfer notes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Match the 33 existing plain Assert: steps; converting the whole file to
[=/Assert=] can be a separate editorial change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Matches how BufferSource/detached is referenced throughout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Get a copy of the bytes" returns the empty byte sequence for a detached
buffer source before it ever reads the byte length, so only a non-detached
out-of-bounds DataView reaches the throwing path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@MattiasBuelens

Copy link
Copy Markdown
Contributor Author

I ran the new tests from web-platform-tests/wpt#62301 against stable Chromium and Firefox, and there were already a few issues. 😅

Chromium currently rejects SharedArrayBuffers and DataViews on WebAssembly methods. see web-platform-tests/wpt#62301 (comment). Definitely a bug, since the Web IDL standard as published today already requires this to work. I filed #555029264 on their end.

However, when Chromium decides to fix this bug, we'll need to decide what behavior they should be implementing for DataViews, specifically for out-of-bounds DataViews.

  • The published Web IDL standard would read a stale [[ByteLength]] field - definitely wrong.
  • Gecko (in Firefox 154) treats this as if the DataView were empty, matching the behavior for an out-of-bounds typed array. This appears to be the more "natural" behavior, as browsers can treat typed arrays and DataViews more or less the same.
  • With this PR, browsers would need to throw instead. This aligns with ECMAScript's DataView.prototype.byteLength, but requires browsers to have separate paths for typed arrays and DataViews instead of a single ArrayBufferView path.

Note that I haven't tested WebKit yet, since I don't have a macOS device. It might be interesting to know how they handle out-of-bounds DataViews, so we can better assess the impact of this change.

@bakkot Are we still happy with this change as discussed earlier? If yes, I'll make sure to get implementer's interest from the Gecko folks, since this will change observable behavior in their implementation.

@bakkot

bakkot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

I'll review this in a day or two though I'm somewhat rusty on the webidl side of things.

FWIW, another possible fix for the DataView issue is to make those operations not throw on DataViews in ECMAScript (and then have the web match). Making errors into non-errors is almost always web-compat. (Fun fact: ArrayBuffer.prototype.byteLength used to be spec'd to throw also; this was changed in tc39/ecma262#2164.)

@MattiasBuelens

Copy link
Copy Markdown
Contributor Author

FWIW, another possible fix for the DataView issue is to make those operations not throw on DataViews in ECMAScript (and then have the web match). Making errors into non-errors is almost always web-compat. (Fun fact: ArrayBuffer.prototype.byteLength used to be spec'd to throw also; this was changed in tc39/ecma262#2164.)

Interesting! Looks like this was raised before: tc39/ecma262#2635. I'll comment there to bring this back to TC39's attention.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

"byte length of a buffer source type" needs updating for resizable and detached buffers

4 participants