#![cfg(feature = "failpoints")]
use bashkit::{Bash, ControlFlow, ExecResult, ExecutionLimits};
use serial_test::serial;
use std::time::Duration;
async fn run_script(script: &str) -> ExecResult {
let mut bash = Bash::new();
bash.exec(script).await.unwrap_or_else(|e| ExecResult {
stdout: String::new(),
stderr: e.to_string(),
exit_code: 1,
control_flow: ControlFlow::None,
..Default::default()
})
}
async fn run_script_with_limits(script: &str, limits: ExecutionLimits) -> ExecResult {
let mut bash = Bash::builder().limits(limits).build();
bash.exec(script).await.unwrap_or_else(|e| ExecResult {
stdout: String::new(),
stderr: e.to_string(),
exit_code: 1,
control_flow: ControlFlow::None,
..Default::default()
})
}
#[tokio::test]
#[serial]
async fn security_command_limit_skip_increment() {
fail::cfg("limits::tick_command", "return(skip_increment)").unwrap();
let result = run_script_with_limits(
"echo 1; echo 2; echo 3; echo 4; echo 5",
ExecutionLimits::new().max_commands(3),
)
.await;
fail::cfg("limits::tick_command", "off").unwrap();
assert!(result.exit_code == 0 || result.stderr.contains("limit"));
}
#[tokio::test]
#[serial]
async fn security_command_limit_overflow() {
fail::cfg("limits::tick_command", "return(force_overflow)").unwrap();
let result = run_script("echo hello").await;
fail::cfg("limits::tick_command", "off").unwrap();
assert!(
result.stderr.contains("limit") || result.stderr.contains("exceeded"),
"Expected limit error, got: {}",
result.stderr
);
}
#[tokio::test]
#[serial]
async fn security_loop_counter_reset() {
fail::cfg("limits::tick_loop", "1*return(reset_counter)").unwrap();
let result = run_script_with_limits(
"for i in 1 2 3 4 5; do echo $i; done",
ExecutionLimits::new()
.max_loop_iterations(10)
.max_commands(50)
.timeout(Duration::from_secs(2)),
)
.await;
fail::cfg("limits::tick_loop", "off").unwrap();
assert!(result.exit_code == 0 || result.exit_code == 127);
}
#[test]
#[serial]
fn security_function_depth_bypass() {
let handle = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024) .spawn(|| {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
fail::cfg("limits::push_function", "return(skip_check)").unwrap();
let result = run_script_with_limits(
r#"
recurse() {
echo "depth"
recurse
}
recurse
"#,
ExecutionLimits::new()
.max_function_depth(5)
.max_commands(100)
.timeout(Duration::from_secs(2)),
)
.await;
fail::cfg("limits::push_function", "off").unwrap();
assert!(
result.stderr.contains("limit")
|| result.stderr.contains("exceeded")
|| result.exit_code != 0,
"Recursive function should be limited"
);
});
})
.unwrap();
handle.join().unwrap();
}
#[tokio::test]
#[serial]
async fn security_fs_read_io_error() {
fail::cfg("fs::read_file", "return(io_error)").unwrap();
let result = run_script("cat /tmp/test.txt").await;
fail::cfg("fs::read_file", "off").unwrap();
assert!(result.exit_code != 0);
}
#[tokio::test]
#[serial]
async fn security_fs_read_permission_denied() {
fail::cfg("fs::read_file", "return(permission_denied)").unwrap();
let result = run_script("cat /tmp/test.txt").await;
fail::cfg("fs::read_file", "off").unwrap();
assert!(result.exit_code != 0);
assert!(
result.stderr.contains("permission")
|| result.stderr.contains("denied")
|| result.stderr.contains("error"),
"Expected permission error, got: {}",
result.stderr
);
}
#[tokio::test]
#[serial]
async fn security_fs_corrupt_data() {
fail::cfg("fs::read_file", "return(corrupt_data)").unwrap();
let result = run_script("cat /tmp/test.txt | grep something").await;
fail::cfg("fs::read_file", "off").unwrap();
let _ = result.exit_code;
}
#[tokio::test]
#[serial]
async fn security_fs_write_failure() {
fail::cfg("fs::write_file", "return(io_error)").unwrap();
let result = run_script("echo 'test' > /tmp/output.txt").await;
fail::cfg("fs::write_file", "off").unwrap();
assert!(result.exit_code != 0 || result.stderr.contains("error"));
}
#[tokio::test]
#[serial]
async fn security_fs_disk_full() {
fail::cfg("fs::write_file", "return(disk_full)").unwrap();
let result = run_script("echo 'large data' > /tmp/output.txt").await;
fail::cfg("fs::write_file", "off").unwrap();
assert!(result.exit_code != 0);
}
#[tokio::test]
#[serial]
async fn security_interp_execution_error() {
fail::cfg("interp::execute_command", "return(error)").unwrap();
let result = run_script("echo hello").await;
fail::cfg("interp::execute_command", "off").unwrap();
assert!(result.exit_code != 0 || result.stderr.contains("error"));
}
#[tokio::test]
#[serial]
async fn security_interp_exit_nonzero() {
fail::cfg("interp::execute_command", "return(exit_nonzero)").unwrap();
let result = run_script("echo hello").await;
fail::cfg("interp::execute_command", "off").unwrap();
assert_eq!(result.exit_code, 127);
assert!(result.stderr.contains("injected failure"));
}
#[tokio::test]
#[serial]
async fn security_multiple_failpoints() {
fail::cfg("limits::tick_command", "5%return(skip_increment)").unwrap();
fail::cfg("fs::read_file", "10%return(io_error)").unwrap();
let result = run_script_with_limits(
r#"
for i in 1 2 3; do
echo "iteration $i"
done
"#,
ExecutionLimits::new()
.max_commands(100)
.max_loop_iterations(100),
)
.await;
fail::cfg("limits::tick_command", "off").unwrap();
fail::cfg("fs::read_file", "off").unwrap();
let _ = result.exit_code;
}
#[tokio::test]
#[serial]
async fn security_probabilistic_failures() {
fail::cfg("limits::tick_command", "10%return(corrupt_high)").unwrap();
let mut success_count = 0;
let mut failure_count = 0;
for _ in 0..10 {
let result = run_script_with_limits(
"echo 1; echo 2; echo 3",
ExecutionLimits::new().max_commands(100),
)
.await;
if result.exit_code == 0 {
success_count += 1;
} else {
failure_count += 1;
}
}
fail::cfg("limits::tick_command", "off").unwrap();
println!(
"Probabilistic test: {} successes, {} failures",
success_count, failure_count
);
}
#[tokio::test]
#[serial]
async fn security_example_custom_failpoint_usage() {
fail::cfg("fs::write_file", "return(permission_denied)").unwrap();
let result = run_script("echo 'secret' > /tmp/sensitive.txt").await;
fail::cfg("fs::write_file", "off").unwrap();
assert!(
result.exit_code != 0,
"Write to sensitive file should fail with permission denied"
);
}