Fix integer overflow in ReadFile buffer preallocation#29254
Conversation
When a file Blob backed by a non-regular file (pipe, character device) is sliced with an end value close to maxInt(u52), the max_length is propagated to this.size, and adding 16 bytes of slop for the initial capacity overflows the u52 SizeType. Use saturating addition so the allocation attempt fails cleanly with OutOfMemory instead of panicking.
|
Updated 4:09 AM PT - Apr 13th, 2026
❌ @autofix-ci[bot], your commit bfa7938 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 29254That installs a local version of the PR into your bun-29254 --bun |
|
Caution Review failedPull request was closed or merged during review WalkthroughModified buffer capacity calculation in file read operations to use saturating addition instead of plain addition to handle integer overflow, and added a test case for large file read operations via spawned Bun process. Changes
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
Duplicate of #28849 (same saturating-add fix on the same line). Closing in favor of the earlier PR. |
|
|
||
| // 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; |
There was a problem hiding this comment.
🔴 When fails with OOM in , the catch block sets only but not , causing to silently return an empty instead of rejecting the JS promise. The fix is to also set in the catch block, mirroring the Windows () path which correctly sets both fields.
Extended reasoning...
What the bug is and how it manifests
In ReadFile.runAsyncWithFD (src/bun.js/webcore/blob/read_file.zig, lines 387–390), when initCapacity fails with OutOfMemory, the catch block only assigns this.errno = err but never sets this.system_error. However, the then() function (line ~261) exclusively checks this.system_error to decide whether to call the callback with an error. Since system_error remains null, then() falls through to the success path and returns an empty ArrayBuffer to JavaScript with no indication that anything went wrong.
The specific code path that triggers it
The PR introduces saturating addition (+|) to fix the integer overflow panic. Before, this.size + 16 would overflow and panic in debug or wrap in release — either way, this specific error path was rarely reached in practice. After the fix, a near-maxInt(u52) slice value passes through saturating addition to initCapacity, which then legitimately fails with OutOfMemory. The catch block fires, sets only errno, calls onFinish(), and returns early. Later, then() reads this.system_error (null) and dispatches success.
Why existing code doesn't prevent it
Every other error path in ReadFile sets both errno and system_error together (e.g., lines 146–147, 216–217, 316–317, 329–330 in the unmodified file). Only this one catch block is inconsistent. The ReadFileUV (Windows path) equivalent is correctly written: it sets both this.errno = error.OutOfMemory and this.system_error = bun.sys.Error.fromCode(bun.sys.E.NOMEM, .read).toSystemError() in its ensureTotalCapacityPrecise catch block.
What the impact would be
On Linux/macOS, a caller doing await Bun.file('/dev/stdin').slice(0, 2**52 - 2).arrayBuffer() (or any non-regular file with actual data) would receive an empty ArrayBuffer with exitCode 0 instead of a rejected promise or thrown error. The test added in this PR uses /dev/null which always returns 0 bytes anyway, so the silent empty result happens to be correct — this masks the bug in CI.
How to fix it
In the initCapacity catch block, add the missing assignment before this.onFinish():
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();
this.onFinish();
return;
};Step-by-step proof
- User calls
await Bun.file('/dev/stdin').slice(0, 2**52 - 2).arrayBuffer()on Linux. resolveSizeAndLastModifiedsees a non-regular file (could_block = true) and setsthis.size = this.max_length = 2^52 - 2.runAsyncWithFDenters the buffer pre-allocation branch:this.size +| 16saturates to2^52 - 2 + 16 = 2^52 + 14(or clamps to maxInt), which is passed toinitCapacity.initCapacityfails withOutOfMemory; the catch block setsthis.errno = error.OutOfMemorybut leavesthis.system_error = null.onFinish()is called, eventually dispatchingthen().- In
then():const system_error = this.system_error;→null. Theif (system_error) |err|branch is skipped. cb(cb_ctx, .{ .result = .{ .buf = buf, .total_size = total_size, .is_temporary = true } })is called withbuf = &[](empty), resolving the promise successfully with an empty buffer.
Fuzzilli found a debug panic (
integer overflow) inReadFile.runAsyncWithFD. Fingerprint:9cb0cbabf00a5721.What
When reading a file Blob backed by a non-regular file (pipe, character device) that has been sliced with an end value close to
maxInt(u52),this.sizeis set tomax_lengthand thenthis.size + 16overflows theu52SizeTypewhen computing the initial buffer capacity.How
Use saturating addition (
+|) so the allocation request is passed through to the allocator, which will fail cleanly withOutOfMemory(handled by the existingcatch) instead of panicking on overflow.