Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions src/runtime/api/BunObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1864,41 +1864,47 @@ pub(crate) fn mmap_file(global_this: &JSGlobalObject, callframe: &CallFrame) ->
)));
}
offset = usize::try_from(offset_value).expect("int cast");
// Align the offset down to a page boundary.
let page = bun_sys::page_size();
offset -= offset % page;
}
} else if !opts.is_undefined_or_null() {
return Err(global_this
.throw_invalid_arguments(format_args!("Expected options to be an object")));
}
}

let map = match bun_sys::mmap_file(buf_z, flags, map_size, offset) {
Ok(map) => map,
let (map, delta) = match bun_sys::mmap_file(buf_z, flags, map_size, offset) {
Ok(result) => result,
Err(err) => {
use bun_jsc::SysErrorJsc as _;
return Err(global_this.throw_value(err.to_js(global_this)));
}
};

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::<u8>(), size as usize);
// `ptr` is `map_base + delta` where `map_base` is page-aligned and
// `delta < page_size`, so rounding down recovers the mmap base.
let page = bun_sys::page_size();
let addr = ptr as usize;
let _ = sys::munmap((addr - addr % page) as *mut u8, size as usize);
}

let map_len = map.len();
// SAFETY: `mmap_file` guarantees `map_len == view_size + delta` with
// `view_size > 0`, so `delta < map_len` and the add stays in-bounds.
let view_ptr = unsafe { map.as_ptr().add(delta) };
let view_len = map_len - delta;

// SAFETY: `map` is the live mapping `bun_sys::mmap_file` just created
// (`&'static mut [u8]`, no drop guard); ownership moves to JSC, which
// unmaps it exactly once via `munmap_dealloc` with the length stuffed
// into the ctx pointer.
// unmaps it exactly once via `munmap_dealloc` with the full mapping
// length stuffed into the ctx pointer.
unsafe {
jsc::array_buffer::make_typed_array_with_bytes_no_copy(
global_this,
jsc::TypedArrayType::TypeUint8,
map.as_ptr().cast_mut().cast::<c_void>(),
map.len(),
view_ptr.cast_mut().cast::<c_void>(),
view_len,
Some(munmap_dealloc),
map.len() as *mut c_void,
map_len as *mut c_void,
)
}
}
Expand Down
29 changes: 22 additions & 7 deletions src/sys/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3380,14 +3380,15 @@ mod posix_impl {
}

