From 3bb83cdb2443fdd2e5a6cbb3057febbe532e9389 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Tue, 31 Mar 2026 02:48:12 +0000 Subject: [PATCH 1/2] fix(interpreter): exit builtin terminates execution in compound commands The exit builtin returned a plain exit code without ControlFlow signaling, so commands after exit in if/while/for/case blocks kept running. Added ControlFlow::Exit variant, propagated through the interpreter, consumed at subshell and command substitution boundaries (matching bash semantics). --- crates/bashkit/src/builtins/flow.rs | 8 ++- crates/bashkit/src/interpreter/mod.rs | 22 ++++++ crates/bashkit/src/interpreter/state.rs | 5 +- .../tests/spec_cases/bash/exit-status.test.sh | 71 +++++++++++++++++++ 4 files changed, 102 insertions(+), 4 deletions(-) diff --git a/crates/bashkit/src/builtins/flow.rs b/crates/bashkit/src/builtins/flow.rs index cffad1e5..7f20df49 100644 --- a/crates/bashkit/src/builtins/flow.rs +++ b/crates/bashkit/src/builtins/flow.rs @@ -56,9 +56,11 @@ impl Builtin for Exit { .unwrap_or(0) & 0xFF; - // For now, we just return the exit code - // In a full implementation, this would terminate the shell - Ok(ExecResult::err(String::new(), exit_code)) + Ok(ExecResult { + exit_code, + control_flow: ControlFlow::Exit(exit_code), + ..Default::default() + }) } } diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index e243c323..a4012044 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -1408,6 +1408,16 @@ impl Interpreter { self.last_exit_code = saved_exit; self.aliases = saved_aliases; self.coproc_buffers = saved_coproc; + + // Consume Exit control flow at subshell boundary — exit only + // terminates the subshell, not the parent shell. + if let Ok(ref mut res) = result + && let ControlFlow::Exit(code) = res.control_flow + { + res.exit_code = code; + res.control_flow = ControlFlow::None; + } + result } CompoundCommand::BraceGroup(commands) => self.execute_command_sequence(commands).await, @@ -1680,6 +1690,15 @@ impl Interpreter { ..Default::default() }); } + ControlFlow::Exit(code) => { + return Ok(ExecResult { + stdout, + stderr, + exit_code: code, + control_flow: ControlFlow::Exit(code), + ..Default::default() + }); + } ControlFlow::None => {} } } @@ -5880,6 +5899,9 @@ impl Interpreter { let cmd_result = self.execute_command(cmd).await?; stdout.push_str(&cmd_result.stdout); self.last_exit_code = cmd_result.exit_code; + if matches!(cmd_result.control_flow, ControlFlow::Exit(_)) { + break; + } } // Fire EXIT trap set inside the command substitution if let Some(trap_cmd) = self.traps.get("EXIT").cloned() diff --git a/crates/bashkit/src/interpreter/state.rs b/crates/bashkit/src/interpreter/state.rs index 1c7ca17a..9e455c7e 100644 --- a/crates/bashkit/src/interpreter/state.rs +++ b/crates/bashkit/src/interpreter/state.rs @@ -11,6 +11,8 @@ pub enum ControlFlow { Continue(u32), /// Return from a function (with exit code) Return(i32), + /// Exit the shell (with exit code) + Exit(i32), } /// Structured side-effect channel for builtins that need to communicate @@ -174,6 +176,7 @@ impl LoopAccumulator { ControlFlow::Return(code) => { LoopAction::Exit(self.build_exit(ControlFlow::Return(code))) } + ControlFlow::Exit(code) => LoopAction::Exit(self.build_exit(ControlFlow::Exit(code))), ControlFlow::None => LoopAction::None, } } @@ -193,7 +196,7 @@ impl LoopAccumulator { /// Build an exit result, draining accumulated stdout/stderr. fn build_exit(&mut self, control_flow: ControlFlow) -> ExecResult { let exit_code = match control_flow { - ControlFlow::Return(code) => code, + ControlFlow::Return(code) | ControlFlow::Exit(code) => code, _ => self.exit_code, }; ExecResult { diff --git a/crates/bashkit/tests/spec_cases/bash/exit-status.test.sh b/crates/bashkit/tests/spec_cases/bash/exit-status.test.sh index 14decc12..b2617e81 100644 --- a/crates/bashkit/tests/spec_cases/bash/exit-status.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/exit-status.test.sh @@ -168,3 +168,74 @@ false || false; echo $? 1 0 ### end + +### exit_in_if_block +# exit inside if block terminates script +if true; then echo before; exit 0; fi +echo SHOULD_NOT_REACH +### expect +before +### end + +### exit_in_if_block_with_code +# exit with non-zero code inside if block +if true; then exit 42; fi +echo SHOULD_NOT_REACH +### exit_code: 42 +### expect +### end + +### exit_in_while_loop +# exit inside while loop terminates script +i=0 +while true; do + echo "iter $i" + if [ "$i" -eq 1 ]; then exit 0; fi + i=$((i + 1)) +done +echo SHOULD_NOT_REACH +### expect +iter 0 +iter 1 +### end + +### exit_in_for_loop +# exit inside for loop terminates script +for x in a b c; do + echo "$x" + if [ "$x" = "b" ]; then exit 5; fi +done +echo SHOULD_NOT_REACH +### exit_code: 5 +### expect +a +b +### end + +### exit_in_case_block +# exit inside case block terminates script +case "yes" in + yes) echo matched; exit 0 ;; +esac +echo SHOULD_NOT_REACH +### expect +matched +### end + +### exit_in_subshell_does_not_propagate +# exit inside subshell only terminates the subshell +(exit 7) +echo "after subshell: $?" +### expect +after subshell: 7 +### end + +### exit_in_function +# exit inside function terminates the whole script +f() { echo in_func; exit 3; } +f +echo SHOULD_NOT_REACH +### exit_code: 3 +### expect +in_func +### end From 335238efe4324607f61e3c001331f3040da8d706 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Tue, 31 Mar 2026 03:18:42 +0000 Subject: [PATCH 2/2] chore(supply-chain): vet wasm-bindgen 0.2.116, js-sys/web-sys 0.3.93 Bump exemptions for wasm-bindgen ecosystem minor version bumps. --- supply-chain/config.toml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/supply-chain/config.toml b/supply-chain/config.toml index 699afa47..75fb6bbb 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -663,7 +663,7 @@ version = "0.1.34" criteria = "safe-to-deploy" [[exemptions.js-sys]] -version = "0.3.92" +version = "0.3.93" criteria = "safe-to-deploy" [[exemptions.leb128fmt]] @@ -1467,23 +1467,23 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" criteria = "safe-to-run" [[exemptions.wasm-bindgen]] -version = "0.2.115" +version = "0.2.116" criteria = "safe-to-deploy" [[exemptions.wasm-bindgen-futures]] -version = "0.4.65" +version = "0.4.66" criteria = "safe-to-deploy" [[exemptions.wasm-bindgen-macro]] -version = "0.2.115" +version = "0.2.116" criteria = "safe-to-deploy" [[exemptions.wasm-bindgen-macro-support]] -version = "0.2.115" +version = "0.2.116" criteria = "safe-to-deploy" [[exemptions.wasm-bindgen-shared]] -version = "0.2.115" +version = "0.2.116" criteria = "safe-to-deploy" [[exemptions.wasm-encoder]] @@ -1503,7 +1503,7 @@ version = "0.244.0" criteria = "safe-to-deploy" [[exemptions.web-sys]] -version = "0.3.92" +version = "0.3.93" criteria = "safe-to-deploy" [[exemptions.web-time]]