Skip to content

Bun.mmap: throw RangeError for files > 4 GiB instead of aborting - #34119

Closed
robobun wants to merge 3 commits into
mainfrom
farm/f596f2c2/mmap-4gib-range-error
Closed

robobun wants to merge 3 commits into
mainfrom
farm/f596f2c2/mmap-4gib-range-error

Conversation

@robobun

@robobun robobun commented Jul 14, 2026 •

Copy link
Copy Markdown
Collaborator

Reproduction

import { writeFileSync, truncateSync } from "node:fs";
writeFileSync("/tmp/big.bin", "");
truncateSync("/tmp/big.bin", 2 ** 32 + 1);   // sparse, no disk used
Bun.mmap("/tmp/big.bin");

Release build: silent SIGABRT, exit 134, empty stderr. Debug build:

ASSERTION FAILED: m_sizeInBytes <= (1ull << 32)
vendor/WebKit/Source/JavaScriptCore/runtime/ArrayBuffer.cpp:150 (ArrayBufferContents ctor)

The boundary is exact: 2 ** 32 maps fine, 2 ** 32 + 1 kills the process. mmap is the API you reach for on huge files, so the first >4 GiB log, VM image, or dataset takes the whole process down with no catchable error.

Cause

mmap_file in src/runtime/api/BunObject.rs passes the fstat()'d size unchecked into make_typed_array_with_bytes_no_copy. JSC ArrayBuffers are hard-capped at MAX_ARRAY_BUFFER_SIZE (1ull << 32, JavaScriptCore/runtime/PageCount.h), enforced by RELEASE_ASSERT in the ArrayBufferContents constructor.

Fix

After bun_sys::mmap_file returns, check map.len() against the limit. On overflow, munmap the region and throw a RangeError that points the user at the existing { size } option:

RangeError: File is too large to mmap: 4294967297 bytes exceeds the maximum
typed array size (4294967296 bytes). Pass { size } to map a smaller range.

The check is placed after the mmap (rather than before) because bun_sys::mmap_file does the fstat internally; checking the returned map.len() also covers an explicit { size } that exceeds the cap after being clamped to the file length.

Related: #33353 applies the same bound to bun:ffi's toArrayBuffer/toBuffer, a different entry point to the same JSC assertion.

Verification

New test in test/js/bun/util/mmap.test.js creates a sparse 2 ** 32 + 1 byte file and spawns a child that:

  • Bun.mmap(f) with no options: must throw RangeError mentioning 4294967297
  • Bun.mmap(f, { size: 2 ** 32 }): exactly at the limit, must succeed with .length === 4294967296
  • Bun.mmap(f, { size: 4096 }): capped, must succeed

Fails on the released binary with signalCode: "SIGABRT" / exitCode: 134:

USE_SYSTEM_BUN=1 bun test test/js/bun/util/mmap.test.js -t "4 GiB"
  {
-   exitCode: 0,          +   exitCode: 134,
-   signalCode: null,     +   signalCode: "SIGABRT",
    ...
  }

Passes on the debug build (all 9 mmap tests green).


no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/util/mmap.test.js

…rting

JSC ArrayBuffers are hard-capped at MAX_ARRAY_BUFFER_SIZE (1<<32 bytes,
JavaScriptCore/runtime/PageCount.h), enforced by RELEASE_ASSERT in the
ArrayBufferContents constructor. mmap_file passed the fstat()'d size
straight into make_typed_array_with_bytes_no_copy with no bound check,
so mapping any file one byte past 4 GiB aborted the whole process with
no catchable error.

Check map.len() against the limit after the mmap succeeds; on overflow,
munmap and throw a RangeError that points at the {size} option.
@robobun

robobun commented Jul 14, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:27 AM PT - Jul 14th, 2026

❌ @robobun, your commit b3282fc has 2 failures in Build #72722 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34119

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

bun-34119 --bun

@coderabbitai

coderabbitai Bot commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in: 4 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

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.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cc6424a9-7609-4cfb-ae57-900a8bda3b9d

📥 Commits

Reviewing files that changed from the base of the PR and between 3d4ce5f and b3282fc.

📒 Files selected for processing (1)
  • test/js/bun/util/mmap.test.js

