diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index 2547980b2d02..0ed281f5e9d9 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -1838,6 +1838,19 @@ pub(crate) fn mmap_file(global_this: &JSGlobalObject, callframe: &CallFrame) -> } }; + // JSC's `MAX_ARRAY_BUFFER_SIZE` (JavaScriptCore/runtime/PageCount.h). + // ArrayBufferContents RELEASE_ASSERTs this, so anything larger aborts. + const MAX_ARRAY_BUFFER_SIZE: usize = 1 << 32; + if map.len() > MAX_ARRAY_BUFFER_SIZE { + let len = map.len(); + let _ = sys::munmap(map.as_ptr().cast_mut(), len); + let err = global_this.create_range_error_instance(format_args!( + "File is too large to mmap: {} bytes exceeds the maximum typed array size ({} bytes). Pass {{ size }} to map a smaller range.", + len, MAX_ARRAY_BUFFER_SIZE, + )); + return Err(global_this.throw_value(err)); + } + extern "C" fn munmap_dealloc(ptr: *mut c_void, size: *mut c_void) { // SAFETY: ptr is the original mmap base, size is its length stuffed into a pointer. let _ = sys::munmap(ptr.cast::(), size as usize); diff --git a/test/js/bun/util/mmap.test.js b/test/js/bun/util/mmap.test.js index 6d507d3b968e..dc7916962803 100644 --- a/test/js/bun/util/mmap.test.js +++ b/test/js/bun/util/mmap.test.js @@ -1,5 +1,6 @@ import { describe, expect, it } from "bun:test"; -import { gcTick, isWindows, tmpdirSync } from "harness"; +import { bunEnv, bunExe, gcTick, isWindows, tmpdirSync } from "harness"; +import { truncateSync, unlinkSync, writeFileSync } from "node:fs"; import { join } from "path"; // TODO: We do not support mmap() on Windows. Maybe we can add it later. @@ -85,6 +86,43 @@ describe.skipIf(isWindows)("Bun.mmap", async () => { expect(() => Bun.mmap(path, null)).not.toThrow(); }); + it("mmap file > 4 GiB throws RangeError instead of aborting", async () => { + // Sparse file: truncate() to 4 GiB + 1 uses no disk space. + const dir = tmpdirSync(); + const big = join(dir, "big.bin"); + writeFileSync(big, ""); + truncateSync(big, 2 ** 32 + 1); + try { + // Spawned so a regression (SIGABRT) fails the test rather than the runner. + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `const f = ${JSON.stringify(big)}; + try { Bun.mmap(f); console.log("no throw"); } + catch (e) { console.log("threw", e.name, e.message); } + // exactly 4 GiB must still succeed + console.log("at-limit", Bun.mmap(f, { size: 2 ** 32 }).length); + // and a capped size on the same file works + console.log("capped", Bun.mmap(f, { size: 4096 }).length);`, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + const lines = stdout.trim().split("\n"); + expect({ lines, signalCode: proc.signalCode, exitCode }).toEqual({ + lines: [expect.stringMatching(/^threw RangeError .*4294967297/), "at-limit 4294967296", "capped 4096"], + signalCode: null, + exitCode: 0, + }); + } finally { + unlinkSync(big); + } + }); + it("mmap handles non-number offset/size without crashing", () => { // These should not crash - non-number values coerce to 0 per JavaScript semantics // Previously these caused assertion failures (issue ENG-22413)