Skip to content

node:fs: fix FileHandle.readableWebStream BYOB read into a subarray view - #35355

Open
robobun wants to merge 2 commits into
mainfrom
farm/e900cd67/filehandle-readablewebstream-byob-offset
Open

node:fs: fix FileHandle.readableWebStream BYOB read into a subarray view#35355
robobun wants to merge 2 commits into
mainfrom
farm/e900cd67/filehandle-readablewebstream-byob-offset

Conversation

@robobun

@robobun robobun commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

FileHandle.readableWebStream()'s pull() handler calls handle.read(view, offset, length) where offset is the index within view at which to start writing. It was passing view.byteOffset, which is the view's offset within its backing ArrayBuffer. For any BYOB reader that supplies a subarray view (nonzero byteOffset), this double-counts the offset and throws ERR_OUT_OF_RANGE because offset + length > view.byteLength.

Repro

const fh = await require("fs").promises.open(path);
const r = fh.readableWebStream().getReader({ mode: "byob" });
await r.read(new Uint8Array(new ArrayBuffer(1000), 500, 100));
// RangeError: The value of "length" is out of range. It must be <= 0. Received 100

Fix

Pass 0 as the offset: handle.read(view, 0, view.byteLength). The native read writes at index 0 of the typed-array view, which already corresponds to view.byteOffset in the underlying buffer.

Node currently has the same bug (v26.3.0, lib/internal/fs/promises.js:362), but the semantics here are unambiguous: controller.byobRequest.view is the consumer's view, and FileHandle.read's offset is the write index within that view. The default reader path was unaffected only because autoAllocateChunkSize always creates a view with byteOffset === 0.

How did you verify your code works?

New test in test/js/node/fs/fs.test.ts passes a Uint8Array(new ArrayBuffer(1000), 500, 100) to a BYOB reader and asserts the result lands at byteOffset: 500 with the correct bytes. Throws ERR_OUT_OF_RANGE without the fix, passes with it. Full fs.test.ts (430 pass) and test-filehandle-readablestream.js / test-filehandle-autoclose.mjs remain green.


no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/fs/fs.test.ts

The pull() handler was passing view.byteOffset as the offset argument to
handle.read(). That argument is the write offset within the view itself,
so a BYOB reader supplying a view with a nonzero byteOffset would throw
ERR_OUT_OF_RANGE because offset + length exceeded the view's byteLength.
The underlying read already writes at index 0 of the typed-array view,
which is view.byteOffset in the backing ArrayBuffer.
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5732795d-41b1-4bae-b136-5c100acc76ee

📥 Commits

Reviewing files that changed from the base of the PR and between cd5f03b and 7712f66.

📒 Files selected for processing (2)
  • src/js/node/fs.promises.ts
  • test/js/node/fs/fs.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 11:44 PM PT - Jul 23rd, 2026

@robobun, your commit 7712f66 has 3 failures in Build #79261 (All Failures):

  • step failed outside runner - exit 255 on 🍎 26 aarch64 - test-bun
  • test/js/node/test/parallel/test-https-server-connections-checking-leak.js - crash reported on 🐧 13 x64
  • 📦 Binary size — 2 over 0.50 MB
  • targetthis build canary: main #79256
    sizeΔ
    bun-darwin-aarch6457.60 MB57.42 MB+179.2 KB
    bun-darwin-x6462.96 MB62.79 MB+177.8 KB
    bun-linux-aarch6470.80 MB70.30 MB+515.4 KB
    bun-linux-x6472.30 MB71.80 MB+513.6 KB
    bun-linux-aarch64-musl64.32 MB64.20 MB+128.0 KB
    bun-linux-x64-musl66.40 MB66.29 MB+112.0 KB
    bun-linux-aarch64-android78.16 MB77.78 MB+385.0 KB
    bun-linux-x64-android80.31 MB79.98 MB+336.3 KB
    bun-freebsd-x6482.57 MB82.43 MB+144.1 KB
    bun-freebsd-aarch6484.32 MB84.14 MB+192.1 KB
    bun-windows-x6479.70 MB79.54 MB+170.0 KB
    bun-windows-aarch6470.35 MB70.19 MB+162.5 KB

    Add [skip size check] to the commit message if this increase is intentional.


🧪   To try this PR locally:

bunx bun-pr 35355

That installs a local version of the PR into your bun-35355 executable, so you can run:

bun-35355 --bun

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. node:fs: implement FileHandle.readableWebStream() per Node.js contract #33694 - Rewrites FileHandle.readableWebStream() from scratch and already passes 0 instead of view.byteOffset in the BYOB pull handler, fixing the same subarray view bug with equivalent test coverage

🤖 Generated with Claude Code

@robobun

robobun commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Not a duplicate of #33694. That PR was a full rewrite of readableWebStream() which has since been superseded by #34505 (merged) and is now in a conflicting state against main. #34505 landed the rewrite but with view.byteOffset instead of 0 in the pull handler; this PR is the targeted fix for that specific bug on main.

@claude claude Bot left a comment

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.

LGTM — one-token fix with a targeted regression test.

What was reviewed

  • Confirmed FileHandle.read's offset is the write index within the view, so passing view.byteOffset double-counted the subarray offset; 0 is correct.
  • Default-reader path unaffected (autoAllocateChunkSize allocates views with byteOffset === 0), so no behavior change there.
  • Test uses tempDir/await using, asserts byteOffset/byteLength/content on the returned view, and would throw ERR_OUT_OF_RANGE without the fix.
Extended reasoning...

Overview

