Skip to content

ReadFile: avoid integer overflow when computing buffer capacity#29000

Closed
robobun wants to merge 6 commits into
mainfrom
farm/87252358/fix-readfile-size-overflow
Closed

ReadFile: avoid integer overflow when computing buffer capacity#29000
robobun wants to merge 6 commits into
mainfrom
farm/87252358/fix-readfile-size-overflow

Conversation

@robobun

@robobun robobun commented Apr 8, 2026

Copy link
Copy Markdown
Collaborator

this.size is a u52 (Blob.SizeType). In ReadFile.runAsyncWithFD, this.size + 16 is evaluated in u52 arithmetic, and overflows when this.size is close to u52 max. In debug builds this traps as SIGILL and looks like a thread-pool crash:

bun.js.webcore.blob.read_file.ReadFile.runAsyncWithFD
/workspace/bun/src/bun.js/webcore/blob/read_file.zig:387:100

bun.js.webcore.Blob.FileOpener(bun.js.webcore.blob.read_file.ReadFile).getFd
/workspace/bun/src/bun.js/webcore/Blob.zig:4810:25

The existing guard this.size != Blob.max_size only excludes the exact max value, so any size in [max_u52 - 15, max_u52 - 1] still overflows.

Reachable today via e.g. Bun.file('/dev/null').slice(0, 2**52 - 15).bytes(): for a character device resolveSizeAndLastModified sets this.size = max_length, which is passed through from the blob slice.

Fix: widen to usize before adding. The allocator's existing catch handler gracefully handles the subsequent OutOfMemory.

this.size is u52 (Blob.SizeType). For character devices or pipes where
max_length is near u52 max, this.size + 16 overflows and traps as SIGILL
in debug builds. Widen to usize before adding.
@github-actions github-actions Bot added the claude label Apr 8, 2026
@robobun

robobun commented Apr 8, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Cast file-size to usize for buffer allocation in ReadFile, set an explicit system_error on allocation failure, and add a platform-conditional test asserting large-slice behavior on character devices (non-Windows).

Changes

Cohort / File(s) Summary
Blob read implementation
src/bun.js/webcore/blob/read_file.zig
Cast this.size to usize when computing initCapacity (@as(usize, this.size) + 16) and set this.system_error from bun.sys.Error.fromCode(... NOMEM ...) before onFinish() on allocation failure.
Tests — blob OOM / char device
test/js/web/fetch/blob-oom.test.ts
Import isWindows and add a describe.if(!isWindows) suite that checks Bun.file("/dev/null").slice(0, 2 ** 52 - 15).bytes() throws, ensuring OOM propagation for large slice on character devices.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: casting this.size to usize to avoid integer overflow when computing buffer capacity in ReadFile.
Description check ✅ Passed The PR description provides clear context about the u52 overflow issue, existing guard limitations, reachability example, and the implemented fix, aligning well with the template sections.

