High chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in Extensions API
DescriptionInteger overflow in Extensions API
ComponentExtensions API
Bug ClassInteger Overflow
Tracker515443146
Fix commita0f367a0f93e (chromium/src) +168/-18
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-08

Changed Functions

FunctionChangeNotes
if
extensions/browser/api/declarative_net_request/declarative_net_request_api.cc
modified

Files Changed

  • extensions/browser/api/declarative_net_request/constants.h
  • extensions/browser/api/declarative_net_request/declarative_net_request_api.cc
  • extensions/browser/api/declarative_net_request/file_backed_ruleset_source.cc
  • extensions/browser/api/declarative_net_request/file_backed_ruleset_source.h
  • extensions/browser/api/declarative_net_request/file_sequence_helper.cc
From a0f367a0f93ed98023a147c051eb599d29666500 Mon Sep 17 00:00:00 2001
From: Kelvin Jiang <[email protected]>
Date: Fri, 12 Jun 2026 16:49:34 -0700
Subject: [PATCH] [DNR] Cap rulesets to 2GB to fix buffer overflow and indexing crashes

Cap the maximum file size of DNR rulesets to 512mb since currently, we
use 32 bit relative offsets for FlatBuffers which has a limit to where
in memory it could point before triggering buffer over/underflows.

The max ruleset file size was uncapped before. Capping at 512mb should
be way more than enough to fit the max number of rules assuming rules
are constructed "reasonably" and not say, loaded with millions of custom
header matching values intended to bloat size.

Fixed: 515443146
Change-Id: Idfb5cc481d31d09657276fefc8f85100223a2f1c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7909934
Reviewed-by: Devlin Cronin <[email protected]>
Commit-Queue: Kelvin Jiang <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1646324}
---

diff --git a/extensions/browser/api/declarative_net_request/constants.h b/extensions/browser/api/declarative_net_request/constants.h
index 1477c66..d931bc5 100644
--- a/extensions/browser/api/declarative_net_request/constants.h
+++ b/extensions/browser/api/declarative_net_request/constants.h
@@ -114,10 +114,11 @@
   kErrorWriteJson = 17,
   kErrorWriteFlatbuffer = 18,
   kErrorUnsafeRuleCountExceeded = 19,
+  kErrorCreateMatcher_RulesetFileSizeLimitExceeded = 20,
 
   // Magic constant used by histograms code. Should be equal to the largest enum
   // value.
-  kMaxValue = kErrorUnsafeRuleCountExceeded,
+  kMaxValue = kErrorCreateMatcher_RulesetFileSizeLimitExceeded,
 };
 
 // Describes the result of loading a single JSON Ruleset.
@@ -148,9 +149,12 @@
   // prefs.
   kErrorChecksumNotFound = 5,
 
+  // Ruleset loading failed because the indexed file exceeded the size limit.
+  kErrorRulesetFileSizeLimitExceeded = 6,
+
   // Magic constant used by histograms code. Should be equal to the largest enum
   // value.
-  kMaxValue = kErrorChecksumNotFound,
+  kMaxValue = kErrorRulesetFileSizeLimitExceeded,
 };
 
 // Specifies whether and how extensions require host permissions to modify the
diff --git a/extensions/browser/api/declarative_net_request/declarative_net_request_api.cc b/extensions/browser/api/declarative_net_request/declarative_net_request_api.cc
index be15017..7ea6fdb 100644
--- a/extensions/browser/api/declarative_net_request/declarative_net_request_api.cc
+++ b/extensions/browser/api/declarative_net_request/declarative_net_request_api.cc
@@ -208,7 +208,8 @@
   // Unlike errors such as kJSONParseError, which normally denote corruption, a
   // read error is probably a transient error.  Hence raise an error instead of
   // returning an empty list.
