Skip to content

Fix integer overflow in ReadFile buffer pre-allocation#29207

Closed
robobun wants to merge 2 commits into
mainfrom
farm/8fde255c/fix-readfile-size-overflow
Closed

Fix integer overflow in ReadFile buffer pre-allocation#29207
robobun wants to merge 2 commits into
mainfrom
farm/8fde255c/fix-readfile-size-overflow

Conversation

@robobun

@robobun robobun commented Apr 12, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes an integer overflow panic in ReadFile.runAsyncWithFD (and the equivalent spot in ReadFileUV) when computing the initial buffer capacity.

ReadFile.size is a u52. When a file Blob backed by a non-regular file (e.g. /dev/zero) is sliced to a length near Blob.max_size (maxInt(u52)) and then read, resolveSizeAndLastModified sets this.size = this.max_length, and then this.size + 16 overflows and panics in debug builds.

Minimal repro (before this change, panics with integer overflow):

await Bun.file("/dev/zero").slice(0, 4503599627370490).text();

The fix uses a saturating add (+|) so the value caps at maxInt(u52); the subsequent allocation then fails with OutOfMemory, which is already handled by the existing catch block.

How did you verify your code works?

  • Reproduced the panic with the minimal repro above against the unfixed binary; the fixed binary exits cleanly.
  • Added a regression test in test/js/bun/util/bun-file-read.test.ts that spawns a subprocess and asserts it exits with code 0 / no signal. Test fails (timeout, subprocess panics and hangs) on the unfixed binary and passes on the fixed binary.
  • Ran the original Fuzzilli crash script repeatedly against the fixed binary with no panics.

Fuzzilli fingerprint: 4540f8005402e562

When a file Blob is sliced to a length near Blob.max_size (maxInt(u52))
and then read, computing the initial buffer capacity as size + 16
overflows u52 and panics in debug builds. Use saturating add so the
allocation attempt proceeds (and fails with OOM, which is already
handled) instead of crashing.
@robobun

robobun commented Apr 12, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:49 PM PT - Apr 11th, 2026

@autofix-ci[bot], your commit d08e2ff has 1 failures in Build #45270 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 29207

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

bun-29207 --bun

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. fix integer overflow in ReadFile buffer capacity computation #28849 - Fixes the same this.size + 16 u52 overflow in ReadFile.runAsyncWithFD using saturating addition (+|)
  2. ReadFile: avoid integer overflow when computing buffer capacity #29000 - Fixes the same overflow in ReadFile.runAsyncWithFD by widening to usize before adding 16
  3. blob: avoid size overflow when allocating ReadFile buffer #29008 - Fixes the same overflow by tightening the max_size guard and casting to usize

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 02ccd362-a7b9-47dd-8921-affa9bd24cfe

📥 Commits

Reviewing files that changed from the base of the PR and between 5bd3812 and d08e2ff.

📒 Files selected for processing (1)
  • test/js/bun/util/bun-file-read.test.ts

Walkthrough

Adjusted buffer capacity sizing in file-reading code to use saturating addition for trailing-space allocations, and added a POSIX-only test that verifies reading a slice near Blob.max_size from /dev/zero exits normally.

Changes

Cohort / File(s) Summary
Blob read buffer adjustments
src/bun.js/webcore/blob/read_file.zig
Replaced standard addition with saturating addition when adding trailing capacity (use +| 16 instead of + 16) in initCapacity(...) and ensureTotalCapacityPrecise(...).
POSIX large-read test
test/js/bun/util/bun-file-read.test.ts
Added POSIX-only test harness imports and a test that spawns bun to read a sliced Bun.file("/dev/zero") at a length near Blob.max_size, asserting the process exits with code 0.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly describes the main change: fixing an integer overflow in ReadFile buffer pre-allocation, which is the core issue addressed by replacing standard addition with saturating addition in capacity sizing expressions.
Description check ✅ Passed The PR description fully addresses both required template sections: clearly explains what the PR does (fixes integer overflow panic with detailed root cause) and comprehensively documents verification (minimal repro, regression test added, Fuzzilli crash script testing).

✏️ 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.

@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 `@src/bun.js/webcore/blob/read_file.zig`:
- Around line 387-390: The allocation catch in the ArrayListUnmanaged
initCapacity path currently only sets this.errno and calls this.onFinish(), so
then() won't reject; update the catch in the non-Windows branch to also set
this.system_error (e.g., assign the allocation error or a mapped OOM
system_error to this.system_error) before calling this.onFinish() and returning,
ensuring code paths in the read handler (the this.buffer init, this.errno,
this.system_error, onFinish(), and then() rejection behavior) correctly
propagate OOM to the caller.
🪄 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: 77ca85e6-521a-4421-80b9-a1b2dd70f1c1

📥 Commits

Reviewing files that changed from the base of the PR and between b18f268 and 5bd3812.

📒 Files selected for processing (2)
  • src/bun.js/webcore/blob/read_file.zig
  • test/js/bun/util/bun-file-read.test.ts

