Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Metrics
DescriptionInsufficient validation of untrusted input in Metrics
ComponentMetrics
Bug ClassLogic Error
Tracker520189702
Fix commit544895299e2e (chromium/src) +78/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-16

Changed Functions

FunctionChangeNotes
switch
base/metrics/histogram_base.cc
modified
TEST_F
base/metrics/histogram_base_unittest.cc
modified

Files Changed

  • base/metrics/histogram_base.cc
  • base/metrics/histogram_base_unittest.cc
From 544895299e2e23d4832b50703fe37dbbfb6a5b88 Mon Sep 17 00:00:00 2001
From: Alexei Svitkine <asvitkine@chromium.org>
Date: Fri, 05 Jun 2026 14:09:17 -0700
Subject: [PATCH] Validate histogram type after deserialization.

In HistogramBase::DeserializeInfo, check if the type of the deserialized
histogram matches the expected type. This prevents returning a
DummyHistogram when deserialization fails due to invalid arguments.

Bug: 520189702
Change-Id: Ic60769ec0390187c6a82356f99c490a89e768e58
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7902851
Reviewed-by: Luc Nguyen <lucnguyen@google.com>
Commit-Queue: Luc Nguyen <lucnguyen@google.com>
Commit-Queue: Alexei Svitkine <asvitkine@chromium.org>
Auto-Submit: Alexei Svitkine <asvitkine@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1642586}
---

diff --git a/base/metrics/histogram_base.cc b/base/metrics/histogram_base.cc
index a24940a..684cf71d 100644
--- a/base/metrics/histogram_base.cc
+++ b/base/metrics/histogram_base.cc
@@ -72,20 +72,35 @@
     return nullptr;
   }
 
+  HistogramBase* result;
   switch (type) {
     case HISTOGRAM:
-      return Histogram::DeserializeInfoImpl(iter, mapper);
+      result = Histogram::DeserializeInfoImpl(iter, mapper);
+      break;
     case LINEAR_HISTOGRAM:
-      return LinearHistogram::DeserializeInfoImpl(iter, mapper);
+      result = LinearHistogram::DeserializeInfoImpl(iter, mapper);
+      break;
     case BOOLEAN_HISTOGRAM:
-      return BooleanHistogram::DeserializeInfoImpl(iter, mapper);
+      result = BooleanHistogram::DeserializeInfoImpl(iter, mapper);
+      break;
     case CUSTOM_HISTOGRAM:
-      return CustomHistogram::DeserializeInfoImpl(iter, mapper);
+      result = CustomHistogram::DeserializeInfoImpl(iter, mapper);
+      break;
     case SPARSE_HISTOGRAM:
-      return SparseHistogram::DeserializeInfoImpl(iter, mapper);
+      result = SparseHistogram::DeserializeInfoImpl(iter, mapper);
+      break;
     default:
       return nullptr;
   }
+
+  if (result != nullptr &&
+      result->GetHistogramType() != static_cast<HistogramType>(type)) {
+    // If there's a type mismatch, this could be a DummyHistogram returned by
+    // FactoryGetInternal() due to invalid arguments. In this case, return
+    // nullptr to indicate an error.
+    return nullptr;
+  }
+  return result;
 }
 
 HistogramBase::HistogramBase(DurableStringView name)
diff --git a/base/metrics/histogram_base_unittest.cc b/base/metrics/histogram_base_unittest.cc
index dc88844..73b2071 100644
--- a/base/metrics/histogram_base_unittest.cc
+++ b/base/metrics/histogram_base_unittest.cc
@@ -248,4 +248,62 @@
   EXPECT_EQ(add_count, samples->GetCount(0));
 }
 
