Skip to content

node:fs: validate buffer type before offset in fs.read/readSync#32301

Open
robobun wants to merge 3 commits into
mainfrom
claude/farm/9d47e6bb/fs-read-error-order
Open

node:fs: validate buffer type before offset in fs.read/readSync#32301
robobun wants to merge 3 commits into
mainfrom
claude/farm/9d47e6bb/fs-read-error-order

Conversation

@robobun

@robobun robobun commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

What

fs.readSync(fd, "not a buffer", -1, 5) (and the callback-based fs.read equivalent) threw ERR_OUT_OF_RANGE for offset instead of ERR_INVALID_ARG_TYPE for buffer. Node.js validates the buffer's type first, so when both arguments are invalid the buffer error wins.

const fs = require("fs");
try { fs.readSync(0, "not a buffer", -1, 5) } catch (e) { console.log(e.code) }
// Node:       ERR_INVALID_ARG_TYPE
// Bun before: ERR_OUT_OF_RANGE
// Bun after:  ERR_INVALID_ARG_TYPE

Why

args::Read::from_js in src/runtime/node/node_fs.rs validated offset (via validate_integer) before calling Buffer::from_js on the buffer argument. The ordering diverged in #31559, which moved Buffer::from_js after length.toNumber() so the destination buffer is snapshotted after user code in valueOf could detach it. That move also pushed the buffer type check past offset validation, so the Node error ordering (validateBuffer, then validateInteger(offset)) was no longer matched.

Fix

Restore the Buffer::from_js type check to immediately follow buffer_value (matching Node and the Zig reference), and re-snapshot the buffer after length.toNumber() so the subsequent bounds checks and the read itself still observe the post-coercion backing store. The existing detach-during-coercion test from #31559 continues to pass.

Verified

  • New tests fail on the released build with ERR_OUT_OF_RANGE, pass with this change.
  • readSync > rejects the read when the length argument detaches the destination buffer during coercion still passes.
  • Full readSync describe block: 9 pass, 0 fail.

args::Read::from_js was validating the offset argument before checking
that the buffer argument is a TypedArray. When both arguments were
invalid, Bun threw ERR_OUT_OF_RANGE for offset where Node.js throws
ERR_INVALID_ARG_TYPE for buffer.

The ordering diverged in #31559, which moved the Buffer::from_js call
after length.toNumber() so the destination buffer is snapshotted after
user code (valueOf on the length argument) could detach it. That move
also pushed the type check past offset validation.

Restore the type check to its original position (matching Node and the
Zig reference) and keep a second Buffer::from_js after toNumber() so the
bounds checks and the read itself still observe the post-coercion
backing store.
@robobun

robobun commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 10:08 AM PT - Jun 15th, 2026

@robobun, your commit cc7d065 has some failures in Build #62555 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 32301

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

bun-32301 --bun

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

More reviews will be available in 2 minutes. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

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.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8a252bf4-f644-47ac-ad69-1df39cb3e4ce

📥 Commits

Reviewing files that changed from the base of the PR and between a4f3a88 and cc7d065.

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

Walkthrough

args::Read::from_js in node_fs.rs is updated to validate the buffer argument as a TypedArray and to re-snapshot the buffer after length coercion (which can trigger JS re-entrancy and detach the original backing store). Two regression tests are added to verify ERR_INVALID_ARG_TYPE is reported for invalid buffer arguments in readSync and fs.read.

Changes

Buffer argument validation and re-snapshot in fs read

Layer / File(s) Summary
Buffer type check and post-coercion re-snapshot
src/runtime/node/node_fs.rs
args::Read::from_js now calls Buffer::from_js(ctx, buffer_value) and throws ERR_INVALID_ARG_TYPE for non-TypedArray inputs. After length.toNumber() coercion (which may re-enter JS and detach the buffer), the buffer is re-loaded from buffer_value; a failed re-parse falls back to the original.
Regression tests for validation ordering
test/js/node/fs/fs.test.ts
Adds a sync readSync test and an async fs.read callback test, both asserting that a non-buffer first argument produces a TypeError with code ERR_INVALID_ARG_TYPE and a message containing "buffer", even when offset is also invalid.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: validating buffer type before offset in fs.read/readSync, which directly matches the changeset's core fix.
Description check ✅ Passed The description follows the template with clear 'What' and 'Why' sections, plus a 'Fix' section explaining the solution and 'Verified' section detailing test results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

