Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Variations
DescriptionInsufficient validation of untrusted input in Variations
ComponentVariations
Bug ClassLogic Error
Tracker513323066
Fix commitfe5796ad6fa8 (chromium/src) +57/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
TEST_P
components/variations/variations_seed_store_unittest.cc
modified

Files Changed

  • components/variations/metrics.h
  • components/variations/variations_seed_store.cc
  • components/variations/variations_seed_store_unittest.cc
  • tools/metrics/histograms/metadata/variations/enums.xml
From fe5796ad6fa8684aa2de4fb886faaa8b4f689b16 Mon Sep 17 00:00:00 2001
From: Łukasz Burchard <lburchard@google.com>
Date: Tue, 16 Jun 2026 07:25:27 -0700
Subject: [PATCH] [variations] Enforce uncompressed size limit on seeds

This change mitigates a potential Out-Of-Memory (OOM) crash
vulnerability when decompressing GZIP-compressed variations seeds.
Currently, variations seed decompression trustfully reads the ISIZE
footer of the gzip stream to resize the allocation buffer before
decompressing. A malicious or compromised network actor intercepting
variations requests could serve a gzip bomb with a forged ISIZE footer
(e.g., 4GB) to trigger a deterministic OOM crash.

This CL enforces a maximum uncompressed variations seed size limit of 50
MiB. Seeds whose uncompressed size (as reported by the gzip footer)
exceeds this limit are rejected safely before decompression is
attempted.

TAG=agy
CONV=dcf7b49a-43d1-420d-a3b0-bc59b4b449bc

