Repository navigation
Conversation
read, write, pread, pwrite, fstat, fstatx, fchmod, fchown, ftruncate, futimens, fsync, lseek, recv, send and pwritev built their error with only the errno and the syscall tag. The Node SystemError then had fd: undefined. Bun 1.3 set err.fd on these errors, and the Windows wrappers in sys_uv still do. Add check_fd! and check_once_fd! next to the path variants and use them in posix_impl. node:fs fsync now uses errno_sys_fd like fdatasync.
|
Warning Review limit reached
On-demand reviews are free for the next 24 days. After that, they cost $0.25 per reviewed file. Or wait 5 minutes for your next included review. View limit detailsLimit details: You’ve used all 5 included reviews currently available. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
Comment |
|
Updated 11:50 PM PT - Aug 27th, 2026
❌ @robobun, your commit bcc2904 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 40712That installs a local version of the PR into your bun-40712 --bun |
|
Status: ready for review. Repro: CI on bcc2904 (build 107404): 180 of 181 jobs pass. The one failure is |
There was a problem hiding this comment.
LGTM — the Windows readv/writev syscall-tag branching addresses my earlier note, and the rest is a mechanical .with_fd(fd) mirror of the existing .with_path() pattern.
What was reviewed: Checked every check! → check_fd! / check_once! → check_once_fd! swap preserves the EINTR-retry vs single-shot semantics per arm. Confirmed the removed check_once! and MaybeSysResultExt::errno_sys have no remaining callers (the errno_sys hits in sys/lib.rs, sys/windows/mod.rs, posix_event_loop.rs, udp_socket.rs are separate definitions). Looked at whether the "writeSync on a read-only fd" test would see EPERM instead of EBADF on Windows — ruled out. Verified fstatx's .with_fd(fd) is applied only on the fd-taking wrapper, not statx/lstatx which pass AT_FDCWD + path.
Extended reasoning...
Overview
This PR attaches the file descriptor to bun_sys::Error at every fd-taking POSIX syscall wrapper in src/sys/lib.rs, so JS-visible node:fs errors carry err.fd. It introduces check_fd! (EINTR-retrying) and macOS-only check_once_fd! (single-shot for $NOCANCEL arms) that mirror the existing check_p!/check_once_p! path-attaching macros, and swaps them in at read/write/pread/pwrite/fstat/fchmod/fchown/ftruncate/futimens/fsync/lseek/recv/send/pwritev plus the corresponding linux_syscall::* .map_err arms. fstatx maps its error through .with_fd(fd). In node_fs.rs, _fsync switches to errno_sys_fd and the now-unused errno_sys trait method is removed. The dead check_once! macro is deleted. Tests add a table-driven describe block in fs.test.ts covering 12 fd-based sync calls on a bad fd plus a real read-only fd case.
Security risks
None. The change only adds metadata (fd) to already-constructed error objects on failure paths; no new syscalls, no input parsing, no auth/crypto surface. The fd value echoed back is one the caller already supplied.
Level of scrutiny
Moderate, focused on cross-platform consistency. Each wrapper has 2-3 #[cfg(...)] arms (macOS $NOCANCEL, Linux/Android direct syscall, other-POSIX libc), and each arm was touched. I verified the retry-vs-single-shot semantics were preserved in every arm by comparing the old macro's body against the new one — check_fd! is byte-identical to check! plus .with_fd($fd), and check_once_fd! is byte-identical to the removed check_once! plus .with_fd($fd). The statx_impl change from struct-init to Error::from_code_int is behaviorally equivalent (same fields set). The deleted EINTR explanatory comment's content survives on the check_once_p! line comment.
Other factors
My earlier inline comment (readvSync/writevSync asserting syscall: "readv"/"writev" on Windows where sys_uv::preadv/pwritev tag as read/write) was addressed in a follow-up commit with isWindows ? "read" : "readv" branching. The one candidate this run investigated — whether the read-only-fd writeSync test would see EPERM on Windows — was ruled out. Grep confirms no remaining callers of the removed check_once! macro or the MaybeSysResultExt::errno_sys trait method (other errno_sys hits are unrelated free functions or a separate Windows-only trait on bun_sys::Result). The github-actions bot's inline notes at src/sys/lib.rs were followed by author commits and the current diff shows no residual issue at those lines. The change is mechanical enough, and mirrors an established in-tree pattern closely enough, that I'm comfortable approving.
Problem
node:fscalls (readSync,writeSync,fstatSync,ftruncateSync,fchmodSync,fchownSync,futimesSync,fsyncSync, and their async forms) havefd: undefined. Bun 1.3.0 seterr.fdon all of them.readvSync,writevSync,fdatasyncSyncandcloseSyncstill do.src/sys/lib.rsbuild the error with only the errno and the syscall tag:Error::from_code_int(e, Tag::read)atsrc/sys/lib.rs:2019, and the same shape forwrite,pread,pwrite,fstat,fstatx,fchmod,fchown,ftruncate,futimens,fsync,lseek,recv,sendandpwritev. Nothing calls.with_fd(fd). The Windows wrappers insrc/sys/sys_uv.rsdo call it.Fix
check_fd!andcheck_once_fd!next to the existingcheck_p!andcheck_once_p!macros inposix_impl. They attach the fd the same way the path variants attach the path.map_errarms and the top-levelpwritevget.with_fd(fd)directly, likereadvandwritevalready do.statx_implattaches the fd only forfstatx.statxandlstatxpassAT_FDCWDplus a path, andnode_fs.rsattaches the path for those.node_fs.rsfsyncnow useserrno_sys_fd, likefdatasyncand the Windows path. The unusederrno_syshelper is removed.test/js/node/fs/fs.test.ts(13 new cases, 9 fail on bun 1.4.1). Alsotest/js/node/fs/promises.test.js,fs-stat-seccomp-linux.test.ts,fs-write-offset-bound.test.ts,test/js/bun/io/bun-write.test.js,test/js/bun/util/bun-file.test.ts,error-name-preservation.test.ts.Background
bun_sys::Error(src/sys/Error.rs) is the syscall error. It carrieserrno,syscall,path,destandfd.to_system_errorcopies each field that is set onto the JSSystemError, soerr.fdin JS exists only when the wrapper setsfd.with_fdandwith_pathare the two builders for that context. The path-based wrappers usecheck_p!, which callswith_path. Before this change there was no fd equivalent, so the fd wrappers used plaincheck!.$NOCANCELsyscalls once and do not retry onEINTR.check_once_fd!is the single-shot form for those arms. The oldcheck_once!had no other users and is removed.Notes
Repro on bun 1.4.1:
bun 1.3.0 prints
EBADF true <fd>. Node does not seterr.fdon these errors at all, so this restores Bun's own behavior rather than matching Node.node_fs.rspread_innerandpwrite_innerrebuild the error withfdthemselves, which is whyreadSyncandwriteSyncwith apositionkepterr.fd. They are unchanged.with_fdhas adebug_assert!(fd != Fd::INVALID). Every wrapper changed here receives a validated fd:node:fsrejects negative fds withERR_OUT_OF_RANGEbefore the syscall, and the internal callers (Blob,FileSink,copy_file, the resolver) check forFd::INVALIDbefore they read or write. The Windows wrappers have usedwith_fdon these same paths all along. #31165 proposes to drop that assertion and is independent of this change.The
fchownSynccase is skipped on Windows because libuv implementsuv_fs_fchownas a no-op there.cargo check -p bun_syspasses forx86_64-unknown-linux-gnu,aarch64-apple-darwinandx86_64-unknown-freebsd.test/js/bun/io/bun-write.test.js"on large files" timed out at 5 s once under the debug ASAN build (a 256 MB copy withcopy_file_rangedisabled). It exercises a success path only and is unrelated to this change.no test proof · iteration 2 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/node/fs/fs.test.ts