Fix integer overflow when reading a sliced non-regular file Blob#29293
Fix integer overflow when reading a sliced non-regular file Blob#29293robobun wants to merge 2 commits into
Conversation
Bun.file("/dev/null").slice(1).arrayBuffer() would panic in debug builds.
Slicing a file Blob with unknown size yields a Blob with size close to
Blob.max_size. For non-regular files with stat.size == 0, ReadFile used
this value directly as the initial buffer capacity, and `size + 16`
overflowed u52.
Cap the initial buffer size for non-regular files to 4k (same as the
unsliced case — the read loop grows the buffer via its 64KB stack
buffer as needed), and use saturating add for defense in depth.
|
Updated 10:09 PM PT - Apr 13th, 2026
❌ @autofix-ci[bot], your commit 116b50d has some failures in 🧪 To try this PR locally: bunx bun-pr 29293That installs a local version of the PR into your bun-29293 --bun |
|
Caution Review failedPull request was closed or merged during review WalkthroughRefactored blob file reading logic to consistently cap 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 #29284 — same root cause, same fix in Fuzzer fingerprint |
There was a problem hiding this comment.
LGTM — clean, minimal crash fix with a good regression test.
Extended reasoning...
Overview
The PR fixes an integer overflow panic in ReadFile.runAsyncWithFD (and the Windows ReadFileUV counterpart). Two files are changed: the Zig read-file implementation and a TypeScript test file.
Root Cause & Fix Correctness
The old ternary if (this.max_length == Blob.max_size) 4096 else this.max_length was intended to use 4096 for unsliced blobs and the user's slice length for sliced blobs. But .slice(1) on an unknown-size blob yields max_length = max_size - 1 (~4.5 PB), making the else branch pass a near-maximum u52 value as the initial buffer capacity. The subsequent + 16 then overflowed. The replacement @min(this.max_length, 4096) is strictly equivalent for the unsliced case (@min(max_size, 4096) == 4096) and correctly bounds small explicit slices (e.g., .slice(0, 100) → @min(100, 4096) == 100), fixing the overflow for all inputs.
Security Risks
None. This is a crash/panic fix with no auth, crypto, or data-exposure implications. The saturating add (+|) on the regular-file path is a sound defense-in-depth measure.
Level of Scrutiny
Low — the change is three deleted lines replaced by one @min call, applied identically in two symmetric locations. The logic is straightforward, the comment in the Windows path explicitly says to keep it in sync, and the PR does so. The regression test is appropriate and spawns a child process to validate both /dev/null and /dev/zero paths.
Other Factors
No CODEOWNER concerns, no design decisions, no outstanding reviewer comments. The fix is a direct response to a Fuzzilli-found crash with a clear fingerprint.
Fuzzilli found a crash (fingerprint
851b2e15154d15ae) inReadFile.runAsyncWithFDatthis.size + 16.Root cause
Bun.file(path).slice(1)on a file Blob with unknown size (Blob.max_size) produces a sliced Blob withsize = max_size - 1. When reading a non-regular file (char device, pipe, etc.) withstat.size == 0,resolveSizeAndLastModifiedsetthis.size = this.max_lengthwhenevermax_length != Blob.max_size— intending to use the user's explicit slice length as a buffer size hint. But after.slice(1),max_lengthismax_size - 1(~4.5 PB), and the subsequentthis.size + 16overflowedu52and panicked.Repro
Fix
this.sizehere is only the initial buffer capacity — the read loop grows it as needed via a 64 KB stack buffer, and the actual read limit ismax_length. Cap the initial capacity to@min(max_length, 4096), which matches what already happens in the unsliced case and avoids trying to pre-allocate petabytes.Also use saturating add (
+|) on the+ 16for the regular-file path as defense in depth.Applied the same
@minfix to the WindowsReadFileUVpath (which the comment says to keep in sync) — there it didn't panic but returned a spuriousENOMEM.