Skip to content

Commit 4977823

Browse files
authored
fix(builtins): cap parallel cartesian product size to prevent memory blowup (#1054)
## Summary - Add `MAX_CARTESIAN_PRODUCT` constant (100,000) to cap combinations - Pre-calculate total product size with `checked_mul` before allocating - Return error with exit code 1 when limit exceeded - Small cartesian products still work correctly ## Test plan - [ ] Small products (2x2=4) work correctly - [ ] Huge products (4^20 ~1 trillion) return error - [ ] End-to-end test via `run_parallel()` with 20 groups - [ ] All existing parallel tests pass Closes #991
1 parent 45e76cc commit 4977823

File tree

1 file changed

+65
-5
lines changed

1 file changed

+65
-5
lines changed

crates/bashkit/src/builtins/parallel.rs

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)