diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index 26d5ee7fe..e8a9eba23 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -5012,7 +5012,16 @@ impl Interpreter { } fn ensure_persistent_fd_capacity(&self, fd: i32) -> Result<()> { - if fd <= 2 || self.exec_fd_table.contains_key(&fd) || self.coproc_buffers.contains_key(&fd) + if fd < 0 { + return Err(crate::error::Error::Execution(format!( + "invalid file descriptor: {}", + fd + ))); + } + + if (0..=2).contains(&fd) + || self.exec_fd_table.contains_key(&fd) + || self.coproc_buffers.contains_key(&fd) { return Ok(()); } diff --git a/crates/bashkit/tests/integration/threat_model_tests.rs b/crates/bashkit/tests/integration/threat_model_tests.rs index 7bf0b4be9..08cd10125 100644 --- a/crates/bashkit/tests/integration/threat_model_tests.rs +++ b/crates/bashkit/tests/integration/threat_model_tests.rs @@ -74,6 +74,41 @@ mod resource_exhaustion { assert_eq!(result.stdout.trim(), "ok"); } + /// TM-DOS-063: Negative fd vars must be rejected and must not bypass fd limits. + #[tokio::test] + async fn fd_limit_rejects_negative_fd_var_output() { + let limits = ExecutionLimits::new().max_file_descriptors(1); + let mut bash = Bash::builder().limits(limits).build(); + + let result = bash.exec("v=-1; exec {v}>/tmp/neg-out").await; + + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("invalid file descriptor"), + "Expected invalid file descriptor error, got: {}", + err + ); + } + + /// TM-DOS-063: Negative fd vars for input must be rejected. + #[tokio::test] + async fn fd_limit_rejects_negative_fd_var_input() { + let limits = ExecutionLimits::new().max_file_descriptors(1); + let mut bash = Bash::builder().limits(limits).build(); + bash.exec("echo hi >/tmp/neg-in").await.unwrap(); + + let result = bash.exec("v=-1; exec {v}