Walkthrough

Changes

mmap safety

Layer / File(s) Summary
Oversized mapping guard
src/runtime/api/BunObject.rs
mmap_file caps mappings at MAX_ARRAY_BUFFER_SIZE, unmaps oversized regions, and returns a descriptive RangeError.
Large-file regression coverage
test/js/bun/util/mmap.test.js
A spawned Bun process verifies oversized-file errors, 4 GiB boundary support, smaller explicit mappings, and cleanup behavior.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: Bun.mmap now throws RangeError instead of aborting for >4 GiB files.
Description check ✅ Passed The description covers the change and verification thoroughly, though it uses custom headings instead of the template's exact section titles.
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.

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

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/api/BunObject.rs`:
- Around line 1844-1852: Update the oversized-mapping cleanup in the error path
of the surrounding mmap function to handle the result of sys::munmap instead of
discarding it. Preserve the RangeError return, but log any unmap failure with
scoped_log! and include the failure details for diagnostics.

In `@test/js/bun/util/mmap.test.js`:
- Around line 89-125: Remove the generic stderr content assertion from the mmap
subprocess test after the existing stdout, signalCode, and exitCode
expectations. Keep awaiting proc.stderr.text() so stderr remains drained, and
rely on the process outcome fields for abort detection.
🪄 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: f1caaf99-c3da-498d-843a-cb4f6c6e0420

📥 Commits

Reviewing files that changed from the base of the PR and between 16c5576 and 2944959.

📒 Files selected for processing (2)
  • src/runtime/api/BunObject.rs
  • test/js/bun/util/mmap.test.js

Comment thread src/runtime/api/BunObject.rs
Comment thread test/js/bun/util/mmap.test.js Outdated
signalCode: null and exitCode: 0 in the toEqual already detect any abort;
the stderr check is redundant and can false-positive on benign debug output.
Comment thread test/js/bun/util/mmap.test.js Outdated

@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 — the stderr-marker nit from my earlier pass is addressed in b3282fc, and the remaining change is a straightforward bounds check that turns a JSC RELEASE_ASSERT abort into a catchable RangeError.

What was reviewed:

  • Boundary is > (1 << 32), matching JSC's m_sizeInBytes <= (1ull << 32) assert — exactly 4 GiB still succeeds, and the test's at-limit case covers it.
  • Error path munmaps before throwing; let _ = matches the existing munmap_dealloc convention directly below.
  • Test spawns a subprocess so a SIGABRT regression fails the assertion (signalCode: null / exitCode: 0) rather than the runner; stderr is drained concurrently but not asserted on.
Extended reasoning...

Overview

Two files: a 13-line addition to mmap_file in src/runtime/api/BunObject.rs that checks map.len() against JSC's MAX_ARRAY_BUFFER_SIZE (1 << 32) after the mmap succeeds, munmaps and throws a RangeError on overflow; and a new subprocess test in test/js/bun/util/mmap.test.js covering >4 GiB (throws), exactly 4 GiB (succeeds), and a capped { size: 4096 } (succeeds).

Security risks

None. This adds a defensive size check to a local-file mmap path that previously aborted the process. It strictly narrows the reachable behavior (abort → catchable error) and does not expose any new surface, change permissions, or handle untrusted network input.

Level of scrutiny

Low-to-moderate. The Rust change is a single guarded early-return with cleanup, following the exact pattern already used a few lines below (munmap_dealloc also discards the munmap result with let _ =). I verified: the constant matches JSC's MAX_ARRAY_BUFFER_SIZE (also referenced in src/jsc/bindings/JSBuffer.{h,cpp}); the comparison operator (>) matches the JSC assert's inclusive bound (<=); sys::munmap's signature (*mut u8, usize) matches the call; and sys / create_range_error_instance are already in scope. On 64-bit targets (all Bun targets) 1usize << 32 is well-defined.

Other factors

All prior review threads are resolved: my earlier nit about the .not.toContain("ASSERTION FAILED") assertion was addressed in b3282fc (stderr is now drained via Promise.all but not asserted on), and the CodeRabbit munmap-logging suggestion was withdrawn after the author pointed out it matches the existing munmap_dealloc convention and cannot realistically fail on a just-returned mapping. The test uses a sparse file (no disk cost), spawns a child so a regressed SIGABRT surfaces as signalCode: "SIGABRT" in the toEqual rather than killing the runner, and matches the file's existing tmpdirSync/bunExe/bunEnv conventions. The bug hunting system found no issues.

@robobun

robobun commented Jul 14, 2026 •

Copy link
Copy Markdown
Collaborator Author

CI status on b3282fc (build #72722, finished: 284 passed, 2 failed):

  • test/js/bun/util/mmap.test.js passed on every lane.
  • cargo clippy (GitHub Actions) is red on src/runtime/napi/napi_body.rs:2811 (undocumented_unsafe_blocks), introduced on main by 73b6c14 (napi: keep threadsafe functions alive after their env is torn down #34067). Failing across all branches; this diff doesn't touch that file.
  • test/cli/run/no-orphans.test.ts timed out after 30s on darwin 26 aarch64 (bun run --no-orphans (perl): fast-exit intermediate ... daemon still reaped). Process-reaping test, unrelated to Bun.mmap.
  • test/js/third_party/grpc-js/test-tonic.test.ts failed on darwin 14 aarch64 with rustup could not choose a version of cargo to run, because one wasn't specified explicitly, and no default is configured. CI machine configuration issue.
  • All other annotations are [flaky] (passed on retry).

Ready for review.

Jarred-Sumner pushed a commit that referenced this pull request Aug 18, 2026
…rayBuffer limit (#39558)

### Problem
- `await Bun.file(path).arrayBuffer()` on a file of exactly 2^32 bytes
reads the whole file and then crashes with `panic: int cast:
TryFromIntError(PosOverflow)`. The same happens through `new
Response(Bun.file(path)).arrayBuffer()`. Bun 1.3.14 returns the
4294967296-byte ArrayBuffer, so this is a regression of the Rust port.
- Cause: `ArrayBuffer::from_bytes` and `from_owned_bytes`
(`src/jsc/array_buffer.rs:392`) convert the length through `u32` with
`expect` before they store it in a `usize` field. The read path reaches
them from `Blob.rs:3074` with the bytes it read.
- Behind that panic sits a second failure. JSC's
`ArrayBuffer::createFromBytes` RELEASE_ASSERTs when it is given more
than `MAX_ARRAY_BUFFER_SIZE` (2^32) bytes. `Bun.mmap()` of a file larger
than 4 GiB aborts there today, and a file of 2^32 + 1 bytes would abort
there once the panic is gone.

### Fix
- `from_bytes` and `from_owned_bytes` store the length as is. A file of
exactly 2^32 bytes is returned whole again.
- `Bun::rejectBytesNoCopyAboveArrayBufferLimit` (`JSBuffer.cpp`) rejects
a length above `MAX_ARRAY_BUFFER_SIZE` with the `RangeError: Out of
memory` that `new ArrayBuffer(2 ** 32 + 1)` throws. The four functions
that adopt bytes from Rust call it before `createFromBytes`:
`Bun__makeArrayBufferWithBytesNoCopy` and
`Bun__makeTypedArrayWithBytesNoCopy` (every `ArrayBuffer::to_js*` call,
and `Bun.mmap`), `JSBuffer__bufferFromPointerAndLengthAndDeinit`
(`create_buffer*`, `to_node_buffer`) and `JSBuffer__fromMmap`
(`to_js_buffer_from_memfd`).
- The caller has already handed the bytes over, so the helper runs the
deallocator before it throws. That frees the read buffer, unmaps the
mapping, or drops the Blob store reference, exactly as a collection
would. The typed array function already ran the deallocator when
creation failed after `createFromBytes`, and the Buffer function already
runs it for an empty length, so no caller releases the bytes twice. The
doc comments on the Rust wrappers state this.
- `ArrayBuffer__fromSharedMemfd` returns an empty value for such a
length. Its only caller (`Blob.rs`, the `Clone` arm) then falls back to
the copying allocation, which already throws the same RangeError.
- Verified with `test/js/web/fetch/blob-oom.test.ts` ("at the 4 GiB
ArrayBuffer limit"): a sparse file of 2^32 bytes comes back whole, and a
file of 2^32 + 1 bytes rejects with the RangeError. On main both cases
die with the panic above after reading 4 GiB. The block skips below 10
GiB of RAM and on Windows, where the file reader itself rejects files of
2^32 bytes or more with ENOMEM (`read_file.rs`, `ReadFileUV`). Each case
has a 120 s timeout, like the 2 GiB cases in the same file (about 6 s
each in a debug build here). The two existing 2 GiB cases get the 90 s
timeout they already needed on loaded debug builds.
- Verified with `test/js/bun/util/mmap.test.js`: a sparse file of 2^32 +
4096 bytes throws a RangeError from `Bun.mmap(file)` and from `{ size: 2
** 32 + 1 }`, while `{ size: 2 ** 32 }` still maps. On main the first
call aborts with SIGABRT. This case costs address space only.
- The two Buffer entry points have no test of their own. The only way to
reach them with such a length is more than 4 GiB of piped subprocess
output (`to_js_buffer_from_memfd` is not reachable from JS today: a
buffer or blob is not accepted as stdout). They call the same helper the
two tests above exercise.
- `bun bd test` on `blob.test.ts`, `blob-cow.test.ts`, `body.test.ts`,
`zstd.test.ts`, `ffi.test.js`, `spawn.test.ts` and `buffer.test.js`: all
pass.
- Related open PRs. #33353 removes the same `u32` conversion for
`bun:ffi` and adds a check in the FFI layer. #34119 adds a `Bun.mmap`
message with the byte count in `BunObject.rs`. #37243 covers in-memory
string and `InternalBlob` bodies, which take the separate
`fromDefaultAllocator` path. All three still apply on top of this
change. The mmap test here only checks the error name, so it holds with
or without #34119.

### Background
A Blob that is backed by a file is read on a thread into a Rust buffer.
To return it as an `ArrayBuffer` without a copy, Bun builds an
`ArrayBuffer` descriptor (`ptr`, `len`) and hands it to JSC through
`Bun__makeArrayBufferWithBytesNoCopy`. JSC then owns the bytes and calls
the deallocator that was passed along when the object is collected. JSC
stores the size of an ArrayBuffer as a `size_t`, but caps it at
`MAX_ARRAY_BUFFER_SIZE` (`PageCount.h`, 2^32 on 64-bit). Its allocating
constructors return null above the cap, and Bun turns that into
`RangeError: Out of memory`. The adopting constructor used for zero-copy
hand-offs asserts instead, so the check has to happen in Bun before the
hand-off. The `u32` conversion is the port of a `@as(u32,
@intcast(len))` in the Zig version, which had the same `usize` fields.
In a release build that cast was unchecked, and in practice the value
passed through, which is why 1.3.14 returned the buffer. The port made
it a checked conversion, so the same input now panics.

<!-- robobun:evidence:begin -->

---

**no test proof** · iteration 1 · Platform-specific test(s) that do not
run on this machine. Deferring to CI, which covers all platforms:
test/js/bun/util/mmap.test.js test/js/web/fetch/blob-oom.test.ts

<!-- robobun:evidence:end -->
@robobun

robobun commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

The abort is fixed on main by #39558 (92fa5e4). That change rejects every zero-copy hand-off above the 4 GiB ArrayBuffer limit with a RangeError. Bun.mmap goes through Bun__makeTypedArrayWithBytesNoCopy, so it is covered. #39558 also adds a test for this case to test/js/bun/util/mmap.test.js.

Verified on a debug build of main (32e8703) with the reproduction from this PR (a sparse file of 2^32 + 1 bytes):

  • Bun.mmap(file) throws RangeError: Out of memory. The process exits with code 0.
  • Bun.mmap(file, { size: 2 ** 32 + 1 }) throws the same RangeError.
  • Bun.mmap(file, { size: 2 ** 32 }) still maps 4294967296 bytes.
  • bun bd test test/js/bun/util/mmap.test.js passes (21 tests).
  • The test from this PR, run as is against main, fails only on the message text. It expects the byte count in the message, and main throws Out of memory, which is also what new ArrayBuffer(2 ** 32 + 1) throws.

The only remaining difference in this PR is the more specific error message. Closing as superseded.

@robobun robobun closed this Aug 19, 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.

1 participant