-  if (read_json_result.status == Status::kFileReadError) {
+  if (read_json_result.status == Status::kFileReadError ||
+      read_json_result.status == Status::kRulesetFileSizeLimitExceeded) {
     Respond(Error(declarative_net_request::kInternalErrorGettingDynamicRules));
     return;
   }
diff --git a/extensions/browser/api/declarative_net_request/file_backed_ruleset_source.cc b/extensions/browser/api/declarative_net_request/file_backed_ruleset_source.cc
index de360cc..9574cdee 100644
--- a/extensions/browser/api/declarative_net_request/file_backed_ruleset_source.cc
+++ b/extensions/browser/api/declarative_net_request/file_backed_ruleset_source.cc
@@ -26,6 +26,7 @@
 #include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/timer/elapsed_timer.h"
+#include "base/types/expected.h"
 #include "base/values.h"
 #include "content/public/browser/browser_context.h"
 #include "extensions/browser/api/declarative_net_request/constants.h"
@@ -48,12 +49,40 @@
 
 constexpr const char kFileDoesNotExistError[] = "File does not exist.";
 constexpr const char kFileReadError[] = "File read error.";
+constexpr const char kRulesetFileSizeLimitExceededError[] =
+    "Ruleset file size limit exceeded.";
 
 constexpr const char kDynamicRulesetDirectory[] = "DNR Extension Rules";
 constexpr const char kDynamicRulesJSONFilename[] = "rules.json";
 constexpr const char kDynamicIndexedRulesFilename[] = "rules.fbs";
 
-// Helper to retrieve the filename for the given |file_path|.
+// Describes the results of reading a file.
+enum class FileReadError {
+  // File reading failed because the file size exceeded a specified limit.
+  kSizeLimitExceeded,
+  // Used for all other failures.
+  kOther,
+};
+
+// Helper to read the ruleset file from the given `file_path`. Returns an error
+// if the file is larger than the maximum ruleset file size or if any other file
+// read error occurs.
+base::expected<std::string, FileReadError> ReadRulesetFileToString(
+    const base::FilePath& file_path) {
+  std::string contents;
+  const size_t kMaxSize = GetMaximumRulesetFileSize();
+  if (!base::ReadFileToStringWithMaxSize(file_path, &contents, kMaxSize)) {
+    const FileReadError error = contents.size() == kMaxSize
+                                    ? FileReadError::kSizeLimitExceeded
+                                    : FileReadError::kOther;
+
+    return base::unexpected(error);
+  }
+
+  return contents;
+}
+
+// Helper to retrieve the filename for the given `file_path`.
 std::string GetFilename(const base::FilePath& file_path) {
   return file_path.BaseName().AsUTF8Unsafe();
 }
@@ -384,12 +413,17 @@
     return;
   }
 
-  std::string json_contents;
-  if (!base::ReadFileToString(json_path_, &json_contents)) {
+  auto contents = ReadRulesetFileToString(json_path_);
+  if (!contents.has_value()) {
+    std::string error_message =
+        (contents.error() == FileReadError::kSizeLimitExceeded)
+            ? kRulesetFileSizeLimitExceededError
+            : kFileReadError;
     std::move(callback).Run(IndexAndPersistJSONRulesetResult::CreateErrorResult(
-        GetErrorWithFilename(json_path_, kFileReadError)));
+        GetErrorWithFilename(json_path_, error_message)));
     return;
   }
+  std::string json_contents = std::move(contents).value();
 
   decoder->ParseJson(json_contents,
                      base::BindOnce(&OnSafeJSONParse, json_path_, Clone(),
@@ -404,11 +438,17 @@
                                                   kFileDoesNotExistError);
   }
 
-  std::string json_contents;
-  if (!base::ReadFileToString(json_path_, &json_contents)) {
+  auto contents = ReadRulesetFileToString(json_path_);
+  if (!contents.has_value()) {
+    if (contents.error() == FileReadError::kSizeLimitExceeded) {
+      return ReadJSONRulesResult::CreateErrorResult(
+          Status::kRulesetFileSizeLimitExceeded,
+          kRulesetFileSizeLimitExceededError);
+    }
     return ReadJSONRulesResult::CreateErrorResult(Status::kFileReadError,
                                                   kFileReadError);
   }
+  std::string json_contents = std::move(contents).value();
 
   auto value_with_error = base::JSONReader::ReadAndReturnValueWithError(
       json_contents, base::JSON_PARSE_RFC /* options */);
@@ -448,10 +488,13 @@
     return LoadRulesetResult::kErrorInvalidPath;
   }
 
