Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -877,13 +877,33 @@ impl Interpreter {
&self.limits
}

/// `set -o` option variable names (SHOPT_e, SHOPT_x, etc.) that are
/// transient and must be reset between exec() calls (TM-ISO-023).
/// `shopt` options (SHOPT_expand_aliases, SHOPT_extglob, etc.) are
/// persistent session configuration and are NOT reset.
const SET_OPTION_VARS: &'static [&'static str] = &[
"SHOPT_a",
"SHOPT_e",
"SHOPT_f",
"SHOPT_n",
"SHOPT_u",
"SHOPT_v",
"SHOPT_x",
"SHOPT_C",
"SHOPT_pipefail",
];

/// THREAT[TM-ISO-005/006/007]: Reset per-exec transient state.
/// Called by Bash::exec() before each top-level execution to prevent
/// traps, exit code, and shell options from leaking across calls.
/// traps, exit code, and `set` options from leaking across calls.
/// `shopt` options (expand_aliases, extglob, etc.) are intentionally
/// preserved — they are persistent session configuration.
pub fn reset_transient_state(&mut self) {
self.traps.clear();
self.last_exit_code = 0;
self.variables.retain(|k, _| !k.starts_with("SHOPT_"));
for var in Self::SET_OPTION_VARS {
self.variables.remove(*var);
}
}

/// Set an environment variable.
Expand Down
6 changes: 3 additions & 3 deletions crates/bashkit/tests/snapshot_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,9 @@ async fn shell_options_survive_snapshot_roundtrip() {
let mut bash2 = Bash::new();
bash2.restore_shell_state(&restored);

// exec() calls reset_transient_state which clears SHOPT_* vars,
// so we verify the state was restored correctly by inspecting it
// before the next exec() call.
// `set` options (SHOPT_e, SHOPT_pipefail) are transient — they are
// cleared by reset_transient_state() between exec() calls (TM-ISO-023).
// Verify the snapshot restored them correctly before the next exec().
let state2 = bash2.shell_state();
assert_eq!(
state2.variables.get("SHOPT_e").map(|s| s.as_str()),
Expand Down
18 changes: 18 additions & 0 deletions crates/bashkit/tests/threat_model_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,24 @@ mod session_isolation {
assert!(!result.stdout.contains("LEAKED"));
}

/// Alias expansion must work across separate exec() calls (issue #1130).
/// shopt -s expand_aliases set in one exec() must persist to the next.
#[tokio::test]
async fn alias_expansion_persists_across_exec_calls() {
let mut bash = Bash::new();

bash.exec("shopt -s expand_aliases").await.unwrap();
bash.exec("alias ll='echo alias_worked'").await.unwrap();

let result = bash.exec("ll").await.unwrap();
assert_eq!(
result.exit_code, 0,
"alias should resolve: {}",
result.stderr
);
assert_eq!(result.stdout.trim(), "alias_worked");
}

/// TM-ISO-020: Trap handlers in one session must not fire in another
#[tokio::test]
async fn threat_isolation_trap_isolation() {
Expand Down
4 changes: 2 additions & 2 deletions specs/006-threat-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@ Only exact domain matches are allowed (TM-NET-017).
| TM-ISO-006 | No per-instance variable/memory budget | Unbounded `HashMap` growth in variables, arrays, functions exhausts process memory | — | **OPEN** |
| TM-ISO-021 | EXIT trap leaks across `exec()` calls | EXIT trap set in one `exec()` fires in subsequent `exec()` calls on same Bash instance | — | **OPEN** |
| TM-ISO-022 | `$?` leaks across `exec()` calls | Exit code from one `exec()` visible as `$?` in next `exec()` instead of resetting to 0 | — | **OPEN** |
| TM-ISO-023 | `set -e` leaks across `exec()` calls | Shell options (`set -e`, etc.) persist across `exec()` calls, causing unexpected abort behavior | | **OPEN** |
| TM-ISO-023 | `set -e` leaks across `exec()` calls | `set` options (`-e`, `-x`, etc.) persist across `exec()` calls, causing unexpected abort behavior | `reset_transient_state()` clears `SET_OPTION_VARS` | **FIXED** |

**TM-ISO-004**: Fixed. The jq builtin now injects shell variables via a custom jaq context variable
(`$__bashkit_env__`) and overrides the `env` filter to read from it instead of `std::env`.
Expand Down Expand Up @@ -1224,7 +1224,7 @@ This section maps former vulnerability IDs to the new threat ID scheme and track
| TM-INJ-021 | `export` overwrites readonly variables | Integrity bypass — `export X=new` overwrites `readonly X=old` | Check readonly attribute in export assignment path |
| TM-ISO-021 | EXIT trap leaks across `exec()` calls | Cross-invocation interference — trap from exec N fires in exec N+1 | Reset traps in `reset_for_execution()` |
| TM-ISO-022 | `$?` leaks across `exec()` calls | State pollution — exit code from previous exec visible to next exec | Reset `last_exit_code` in `reset_for_execution()` |
| TM-ISO-023 | `set -e` leaks across `exec()` calls | Unexpected abort — shell options from previous exec affect next exec | Reset shell options in `reset_for_execution()` |
| TM-ISO-023 | `set -e` leaks across `exec()` calls | Unexpected abort — `set` options from previous exec affect next exec | `SET_OPTION_VARS` cleared in `reset_transient_state()` (**FIXED**) |
| TM-ISO-024 | `$?` leaks into VFS subprocess | Parent `last_exit_code` visible inside VFS script subprocess, causing false `set -e` failures | Reset `last_exit_code = 0` and `nounset_error = None` in `execute_script_content` subprocess isolation |
| TM-INT-007 | `/dev/urandom` empty with `head -c` | Weak randomness — `head -c 16 /dev/urandom` returns empty string | Fix virtual device pipe handling in head builtin |
| TM-DOS-044 | Nested `$()` stack overflow (regression) | Process crash (SIGABRT) at depth ~50 despite #492 fix | Interpreter execution path may need separate depth tracking from lexer fix |
Expand Down
Loading