1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// THREAT[TM-DOS-089]: Stack overflow regression tests for nested command substitution.
// Before the Box::pin fix, each $(...) level inlined its async state machine into the
// caller's stack frame, causing overflow at moderate depths. The fix moves the command
// substitution body to the heap via Box::pin, keeping stack usage constant per level.
use bashkit::{Bash, ExecutionLimits};
/// Live output routing must preserve the same substitution depth guarantee.
#[tokio::test]
async fn depth_32_live_execution_no_stack_overflow() {
let mut script = "echo hello".to_string();
for _ in 0..32 {
script = format!("echo $({script})");
}
let mut bash = Bash::builder()
.limits(
ExecutionLimits::new()
.max_commands(500)
.max_subst_depth(32)
.timeout(std::time::Duration::from_secs(5)),
)
.build();
if let Ok(result) = bash.execute(&script).capture(64).await {
assert!(!result.stdout.is_empty() || result.exit_code != 0);
}
}
/// Default max_subst_depth (32) must not cause stack overflow.
/// Before the Box::pin fix, this would SIGABRT at ~20-30 levels.
/// Regression test for issue #1089.
#[tokio::test]
async fn depth_32_no_stack_overflow() {
let depth = 32;
let mut cmd = "echo hello".to_string();
for _ in 0..depth {
cmd = format!("echo $({})", cmd);
}
let mut bash = Bash::builder()
.limits(
ExecutionLimits::new()
.max_commands(500)
.max_subst_depth(32)
.timeout(std::time::Duration::from_secs(5)),
)
.build();
// Must not stack-overflow — should return a result (possibly truncated by depth limit)
let result = bash.exec(&cmd).await;
if let Ok(r) = result {
// Depth limit error is also acceptable (Err case)
assert!(!r.stdout.is_empty() || r.exit_code != 0);
}
}
/// Deeply nested $() in arithmetic context (the fuzz crash vector).
/// Regression test for the specific #1089 crash input pattern.
#[tokio::test]
async fn nested_subst_in_arithmetic_no_overflow() {
// Simulates the crash input: nested $( inside $((...))
let mut inner = "echo 1".to_string();
for _ in 0..15 {
inner = format!("echo $({})", inner);
}
let script = format!("echo $(($({})))", inner);
let mut bash = Bash::builder()
.limits(
ExecutionLimits::new()
.max_commands(500)
.max_subst_depth(20)
.timeout(std::time::Duration::from_secs(5)),
)
.build();
// Must not panic or SIGABRT
let _ = bash.exec(&script).await;
}
/// THREAT[TM-DOS-125]: a script that re-runs itself through `sh`/`bash`
/// recursed until the process stack overflowed (taking every tenant in the
/// process with it). Child shells now count against the function depth.
#[tokio::test]
async fn recursive_child_shell_is_bounded() {
let mut bash = Bash::new();
let r = bash
.exec("echo 'sh /tmp/s.sh' > /tmp/s.sh; sh /tmp/s.sh; echo rc=$?")
.await;
let r = r.unwrap();
assert!(r.stdout.contains("rc="), "stdout: {}", r.stdout);
assert!(
r.stderr.contains("maximum nesting depth exceeded"),
"stderr: {}",
r.stderr
);
let r = bash
.exec("f() { bash -c 'f() { bash -c f; }; f'; }; f; echo done")
.await
.unwrap();
assert!(r.stdout.contains("done"), "stdout: {}", r.stdout);
}