Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
82 changes: 69 additions & 13 deletions src/runtime/node/node_fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,16 @@
#[cfg(windows)]
use bun_sys::sys_uv as Syscall;

// Kernel limit on iovec count for a single readv(2)/writev(2). libuv's
// `uv__getiovmax()` prefers compile-time `IOV_MAX`; Linux headers spell it
// `UIO_MAXIOV`. Windows has no kernel iovec limit (sys_uv chunks internally).
#[cfg(any(target_os = "linux", target_os = "android"))]
const IOV_MAX: usize = libc::UIO_MAXIOV as usize;
#[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))]
const IOV_MAX: usize = libc::IOV_MAX as usize;
#[cfg(windows)]
const IOV_MAX: usize = core::ffi::c_uint::MAX as usize;

/// In-place RAII wrapper for a libuv `fs_t` request.
///
/// `scopeguard::guard(fs_t, |mut r| r.deinit())` is *wrong* here: its `Drop`
Expand Down Expand Up @@ -6344,7 +6354,10 @@

fn preadv_inner(&mut self, args: &args::Readv) -> Maybe<ret::Readv> {
let position = args.position.unwrap();
match Syscall::preadv(args.fd, args.buffers.buffers.as_slice(), position as i64) {
let bufs = args.buffers.buffers.as_slice();
// libuv `uv__fs_read`: cap `nbufs` at IOV_MAX and issue one syscall.
let bufs = &bufs[..bufs.len().min(IOV_MAX)];
match Syscall::preadv(args.fd, bufs, position as i64) {
Err(err) => Err(err),
Ok(amt) => Ok(ret::Readv {
bytes_read: amt as u64,
Expand All @@ -6353,7 +6366,10 @@
}

fn readv_inner(&mut self, args: &args::Readv) -> Maybe<ret::Readv> {
match Syscall::readv(args.fd, args.buffers.buffers.as_slice()) {
let bufs = args.buffers.buffers.as_slice();
// libuv `uv__fs_read`: cap `nbufs` at IOV_MAX and issue one syscall.
let bufs = &bufs[..bufs.len().min(IOV_MAX)];
match Syscall::readv(args.fd, bufs) {
Err(err) => Err(err),
Ok(amt) => Ok(ret::Readv {
bytes_read: amt as u64,
Expand All @@ -6362,7 +6378,7 @@
}

fn pwritev_inner(&mut self, args: &args::Writev) -> Maybe<ret::Write> {
let position = args.position.unwrap();
let mut position = args.position.unwrap() as i64;
// `PlatformIoVec`
// and `PlatformIoVecConst` are layout-identical (`{ *void, usize }`); the
// kernel never writes through `iov_base` for pwritev(2).
Expand All @@ -6376,25 +6392,65 @@
args.buffers.buffers.len(),
)
};
match Syscall::pwritev(args.fd, vecs, position as i64) {
Err(err) => Err(err),
Ok(amt) => Ok(ret::Write {
bytes_written: amt as u64,
}),
// libuv `uv__fs_write_all`: loop IOV_MAX-sized batches until every
// buffer is written; an error after the first batch returns the
// accumulated total instead of the error.
let mut remaining = vecs;
let mut total: u64 = 0;
while !remaining.is_empty() {
let chunk_len = remaining.len().min(IOV_MAX);
let chunk = &remaining[..chunk_len];
match Syscall::pwritev(args.fd, chunk, position) {
Err(err) if total == 0 => return Err(err),
Err(_) => break,
Ok(0) => break,
Ok(amt) => {
total += amt as u64;
position = position.wrapping_add(amt as i64);
let chunk_capacity: usize = chunk.iter().map(|b| b.len as usize).sum();
if amt < chunk_capacity {
break;
}
remaining = &remaining[chunk_len..];
}
}
}
Ok(ret::Write {
bytes_written: total,
})
}

fn writev_inner(&mut self, args: &args::Writev) -> Maybe<ret::Write> {
// The mutable iovec slice doubles as `iovec_const` for writev(2); the kernel
// never writes through `iov_base`. `PlatformIoVec` and
// `PlatformIoVecConst` are layout-identical (`{ *void, usize }`), so
// pass the slice through `Syscall::writev` as-is.
match Syscall::writev(args.fd, args.buffers.buffers.as_slice()) {
Err(err) => Err(err),
Ok(amt) => Ok(ret::Write {
bytes_written: amt as u64,
}),
// libuv `uv__fs_write_all`: loop IOV_MAX-sized batches until every
// buffer is written; an error after the first batch returns the
// accumulated total instead of the error.
let mut remaining = args.buffers.buffers.as_slice();
let mut total: u64 = 0;
while !remaining.is_empty() {
let chunk_len = remaining.len().min(IOV_MAX);
let chunk = &remaining[..chunk_len];
match Syscall::writev(args.fd, chunk) {
Err(err) if total == 0 => return Err(err),
Err(_) => break,
Ok(0) => break,
Ok(amt) => {
total += amt as u64;
let chunk_capacity: usize =
chunk.iter().map(|b| sys::platform_iovec_len(b)).sum();

Check failure on line 6443 in src/runtime/node/node_fs.rs

View workflow job for this annotation

GitHub Actions / cargo clippy

redundant closure
if amt < chunk_capacity {
break;
}
remaining = &remaining[chunk_len..];
}
}
}
Ok(ret::Write {
bytes_written: total,
})
}

pub fn readdir(&mut self, args: &args::Readdir, flavor: Flavor) -> Maybe<ret::Readdir> {
Expand Down
12 changes: 12 additions & 0 deletions src/sys/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4618,6 +4618,18 @@ pub fn platform_iovec_create(buf: &mut [u8]) -> PlatformIoVec {
}
}

#[inline]
pub const fn platform_iovec_len(iov: &PlatformIoVec) -> usize {
#[cfg(unix)]
{
iov.iov_len
}
#[cfg(windows)]
{
iov.len as usize
}
}

/// Windows `PlatformIOVecConst` — same `uv_buf_t` layout (libuv has no
/// const-buf type), with `base` typed `*const u8` so callers can build it
/// from `&[u8]` without casts.
Expand Down
111 changes: 111 additions & 0 deletions test/js/node/fs/fs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1680,6 +1680,117 @@ it("preadv", () => {
expect(buffers[2]).toEqual(new Uint8Array([10, 11, 12]));
});

describe("writev/readv with more than IOV_MAX buffers", () => {
// IOV_MAX is 1024 on Linux and macOS. Node's libuv loops writev in
// IOV_MAX-sized batches and caps readv at IOV_MAX; Bun previously passed
// the whole array to one syscall and got EINVAL for any count > 1024.
const n = 2000;
const makeWriteBufs = () => Array.from({ length: n }, (_, i) => Buffer.from([i & 0xff]));
const expectedBytes = Buffer.from(Array.from({ length: n }, (_, i) => i & 0xff));
// libuv caps readv at IOV_MAX on POSIX; Windows libuv reads every buffer.
const readvCap = isWindows ? n : 1024;

it("writevSync writes every buffer", () => {
using dir = tempDir("writev-iovmax-sync", {});
const file = join(String(dir), "out");
const fd = openSync(file, "w");
try {
expect(writevSync(fd, makeWriteBufs())).toBe(n);
} finally {
closeSync(fd);
}
expect(readFileSync(file).equals(expectedBytes)).toBe(true);
});

it("writevSync with position writes every buffer", () => {
using dir = tempDir("pwritev-iovmax-sync", {});
const file = join(String(dir), "out");
const fd = openSync(file, "w");
try {
writeSync(fd, Buffer.from("head"), 0, 4, 0);
expect(writevSync(fd, makeWriteBufs(), 4)).toBe(n);
} finally {
closeSync(fd);
}
const out = readFileSync(file);
expect(out.subarray(0, 4).toString()).toBe("head");
expect(out.subarray(4).equals(expectedBytes)).toBe(true);
});

it("fs.writev (callback) writes every buffer", async () => {
using dir = tempDir("writev-iovmax-cb", {});
const file = join(String(dir), "out");
const fd = openSync(file, "w");
try {
const { promise, resolve, reject } = Promise.withResolvers<number>();
fs.writev(fd, makeWriteBufs(), (err, written) => (err ? reject(err) : resolve(written)));
expect(await promise).toBe(n);
} finally {
closeSync(fd);
}
expect(readFileSync(file).equals(expectedBytes)).toBe(true);
});

it("FileHandle.writev writes every buffer", async () => {
using dir = tempDir("writev-iovmax-fh", {});
const file = join(String(dir), "out");
const fh = await _promises.open(file, "w");
try {
const { bytesWritten } = await fh.writev(makeWriteBufs());
expect(bytesWritten).toBe(n);
} finally {
await fh.close();
}
expect(readFileSync(file).equals(expectedBytes)).toBe(true);
});

it("readvSync caps at IOV_MAX instead of failing", () => {
using dir = tempDir("readv-iovmax-sync", {});
const file = join(String(dir), "in");
writeFileSync(file, Buffer.alloc(n, 7));
const fd = openSync(file, "r");
try {
const buffers = Array.from({ length: n }, () => Buffer.alloc(1));
expect(readvSync(fd, buffers)).toBe(readvCap);
expect(buffers[0][0]).toBe(7);
expect(buffers[readvCap - 1][0]).toBe(7);
} finally {
closeSync(fd);
}
});

it("readvSync with position caps at IOV_MAX instead of failing", () => {
using dir = tempDir("preadv-iovmax-sync", {});
const file = join(String(dir), "in");
writeFileSync(file, Buffer.concat([Buffer.from("xxx"), Buffer.alloc(n, 7)]));
const fd = openSync(file, "r");
try {
const buffers = Array.from({ length: n }, () => Buffer.alloc(1));
expect(readvSync(fd, buffers, 3)).toBe(readvCap);
expect(buffers[0][0]).toBe(7);
expect(buffers[readvCap - 1][0]).toBe(7);
} finally {
closeSync(fd);
}
});

it("FileHandle.readv caps at IOV_MAX instead of failing", async () => {
using dir = tempDir("readv-iovmax-fh", {});
const file = join(String(dir), "in");
writeFileSync(file, Buffer.alloc(n, 7));
const fh = await _promises.open(file, "r");
try {
const buffers = Array.from({ length: n }, () => Buffer.alloc(1));
const { bytesRead } = await fh.readv(buffers, 0);
expect(bytesRead).toBe(readvCap);
expect(buffers[0][0]).toBe(7);
expect(buffers[readvCap - 1][0]).toBe(7);
} finally {
await fh.close();
}
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

describe("writeSync", () => {
it("works with bigint", () => {
const dest = join(tmpdir(), "writeSync-large-file-bigint.txt");
Expand Down
Loading