Comment thread src/runtime/node/node_fs.rs Outdated
Comment thread test/js/node/fs/fs.test.ts Outdated
…n load-bearing

The re-snapshot after length.toNumber() now asserts the JSCell-type
invariant via .expect() instead of falling back to the pre-coercion
snapshot on the unreachable None arm.

The second readSync assertion now checks the error message names
"buffer"; a string offset also produces ERR_INVALID_ARG_TYPE via
validateInteger, so the error code alone did not distinguish which
argument was rejected.

@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 — both nits from the prior review are addressed (.expect() on the re-snapshot, message-asserting test for the string-offset case).

Extended reasoning...

Overview

This PR reorders argument validation in args::Read::from_js (src/runtime/node/node_fs.rs) so the buffer's type is checked before offset, matching Node.js's validateBuffervalidateInteger ordering. The post-length.toNumber() re-snapshot from #31559 is preserved, now spelled as .expect(...) on a provably-unreachable None arm. Two regression tests in test/js/node/fs/fs.test.ts cover both readSync and callback fs.read, asserting the error code and that the message names "buffer".

Since my previous review, commit bc63519 applied both suggested changes: .unwrap_or(buffer).expect("buffer JSCell type is immutable; detached views return Some(len=0)"), and the string-offset assertion now checks message: expect.stringContaining('"buffer"') so it's load-bearing against the pre-fix behavior. Both inline comments are marked resolved.

Security risks

None. The change only affects which validation error surfaces first when arguments are already invalid — execution never reaches the read syscall on these paths. The early Buffer::from_js type check rejects non-TypedArray inputs sooner than before, and the .expect() guards an internal invariant (JSCell type immutability) that, if ever violated, would otherwise risk a stale-pointer write — failing loud is the safer spelling there.

Level of scrutiny

Low-to-moderate. This is a ~15-line Node-compat fix to error-path ordering in a single function, plus ~40 lines of targeted tests. The only subtlety is the interaction with #31559's detach-during-coercion safeguard, which the diff explicitly preserves (re-snapshot still happens after arg.to_number(ctx)?), and the PR description confirms the existing detach test still passes.

Other factors

  • Bug-hunting system found nothing on this revision.
  • Both prior 🟡 nits were addressed exactly as suggested.
  • Author verified the new tests fail on the released build with ERR_OUT_OF_RANGE and pass with the fix; full readSync describe block passes 9/9.
  • The .expect() arm is unreachable per JSC__JSValue__asArrayBuffer semantics (detached views return Some(len=0)), so no new panic surface in practice.

@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 — both review nits addressed (.expect() on the re-snapshot, message-asserting second test case).

Extended reasoning...

Overview

Reorders argument validation in args::Read::from_js (src/runtime/node/node_fs.rs) so the buffer type check happens before offset validation, matching Node.js error precedence. The post-length.toNumber() re-snapshot from #31559 is preserved, now spelled with .expect() on the documented invariant. Adds three regression assertions in test/js/node/fs/fs.test.ts.

Security risks

None. The change only affects which validation error fires first; the detach-during-coercion safeguard is intact and now fail-loud rather than silently falling back. No new I/O paths, no auth/crypto/permissions surface.

Level of scrutiny

Low–moderate. ~10 effective lines of Rust in a single argument-parsing function plus straightforward test additions. The only subtlety — keeping the re-snapshot after JS re-entry — is handled correctly and commented.

Other factors

I left two nit-level comments on the previous revision (.unwrap_or(buffer).expect(...), and making the string-offset assertion check the message). Commit bc63519 applied both exactly as suggested; both threads are resolved. The bug-hunting pass on the updated revision found nothing. Tests assert both code and message so they're load-bearing against the pre-fix behavior, and the PR description confirms the #31559 detach test still passes.

@robobun

robobun commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

Diff is green: 280 jobs passed on build #62555, test/js/node/fs/fs.test.ts passes on every lane that ran it, and the only annotations are warning-level known-flaky Windows tests (update_interactive_install, spawn, fetch-leak) that passed on retry.

The remaining red is three darwin test lanes (darwin-14-aarch64, darwin-14-x64, darwin-26-aarch64) that expired in the agent queue without running, on both this build and the previous one (#62493). That's infra, not something a ~15-line Rust reorder in node_fs.rs can cause.

Ready for a maintainer to merge once the darwin queue recovers or the expired lanes are waived.

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