CVE-2025-43501
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