Skip to content

node:fs: skip zero-fill of 256 KB pre-stat buffer in async readFile#32295

Open
robobun wants to merge 1 commit into
mainfrom
farm/36fd2698/readfile-async-buf-uninit
Open

node:fs: skip zero-fill of 256 KB pre-stat buffer in async readFile#32295
robobun wants to merge 1 commit into
mainfrom
farm/36fd2698/readfile-async-buf-uninit

Conversation

@robobun

@robobun robobun commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

What

The async flavor of readFileWithOptions allocates a 256 KB scratch buffer for the read-before-stat optimization. In Zig (node_fs.zig:5193) this was a comptime-sized uninitialised stack array:

var async_stack_buffer: [if (flavor == .sync) 0 else 256 * 1024]u8 = undefined;

The Rust port (node_fs.rs:7145) cannot size a stack array on flavor, so it heap-allocates, but used vec![0u8; 256 * 1024], which also zero-fills the slab on every async fs.readFile / fs.promises.readFile call. The buffer is write-only: it is handed straight to Syscall::read and only the [..total] prefix the kernel filled is ever observed. The 256 KB memset is wasted work the Zig version did not do.

Fix

Allocate via try_reserve_exact + VecExt::expand_to_capacity, matching the uninit-slab pattern already used in copy_file_using_read_write_loop (node_fs.rs:4896) 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 behaviour as the sync-without-VM path already has).

Verification

bun bd test test/js/node/fs/fs.test.ts -t readFile
# 20 pass, 1 skip (windows), 0 fail

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).
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

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

More reviews will be available in 4 minutes and 13 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

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.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dbd60196-2212-455a-b14d-400088a55d60

📥 Commits

Reviewing files that changed from the base of the PR and between e0acad3 and 61f26c6.

📒 Files selected for processing (1)
  • src/runtime/node/node_fs.rs

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

@robobun

robobun commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 4:31 AM PT - Jun 15th, 2026

@robobun, your commit 61f26c6 has 3 failures in Build #62420 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32295

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

bun-32295 --bun

@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.

No issues found — the change mirrors the exact try_reserve_exact + expand_to_capacity pattern already used a few lines below (and in copy_file_using_read_write_loop), but since it adds a new unsafe block in the async readFile hot path it's worth a quick human glance.

Extended reasoning...

Overview

This PR replaces vec![0u8; 256 * 1024] with Vec::new() + try_reserve_exact + unsafe { expand_to_capacity() } for the pre-stat scratch buffer in the async branch of readFileWithOptions (src/runtime/node/node_fs.rs:7145-7152), and hoists the use bun_collections::vec_ext::VecExt import up so it covers both call sites. Net change is ~10 lines in a single function.

Correctness

I traced the buffer's lifetime: pre_stat_buf is sliced from async_stack_buffer and handed to Syscall::read(fd, available) in a loop; only &pre_stat_buf[..total] (the kernel-filled prefix) is ever read afterward. The SAFETY comment is accurate. On try_reserve_exact failure the vec stays empty, the pre-stat loop is skipped (available.is_empty() is immediately true), and execution falls through to fstat — identical to the existing sync-without-VM path at line 7159. One minor behavioural delta: expand_to_capacity() sets len = capacity(), so if the allocator rounds 256 KB up the loop may read slightly past 256 KB, whereas the old vec![0u8; N] set len to exactly N. This is harmless (still bounded, same semantics) and matches the precedent at lines 4896-4903.

Security risks

None identified. The uninit bytes are never observed by user code — they're write-only into read(2). This is the same uninit-slab idiom already accepted in this function at line 7296 and in copy_file_using_read_write_loop at line 4902, with the same VecExt::expand_to_capacity helper whose documented contract (vec_ext.rs:121-125) covers exactly this use.

Level of scrutiny

Medium. It's a small, mechanical perf-parity fix that copies an established in-file pattern verbatim, and the bug-hunting pass found nothing. But it introduces a new unsafe block in fs.readFile/fs.promises.readFile — one of the hottest, most widely-exercised paths in the runtime — so I'd rather a human confirm the SAFETY reasoning than auto-approve.

Other factors

readFile tests pass per the PR description. No outstanding reviewer comments. No CODEOWNER signal available to me.

@robobun

robobun commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

CI: all individual lanes passed. The aggregate is red because of test/js/web/streams/streams-leak.test.ts, which is flaking across four platform configs on this build (Debian 13 aarch64 with 3 retries, Debian 13 x64-baseline, Ubuntu 25.04 x64-baseline, Alpine 3.23 x64-baseline with 2 retries). That test exercises web ReadableStream RSS thresholds and does not go through node:fs readFile; this diff only touches the async pre-stat scratch buffer in readFileWithOptions.

No fs or readFile test failures on any lane. Ready for review.

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