Repository navigation
Conversation
ReturnCode::errno() maps a negative libuv code to the POSIX E discriminant that bun_sys::Error.errno stores, but ReturnCodeI64::errno() (the type of uv_fs_t.result, so every async fs completion) returned the raw |UV_E*| magnitude. Callers store both the same way with from_libuv unset, and on Windows E also declares the sparse UV_E* tail variants at exactly those magnitudes, so the error resolved to E::UV_EBADF and reached JS as code "UV_EBADF" with message "UV_EBADF: unknown error, write" instead of "EBADF: bad file descriptor, write". Affected: Bun.write to a fd (WriteFileWindows), the Bun.write read/write copy loop (CopyFileWindows), FileSink writes and Bun.file().stream() reads on Windows (PipeWriter/PipeReader file completions via Error::from_uv_rc64). Both return-code types now share one translation; codes missing from the table fold to E::UNKNOWN so a failed call can never read as success.
|
Warning Review limit reached
Next review available in: 13 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (5)
Comment |
|
Status: reproduced on Windows Server 2019 x64 with bun 1.4.0 ( |
There was a problem hiding this comment.
LGTM — aligns ReturnCodeI64::errno() with ReturnCode::errno() so async libuv fs errors on Windows report EBADF instead of UV_EBADF.
What was reviewed:
- Traced consumers of
ReturnCodeI64::errno()(WriteFileWindows::on_write_complete,CopyFileWindowsread/write loop,PipeWriter/PipeReaderfile completions viafrom_uv_rc64) — all store intoError.errnowithfrom_libuv=falseand expect the translated discriminant. - Confirmed
node_fshandlers usingfrom_libuv: truecompute(-rc) as _directly and do not go througherrno(), so they're unaffected. - Verified
E_UNKNOWN_DISCRIMINANT = 134matchesE::UNKNOWNinwindows_errno.rs; the newSome(UNKNOWN)fallback for unmapped negative codes prevents theusize::try_from(rc.int()).expect(..)panic atwrite_file.rs:1087on the fall-through path.
Extended reasoning...
Overview
This PR fixes a Windows-only error-code mislabeling in src/libuv_sys/libuv.rs. ReturnCodeI64::errno() (used for async uv_fs_t.result) previously returned the raw |UV_E*| magnitude (e.g. 4083 for UV_EBADF) instead of the translated POSIX E discriminant (9 for EBADF), unlike its sibling ReturnCode::errno(). Consumers stored this into bun_sys::Error.errno with from_libuv=false, so resolve_system_errno matched the sparse E::UV_EBADF tail variant and JS saw code: "UV_EBADF" / "unknown error". The fix routes both return-code types through a shared uv_err_to_e_discriminant_lossy helper (folding unmapped codes to E::UNKNOWN), aliases err_enum() to errno(), and adds a const _: () = { ... } block asserting the contract at compile time. src/sys/Error.rs gets a doc-comment update only. Five new tests cover each affected producer.
Security risks
None. This is pure error-code translation for user-visible diagnostics; no auth, crypto, path handling, or untrusted-input parsing is involved.
Level of scrutiny
Medium-low. The changed code is #[cfg(windows)]-gated and small (~40 net Rust lines). The behavior change is a strict bug fix (aligning two nominally-identical accessors), and I traced every consumer of ReturnCodeI64::errno() / from_uv_rc64 / .to_error() on io_request.result to confirm they all expect the translated discriminant. The one semantic widening — ReturnCode::errno() now returns Some(E::UNKNOWN) instead of None for negative codes missing from the table — is defensive: no libuv code is missing today, and returning None would let write_file.rs:1087's usize::try_from(rc.int()).expect(..) panic on a negative fall-through. The compile-time assertions pin UV_EBADF → 9, 0/i64::MAX → None, and unmapped/overflow → E_UNKNOWN_DISCRIMINANT on both types.
Other factors
E_UNKNOWN_DISCRIMINANT = 134matchesE::UNKNOWN = 134insrc/errno/windows_errno.rs, and theUV_UNKNOWN => 134table row it references.- The
node:fscompletion handlers that store raw magnitudes withfrom_libuv: true(src/runtime/node/node_fs.rs) compute(-rc) as _directly and never callReturnCodeI64::errno(), so they are untouched — matching the PR description's scoping note. - Tests follow harness conventions:
tempDir,bunEnv/bunExe, concurrent pipe draining, exact-shape assertions on{code, errno, syscall, message}, negative-contract check that the target file is unmodified, andit.skipIf(!isWindows)with a stated reason for the two Windows-only paths. - No prior human reviews or outstanding comments on the timeline.
|
Updated 3:33 AM PT - Aug 11th, 2026
✅ @robobun, your commit f722611c4bc79d52d0ba709add59ea66f8a55e08 passed in 🧪 To try this PR locally: bunx bun-pr 37465That installs a local version of the PR into your bun-37465 --bun |
|
Note for whoever rebases or merges this: the compile-time assertion block this adds to A separate report of the same mislabeling reached me, described from the |
|
Superseded by #40860. It removed the Verified on a Windows x64 debug build of main (b49398c). The repro prints Closing. |
Repro (Windows only)
Bun.write(Bun.file(fd), new Uint8Array(3)),Bun.write(Bun.file(fd), Bun.stdin)with a piped stdin,Bun.file(fd).writer()andBun.file(fd).stream()on a write-only fd all report the sameUV_E*code on Windows.node:fson the same fd, andBun.file(fd).text(), already reportEBADF. User code checkingerr.code === "EBADF"(or any other errno name) therefore fails on Windows for these Bun APIs only.Cause
libuv hands back an error either as the
c_intreturn value of a synchronous call (ReturnCode) or, for asynchronous requests, inuv_fs_t.result(ReturnCodeI64).ReturnCode::errno()maps the negativeUV_E*code to the POSIXEdiscriminant thatbun_sys::Error.errnostores;ReturnCodeI64::errno()returned the raw|UV_E*|magnitude (4083 forUV_EBADF) instead. Every consumer treats the two the same way and stores the value withfrom_libuvunset:Error::from_uv_rc64(behindReturnCodeI64::to_error, used byCopyFileWindows's read/write loop and thePipeWriter/PipeReaderfile completions that backFileSinkandBun.file().stream()), andWriteFileWindows::on_write_completedirectly.Nothing catches this because on Windows the
Eenum also declares the sparseUV_E*tail variants at exactly those magnitudes, soError::resolve_system_errnoturns 4083 intoE::UV_EBADF: strum names itUV_EBADF, and the libuv message table only has labels for theE*variants, hence "unknown error". Rust-side comparisons such aserr.get_errno() == E::EPIPE(for example the shell's broken-pipe bookkeeping inIOWriter::fail_pending_writers) also never matched errors produced by these paths on Windows. The asymmetry between the two return-code types predates the Rust port.Fix
ReturnCodeI64::errno()translates the code exactly likeReturnCode::errno();err_enum()is now an alias of it, as it already was onReturnCode. The translation is shared through one helper that folds codes missing from the table toE::UNKNOWN(the fallbacktranslate_uv_error_to_ealready uses), so a negative result can never come back asNoneand be mistaken for success. That matters for the async callers, which go on totry_from(rc.int()).expect(..)on the "success" path; every code in libuv'serrno.his in the table today, so this only changes what would happen on a future libuv addition. A compile-time block pins both types to the same contract.Fixing the producer rather than teaching
resolve_system_errnoto foldUV_E*variants keepsError.errnoholding one kind of value forfrom_libuv == false, which is whatget_errno()/is_retry()assume. Thenode:fscompletion handlers, which store the raw magnitude withfrom_libuv: true, are a separate convention and are left alone; #32066 is reworking those and independently adds the same unmapped-code fallback toReturnCode::errno(), while explicitly leaving this mislabeling out of scope, so the two overlap only in that hunk.Verification
New tests cover each affected producer:
Bun.write(Bun.file(fd), string | Uint8Array)andBun.write(Bun.file(fd), Bun.stdin)intest/js/bun/io/bun-write.test.js,Bun.file(fd).writer()intest/js/bun/util/filesink.test.ts,Bun.file(fd).stream()intest/js/bun/util/bun-file.test.ts. The write tests assert the same code/errno/message on every platform (errno is-4083on Windows and-9elsewhere, matching Node); the pipe-source and stream tests are Windows only because POSIX takes different paths there (a pipe source is rejected earlier, and a failingstream()read does not settle yet).On Windows x64, all five cases fail with bun 1.4.0 (
"code": "UV_EBADF","message": "UV_EBADF: unknown error, write") and pass with this branch;bun-write.test.js,filesink.test.ts,bun-file.test.ts,spawn.test.ts,translate-uv-error-windows.test.tsanderror-name-from-libuv.test.tspass on the debug build. On POSIX the new tests pass before and after, since the changed code is#[cfg(windows)].cargo checkand clippy are clean forbun_libuv_sys/bun_sysonx86_64-pc-windows-msvc.