Skip to content

fix(interp): report write errors on builtin stdout instead of exiting 0 - #708

Open
astuyve wants to merge 1 commit into
mainfrom
patina/write-errors
Open

astuyve wants to merge 1 commit into
mainfrom
patina/write-errors

Conversation

@astuyve

@astuyve astuyve commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator

What

CallContext.Out/Outf drop the error from the underlying write, and the dispatcher only looked at the builtin's own return code. So echo, printf, sed, awk, sort, grep, cut, wc, ls, find, strings and sha256sum exited 0 on ENOSPC/EIO/EFBIG and left a truncated redirect target.

The dispatcher now wraps the builtin's stdout in a builtins.WriteGuard, which remembers the first write error and fails every later write with it:

  • Builtins that check their writes stop early.
  • When the builtin otherwise succeeded, the dispatcher prints one <cmd>: write error: <reason> line and sets the status to 1, as bash does (e.g. echo: write error: no space left on device; lowercase like rshell's other messages and tr's existing write error).
  • Builtins that already failed and said why (cat, head, uniq) aren't reported twice.
  • A broken pipe stays silent, as bash builtins die quietly from SIGPIPE.
  • The find -exec / xargs child dispatch gets the same treatment.

Credit

Found by Jacob Hayes with his deterministic-simulation tool patina (finding #1: SILENT oracle, random EIO leg, about 25k runs; reproduces natively with RLIMIT_FSIZE). Findings thread: https://dd.slack.com/archives/D0AC8RLB21J/p1793475631273749

Tests

interp/builtin_write_error_test.go gives the runner a stdout that fails with ENOSPC:

  • TestBuiltinWriteErrorReported: echo, printf, sed, sort, grep, wc, cut, cat, head and uniq each exit 1 with exactly one stderr line.
  • TestBuiltinWriteErrorFormat: the exact echo message.
  • TestBuiltinWriteErrorBrokenPipeSilent: EPIPE prints nothing.

The first two fail on main for the Out-based builtins. go test ./... passes except TestPSPentestLargePIDListDoesNotHang in builtins/ps, which fails the same way on clean main on this host (it has real processes with the PIDs the test assumes are missing).

🤖 Generated with Claude Code

CallContext.Out/Outf drop the error from the underlying write, and the
dispatcher only looked at the builtin's own return code. So echo,
printf, sed, sort, grep, cut, wc and the other builtins that print
through Out exited 0 on ENOSPC/EIO/EFBIG and left a truncated file.

Wrap the builtin's stdout in a WriteGuard that remembers the first
write error and fails every later write with it, so builtins that check
their writes stop early. When the builtin otherwise succeeded, the
dispatcher prints one `<cmd>: write error: <reason>` line and sets the
status to 1, as bash does. Builtins that already failed and said why
(cat, head, uniq) are left alone, and a broken pipe stays silent. The
find -exec / xargs child dispatch gets the same treatment.

Found by Jacob Hayes with patina (https://github.com/JacobHayes/patina).

Environment: Datadog workspace

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@astuyve
astuyve marked this pull request as ready for review November 1, 2026 00:00
@astuyve
astuyve requested review from a team as code owners November 1, 2026 00:00
@astuyve
astuyve requested a review from louis-cqrl November 1, 2026 00:00

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant