Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in Base
DescriptionHeap buffer overflow in Base
ComponentBase
Bug ClassOOB
Tracker533331920
Fix commit66f992d2d47f (chromium/src) +53/-3
CISA KEVNot listed
CreditedViktoria Zlatinova
Disclosed2026-08-06

Changed Functions

FunctionChangeNotes
TEST
base/substring_set_matcher/substring_set_matcher_unittest.cc
modified
for
base/substring_set_matcher/substring_set_matcher_unittest.cc
modified

Files Changed

  • base/substring_set_matcher/substring_set_matcher.h
  • base/substring_set_matcher/substring_set_matcher_unittest.cc
From 66f992d2d47fd30779ec386af2ea73f93bcb24c8 Mon Sep 17 00:00:00 2001
From: Dominic Battré <battre@chromium.org>
Date: Mon, 27 Jul 2026 12:19:59 -0700
Subject: [PATCH] [M144] Fix heap-buffer-overflow in SubstringSetMatcher

Original change's description:
> Fix heap-buffer-overflow in SubstringSetMatcher
>
> SubstringSetMatcher::AhoCorasickNode::SetEdge could overflow its
> allocated edge capacity when duplicate patterns are provided in release
> builds. In release builds (without DCHECKs), duplicate patterns are not
> rejected and can result in multiple kMatchIDLabel edges being added to
> the same node. This can exceed the maximum capacity of 260 edges and
> cause an out-of-bounds write when subsequent edges (like failure edges)
> are added.
>
> This CL makes SetMatchID idempotent. If a node is already marked as the
> end of a pattern, we return early and do not add a duplicate
> kMatchIDLabel edge.
>
> This behavior is now documented in Build(), and a regression test has
> been added to verify that duplicate patterns are handled safely by
> ignoring subsequent duplicates in release builds.
>
> Bug: 533331920
>
> TAG=agy
> CONV=95405c0a-c9e1-4622-aefd-20ac03e2cd86
>
> Change-Id: I68933c463aa1a37e210002529c67116aece19523
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8076938
> Commit-Queue: Dominic Battré <battre@chromium.org>
> Reviewed-by: Jan Keitel <jkeitel@google.com>
> Cr-Commit-Position: refs/heads/main@{#1661676}

(cherry picked from commit 6ed29677f1d437e8acb732da6e86b19a3209294b)

Bug: 539345880,533331920
Change-Id: I68933c463aa1a37e210002529c67116aece19523
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8157159
Auto-Submit: chrome-cherry-picker@chops-service-accounts.iam.gserviceaccount.com <chrome-cherry-picker@chops-service-accounts.iam.gserviceaccount.com>
Bot-Commit: rubber-stamper@appspot.gserviceaccount.com <rubber-stamper@appspot.gserviceaccount.com>
Commit-Queue: rubber-stamper@appspot.gserviceaccount.com <rubber-stamper@appspot.gserviceaccount.com>
Reviewed-by: Dominic Battré <battre@chromium.org>
Cr-Commit-Position: refs/branch-heads/7559@{#5157}
Cr-Branched-From: 223dfbac1c7542a06b422390d954afe5b560b607-refs/heads/main@{#1552494}
---

diff --git a/base/substring_set_matcher/substring_set_matcher.h b/base/substring_set_matcher/substring_set_matcher.h
index cd7d488e1..356e2f1 100644
--- a/base/substring_set_matcher/substring_set_matcher.h
+++ b/base/substring_set_matcher/substring_set_matcher.h
@@ -31,8 +31,11 @@
   ~SubstringSetMatcher();
 
   // Registers all |patterns|. Each pattern needs to have a unique ID and all
-  // pattern strings must be unique. Build() should be called exactly once
-  // (before it is called, the tree is empty).
+  // pattern strings must be unique. If duplicate pattern strings are passed,
+  // it will trigger a CHECK failure in DCHECK-enabled builds. In release
+  // builds, duplicate patterns are silently ignored (only the first one is
+  // registered) to prevent memory corruption. Build() should be called
+  // exactly once (before it is called, the tree is empty).
   //
   // Complexity:
   //    Let n = number of patterns.
@@ -202,7 +205,11 @@
     void SetFailure(NodeID failure);
 
     void SetMatchID(MatcherStringPattern::ID id) {
-      DCHECK(!IsEndOfPattern());
+      // A node ends at most one pattern; a duplicate would add a second
+      // kMatchIDLabel edge that can overflow the node's storage in SetEdge().
+      if (IsEndOfPattern()) {
+        return;
+      }
       DCHECK(id < kInvalidNodeID);  // This is enforced by Build().
       SetEdge(kMatchIDLabel, static_cast<NodeID>(id));
       has_outputs_ = true;
diff --git a/base/substring_set_matcher/substring_set_matcher_unittest.cc b/base/substring_set_matcher/substring_set_matcher_unittest.cc
index a253fc80..bb51fa2 100644
--- a/base/substring_set_matcher/substring_set_matcher_unittest.cc
+++ b/base/substring_set_matcher/substring_set_matcher_unittest.cc
@@ -228,4 +228,47 @@
   matcher.Build(patterns);
 }
 
+// In DCHECK-enabled builds, duplicate patterns trigger a CHECK failure as they
+// violate the API contract. In release builds, duplicate patterns are silently
+// ignored (only the first one is registered) to prevent memory corruption.
+#if !DCHECK_IS_ON()
+TEST(SubstringSetMatcherTest, DuplicatePatterns) {
+  std::vector<MatcherStringPattern> patterns;
+  MatcherStringPattern::ID id = 0;
+
+  // "a" is a pattern so node "ba" gets a non-root failure edge.
+  patterns.emplace_back("a", id++); // ID 0
+
+  // Four identical "ba" patterns.
+  patterns.emplace_back("ba", id++); // ID 1
+  patterns.emplace_back("ba", id++); // ID 2
+  patterns.emplace_back("ba", id++); // ID 3
+  patterns.emplace_back("ba", id++); // ID 4
+
+  // 256 character children on node "ba" to fill capacity.
+  for (int i = 0; i < 256; ++i) {
+    std::string str;
+    str.push_back('b');
+    str.push_back('a');
+    str.push_back(static_cast<char>(i));
+    patterns.emplace_back(str, id++);
+  }
+
+  SubstringSetMatcher matcher;
+  ASSERT_TRUE(matcher.Build(patterns));
+
+  std::set<MatcherStringPattern::ID> matches;
+  matcher.Match("ba", &matches);
+
+  // We expect to match "a" (ID 0) and the first "ba" pattern (ID 1).
+  // Duplicate "ba" patterns (IDs 2-4) should be ignored.
+  EXPECT_EQ(2u, matches.size());
+  EXPECT_TRUE(matches.find(0) != matches.end());
+  EXPECT_TRUE(matches.find(1) != matches.end());
+  EXPECT_TRUE(matches.find(2) == matches.end());
+  EXPECT_TRUE(matches.find(3) == matches.end());
+  EXPECT_TRUE(matches.find(4) == matches.end());
+}
+#endif  // !DCHECK_IS_ON()
+
 }  // namespace base
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/base/substring_set_matcher/substring_set_matcher_unittest.cc b/base/substring_set_matcher/substring_set_matcher_unittest.cc
index a253fc80..bb51fa2 100644
--- a/base/substring_set_matcher/substring_set_matcher_unittest.cc
+++ b/base/substring_set_matcher/substring_set_matcher_unittest.cc
@@ -228,4 +228,47 @@
   matcher.Build(patterns);
 }
 
+// In DCHECK-enabled builds, duplicate patterns trigger a CHECK failure as they
+// violate the API contract. In release builds, duplicate patterns are silently
+// ignored (only the first one is registered) to prevent memory corruption.
+#if !DCHECK_IS_ON()
+TEST(SubstringSetMatcherTest, DuplicatePatterns) {
+  std::vector<MatcherStringPattern> patterns;
+  MatcherStringPattern::ID id = 0;
+
+  // "a" is a pattern so node "ba" gets a non-root failure edge.
+  patterns.emplace_back("a", id++); // ID 0
+
+  // Four identical "ba" patterns.
+  patterns.emplace_back("ba", id++); // ID 1
+  patterns.emplace_back("ba", id++); // ID 2
+  patterns.emplace_back("ba", id++); // ID 3
+  patterns.emplace_back("ba", id++); // ID 4
+
+  // 256 character children on node "ba" to fill capacity.
+  for (int i = 0; i < 256; ++i) {
+    std::string str;
+    str.push_back('b');
+    str.push_back('a');
+    str.push_back(static_cast<char>(i));
+    patterns.emplace_back(str, id++);
+  }
+
+  SubstringSetMatcher matcher;
+  ASSERT_TRUE(matcher.Build(patterns));
+
+  std::set<MatcherStringPattern::ID> matches;
+  matcher.Match("ba", &matches);
+
+  // We expect to match "a" (ID 0) and the first "ba" pattern (ID 1).
+  // Duplicate "ba" patterns (IDs 2-4) should be ignored.
+  EXPECT_EQ(2u, matches.size());
+  EXPECT_TRUE(matches.find(0) != matches.end());
+  EXPECT_TRUE(matches.find(1) != matches.end());
+  EXPECT_TRUE(matches.find(2) == matches.end());
+  EXPECT_TRUE(matches.find(3) == matches.end());
+  EXPECT_TRUE(matches.find(4) == matches.end());
+}
+#endif  // !DCHECK_IS_ON()
+
 }  // namespace base
