From 900dae933904c2a54698e1d9ad73e749c2f2a256 Mon Sep 17 00:00:00 2001 From: June Date: Wed, 22 Jul 2026 14:48:24 -0700 Subject: [PATCH] automata: don't use the backtracker when it can't search any haystack The bounded backtracker needs `len(NFA states) * (len(haystack) + 1)` bits of visited capacity. When the NFA is big enough relative to that capacity, it cannot search even an empty haystack. `BoundedBacktracker::max_haystack_len` saturates to `0` in that case, which is indistinguishable from "an empty haystack is fine." The meta engine's length check is `len > max_haystack_len()`, so an empty haystack passed it, the search returned `HaystackTooLong`, and `BoundedBacktrackerEngine::is_match` unwrapped that error and panicked. The meta engine now declines to build the backtracker at all when it lacks the capacity for a zero-length haystack, which restores the invariant that this engine is only reachable for haystacks it can actually search. Note that only the empty haystack panicked. For any longer haystack the existing length check already rejected the backtracker. Fixes #1344 --- regex-automata/src/meta/wrappers.rs | 15 +++++++++++++++ regex-automata/tests/meta/mod.rs | 1 + regex-automata/tests/meta/regression.rs | 21 +++++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 regex-automata/tests/meta/regression.rs diff --git a/regex-automata/src/meta/wrappers.rs b/regex-automata/src/meta/wrappers.rs index 8d6f738e4c..ca0eed5677 100644 --- a/regex-automata/src/meta/wrappers.rs +++ b/regex-automata/src/meta/wrappers.rs @@ -208,6 +208,21 @@ impl BoundedBacktrackerEngine { .configure(backtrack_config) .build_from_nfa(nfa.clone()) .map_err(BuildError::nfa)?; + // If the backtracker can't even search an empty haystack, then + // don't use it at all. `max_haystack_len` saturates to `0` in that + // case, which the length check in `get` below can't distinguish + // from "an empty haystack is fine." + let nfa_state_len = engine.get_nfa().states().len(); + let max_capacity = 8 * engine.get_config().get_visited_capacity(); + if nfa_state_len > max_capacity { + debug!( + "BoundedBacktracker not used because it needs at least \ + {:?} bits of visited capacity to search even an empty \ + haystack, but only has {:?} bits", + nfa_state_len, max_capacity, + ); + return Ok(None); + } debug!( "BoundedBacktracker built (max haystack length: {:?})", engine.max_haystack_len() diff --git a/regex-automata/tests/meta/mod.rs b/regex-automata/tests/meta/mod.rs index 9d6ab475ef..5a71144e65 100644 --- a/regex-automata/tests/meta/mod.rs +++ b/regex-automata/tests/meta/mod.rs @@ -1,2 +1,3 @@ +mod regression; #[cfg(not(miri))] mod suite; diff --git a/regex-automata/tests/meta/regression.rs b/regex-automata/tests/meta/regression.rs new file mode 100644 index 0000000000..a05717c602 --- /dev/null +++ b/regex-automata/tests/meta/regression.rs @@ -0,0 +1,21 @@ +use regex_automata::meta::Regex; + +// Tests that a regex whose NFA is too big for the bounded backtracker's +// visited capacity doesn't panic on an empty haystack. +// +// See: https://github.com/rust-lang/regex/issues/1344 +#[test] +fn backtracker_visited_capacity_too_small_empty_haystack() { + // The other engines are disabled so that this exercises the backtracker + // path rather than relying on them declining this pattern. + let config = Regex::config() + .nfa_size_limit(Some(1_000_000_000)) + .dfa(false) + .hybrid(false) + .onepass(false); + let re = + Regex::builder().configure(config).build(r"^.{0,404600}$").unwrap(); + + assert!(re.is_match("")); + assert_eq!(Some((0, 0)), re.find("").map(|m| (m.start(), m.end()))); +}