shell: exit with the positive errno when a builtin's output write fails
Problem
- A builtin whose output write fails reports a garbage exit code.
echo hi > /dev/fullandwhich sh > /dev/fullexit 65508 (ShellError: Failed with exit code 65508).export > /dev/fullandcd /nonexistent 2> /dev/fullexit 1 and drop the errno. echo.rs:116andwhich.rs:182castSystemError.errnoto theu16exit code. That field holds the negated errno (fill_system_error_common,src/sys/Error.rs:371), so ENOSPC (-28) wraps to 65508.export.rs:85andcd.rs:127hard-code 1.
Fix
- The four builtins read the errno through
SystemError::get_errno(), which undoes the negation.catandrmalready do this, and the Zig builtins did too (getErrno()).cdkeeps exit 1 when the stderr message is written without error. mvreads a failed rename's errno throughbun_sys::Error::get_errno()instead of the raw field. That field is already positive, so the value does not change on POSIX.- Excluded on purpose:
pwd,basename,dirname,seqandyeskeep exit 1 on a failed write. They did that before the rewrite too. #40033 proposes one status for every builtin. - Verified:
test/js/bun/shell/bunshell.test.ts(new test, stock bun reports 65508/65508/1/1). Also the rest ofbunshell.test.ts,commands/{echo,which,mv}.test.ts,epipe.test.ts,shell-pipe-read-fault.test.ts,shell-write-fault.test.ts.
Background
- A builtin that writes to a real fd (a file redirect, or the process stdout) goes through
IOWriter, the shell's per-fd write queue. The result arrives later in the builtin'son_io_writer_chunkas anOption<bun_sys::SystemError>. bun_sys::SystemErroris the JS-facing error shape. Itserrnois stored negated to match Node'serr.errno.get_errno()returns the positiveEvalue.bun_sys::Erroris the syscall-level error. Itserrnois the positiveu16.
Notes
Values from the repro with /dev/full as the redirect target (Linux, ENOSPC = 28):
| command | 1.4.1 | now |
|---|---|---|
echo hi > /dev/full | 65508 | 28 |
which sh > /dev/full | 65508 | 28 |
export FOO=bar; export > /dev/full | 1 | 28 |
cd /nonexistent 2> /dev/full | 1 | 28 |
cd /nonexistent (stderr ok) | 1 | 1 |
mv nosuch dst | 2 | 2 |
pwd > /dev/full | 1 | 1 |
ls / > /dev/full | 0 | 0 |
On Windows SystemError.errno is the libuv code (UV_ENOSPC = -4075), so the raw cast gave a different garbage value there. get_errno() canonicalizes it to the E discriminant, so the exit code is 28 on every platform.
The test is Linux only because /dev/full does not exist on macOS and Windows.
The excluded sites, with the pre-rewrite Zig behavior (commit 23427dbc12^, src/shell/builtin/*.zig):
pwd.zig,basename.zig,dirname.zig,seq.zig,yes.zig:onIOWriterChunkreturneddone(1)on a write error. The Rust port (pwd.rs:72,basename.rs:62,dirname.rs:64,seq.rs:209,yes.rs:166) does the same.ls.zig:onIOWriterChunkdropped the error.ls.rs:182does the same, sols / > /dev/fullexits 0.echo.zig,which.zig,export.zig,cd.zig:onIOWriterChunkreturneddone(e.getErrno()). The Rust port changed these four, and this PR restores them.
#40033 and #32278 are open PRs that change the same sites to exit 1 (coreutils style) for every failed write. This PR keeps the errno as the exit code, which is what the other builtins report and what the Zig implementation did.
no test proof · iteration 1 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/shell/bunshell.test.ts