+TEST_F(HistogramBaseTest, DeserializeTypeMismatch) {
+  // Create a LinearHistogram and register it in the StatisticsRecorder.
+  HistogramBase* histogram = LinearHistogram::FactoryGet(
+      "TestMismatchedHistogram", 1, 1000, 10, HistogramBase::kNoFlags);
+  Pickle real_pickle;
+  histogram->SerializeInfo(&real_pickle);
+
+  // Read the serialized fields using a PickleIterator.
+  int type;
+  std::string name;
+  int flags;
+  int declared_min;
+  int declared_max;
+  uint32_t bucket_count;
+  uint32_t range_checksum;
+
+  PickleIterator iter(real_pickle);
+  ASSERT_TRUE(iter.ReadInt(&type));
+  ASSERT_TRUE(iter.ReadString(&name));
+  ASSERT_TRUE(iter.ReadInt(&flags));
+  ASSERT_TRUE(iter.ReadInt(&declared_min));
+  ASSERT_TRUE(iter.ReadInt(&declared_max));
+  ASSERT_TRUE(iter.ReadUInt32(&bucket_count));
+  ASSERT_TRUE(iter.ReadUInt32(&range_checksum));
+
+  EXPECT_EQ(LINEAR_HISTOGRAM, type);
+
+  // Verify that deserializing the unmodified base pickle succeeds.
+  PickleIterator real_iter(real_pickle);
+  HistogramBase* deserialized_real =
+      HistogramBase::DeserializeInfo(&real_iter, base::NullCallback());
+  EXPECT_EQ(histogram, deserialized_real);
+  // And that it succeeds a second time too.
+  PickleIterator real_iter2(real_pickle);
+  HistogramBase* deserialized_real2 =
+      HistogramBase::DeserializeInfo(&real_iter2, base::NullCallback());
+  EXPECT_EQ(histogram, deserialized_real2);
+
+  // Construct a modified pickle claiming the type is HISTOGRAM instead of
+  // LINEAR_HISTOGRAM.
+  Pickle mismatched_pickle;
+  mismatched_pickle.WriteInt(HISTOGRAM);  // Mismatched type!
+  mismatched_pickle.WriteString(name);
+  mismatched_pickle.WriteInt(flags);
+  mismatched_pickle.WriteInt(declared_min);
+  mismatched_pickle.WriteInt(declared_max);
+  mismatched_pickle.WriteUInt32(bucket_count);
+  mismatched_pickle.WriteUInt32(range_checksum);
+
+  // Deserialize the mismatched pickle. Because "TestMismatchedHistogram"
+  // is already registered as a LINEAR_HISTOGRAM, looking it up as a
+  // HISTOGRAM should detect the type mismatch and return nullptr.
+  PickleIterator mismatched_iter(mismatched_pickle);
+  HistogramBase* deserialized =
+      HistogramBase::DeserializeInfo(&mismatched_iter, base::NullCallback());
+  EXPECT_FALSE(deserialized);
+}
+
 }  // namespace base
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/base/metrics/histogram_base_unittest.cc b/base/metrics/histogram_base_unittest.cc
index dc88844..73b2071 100644
--- a/base/metrics/histogram_base_unittest.cc
+++ b/base/metrics/histogram_base_unittest.cc
@@ -248,4 +248,62 @@
   EXPECT_EQ(add_count, samples->GetCount(0));
 }
 
+TEST_F(HistogramBaseTest, DeserializeTypeMismatch) {
+  // Create a LinearHistogram and register it in the StatisticsRecorder.
+  HistogramBase* histogram = LinearHistogram::FactoryGet(
+      "TestMismatchedHistogram", 1, 1000, 10, HistogramBase::kNoFlags);
+  Pickle real_pickle;
+  histogram->SerializeInfo(&real_pickle);
+
+  // Read the serialized fields using a PickleIterator.
+  int type;
+  std::string name;
+  int flags;
+  int declared_min;
+  int declared_max;
+  uint32_t bucket_count;
+  uint32_t range_checksum;
+
+  PickleIterator iter(real_pickle);
+  ASSERT_TRUE(iter.ReadInt(&type));
+  ASSERT_TRUE(iter.ReadString(&name));
+  ASSERT_TRUE(iter.ReadInt(&flags));
+  ASSERT_TRUE(iter.ReadInt(&declared_min));
+  ASSERT_TRUE(iter.ReadInt(&declared_max));
+  ASSERT_TRUE(iter.ReadUInt32(&bucket_count));
+  ASSERT_TRUE(iter.ReadUInt32(&range_checksum));
+
+  EXPECT_EQ(LINEAR_HISTOGRAM, type);
+
+  // Verify that deserializing the unmodified base pickle succeeds.
+  PickleIterator real_iter(real_pickle);
+  HistogramBase* deserialized_real =
+      HistogramBase::DeserializeInfo(&real_iter, base::NullCallback());
+  EXPECT_EQ(histogram, deserialized_real);
+  // And that it succeeds a second time too.
+  PickleIterator real_iter2(real_pickle);
+  HistogramBase* deserialized_real2 =
+      HistogramBase::DeserializeInfo(&real_iter2, base::NullCallback());
+  EXPECT_EQ(histogram, deserialized_real2);
+
+  // Construct a modified pickle claiming the type is HISTOGRAM instead of
+  // LINEAR_HISTOGRAM.
+  Pickle mismatched_pickle;
+  mismatched_pickle.WriteInt(HISTOGRAM);  // Mismatched type!
+  mismatched_pickle.WriteString(name);
+  mismatched_pickle.WriteInt(flags);
+  mismatched_pickle.WriteInt(declared_min);
+  mismatched_pickle.WriteInt(declared_max);
+  mismatched_pickle.WriteUInt32(bucket_count);
+  mismatched_pickle.WriteUInt32(range_checksum);
+
+  // Deserialize the mismatched pickle. Because "TestMismatchedHistogram"
+  // is already registered as a LINEAR_HISTOGRAM, looking it up as a
+  // HISTOGRAM should detect the type mismatch and return nullptr.
+  PickleIterator mismatched_iter(mismatched_pickle);
+  HistogramBase* deserialized =
+      HistogramBase::DeserializeInfo(&mismatched_iter, base::NullCallback());
+  EXPECT_FALSE(deserialized);
+}
+
 }  // namespace base
