sys: add the ENOEXEC row to the libuv error text table - #39318
Conversation
libuv_error_map.rs had no row for ENOEXEC, so a spawn that failed with it was reported as "ENOEXEC: unknown error, posix_spawn '...'" instead of carrying uv.h's "exec format error". The ENOTSUP row was also compiled out on FreeBSD, where SystemErrno::ENOTSUP is the EOPNOTSUPP alias and indexes the table fine, leaving that errno unlabelled there. Add a source lint that replays the table's rows for each target_os and compares them with UV_ERRNO_MAP in uv.h, and a spawnSync test for the message.
|
Warning Review limit reached
Next review available in: 23 minutes Limit details: You’ve used all 5 included reviews currently available under your plan. 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 (3)
Comment |
|
Status: ready for review. Reproduced on bun 1.4.0 (Linux): |
There was a problem hiding this comment.
LGTM — one missing table row plus a cfg-guard drop, both verified against uv.h and the per-target errno enums.
What was reviewed:
- Confirmed
ENOEXECexists as aSystemErrnovariant on linux/darwin/freebsd/windows andENOTSUPis an associated const on FreeBSD aliasingEOPNOTSUPP, so the un-gated row indexes a real slot in const fn on every target. - Checked the new lint's parsers against
windows_errno.rs(which has both anEenum and aSystemErrnoenum) — it anchors onpub enum SystemErrno {, so it reads the E-prefixed variants and ignores theE::alias consts it doesn't need. - The spawn test is hermetic (
tempDir+using), POSIX-gated with a stated reason, and asserts the exact code and message;Bun.spawnthrows synchronously onposix_spawnfailure so the non-asyncthrownByshape is correct.
Extended reasoning...
Overview
Two-line data fix in src/sys/libuv_error_map.rs: add the ENOEXEC -> "exec format error" row (present in uv.h's UV_ERRNO_MAP at line 161 but missing from this copy on every target) and drop the #[cfg(not(target_os = "freebsd"))] guard on the ENOTSUP row (FreeBSD has SystemErrno::ENOTSUP as an associated const aliasing EOPNOTSUPP, so the guard only left slot 45 reading "unknown error"). A new source-lint test replays the table's rows per target against uv.h so a future missing row fails CI, and a POSIX-only spawn test asserts the user-visible message for a shebang-less script.
Security risks
None. This is a static errno -> const &str lookup table used only to format error messages; the added rows are string literals copied verbatim from vendored uv.h. No parsing of untrusted input, no allocation, no control-flow change.
Level of scrutiny
Low for the Rust change (pure table data; the const fn shape is unchanged; the PR ran cargo check -p bun_sys for freebsd/darwin/windows/android). Medium for the new lint — it's a hand-rolled parser over three source files, but I traced its regexes against the actual file shapes: uv.h's XX(NAME, "text") lines, the arr[SystemErrno::X as usize] = "..." rows with their #[cfg] blocks, and each target's pub enum SystemErrno body. The Windows file has two enums (E with bare names at ~line 100 and SystemErrno with E-prefixed names at line 415); the lint correctly anchors on the latter. The lint has size guards (> 50) so an empty parse fails loudly, and unknown cfg predicates throw rather than silently evaluating.
Other factors
The PR description shows the lint failing on main with exactly ENOEXEC (all five targets) plus ENOTSUP (FreeBSD only) and passing after — the fix and the check are proven paired. The spawn test follows harness conventions (tempDir, using, exact-message assertion, it.if(isPosix) with the Windows exclusion reason stated). The author flagged the interaction with open PR #31717 (sh-retry on ENOEXEC) up front. The source-lints directory already contains ~25 similar file-parsing checks, so the pattern is established.
|
Updated 12:04 AM PT - Aug 16th, 2026
✅ @robobun, your commit 9273e5f9506d2831a892c30d4e7fee972be9b792 passed in 🧪 To try this PR locally: bunx bun-pr 39318That installs a local version of the PR into your bun-39318 --bun |
Problem
ENOEXEC: unknown error, posix_spawn '/path/to/file'fromBun.spawn,Bun.spawnSync, and thenode:child_processfunctions built on them (the "unknown error in execFileSync" of unknown error inexecFileSyncfromnode:child_process#31710). libuv's text for ENOEXEC isexec format error(util.getSystemErrorMessage(-8)returns it), so the message should readENOEXEC: exec format error, posix_spawn '/path/to/file', as every other spawn failure does (ENOENT: no such file or directory, posix_spawn '...').LIBUV_ERROR_MAPinsrc/sys/libuv_error_map.rsis filled with"unknown error"and then written one row per errno; it has no row forENOEXEC. uv.h'sUV_ERRNO_MAP(src/jsc/bindings/libuv/uv.h:161) has one, and the other two in-tree copies of that table (ProcessBindingUV.cpp,util.ts, both updated in node compat batch: callback-throw dispatch, Assert class + native deep-equality parity, Intl gate + URL/buffer fallout, compile cache, watch kill-signal, profilers (+98 tests) #34660) have it too. The row is missing on every target;ENOEXECis aSystemErrnovariant on all of them.ENOTSUProw is#[cfg(not(target_os = "freebsd"))]'d out on the grounds that FreeBSD has noENOTSUPvariant. It has the associated constSystemErrno::ENOTSUP = EOPNOTSUPP(src/errno/freebsd_errno.rs:117), which indexes the table fine, so the guard only leaves that errno reading "unknown error" on FreeBSD.Fix
src/sys/libuv_error_map.rs: addENOEXEC->"exec format error"(at the end, where uv.h has it; the file follows uv.h's order), and write theENOTSUProw on FreeBSD as well.UV_ENOTSUPis-45, FreeBSD'sEOPNOTSUPP, because its<sys/errno.h>definesENOTSUPasEOPNOTSUPP; labelling slot 45 with uv.h's ENOTSUP text is therefore what libuv does there too.test/internal/source-lints/libuv-error-map.test.ts(new): parses uv.h'sUV_ERRNO_MAP, the table's rows together with their#[cfg]predicates, and theSystemErrnoenum of each target (variants plus alias consts), then checks per target that every uv.h errno the target has is labelled with uv.h's text and written once, that every row is a uv.h errno, and that the only uv.h rows no target can hold areEAI_*andUNKNOWN. On main it fails on all five targets forENOEXECand additionally on FreeBSD forENOTSUP(output below); it passes with this change. Reading the source is the only way to cover the FreeBSD and Windows rows from a Linux runner, and it picks up future uv.h rows as well. It lives in the source-lints directory, whose workflow already triggers onsrc/**/*.rsandsrc/jsc/bindings/**.test/js/bun/spawn/spawnSync.test.ts:Bun.spawnSyncandBun.spawnof a no-shebang script throwENOEXECwith theexec format errormessage. POSIX only: libuv on Windows reports an unloadable image asEFTYPEorUNKNOWN(checked against itserror.c, and node on Windows printsUNKNOWNfor the same file), so the Windows row is covered by the lint alone. Fails on bun 1.4.0 withunknown error, passes with this change.bun test test/internal/source-lints/(all pass);cargo check -p bun_sysforx86_64-unknown-freebsd,aarch64-apple-darwin,x86_64-pc-windows-msvc, andaarch64-linux-android; the existing spawnSync.test.ts file with the debug build./bin/shon ENOEXEC, as libuv does. If it lands, the spawnSync test here stops seeing ENOEXEC and needs to pick a file/bin/shalso cannot run, or go; the table row and the lint are unaffected.Background
bun_sys::Error::to_system_error(src/sys/Error.rs) builds every JS-facing syscall error as"<CODE>: <label>, <syscall> '<path>'", the shape of node'sUVException.<label>is looked up inLIBUV_ERROR_MAP, the in-tree copy of libuv'suv_strerror()texts, so a missing row shows up directly in user-visible messages. Shell builtins use a separatestrerror()-style table (coreutils_error_map, being fixed for macOS in bun_core: use strerror() texts in the macOS coreutils_error_map #39264); it is not involved here.SystemErrno(src/errno/<os>_errno.rs) is a per-target errno enum; the table is anEnumMapover it, built in aconst fnby indexing an array withSystemErrno::X as usize. Rows for errnos some target lacks as a variant (ECHARSET,EOF,ENONET, ...) are wrapped in#[cfg]blocks; an errno present on a target but without an active row keeps the"unknown error"fill.ENOTSUPandEOPNOTSUPPare one errno (45), so the enum has anEOPNOTSUPPvariant and anENOTSUPassociated const aliasing it; casting the alias withas usizeyields 45 like a variant would.Repro on bun 1.4.0 and with this change (Linux)
Lint output on main