-  std::string ruleset_data;
-  if (!base::ReadFileToString(indexed_path(), &ruleset_data)) {
-    return LoadRulesetResult::kErrorCannotReadFile;
+  auto data = ReadRulesetFileToString(indexed_path());
+  if (!data.has_value()) {
+    return data.error() == FileReadError::kSizeLimitExceeded
+               ? LoadRulesetResult::kErrorRulesetFileSizeLimitExceeded
+               : LoadRulesetResult::kErrorCannotReadFile;
   }
+  std::string ruleset_data = std::move(data).value();
 
   if (!StripVersionHeaderAndParseVersion(&ruleset_data)) {
     return LoadRulesetResult::kErrorVersionMismatch;
diff --git a/extensions/browser/api/declarative_net_request/file_backed_ruleset_source.h b/extensions/browser/api/declarative_net_request/file_backed_ruleset_source.h
index 5e53e4b1..e6e26b6 100644
--- a/extensions/browser/api/declarative_net_request/file_backed_ruleset_source.h
+++ b/extensions/browser/api/declarative_net_request/file_backed_ruleset_source.h
@@ -112,10 +112,11 @@
     // Status returned when the list of rules to be read exceeds the static rule
     // count limit.
     kRuleCountLimitExceeded = 5,
+    kRulesetFileSizeLimitExceeded = 6,
 
     // Magic constant used by histograms code. Should be equal to the maximum
     // enum value.
-    kMaxValue = kRuleCountLimitExceeded
+    kMaxValue = kRulesetFileSizeLimitExceeded
   };
 
   static ReadJSONRulesResult CreateErrorResult(Status status,
diff --git a/extensions/browser/api/declarative_net_request/file_sequence_helper.cc b/extensions/browser/api/declarative_net_request/file_sequence_helper.cc
index 31b8a9a..199b255 100644
--- a/extensions/browser/api/declarative_net_request/file_sequence_helper.cc
+++ b/extensions/browser/api/declarative_net_request/file_sequence_helper.cc
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/extensions/browser/api/declarative_net_request/file_sequence_helper_unittest.cc b/extensions/browser/api/declarative_net_request/file_sequence_helper_unittest.cc
index a2416cba..2d1e5ded 100644
--- a/extensions/browser/api/declarative_net_request/file_sequence_helper_unittest.cc
+++ b/extensions/browser/api/declarative_net_request/file_sequence_helper_unittest.cc
@@ -218,7 +218,7 @@
 }
 
 TEST_F(FileSequenceHelperTest, IndexedRulesetDeleted) {
-  const size_t kNumRulesets = 3;
+  constexpr size_t kNumRulesets = 3;
   std::vector<TestCase> test_cases = InitializeRulesets(kNumRulesets);
 
   TestLoadRulesets(test_cases);
@@ -238,7 +238,7 @@
 }
 
 TEST_F(FileSequenceHelperTest, ChecksumMismatch) {
-  const size_t kNumRulesets = 4;
+  constexpr size_t kNumRulesets = 4;
   std::vector<TestCase> test_cases = InitializeRulesets(kNumRulesets);
 
   TestLoadRulesets(test_cases);
@@ -269,7 +269,7 @@
 }
 
 TEST_F(FileSequenceHelperTest, RulesetFormatVersionMismatch) {
-  const size_t kNumRulesets = 4;
+  constexpr size_t kNumRulesets = 4;
   std::vector<TestCase> test_cases = InitializeRulesets(kNumRulesets);
 
   TestLoadRulesets(test_cases);
@@ -289,7 +289,7 @@
 }
 
 TEST_F(FileSequenceHelperTest, JSONAndIndexedRulesetDeleted) {
-  const size_t kNumRulesets = 3;
+  constexpr size_t kNumRulesets = 3;
   std::vector<TestCase> test_cases = InitializeRulesets(kNumRulesets);
 
   TestLoadRulesets(test_cases);
@@ -374,5 +374,71 @@
   }
 }
 
