Skip to content

Commit b65dce2

Browse files
authored
fix(builtins): cap AWK printf width/precision to prevent memory exhaustion (#1048)
## Summary - Cap width and precision values to 10,000 in AWK `format_string()` - Return error + exit code 2 when limit exceeded - Apply same cap to bash `printf` builtin width parsing for consistency - Prevents ~1GB allocation from `printf "%999999999d", 1` ## Test plan - [ ] `awk '{printf "%999999999d", 1}'` rejected with error, exit 2 - [ ] `awk '{printf "%999999999.5f", 1}'` rejected with error, exit 2 - [ ] `awk '{printf "%20d\n", 42}'` works correctly - [ ] `awk '{printf "%10000d\n", 1}'` works at boundary - [ ] All 1929 existing spec tests pass Closes #983
1 parent 0b91a41 commit b65dce2

File tree

3 files changed

+93
-14
lines changed

3 files changed

+93
-14
lines changed

crates/bashkit/src/builtins/awk.rs

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2490,7 +2490,14 @@ impl AwkInterpreter {
24902490
}
24912491
let format = self.eval_expr(&args[0]).as_string();
24922492
let values: Vec<AwkValue> = args[1..].iter().map(|a| self.eval_expr(a)).collect();
2493-
AwkValue::String(self.format_string(&format, &values))
2493+
match self.format_string(&format, &values) {
2494+
Ok(s) => AwkValue::String(s),
2495+
Err(e) => {
2496+
self.stderr_output.push_str(&e);
2497+
self.stderr_output.push('\n');
2498+
AwkValue::String(String::new())
2499+
}
2500+
}
24942501
}
24952502
"toupper" => {
24962503
if args.is_empty() {
@@ -2768,7 +2775,14 @@ impl AwkInterpreter {
27682775
return_value
27692776
}
27702777

2771-
fn format_string(&self, format: &str, values: &[AwkValue]) -> String {
2778+
/// Max width/precision for format specifiers to prevent memory exhaustion
2779+
const MAX_FORMAT_WIDTH: usize = 10000;
2780+
2781+
fn format_string(
2782+
&self,
2783+
format: &str,
2784+
values: &[AwkValue],
2785+
) -> std::result::Result<String, String> {
27722786
let mut result = String::new();
27732787
let mut chars = format.chars().peekable();
27742788
let mut value_idx = 0;
@@ -2839,8 +2853,17 @@ impl AwkInterpreter {
28392853
break;
28402854
}
28412855
}
2842-
if !w.is_empty() {
2843-
width = w.parse().ok();
2856+
if !w.is_empty()
2857+
&& let Ok(w_val) = w.parse::<usize>()
2858+
{
2859+
if w_val > Self::MAX_FORMAT_WIDTH {
2860+
return Err(format!(
2861+
"awk: format width {} exceeds maximum ({})",
2862+
w_val,
2863+
Self::MAX_FORMAT_WIDTH
2864+
));
2865+
}
2866+
width = Some(w_val);
28442867
}
28452868

28462869
// Parse precision
@@ -2857,8 +2880,17 @@ impl AwkInterpreter {
28572880
}
28582881
precision = if p.is_empty() {
28592882
Some(0)
2883+
} else if let Ok(p_val) = p.parse::<usize>() {
2884+
if p_val > Self::MAX_FORMAT_WIDTH {
2885+
return Err(format!(
2886+
"awk: format precision {} exceeds maximum ({})",
2887+
p_val,
2888+
Self::MAX_FORMAT_WIDTH
2889+
));
2890+
}
2891+
Some(p_val)
28602892
} else {
2861-
p.parse().ok()
2893+
None
28622894
};
28632895
}
28642896

@@ -2967,7 +2999,7 @@ impl AwkInterpreter {
29672999
}
29683000
}
29693001

2970-
result
3002+
Ok(result)
29713003
}
29723004

29733005
/// Total bytes buffered across all output streams.
@@ -3026,11 +3058,19 @@ impl AwkInterpreter {
30263058
AwkAction::Printf(format_expr, args, target) => {
30273059
let format_str = self.eval_expr(format_expr).as_string();
30283060
let values: Vec<AwkValue> = args.iter().map(|a| self.eval_expr(a)).collect();
3029-
let text = self.format_string(&format_str, &values);
3030-
if !self.write_output(&text, target) {
3031-
return AwkFlow::Exit(Some(2));
3061+
match self.format_string(&format_str, &values) {
3062+
Ok(text) => {
3063+
if !self.write_output(&text, target) {
3064+
return AwkFlow::Exit(Some(2));
3065+
}
3066+
AwkFlow::Continue
3067+
}
3068+
Err(e) => {
3069+
self.stderr_output.push_str(&e);
3070+
self.stderr_output.push('\n');
3071+
AwkFlow::Exit(Some(2))
3072+
}
30323073
}
3033-
AwkFlow::Continue
30343074
}
30353075
AwkAction::Assign(name, expr) => {
30363076
let value = self.eval_expr(expr);

crates/bashkit/src/builtins/printf.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ impl Builtin for Printf {
6262
}
6363

6464
/// Parsed format specification
65-
// Max precision to prevent panics in Rust's format! macro (u16::MAX limit)
66-
const MAX_PRECISION: usize = 10000;
65+
// Max width/precision to prevent memory exhaustion from huge format specifiers
66+
const MAX_FORMAT_WIDTH: usize = 10000;
6767

6868
struct FormatSpec {
6969
left_align: bool,
@@ -119,7 +119,10 @@ impl FormatSpec {
119119
let width = if width_str.is_empty() {
120120
None
121121
} else {
122-
width_str.parse().ok()
122+
width_str
123+
.parse()
124+
.ok()
125+
.map(|w: usize| w.min(MAX_FORMAT_WIDTH))
123126
};
124127

125128
// Parse precision
@@ -140,7 +143,10 @@ impl FormatSpec {
140143
if prec_str.is_empty() {
141144
Some(0)
142145
} else {
143-
prec_str.parse().ok().map(|p: usize| p.min(MAX_PRECISION))
146+
prec_str
147+
.parse()
148+
.ok()
149+
.map(|p: usize| p.min(MAX_FORMAT_WIDTH))
144150
}
145151
} else {
146152
None
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
### awk_printf_huge_width_rejected
2+
### bash_diff: bashkit caps printf width to prevent OOM; real bash allows unlimited width
3+
# printf with enormous width should error, not OOM
4+
echo "" | awk '{printf "%999999999d", 1}' 2>&1
5+
echo "exit: $?"
6+
### expect
7+
awk: format width 999999999 exceeds maximum (10000)
8+
exit: 2
9+
### end
10+
11+
### awk_printf_normal_width_works
12+
# Normal width should still work
13+
echo "" | awk '{printf "%20d\n", 42}'
14+
### expect
15+
42
16+
### end
17+
18+
### awk_printf_max_width_works
19+
# Width at limit boundary should work
20+
echo "" | awk '{printf "%10000d\n", 1}' | wc -c
21+
### expect
22+
10001
23+
### end
24+
25+
### awk_printf_huge_precision_rejected
26+
### bash_diff: bashkit caps printf precision to prevent OOM; real bash allows unlimited precision
27+
# Huge precision should also error
28+
echo "" | awk '{printf "%.999999999f", 1}' 2>&1
29+
echo "exit: $?"
30+
### expect
31+
awk: format precision 999999999 exceeds maximum (10000)
32+
exit: 2
33+
### end

0 commit comments

Comments
 (0)