Released December 12, 2025

CVE-2025-43501

CVSS 4.3 severity medium CWE-787CWE-120 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L

Overview

Impact:

Description: Processing maliciously crafted web content may lead to an unexpected process crash

Researchers: Hossein Lotfi (@hosselot) of Trend Micro Zero Day Initiative

Key insight. HashTable sized itself with plain unsigned arithmetic, so an enormous key count could overflow the power-of-two/doubling math and allocate a table far too small; Checked<unsigned> makes the overflow trap.

Patch Details

diff --git a/Source/WTF/wtf/HashTable.h b/Source/WTF/wtf/HashTable.h
index 20525b041e77..d8bac5608901 100644
--- a/Source/WTF/wtf/HashTable.h
+++ b/Source/WTF/wtf/HashTable.h
@@ -31,6 +31,7 @@
 #include <utility>
 #include <wtf/AlignedStorage.h>
 #include <wtf/Assertions.h>
+#include <wtf/CheckedArithmetic.h>
 #include <wtf/DebugHeap.h>
 #include <wtf/FastMalloc.h>
 #include <wtf/HashTraits.h>
@@ -570,6 +571,7 @@ DECLARE_ALLOCATOR_WITH_HEAP_IDENTIFIER(HashTable);
         void remove(ValueType*);
 
         void deleteWeakNullEntries();
+        void deleteReleasedWeakBuckets();
 
         static constexpr unsigned computeBestTableSize(unsigned keyCount);
         bool shouldExpand() const { return HashTableSizePolicy::shouldExpand(keyCount() + deletedCount(), tableSize()); }
@@ -579,7 +581,7 @@ DECLARE_ALLOCATOR_WITH_HEAP_IDENTIFIER(HashTable);
         void shrink() { rehash(tableSize() / 2, nullptr); }
         void shrinkToBestSize();
 
-        ValueType* rehash(unsigned newTableSize, ValueType* entry);
+        ValueType* rehash(Checked<unsigned> newTableSize, ValueType* entry);
         ValueType* reinsert(ValueType&&);
 
         static void initializeBucket(ValueType& bucket);
@@ -1242,8 +1244,8 @@ DECLARE_ALLOCATOR_WITH_HEAP_IDENTIFIER(HashTable);
         if constexpr (KeyTraits::hasIsWeakNullValueFunction)
             deleteWeakNullEntries();
 
-        unsigned newSize;
-        unsigned oldSize = tableSize();
+        Checked<unsigned> newSize;
+        Checked<unsigned> oldSize = tableSize();
         if (!oldSize)
             newSize = KeyTraits::minimumTableSize;
         else if (mustRehashInPlace())
@@ -1257,7 +1259,7 @@ DECLARE_ALLOCATOR_WITH_HEAP_IDENTIFIER(HashTable);
     template<typename Key, typename Value, typename Extractor, typename HashFunctions, typename Traits, typename KeyTraits, typename Malloc>
     constexpr unsigned HashTable<Key, Value, Extractor, HashFunctions, Traits, KeyTraits, Malloc>::computeBestTableSize(unsigned keyCount)
     {
-        unsigned bestTableSize = roundUpToPowerOfTwo(keyCount);
+        Checked<unsigned> bestTableSize = roundUpToPowerOfTwo(keyCount);
         constexpr double minLoadRatio = 1.0 / minLoad;
 
         if (HashTableSizePolicy::shouldExpand(keyCount, bestTableSize))
@@ -1285,7 +1287,7 @@ DECLARE_ALLOCATOR_WITH_HEAP_IDENTIFIER(HashTable);
             if (aboveThresholdForEagerExpansion(largeLoadFactor, keyCount, bestTableSize))
                 bestTableSize *= 2;
         }
-        return std::max(bestTableSize, static_cast<unsigned>(KeyTraits::minimumTableSize));
+        return std::max(bestTableSize.value(), KeyTraits::minimumTableSize);
     }
 
     template<typename Key, typename Value, typename Extractor, typename HashFunctions, typename Traits, typename KeyTraits, typename Malloc>
@@ -1295,7 +1297,21 @@ DECLARE_ALLOCATOR_WITH_HEAP_IDENTIFIER(HashTable);
     }
 
     template<typename Key, typename Value, typename Extractor, typename HashFunctions, typename Traits, typename KeyTraits, typename Malloc>