Loading diff…

Original Bug Report

reported by vi...@microsoft.com

Security: heap-buffer-overflow in base::SubstringSetMatcher::AhoCorasickNode::SetEdge (OOB write on duplicate patterns)

VULNERABILITY DETAILS

Heap-buffer-overflow (4-byte out-of-bounds write) in base::SubstringSetMatcher during tree construction (Build()), reported by GWP-ASan: WRITE of size 4, 0 bytes to the right of a 1040-byte region.

Root cause: SubstringSetMatcher::Build() requires unique pattern strings, but its uniqueness CHECK is compiled only in DCHECK-enabled builds and is absent from release builds. Duplicate strings terminate at the same Aho-Corasick node, causing repeated calls to SetMatchID() and redundant kMatchIDLabel edges. Each node’s edge array is capped at 260 four-byte entries (1040 bytes). In the repro, 256 character edges plus one valid and three duplicate match-id edges fill the array. When Build() subsequently adds the failure edge, SetFailure() calls SetEdge() to write the 261st edge, four bytes past the allocation.

The debug-only uniqueness check is compiled out of release builds, so nothing prevents the corruption there. GWP-ASan caught it via a guard page; without the guard page (the common, unsampled case) it is a silent 4-byte corruption of the adjacent heap allocation. The write value is a packed AhoCorasickEdge (9-bit label + 23-bit node index), and both the location and the node-index value are influenced by the input pattern set.

