Skip to content

Commit 7e31915

Browse files
authored
fix(builtins): cap AWK output buffer size to prevent memory exhaustion (#1055)
## Summary - Add `MAX_AWK_OUTPUT_BYTES` constant (10MB) to cap total AWK output - `write_output()` now checks total across stdout, stderr, file_outputs, and file_appends - Returns error with exit code 2 when limit exceeded - Prevents OOM from `awk '{for(i=0;i<100000;i++) printf "%10000s\n", "x"}'` ## Test plan - [ ] Stdout overflow caught with exit code 2 - [ ] Normal output under limit works correctly - [ ] File redirect overflow also caught - [ ] All existing AWK tests pass Closes #987
1 parent 4977823 commit 7e31915

File tree

1 file changed

+76
-3
lines changed
  • crates/bashkit/src/builtins

1 file changed

+76
-3
lines changed

crates/bashkit/src/builtins/awk.rs

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2080,6 +2080,10 @@ enum AwkFlow {
20802080
/// THREAT[TM-DOS-027]: Maximum recursion depth for awk user-defined function calls.
20812081
const MAX_AWK_CALL_DEPTH: usize = 64;
20822082

2083+
/// THREAT[TM-DOS-027]: Maximum total AWK output size (stdout + stderr + file redirects)
2084+
/// to prevent memory exhaustion. 10 MB.
2085+
const MAX_AWK_OUTPUT_BYTES: usize = 10_000_000;
2086+
20832087
struct AwkInterpreter {
20842088
state: AwkState,
20852089
output: String,
@@ -2966,8 +2970,22 @@ impl AwkInterpreter {
29662970
result
29672971
}
29682972

2973+
/// Total bytes buffered across all output streams.
2974+
fn total_output_bytes(&self) -> usize {
2975+
self.output.len()
2976+
+ self.stderr_output.len()
2977+
+ self.file_outputs.values().map(|v| v.len()).sum::<usize>()
2978+
+ self.file_appends.values().map(|v| v.len()).sum::<usize>()
2979+
}
2980+
29692981
/// Write text to stdout buffer or to a file output buffer based on the target.
2970-
fn write_output(&mut self, text: &str, target: &Option<AwkOutputTarget>) {
2982+
/// Returns `false` if the write would exceed [`MAX_AWK_OUTPUT_BYTES`].
2983+
fn write_output(&mut self, text: &str, target: &Option<AwkOutputTarget>) -> bool {
2984+
if self.total_output_bytes() + text.len() > MAX_AWK_OUTPUT_BYTES {
2985+
self.stderr_output
2986+
.push_str("awk: output limit exceeded (max 10MB)\n");
2987+
return false;
2988+
}
29712989
match target {
29722990
None => self.output.push_str(text),
29732991
Some(AwkOutputTarget::Truncate(expr)) | Some(AwkOutputTarget::Append(expr)) => {
@@ -2984,6 +3002,7 @@ impl AwkInterpreter {
29843002
}
29853003
}
29863004
}
3005+
true
29873006
}
29883007

29893008
/// Execute action. Returns flow control signal.
@@ -2999,14 +3018,18 @@ impl AwkInterpreter {
29993018
.collect();
30003019
let mut text = parts.join(&self.state.ofs);
30013020
text.push_str(&self.state.ors);
3002-
self.write_output(&text, target);
3021+
if !self.write_output(&text, target) {
3022+
return AwkFlow::Exit(Some(2));
3023+
}
30033024
AwkFlow::Continue
30043025
}
30053026
AwkAction::Printf(format_expr, args, target) => {
30063027
let format_str = self.eval_expr(format_expr).as_string();
30073028
let values: Vec<AwkValue> = args.iter().map(|a| self.eval_expr(a)).collect();
30083029
let text = self.format_string(&format_str, &values);
3009-
self.write_output(&text, target);
3030+
if !self.write_output(&text, target) {
3031+
return AwkFlow::Exit(Some(2));
3032+
}
30103033
AwkFlow::Continue
30113034
}
30123035
AwkAction::Assign(name, expr) => {
@@ -4381,4 +4404,54 @@ mod tests {
43814404
.unwrap();
43824405
assert_eq!(result.stdout, "ok\n");
43834406
}
4407+
4408+
#[tokio::test]
4409+
async fn test_awk_output_limit_exceeded() {
4410+
// Each iteration prints a 1000-char line. 100k iters = ~100MB >> 10MB limit.
4411+
let result = run_awk(
4412+
&[r#"BEGIN { s = sprintf("%1000s", "x"); for(i=0;i<100000;i++) print s }"#],
4413+
None,
4414+
)
4415+
.await
4416+
.unwrap();
4417+
assert_eq!(result.exit_code, 2);
4418+
assert!(
4419+
result.stderr.contains("output limit exceeded"),
4420+
"stderr should mention output limit: {}",
4421+
result.stderr
4422+
);
4423+
assert!(
4424+
result.stdout.len() <= 11_000_000,
4425+
"stdout should be bounded: {} bytes",
4426+
result.stdout.len()
4427+
);
4428+
}
4429+
4430+
#[tokio::test]
4431+
async fn test_awk_output_under_limit_ok() {
4432+
// Small output well under 10MB should succeed normally
4433+
let result = run_awk(&[r#"BEGIN { for(i=0;i<100;i++) print "hello" }"#], None)
4434+
.await
4435+
.unwrap();
4436+
assert_eq!(result.exit_code, 0);
4437+
let lines: Vec<&str> = result.stdout.trim().split('\n').collect();
4438+
assert_eq!(lines.len(), 100);
4439+
}
4440+
4441+
#[tokio::test]
4442+
async fn test_awk_file_redirect_output_limit() {
4443+
// File redirect output should also be bounded
4444+
let result = run_awk(
4445+
&[r#"BEGIN { s = sprintf("%1000s", "x"); for(i=0;i<100000;i++) print s > "/tmp/out" }"#],
4446+
None,
4447+
)
4448+
.await
4449+
.unwrap();
4450+
assert_eq!(result.exit_code, 2);
4451+
assert!(
4452+
result.stderr.contains("output limit exceeded"),
4453+
"stderr should mention output limit: {}",
4454+
result.stderr
4455+
);
4456+
}
43844457
}

0 commit comments

Comments
 (0)