fs: clamp out-of-range libuv error results instead of truncating into errno
What does this PR do?
Fixes a Windows-only Panic: integer does not fit in destination type in the async node:fs completion path, along with two sibling defects at the same conversion boundary: truncated errnos that alias unrelated error codes, and failed libuv results silently treated as success.
Stack (from shipped 1.3.14 Windows x64 builds)
Panic: integer does not fit in destination type
uv_callback src/runtime/node/node_fs.zig:264
uv__work_done libuv threadpool.c:340
uv__process_reqs libuv win/core.c:605
uv_run libuv win/core.c:737
tickWithTimeout src/uws_sys/Loop.zig:250
Cause
On Windows, async node:fs operations complete through uv_callback, which hands req.result to the per-op handlers (uv_open, uv_read, uv_write, uv_close, uv_readv, uv_writev, uv_statfs). Each handler converted a negative result with a raw narrowing cast into the u16 Error.errno field:
if rc < 0 {
return Err(sys::Error { errno: (-rc) as _, from_libuv: true, .. });
}
Normal libuv error codes are small negatives (-4095..-1) and fit. But libuv's uv_translate_sys_error returns already-negative inputs unchanged (vendor/libuv/src/win/error.c), so req->result can carry raw system codes (NTSTATUS-shaped values) whose magnitude does not fit u16. The shipped Zig implementation used @intCast, which panics in release-safe builds: that is the crash above, seen steadily in the wild on 1.3.13/1.3.14 Windows. The Rust port's as cast truncates instead, which trades the crash for a lie: -0x10002 reads back as errno 2 and the user sees ENOENT for an I/O failure that was nothing of the sort.
Auditing the same boundary turned up two more defects of the same class:
ReturnCode::errno()returnedNonefor a negative but unmapped code. Everyif let Some(errno) = rc.errno()caller treatsNoneas success: the syncuv_fs_futime/uv_fs_mkdtemp/uv_fs_realpath/uv_fs_utime/uv_fs_lutimesites in node_fs,Fd::close, and everything behindReturnCodeExt::to_error/to_result(named pipes, fs watcher, blob copy). An unmapped failure was silently swallowed;realpaththen reads thereq.ptrlibuv never populated.ReturnCodeI64::err_enum()anderr_enum_e()cast i64 to c_int withas, so a result below i32 range could wrap and alias a validUV_E*code.Error::from_code_inthad the same narrowing with.unwrap()(a panic), andIntoErrnoInt for i32the same truncation.
Fix
One total conversion, bun_libuv_sys::uv_raw_errno(rc: i64) -> Option<u16>: negative results keep their raw |UV_E*| magnitude when it fits u16 and clamp to |UV_UNKNOWN| (4094) when it does not, so they resolve to EUNKNOWN downstream instead of panicking, aliasing, or being dropped. The seven node_fs uv handlers, ReturnCode::raw_errno(), and ReturnCodeI64::errno() all route through it.
ReturnCode::errno() and ReturnCodeI64::err_enum() now resolve unmapped negative codes to the E::UNKNOWN discriminant instead of None, so a failed result can never be mistaken for success. err_enum_e saturates its i64-to-c_int conversion, and from_code_int / IntoErrnoInt for i32 clamp to UNKNOWN instead of panicking or truncating.
The load-bearing hunks are src/libuv_sys/lib.rs (the new conversion) and the errno() / err_enum() fallbacks in src/libuv_sys/libuv.rs; the node_fs and Error.rs changes reroute existing call sites through them.
Verification
There is no JS-reachable way to inject an out-of-range req.result (the triggering environments involve unusual Windows error sources such as network filesystems or filter drivers), so this follows the pattern from #29571 / #29651: the conversions are exposed through bun:internal-for-testing (uvRawErrno, uvTranslatedErrno) and pinned in test/js/node/fs/translate-uv-error-windows.test.ts:
uvRawErrnoruns on every platform: in-range magnitudes pass through unchanged;-65536,-65538,-1073741804(NTSTATUS0xC0000014as i32), and-(2**53)clamp to 4094, where the old cast produced 0, 2 (ENOENT), 20 (ENOTDIR), and 0.uvTranslatedErrno(Windows lanes): unmapped-4021and-123456resolve to 134 (EUNKNOWN) instead of disappearing as success; known mappings (-4058toENOENT) are unchanged.- The test file fails on the unfixed build (the
bun:internal-for-testingexports do not exist there) and passes with this change. cargo check --workspaceis clean onx86_64-pc-windows-msvc,aarch64-pc-windows-msvc, and Linux.
Out of scope, noted for completeness: a few call sites store the raw |UV_E*| magnitude without setting from_libuv (Error::from_uv_rc64, the write_file completion), which makes in-range codes display with an UNKNOWN name. That is a pre-existing mislabeling with different user-visible behavior and is not touched here.