@@ -2080,6 +2080,10 @@ enum AwkFlow {
20802080/// THREAT[TM-DOS-027]: Maximum recursion depth for awk user-defined function calls.
20812081const 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+
20832087struct 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