CVE-2026-11171
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/platform/geometry/length.cc |
modified |
Files Changed
third_party/blink/renderer/platform/geometry/length.ccthird_party/blink/renderer/platform/geometry/length.h
Patch
From f153d9ff63f9e002d2476950a83f66cb353212f8 Mon Sep 17 00:00:00 2001 From: Jeremy Roman <[email protected]> Date: Mon, 13 Apr 2026 21:01:49 -0700 Subject: [PATCH] Tweak CalculationHandle map to avoid reserved values This change updates the type of `Length::calculation_handle_` from `int` to `unsigned` (which always has defined overflow), and tweaks how we skip values. The empty (0) and deleted (UINT_MAX) values are now explicitly skipped, avoiding the potential of them being used and breaking the hash table. While here, adjust the loop to do a single map access, rather than three (Contains on the previous index, Contains on the new index, Set on the new index) in the typical case. A more sophisticated scheme for allocating these handles (like a proper memory allocator with free lists etc) would be possible but this is simpler and sufficient for now. Finally, for the avoidance of doubt, CHECKs are added that validate that the per-member counts don't overflow, and MemberWithCount construction is deferred until we actually succeed at inserting it since this may require a write barrier in certain build configurations. Redundant DCHECKs are removed (namely, HashMap::at always crashes if the key is absent). Bug: 502322843 Change-Id: I5e785f74a93c07274dd1c219e7f415f589b36668 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7760155 Commit-Queue: Jeremy Roman <[email protected]> Reviewed-by: Ian Kilpatrick <[email protected]> Cr-Commit-Position: refs/heads/main@{#1614208} --- diff --git a/third_party/blink/renderer/platform/geometry/length.cc b/third_party/blink/renderer/platform/geometry/length.cc index 4598b82..0d50141 100644 --- a/third_party/blink/renderer/platform/geometry/length.cc +++ b/third_party/blink/renderer/platform/geometry/length.cc @@ -31,6 +31,7 @@ #include "third_party/blink/renderer/platform/geometry/calculation_value.h" #include "third_party/blink/renderer/platform/heap/collection_support/heap_hash_map.h" #include "third_party/blink/renderer/platform/wtf/allocator/allocator.h" +#include "third_party/blink/renderer/platform/wtf/hash_traits.h" #include "third_party/blink/renderer/platform/wtf/size_assertions.h" #include "third_party/blink/renderer/platform/wtf/static_constructors.h" #include "third_party/blink/renderer/platform/wtf/text/string_builder.h" @@ -69,58 +70,55 @@ struct MemberWithCount { DISALLOW_NEW(); - public: + MemberWithCount() = default; + MemberWithCount(const CalculationValue* value) : value(value) {} void Trace(Visitor* visitor) const { visitor->Trace(value); } + Member<const CalculationValue> value; unsigned count = 1u; }; void Trace(Visitor* visitor) const { visitor->Trace(map_); } - int insert(const CalculationValue* calc_value) { - DCHECK(index_); + unsigned insert(const CalculationValue* calc_value) { // FIXME calc(): https://bugs.webkit.org/show_bug.cgi?id=80489 // This monotonically increasing handle generation scheme is potentially // wasteful of the handle space. Consider reusing empty handles. - while (map_.Contains(index_)) + do { index_++; - - map_.Set(index_, MemberWithCount(calc_value, 1u)); - + } while (IsHashTraitsEmptyOrDeletedValue<HashTraits<unsigned>>(index_) || + !map_.insert(index_, calc_value).is_new_entry); return index_; } - const CalculationValue& Get(int index) const { - DCHECK(map_.Contains(index)); + const CalculationValue& Get(unsigned index) const { return *map_.at(index).value; } - unsigned GetCount(int index) const { - DCHECK(map_.Contains(index)); - return map_.at(index).count; - } + unsigned GetCount(unsigned index) const { return map_.at(index).count; } wtf_size_t GetMapSize() const { return map_.size(); } - void DecrementCount(int index) { - DCHECK(map_.Contains(index)); + void DecrementCount(unsigned index) { auto iter = map_.find(index); - --iter->value.count; - if (iter->value.count == 0u) { - map_.erase(index); + CHECK(iter != map_.end()); + unsigned count = --iter->value.count; + if (count == 0u) { + map_.erase(iter); } } - void IncrementCount(int index) { - DCHECK(map_.Contains(index)); + void IncrementCount(unsigned index) { auto iter = map_.find(index); - ++iter->value.count; + CHECK(iter != map_.end()); + unsigned count = ++iter->value.count; + CHECK_GT(count, 0u); } private: - int index_ = 1; - HeapHashMap<int, MemberWithCount> map_; + unsigned index_ = 0; + HeapHashMap<unsigned, MemberWithCount> map_; }; static CalculationValueHandleMap& CalcHandles() { diff --git a/third_party/blink/renderer/platform/geometry/length.h b/third_party/blink/renderer/platform/geometry/length.h index 5897e5f..2f09390 100644 --- a/third_party/blink/renderer/platform/geometry/length.h +++ b/third_party/blink/renderer/platform/geometry/length.h @@ -388,7 +388,7 @@ Length BlendSameTypes(const Length& from, double progress, ValueRange) const; - int CalculationHandle() const { + unsigned CalculationHandle() const { DCHECK(IsCalculated()); return calculation_handle_; } @@ -397,7 +397,7 @@ union { // If kType == kCalculated. - int calculation_handle_; + unsigned calculation_handle_; // Otherwise. Must be zero if not in use (e.g., for kAuto or kNone). float value_;
Original Bug Report
Potential OOB Heap Write / RCE via Integer Overflow in CalculationValueHandleMap
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 without the Chrome Security team.
Overview: An integer overflow in CalculationValueHandleMap allows a handle index to reach -1, which silently collides with the internal DeletedValue sentinel of the underlying WTF::HashMap. During table rehashing, this handle is dropped, leaving orphaned CSS Length objects. When these objects are destroyed, a failed map lookup returns map_.end(), causing an out-of-bounds decrement that perfectly aligns with the vtable pointer of adjacent polymorphic Oilpan objects.
Affected files:
third_party/blink/renderer/platform/geometry/length.ccthird_party/blink/renderer/platform/geometry/length.h
Estimated timestamp from git blame: 2025-05-28
Background and Root Cause
The CalculationValueHandleMap class (in third_party/blink/renderer/platform/geometry/length.cc) manages unique integer handles for CalculationValue objects used by CSS calc() expressions. It assigns handles using a monotonically increasing 32-bit signed integer, index_.
int insert(const CalculationValue* calc_value) {
DCHECK(index_);
while (map_.Contains(index_))
index_++;
map_.Set(index_, MemberWithCount(calc_value, 1u));
return index_;
}
While handles can be freed, index_ is never reset. After approximately 2.14 billion insertions, index_ overflows INT_MAX. Because Chromium builds use -fno-strict-overflow (defining signed integer overflow to wrap), index_ continues incrementing from INT_MIN up to -1.
Sentinel Collision and Rehash Drop
The handles are stored in a WTF::HeapHashMap<int, MemberWithCount>. For integer keys, WTF::IntHashTraits<int> reserves -1 as the DeletedValue() sentinel.
When index_ reaches -1, map_.Set(-1, ...) successfully inserts the key into an empty bucket. In release builds, DCHECK macros are compiled out, so no fatal validation prevents this insertion. However, the hash table’s internal state is now corrupted: IsDeletedBucket(*entry) evaluates to true for the -1 bucket, even though it is active.
If the hash table subsequently expands, HashTable::RehashTo() iterates over the array to copy elements. It uses the condition if (IsEmptyOrDeletedBucket(table_[i])) continue;. Because the -1 key matches the deleted sentinel, the bucket is skipped and permanently dropped, leaving the associated CSS Length object holding an orphaned handle.
Out-of-bounds Dereference and RCE Primitive
When the DOM element owning the orphaned Length object is removed, the Length destructor calls CalcHandles().DecrementCount(-1).
void DecrementCount(int index) {
DCHECK(map_.Contains(index)); // Omitted in release builds
auto iter = map_.find(index);
--iter->value.count;
// ...
}
Because the bucket was dropped during the rehash, map_.find(-1) fails and returns map_.end(). In release builds, operator-> on WTF::HashTable iterators lacks bounds checks. The code unconditionally executes --iter->value.count, resulting in an out-of-bounds memory write.
Perfect Memory Alignment for VTable Corruption
The backing store of the hash table is allocated on the Blink GC (Oilpan) heap.
- The
KeyValuePair<int, MemberWithCount>struct is exactly 12 bytes long (4-byteintkey, 4-byteMembercompressed pointer, 4-byteunsigned count). - If the hash table has a capacity (
table_size_) of 8, the payload is 96 bytes. - Oilpan objects on 64-bit platforms have an 8-byte
HeapObjectHeader. The total allocation is8 + 96 = 104bytes. - Oilpan allocation granularity is 8 bytes. Since 104 is a multiple of 8, there is zero padding at the end of the allocation.
- The
end()iterator points totable_ + table_size_, which is exactly offset 104—the start of the next object’sHeapObjectHeader. - The decrement targets
value.count, located at offset +8 relative to the iterator. Therefore, the memory accessed is offset104 + 8 = 112from the start of the first allocation. - Offset 112 bypasses the adjacent object’s header and points exactly to the first 8 bytes of the adjacent object’s payload.
For polymorphic C++ objects (e.g., ScriptWrappable DOM nodes), the first 8 bytes of the payload hold the vtable pointer. The -- operation deterministically decrements the adjacent object’s vtable pointer. By shaping the heap to place a controlled DOM node adjacent to the hash table, an attacker can hijack control flow upon the next virtual method call, leading to Remote Code Execution (RCE) in the renderer process.
Potential Attacker Steps to Trigger (Theoretical)
(Note: These are suggested steps; a working Proof of Concept has not yet been executed by our tooling.)
- Execute a JavaScript loop that rapidly creates and deletes unresolvable
calc()expressions (e.g.,element.style.width = "calc(" + i + "% + 1px)"). This advances theindex_counter without exhausting memory. - Stop deleting elements when
index_approaches-1. - Insert the
-1handle. - Create a sufficient number of new
calc()elements to exceed the load factor of theCalculationValueHandleMap, forcing a rehash that silently drops the-1bucket. - Perform heap feng-shui to allocate a target polymorphic object (e.g., a specific DOM node) immediately following the current
HashTablebacking store. - Remove the DOM element associated with the
-1handle, triggering the out-of-bounds--iter->value.counton the adjacent vtable pointer. - Call a virtual method on the adjacent object to hijack execution flow to attacker-controlled memory.
Suggested Fix
To remediate this issue, CalculationValueHandleMap::insert must prevent the use of reserved sentinel values (specifically 0 and -1).
The while loop should be updated to skip negative values and zero:
int insert(const CalculationValue* calc_value) {
while (index_ <= 0 || map_.Contains(index_)) {
index_++;
if (index_ <= 0) {
index_ = 1;
}
}
map_.Set(index_, MemberWithCount(calc_value, 1u));
return index_;
}
Alternatively, consider migrating the map’s key to a robust 64-bit ID generator (like base::IdType or a uint64_t sequence) to entirely preclude overflow conditions within a realistic timeframe.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.