CVE-2026-17694
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ElementDatathird_party/blink/renderer/core/dom/element_data.h |
modified | |
CORE_EXPORTthird_party/blink/renderer/core/dom/element_data.h |
modified | |
ShareableElementDatathird_party/blink/renderer/core/dom/element_data.h |
modified | |
ShareableElementDatathird_party/blink/renderer/core/dom/element_data_cache.h |
modified | |
ElementDataCachethird_party/blink/renderer/core/dom/element_data_cache.h |
modified | |
CORE_EXPORTthird_party/blink/renderer/core/dom/element_data_cache.h |
modified | |
ElementDataCacheTestthird_party/blink/renderer/core/dom/element_data_cache_test.cc |
modified | |
TEST_Fthird_party/blink/renderer/core/dom/element_data_cache_test.cc |
modified |
Files Changed
third_party/blink/renderer/core/css/css_property_value_set.ccthird_party/blink/renderer/core/css/resolver/matched_properties_cache.ccthird_party/blink/renderer/core/dom/build.gnithird_party/blink/renderer/core/dom/element_data.hthird_party/blink/renderer/core/dom/element_data_cache.ccthird_party/blink/renderer/core/dom/element_data_cache.hthird_party/blink/renderer/core/dom/element_data_cache_test.cc
Patch
From 693de6d8a7bd2b232ac4b905b0072978ac07fae1 Mon Sep 17 00:00:00 2001
From: Joey Arhar <jarhar@chromium.org>
Date: Wed, 24 Jun 2026 10:12:22 -0700
Subject: [PATCH] Avoid sentinel hash keys in ElementDataCache
This patch avoids sentinel has key values (0 and 0xFFFFFFFF) by flipping
the top bit. This pattern was already being done in multiple places, so
it has been moved into a common helper method.
Fixed: 517511796
Change-Id: Idebb9cc6e187df4e7f49f4299cfab911ba5cf68b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7883969
Commit-Queue: Joey Arhar <jarhar@chromium.org>
Reviewed-by: Joey Arhar <jarhar@chromium.org>
Reviewed-by: Steinar H Gunderson <sesse@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1651801}
---
diff --git a/third_party/blink/renderer/core/css/css_property_value_set.cc b/third_party/blink/renderer/core/css/css_property_value_set.cc
index 2977ed19..ea77b1e 100644
--- a/third_party/blink/renderer/core/css/css_property_value_set.cc
+++ b/third_party/blink/renderer/core/css/css_property_value_set.cc
@@ -83,14 +83,7 @@
AddIntToHash(hash, property.Value().Hash());
}
- static_assert((HashTraits<unsigned>::EmptyValue() ^ 0x80000000) !=
- HashTraits<unsigned>::DeletedValue(),
- "We assume below that flipping the top bit will not turn "
- "EmptyValue into DeletedValue or vice versa");
- if (hash == HashTraits<unsigned>::EmptyValue() ||
- hash == HashTraits<unsigned>::DeletedValue()) {
- hash ^= 0x80000000;
- }
+ hash = EnsureValidHash(hash);
return hash;
}
diff --git a/third_party/blink/renderer/core/css/resolver/matched_properties_cache.cc b/third_party/blink/renderer/core/css/resolver/matched_properties_cache.cc
index 6093f098..0deb0e0 100644
--- a/third_party/blink/renderer/core/css/resolver/matched_properties_cache.cc
+++ b/third_party/blink/renderer/core/css/resolver/matched_properties_cache.cc
@@ -58,15 +58,7 @@
}))
<< "This should have been checked in AddMatchedProperties()";
unsigned hash = StringHasher::HashMemory(base::as_byte_span(hashes));
- hash = HashInts(hash, additional_hash);
-
- // See CSSPropertyValueSet::ComputeHash() for asserts that this is safe.
- if (hash == HashTraits<unsigned>::EmptyValue() ||
- hash == HashTraits<unsigned>::DeletedValue()) {
- hash ^= 0x80000000;
- }
-
- return hash;
+ return EnsureValidHash(HashInts(hash, additional_hash));
}
CachedMatchedProperties::CachedMatchedProperties(
diff --git a/third_party/blink/renderer/core/dom/build.gni b/third_party/blink/renderer/core/dom/build.gni
index 0529e9e5..d272cbdc 100644
--- a/third_party/blink/renderer/core/dom/build.gni
+++ b/third_party/blink/renderer/core/dom/build.gni
@@ -317,6 +317,7 @@
"document_statistics_collector_test.cc",
"document_test.cc",
"dom_node_ids_test.cc",
+ "element_data_cache_test.cc",
"element_test.cc",
"events/event_path_test.cc",
"events/event_target_test.cc",
diff --git a/third_party/blink/renderer/core/dom/element_data.h b/third_party/blink/renderer/core/dom/element_data.h
index 8d3e9bb9..58514ac6 100644
--- a/third_party/blink/renderer/core/dom/element_data.h
+++ b/third_party/blink/renderer/core/dom/element_data.h
@@ -53,7 +53,7 @@
// ElementData represents very common, but not necessarily unique to an element,
// data such as attributes, inline style, and parsed class names and ids.
-class ElementData : public GarbageCollected<ElementData> {
+class CORE_EXPORT ElementData : public GarbageCollected<ElementData> {
public:
// Override GarbageCollected's finalizeGarbageCollectedObject to
// dispatch to the correct subclass destructor.
@@ -173,7 +173,7 @@
// the parser during page load for elements that have identical attributes. This
// is a memory optimization since it's very common for many elements to have
// duplicate sets of attributes (ex. the same classes).
-class ShareableElementData final : public ElementData {
+class CORE_EXPORT ShareableElementData final : public ElementData {
public:
static ShareableElementData* CreateWithAttributes(
const Vector<Attribute, kAttributePrealloc>&);
diff --git a/third_party/blink/renderer/core/dom/element_data_cache.cc b/third_party/blink/renderer/core/dom/element_data_cache.cc
index 51579fd..134648de 100644
--- a/third_party/blink/renderer/core/dom/element_data_cache.cc
+++ b/third_party/blink/renderer/core/dom/element_data_cache.cc
@@ -46,9 +46,17 @@
ElementDataCache::CachedShareableElementDataWithAttributes(
const StringImpl* tag_name,
const Vector<Attribute, kAttributePrealloc>& attributes) {
+ unsigned hash = HashInts(tag_name->GetHash(), AttributeHash(attributes));
+ return CachedElementData(tag_name, attributes, hash);
+}
+
+ShareableElementData* ElementDataCache::CachedElementData(
+ const StringImpl* tag_name,
+ const Vector<Attribute, kAttributePrealloc>& attributes,
+ unsigned hash) {
DCHECK(!attributes.empty());
- unsigned hash = HashInts(tag_name->GetHash(), AttributeHash(attributes));
+ hash = EnsureValidHash(hash);
ShareableElementDataCache::ValueType* it =
shareable_element_data_cache_.insert(hash, std::pair(nullptr, nullptr))
.stored_value;
diff --git a/third_party/blink/renderer/core/dom/element_data_cache.h b/third_party/blink/renderer/core/dom/element_data_cache.h
index 643807f..7481f13 100644
--- a/third_party/blink/renderer/core/dom/element_data_cache.h
+++ b/third_party/blink/renderer/core/dom/element_data_cache.h
@@ -36,7 +36,8 @@
class ShareableElementData;
-class ElementDataCache final : public GarbageCollected<ElementDataCache> {
+class CORE_EXPORT ElementDataCache final
+ : public GarbageCollected<ElementDataCache> {
public:
ElementDataCache();
@@ -47,9 +48,23 @@
const StringImpl* tag_name,
const Vector<Attribute, kAttributePrealloc>&);
+ // This is the same as CachedShareableElementDataWithoutAttributes, but uses
+ // the provided hash instead of generating one from the arguments.
+ ShareableElementData* CachedElementDataWithHashForTesting(
+ const StringImpl* tag_name,
+ const Vector<Attribute, kAttributePrealloc>& attributes,
+ unsigned hash) {
+ return CachedElementData(tag_name, attributes, hash);
+ }
+
void Trace(Visitor*) const;
private:
+ ShareableElementData* CachedElementData(
+ const StringImpl* tag_name,
+ const Vector<Attribute, kAttributePrealloc>&,
+ unsigned hash);
+
using ShareableElementDataCache =
HeapHashMap<unsigned,
std::pair<const StringImpl*, Member<ShareableElementData>>,
diff --git a/third_party/blink/renderer/core/dom/element_data_cache_test.cc b/third_party/blink/renderer/core/dom/element_data_cache_test.cc
new file mode 100644
index 0000000..15f4b67
--- /dev/null
+++ b/third_party/blink/renderer/core/dom/element_data_cache_test.cc
@@ -0,0 +1,48 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/dom/element_data_cache.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/dom/attribute.h"
+#include "third_party/blink/renderer/core/dom/element_data.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
+#include "third_party/blink/renderer/platform/heap/heap_test_utilities.h"
+#include "third_party/blink/renderer/platform/heap/persistent.h"
+
+namespace blink {
+
+namespace {
+
+class ElementDataCacheTest : public TestSupportingGC {};
+
+TEST_F(ElementDataCacheTest, SentinelKeyWithFixDoesNotCauseUAF) {
+ Persistent<ElementDataCache> cache = MakeGarbageCollected<ElementDataCache>();
+
+ String tag_name = "div";
+ Vector<Attribute, kAttributePrealloc> attributes;
+ attributes.push_back(Attribute(html_names::kIdAttr, AtomicString("foo")));
+
+ // Call the method with a sentinel hash. We expect it to be XOR'd.
+ unsigned sentinel_hash = 0xFFFFFFFFu;
+
+ WeakPersistent<ShareableElementData> witness;
+
+ {
+ ShareableElementData* data = cache->CachedElementDataWithHashForTesting(
+ tag_name.Impl(), attributes, sentinel_hash);
+ ASSERT_NE(nullptr, data);
+ witness = data;
+ }
+
+ // The object should be traced and survive this GC.
Regression Test / PoC
diff --git a/third_party/blink/renderer/core/dom/element_data_cache_test.cc b/third_party/blink/renderer/core/dom/element_data_cache_test.cc
new file mode 100644
index 0000000..15f4b67
--- /dev/null
+++ b/third_party/blink/renderer/core/dom/element_data_cache_test.cc
@@ -0,0 +1,48 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/dom/element_data_cache.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/dom/attribute.h"
+#include "third_party/blink/renderer/core/dom/element_data.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
+#include "third_party/blink/renderer/platform/heap/heap_test_utilities.h"
+#include "third_party/blink/renderer/platform/heap/persistent.h"
+
+namespace blink {
+
+namespace {
+
+class ElementDataCacheTest : public TestSupportingGC {};
+
+TEST_F(ElementDataCacheTest, SentinelKeyWithFixDoesNotCauseUAF) {
+ Persistent<ElementDataCache> cache = MakeGarbageCollected<ElementDataCache>();
+
+ String tag_name = "div";
+ Vector<Attribute, kAttributePrealloc> attributes;
+ attributes.push_back(Attribute(html_names::kIdAttr, AtomicString("foo")));
+
+ // Call the method with a sentinel hash. We expect it to be XOR'd.
+ unsigned sentinel_hash = 0xFFFFFFFFu;
+
+ WeakPersistent<ShareableElementData> witness;
+
+ {
+ ShareableElementData* data = cache->CachedElementDataWithHashForTesting(
+ tag_name.Impl(), attributes, sentinel_hash);
+ ASSERT_NE(nullptr, data);
+ witness = data;
+ }
+
+ // The object should be traced and survive this GC.
+ PreciselyCollectGarbage();
+
+ ASSERT_NE(nullptr, witness.Get())
+ << "ShareableElementData was swept; trace skipped the bucket despite fix";
+}
+
+} // namespace
+} // namespace blink
Original Bug Report
Potential Oilpan Tracing Bypass and UAF in ElementDataCache due to Unfiltered Hash Keys
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: ElementDataCache fails to sanitize hash keys before inserting them into shareable_element_data_cache_, allowing reserved sentinel values (0 and 0xFFFFFFFF) to be stored in release builds. During garbage collection, Oilpan tracing skips the bucket containing the sentinel key, resulting in the cached ShareableElementData being prematurely swept. Subsequent lookups retrieve and dereference this swept object, potentially leading to a high-severity Use-After-Free (UAF) in the renderer process.
Affected files:
third_party/blink/renderer/core/dom/element_data_cache.ccthird_party/blink/renderer/core/dom/element_data_cache.h
Estimated timestamp from git blame: 2025-01-06
Root Cause
In third_party/blink/renderer/core/dom/element_data_cache.cc, the function ElementDataCache::CachedShareableElementDataWithAttributes retrieves a key using HashInts and inserts it directly into the shareable_element_data_cache_ map without filtering out reserved sentinel keys:
unsigned hash = HashInts(tag_name->GetHash(), AttributeHash(attributes));
ShareableElementDataCache::ValueType* it =
shareable_element_data_cache_.insert(hash, std::pair(nullptr, nullptr))
.stored_value;
The backing cache shareable_element_data_cache_ is a HeapHashMap defined in element_data_cache.h as:
using ShareableElementDataCache =
HeapHashMap<unsigned,
std::pair<const StringImpl*, Member<ShareableElementData>>,
AlreadyHashedTraits>;
Because it uses AlreadyHashedTraits (which inherits from GenericHashTraits<unsigned> -> IntHashTraits<unsigned>), the keys are not re-hashed. Under IntHashTraits<unsigned>, the hash table reserves two sentinel values:
EmptyValue() == 0DeletedValue() == 0xFFFFFFFF(i.e.static_cast<unsigned>(-1))
Normally, when custom or raw integer hashes are used directly as table keys, sentinel collisions must be avoided. For example, the CSS MatchedPropertiesCache handles this safely:
// third_party/blink/renderer/core/css/resolver/matched_properties_cache.cc
if (hash == HashTraits<unsigned>::EmptyValue() ||
hash == HashTraits<unsigned>::DeletedValue()) {
hash ^= 0x80000000;
}
However, this mitigation is missing in ElementDataCache::CachedShareableElementDataWithAttributes, allowing raw sentinel hash values to be inserted directly into the hash table.
Oilpan Tracing Bypass and Use-After-Free (UAF)
In official release builds (where the assertion DCHECK(!IsEmptyOrDeletedBucket(*entry)) is compiled out), the table successfully stores the entry at the sentinel key 0xFFFFFFFF.
During Garbage Collection, Oilpan traces the backing storage of HeapHashMap but explicitly skips buckets whose key matches the empty or deleted sentinels to avoid scanning inactive entries:
// third_party/blink/renderer/platform/heap/collection_support/heap_hash_table_backing.h
if (!IsHashTraitsEmptyOrDeletedValue<typename Table::KeyTraitsType>(
*concurrent_bucket.key())) { // <--- Returns true for 0xFFFFFFFF, skipping the bucket
...Trace(visitor, concurrent_bucket.bucket());
}
Because tracing is skipped, the strong reference Member<ShareableElementData> inside that bucket is never marked by the garbage collector. When GC completes, the underlying ShareableElementData object is swept and reclaimed, leaving a dangling pointer in the active cache slot.
On subsequent lookup for the same key, because AlreadyHashedTraits::kSafeToCompareToEmptyOrDeleted is true, the Equal() comparison is evaluated before checking IsDeletedBucket(). This matches 0xFFFFFFFF == 0xFFFFFFFF and retrieves the stale slot.
ElementDataCache::CachedShareableElementDataWithAttributes then evaluates the returned value:
if (it->value.second == nullptr || it->value.first != tag_name ||
!HasSameAttributes(attributes, *it->value.second)) { // <--- Dereferences swept object (UAF)
If the comparison evaluates to false, the dangling pointer is returned to the element creation logic, resulting in a long-lived dangling pointer that can be leveraged to execute code within the renderer process.
Potential Steps to Reproduce
(Note: These are potential steps as our analysis has been conducted statically without a functional runtime environment or PoC)
- Navigate to a webpage hosting malicious JavaScript.
- The JavaScript dynamically creates various combinations of tag names and attribute sets on helper elements to locate a set whose pointers and hash key map exactly to
0xFFFFFFFFunder the deterministicHashIntsimplementation. - The page streams or parses an HTML element with this exact combination, forcing the insertion of key
0xFFFFFFFFin theElementDataCache. - The page drops all external references to the element and its
ShareableElementData. - Trigger an Oilpan Garbage Collection cycle. Since the bucket at key
0xFFFFFFFFis treated as a deleted sentinel, the tracing phase skips it and sweeps theShareableElementDataobject. - Trigger the creation of another element with the same attributes. The cache returns the dangling pointer to the swept memory, causing a Use-After-Free during attribute verification or subsequent styling.
Recommended Fix
Sanitize the computed hash key in CachedShareableElementDataWithAttributes to avoid reserved sentinel values before attempting to lookup or insert into the hash map:
unsigned hash = HashInts(tag_name->GetHash(), AttributeHash(attributes));
if (hash == HashTraits<unsigned>::EmptyValue() ||
hash == HashTraits<unsigned>::DeletedValue()) {
hash ^= 0x80000000;
}
This ensures sentinel values are never used as active keys in HeapHashMap.
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
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.