From aa3bb4ec88cb376f8ec3470f63d60fc05f1cd652 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Thu, 2 Apr 2026 14:35:42 +0000 Subject: [PATCH] fix(parser): cap lookahead in looks_like_brace_expansion to 10K chars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #997 — looks_like_brace_expansion scanned to end of input for each unmatched {, causing O(n^2) total scanning on pathological input. Limits scan to 10,000 characters per invocation. --- crates/bashkit/src/parser/lexer.rs | 9 ++++++ .../bash/brace_expansion_lookahead.test.sh | 30 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 crates/bashkit/tests/spec_cases/bash/brace_expansion_lookahead.test.sh diff --git a/crates/bashkit/src/parser/lexer.rs b/crates/bashkit/src/parser/lexer.rs index ae24f2c5..445fc3fa 100644 --- a/crates/bashkit/src/parser/lexer.rs +++ b/crates/bashkit/src/parser/lexer.rs @@ -1412,7 +1412,11 @@ impl<'a> Lexer<'a> { /// Check if the content starting with { looks like a brace expansion /// Brace expansion: {a,b,c} or {1..5} (contains , or ..) /// Brace group: { cmd; } (contains spaces, semicolons, newlines) + /// THREAT[TM-DOS]: Caps lookahead to prevent O(n^2) scanning when input + /// contains many unmatched `{` characters (issue #997). fn looks_like_brace_expansion(&self) -> bool { + const MAX_LOOKAHEAD: usize = 10_000; + // Clone the iterator to peek ahead without consuming let mut chars = self.chars.clone(); @@ -1425,8 +1429,13 @@ impl<'a> Lexer<'a> { let mut has_comma = false; let mut has_dot_dot = false; let mut prev_char = None; + let mut scanned = 0usize; for ch in chars { + scanned += 1; + if scanned > MAX_LOOKAHEAD { + return false; + } match ch { '{' => depth += 1, '}' => { diff --git a/crates/bashkit/tests/spec_cases/bash/brace_expansion_lookahead.test.sh b/crates/bashkit/tests/spec_cases/bash/brace_expansion_lookahead.test.sh new file mode 100644 index 00000000..3c2618d6 --- /dev/null +++ b/crates/bashkit/tests/spec_cases/bash/brace_expansion_lookahead.test.sh @@ -0,0 +1,30 @@ +# Brace expansion lookahead limit +# Regression tests for issue #997 + +### normal_brace_expansion +# Normal brace expansion still works +echo {a,b,c} +### expect +a b c +### end + +### range_brace_expansion +# Range brace expansion still works +echo {1..3} +### expect +1 2 3 +### end + +### unmatched_brace_literal +# Unmatched { is treated as literal +echo {abc +### expect +{abc +### end + +### nested_brace_expansion +# Nested brace expansion works +echo {a,{b,c}} +### expect +a b c +### end