diff --git a/crates/bashkit/src/builtins/awk.rs b/crates/bashkit/src/builtins/awk.rs index e625edfb..b316fab7 100644 --- a/crates/bashkit/src/builtins/awk.rs +++ b/crates/bashkit/src/builtins/awk.rs @@ -2490,7 +2490,14 @@ impl AwkInterpreter { } let format = self.eval_expr(&args[0]).as_string(); let values: Vec = args[1..].iter().map(|a| self.eval_expr(a)).collect(); - AwkValue::String(self.format_string(&format, &values)) + match self.format_string(&format, &values) { + Ok(s) => AwkValue::String(s), + Err(e) => { + self.stderr_output.push_str(&e); + self.stderr_output.push('\n'); + AwkValue::String(String::new()) + } + } } "toupper" => { if args.is_empty() { @@ -2768,7 +2775,14 @@ impl AwkInterpreter { return_value } - fn format_string(&self, format: &str, values: &[AwkValue]) -> String { + /// Max width/precision for format specifiers to prevent memory exhaustion + const MAX_FORMAT_WIDTH: usize = 10000; + + fn format_string( + &self, + format: &str, + values: &[AwkValue], + ) -> std::result::Result { let mut result = String::new(); let mut chars = format.chars().peekable(); let mut value_idx = 0; @@ -2839,8 +2853,17 @@ impl AwkInterpreter { break; } } - if !w.is_empty() { - width = w.parse().ok(); + if !w.is_empty() + && let Ok(w_val) = w.parse::() + { + if w_val > Self::MAX_FORMAT_WIDTH { + return Err(format!( + "awk: format width {} exceeds maximum ({})", + w_val, + Self::MAX_FORMAT_WIDTH + )); + } + width = Some(w_val); } // Parse precision @@ -2857,8 +2880,17 @@ impl AwkInterpreter { } precision = if p.is_empty() { Some(0) + } else if let Ok(p_val) = p.parse::() { + if p_val > Self::MAX_FORMAT_WIDTH { + return Err(format!( + "awk: format precision {} exceeds maximum ({})", + p_val, + Self::MAX_FORMAT_WIDTH + )); + } + Some(p_val) } else { - p.parse().ok() + None }; } @@ -2967,7 +2999,7 @@ impl AwkInterpreter { } } - result + Ok(result) } /// Total bytes buffered across all output streams. @@ -3026,11 +3058,19 @@ impl AwkInterpreter { AwkAction::Printf(format_expr, args, target) => { let format_str = self.eval_expr(format_expr).as_string(); let values: Vec = args.iter().map(|a| self.eval_expr(a)).collect(); - let text = self.format_string(&format_str, &values); - if !self.write_output(&text, target) { - return AwkFlow::Exit(Some(2)); + match self.format_string(&format_str, &values) { + Ok(text) => { + if !self.write_output(&text, target) { + return AwkFlow::Exit(Some(2)); + } + AwkFlow::Continue + } + Err(e) => { + self.stderr_output.push_str(&e); + self.stderr_output.push('\n'); + AwkFlow::Exit(Some(2)) + } } - AwkFlow::Continue } AwkAction::Assign(name, expr) => { let value = self.eval_expr(expr); diff --git a/crates/bashkit/src/builtins/printf.rs b/crates/bashkit/src/builtins/printf.rs index c7ef32cf..a6c07870 100644 --- a/crates/bashkit/src/builtins/printf.rs +++ b/crates/bashkit/src/builtins/printf.rs @@ -62,8 +62,8 @@ impl Builtin for Printf { } /// Parsed format specification -// Max precision to prevent panics in Rust's format! macro (u16::MAX limit) -const MAX_PRECISION: usize = 10000; +// Max width/precision to prevent memory exhaustion from huge format specifiers +const MAX_FORMAT_WIDTH: usize = 10000; struct FormatSpec { left_align: bool, @@ -119,7 +119,10 @@ impl FormatSpec { let width = if width_str.is_empty() { None } else { - width_str.parse().ok() + width_str + .parse() + .ok() + .map(|w: usize| w.min(MAX_FORMAT_WIDTH)) }; // Parse precision @@ -140,7 +143,10 @@ impl FormatSpec { if prec_str.is_empty() { Some(0) } else { - prec_str.parse().ok().map(|p: usize| p.min(MAX_PRECISION)) + prec_str + .parse() + .ok() + .map(|p: usize| p.min(MAX_FORMAT_WIDTH)) } } else { None diff --git a/crates/bashkit/tests/spec_cases/bash/awk-printf-width.test.sh b/crates/bashkit/tests/spec_cases/bash/awk-printf-width.test.sh new file mode 100644 index 00000000..a607bc2b --- /dev/null +++ b/crates/bashkit/tests/spec_cases/bash/awk-printf-width.test.sh @@ -0,0 +1,33 @@ +### awk_printf_huge_width_rejected +### bash_diff: bashkit caps printf width to prevent OOM; real bash allows unlimited width +# printf with enormous width should error, not OOM +echo "" | awk '{printf "%999999999d", 1}' 2>&1 +echo "exit: $?" +### expect +awk: format width 999999999 exceeds maximum (10000) +exit: 2 +### end + +### awk_printf_normal_width_works +# Normal width should still work +echo "" | awk '{printf "%20d\n", 42}' +### expect + 42 +### end + +### awk_printf_max_width_works +# Width at limit boundary should work +echo "" | awk '{printf "%10000d\n", 1}' | wc -c +### expect +10001 +### end + +### awk_printf_huge_precision_rejected +### bash_diff: bashkit caps printf precision to prevent OOM; real bash allows unlimited precision +# Huge precision should also error +echo "" | awk '{printf "%.999999999f", 1}' 2>&1 +echo "exit: $?" +### expect +awk: format precision 999999999 exceeds maximum (10000) +exit: 2 +### end