✏️ 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/bun.js/webcore/blob/read_file.zig
robobun and others added 2 commits April 8, 2026 13:55
then() only inspects this.system_error to propagate errors to JS, but
the catch handler for initCapacity in runAsyncWithFD was only setting
this.errno. An OOM from the initial buffer allocation silently returned
an empty result. Match the ReadFileUV pattern and set both.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/js/web/fetch/blob-oom.test.ts`:
- Around line 153-158: The current assertion uses a broad toThrow() which can
catch unrelated errors; tighten it to assert an OOM-specific failure by
expecting an OOM-shaped message or code from the Promise returned by
Bun.file(...).slice(...).bytes(): replace expect(async () => await
Bun.file("/dev/null").slice(0, 2 ** 52 - 15).bytes()).toThrow() with an
assertion that matches an out-of-memory error (e.g., toThrow(/out of memory/i)
or toThrowErrorMatchingInlineSnapshot with the OOM message), so the test for
blob-oom.test.ts specifically verifies OOM propagation from
Bun.file(...).slice(...).bytes().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f5052f6f-e342-4a1b-9c18-41b8130cd3a9

📥 Commits

Reviewing files that changed from the base of the PR and between a661ed5 and cf4a0b2.

📒 Files selected for processing (2)
  • src/bun.js/webcore/blob/read_file.zig
  • test/js/web/fetch/blob-oom.test.ts

Comment on lines +153 to +158
expect(
async () =>
await Bun.file("/dev/null")
.slice(0, 2 ** 52 - 15)
.bytes(),
).toThrow();

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.

🧹 Nitpick | 🔵 Trivial

Make this regression assertion OOM-specific.

toThrow() is broad and may pass on unrelated exceptions. Since this test is validating OOM propagation, assert an OOM-shaped error message/code.

Suggested assertion tightening
-    expect(
-      async () =>
-        await Bun.file("/dev/null")
-          .slice(0, 2 ** 52 - 15)
-          .bytes(),
-    ).toThrow();
+    expect(
+      async () =>
+        await Bun.file("/dev/null")
+          .slice(0, 2 ** 52 - 15)
+          .bytes(),
+    ).toThrow(/out of memory|enomem|cannot allocate memory/i);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(
async () =>
await Bun.file("/dev/null")
.slice(0, 2 ** 52 - 15)
.bytes(),
).toThrow();
expect(
async () =>
await Bun.file("/dev/null")
.slice(0, 2 ** 52 - 15)
.bytes(),
).toThrow(/out of memory|enomem|cannot allocate memory/i);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/js/web/fetch/blob-oom.test.ts` around lines 153 - 158, The current
assertion uses a broad toThrow() which can catch unrelated errors; tighten it to
assert an OOM-specific failure by expecting an OOM-shaped message or code from
the Promise returned by Bun.file(...).slice(...).bytes(): replace expect(async
() => await Bun.file("/dev/null").slice(0, 2 ** 52 - 15).bytes()).toThrow() with
an assertion that matches an out-of-memory error (e.g., toThrow(/out of
memory/i) or toThrowErrorMatchingInlineSnapshot with the OOM message), so the
test for blob-oom.test.ts specifically verifies OOM propagation from
Bun.file(...).slice(...).bytes().

@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 the u52 overflow fix and the OOM propagation fix are correctly applied, matching the ReadFileUV pattern.

Extended reasoning...

Overview

Two files changed: src/bun.js/webcore/blob/read_file.zig (2-line fix in runAsyncWithFD) and test/js/web/fetch/blob-oom.test.ts (new regression test). The fix targets a narrow, well-understood integer overflow in buffer capacity computation for the non-blocking read path.

Security risks

None. The overflow path could previously cause debug-build panics (SIGILL) or undefined behavior, but is not a security boundary — it's an internal memory allocation. The fix correctly converts from u52 to usize before arithmetic and properly propagates OOM as a JS exception.

Level of scrutiny

Low. The diff is minimal and mechanical: a cast insertion and a missing field assignment. Both changes follow established patterns already present in ReadFileUV and other error paths in the same file. The fix is straightforward and well-tested.

Other factors

My prior inline comment identified the missing system_error assignment that would have caused silent data-loss for character devices with actual data. The author addressed this correctly in commit 8e273937d1, and the regression test now asserts .toThrow() rather than silently accepting empty bytes. No outstanding concerns remain.

@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.

Both fixes are in place and my prior feedback was addressed.

Extended reasoning...

Overview

This PR touches (the ReadFile async path) and a regression test in . The changes are small and focused: a one-line type cast to prevent u52 overflow, plus a assignment in the OOM catch block.

Security risks

None. This is a numeric-type fix to prevent integer overflow in a buffer allocation path. No auth, crypto, or permissions code is involved.

Level of scrutiny

Low-to-medium. The change is mechanical and follows the exact pattern used by . My earlier inline comment flagged the missing — the author resolved it in the same PR. The current code correctly sets both and on OOM, matching all other error paths in the file.