+// Test that attempting to load a static ruleset file larger than a maximum size
+// fails cleanly. Regression for crbug.com/515443146.
+TEST_F(FileSequenceHelperTest, MaxRulesetSizeStatic) {
+  constexpr size_t kNumRulesets = 1;
+  std::vector<TestCase> test_cases = InitializeRulesets(kNumRulesets);
+
+  // 1. Set a low but valid limit (12 KB). The ruleset should load successfully.
+  base::AutoReset<size_t> limit_override =
+      CreateScopedMaxRulesetSizeOverrideForTesting(12 * 1024);
+  TestLoadRulesets(test_cases);
+
+  // 2. Set the limit to 1 byte. Loading the ruleset should now fail because
+  // the indexed ruleset file on disk is larger than 1 byte.
+  base::AutoReset<size_t> limit_override_2 =
+      CreateScopedMaxRulesetSizeOverrideForTesting(1);
+
+  // Since we loaded successfully in step 1, the indexed file now exists.
+  // When we try to load it again with the new file size limit:
+  // - CreateVerifiedMatcher will fail to read the indexed file as its size
+  //   exceeds the limit.
+  // - Chrome will try to re-index from JSON. This fails as the JSON file size
+  //   also exceeds the limit.
+  // - Loading fails with kErrorRulesetFileSizeLimitExceeded.
+  test_cases[0].expected_result.indexing_successful = false;
+  test_cases[0].expected_result.load_result =
+      LoadRulesetResult::kErrorRulesetFileSizeLimitExceeded;
+
+  TestLoadRulesets(test_cases);
+}
+
+// Test that attempting to update dynamic rules when the existing ruleset file
+// is larger than a maximum size fails cleanly. Regression for
+// crbug.com/515443146.
+TEST_F(FileSequenceHelperTest, MaxRulesetSizeDynamic) {
+  // Simulate adding rules for the first time i.e. with no JSON and indexed
+  // ruleset files.
+  FileBackedRulesetSource source = CreateTemporarySource();
+  base::DeleteFile(source.json_path());
+  base::DeleteFile(source.indexed_path());
+
+  // Write a rule first so the file exists.
+  std::vector<api::declarative_net_request::Rule> api_rules;
+  api_rules.push_back(GetAPIRule(CreateGenericRule()));
+  TestAddDynamicRules(source.Clone(), std::move(api_rules),
+                      ReadJSONRulesResult::Status::kFileDoesNotExist,
+                      UpdateDynamicRulesStatus::kSuccess,
+                      /*expected_error=*/std::nullopt,
+                      /*expected_did_load_successfully=*/true);
+
+  // Set a very small limit so any subsequent write will cause the rules file to
+  // exceed the maximum size.
+  base::AutoReset<size_t> limit_override =
+      CreateScopedMaxRulesetSizeOverrideForTesting(1);
+
+  // Attempting to add another rule should fail because reading the dynamic rule
+  // file fails due to it exceeding the maximum size.
+  api_rules.clear();
+  api_rules.push_back(GetAPIRule(CreateGenericRule()));
+  TestAddDynamicRules(
+      source.Clone(), std::move(api_rules),
+      ReadJSONRulesResult::Status::kRulesetFileSizeLimitExceeded,
+      UpdateDynamicRulesStatus::kErrorReadJSONRules,
+      kInternalErrorUpdatingDynamicRules,
+      /*expected_did_load_successfully=*/false);
+}
+
 }  // namespace
 }  // namespace extensions::declarative_net_request
Loading diff…

Original Bug Report

reported by [email protected]

OOB heap read in browser process via 32-bit FlatBufferBuilder overflow in DNR indexing

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A malicious extension can potentially trigger an out-of-bounds (OOB) heap read in the unsandboxed browser process during Declarative Net Request (DNR) ruleset indexing. This is caused by a 32-bit integer overflow in the FlatBuffers library when serializing large rulesets, which leads to pointer underflows during rule sorting. The issue is reachable because DNR ruleset parsing now occurs in-process following the migration to the Rust-backed JSON parser.

