From 61f26c66e50581e0b7137862a22b74a4b3ce5103 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 15 Jun 2026 09:08:11 +0000 Subject: [PATCH] node:fs: skip zero-fill of 256 KB pre-stat buffer in async readFile The async flavor of readFileWithOptions allocates a 256 KB scratch buffer for the read-before-stat optimization. The Zig port used a comptime-sized uninitialised stack array; the Rust port heap-allocates (unavoidable, the array size depends on `flavor`), but used `vec![0u8; 256*1024]`, which zero-fills the slab on every call even though it is write-only (immediately passed to `Syscall::read` and only the kernel-filled prefix is ever observed). Switch to `Vec::with_capacity` via `try_reserve_exact` + `VecExt::expand_to_capacity`, matching the pattern already used for the write-only slab in `copy_file_using_read_write_loop` and the post-stat read loop later in the same function. On allocation failure the buffer is left empty and the pre-stat loop is skipped, falling through to fstat (same as sync-without-VM already does). --- src/runtime/node/node_fs.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/runtime/node/node_fs.rs b/src/runtime/node/node_fs.rs index bc869fbd776..a15a57ffdc8 100644 --- a/src/runtime/node/node_fs.rs +++ b/src/runtime/node/node_fs.rs @@ -7138,12 +7138,18 @@ impl NodeFS { // The sync case borrows `vm.rareData().pipeReadBuffer()` (a per-VM // 256 KB heap slab) when a VM is present, otherwise leaves the buffer // zero-length so the loop is skipped and we fall through to fstat. - // The async path heap-allocates a 256 KB buffer instead. - let mut async_stack_buffer: Vec = if flavor == Flavor::Sync { - Vec::new() - } else { - vec![0u8; 256 * 1024] - }; + // The async path heap-allocates a 256 KB buffer instead (Zig used a + // comptime-sized stack array; Rust cannot size a stack array on + // `flavor`, so heap is forced, but the slab stays uninitialised: it + // is write-only, `Syscall::read` hands it straight to the kernel). + use bun_collections::vec_ext::VecExt as _; + let mut async_stack_buffer: Vec = Vec::new(); + if flavor != Flavor::Sync && async_stack_buffer.try_reserve_exact(256 * 1024).is_ok() { + // SAFETY: `u8` has no validity invariant; the buffer is handed + // straight to the kernel which only stores into it. Only the + // `[..total]` prefix actually filled by `read` is ever observed. + unsafe { async_stack_buffer.expand_to_capacity() }; + } let pre_stat_buf: &mut [u8] = if flavor == Flavor::Sync { match self.vm { // SAFETY: `self.vm` is the live owning `*mut VirtualMachine`; @@ -7285,7 +7291,6 @@ impl NodeFS { // specialisation), which dominated `readFileSync` of large files. Use // `VecExt::expand_to_capacity` (the tail is write-only — `Syscall::read` // hands it straight to the kernel, which only stores into it). - use bun_collections::vec_ext::VecExt as _; // SAFETY: `u8` has no validity invariant; the buffer is handed straight // to the kernel which only stores into it. unsafe { buf.expand_to_capacity() };