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
6 changes: 5 additions & 1 deletion crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4013,14 +4013,18 @@ impl Interpreter {
let saved_aliases = self.aliases.clone();
let saved_coproc = self.coproc_buffers.clone();

// Child only sees exported variables (env), not all shell variables
// Child only sees exported variables (env), not all shell variables.
// Reset last_exit_code so $? starts at 0 (matches real bash subprocess).
// Clear nounset_error to prevent parent expansion errors from leaking.
self.variables = self.env.clone();
self.arrays.clear();
self.assoc_arrays.clear();
self.functions.clear();
self.traps.clear();
self.aliases.clear();
self.coproc_buffers.clear();
self.last_exit_code = 0;
self.nounset_error = None;

// Push call frame: $0 = script name, $1..N = args
self.call_stack = vec![CallFrame {
Expand Down
89 changes: 89 additions & 0 deletions crates/bashkit/tests/script_execution_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,3 +409,92 @@ async fn declare_f_no_args_lists_all_functions() {
assert!(result.stdout.contains("foo"));
assert!(result.stdout.contains("bar"));
}

/// Issue #842: $? should be 0 at start of VFS script subprocess, even when
/// parent had non-zero exit code. Subprocess isolation must reset last_exit_code
/// to match real bash subprocess behavior.
#[tokio::test]
async fn exec_vfs_script_initial_exit_code_is_zero() {
let mut bash = Bash::new();
let fs = bash.fs();

fs.write_file(Path::new("/check_exit.sh"), b"#!/bin/bash\necho $?\n")
.await
.unwrap();
fs.chmod(Path::new("/check_exit.sh"), 0o755).await.unwrap();

// Parent has non-zero exit code, then run VFS script
let result = bash.exec("false; /check_exit.sh").await.unwrap();
assert_eq!(
result.stdout.trim(),
"0",
"VFS script subprocess should start with $?=0 (like real bash); got stdout={:?}",
result.stdout
);
}

/// Issue #842: `set -euo pipefail` in VFS script should not trigger false failure
/// even after a prior non-zero exit code in the parent shell.
#[tokio::test]
async fn exec_vfs_script_set_e_after_prior_failure() {
let mut bash = Bash::new();
let fs = bash.fs();

fs.write_file(
Path::new("/test.sh"),
br#"#!/bin/bash
set -euo pipefail
MY_VAR="hello"
echo "${MY_VAR}"
"#,
)
.await
.unwrap();
fs.chmod(Path::new("/test.sh"), 0o755).await.unwrap();

let result = bash.exec("false; /test.sh").await.unwrap();
assert_eq!(
result.exit_code, 0,
"VFS script with set -e should succeed after prior failure; stdout={:?}, stderr={:?}",
result.stdout, result.stderr
);
assert_eq!(result.stdout.trim(), "hello");
}

/// Issue #842: Nested VFS scripts with set -e and command substitution.
#[tokio::test]
async fn exec_vfs_script_set_e_nested_scripts() {
let mut bash = Bash::new();
let fs = bash.fs();

fs.write_file(
Path::new("/inner.sh"),
br#"#!/bin/bash
set -euo pipefail
echo "inner_output"
"#,
)
.await
.unwrap();
fs.chmod(Path::new("/inner.sh"), 0o755).await.unwrap();

fs.write_file(
Path::new("/outer.sh"),
br#"#!/bin/bash
set -euo pipefail
RESULT="$(/inner.sh)"
echo "${RESULT}"
"#,
)
.await
.unwrap();
fs.chmod(Path::new("/outer.sh"), 0o755).await.unwrap();

let result = bash.exec("/outer.sh").await.unwrap();
assert_eq!(
result.exit_code, 0,
"Nested VFS scripts with set -e should work; stdout={:?}, stderr={:?}",
result.stdout, result.stderr
);
assert_eq!(result.stdout.trim(), "inner_output");
}
1 change: 1 addition & 0 deletions specs/006-threat-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,7 @@ This section maps former vulnerability IDs to the new threat ID scheme and track
| 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-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
2 changes: 1 addition & 1 deletion supply-chain/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ version = "1.1.0"
criteria = "safe-to-deploy"

[[exemptions.cmake]]
version = "0.1.57"
version = "0.1.58"
criteria = "safe-to-deploy"

[[exemptions.cobs]]
Expand Down
Loading