Bug: 513323066
Change-Id: Ic165be6d1bf76ef8b26b6da21a8669bcf99b74f3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7907587
Reviewed-by: Alexei Svitkine <asvitkine@chromium.org>
Commit-Queue: Łukasz Burchard <lburchard@google.com>
Reviewed-by: Ramon Cano Aparicio <rcanoaparicio@google.com>
Cr-Commit-Position: refs/heads/main@{#1647532}
---

diff --git a/components/variations/metrics.h b/components/variations/metrics.h
index 5d312c1e8..d307f965 100644
--- a/components/variations/metrics.h
+++ b/components/variations/metrics.h
@@ -53,6 +53,7 @@
 //
 // These values are persisted to logs. Entries should not be renumbered and
 // numeric values should never be reused.
+// LINT.IfChange(StoreSeedResult)
 enum class StoreSeedResult {
   kSuccess = 0,
   // kFailedEmpty = 1,  // Deprecated.
@@ -74,8 +75,11 @@
   kNonGzipDeltaCount = 13,
   kGzipFullCount = 14,
   kNonGzipFullCount = 15,
-  kMaxValue = kNonGzipFullCount,
+  // The uncompressed size of the seed exceeded the limit.
+  kUncompressedSizeLimitExceeded = 16,
+  kMaxValue = kUncompressedSizeLimitExceeded,
 };
+// LINT.ThenChange(//tools/metrics/histograms/metadata/variations/enums.xml:VariationsSeedStoreResult)
 
 // The result of updating the date associated with an existing stored variations
 // seed.
diff --git a/components/variations/variations_seed_store.cc b/components/variations/variations_seed_store.cc
index c91da89..bb15365 100644
--- a/components/variations/variations_seed_store.cc
+++ b/components/variations/variations_seed_store.cc
@@ -10,7 +10,9 @@
 
 #include "base/base64.h"
 #include "base/build_time.h"
+#include "base/byte_size.h"
 #include "base/command_line.h"
+#include "base/debug/dump_without_crashing.h"
 #include "base/functional/callback_helpers.h"
 #include "base/logging.h"
 #include "base/metrics/histogram_functions.h"
@@ -149,6 +151,23 @@
 // Returns success or error, populating result on success.
 StoreSeedResult Uncompress(const std::string& compressed, std::string* result) {
   DCHECK(result);
+  uint32_t uncompressed_size = compression::GetUncompressedSize(compressed);
+
+  // Dump without crashing to alert us that actual seeds are approaching the
+  // rejection threshold below.
+  constexpr static base::ByteSize kDumpThreshold = base::MiBU(40);
+  if (uncompressed_size > kDumpThreshold.InBytes()) {
+    base::debug::DumpWithoutCrashing();
+  }
+
+  // Enforce a maximum uncompressed size to prevent OOM / Gzip bomb crashes.
+  // We use 50 MiB as a conservative limit, similar to seed_reader_writer.cc.
+  constexpr static base::ByteSize kMaxUncompressedSeedSize = base::MiBU(50);
+  if (uncompressed_size > kMaxUncompressedSeedSize.InBytes()) {
+    VLOG(1) << "Rejecting seed: uncompressed size " << uncompressed_size
+            << " exceeds limit of " << kMaxUncompressedSeedSize.InBytes();
+    return StoreSeedResult::kUncompressedSizeLimitExceeded;
+  }
   if (!compression::GzipUncompress(compressed, result)) {
     return StoreSeedResult::kFailedUngzip;
   }
diff --git a/components/variations/variations_seed_store_unittest.cc b/components/variations/variations_seed_store_unittest.cc
index b73ddaa..63d7bde7 100644
--- a/components/variations/variations_seed_store_unittest.cc
+++ b/components/variations/variations_seed_store_unittest.cc
@@ -1614,6 +1614,34 @@
   EXPECT_EQ(serialized_seed, SerializeSeed(stored_seed_));
 }
 
+TEST_P(StoreSeedDataAllGroupsTest, GzipUncompressSizeLimit) {
+  TestVariationsSeedStore seed_store(&prefs_, temp_dir_.GetPath());
+  ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
+            GetParam().field_trial_group);
+
+  std::string compressed;
+  ASSERT_TRUE(compression::GzipCompress(CreateTooLargeData(), &compressed));
+
+  base::HistogramTester histogram_tester;
+
+  // Storing should fail because the uncompressed size exceeds the 50 MiB limit.
+  EXPECT_FALSE(
+      StoreSeedData(seed_store, compressed, {.is_gzip_compressed = true}));
+
+  histogram_tester.ExpectBucketCount("Variations.SeedStoreResult",
+                                     StoreSeedResult::kGzipFullCount, 1);
+  histogram_tester.ExpectBucketCount(
+      "Variations.SeedStoreResult",
+      StoreSeedResult::kUncompressedSizeLimitExceeded, 1);
+  histogram_tester.ExpectTotalCount("Variations.SeedStoreResult", 2);
+
+  // Check that trying to store a too-large seed leaves the local state
+  // unchanged and does not write to the seed file.
+  CheckRegularSeedAndSeedPrefsAreCleared(prefs_, seed_store);
+  EXPECT_FALSE(timer_.IsRunning());
+  EXPECT_FALSE(base::PathExists(temp_seed_file_path_));
+}
+
 TEST_P(StoreSeedDataAllGroupsTest, CountryCode) {
   TestVariationsSeedStore seed_store(&prefs_, temp_dir_.GetPath());
   ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
diff --git a/tools/metrics/histograms/metadata/variations/enums.xml b/tools/metrics/histograms/metadata/variations/enums.xml
index 10ea51c..f0113b7 100644
--- a/tools/metrics/histograms/metadata/variations/enums.xml
+++ b/tools/metrics/histograms/metadata/variations/enums.xml
@@ -293,6 +293,8 @@
 
 <!-- LINT.ThenChange(//components/variations/metrics.h:LoadSeedResult) -->
 
+<!-- LINT.IfChange(VariationsSeedStoreResult) -->
+
 <enum name="VariationsSeedStoreResult">
   <int value="0" label="Success"/>
   <int value="1" label="Failed - Empty Seed (obsolete)"/>
@@ -310,8 +312,11 @@
   <int value="13" label="Non-Gzip Delta Count"/>
   <int value="14" label="Gzip Full Count"/>
   <int value="15" label="Non-Gzip Full Count"/>
+  <int value="16" label="Failed - Uncompressed Size Limit Exceeded"/>
 </enum>
 
+<!-- LINT.ThenChange(//components/variations/metrics.h:StoreSeedResult) -->
+
 <enum name="VariationsSeedUsage">
   <int value="0" label="Regular seed used"/>
   <int value="1" label="Expired regular seed">No seed used</int>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/variations/variations_seed_store_unittest.cc b/components/variations/variations_seed_store_unittest.cc
index b73ddaa..63d7bde7 100644
--- a/components/variations/variations_seed_store_unittest.cc
+++ b/components/variations/variations_seed_store_unittest.cc
@@ -1614,6 +1614,34 @@
   EXPECT_EQ(serialized_seed, SerializeSeed(stored_seed_));
 }
 
+TEST_P(StoreSeedDataAllGroupsTest, GzipUncompressSizeLimit) {
+  TestVariationsSeedStore seed_store(&prefs_, temp_dir_.GetPath());
+  ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
+            GetParam().field_trial_group);
+
+  std::string compressed;
+  ASSERT_TRUE(compression::GzipCompress(CreateTooLargeData(), &compressed));
+
+  base::HistogramTester histogram_tester;
+
+  // Storing should fail because the uncompressed size exceeds the 50 MiB limit.
+  EXPECT_FALSE(
+      StoreSeedData(seed_store, compressed, {.is_gzip_compressed = true}));
+
+  histogram_tester.ExpectBucketCount("Variations.SeedStoreResult",
+                                     StoreSeedResult::kGzipFullCount, 1);
+  histogram_tester.ExpectBucketCount(
+      "Variations.SeedStoreResult",
+      StoreSeedResult::kUncompressedSizeLimitExceeded, 1);
+  histogram_tester.ExpectTotalCount("Variations.SeedStoreResult", 2);
+
+  // Check that trying to store a too-large seed leaves the local state
+  // unchanged and does not write to the seed file.
+  CheckRegularSeedAndSeedPrefsAreCleared(prefs_, seed_store);
+  EXPECT_FALSE(timer_.IsRunning());
+  EXPECT_FALSE(base::PathExists(temp_seed_file_path_));
+}
+
 TEST_P(StoreSeedDataAllGroupsTest, CountryCode) {
   TestVariationsSeedStore seed_store(&prefs_, temp_dir_.GetPath());
   ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
Loading diff…

Original Bug Report

reported by vm...@google.com

Bypass of Finch security mitigations via MITM-driven crash streak escalation

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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A network-position attacker can disable all Finch-delivered security mitigations by forcing Chrome into ‘Null Seed’ safe mode. This is achieved by inducing repeated browser process crashes using a malicious gzip response during an insecure HTTP fallback, which prevents the variations crash streak from being reset.

Affected files:

  • components/variations/service/safe_seed_manager.cc
  • components/metrics/clean_exit_beacon.cc
  • components/variations/service/variations_field_trial_creator.cc
  • components/variations/variations_seed_store.cc
  • components/variations/service/variations_service.cc

Estimated timestamp from git blame: 2022-11-15

Summary

A potential vulnerability in the Variations Safe Mode mechanism allows a network-position (MITM) attacker to bypass emergency security mitigations delivered via Finch (variations seeds). By suppressing successful variations seed fetches and inducing a series of browser process crashes, an attacker can escalate the ‘crash streak’ to a threshold that forces Chrome into a kNullSeed state. In this state, all field trials and emergency kill-switches are disabled, reverting features to their compile-time default states.

Root Cause Analysis

The Variations Safe Mode is designed to protect users from problematic variations seeds by reverting to a ‘Safe Seed’ or a ‘Null Seed’ if Chrome crashes repeatedly during startup. The logic governing this escalation can be manipulated:

  1. Gzip-Bomb Vulnerability: In components/variations/variations_seed_store.cc, the ProcessSeedData function handles gzip-compressed seeds. It calls compression::GzipUncompress, which extracts the expected uncompressed size from the last 4 bytes of the gzip stream (the ISIZE field) and calls resize() on the output string before decompression begins.
  2. Deterministic OOM: A network attacker can serve a small gzip stream with an ISIZE value set to a large number (e.g., 4GB). In the browser process, std::string::resize() will attempt a massive allocation. If the allocation fails or exceeds internal limits, PartitionAlloc will trigger an immediate Out-Of-Memory (OOM) crash.
  3. Persistence of Crash Streak: The kVariationsCrashStreak counter (in components/metrics/clean_exit_beacon.cc) is incremented whenever a session is determined to have exited uncleanly. It is only reset in SafeSeedManager::RecordSuccessfulFetch, which is called after a seed is successfully fetched, processed, and verified. Since the crash occurs during processing (before signature verification), the streak is never reset.
  4. Safe Mode Escalation: If the crash streak reaches the kCrashStreakNullSeedThreshold (default is 4), SafeSeedManager::GetSeedType() returns SeedType::kNullSeed, causing VariationsFieldTrialCreator to skip the application of all variations trials.

Potential Attack Scenario

  1. Network MITM: The attacker intercepts traffic to the variations server (clientservices.googleapis.com).
  2. Force HTTP Fallback: The attacker blocks HTTPS connections, forcing Chrome to attempt an insecure HTTP fetch.
  3. Deliver Gzip Bomb: The attacker serves a 200 OK response with the IM: gzip header and a gzip-compressed payload with a malicious ISIZE footer.
  4. Induce Crashes: The browser process crashes during GzipUncompress. The attacker repeats this for four consecutive launches.
  5. Mitigation Bypass: On the fifth launch, Chrome enters kNullSeed mode. The attacker then delivers an exploit for a vulnerability that was previously mitigated by a Finch kill-switch.

Suggested Fix

  1. Size Validation: Implement a strict maximum size limit for uncompressed variations seeds in VariationsSeedStore::ProcessSeedData before calling GzipUncompress or within GzipUncompress itself.
  2. Footer Verification: Do not rely solely on the gzip footer’s ISIZE for allocation; instead, use a streaming decompression approach with a fixed-size buffer or a capped growth strategy.
  3. Strict HTTP Handling: Consider disabling HTTP fallback for variations seeds or enforcing stricter validation on insecurely fetched data before processing.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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