-    auto HashTable<Key, Value, Extractor, HashFunctions, Traits, KeyTraits, Malloc>::rehash(unsigned newTableSize, ValueType* entry) -> ValueType*
+    void HashTable<Key, Value, Extractor, HashFunctions, Traits, KeyTraits, Malloc>::deleteReleasedWeakBuckets()
+    {
+        unsigned tableSize = this->tableSize();
+        for (unsigned i = 0; i < tableSize; ++i) {
+            auto& entry = m_table[i];
+            if (isReleasedWeakBucket(entry)) {
+                deleteBucket(entry);
+                setDeletedCount(deletedCount() + 1);
+                setKeyCount(keyCount() - 1);
+            }
+        }
+    }
+
+    template<typename Key, typename Value, typename Extractor, typename HashFunctions, typename Traits, typename KeyTraits, typename Malloc>
+    auto HashTable<Key, Value, Extractor, HashFunctions, Traits, KeyTraits, Malloc>::rehash(Checked<unsigned> newTableSize, ValueType* entry) -> ValueType*
     {
         internalCheckTableConsistencyExceptSize();
 

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2025-43501
Bugzilla 301371
Component WTF
Bug Class IntegerOverflow
Severity medium
Commit ad4544045d66266c…
Advisory Apple Advisory

Terminology

  • WTF HashTable — The core open-addressing hash table underlying HashMap/HashSet across WebKit, sized in powers of two.
  • Checked<unsigned> — A wrapper that traps arithmetic overflow instead of wrapping, forcing overflow to be handled rather than silently producing a small value.
  • Rehash / best table size — On growth the table computes a new power-of-two size from the key count; overflow here yields an undersized allocation.

Root Cause Analysis

This fixes an unsigned integer overflow in WTF::HashTable’s size computation that could produce an undersized table allocation and subsequent out-of-bounds access. computeBestTableSize() and rehash() computed table sizes with plain unsigned arithmetic: bestTableSize = roundUpToPowerOfTwo(keyCount) followed by conditional doublings (bestTableSize *= 2), and rehash() derived newSize from oldSize. For a hash table driven to a very large key count, these unsigned multiplications/roundings can overflow 2^32 and wrap to a small value, so the table is allocated far smaller than the number of entries it must hold; reinserting entries then indexes past the allocation — an out-of-bounds read/write and heap corruption. The fix converts the size accounting to Checked<unsigned> (newSize, oldSize, bestTableSize, and rehash’s newTableSize parameter) so any overflow in the rounding or doubling traps instead of silently wrapping, and returns std::max(bestTableSize.value(), minimumTableSize). It also adds deleteReleasedWeakBuckets(), which sweeps released weak buckets (decrementing keyCount, incrementing deletedCount) before rehash so the count feeding the size computation reflects reclaimed weak entries. The restored invariant is that table-size math cannot silently wrap to an undersized allocation. The precise overflowing multiply is one of the bestTableSize *= 2 / roundUpToPowerOfTwo steps now wrapped in Checked<unsigned>.

Attack Path

1. Grow a hash table

Drive a WTF HashTable/HashMap/HashSet (reachable via DOM/JS object bookkeeping) toward an enormous key count.

2. Overflow the size math

computeBestTableSize/rehash’s unsigned rounding and doubling wrap past 2^32 to a small size.

3. Undersized allocation

The table backing store is allocated too small for the live entries.

4. Out-of-bounds write

Reinserting entries indexes beyond the allocation, corrupting the heap in the WebContent process.

Impact Assessment

A heap out-of-bounds write primitive in the WebContent process, driven by inflating a hash table’s key count until its size computation overflows and under-allocates. Size-overflow-to-undersized-allocation is a classic, strong corruption primitive; the advisory rates it a crash, but the class supports controlled heap corruption toward code execution.

Audit Directions

Same file: residual unsigned math

In HashTable.h confirm all size/count arithmetic feeding allocation (tableSize, keyCount, deletedCount) is overflow-checked, including reinsert and shrink paths.

Container size computations

Grep WTF containers (Vector, HashTable, BitVector) for roundUpToPowerOfTwo/*=2 on unsigned sizes without Checked<> before an allocation.

Changed Functions

Function File Change Note
HashTable::computeBestTableSize Source/WTF/wtf/HashTable.h modified Uses Checked<unsigned> for bestTableSize so roundUpToPowerOfTwo and the *=2 doublings trap on overflow; returns std::max(bestTableSize.value(), minimumTableSize).
HashTable::rehash Source/WTF/wtf/HashTable.h modified Takes Checked<unsigned> newTableSize and computes newSize/oldSize as Checked<unsigned> to prevent wraparound to an undersized table.
HashTable::deleteReleasedWeakBuckets Source/WTF/wtf/HashTable.h added Sweeps released weak buckets before rehash (adjusting key/deleted counts) so the size computation is based on live entries.

Files Changed

Source Files

  • Source/WTF/wtf/HashTable.h