You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit d42ae92
Browse filesBrowse the repository at this point in the historyBrowse files
A script that re-runs itself through `sh` (`echo 'sh s.sh' > s.sh; sh
s.sh`) nested in-process interpreters until the process stack overflowed,
aborting every tenant in the process. Child shells now count against
max_function_depth and a dedicated cap of 8 (TM-DOS-125, L-PROC-004).
Claude-Session: https://claude.ai/code/session_019aFikmptPc91Fj4N2iDXQA
Copy file name to clipboardExpand all lines: crates/bashkit/docs/threat-model.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -116,6 +116,7 @@ through configurable limits.
116
116
| Input wait vs. timeout (TM-DOS-120) | Abusing the keyboard-wait exclusion to run work past the deadline | Only time blocked reading the terminal is excluded; CPU work and `sleep` still count | MITIGATED |
117
117
| Concurrent pipeline amplification (TM-DOS-124) | An endless loop piped into `head`, a producer whose reader quit, or recursive pipelines `f() { f \| f; }`| 4 KiB pipes with backpressure; a writer whose reader is gone ends with 141 (SIGPIPE); streaming nesting counts toward `max_subshell_depth` and falls back to sequential stages past 4 levels; stages share the session budget, timeout and cancellation | MITIGATED |
118
118
| Random generator amplification (TM-DOS-123) |`openssl rand` asked for gigabytes | Requests capped at 1 MiB; fixed-size `uuidgen`/`$SRANDOM`| MITIGATED |
119
+
| Child-shell recursion (TM-DOS-125) | Script that re-runs itself via `sh`| Child shells count against function depth, max 8 nested | MITIGATED |
| Silent truncation at builtin caps (TM-DOS-109) |`seq 200000`, an awk loop past its cap, or an oversized `sprintf` expression returns incomplete output with exit 0 | Caps report `<cmd>: <what> limit (<N>) exceeded` on stderr and exit non-zero; awk caps and formatting errors are fatal |**MITIGATED**|
121
122
| In-builtin memory growth (TM-DOS-110) |`awk 'BEGIN { s = "x"; while (1) s = s s }'` or `jq -n '"x" \| until(false; . + .)'` allocates until the host aborts | awk checks each string against a 16 MiB cap before allocating it, caps `$N` field indexes, and caps total variable memory at `max_live_intermediate_bytes` (fatal, exit 2). jq meters every live string, array and object against the same limit and fails before growing (exit 5); non-emitting jq loops and pure recursion stop at the timeout; non-tail recursion hits a live-context ceiling (64) before host stack exhaustion |**MITIGATED**|
Copy file name to clipboardExpand all lines: knowledge/log.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,6 +2,7 @@
2
2
3
3
## 2026-10-07
4
4
5
+
***Security**: A script that re-runs itself through `sh`/`bash` crashed the whole process with a stack overflow (TM-DOS-125); nothing bounded nested child shells. They now count against `max_function_depth` plus their own cap of 8, measured as what fits a 2 MiB debug-build stack (about 15 bare `sh` levels fit; a level costs several function calls). Found while bounding `make` recursion.
5
6
***Fix**: awk follows POSIX `opt_nls`: a newline after the `)` of an `if`/`for`/`while` header, after `else`/`do`, `&&`, `||` or `,` continues the statement (it became `;`, so `for (...)⏎ body` failed with `unexpected character: ;`, the most frequent awk error in gap telemetry, 22 replayed scripts). `do body; while (...)` accepts the `;`. A regex rule pattern can be an operand (`/=/ && !/^#/`, `/^$/ || /^;/`), and a bare `/re/` in an expression is `$0 ~ /re/` (it was the truthy pattern text, so `if (/^\[/)` matched every line).
6
7
***Feature**: Missing everyday commands: `arch`, `sum`, `shasum`, `egrep`, `fgrep`, `link`, `unlink`, `chgrp`, `nohup`, `nice`, `flock`, `getconf`, `tty`, `sync`, `hostid`, `groups`, `logname`, `users`, `who`, `uptime`, `free`. Output matches GNU coreutils/util-linux where the answer is not host identity (spec `system-commands.test.sh` checked against real bash); identity answers (`free`, `uptime`, `groups`, `hostid`) are virtual and agree with `nproc` and `/proc/meminfo`. `nohup`/`nice`/`flock` just run their command: there are no signals, priorities or competing lock holders inside one session. `/bin/bash` and `/bin/sh` now exist in the root filesystem, and `type`/`command -V` report interpreter-dispatched builtins (`builtin`, `command`, `let`, `typeset`, `exec`, `getopts`, `declare`) instead of "not found".
7
8
***Fix**: `cmd N>file` with N≥3 used to send the command's stdout into the file, so `( flock -n 9 && ... ) 9>lock` printed nothing. Such redirects now open fd N only: stdout is untouched, the file is created/truncated, and writes to `>&N` inside a compound command or function land in it.
Copy file name to clipboardExpand all lines: knowledge/operations/limitations.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -45,6 +45,7 @@ execution model. Evidence is a threat-model ID, a test, or `stance`
45
45
| L-PROC-001 |`exec` does not replace the process; `exec cmd` runs cmd then stops execution (fd redirects work) | True process replace would break sandbox containment | TM-ESC-005 |
46
46
| L-PROC-002 | Background jobs run concurrently (`jobs`, `wait -n`, `kill`, `ps`, `pgrep`), but cannot be stopped or resumed: `kill -STOP/-CONT` are ignored, there is no `suspend`/Ctrl-Z, `bg` is a no-op and `fg` just waits; jobs end when `exec()` returns | No terminal process groups in a virtual shell; a job must not outlive the call that owns it (TM-DOS-122) |`l_proc_002_no_job_control`|
47
47
| L-PROC-003 | No process spawning; external commands run as builtins | Core sandbox model: no fork/exec escape surface |`l_proc_003_no_process_spawning`|
48
+
| L-PROC-004 |`bash`/`sh` child shells nest at most 8 deep (and count against `max_function_depth`); deeper nesting fails `maximum nesting depth exceeded`| Child shells run in-process on one stack; unbounded nesting crashed the process (TM-DOS-125) |`l_proc_004_child_shell_depth`|
48
49
| L-PIPE-001 | Pipeline stages stream only from the first stage that runs shell code (loop, group, function, `eval`). Leading single-builtin stages run to completion first and hand over their whole output, so they never get SIGPIPE: `yes \| head -1` gives `PIPESTATUS``1 0` plus yes's output-cap notice, where bash gives `141 0`. Downstream of a streaming stage, a single builtin reads all its input before it starts, except `head` and `read`: `while :; do echo; done \| cat \| head -1` runs until a limit. Pipes hold 4 KiB, not 64 KiB, so a finite loop writing more than that into an early-exiting reader exits 141 where bash would exit 0. Pipelines nested more than 4 subshells deep run their stages in sequence | Builtins return their output as one value, not a stream; streaming them needs a reader/writer `Context`. The smaller pipe bounds how far a producer runs ahead (each byte costs budgeted commands); the nesting cap bounds native stack (TM-DOS-124) |`l_pipe_001_stages_run_sequentially`|
49
50
| L-RAND-001 |`uuidgen` makes random (v4) UUIDs only (no `-t`, `-m`, `-s`); `openssl` implements only `rand`| Time/MAC-based UUIDs would leak host identity; a full TLS/crypto toolkit is out of scope |`uuidgen_time_based_unsupported`|
50
51
| L-FS-001 | Symlinks are followed, but `..` after a linked directory resolves lexically (`/link/..` is the link's parent, the `cd -L` view), and `ln` without `-s` (and `link`) makes a symlink, not a hard link | The interpreter normalizes paths before the VFS sees them; the VFS has no inodes to share |`symlink.test.sh`, `ln_default_symbolic`|
Copy file name to clipboardExpand all lines: knowledge/security/threat-model.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -292,6 +292,7 @@ runaway scripts without permanently breaking the session.
292
292
| TM-DOS-122 | Background job flooding |`while :; do sleep 99 & done`, or a fork bomb `f(){ f & f & }; f`, keeps unbounded concurrent forked interpreters alive |`ExecutionLimits::max_background_jobs` (default 64, `hardened()` 16) caps live jobs; the next `&` prints bash's `fork: retry: Resource temporarily unavailable` and returns 1 without forking. Jobs are polled on the caller's task (no `tokio::spawn`), share the session's command/loop budget, timeout and cancel token, and `exec()` waits for or drops every job before returning, so nothing outlives the call. Regressions: `jobs::tests::*`, `jobs.test.sh` spec cases, `background_job_limit`|**MITIGATED**|
293
293
| TM-DOS-123 | Random generator amplification |`openssl rand 99999999999` asks one builtin call for gigabytes of CSPRNG output |`openssl rand` caps a request at `OPENSSL_RAND_MAX_BYTES` (1 MiB) and fails above it; `uuidgen` and `$SRANDOM` emit fixed sizes. Bytes come from `getrandom` (OS CSPRNG), never the `$RANDOM` LCG. Regression: `builtins::random::tests::openssl_rand_limits`|**MITIGATED**|
294
294
| TM-DOS-124 | Concurrent pipeline amplification | An endless producer (`while :; do echo x; done \| head`) buffering without bound, a producer whose reader quit running forever, or recursive pipelines (`f() { f \| f; }`) nesting forked stages and native stack | Pipes hold 4 KiB and writers wait at command boundaries; a writer whose reader is gone ends with 141 at its next command; a reader that never reads stdin ends the producer when it finishes. A streaming group counts as one subshell level (`max_subshell_depth`) and past 4 levels stages run in sequence, keeping stack cost near plain function recursion. Stages are forked like jobs: they share the execution budget, timeout and cancel token and never outlive the pipeline. Regressions: `pipeline_tests::threat_endless_producer_into_non_reader_ends`, `threat_recursive_pipeline_is_bounded`, `pipes-redirects.test.sh` SIGPIPE cases |**MITIGATED**|
295
+
| TM-DOS-125 | Child-shell recursion stack overflow | A script that runs itself via `sh`/`bash` (`echo 'sh s.sh' > s.sh; sh s.sh`) nested interpreters until the process stack overflowed, aborting every tenant in the process | Each child shell counts against `max_function_depth` and a separate cap of `MAX_CHILD_SHELL_DEPTH` (8) nested shells, sized so the deepest mix fits a 2 MiB debug stack; over the cap the shell fails `maximum nesting depth exceeded` (exit 2). Forked jobs inherit the depth because they are polled on the caller's stack. Regression: `recursive_child_shell_is_bounded`|**MITIGATED**|
295
296
| TM-DOS-101 | yq structured-data amplification | YAML aliases can expand exponentially during deserialization; deep YAML/JSON, document floods, jaq generators, and YAML re-serialization can consume stack, CPU, or memory beyond the source size | Reject YAML alias tokens with a bounded lexical pass before deserialization; VFS/stdin and aggregate input budgets; serde_yaml_ng recursion cap 128 plus Bashkit depth 100; 4096-document cap; shared jaq work/deadline/output limits; post-serialization stdout cap; YAML+JSON depth/document/input/output regressions plus `yq_fuzz` and arbitrary-input proptest |**MITIGATED**|
296
297
| TM-DOS-107 | Static-analysis command validation amplification | A large comment before thousands of commands makes a whole-script ordered-subsequence scan per command | Build one source-character position index, then validate each analyzed command-name character with a logarithmic position lookup instead of rescanning source |**MITIGATED**|
297
298
**TM-DOS-051** is historical. The custom parser was deleted with the `yaml`
0 commit comments