Loading diff…

Original Bug Report

reported by vm...@google.com

Invalid free of static DummyHistogram singleton via metrics shared memory merge

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 compromised renderer process can potentially trigger an invalid free of the static DummyHistogram singleton in the browser process. By writing malformed metrics data to the shared memory region, the renderer can force the browser to deserialize an invalid histogram that falls back to the static DummyHistogram singleton. If a histogram with the name ‘dummy_histogram’ was already registered, the browser attempts to delete the duplicate, invoking ‘delete’ on non-heap static storage.

Affected files:

  • base/metrics/persistent_histogram_allocator.cc
  • base/metrics/histogram.cc
  • base/metrics/statistics_recorder.cc

Estimated timestamp from git blame: 2022-02-16

Description

Static analysis of the Chromium metrics subsystem indicates a potential vulnerability where a compromised renderer process can trigger an invalid delete of the static DummyHistogram singleton in the browser process, leading to allocator corruption or a browser crash.

Root Cause Analysis

  1. Unconditional Registration/Deletion in Persistent Allocator: In base/metrics/persistent_histogram_allocator.cc:751-758, GetOrCreateStatisticsRecorderHistogram() deserializes histogram information from shared memory and unconditionally registers the result using StatisticsRecorder::RegisterOrDeleteDuplicate():

    existing = DeserializeHistogramInfo(&iter, mapper);
    if (!existing) {
      return nullptr;
    }
    // ...
    return StatisticsRecorder::RegisterOrDeleteDuplicate(existing);
    
  2. Fallback to Static Singleton: If the construction arguments serialized in shared memory fail validation checks in the browser process, Histogram::DeserializeInfoImpl() -> Histogram::FactoryGetInternal() returns the static DummyHistogram singleton instead of allocating a new histogram on the heap:

    // base/metrics/histogram.cc:752-773
    HistogramBase* Histogram::FactoryGetInternal(...) {
      const auto validity = InspectConstructionArguments(name, name_hash, &minimum, &maximum, &bucket_count);
      if (validity != kOK) {
        // ...
        base::debug::DumpWithoutCrashing();
        return DummyHistogram::GetInstance(); // <-- Returns static singleton
      }
      return Factory(name, name_hash, minimum, maximum, bucket_count, flags).Build();
    }
    
  3. Deallocation on Collision: StatisticsRecorder::RegisterOrDeleteDuplicate(histogram) takes ownership of the passed pointer and deletes it if a different object with the same name hash is already registered:

    // base/metrics/statistics_recorder.cc:180-187
    if (histogram == registered) {
      return histogram;
    }
    // We already have a histogram with this name.
    histogram_deleter.reset(histogram); // <-- Invokes delete on the static singleton
    return registered;
    

    The name hash of DummyHistogram is HashMetricName("dummy_histogram"). If any histogram with this name (or a colliding hash) is already registered when the malformed record is parsed, the static DummyHistogram singleton is deleted.

Potential Trigger Steps

As we currently do not have the capability to execute code or run runtime environments to construct a working PoC, the following are the potential steps an attacker might take to trigger this flow from a compromised renderer:

  1. Locate and map the writable shared memory region assigned for UMA metrics (UnsafeSharedMemoryRegion).
  2. Write a valid PersistentHistogramData structure named "dummy_histogram" to the shared memory segment, causing the browser to heap-allocate and register it in the StatisticsRecorder during its periodic merge phase.
  3. Write a second, malformed PersistentHistogramData structure sequentially afterward. Set its parameters to trigger a validation failure (for example, setting minimum == maximum and bucket_count == 2), while ensuring the ranges array remains sorted and unique to pass early-stage allocator checks.
  4. When the browser merges this second record, DeserializeHistogramInfo will fall back to returning DummyHistogram::GetInstance().
  5. The browser then calls RegisterOrDeleteDuplicate() with the singleton. It detects a duplicate at the "dummy_histogram" hash slot, and attempts to run delete on the static NoDestructor<DummyHistogram> storage.

Proposed Fix

Prevent the registration of DUMMY_HISTOGRAM types in GetOrCreateStatisticsRecorderHistogram() before they are passed to the StatisticsRecorder. If the deserialized histogram is a dummy histogram, discard it safely:

// base/metrics/persistent_histogram_allocator.cc
existing = DeserializeHistogramInfo(&iter, mapper);
if (!existing) {
  return nullptr;
}
if (existing->GetHistogramType() == HistogramBase::DUMMY_HISTOGRAM) {
  return nullptr;
}

Evaluated with Chrome root at commit: d8b226a3be7c9c1ac9240c09e14698866c82e4ac


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