Affected files:

  • components/url_pattern_index/url_pattern_index.cc
  • third_party/flatbuffers/src/include/flatbuffers/vector_downward.h
  • third_party/flatbuffers/src/include/flatbuffers/flatbuffer_builder.h
  • extensions/browser/api/declarative_net_request/flat_ruleset_indexer.cc

Estimated timestamp from git blame: Unknown (Google3 checkout)

Summary

A potential vulnerability in the Declarative Net Request (DNR) indexing logic allows a malicious extension to trigger an out-of-bounds (OOB) heap read in the browser process. The root cause is a 32-bit integer overflow in the flatbuffers::FlatBufferBuilder class (specifically within its backing store, vector_downward). When serialized data exceeds 4 GiB, internal size tracking wraps around, leading to pointer underflows and OOB memory access during the rule sorting phase of indexing.

Technical Details

FlatBufferBuilder 32-bit Overflow

The flatbuffers::vector_downward<uoffset_t> class (where uoffset_t is uint32_t) tracks the byte size of serialized data in a 32-bit field size_. While the write cursor cur_ is a 64-bit pointer, the size_ field is incremented using 32-bit arithmetic:

// third_party/flatbuffers/src/include/flatbuffers/vector_downward.h:155
inline uint8_t* make_space(size_t len) {
  if (len) {
    ensure_space(len);
    cur_  -= len;                                 // 64-bit pointer decrement
    size_ += static_cast<SizeT>(len);             // 32-bit size increment (wraps at 4 GiB)
  }
  return cur_;
}

In release builds, the FLATBUFFERS_ASSERT checks are disabled. If an extension provides a ruleset that serializes to more than 4 GiB, size_ wraps around.

Browser Process Pointer Underflow

Following the transition to the Rust-backed base::JSONReader, JSON parsing of DNR rulesets during extension installation occurs in-process within the browser process (orchestrated by SandboxedUnpacker and InstallIndexHelper). This removes Mojo IPC message size limits that previously acted as a safeguard.

Once rules are indexed, UrlPatternIndexBuilder::Finish() attempts to sort the rules by priority. It resolves rule offsets to pointers using flatbuffers::GetTemporaryPointer:

// third_party/flatbuffers/src/include/flatbuffers/flatbuffer_builder.h:1510
template <typename T>
const T* GetTemporaryPointer(const FlatBufferBuilder& fbb, Offset<T> offset) {
  return reinterpret_cast<const T*>(fbb.GetCurrentBufferPointer() + fbb.GetSize() - offset.o);
}

If fbb.GetSize() has wrapped (e.g., to 100 MB) but the offset.o for a rule was generated before the wrap (e.g., at 3.5 GB), the calculation fbb.GetSize() - offset.o results in a large negative value. When added to GetCurrentBufferPointer(), this causes a pointer underflow, resulting in an address billions of bytes before the valid heap buffer. Dereferencing this pointer during sorting leads to an OOB heap read.

Potential Attack Sequence (Theoretical)

  1. An attacker develops an extension with the declarativeNetRequest permission.
  2. The extension includes a static ruleset JSON file containing approximately 330,000 rules (the current maximum limit) with exceptionally large urlFilter and domain strings, designed to exceed 4 GiB when serialized.
  3. A user installs the extension on a 64-bit system with sufficient RAM (e.g., 32 GB).
  4. During installation, the browser process indexes the ruleset. The FlatBufferBuilder size overflows, and the subsequent sorting process triggers an OOB read, potentially leaking sensitive browser memory or causing a browser crash.

Suggested Fix

  1. Use flatbuffers::FlatBufferBuilder64 for DNR ruleset indexing to support offsets larger than 32 bits.
  2. Alternatively, implement explicit byte-size limits in FlatRulesetIndexer or RulesetSource to ensure the total serialized size cannot exceed a safe threshold (e.g., 2 GiB) before the overflow occurs.

Evaluated with Chrome root at commit: 29093e11cf509e3593f6229e4b1b075cca356049


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker