CVE-2026-17774
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Pcomponents/variations/variations_seed_store_unittest.cc |
modified |
Files Changed
components/variations/metrics.hcomponents/variations/variations_seed_store.cccomponents/variations/variations_seed_store_unittest.cctools/metrics/histograms/metadata/variations/enums.xml
Patch
From fe5796ad6fa8684aa2de4fb886faaa8b4f689b16 Mon Sep 17 00:00:00 2001 From: Łukasz Burchard <[email protected]> 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 <[email protected]> Commit-Queue: Łukasz Burchard <[email protected]> Reviewed-by: Ramon Cano Aparicio <[email protected]> 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>
Regression Test / PoC
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),
Original Bug Report
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.cccomponents/metrics/clean_exit_beacon.cccomponents/variations/service/variations_field_trial_creator.cccomponents/variations/variations_seed_store.cccomponents/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:
- Gzip-Bomb Vulnerability: In
components/variations/variations_seed_store.cc, theProcessSeedDatafunction handles gzip-compressed seeds. It callscompression::GzipUncompress, which extracts the expected uncompressed size from the last 4 bytes of the gzip stream (theISIZEfield) and callsresize()on the output string before decompression begins. - Deterministic OOM: A network attacker can serve a small gzip stream with an
ISIZEvalue 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. - Persistence of Crash Streak: The
kVariationsCrashStreakcounter (incomponents/metrics/clean_exit_beacon.cc) is incremented whenever a session is determined to have exited uncleanly. It is only reset inSafeSeedManager::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. - Safe Mode Escalation: If the crash streak reaches the
kCrashStreakNullSeedThreshold(default is 4),SafeSeedManager::GetSeedType()returnsSeedType::kNullSeed, causingVariationsFieldTrialCreatorto skip the application of all variations trials.
Potential Attack Scenario
- Network MITM: The attacker intercepts traffic to the variations server (
clientservices.googleapis.com). - Force HTTP Fallback: The attacker blocks HTTPS connections, forcing Chrome to attempt an insecure HTTP fetch.
- Deliver Gzip Bomb: The attacker serves a 200 OK response with the
IM: gzipheader and a gzip-compressed payload with a maliciousISIZEfooter. - Induce Crashes: The browser process crashes during
GzipUncompress. The attacker repeats this for four consecutive launches. - Mitigation Bypass: On the fifth launch, Chrome enters
kNullSeedmode. The attacker then delivers an exploit for a vulnerability that was previously mitigated by a Finch kill-switch.
Suggested Fix
- Size Validation: Implement a strict maximum size limit for uncompressed variations seeds in
VariationsSeedStore::ProcessSeedDatabefore callingGzipUncompressor withinGzipUncompressitself. - Footer Verification: Do not rely solely on the gzip footer’s
ISIZEfor allocation; instead, use a streaming decompression approach with a fixed-size buffer or a capped growth strategy. - 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.