CVE-2026-95308
Overview
Background
- `FieldTrialList`
- the
baseclass that manages Chrome field trials (A/B experiments) and shares their state, including params, across processes via a shared-memory allocator. - `internal::FieldTrialEntry`
- the fixed-size header struct stored in the field-trial shared memory, immediately followed by a variable-length
Pickleblob whose length is given by itspickle_sizemember. - `base::CheckedNumeric`
- a numeric wrapper type that tracks overflow across arithmetic and reports it through
IsValid(), refusing to yield a wrapped value viaValueOrDie(). - `base::Pickle`
- Chrome’s serialization buffer format; here it holds the serialized field-trial params read out of the shared-memory entry.
Root Cause Analysis
In FieldTrialList::GetParamsFromSharedMemory, the code computed actual_size = sizeof(internal::FieldTrialEntry) + entry->pickle_size using ordinary uint64_t arithmetic and then bounds-checked it against allocated_size. Because entry->pickle_size is attacker-influenceable data read directly from shared memory, a corrupted or maliciously large value causes this addition to wrap around, producing a small actual_size that spuriously passes the allocated_size < actual_size check. The invariant violated is that actual_size must faithfully represent the header-plus-pickle span so the subsequent read stays within the allocated region; an overflowed sum breaks that guarantee and lets the code proceed to read a Pickle whose declared size exceeds the buffer.
The fix recomputes the sum with base::CheckedNumeric<uint64_t> and rejects the entry when !actual_size.IsValid() (overflow occurred) or when allocated_size < actual_size.ValueOrDie(), so any wrapping value now returns false instead of passing the check.
pickle_size with unchecked native integer arithmetic, letting the sum wrap; the fix routes the addition through base::CheckedNumeric so overflow is detected and the entry is rejected before any out-of-bounds read.Attack Path
- Corrupt the shared-memory entry
An attacker with the ability to tamper with the field-trial shared memory sets an
internal::FieldTrialEntry’spickle_sizeto a value nearuint64_tmax (the unittest usesstd::numeric_limits<uint64_t>::max() - 8). - Trigger the params read
Chrome calls
FieldTrialList::GetParamsFromSharedMemoryfor that trial, which fetches theentryand computesactual_size = sizeof(internal::FieldTrialEntry) + entry->pickle_size. - Overflow bypasses the size check
The addition wraps to a tiny value, so
allocated_size < actual_sizeis false and the function does not bail out as it should. - Out-of-bounds read of the pickle
The code proceeds to interpret
pickle_sizebytes as abase::Picklepast the allocated region, causing an out-of-bounds read as described in the advisory.(inferred)— the diff does not show the exact read sink beyond the bypassed check.
Impact Assessment
pickle_size stored in shared memory, which the advisory frames as a corrupted/maliciously set value; any escalation beyond an out-of-bounds read is (inferred). The advisory states this as an integer overflow in Metrics and does not indicate exploitation in the wild.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifbase/metrics/field_trial.cc |
modified | |
TEST_Fbase/metrics/field_trial_unittest.cc |
modified |
Files Changed
base/metrics/field_trial.ccbase/metrics/field_trial.hbase/metrics/field_trial_unittest.cc
Audit Directions
- Untrusted length arithmetic before bounds checksAudit any code that adds a shared-memory- or IPC-supplied size (like
pickle_size) to a fixed header size before comparing against an allocation, and requirebase::CheckedNumericorbase::CheckAddthere. - Shared-memory struct deserializationReview other consumers of the field-trial allocator and similar
GetAsObject-style reads for trusting embedded length/count fields without validating them against the allocated span. - Metrics/field-trial cross-process surfacesLook for sibling functions in
base/metrics/field_trial.ccthat derive offsets or sizes frominternal::FieldTrialEntryfields, since those inherit the same attacker-controlled-input risk.
Patch
From 7ad22ffdfb5ed234ae2c5433e4e77c61b08236d9 Mon Sep 17 00:00:00 2001
From: Alexei Svitkine <asvitkine@chromium.org>
Date: Wed, 26 Aug 2026 08:07:22 -0700
Subject: [PATCH] Fix potential integer overflow in FieldTrialList::GetParamsFromSharedMemory
Use base::CheckedNumeric to calculate the actual size of the FieldTrialEntry
plus its pickle data. This prevents an integer overflow if the pickle_size
stored in shared memory is corrupted/maliciously set to a huge value,
which could otherwise lead to an out-of-bounds read.
Bug: 513714849
TAG=agy
CONV=b41b68e5-128d-4ced-a8a0-71ddd5e476a9
Change-Id: I565224eaa9b1facc9558f7509c537eee5b35e3ef
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8297538
Commit-Queue: Alexei Svitkine <asvitkine@chromium.org>
Reviewed-by: Luc Nguyen <lucnguyen@google.com>
Auto-Submit: Alexei Svitkine <asvitkine@chromium.org>
Commit-Queue: Luc Nguyen <lucnguyen@google.com>
Cr-Commit-Position: refs/heads/main@{#1686402}
---
diff --git a/base/metrics/field_trial.cc b/base/metrics/field_trial.cc
index c932cfd..823d0c3 100644
--- a/base/metrics/field_trial.cc
+++ b/base/metrics/field_trial.cc
@@ -24,6 +24,7 @@
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/numerics/safe_conversions.h"
+#include "base/numerics/checked_math.h"
#include "base/process/memory.h"
#include "base/process/process_handle.h"
#include "base/process/process_info.h"
@@ -869,8 +870,9 @@
field_trial->ref_, &allocated_size);
CHECK(entry);
- uint64_t actual_size = sizeof(internal::FieldTrialEntry) + entry->pickle_size;
- if (allocated_size < actual_size) {
+ base::CheckedNumeric<uint64_t> actual_size = sizeof(internal::FieldTrialEntry);
+ actual_size += entry->pickle_size;
+ if (!actual_size.IsValid() || allocated_size < actual_size.ValueOrDie()) {
return false;
}
diff --git a/base/metrics/field_trial.h b/base/metrics/field_trial.h
index 48de80cd..18b4371 100644
--- a/base/metrics/field_trial.h
+++ b/base/metrics/field_trial.h
@@ -284,6 +284,8 @@
FRIEND_TEST_ALL_PREFIXES(FieldTrialListTest, ClearParamsFromSharedMemory);
FRIEND_TEST_ALL_PREFIXES(FieldTrialListTest,
TestGetRandomizedFieldTrialCount);
+ FRIEND_TEST_ALL_PREFIXES(FieldTrialListTest,
+ GetParamsFromSharedMemory_Overflow);
FRIEND_TEST_ALL_PREFIXES(FieldTrialTest, SetLowAnonymity);
// MATCHER(CompareActiveGroupToFieldTrialMatcher, "")
@@ -668,6 +670,8 @@
FRIEND_TEST_ALL_PREFIXES(FieldTrialListTest, CheckReadOnlySharedMemoryRegion);
FRIEND_TEST_ALL_PREFIXES(FieldTrialListTest,
GetActiveFieldTrialGroups_RuntimeOverrides);
+ FRIEND_TEST_ALL_PREFIXES(FieldTrialListTest,
+ GetParamsFromSharedMemory_Overflow);
FRIEND_TEST_ALL_PREFIXES(TestFeatureVisitor, FeatureHasParams);
// Required so that |FieldTrialListIncludingLowAnonymity| can expose APIs from
diff --git a/base/metrics/field_trial_unittest.cc b/base/metrics/field_trial_unittest.cc
index 17e248a9..329c03de 100644
--- a/base/metrics/field_trial_unittest.cc
+++ b/base/metrics/field_trial_unittest.cc
@@ -6,6 +6,7 @@
#include <stddef.h>
+#include <limits>
#include <memory>
#include <set>
#include <string_view>
@@ -1365,6 +1366,37 @@
base::RuntimeFieldTrialOverrides::GetInstance()->ResetForTesting();
}
+TEST_F(FieldTrialListTest, GetParamsFromSharedMemory_Overflow) {
+ test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitWithEmptyFeatureAndFieldTrialLists();
+
+ const char* trial_name = "Trial";
+ const char* group_name = "Group";
+ scoped_refptr<FieldTrial> trial =
+ FieldTrialList::CreateFieldTrial(trial_name, group_name);
+ ASSERT_TRUE(trial);
+
+ FieldTrialList::InstantiateFieldTrialAllocatorIfNeeded();
+ FieldTrialList::FieldTrialAllocator* allocator =
+ FieldTrialList::GetInstance()->field_trial_allocator_.get();
+ ASSERT_TRUE(allocator);
+
+ FieldTrial::FieldTrialRef ref = trial->ref_;
+ ASSERT_NE(ref, FieldTrialList::FieldTrialAllocator::kReferenceNull);
+
+ internal::FieldTrialEntry* entry =
+ allocator->GetAsObject<internal::FieldTrialEntry>(ref);
+ ASSERT_TRUE(entry);
+
+ // Set pickle_size to a value that will overflow when added to
+ // sizeof(FieldTrialEntry).
+ entry->pickle_size = std::numeric_limits<uint64_t>::max() - 8;
+
+ FieldTrialParams params;
+ // This should return false and not crash.
+ EXPECT_FALSE(FieldTrialList::GetParamsFromSharedMemory(trial.get(), ¶ms));
+}
+
TEST_F(FieldTrialTest, TestAllParamsToString) {
std::string exptected_output = "t1.g1:p1/v1/p2/v2";
Regression Test / PoC
diff --git a/base/metrics/field_trial_unittest.cc b/base/metrics/field_trial_unittest.cc
index 17e248a9..329c03de 100644
--- a/base/metrics/field_trial_unittest.cc
+++ b/base/metrics/field_trial_unittest.cc
@@ -6,6 +6,7 @@
#include <stddef.h>
+#include <limits>
#include <memory>
#include <set>
#include <string_view>
@@ -1365,6 +1366,37 @@
base::RuntimeFieldTrialOverrides::GetInstance()->ResetForTesting();
}
+TEST_F(FieldTrialListTest, GetParamsFromSharedMemory_Overflow) {
+ test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitWithEmptyFeatureAndFieldTrialLists();
+
+ const char* trial_name = "Trial";
+ const char* group_name = "Group";
+ scoped_refptr<FieldTrial> trial =
+ FieldTrialList::CreateFieldTrial(trial_name, group_name);
+ ASSERT_TRUE(trial);
+
+ FieldTrialList::InstantiateFieldTrialAllocatorIfNeeded();
+ FieldTrialList::FieldTrialAllocator* allocator =
+ FieldTrialList::GetInstance()->field_trial_allocator_.get();
+ ASSERT_TRUE(allocator);
+
+ FieldTrial::FieldTrialRef ref = trial->ref_;
+ ASSERT_NE(ref, FieldTrialList::FieldTrialAllocator::kReferenceNull);
+
+ internal::FieldTrialEntry* entry =
+ allocator->GetAsObject<internal::FieldTrialEntry>(ref);
+ ASSERT_TRUE(entry);
+
+ // Set pickle_size to a value that will overflow when added to
+ // sizeof(FieldTrialEntry).
+ entry->pickle_size = std::numeric_limits<uint64_t>::max() - 8;
+
+ FieldTrialParams params;
+ // This should return false and not crash.
+ EXPECT_FALSE(FieldTrialList::GetParamsFromSharedMemory(trial.get(), ¶ms));
+}
+
TEST_F(FieldTrialTest, TestAllParamsToString) {
std::string exptected_output = "t1.g1:p1/v1/p2/v2";