Medium chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in Blink
DescriptionInteger overflow in Blink
ComponentBlink
Bug ClassInteger Overflow
Tracker502322843
Fix commitf153d9ff63f9 (chromium/src) +23/-25
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/platform/geometry/length.cc
modified

Files Changed

  • third_party/blink/renderer/platform/geometry/length.cc
  • third_party/blink/renderer/platform/geometry/length.h
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_;
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • third_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.

  1. The KeyValuePair<int, MemberWithCount> struct is exactly 12 bytes long (4-byte int key, 4-byte Member compressed pointer, 4-byte unsigned count).
  2. If the hash table has a capacity (table_size_) of 8, the payload is 96 bytes.
  3. Oilpan objects on 64-bit platforms have an 8-byte HeapObjectHeader. The total allocation is 8 + 96 = 104 bytes.
  4. Oilpan allocation granularity is 8 bytes. Since 104 is a multiple of 8, there is zero padding at the end of the allocation.
  5. The end() iterator points to table_ + table_size_, which is exactly offset 104—the start of the next object’s HeapObjectHeader.
  6. The decrement targets value.count, located at offset +8 relative to the iterator. Therefore, the memory accessed is offset 104 + 8 = 112 from the start of the first allocation.
  7. 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.)

  1. Execute a JavaScript loop that rapidly creates and deletes unresolvable calc() expressions (e.g., element.style.width = "calc(" + i + "% + 1px)"). This advances the index_ counter without exhausting memory.
  2. Stop deleting elements when index_ approaches -1.
  3. Insert the -1 handle.
  4. Create a sufficient number of new calc() elements to exceed the load factor of the CalculationValueHandleMap, forcing a rehash that silently drops the -1 bucket.
  5. Perform heap feng-shui to allocate a target polymorphic object (e.g., a specific DOM node) immediately following the current HashTable backing store.
  6. Remove the DOM element associated with the -1 handle, triggering the out-of-bounds --iter->value.count on the adjacent vtable pointer.
  7. 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.

View on issue tracker