From 9970f6e4d0ce2ac2042680468303673dc607ea99 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Thu, 2 Apr 2026 14:30:06 +0000 Subject: [PATCH] fix(interpreter): cap glob_match invocations in remove_pattern_glob MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #994 — remove_pattern_glob called glob_match for every split point of the input string, causing O(n^2) CPU exhaustion on long strings with bracket expressions. Caps at 10,000 invocations. --- crates/bashkit/src/interpreter/mod.rs | 13 +++++++ .../spec_cases/bash/glob_match_cap.test.sh | 35 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 crates/bashkit/tests/spec_cases/bash/glob_match_cap.test.sh diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index c79612c9..e22733ee 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -6900,6 +6900,9 @@ impl Interpreter { } /// Remove prefix/suffix using glob_match for patterns with brackets or extglob. + /// + /// THREAT[TM-DOS]: Cap glob_match invocations to prevent O(n^2) CPU + /// exhaustion on long strings with bracket/extglob patterns. fn remove_pattern_glob( &self, value: &str, @@ -6907,11 +6910,17 @@ impl Interpreter { prefix: bool, longest: bool, ) -> String { + const MAX_GLOB_MATCH_CALLS: usize = 10_000; let chars: Vec = value.chars().collect(); + let mut calls = 0usize; if prefix { // Try each prefix length; shortest = first match, longest = last match let mut last_match = None; for i in 0..=chars.len() { + calls += 1; + if calls > MAX_GLOB_MATCH_CALLS { + break; + } let candidate: String = chars[..i].iter().collect(); if self.glob_match(&candidate, pattern) { if !longest { @@ -6927,6 +6936,10 @@ impl Interpreter { // Suffix removal: try each suffix length let mut last_match = None; for i in (0..=chars.len()).rev() { + calls += 1; + if calls > MAX_GLOB_MATCH_CALLS { + break; + } let candidate: String = chars[i..].iter().collect(); if self.glob_match(&candidate, pattern) { if !longest { diff --git a/crates/bashkit/tests/spec_cases/bash/glob_match_cap.test.sh b/crates/bashkit/tests/spec_cases/bash/glob_match_cap.test.sh new file mode 100644 index 00000000..14846d83 --- /dev/null +++ b/crates/bashkit/tests/spec_cases/bash/glob_match_cap.test.sh @@ -0,0 +1,35 @@ +# Glob match cap in remove_pattern_glob +# Regression tests for issue #994 + +### bracket_prefix_removal_works +# Normal bracket pattern prefix removal still works +x="abcdef" +echo "${x#[a]*}" +### expect +bcdef +### end + +### bracket_suffix_removal_works +# Normal bracket pattern suffix removal still works +x="abcdef" +echo "${x%[f]}" +### expect +abcde +### end + +### bracket_longest_prefix_removal_works +# Longest bracket pattern prefix removal still works +x="aaabbb" +result="${x##[a]*}" +echo "result=${#result}" +### expect +result=0 +### end + +### normal_prefix_removal_unaffected +# Standard patterns still work correctly +x="hello_world" +echo "${x#hello_}" +### expect +world +### end