Comment on lines +387 to 390
this.buffer = std.ArrayListUnmanaged(u8).initCapacity(bun.default_allocator, this.size +| 16) catch |err| {
this.errno = err;
this.onFinish();
return;

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.

⚠️ Potential issue | 🟠 Major

Propagate OOM to system_error on the non-Windows path.

At Line 387, the catch path sets this.errno but not this.system_error; then() rejects only when system_error exists, so allocation failure can surface as a successful empty read instead of an error.

Suggested fix
-        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.errno = err;
-                this.onFinish();
-                return;
-            };
+        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.errno = err;
+                if (err == error.OutOfMemory) {
+                    this.system_error = bun.sys.Error.fromCode(bun.sys.E.NOMEM, .read).toSystemError();
+                }
+                this.onFinish();
+                return;
+            };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun.js/webcore/blob/read_file.zig` around lines 387 - 390, The allocation
catch in the ArrayListUnmanaged initCapacity path currently only sets this.errno
and calls this.onFinish(), so then() won't reject; update the catch in the
non-Windows branch to also set this.system_error (e.g., assign the allocation
error or a mapped OOM system_error to this.system_error) before calling
this.onFinish() and returning, ensuring code paths in the read handler (the
this.buffer init, this.errno, this.system_error, onFinish(), and then()
rejection behavior) correctly propagate OOM to the caller.

@robobun

robobun commented Apr 12, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as duplicate of #28849 which has the same fix plus proper system_error propagation on OOM.

Note for whoever merges #28849: the Windows ReadFileUV path at read_file.zig:701 has the same this.size + 16 expression and should also use saturating add (it's guarded by a > maxInt(ULONG) check above so less likely to hit, but still technically reachable).

@robobun robobun closed this Apr 12, 2026
@robobun robobun deleted the farm/8fde255c/fix-readfile-size-overflow branch April 12, 2026 04:33
Comment on lines 384 to 390

// 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, this.size +| 16) catch |err| {
this.errno = err;
this.onFinish();
return;

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.

🔴 In ReadFile.runAsyncWithFD, the OOM catch block sets only this.errno but leaves this.system_error null; since then() dispatches errors solely via system_error, an OOM during buffer pre-allocation silently resolves the JS promise with an empty string instead of rejecting. Add this.system_error = bun.sys.Error.fromCode(bun.sys.E.NOMEM, .read).toSystemError(); alongside the existing this.errno = err; at line 388.

Extended reasoning...

What the bug is and how it manifests

In ReadFile.runAsyncWithFD (the POSIX/non-UV path), when initCapacity fails with OutOfMemory (now reliably triggered by the PR's saturating-add fix passing maxInt(u52) as the capacity), the catch block executes:

this.errno = err;
this.onFinish();
return;

It sets this.errno but leaves this.system_error = null. The then() dispatcher — the only path that converts the task result into a JS callback — checks exclusively this.system_error to decide whether to call the callback with an error:

const system_error = this.system_error;
// ...
if (system_error) |err| {
    cb(cb_ctx, ReadFileResultType{ .err = err });
    return;
}
cb(cb_ctx, .{ .result = .{ .buf = buf, .total_size = total_size, .is_temporary = true } });

Because system_error is null, then() falls through to the success path, calling cb with buf = this.buffer.items which is an empty slice — the allocation never succeeded. The JS promise therefore resolves with "" instead of rejecting.

The specific code path that triggers it

  1. JS calls Bun.file("/dev/zero").slice(0, 4503599627370490).text()
  2. resolveSizeAndLastModified sets this.size = this.max_length (≈ maxInt(u52))
  3. runAsyncWithFD reaches initCapacity(bun.default_allocator, this.size +| 16) — after the PR fix, this is maxInt(u52)
  4. initCapacity returns error.OutOfMemory
  5. Only this.errno = err is set; this.system_error stays null
  6. onFinish()then() → success callback with empty buffer

Why existing code doesn't prevent it

The then() function never checks this.errno at all; it is solely gated on this.system_error. This is an oversight: every other OOM handler in the same file (ReadFileUV lines ~702–703, ~731–732, ~764–765, ~791–792, and the ensureUnusedCapacity path) correctly sets both this.errno and this.system_error. Only this one catch block was left incomplete.

What the impact would be

A caller reading a large Blob on POSIX (e.g., a sliced /dev/zero) will receive an empty string from .text() / .arrayBuffer() / etc. without any error indication. This is silent data loss/corruption: code that checks the result value rather than expecting a rejection will silently process an empty string as if it were the file's contents.

How to fix it

Add the missing line inside the catch block:

this.buffer = std.ArrayListUnmanaged(u8).initCapacity(bun.default_allocator, this.size +| 16) catch |err| {
    this.errno = err;
    this.system_error = bun.sys.Error.fromCode(bun.sys.E.NOMEM, .read).toSystemError(); // <-- add this
    this.onFinish();
    return;
};

Step-by-step proof

  1. Bun.file("/dev/zero").slice(0, 4503599627370490).text() is awaited in JS.
  2. ReadFile.createWithCtx allocates a ReadFile with max_length = 4503599627370490 (~maxInt(u52)).
  3. resolveSizeAndLastModified: /dev/zero is not a regular file, stat.size == 0, so this.size = this.max_length = 4503599627370490.
  4. runAsyncWithFD: this.size != Blob.max_size (they happen to be equal only at exact maxInt(u52), but after saturating add the capacity request is still enormous) → initCapacity(allocator, 4503599627370507) is called.
  5. The allocator returns error.OutOfMemory. The catch block sets this.errno = error.OutOfMemory only.
  6. onFinish() is called; eventually then() runs on the JS thread.
  7. In then(): this.store != null → skip first two branches. system_error = this.system_error = null. if (system_error) |err| is false. cb is called with .result = .{ .buf = &[]{}, .total_size = 0, .is_temporary = true }.
  8. JS promise resolves with "" — no error thrown, no rejection.

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