Files

6.9 KiB
Raw Permalink Blame History

nash ↔ bash divergence log

Live record of behavioral divergences found by the replay harness (replay.py, corpus in corpus.jsonl). Per docs/NASH.md §10.3/§11, each entry stays open until fixed in the fork (or upstream) and re-verified by the corpus.

Open

(none)

Closed (recent)

D3 — errexit re-triggered by a compound command's aggregated status (fixed in fork)

  • Found: narOS N2 first agent-tier CI build (run 29669891162): with /bin/sh → nash baked into naros-base, trixie's tzdata postinst exited 1 under dpkg configure, cascading into unconfigured python3/nodejs and failing the whole naros-agent image build — exactly the loud-in-CI dogfood failure mode the agent Dockerfile courts on purpose.
  • Repro: set -e; if true; then false && true; fi; echo hi → bash prints hi (exit 0), nash exited 1. In tzdata's postinst the trigger was which restorecon >/dev/null 2>&1 && restorecon … (restorecon absent) as the last statement of an if body under set -e.
  • Root cause: brush applies errexit at Pipeline::execute, and an if/brace-group/case/for compound is itself a (single-command) pipeline — so a failure that was already exempt inside the compound (short-circuited AND-OR list, !-negated pipeline) re-triggered errexit on the compound's aggregated status. bash never re-adjudicates a grouping/looping compound's status; it does re-trigger for simple commands (incl. function calls), subshells, [[ ]], and (( )).
  • Fix: errexit_applies_to_pipeline in brush-core/src/interp.rs — the pipeline-level errexit/ERR-trap application now fires only for multi-command pipelines, simple commands, subshells, extended tests, and arithmetic commands. Verified against bash on a 15-case matrix (if/brace/case/for/while bodies, function calls, subshells, cmdsub assignment, !, ||, pipe-to-cat, [[ ]], compound redirect failures) — all matching; trixie tzdata postinst green under nash; corpus 101/101 = 100%; candidate for upstreaming.
  • Regression tests: corpus errexit-if-andlist, errexit-brace-andlist, errexit-if-bang, errexit-fn-status.

D2 — non-ASCII bytes re-encoded through read/echo under C/empty locale (fixed in fork)

  • Found: narOS N0 rootfs validation (first real-world install run under the divert): with /bin/sh → nash, ca-certificates' postinst (update-ca-certificates, a while read-over-conf loop) mangled the UTF-8 filename NetLock_Arany_=Class_Gold=_Főtanúsítvány.crt and failed the whole image build. Worked around structurally in os/mkimage/build-rootfs.sh (the divert is now always the image's last configure step), but any runtime apt install inside narOS still runs postinsts under nash, so this blocks narOS/M3 forcing gates until fixed.
  • Repro (nash 0.4.0 musl arm64, empty locale): echo 'Főtanúsítvány' | nash -c 'while read x; do echo "$x"; done' — bash emits the input bytes unchanged (F \305\221 t a n \303\272 …); nash emits each byte Latin-1→UTF-8 double-encoded (F \303\205 \302\221 …).
  • Suspected root cause: brush decodes input bytes to String with a lossy/ Latin-1 assumption on the read path (or at word-splitting) instead of keeping raw bytes; on output the char sequence is re-encoded as UTF-8. POSIX shells treat variable values as byte strings.
  • Severity: silent data corruption (not a parse failure, so the §4.1 bash-fallback cannot catch it).
  • Root cause (confirmed): brush-builtins/src/read.rs InputReader::read_event read one byte at a time and did self.buffer[0] as char — a Latin-1 decode — before pushing into the result String (upstream had a TODO(utf-8) on the field). Valid UTF-8 input was thus re-encoded byte-by-byte.
  • Fix: incremental UTF-8 decoding in read_event (// nash(D2) markers): lead byte classifies the sequence length (0xC2–0xF4), continuation bytes are read (blocking, matching read's wait-for-delimiter behavior; a non-continuation byte is pushed back), and valid sequences round-trip byte-identically. bash's -n-counts-bytes semantics are preserved for free because the char-limit check measures line.len() (UTF-8 byte length). Candidate for upstreaming (fixes upstream's own TODO).
  • Residual (accepted): invalid UTF-8 input bytes still degrade to the per-byte Latin-1 mapping (a Rust String cannot hold raw invalid bytes; byte-fidelity there would need Vec<u8> plumbing shell-wide). bash keeps raw bytes. Not corpus-gated; revisit only if it bites in practice.
  • Verified: repro now byte-identical to bash; split-across-writes continuation reassembly correct; read -n 3 byte counting identical to bash (C locale); the original real-world failure (update-ca-certificates under nash) exits 0 with all 141 certs processed; corpus 97/97 = 100% including three new cases (utf8-read-passthru, utf8-while-read-file, utf8-cmdsub-roundtrip); brush-builtins read tests 20/20; brush compat suite failure set identical to the pristine-HEAD baseline rebuilt in the same container (29 shared environmental failures, ±1 flaky SIGPIPE-timing case that passes 10/10 standalone).
  • Regression tests: the three utf8-* corpus cases above.

Closed

D1 — IFS word-splitting applied to literal words (fixed in fork)

  • Found: M0 corpus run (ifs-split), brush-shell-v0.4.0.
  • Repro: IFS=,; set -- a,b,c; echo $1 → bash a b c, nash a.
  • Root cause: brush applied IFS splitting to literal words, not just expansion results: under IFS=,, echo a,b,c printed a b c (bash: a,b,c) and set -- a,b,c received 3 args (bash: 1). POSIX/bash split only the results of parameter/command/arithmetic expansion. Mechanically, brush-core/src/expansion.rs had a two-state ExpansionPiece (Splittable/Unsplittable) conflating "field-splittable" with "glob-active", and literal text was marked Splittable.
  • Fix: added a third piece state LiteralText (never field-split, glob chars active) and produce it for unquoted literal text, the no-expansion-chars fast path, and retained-backslash escapes (// nash: markers in expansion.rs). Corpus back to 100% (94/94); candidate for upstreaming.
  • Regression tests: corpus ifs-split (+ glob-all, glob-txt, cond-pattern, case guard the glob/pattern side); brush compat suite.
  • Fix fallout (caught by the brush compat suite, both fixed): (a) text substituted by ${v:-word}/${v:+word}/${v:=word} is an expansion result and must split — restored via a boundary conversion in the ParameterExpansion arm; (b) compgen -W splits its word-list string as data — restored via ExpanderOptions.field_split_literal_text. Final suite: 1684 succeeded / 6 failed, failure set identical to the pristine-brush baseline in this container (environmental only); 3 upstream known-fail IFS tests now pass (markers flipped in ifs.yaml).