Reachability: SubstringSetMatcher is a shared //base primitive. It is fed attacker-influenceable input through the extension declarativeNetRequest regex path (RegexRulesMatcher/RegexSetMatcher) – FilteredRE2 emits candidate “atom” substrings that are passed to SubstringSetMatcher::Build() – and is also used by components/url_matcher.

VERSION

Chrome Version: 152.0.4141.0; the vulnerable code is long-standing, so all current stable/beta/dev channels are affected. Operating System: Observed on Windows via GWP-ASan. The affected code is platform-independent C++ in //base, so it is not OS-specific.

REPRODUCTION CASE

This is not a web-page bug; it triggers in the browser process during matcher construction.

Minimal deterministic repro (unit test): register the following patterns and call SubstringSetMatcher::Build():

  • “a” (one pattern) - so node “ba” gets a stored non-root failure edge,
  • “ba” four times (distinct IDs) — the duplicate strings that add extra match-id edges,
  • “ba” + <byte> for all 256 byte values — fills node “ba” to its 256 character-edge maximum.

Node “ba” reaches 256 + 4 = 260 edges (the cap); the failure edge added next is the overflowing 261st write. Must be built without DCHECKs (e.g. ASan with dcheck_always_on=false), because Build()’s debug-only uniqueness check otherwise aborts before the release path is reached.

Ready-to-run test (drop into base/substring_set_matcher/substring_set_matcher_unittest.cc):

