@@ -106,14 +106,35 @@ fn parse_parallel_args(args: &[String]) -> std::result::Result<ParallelConfig, S
106106 } )
107107}
108108
109+ /// Maximum number of cartesian product combinations allowed.
110+ /// Prevents exponential memory blowup with many `:::` groups.
111+ const MAX_CARTESIAN_PRODUCT : usize = 100_000 ;
112+
109113/// Generate the cartesian product of multiple argument groups.
110- fn cartesian_product ( groups : & [ Vec < String > ] ) -> Vec < Vec < String > > {
114+ ///
115+ /// Returns an error if the total number of combinations would exceed
116+ /// `MAX_CARTESIAN_PRODUCT` to prevent exponential memory blowup.
117+ fn cartesian_product ( groups : & [ Vec < String > ] ) -> std:: result:: Result < Vec < Vec < String > > , String > {
111118 if groups. is_empty ( ) {
112- return vec ! [ vec![ ] ] ;
119+ return Ok ( vec ! [ vec![ ] ] ) ;
113120 }
121+
122+ // Pre-calculate total combinations to reject before allocating.
123+ groups
124+ . iter ( )
125+ . try_fold ( 1usize , |acc, g| {
126+ acc. checked_mul ( g. len ( ) )
127+ . filter ( |& n| n <= MAX_CARTESIAN_PRODUCT )
128+ } )
129+ . ok_or_else ( || {
130+ format ! (
131+ "parallel: cartesian product too large (exceeds {MAX_CARTESIAN_PRODUCT} combinations)"
132+ )
133+ } ) ?;
134+
114135 let mut result = vec ! [ vec![ ] ] ;
115136 for group in groups {
116- let mut new_result = Vec :: new ( ) ;
137+ let mut new_result = Vec :: with_capacity ( result . len ( ) * group . len ( ) ) ;
117138 for existing in & result {
118139 for item in group {
119140 let mut combo = existing. clone ( ) ;
@@ -123,7 +144,7 @@ fn cartesian_product(groups: &[Vec<String>]) -> Vec<Vec<String>> {
123144 }
124145 result = new_result;
125146 }
126- result
147+ Ok ( result)
127148}
128149
129150/// Build a command string by substituting `{}` with the argument.
@@ -187,7 +208,10 @@ impl Builtin for Parallel {
187208 ) ) ;
188209 }
189210
190- let combinations = cartesian_product ( & config. arg_groups ) ;
211+ let combinations = match cartesian_product ( & config. arg_groups ) {
212+ Ok ( c) => c,
213+ Err ( e) => return Ok ( ExecResult :: err ( format ! ( "{e}\n " ) , 1 ) ) ,
214+ } ;
191215 let num_commands = combinations. len ( ) ;
192216 let effective_jobs = config. jobs . unwrap_or ( num_commands as u32 ) ;
193217
@@ -351,4 +375,40 @@ mod tests {
351375 . contains( "not supported in virtual environment" )
352376 ) ;
353377 }
378+
379+ #[ test]
380+ fn test_cartesian_product_small ( ) {
381+ let groups = vec ! [
382+ vec![ "a" . to_string( ) , "b" . to_string( ) ] ,
383+ vec![ "1" . to_string( ) , "2" . to_string( ) ] ,
384+ ] ;
385+ let result = cartesian_product ( & groups) . unwrap ( ) ;
386+ assert_eq ! ( result. len( ) , 4 ) ;
387+ assert ! ( result. contains( & vec![ "a" . to_string( ) , "1" . to_string( ) ] ) ) ;
388+ assert ! ( result. contains( & vec![ "b" . to_string( ) , "2" . to_string( ) ] ) ) ;
389+ }
390+
391+ #[ test]
392+ fn test_cartesian_product_exceeds_limit ( ) {
393+ // 20 groups of 4 elements each = 4^20 = ~1 trillion combinations
394+ let groups: Vec < Vec < String > > = ( 0 ..20 )
395+ . map ( |_| vec ! [ "a" . into( ) , "b" . into( ) , "c" . into( ) , "d" . into( ) ] )
396+ . collect ( ) ;
397+ let result = cartesian_product ( & groups) ;
398+ assert ! ( result. is_err( ) ) ;
399+ assert ! ( result. unwrap_err( ) . contains( "cartesian product too large" ) ) ;
400+ }
401+
402+ #[ tokio:: test]
403+ async fn test_cartesian_product_limit_via_builtin ( ) {
404+ // Build args: echo ::: a b c d ::: a b c d ... (20 groups)
405+ let mut args: Vec < & str > = vec ! [ "echo" ] ;
406+ for _ in 0 ..20 {
407+ args. push ( ":::" ) ;
408+ args. extend ( [ "a" , "b" , "c" , "d" ] ) ;
409+ }
410+ let result = run_parallel ( & args) . await ;
411+ assert_eq ! ( result. exit_code, 1 ) ;
412+ assert ! ( result. stderr. contains( "cartesian product too large" ) ) ;
413+ }
354414}
0 commit comments