Other factors

  • CI failures shown in the timeline are from an intermediate commit (); the current HEAD is (autofix.ci formatting pass).
  • The coderabbitai nit about tightening to be OOM-specific is trivial and does not affect correctness or the regression coverage.
  • No CODEOWNERS-owned paths are touched.

@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.

My prior concern about the missing system_error assignment in the OOM catch handler has been addressed — LGTM.

Extended reasoning...

Overview

This PR fixes a u52 integer overflow in ReadFile.runAsyncWithFD when computing the initCapacity argument. The change is minimal: a @as(usize, this.size) + 16 cast replaces this.size + 16, and the OOM catch handler now also sets this.system_error to properly propagate the error to JS. A non-Windows regression test is added.

Security risks

None. This is a memory-safety fix (overflow → panic in debug builds) with no authentication, cryptography, or permission surface.

Level of scrutiny

Low. The diff is 3 changed lines in the Zig source plus a new test block. The pattern exactly matches the existing ReadFileUV OOM handler. No novel logic is introduced.

Other factors

My prior inline review flagged that the catch handler only set this.errno and not this.system_error, causing OOM to be silently swallowed. The author addressed this in commit 8e27393. The one remaining open item — the bare .toThrow() in the test instead of an OOM-specific matcher — is a nit already covered by inline comments from CodeRabbit and this review cycle. It does not affect correctness of the fix itself.

Comment on lines +151 to +159
// OOM from the buffer allocation is propagated to JS rather than
// silently returning an empty result.
expect(
async () =>
await Bun.file("/dev/null")
.slice(0, 2 ** 52 - 15)
.bytes(),
).toThrow();
});

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.

🟡 The new regression test at line 158 uses bare .toThrow() without an error-message predicate, so it passes for any thrown exception — not specifically the ENOMEM that the OOM propagation fix is meant to surface. The test comment explicitly states it 'verifies that OOM from the buffer allocation is propagated to JS rather than silently returning an empty result', but .toThrow() cannot verify that; it would also pass if an unrelated EPERM or ENOENT were thrown while OOM was silently swallowed. The fix is to use .toThrow(/out of memory|enomem|cannot allocate memory/i), consistent with the OOM-specific assertions in the 'Memory' describe block (lines 34, 60, 91) that use exact messages like 'Out of memory'.

Extended reasoning...

What the bug is: The test at test/js/web/fetch/blob-oom.test.ts:158 uses expect(async () => ...).toThrow() with no error-message argument. This assertion succeeds for any thrown exception, not just the ENOMEM error that bun.sys.Error.fromCode(bun.sys.E.NOMEM, .read).toSystemError() produces.

The specific false-positive scenario: If the OOM propagation fix (this.system_error = ... assignment) were accidentally reverted, reading /dev/null would silently return empty bytes (the pre-fix behaviour for the ReadFile path). In that specific scenario .toThrow() would indeed fail — so the test does guard against the primary regression. However, if some other unrelated exception fires (e.g. a permissions change on /dev/null, a libc change that makes character-device stat behave differently, or a future refactor that introduces an early error return), the test would incorrectly pass even while OOM is silently swallowed.

Why existing code doesn't prevent it: The Memory describe-block OOM tests at lines 34, 60, and 91 all pass an exact string to .toThrow() (e.g., 'Out of memory'). The new test follows the weaker pattern of the Bun.file describe block (lines 130, 134, 138) rather than the stronger OOM-specific pattern used elsewhere in the same file.

Impact: The assertion specificity is inconsistent with the stated test comment. In practice the risk is low because /dev/null is guaranteed accessible on non-Windows systems, but the test does not fully validate the invariant it claims to validate.

How to fix: Replace .toThrow() with .toThrow(/out of memory|enomem|cannot allocate memory/i). The NOMEM error from bun.sys.Error.fromCode(bun.sys.E.NOMEM, .read) typically surfaces in JS as 'ENOMEM' or 'Out of memory'.