// Regression test for a heap-buffer-overflow in AhoCorasickNode::SetEdge().
// Duplicate pattern strings give one node multiple kMatchIDLabel edges. Node
// "ba" reaches 256 char edges + 4 duplicate match IDs = 260 (the storage cap),
// so the failure edge added next is the overflowing 261st SetEdge() call.
// Built only without DCHECKs: the duplicate strings trip Build()'s DCHECK-only
// uniqueness check before this release-only overflow path is reached.
#if !DCHECK_IS_ON()
TEST(SubstringSetMatcherTest, DuplicatePatternEdgeOverflow) {
  std::vector<MatcherStringPattern> patterns;
  MatcherStringPattern::ID id = 0;

  // "a" is a pattern so node "ba" gets a non-root failure edge.
  patterns.emplace_back("a", id++);

  // Four identical "ba" patterns -> four match-ID edges (three illegal dups).
  patterns.emplace_back("ba", id++);
  patterns.emplace_back("ba", id++);
  patterns.emplace_back("ba", id++);
  patterns.emplace_back("ba", id++);

  // 256 character children on node "ba".
  for (int i = 0; i < 256; ++i) {
    std::string str;
    str.push_back('b');
    str.push_back('a');
    str.push_back(static_cast<char>(i));
    patterns.emplace_back(str, id++);
  }

  SubstringSetMatcher matcher;
  matcher.Build(patterns);
}
#endif  // !DCHECK_IS_ON()

POTENTIAL FIX

Make SetMatchID() idempotent. A node can end at most one pattern, so repeated calls for a duplicate string should keep the first match ID and add no further edge.

File: base/substring_set_matcher/substring_set_matcher.h

void SetMatchID(MatcherStringPattern::ID id) {
  // A node ends at most one pattern; a duplicate would add a second
  // kMatchIDLabel edge that can overflow the node's storage in SetEdge().
  if (IsEndOfPattern()) {
    return;
  }
  DCHECK(id < kInvalidNodeID);  // This is enforced by Build().
  SetEdge(kMatchIDLabel, static_cast<NodeID>(id));
  has_outputs_ = true;
}

This preserves behavior for valid unique inputs while preventing redundant match-ID edges from exhausting the node’s edge storage in release builds.

FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION

Type of crash: browser process Crash State:

#0  base::SubstringSetMatcher::AhoCorasickNode::SetEdge
    base/substring_set_matcher/substring_set_matcher.cc:479
#1  base::SubstringSetMatcher::AhoCorasickNode::SetFailure
    base/substring_set_matcher/substring_set_matcher.cc:490
#2  base::SubstringSetMatcher::CreateFailureAndOutputEdges
    base/substring_set_matcher/substring_set_matcher.cc:318
#3  base::SubstringSetMatcher::BuildAhoCorasickTree
    base/substring_set_matcher/substring_set_matcher.cc:224
#4  base::SubstringSetMatcher::Build
    base/substring_set_matcher/substring_set_matcher.cc:90
#5  base::SubstringSetMatcher::Build
    base/substring_set_matcher/substring_set_matcher.cc:51
#6  extensions::declarative_net_request::RegexRulesMatcher::MatchHelper::InitializeMatcher
    extensions/browser/api/declarative_net_request/regex_rules_matcher.cc:331
#7  extensions::declarative_net_request::RegexRulesMatcher::MatchHelper::MatchHelper
    extensions/browser/api/declarative_net_request/regex_rules_matcher.cc:194
#8  extensions::declarative_net_request::RegexRulesMatcher::RegexRulesMatcher
    extensions/browser/api/declarative_net_request/regex_rules_matcher.cc:110
#9  extensions::declarative_net_request::RulesetMatcher::RulesetMatcher
    extensions/browser/api/declarative_net_request/ruleset_matcher.cc:257
#10 std::__Cr::make_unique
    third_party/libc++/src/include/__memory/unique_ptr.h:756
#11 extensions::declarative_net_request::RulesetSource::CreateVerifiedMatcher
    extensions/browser/api/declarative_net_request/ruleset_source.cc:142
#12 extensions::declarative_net_request::FileBackedRulesetSource::CreateVerifiedMatcher
    extensions/browser/api/declarative_net_request/file_backed_ruleset_source.cc:465
#13 extensions::declarative_net_request::RulesetInfo::CreateVerifiedMatcher
    extensions/browser/api/declarative_net_request/file_sequence_helper.cc:386
#14 extensions::declarative_net_request::FileSequenceHelper::UpdateDynamicRules
    extensions/browser/api/declarative_net_request/file_sequence_helper.cc:489

FileSequenceHelper::UpdateDynamicRules was dispatched asynchronously through base::OnceCallback on a base::ThreadPool worker. Generic callback and worker-thread frames are omitted

CREDIT INFORMATION

Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited? Reporter credit: Viktoria Zlatinova

View on issue tracker