CVE-2026-12465
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
switchbase/metrics/histogram_base.cc |
modified | |
TEST_Fbase/metrics/histogram_base_unittest.cc |
modified |
Files Changed
base/metrics/histogram_base.ccbase/metrics/histogram_base_unittest.cc
Patch
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
Regression Test / PoC
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
Original Bug Report
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.ccbase/metrics/histogram.ccbase/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
-
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 usingStatisticsRecorder::RegisterOrDeleteDuplicate():existing = DeserializeHistogramInfo(&iter, mapper); if (!existing) { return nullptr; } // ... return StatisticsRecorder::RegisterOrDeleteDuplicate(existing); -
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 staticDummyHistogramsingleton 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(); } -
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
DummyHistogramisHashMetricName("dummy_histogram"). If any histogram with this name (or a colliding hash) is already registered when the malformed record is parsed, the staticDummyHistogramsingleton 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:
- Locate and map the writable shared memory region assigned for UMA metrics (
UnsafeSharedMemoryRegion). - Write a valid
PersistentHistogramDatastructure named"dummy_histogram"to the shared memory segment, causing the browser to heap-allocate and register it in theStatisticsRecorderduring its periodic merge phase. - Write a second, malformed
PersistentHistogramDatastructure sequentially afterward. Set its parameters to trigger a validation failure (for example, settingminimum == maximumandbucket_count == 2), while ensuring the ranges array remains sorted and unique to pass early-stage allocator checks. - When the browser merges this second record,
DeserializeHistogramInfowill fall back to returningDummyHistogram::GetInstance(). - The browser then calls
RegisterOrDeleteDuplicate()with the singleton. It detects a duplicate at the"dummy_histogram"hash slot, and attempts to rundeleteon the staticNoDestructor<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.