Step-by-step proof of the weakness:

  1. Suppose a future change accidentally removes the this.system_error = ... line from the catch block in read_file.zig:389.
  2. OOM is now silently swallowed — then() returns empty bytes to JS.
  3. Simultaneously, suppose /dev/null raises EPERM for some other reason before reaching the OOM path.
  4. .toThrow() sees an exception (EPERM) and passes — even though OOM is not being propagated.
  5. With .toThrow(/out of memory|enomem|cannot allocate memory/i), step 4 would fail, correctly catching the regression.

Addressing refutations: The refutation notes that the false-positive scenario (different exception + silent OOM simultaneously) is implausible for /dev/null. This is correct, which is why the severity is nit rather than normal. The bare .toThrow() is also consistent with lines 130–138 in the same file. CodeRabbit independently flagged this as 'Nitpick | Trivial' inline.

🔬 also observed by coderabbitai

@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.

Both the u52 overflow and the missing system_error assignment on OOM (which I flagged earlier) are now fixed — LGTM.

Extended reasoning...

Overview

This PR touches and . The changes are small and targeted: a single cast to prevent integer overflow in , an added assignment in the same catch block, and a new platform-conditional regression test.

Security risks

None. The fix closes an integer overflow that caused a debug-mode SIGILL trap; it does not touch auth, crypto, or permission code.

Level of scrutiny

Low. Both changes are mechanical corrections that match existing patterns in the codebase ( already used the same assignment pattern). The test is appropriately guarded with .

Other factors

The one remaining open nit — tightening the test assertion from bare to an OOM-specific predicate — was flagged in my prior inline comment and by CodeRabbit, and is documented in the thread. It is trivial and does not affect correctness of the fix. No new bugs were found by the bug hunting system.

@robobun

robobun commented Apr 8, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed four empty retriggers chasing unrelated CI failures:

  • #44445 darwin-aarch64-build-bun: cancelled mid-artifact-download (infra)
  • #44449 alpine-baseline: webview-chrome: click(selector) waits for animation to stop (known flaky — recent deflake commits on the file)
  • #44471 ubuntu-25.04-baseline: R2 should be able to upload large files in one go using Bun.write timed out + MinIO XMinioStorageFull (cloud timeouts + test-infra disk full)
  • #44487 windows-2019: DEV:stress-1: crash #18910Unhandled rejection: TypeError: self[Symbol.for(...)] is not a function at test/bake/client-fixture.mjs:154 then Client exited: Reload failed (HMR test harness)

None touch src/bun.js/webcore/blob/read_file.zig or the ReadFile async path. The fix is 2 lines (@as(usize, this.size) + 16 + system_error on OOM catch) and the regression test passes locally. Leaving this for a human to retry or merge past the flakes.

@@ -384,8 +384,9 @@ pub const ReadFile = struct {

// add an extra 16 bytes to the buffer to avoid having to resize it for trailing extra data
if (!this.could_block or (this.size > 0 and this.size != Blob.max_size))
this.buffer = std.ArrayListUnmanaged(u8).initCapacity(bun.default_allocator, this.size + 16) catch |err| {
this.buffer = std.ArrayListUnmanaged(u8).initCapacity(bun.default_allocator, @as(usize, this.size) + 16) catch |err| {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
this.buffer = std.ArrayListUnmanaged(u8).initCapacity(bun.default_allocator, @as(usize, this.size) + 16) catch |err| {
this.buffer = std.ArrayListUnmanaged(u8).initCapacity(bun.default_allocator, @min(@as(usize, this.size) + 16, bun.jsc.VirtualMachine.synthetic_allocation_limit)) catch |err| {

@robobun

robobun commented Apr 14, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #29284, which fixes the root cause (caps the initial buffer for non-regular files at 4096 so the read actually succeeds instead of rejecting with ENOMEM) and also covers the Windows ReadFileUV path.

@robobun robobun closed this Apr 14, 2026
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.

2 participants