Repository navigation
fix(indirect): preserve nested subscripts with a complete-reference boundary - #2674
Conversation
…sion
An indirect array target such as r='a[0]:-${!r}]' passed validation whole, so desugar re-parsed ${a[0]:-${!r}]} and the unset-element default re-entered indirect expansion without bound (stack overflow, SIGABRT).
Truncate the resolved bracket target at the first ] after [ before validation. Real bash resolves a[0] and ignores the rest (verified oracles), so trailing operands — including nested ${!...} — never reach the re-parse. Plain names, a[$i], a[@], assoc subscripts pass through unchanged; $(/backquote/process-substitution guards unchanged.
Co-Authored-By: yolop <yolop@everruns.com>
…oundary
PR 2673 truncated indirect array targets at the first ']', which broke valid nested subscripts (a[b[0]] errored; both real bash versions print two). The boundary now ends at the close of the first complete [...] reference, tracking nested brackets and skipping quoted/escaped text, so trailing operators still never reach the desugar re-parse (self/mutual ${!...} cycles terminate with bash-parity output) while nested and quoted-key references resolve whole.
Tests: hostile self/mutual cycles run in a bounded child CLI under a wall-clock watchdog with output caps and recorded producer status; finite nested indexed indirects assert byte equality with real bash; assoc direct/indirect agreement. Renumbers the prior TM-DOS-134 rows to TM-DOS-135 (tilde budget work owns 134).
Co-Authored-By: yolop <yolop@everruns.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
bashkit | 164aed2 | Commit Preview URL Branch Preview URL |
Oct 11 2026, 02:22 AM |
|
Blocking verification note for head 89e45cb: Coverage is genuinely failing, not pending and not a stack-size waiver case. The monitor independently ran this finite input:
Resolve the oracle/version mismatch with explicit justified semantics and |
…plete
Modern bash (5.x) rejects indirect array targets with trailing operator/nesting text (invalid variable name); older 3.2 ignored the tail. Refusal (instead of stripping) keeps nested ${!...} from the desugar re-parse while valid nested subscripts (a[b[0]]) resolve whole, matching bash 5.x in every probed shape.
Hostile self/mutual cycles run in a bounded child CLI under a wall-clock watchdog with output caps and recorded producer status; finite nested indexed indirects assert byte equality with real bash on both bash generations for complete references; assoc direct/indirect agreement. TM-DOS-135 (tilde owns 134).
Co-Authored-By: yolop <yolop@everruns.com>
|
Resolving each numbered item in the blocking verification note:
Draft left in place for your gate; merge is yours to allow. |
|
The current 164aed2 changes address the reviewed complete-reference boundary One remaining verification gap: run_cli_bounded's stated8KiB output cap is not Also the published head commit includes Read this and all earlier review comments completely. Keep draft until this |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…gression A long -c ARG0 stamped on every report line could evict the structural payload out of the 1 KiB diagnostic budget. Truncate the shell name to 64 chars first so 'syntax error ...' always survives; renumber the threat entry to TM-DOS-136 (main already owns 135 via #2674); add a bounded-child regression exercising the exact finding shape (multiline token x long ARG0) at two scales.
Corrective follow-up to #2674, resolving the remaining review (#2674 (comment)). ## What changed (tests only; no interpreter change) The hostile-cycle child runner now enforces real bounds instead of collecting everything and truncating after the fact: - one reader thread per pipe drains concurrently (a chatty child can no longer deadlock the runner on a full pipe); - an 8 KiB per-pipe sample cap: past the cap the reader stops, the child is killed, and the run is marked `oversized` — a harness failure, never a silent cut; - wall-clock watchdog kills past the limit; the real producer exit status is recorded; - the child launches through fail-closed `ulimit -t/-s` CPU/stack caps (`|| exit 99` before `exec`, so a refusal can never run unbounded) with `exec` keeping the CLI as the real producer; effective values are read back and asserted per run (Rust std exposes no portable per-child CPU/stack/memory caps; Darwin address-space limits are documented as unenforced, not claimed). ## Helper proof (fails visibly on regression) - `bounded_helper_kills_hung_child`: `sleep 30` under a 3 s limit must report timeout; - `bounded_helper_flags_oversize_output`: a ~100 KB overproducer must report oversize with a capped sample. - Hostile self/mutual cycles assert refusal (`invalid variable name`, real producer exit 1, empty stdout) plus non-oversize. ## Risk - Low (test-only) - What can break: environments without `bash` for the launcher shell (the suite already requires `bash` for real-bash differentials) ## Checklist - [x] Tests added or updated - [x] Backward compatibility considered (no interpreter change) Follow-up to #2674; resolves the remaining review comment linked above. --------- Co-authored-by: yolop <yolop@everruns.com>
## What changed Child-process syntax diagnostics (`bash -c`, partial parses) are now constructed through a single bounded `Diagnostic` type capped at 1 KiB (message + source excerpt + 128-byte tail + hint), and the shell name (`-c` ARG0, stamped on every report line) is truncated to 64 chars first. Attacker-controlled script text can no longer flow unbounded into stderr, replay buffers, or ack payloads. Small-input diagnostics keep their exact bash-compatible shape on modern GNU bash. ## Why TM-DOS-136 (renumbered: `origin/main` already owns TM-DOS-135 via #2674): the exact finding shape is `for x "` plus ~20,000 `a`+newline lines plus the closing quote, invoked with a 65,536-char `-c` ARG0. The parser expects `do`; diagnostic storage grew as token lines x ARG0 (a <110 KB request produced a >1.3 GB diagnostic), fanned out across stderr capture, replay buffers, and ack draining — a cross-session memory-exhaustion channel. A total-size cap alone was not enough: a long ARG0 stamped per line could evict the structural payload, so the shell name is truncated before the budget applies. ## Before / After Before (unfixed `origin/main`, analytic + prior failing run): multiline-token x long-ARG0 product allocated unbounded in the diagnostic before stderr capture. After (this branch): a 164 MB token-lines x ARG0 product (5,000 lines x 16 KiB ARG0) completes in ~14 ms with exit 2 and a 1024-byte diagnostic that preserves `syntax error near unexpected token`; all previously-covered child syntax errors still exit 2. ``` test result: ok. 11 passed (partial_parse incl. child_syntax_report_is_bounded + match_bash shape) test result: ok. 14 passed (error:: unit incl. exact-boundary + truncation-marker + long-shell-keeps-payload) test result: ok. syntax_diagnostic_multiline_times_arg0_is_bounded_in_child (200 + 2000 lines x 8 KiB ARG0, bounded child, exit 2, stderr <= 1024) cargo fmt --check: clean; cargo clippy -p bashkit --all-targets: clean ``` ## Risk - Low. Only truncates diagnostic text beyond 1 KiB / shell names beyond 64 chars; exit codes, stdout, and small-input stderr unchanged. No behavior change under the bounds. ## Checklist - [x] Tests added or updated - [x] Backward compatibility considered (internal code; truncation only beyond the bounds)
Corrective follow-up to #2673, addressing the blocking review there.
What changed
#2673 truncated indirect array targets at the first
], which broke valid nested subscripts (r='a[b[0]]'errored; both real bash versions printtwo). The boundary now ends at the close of the first complete[...]reference via a small nesting- and quote-aware scanner (indirect_array_end), so:invalid variable name(exit 1), like modern bash 5.x — so the exact self-cycler='a[0]:-${!r}]'and its mutual variant never reach the desugar re-parse instead of overflowing the stack;a[b[0]]),$i,[@], and assoc (h[$key],h[key]) references resolve whole, matching real bash.Review resolutions (blocking comment on #2673)
indirect_nested_indexed_matches_real_bashasserts byte equality with real bash; assoc direct/indirect agreement in-process (assoc needs bash 4+, noted in-test).indirect_hostile_cycles_terminate_in_bounded_child); Darwin limitation (no portable per-child CPU/stack/memory caps from std) documented in-test.Before / After (merged #2673 tree vs this branch)
Risk
invalid variable name, exit 1) instead of resolving or misparsing — verified against bash 3.2 (lenient, ignores tail) and bash 5.3 (strict, rejects); the refusal follows 5.xChecklist
Related to #2673.
Produced by yolop