Low watch ✔︎ verified CVSS 3.4 chrome Integer Overflow 🔧 Commit mapped 🧬 A size or count is computed in a type too narrow to hold it

Overview

Low
Severity
3.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in Metrics
DescriptionInteger overflow in Metrics
ComponentMetrics
Bug ClassInteger Overflow
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: Overflow and CheckedNumeric fix confirmed. Diff does not show who can write pickle_size or which process parses it; 'attacker-influenceable' and browser surface are unsupported. Likely needs prior compromise; OOB read may only crash.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker513714849
Fix commit7ad22ffdfb5e (chromium/src) +40/-2
CWECWE-190 (Integer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-16 (per vendor advisory)
Fix landed2026-08-26 (public git)
Disclosed2026-09-22
Patch gap27 days — fix public in source before the advisory

Background

`FieldTrialList`
the base class 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 Pickle blob whose length is given by its pickle_size member.
`base::CheckedNumeric`
a numeric wrapper type that tracks overflow across arithmetic and reports it through IsValid(), refusing to yield a wrapped value via ValueOrDie().
`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.

Key insight
The single mistake was performing a bounds-check addition on an untrusted 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

  1. Corrupt the shared-memory entry An attacker with the ability to tamper with the field-trial shared memory sets an internal::FieldTrialEntry’s pickle_size to a value near uint64_t max (the unittest uses std::numeric_limits<uint64_t>::max() - 8).
  2. Trigger the params read Chrome calls FieldTrialList::GetParamsFromSharedMemory for that trial, which fetches the entry and computes actual_size = sizeof(internal::FieldTrialEntry) + entry->pickle_size.
  3. Overflow bypasses the size check The addition wraps to a tiny value, so allocated_size < actual_size is false and the function does not bail out as it should.
  4. Out-of-bounds read of the pickle The code proceeds to interpret pickle_size bytes as a base::Pickle past 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

An attacker who can corrupt the field-trial shared-memory contents can bypass the size validation and induce an out-of-bounds read when field-trial params are parsed, potentially disclosing adjacent memory or crashing the process. The precondition is control over the 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

FunctionChangeNotes
if
base/metrics/field_trial.cc
modified
TEST_F
base/metrics/field_trial_unittest.cc
modified

Files Changed

  • base/metrics/field_trial.cc
  • base/metrics/field_trial.h
  • base/metrics/field_trial_unittest.cc

Audit Directions

  • Untrusted length arithmetic before bounds checks
    Audit 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 require base::CheckedNumeric or base::CheckAdd there.
  • Shared-memory struct deserialization
    Review 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 surfaces
    Look for sibling functions in base/metrics/field_trial.cc that derive offsets or sizes from internal::FieldTrialEntry fields, since those inherit the same attacker-controlled-input risk.
From 7ad22ffdfb5ed234ae2c5433e4e77c61b08236d9 Mon Sep 17 00:00:00 2001
From: Alexei Svitkine <[email protected]>
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 <[email protected]>
Reviewed-by: Luc Nguyen <[email protected]>
Auto-Submit: Alexei Svitkine <[email protected]>
Commit-Queue: Luc Nguyen <[email protected]>
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(), &params));
+}
+
 TEST_F(FieldTrialTest, TestAllParamsToString) {
   std::string exptected_output = "t1.g1:p1/v1/p2/v2";
 
Loading diff…

Regression Test / PoC

shipped with the fix
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(), &params));
+}
+
 TEST_F(FieldTrialTest, TestAllParamsToString) {
   std::string exptected_output = "t1.g1:p1/v1/p2/v2";
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.