/// `bun.sys.mmapFile` — open `path` RDWR, fstat for size, mmap [offset, offset+len).
/// Returns a process-lifetime mmap slice; caller is responsible for
/// `munmap`.
/// Returns `(map, delta)` where `map` is the full page-aligned mapping and
/// `delta = offset % page_size` is the byte offset into `map` at which the
/// requested `offset` begins. Caller is responsible for `munmap(map)`.
pub fn mmap_file(
path: &ZStr,
flags: libc::c_int,
wanted_size: Option<usize>,
offset: usize,
) -> Maybe<&'static mut [u8]> {
) -> Maybe<(&'static mut [u8], usize)> {
let fd = open(path, O::RDWR, 0)?;
// close fd regardless of mmap outcome (the mapping outlives the fd).
let _close = CloseOnDrop::new(fd);
Expand All @@ -3396,22 +3397,36 @@ mod posix_impl {
let result = fstat(fd)?;
usize::try_from(result.st_size).unwrap_or(0)
};

// mmap requires a page-aligned file offset. Map from the aligned
// offset and report the delta so the caller can slice to the
// requested byte.
let page = bun_alloc::page_size();
let delta = offset % page;
let aligned_offset = offset - delta;

let mut size = stat_size.saturating_sub(offset);
if let Some(size_) = wanted_size {
size = size.min(size_);
}
// When size == 0 (offset at/past EOF or size: 0) pass 0 so mmap
// returns EINVAL instead of mapping the leading delta bytes.
let map_len = if size == 0 { 0 } else { size + delta };

match mmap(
core::ptr::null_mut(),
size,
map_len,
libc::PROT_READ | libc::PROT_WRITE,
flags,
fd,
offset as i64,
aligned_offset as i64,
) {
Ok(ptr) => {
// SAFETY: mmap returned a valid mapping of `size` bytes.
Ok(unsafe { core::slice::from_raw_parts_mut(ptr, size) })
// SAFETY: mmap returned a valid mapping of `map_len` bytes.
Ok((
unsafe { core::slice::from_raw_parts_mut(ptr, map_len) },
delta,
))
}
Err(err) => Err(err),
}
Expand Down
65 changes: 63 additions & 2 deletions test/js/bun/util/mmap.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from "bun:test";
import { gcTick, isWindows, tmpdirSync } from "harness";
import { afterAll, describe, expect, it } from "bun:test";
import { gcTick, isWindows, tempDir, tmpdirSync } from "harness";
import { writeFileSync } from "node:fs";
import { join } from "path";

// TODO: We do not support mmap() on Windows. Maybe we can add it later.
Expand Down Expand Up @@ -69,6 +70,66 @@ describe.skipIf(isWindows)("Bun.mmap", async () => {
await gcTick();
});

describe("mmap offset returns bytes starting at the requested position", () => {
const fileSize = 4096 * 3;
const buf = Buffer.alloc(fileSize);
for (let i = 0; i < buf.length; i++) buf[i] = i & 0xff;
const dir = tempDir("mmap-offset", {});
const file = join(String(dir), "data.bin");
writeFileSync(file, buf);
Comment thread
robobun marked this conversation as resolved.
afterAll(() => dir[Symbol.dispose]());

it.each([
{ offset: 0, size: undefined },
{ offset: 1, size: undefined },
{ offset: 100, size: undefined },
{ offset: 4095, size: undefined },
{ offset: 4096, size: undefined },
{ offset: 4097, size: undefined },
{ offset: 100, size: 200 },
{ offset: 4097, size: 10 },
{ offset: 1, size: fileSize * 2 },
])("offset=$offset size=$size", async ({ offset, size }) => {
const map = Bun.mmap(file, size === undefined ? { offset } : { offset, size });
const wantLen = Math.min(fileSize - offset, size ?? Infinity);
expect({ length: map.length, first: map[0], last: map[map.length - 1] }).toEqual({
length: wantLen,
first: buf[offset],
last: buf[offset + wantLen - 1],
});
expect(Buffer.from(map).equals(buf.subarray(offset, offset + wantLen))).toBe(true);
await gcTick();
});
});

it("mmap offset with shared mapping writes land at the requested position", () => {
using dir = tempDir("mmap-offset-write", {});
const file = join(String(dir), "data.bin");
writeFileSync(file, Buffer.alloc(4096 * 2, 0));

const map = Bun.mmap(file, { offset: 100, shared: true });
map[0] = 0xab;
map[1] = 0xcd;

const full = Bun.mmap(file);
expect({ at0: full[0], at99: full[99], at100: full[100], at101: full[101] }).toEqual({
at0: 0,
at99: 0,
at100: 0xab,
at101: 0xcd,
});
});

it("mmap offset past EOF throws EINVAL", () => {
using dir = tempDir("mmap-offset-eof", {});
const file = join(String(dir), "data.bin");
writeFileSync(file, Buffer.alloc(50, 0));

expect(() => Bun.mmap(file, { offset: 100 })).toThrow("EINVAL");
expect(() => Bun.mmap(file, { offset: 50 })).toThrow("EINVAL");
expect(() => Bun.mmap(file, { offset: 1, size: 0 })).toThrow("EINVAL");
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it("mmap rejects negative offset", () => {
expect(() => Bun.mmap(path, { offset: -1 })).toThrow("offset must be a non-negative integer");
});
Expand Down
Loading