Medium CVSS 8.8 webkit OOB 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to memory corruption
ComponentJSC
Sub-componentJSC Runtime
Bug ClassOOB
Tracker293579
Fix commita05032c69610 (WebKit/WebKit) +6/-6
CWECWE-119 (Buffer bounds error)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedYuhao Hu, Yan Kang, Chenggang Wu, and Xiaojie Wei
Disclosed2025-07-29

Background

OrderedHashTable
The insertion-ordered hash table backing JavaScript Map and Set in JSC.
normalizeMapKey
Normalizes a key (e.g. -0 to 0); for object/proxy keys it can invoke user code with side effects.
Rehash / reallocation
Growing the table moves its storage; indices captured before a reallocation become stale.

Root Cause Analysis

This fixes a memory-corruption bug in JavaScriptCore’s OrderedHashTable (backing Map/Set) where storage-derived indices were computed before a JS-observable side effect that can reallocate the storage. In add(), after expandIfNeeded returns the candidate storage, the pre-patch code immediately captured capacity, newEntry = usedCapacity(candidate), the new entry’s key index, and called incrementAliveEntryCount(candidate) — and only THEN, for the first alive entry, called normalizeMapKey(key). normalizeMapKey can run arbitrary JS (via valueOf/toString or a proxy) which may re-enter the engine, allocate, trigger GC, or cause the table to rehash/reallocate, invalidating the previously captured candidate/capacity/newEntry index. The subsequent write then uses a stale index into freed or moved storage — memory corruption.

The fix reorders the operations: it performs the firstAliveEntry normalizeMapKey step first, and only afterward computes capacity/newEntry/newEntryKeyIndex and increments the alive count from the current candidate state.

The restored invariant is that storage-derived indices and counters are computed after any step that can reallocate the storage.

Key insight
The add path cached storage capacity and the new-entry index and bumped the live count before normalizeMapKey, which can reallocate the table — so the write landed at a stale index; reordering the bookkeeping after the side effect fixes it.

Attack Path

  1. Insert a key with side effects Add to a Map/Set a key whose normalization (valueOf/toString or proxy) runs attacker JS.
  2. Reallocate during normalization From that callback, cause the table to rehash/reallocate (or trigger GC), invalidating the pre-computed candidate storage and indices.
  3. Write at a stale index The add continues using the captured capacity/newEntry index into the now-freed/moved storage.
  4. Corrupt memory The stale-index write corrupts heap memory in the WebContent process.

Impact Assessment

A memory-corruption primitive in the WebContent process reachable from ordinary Map/Set usage with a side-effecting key. Stale-index writes into reallocated hash-table storage are controllable with grooming; the advisory rates it memory corruption.

Files Changed

  • Source/JavaScriptCore/runtime/OrderedHashTableHelper.h

Audit Directions

  • Same file: order of side effects
    Audit OrderedHashTableHelper for other places that capture storage pointers/indices before a call that can rehash, GC, or run user code.
  • Side-effecting key normalization
    Grep Map/Set/WeakMap operations for cached candidate/capacity values used across normalizeMapKey / hashValue calls that may re-enter.
diff --git a/Source/JavaScriptCore/runtime/OrderedHashTableHelper.h b/Source/JavaScriptCore/runtime/OrderedHashTableHelper.h
index 8c9190faebc2..ed3de117de16 100644
--- a/Source/JavaScriptCore/runtime/OrderedHashTableHelper.h
+++ b/Source/JavaScriptCore/runtime/OrderedHashTableHelper.h
@@ -439,12 +439,6 @@ public:
         Storage* candidate = expandIfNeeded(globalObject, owner, base);
         RETURN_IF_EXCEPTION(scope, void());
 
-        Storage& candidateRef = *candidate;
-        TableSize capacity = Helper::capacity(candidateRef);
-        Entry newEntry = usedCapacity(candidateRef);
-        TableIndex newEntryKeyIndex = entryDataStartIndex(dataTableStartIndex(capacity), newEntry);
-        incrementAliveEntryCount(candidateRef);
-
         bool firstAliveEntry = result.normalizedKey.isEmpty();
         if (firstAliveEntry) [[unlikely]] {
             result.normalizedKey = normalizeMapKey(key);
@@ -452,6 +446,12 @@ public:
             RETURN_IF_EXCEPTION(scope, void());
         }
 
+        Storage& candidateRef = *candidate;
+        TableSize capacity = Helper::capacity(candidateRef);
+        Entry newEntry = usedCapacity(candidateRef);
+        TableIndex newEntryKeyIndex = entryDataStartIndex(dataTableStartIndex(capacity), newEntry);
+        incrementAliveEntryCount(candidateRef);
+
         bool rehashed = &base != candidate;
         if (rehashed || firstAliveEntry) [[unlikely]]
             result.bucketIndex = bucketIndex(capacity, result.hash);
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker.