diff --git a/crates/bashkit/src/builtins/awk.rs b/crates/bashkit/src/builtins/awk.rs index 1896753f..cf326f64 100644 --- a/crates/bashkit/src/builtins/awk.rs +++ b/crates/bashkit/src/builtins/awk.rs @@ -55,7 +55,6 @@ enum AwkPattern { } #[derive(Debug, Clone)] -#[allow(dead_code)] // Regex and Match used for pattern matching expansion enum AwkExpr { Number(f64), String(String), @@ -69,7 +68,8 @@ enum AwkExpr { Concat(Vec), FuncCall(String, Vec), Regex(String), - Match(Box, String), // expr ~ /pattern/ + #[allow(dead_code)] // matched in eval but construction deferred to pattern expansion + Match(Box, String), // expr ~ /pattern/ PostIncrement(String), // var++ PostDecrement(String), // var-- PreIncrement(String), // ++var @@ -114,7 +114,6 @@ enum AwkAction { var: Option, file: AwkExpr, }, - #[allow(dead_code)] // Exit code support for future Exit(Option), Return(Option), Expression(AwkExpr), @@ -948,7 +947,7 @@ impl<'a> AwkParser<'a> { Ok(Some(AwkOutputTarget::Truncate(target))) } } else if c == '|' { - // TODO: pipe output (e.g., `print ... | "cmd"`) not yet supported + // Pipe output (e.g., `print ... | "cmd"`) not supported in virtual mode Err(Error::Execution( "awk: pipe output redirection (|) is not supported".to_string(), )) diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index 362b915e..6f2456cf 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -7273,8 +7273,14 @@ impl Interpreter { // ${#arr[@]} or ${#arr[*]} — array length if let Some(rest) = inner.strip_prefix('#') { if let Some(bracket) = rest.find('[') { + // Guard against malformed input like ${#[} where bracket+1 > len-1 + let end = rest.len().saturating_sub(1); + if bracket + 1 > end { + // Malformed — treat as string length of empty var + return "0".to_string(); + } let arr_name = &rest[..bracket]; - let idx = &rest[bracket + 1..rest.len().saturating_sub(1)]; + let idx = &rest[bracket + 1..end]; if idx == "@" || idx == "*" { if let Some(arr) = self.arrays.get(arr_name) { return arr.len().to_string(); diff --git a/crates/bashkit/tests/threat_model_tests.rs b/crates/bashkit/tests/threat_model_tests.rs index fd8f6e9e..03f2feb5 100644 --- a/crates/bashkit/tests/threat_model_tests.rs +++ b/crates/bashkit/tests/threat_model_tests.rs @@ -3827,4 +3827,16 @@ mod trace_events { "should record CommandExit with code 127 for not-found" ); } + + // TM-DOS-029: Malformed ${#[} must not panic (fuzz crash from CI) + #[tokio::test] + async fn arithmetic_malformed_brace_length_no_panic() { + let mut bash = Bash::new(); + // Input discovered by arithmetic_fuzz: [${#[ + // Triggered panic "byte range starts at 1 but ends at 0" in + // expand_brace_expr_in_arithmetic when rest="[" and bracket=0. + let r = bash.exec("echo $((0 + ${#[}))").await.unwrap(); + // Should not panic — just return 0 for malformed expression + assert_eq!(r.exit_code, 0); + } } diff --git a/docs/security.md b/docs/security.md index 0265aca8..d7f6badd 100644 --- a/docs/security.md +++ b/docs/security.md @@ -56,7 +56,7 @@ These decisions are documented in [`specs/008-posix-compliance.md`](../specs/008 Bashkit uses multiple layers of security testing: -**Threat model tests** — Over 50 tests in `threat_model_tests.rs` that directly +**Threat model tests** — 185 tests in `threat_model_tests.rs` that directly validate mitigations against documented threat IDs. Each test maps to a specific `TM-*` threat. diff --git a/specs/009-implementation-status.md b/specs/009-implementation-status.md index 9a9de552..7506d29f 100644 --- a/specs/009-implementation-status.md +++ b/specs/009-implementation-status.md @@ -103,28 +103,28 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See ## Spec Test Coverage -**Total spec test cases:** 2382 (2354 pass, 28 skip) +**Total spec test cases:** 2278 (2252 pass, 26 skip) | Category | Cases | In CI | Pass | Skip | Notes | |----------|-------|-------|------|------|-------| -| Bash (core) | 1934 | Yes | 1909 | 25 | `bash_spec_tests` in CI | -| AWK | 104 | Yes | 104 | 0 | loops, arrays, -v, ternary, field assign, getline, %.6g | -| Grep | 91 | Yes | 91 | 0 | -z, -r, -a, -b, -H, -h, -f, -P, --include, --exclude, binary detect | +| Bash (core) | 1809 | Yes | 1786 | 23 | `bash_spec_tests` in CI | +| AWK | 126 | Yes | 126 | 0 | loops, arrays, -v, ternary, field assign, getline, %.6g, delete, dev-stderr | +| Grep | 95 | Yes | 95 | 0 | -z, -r, -a, -b, -H, -h, -f, -P, --include, --exclude, binary detect, rg | | Sed | 75 | Yes | 75 | 0 | hold space, change, regex ranges, -E | -| JQ | 120 | Yes | 119 | 1 | reduce, walk, regex funcs, --arg/--argjson, combined flags, input/inputs, env | -| Python | 58 | Yes | 56 | 2 | embedded Python (Monty) | -| **Total** | **2382** | **Yes** | **2354** | **28** | | +| JQ | 116 | Yes | 115 | 1 | reduce, walk, regex funcs, --arg/--argjson, combined flags, input/inputs, env | +| Python | 57 | Yes | 55 | 2 | embedded Python (Monty) | +| **Total** | **2278** | **Yes** | **2252** | **26** | | ### Bash Spec Tests Breakdown | File | Cases | Notes | |------|-------|-------| -| alias.test.sh | 15 | alias expansion (15 skipped) | -| arith-dynamic.test.sh | 14 | dynamic arithmetic contexts (5 skipped) | +| alias.test.sh | 15 | alias expansion (1 skipped) | +| arith-dynamic.test.sh | 14 | dynamic arithmetic contexts | | arithmetic.test.sh | 68 | includes logical, bitwise, compound assign, increment/decrement, `let` builtin, `declare -i` arithmetic | | array-slicing.test.sh | 8 | array slice operations | | arrays.test.sh | 27 | indices, `${arr[@]}` / `${arr[*]}`, negative indexing `${arr[-1]}` | -| assoc-arrays.test.sh | 15 | associative arrays `declare -A` | +| assoc-arrays.test.sh | 19 | associative arrays `declare -A` | | background.test.sh | 2 | background job handling | | bash-command.test.sh | 25 | bash/sh re-invocation | | bash-flags.test.sh | 13 | bash `-e`, `-x`, `-u`, `-f`, `-o option` flags | @@ -136,8 +136,9 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See | command.test.sh | 9 | `command -v`, `-V`, function bypass | | command-not-found.test.sh | 9 | unknown command handling | | cmd-suggestions.test.sh | 4 | command suggestions on typos | -| command-subst.test.sh | 25 | includes backtick substitution, nested quotes in `$()` | -| conditional.test.sh | 24 | `[[ ]]` conditionals, `=~` regex, BASH_REMATCH, glob `==`/`!=` | +| command-subst.test.sh | 29 | includes backtick substitution, nested quotes in `$()` | +| compgen-path.test.sh | 2 | compgen PATH completion | +| conditional.test.sh | 29 | `[[ ]]` conditionals, `=~` regex, BASH_REMATCH, glob `==`/`!=` | | control-flow.test.sh | 58 | if/elif/else, for, while, case `;;`/`;&`/`;;&`, select, trap ERR, `[[ =~ ]]` BASH_REMATCH, compound input redirects | | comm.test.sh | 6 | comm column comparison | | cuttr.test.sh | 39 | cut and tr commands, `-z` zero-terminated | @@ -148,16 +149,17 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See | du.test.sh | 4 | disk usage reporting | | dirstack.test.sh | 12 | `pushd`, `popd`, `dirs` directory stack operations | | echo.test.sh | 24 | escape sequences | -| empty-bodies.test.sh | 8 | empty loop/function bodies (5 skipped) | +| empty-bodies.test.sh | 8 | empty loop/function bodies | | env.test.sh | 3 | environment variable operations | -| errexit.test.sh | 8 | set -e tests | +| errexit.test.sh | 11 | set -e tests | | eval-bugs.test.sh | 4 | regression tests for eval/script bugs | +| exec-command.test.sh | 5 | exec builtin | | exit-status.test.sh | 18 | exit code propagation | | expr.test.sh | 13 | `expr` arithmetic, string ops, pattern matching, exit codes | | extglob.test.sh | 15 | `@()`, `?()`, `*()`, `+()`, `!()` extended globs | | file.test.sh | 8 | file type detection | | fileops.test.sh | 28 | `mktemp`, `-d`, `-p`, template | -| find.test.sh | 10 | file search | +| find.test.sh | 19 | file search | | functions.test.sh | 26 | local dynamic scoping, nested writes, FUNCNAME call stack, `caller` builtin | | getopts.test.sh | 9 | POSIX option parsing, combined flags, silent mode | | glob-options.test.sh | 13 | dotglob, nocaseglob, failglob, nullglob, noglob, globstar | @@ -165,53 +167,58 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See | gzip.test.sh | 2 | gzip/gunzip compression | | headtail.test.sh | 14 | | | heredoc.test.sh | 13 | heredoc variable expansion, quoted delimiters, file redirects, `<<-` tab strip | -| heredoc-edge.test.sh | 15 | heredoc edge cases (6 skipped) | +| heredoc-edge.test.sh | 15 | heredoc edge cases | | herestring.test.sh | 8 | here-string `<<<` | | hextools.test.sh | 4 | od/xxd/hexdump (3 skipped) | | history.test.sh | 2 | history builtin | | less.test.sh | 3 | less pager | | ln.test.sh | 5 | `ln -s`, `-f`, symlink creation | -| nameref.test.sh | 14 | nameref variables (14 skipped) | +| ls.test.sh | 4 | ls directory listing | +| nameref-assoc.test.sh | 7 | nameref with associative arrays | +| nameref.test.sh | 23 | nameref variables (1 skipped) | | negative-tests.test.sh | 13 | error conditions | | nl.test.sh | 14 | line numbering | | nounset.test.sh | 7 | `set -u` unbound variable checks, `${var:-default}` nounset-aware | -| parse-errors.test.sh | 18 | syntax error detection (13 skipped) | +| parse-errors.test.sh | 18 | syntax error detection (4 skipped) | | paste.test.sh | 4 | line merging with `-s` serial and `-d` delimiter | | path.test.sh | 18 | basename, dirname, `realpath` canonical path resolution | | pipes-redirects.test.sh | 26 | includes stderr redirects | | printenv.test.sh | 2 | printenv builtin | | printf.test.sh | 32 | format specifiers, array expansion, `-v` variable assignment, `%q` shell quoting | | procsub.test.sh | 11 | process substitution | -| quote.test.sh | 35 | quoting edge cases (2 skipped) | -| read-builtin.test.sh | 10 | `read` builtin, IFS splitting, `-r`, `-a` (array), `-n` (nchars), here-string | -| script-exec.test.sh | 10 | script execution by path, $PATH search, exit codes | +| quote.test.sh | 42 | quoting edge cases | +| read-builtin.test.sh | 12 | `read` builtin, IFS splitting, `-r`, `-a` (array), `-n` (nchars), here-string | +| script-exec.test.sh | 14 | script execution by path, $PATH search, exit codes | | seq.test.sh | 12 | `seq` numeric sequences, `-w`, `-s`, decrement, negative | +| set-allexport.test.sh | 5 | set -a / allexport | | shell-grammar.test.sh | 23 | shell grammar edge cases | | sleep.test.sh | 9 | sleep timing | -| sortuniq.test.sh | 32 | sort `-f`/`-n`/`-r`/`-u`/`-V`/`-t`/`-k`/`-s`/`-c`/`-h`/`-M`/`-m`/`-z`/`-o`, uniq `-c`/`-d`/`-u`/`-i`/`-f` | +| sortuniq.test.sh | 39 | sort `-f`/`-n`/`-r`/`-u`/`-V`/`-t`/`-k`/`-s`/`-c`/`-h`/`-M`/`-m`/`-z`/`-o`, uniq `-c`/`-d`/`-u`/`-i`/`-f` | | source.test.sh | 19 | source/., function loading, PATH search, positional params | | stat.test.sh | 7 | stat file information | | string-ops.test.sh | 14 | string replacement (prefix/suffix anchored), `${var:?}`, case conversion | | strings.test.sh | 6 | strings extraction | -| subshell.test.sh | 13 | subshell execution (4 skipped) | +| subprocess-isolation.test.sh | 8 | subprocess variable isolation | +| subshell.test.sh | 13 | subshell execution | | tar.test.sh | 8 | tar archive operations | | tee.test.sh | 6 | tee output splitting | | temp-binding.test.sh | 10 | temporary variable bindings `VAR=val cmd` | -| test-operators.test.sh | 27 | file/string tests, `-nt`/`-ot`/`-ef` file comparisons | +| test-operators.test.sh | 29 | file/string tests, `-nt`/`-ot`/`-ef` file comparisons | +| test-tty.test.sh | 5 | tty detection tests | | textrev.test.sh | 14 | `tac` reverse line order, `rev` reverse characters, `yes` repeated output | | time.test.sh | 11 | Wall-clock only (user/sys always 0) | | timeout.test.sh | 16 | | | type.test.sh | 15 | `type`, `which`, `hash` builtins | | unicode.test.sh | 17 | unicode handling (3 skipped) | -| var-op-test.test.sh | 21 | variable operations (16 skipped) | +| var-op-test.test.sh | 26 | variable operations (1 skipped) | | variables.test.sh | 97 | includes special vars, prefix env, PIPESTATUS, trap EXIT, `${var@Q}`, `\` line continuation, PWD/HOME/USER/HOSTNAME/BASH_VERSION/SECONDS, `set -x` xtrace, `shopt` builtin, nullglob, `set -o`/`set +o` display, `trap -p` | | wait.test.sh | 2 | wait builtin | | watch.test.sh | 2 | watch command | | wc.test.sh | 20 | word count | -| word-split.test.sh | 39 | IFS word splitting (36 skipped) | -| xargs.test.sh | 7 | xargs command | -| blackbox-edge-cases.test.sh | 92 | edge cases for quoting, expansion, redirection, error handling | -| blackbox-exploration.test.sh | 206 | broad coverage exploration: builtins, pipelines, subshells, traps | +| word-split.test.sh | 39 | IFS word splitting (10 skipped) | +| xargs.test.sh | 7 | xargs command (1 skipped) | +| blackbox-edge-cases.test.sh | 89 | edge cases for quoting, expansion, redirection, error handling | +| blackbox-exploration.test.sh | 199 | broad coverage exploration: builtins, pipelines, subshells, traps | ## Shell Features diff --git a/specs/012-maintenance.md b/specs/012-maintenance.md index 8c8dbf17..7d204245 100644 --- a/specs/012-maintenance.md +++ b/specs/012-maintenance.md @@ -119,6 +119,24 @@ Failures persisting **>2 consecutive days** are blocking: 3. Assign to most recent contributor in failing area 4. If upstream dep change: pin to known-good rev, open follow-up issue +## Deferred Items + +When a maintenance pass identifies issues too large to fix inline (e.g. +multi-file refactors, cross-cutting changes), the pass must: + +1. Create a GitHub issue for each deferred item with clear scope and reproduction steps +2. Record the issue numbers in the summary below so they are tracked + +Deferred items are **not** failures — they are expected for large-scope +improvements. The requirement is that they are **tracked**, not silently skipped. + +### Deferred from 2026-03-27 run + +| Issue | Section | Description | +|-------|---------|-------------| +| #880 | Simplification | Migrate 27 builtins from manual arg parsing to ArgParser | +| #881 | Simplification | Extract errexit suppression propagation helper | + ## Automation Sections dependencies, tests, examples, code quality, and nightly CI are fully