One-line change in src/js/node/fs.promises.ts: the readableWebStream() pull handler now calls handle.read(view, 0, view.byteLength) instead of handle.read(view, view.byteOffset, view.byteLength). A new test in test/js/node/fs/fs.test.ts exercises a BYOB reader with a Uint8Array(new ArrayBuffer(1000), 500, 100) subarray view.

Correctness

FileHandle#read(buffer, offset, length) treats offset as the index within the passed view at which to begin writing. The typed-array view's underlying data pointer already accounts for its own byteOffset into the backing ArrayBuffer, so passing view.byteOffset as offset shifts the write target a second time and trips the offset + length <= view.byteLength bounds check whenever byteOffset > 0. Writing at index 0 of the view is the correct semantics for a BYOB request. The default (non-BYOB) path is unaffected because autoAllocateChunkSize always produces a fresh view with byteOffset === 0. The PR notes Node has the identical bug upstream; the semantics are unambiguous, so diverging from a Node bug is appropriate here.

Security risks

None. This is a read-side offset correction inside a bounds-checked path; no new inputs, no relaxed validation.

Level of scrutiny

Low. Single-token change to a JS builtin, semantics are clear from FileHandle.read's own implementation in the same file (validateInteger(offset, "offset", 0) followed by read(fd, buffer, offset, length, position)). No native/memory-safety code touched.

Other factors

The test is placed alongside the existing FileHandle#readableWebStream test, follows harness conventions (tempDir, await using, Buffer.alloc(n, fill).toString()), and asserts exact values (byteOffset === 500, byteLength === 100, content equals 100 'X's). It fails without the fix (throws ERR_OUT_OF_RANGE), satisfying the fails-for-the-right-reason requirement. PR description reports the full fs.test.ts suite and both Node parallel filehandle tests remain green.

@claude claude Bot left a comment

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.

LGTM — one-line fix passing 0 instead of view.byteOffset to handle.read() in the BYOB pull handler.

What was reviewed:

  • Confirmed FileHandle.read's offset param is the write index within the view; view.byteOffset double-counted the backing-buffer offset for subarray views.
  • Default-reader path unaffected (autoAllocateChunkSize views always have byteOffset === 0); respond(bytesRead) still correct since the write starts at view index 0.
  • Test uses tempDir + await using, asserts exact bytes/offset/length, and would throw ERR_OUT_OF_RANGE on the old code — cleanup traced through closeonFileHandleCloseondone so the stream's ref is released.
Extended reasoning...

Overview

Single-character semantic fix in src/js/node/fs.promises.ts: the readableWebStream() pull handler was calling handle.read(view, view.byteOffset, view.byteLength). FileHandle.read's second argument is the offset within the passed view to start writing at, but view.byteOffset is the view's offset into its backing ArrayBuffer. For a BYOB reader that supplies a subarray (new Uint8Array(buf, 500, 100)), this passed offset=500, length=100 into a 100-byte view and tripped the native ERR_OUT_OF_RANGE bounds check. Changing to 0 writes at the start of the view, which already maps to the correct byte in the underlying buffer. A new test in test/js/node/fs/fs.test.ts exercises exactly this case.

Security risks

None. Pure JS, no new inputs, no bounds relaxed — the native read still validates offset + length <= view.byteLength.

Level of scrutiny

Low. The diff is a one-token change whose semantics are unambiguous once you read FileHandle.read a few lines up in the same file: offset is validated as an integer ≥ 0 and passed straight to the native read(fd, buffer, offset, length, position), which indexes into the typed-array view. The default-reader path was never broken because autoAllocateChunkSize always allocates a fresh view with byteOffset === 0, so view.byteOffset and 0 were coincidentally equal there. controller.byobRequest.respond(bytesRead) remains correct because bytes are written starting at index 0 of the request view.

Other factors

  • Test follows harness conventions (tempDir, await using fh, Buffer.alloc(n, fill)), asserts exact byte content and the returned view's byteOffset/byteLength, and reads 100 bytes from a 200-byte regular file so short reads aren't a flake vector.
  • Traced cleanup: reader.releaseLock() then await using fhclose() emits 'close'onFileHandleCloseondone()kUnref, so the stream's extra ref is released and the fd closes.
  • PR description notes Node v26.3.0 has the same bug upstream; deviating from Node here is correct because the semantics of FileHandle.read's offset are documented and the current behavior is a hard failure, not a compat quirk.
  • Duplicate-PR bot flagged #33694; author clarified that #34505 already merged the rewrite with this bug intact and #33694 is stale — this is the targeted follow-up on main.
  • No prior human or claude[bot] reviews; nothing outstanding.

@robobun

robobun commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

The diff here is green: the new FileHandle#readableWebStream BYOB reader accepts a subarray view test fails with ERR_OUT_OF_RANGE on main and passes with the fix, and the full fs.test.ts suite (430 tests) plus test-filehandle-readablestream.js / test-filehandle-autoclose.mjs pass on every lane that ran them.

The remaining CI red on build 79261 is unrelated to this one-token JS builtin change:

  • Format (src/runtime/server/server_body.rs): autofix conflict from main moving ahead of this branch's base; Format passed on the first build (79194) at b9a5b1c.
  • binary-size (+500 KB vs canary): baseline drift; main merged several large removals (Ban <iostream> from release builds and drop it from bun-uws #35256, Remove $vm from release builds #35359, webcrypto ML-DSA/ML-KEM) after this branch was cut, so canary is smaller than this branch's base.
  • test-https-server-connections-checking-leak.js: panic: Failed to start File Watcher: EAGAIN, i.e. inotify instance exhaustion on the CI agent; nothing in this diff touches the file watcher or HTTPS.
  • napi / bun-install-registry / webview-chrome: flaky, passed on retry.

Ready for review.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant