From 0cc5cf3b9b01fdd2019fae44e6f37b358237c6cf Mon Sep 17 00:00:00 2001 From: enanorojo Date: Sat, 8 Aug 2026 11:37:39 +0200 Subject: [PATCH] Fix PossibleMatchRange returning true with min > max for repeated high bytes in Latin1 RE2::PossibleMatchRange() violates its documented contract (re2.h:522-537) for patterns containing a repeated high byte (>= 0x80) in Latin1 encoding: it returns true with min > max. For example, "^<0xFF><0xFF>+" in Latin1 mode returns ok=true, min="\xFF" (1 byte), max="" (0 bytes). The documentation states: "Assuming PossibleMatchRange(&min, &max, N) returns successfully, any string s that is an anchored match for this regexp satisfies min <= s && s <= max." With max == "" only the empty string satisfies s <= max, but the regexp requires at least two characters and never matches the empty string -- a direct contradiction. Root cause (two sites): 1. RE2::PossibleMatchRange (re2.cc): when the inner prog call fails, the caller rounds up *max via PrefixSuccessor(). If the prefix consists entirely of 0xFF bytes, PrefixSuccessor() returns the empty string. The caller then returns true with min > max. 2. DFA::PossibleMatchRange (dfa.cc): the maximum-prefix loop can terminate on the first iteration with !extended and return true with *max empty, without reaching the existing max->empty() guard. Fix: in both sites, when *max is empty and *min is not empty, report failure instead of returning a range that violates the documented invariant. The empty regexp case (min == max == "") is preserved. --- re2/dfa.cc | 7 +++++++ re2/re2.cc | 8 ++++++++ re2/testing/possible_match_test.cc | 10 ++++++++++ 3 files changed, 25 insertions(+) diff --git a/re2/dfa.cc b/re2/dfa.cc index d587a5520..7ba4b2f60 100644 --- a/re2/dfa.cc +++ b/re2/dfa.cc @@ -2106,6 +2106,13 @@ bool DFA::PossibleMatchRange(std::string* min, std::string* max, int maxlen) { } if (!extended) { // Done, no need for PrefixSuccessor. + // If *max is empty and *min is not, we have no way to express + // "no maximum string" and must report failure: returning true + // would give a range with min > max, violating the documented + // invariant. (The empty regexp, with min == max == "", is + // handled correctly here.) + if (max->empty() && !min->empty()) + return false; return true; } } diff --git a/re2/re2.cc b/re2/re2.cc index 2e00d2285..4768b767b 100644 --- a/re2/re2.cc +++ b/re2/re2.cc @@ -626,6 +626,14 @@ bool RE2::PossibleMatchRange(std::string* min, std::string* max, // but we still have useful information from prefix_. // Round up *max to allow any possible suffix. PrefixSuccessor(max); + // If *max is empty, the prefix consisted entirely of 0xFF bytes + // and PrefixSuccessor has no successor to return. This would + // produce a range with min > max, violating the documented + // invariant "min <= s <= max" for any anchored match s. + // (The empty regexp is handled correctly elsewhere: it returns + // min == max == "".) + if (max->empty() && !min->empty()) + return false; } else { // Nothing useful. *min = ""; diff --git a/re2/testing/possible_match_test.cc b/re2/testing/possible_match_test.cc index f217947d6..dd929a277 100644 --- a/re2/testing/possible_match_test.cc +++ b/re2/testing/possible_match_test.cc @@ -165,6 +165,16 @@ TEST(PossibleMatchRange, Failures) { // Fails because it's a malformed regexp. EXPECT_FALSE(RE2("*hello").PossibleMatchRange(&min, &max, 10)) << "min=" << absl::CEscape(min) << ", max=" << absl::CEscape(max); + + // A repeated high byte in Latin1 used to return true with min > max + // (contract violation): min="\xFF", max="". The regexp "^<0xFF><0xFF>+" + // never matches the empty string, so the empty max is a lie. + EXPECT_FALSE(RE2("^\xFF\xFF+", RE2::Latin1). + PossibleMatchRange(&min, &max, 100)) + << "min=" << absl::CEscape(min) << ", max=" << absl::CEscape(max); + EXPECT_FALSE(RE2("^\xFF+", RE2::Latin1). + PossibleMatchRange(&min, &max, 100)) + << "min=" << absl::CEscape(min) << ", max=" << absl::CEscape(max); } // Exhaustive test: generate all regexps within parameters,