Medium CVSS 3.1 webkit Race 🔧 Commit mapped

Overview

Medium
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected process crash
Componentbmalloc
Sub-componentbmalloc
Bug ClassRace
Tracker301940
Fix commit39a5ac271398 (WebKit/WebKit) +76/-46
CWECWE-362 (Race condition)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:N/A:L
CISA KEVNot listed
CreditedPhil Pizlo of Epic Games
Disclosed2025-12-12

Background

libpas / bmalloc
WebKit’s high-performance allocator; segregated heaps use size directories and per-size allocator indices.
Lock-free dependency read
A read validated by a mutation counter and value-dependency chain so it can proceed without the heap lock, provided all used values are captured within the guarded read.
Scavenger
A background thread that decommits/rematerializes expendable allocator memory, concurrently mutating directory structures.

Root Cause Analysis

This fixes a lock-free data race in libpas (bmalloc) that could return a stale/freed medium-directory pointer and drive allocator confusion. pas_segregated_heap_medium_directory_tuple_for_index supports a fast, lock-free lookup guarded by a mutation-count ‘dependency’ scheme: it reads directory metadata without holding the heap lock and validates afterward that the mutation count did not change. The pre-patch impl returned a raw pas_segregated_heap_medium_directory_tuple* (result.tuple) that callers then DEREFERENCED after the lock-free search (reading directory->allocator_index and pas_compact_atomic_segregated_size_directory_ptr_load(&directory->directory)). Because a concurrent mutation (e.g. the scavenger decommitting/rematerializing expendable memory, or directory reallocation) can free or reuse that tuple’s memory between the read and the dereference, callers could dereference stale/freed metadata and obtain a wrong size directory or allocator_index.

The fix introduces pas_segregated_heap_medium_directory_result, which captures the needed values (directory pointer and allocator_index) DURING the guarded read and folds them into the mutation-count dependency chain, returning them BY VALUE; the raw tuple pointer is kept only as tuple_unsafe_without_lock, explicitly documented as valid only while holding the heap lock. Callers (medium_allocator_index_for_index, medium_size_directory_for_index) now use result.allocator_index / result.directory directly instead of dereferencing the tuple, and lock-holding callers use tuple_unsafe_without_lock. Added PAS_TESTING_ASSERTs (size <= allocator->object_size) in pas_local_allocator_try_allocate guard the downstream consequence — using a wrong allocator index to allocate into a size class that is too small.

The restored invariant is that values obtained on the lock-free path are snapshotted within the guarded read rather than fetched by dereferencing a pointer that a concurrent mutation can invalidate.

Key insight
The lock-free medium-directory lookup returned a raw tuple pointer that callers dereferenced after the guarded read, so a concurrent scavenger/reallocation could free it; capturing the needed fields by value within the dependency-checked read removes the race.

Attack Path

  1. Drive concurrent allocation Run JS/web content that churns medium-size allocations while background scavenging runs, so lock-free medium-directory lookups race with mutations.
  2. Race the tuple lifetime A concurrent scavenger/reallocation frees or reuses a medium-directory tuple between the lock-free read and the caller’s dereference.
  3. Read stale metadata The caller dereferences the freed tuple and gets a wrong size directory / allocator_index.
  4. Allocator confusion Allocating with the wrong index places an object into a mismatched size class, corrupting heap metadata in the WebContent process.

Impact Assessment

A race in the core allocator confined to the process using it (WebContent), where a lock-free lookup could dereference freed medium-directory metadata and yield a wrong size class / allocator index — a path to heap metadata corruption. Allocator-internal races are difficult to weaponize and timing-dependent; the advisory rates it a crash, but allocator confusion is a serious corruption class if made reliable.

Changed Functions

FunctionChangeNotes
if
Source/bmalloc/libpas/src/libpas/pas_local_allocator_inlines.h
modified
if
Source/bmalloc/libpas/src/libpas/pas_segregated_heap.c
modified
switch
Source/bmalloc/libpas/src/libpas/pas_segregated_heap.c
modified
if
Source/bmalloc/libpas/src/test/ExpendableMemoryTests.cpp
modified

Files Changed

  • Source/bmalloc/libpas/src/libpas/pas_local_allocator_inlines.h
  • Source/bmalloc/libpas/src/libpas/pas_segregated_heap.c
  • Source/bmalloc/libpas/src/libpas/pas_segregated_heap.h
  • Source/bmalloc/libpas/src/test/ExpendableMemoryTests.cpp

Audit Directions

  • Same file: post-read dereferences
    In pas_segregated_heap.c audit remaining .tuple_unsafe_without_lock uses to confirm they only occur under pas_lock_is_held.
  • Lock-free result plumbing
    Grep libpas for lock-free lookups returning pointers into structures the scavenger can decommit/reallocate; values must be snapshotted inside the dependency chain.
diff --git a/Source/bmalloc/libpas/src/libpas/pas_local_allocator_inlines.h b/Source/bmalloc/libpas/src/libpas/pas_local_allocator_inlines.h
index cb70a8ede00c..48c4c4d51b65 100644
--- a/Source/bmalloc/libpas/src/libpas/pas_local_allocator_inlines.h
+++ b/Source/bmalloc/libpas/src/libpas/pas_local_allocator_inlines.h
@@ -1833,6 +1833,7 @@ pas_local_allocator_try_allocate(pas_local_allocator* allocator,
     pas_allocation_result result;
 
     PAS_TESTING_ASSERT(!allocator->scavenger_data.is_in_use);
+    PAS_TESTING_ASSERT(size <= allocator->object_size || !allocator->object_size);
 
     if (verbose) {
         pas_log("Allocator %p (%s) allocating size = %zu, alignment = %zu.\n",
@@ -1854,6 +1855,7 @@ pas_local_allocator_try_allocate(pas_local_allocator* allocator,
                     (void*)result.begin);
         }
     
+        PAS_TESTING_ASSERT(size <= allocator->object_size);
         return result_filter(
             pas_allocation_result_create_success_with_zero_mode(result.begin, result.zero_mode));
     }
@@ -1875,6 +1877,7 @@ pas_local_allocator_try_allocate(pas_local_allocator* allocator,
             allocator, allocation_mode, counts, result_filter);
         if (verbose)
             pas_log("in small segregated slow return - result.begin = %p\n", (void*)result.begin);
+        PAS_TESTING_ASSERT(size <= allocator->object_size);
         return result;
     }
 
@@ -1882,6 +1885,7 @@ pas_local_allocator_try_allocate(pas_local_allocator* allocator,
         allocator, size, alignment, allocation_mode, counts, result_filter);
     if (verbose)
         pas_log("in generic return - result.begin = %p\n", (void*)result.begin);
+    PAS_TESTING_ASSERT(size <= allocator->object_size);
     return result;
 }
 
diff --git a/Source/bmalloc/libpas/src/libpas/pas_segregated_heap.c b/Source/bmalloc/libpas/src/libpas/pas_segregated_heap.c
index b46f8ac7aca6..9d7fcbf3a977 100644
--- a/Source/bmalloc/libpas/src/libpas/pas_segregated_heap.c
+++ b/Source/bmalloc/libpas/src/libpas/pas_segregated_heap.c
@@ -344,7 +344,7 @@ pas_segregated_size_directory* pas_segregated_heap_size_directory_for_index_slow
 }
 
 typedef struct {
-    pas_segregated_heap_medium_directory_tuple* tuple;
+    pas_segregated_heap_medium_directory_result value;
     uintptr_t dependency;
 } medium_directory_tuple_for_index_impl_result;
 
@@ -360,7 +360,7 @@ medium_directory_tuple_for_index_impl(
 
     unsigned begin;
     unsigned end;
-    pas_segregated_heap_medium_directory_tuple* best;
+    pas_segregated_heap_medium_directory_result best;
     medium_directory_tuple_for_index_impl_result result;
 
     PAS_ASSERT(rare_data);
@@ -370,7 +370,7 @@ medium_directory_tuple_for_index_impl(
 
     begin = 0;
     end = num_medium_directories;
-    best = NULL;
+    best = pas_segregated_heap_medium_directory_result_create_empty();
 
     result.dependency = (uintptr_t)medium_directories;
 
@@ -379,6 +379,7 @@ medium_directory_tuple_for_index_impl(
         pas_segregated_heap_medium_directory_tuple* directory;
         unsigned begin_index;
         unsigned end_index;
+        pas_segregated_heap_medium_directory_result result_for_current;
 
         middle = (begin + end) >> 1;
 
@@ -395,7 +396,7 @@ medium_directory_tuple_for_index_impl(
            or the tuple straddling page boundary, leading to the begin index being zero and the end_index
            having its original value. */
         if (!begin_index) {
-            result.tuple = NULL;
+            result.value = pas_segregated_heap_medium_directory_result_create_empty();
             return result;
         }
 
@@ -403,9 +404,18 @@ medium_directory_tuple_for_index_impl(
 
         result.dependency += begin_index + end_index;
 
+        result_for_current.tuple_unsafe_without_lock = directory;
+        result_for_current.directory =
+            pas_compact_atomic_segregated_size_directory_ptr_load(&directory->directory);
+        result_for_current.allocator_index = directory->allocator_index;
+
+        /* Don't include the tuple in the dependency since you should only be using it if you're
+           holding the lock. */
+        result.dependency +=
+            (uintptr_t)result_for_current.directory + result_for_current.allocator_index;
         if (index < begin_index) {
             end = middle;
-            best = directory;
+            best = result_for_current;
             continue;
         }
 
@@ -414,33 +424,33 @@ medium_directory_tuple_for_index_impl(
             continue;
         }
 
-        result.tuple = directory;
+        result.value = result_for_current;
         return result;
     }
 
     switch (search_mode) {
     case pas_segregated_heap_medium_size_directory_search_within_size_class_progression:
-        result.tuple = NULL;
+        result.value = pas_segregated_heap_medium_directory_result_create_empty();
         return result;
 
     case pas_segregated_heap_medium_size_directory_search_least_greater_equal:
-        result.tuple = best;
+        result.value = best;
         return result;
     }
 
     PAS_ASSERT_NOT_REACHED();
-    result.tuple = NULL;
+    result.value = pas_segregated_heap_medium_directory_result_create_empty();
     return result;
 }
 
-static pas_segregated_heap_medium_directory_tuple*
+static pas_segregated_heap_medium_directory_result
 medium_directory_tuple_for_index_with_lock(
     pas_segregated_heap* heap,
     size_t index,
     pas_segregated_heap_medium_size_directory_search_mode search_mode,
     pas_lock_hold_mode heap_lock_hold_mode)
 {
-    pas_segregated_heap_medium_directory_tuple* result;
+    pas_segregated_heap_medium_directory_result result;
     pas_segregated_heap_rare_data* rare_data;
     pas_segregated_heap_medium_directory_tuple* medium_directories;
 
@@ -455,14 +465,14 @@ medium_directory_tuple_for_index_with_lock(
         medium_directories,
         rare_data->num_medium_directories,
         index,
-        search_mode).tuple;
+        search_mode).value;
 
     pas_heap_lock_unlock_conditionally(heap_lock_hold_mode);
 
     return result;
 }
 
-pas_segregated_heap_medium_directory_tuple*
+pas_segregated_heap_medium_directory_result
 pas_segregated_heap_medium_directory_tuple_for_index(
     pas_segregated_heap* heap,
     size_t index,
@@ -479,7 +489,7 @@ pas_segregated_heap_medium_directory_tuple_for_index(
 
     rare_data = pas_segregated_heap_rare_data_ptr_load(&heap->rare_data);
     if (!rare_data)
-        return NULL;
+        return pas_segregated_heap_medium_directory_result_create_empty();
 
     if (heap_lock_hold_mode == pas_lock_is_held) {
         return medium_directory_tuple_for_index_with_lock(
@@ -502,9 +512,9 @@ pas_segregated_heap_medium_directory_tuple_for_index(
 
     if (pas_mutation_count_matches_with_dependency(
             &rare_data->mutation_count, saved_count, result.dependency)) {
-        if (verbose && !result.tuple)
+        if (verbose && !result.value.directory)
             pas_log("did not find tuple\n");
-        return result.tuple;
+        return result.value;
     }
 
     return medium_directory_tuple_for_index_with_lock(
@@ -519,20 +529,16 @@ unsigned pas_segregated_heap_medium_allocator_index_for_index(
 {
     static const bool verbose = PAS_SHOULD_LOG(PAS_LOG_SEGREGATED_HEAPS);
 
-    pas_segregated_heap_medium_directory_tuple* medium_directory;
+    pas_segregated_heap_medium_directory_result medium_directory;
+    unsigned result;
 
     medium_directory = pas_segregated_heap_medium_directory_tuple_for_index(
         heap, index, search_mode, heap_lock_hold_mode);
 
-    if (medium_directory) {
-        unsigned result;
-        result = medium_directory->allocator_index;
-        if (verbose && !result)
-            pas_log("found null allocator index\n");
-        return result;
-    }
-
-    return 0;
+    result = medium_directory.allocator_index;
+    if (verbose && !result)
+        pas_log("found null allocator index\n");
+    return result;
 }
 
 pas_segregated_size_directory* pas_segregated_heap_medium_size_directory_for_index(
@@ -541,15 +547,12 @@ pas_segregated_size_directory* pas_segregated_heap_medium_size_directory_for_ind
     pas_segregated_heap_medium_size_directory_search_mode search_mode,
     pas_lock_hold_mode heap_lock_hold_mode)
 {
-    pas_segregated_heap_medium_directory_tuple* medium_directory;
+    pas_segregated_heap_medium_directory_result medium_directory;
 
     medium_directory = pas_segregated_heap_medium_directory_tuple_for_index(
         heap, index, search_mode, heap_lock_hold_mode);
 
-    if (medium_directory)
-        return pas_compact_atomic_segregated_size_directory_ptr_load(&medium_directory->directory);
-
-    return NULL;
+    return medium_directory.directory;
 }
 
 static size_t compute_small_index_upper_bound(pas_segregated_heap* heap,
@@ -920,7 +923,7 @@ pas_segregated_heap_ensure_allocator_index(
             pas_segregated_heap_medium_directory_tuple_for_index(
                 heap, index,
                 pas_segregated_heap_medium_size_directory_search_within_size_class_progression,
-                pas_lock_is_held);
+                pas_lock_is_held).tuple_unsafe_without_lock;
         PAS_ASSERT(medium_directory, medium_directory);
         PAS_ASSERT(
             pas_compact_atomic_segregated_size_directory_ptr_load(&medium_directory->directory)
@@ -1594,7 +1597,7 @@ pas_segregated_heap_ensure_size_directory_for_size(
         medium_tuple = pas_segregated_heap_medium_directory_tuple_for_index(
             heap, index,
             pas_segregated_heap_medium_size_directory_search_within_size_class_progression,
-            pas_lock_is_held);
+            pas_lock_is_held).tuple_unsafe_without_lock;
         if (medium_tuple) {
             pas_segregated_heap_rare_data* rare_data;
 
@@ -1715,7 +1718,7 @@ pas_segregated_heap_ensure_size_directory_for_size(
         medium_tuple = pas_segregated_heap_medium_directory_tuple_for_index(
             heap, index,
             pas_segregated_heap_medium_size_directory_search_least_greater_equal,
-            pas_lock_is_held);
+            pas_lock_is_held).tuple_unsafe_without_lock;
         if (medium_tuple) {
             pas_segregated_size_directory* directory;
             directory = pas_compact_atomic_segregated_size_directory_ptr_load(
@@ -2072,7 +2075,7 @@ pas_segregated_heap_ensure_size_directory_for_size(
             next_tuple = pas_segregated_heap_medium_directory_tuple_for_index(
                 heap, index,
                 pas_segregated_heap_medium_size_directory_search_least_greater_equal,
-                pas_lock_is_held);
+                pas_lock_is_held).tuple_unsafe_without_lock;
 
             if (next_tuple &&
                 pas_compact_atomic_segregated_size_directory_ptr_load(
diff --git a/Source/bmalloc/libpas/src/libpas/pas_segregated_heap.h b/Source/bmalloc/libpas/src/libpas/pas_segregated_heap.h
index f828b8c3a009..b8b43401e03b 100644
--- a/Source/bmalloc/libpas/src/libpas/pas_segregated_heap.h
+++ b/Source/bmalloc/libpas/src/libpas/pas_segregated_heap.h
@@ -66,11 +66,13 @@ typedef enum pas_segregated_heap_medium_size_directory_search_mode pas_segregate
 struct pas_heap;
 struct pas_segregated_size_directory;
 struct pas_segregated_heap;
+struct pas_segregated_heap_medium_directory_result;
 struct pas_segregated_heap_medium_directory_tuple;
 struct pas_segregated_heap_rare_data;
 typedef struct pas_heap pas_heap;
 typedef struct pas_segregated_size_directory pas_segregated_size_directory;
 typedef struct pas_segregated_heap pas_segregated_heap;
+typedef struct pas_segregated_heap_medium_directory_result pas_segregated_heap_medium_directory_result;
 typedef struct pas_segregated_heap_medium_directory_tuple pas_segregated_heap_medium_directory_tuple;
 typedef struct pas_segregated_heap_rare_data pas_segregated_heap_rare_data;
 
@@ -126,6 +128,20 @@ struct pas_segregated_heap_medium_directory_tuple {
     pas_segregated_heap_medium_directory_index end_index; /* inclusive */
 };
 
+struct pas_segregated_heap_medium_directory_result {
+    /* This field is super dangerous to use - it's only reliable in the following ways:
+
+       - It's not NULL if we found an entry. But you could also determine that by looking at whether
+         `directory` is not NULL or `allocator_index` is not zero.
+
+       - It'll point to what you want it to point to if you were holding the heap lock the entire time
+         that you were performing the medium directory query. */
+    pas_segregated_heap_medium_directory_tuple* tuple_unsafe_without_lock;
+
+    pas_segregated_size_directory* directory;
+    pas_allocator_index allocator_index;
+};
+
 struct pas_segregated_heap_rare_data {
     /* This code is counter-locked. Note that odd values mean that we are mutating right now. */
     pas_mutation_count mutation_count;
@@ -205,7 +221,16 @@ PAS_API bool pas_segregated_heap_index_is_greater_equal_cached_index_and_cached_
     size_t index,
     const pas_heap_config* config);
 
-PAS_API pas_segregated_heap_medium_directory_tuple*
+static inline pas_segregated_heap_medium_directory_result pas_segregated_heap_medium_directory_result_create_empty(void)
+{
+    pas_segregated_heap_medium_directory_result result;
+    result.tuple_unsafe_without_lock = NULL;
+    result.directory = NULL;
+    result.allocator_index = 0;
+    return result;
+}
+
+PAS_API pas_segregated_heap_medium_directory_result
 pas_segregated_heap_medium_directory_tuple_for_index(
     pas_segregated_heap* heap,
     size_t index,
diff --git a/Source/bmalloc/libpas/src/test/ExpendableMemoryTests.cpp b/Source/bmalloc/libpas/src/test/ExpendableMemoryTests.cpp
index cf21b49be76a..bd4a33d49e55 100644
--- a/Source/bmalloc/libpas/src/test/ExpendableMemoryTests.cpp
+++ b/Source/bmalloc/libpas/src/test/ExpendableMemoryTests.cpp
@@ -274,36 +274,34 @@ void testRematerializeAfterSearchOfDecommitted()
         reinterpret_cast<uintptr_t>(ptr), &bmalloc_heap_config);
     pas_segregated_size_directory* directory = pas_segregated_view_get_size_directory(view);
 
-    pas_segregated_heap_medium_directory_tuple* tuple =
+    pas_segregated_heap_medium_directory_result result =
         pas_segregated_heap_medium_directory_tuple_for_index(
             &heap->segregated_heap,
             pas_segregated_heap_index_for_size(size, BMALLOC_HEAP_CONFIG),
             pas_segregated_heap_medium_size_directory_search_within_size_class_progression,
             pas_lock_is_not_held);
 
-    CHECK(tuple);
-    CHECK_EQUAL(pas_compact_atomic_segregated_size_directory_ptr_load(&tuple->directory),
-                directory);
+    CHECK_EQUAL(result.directory, directory);
 
     pas_scavenger_fake_decommit_expendable_memory();
 
-    tuple->begin_index = 0;
+    result.tuple_unsafe_without_lock->begin_index = 0;
 
-    pas_segregated_heap_medium_directory_tuple* someOtherTuple =
+    pas_segregated_heap_medium_directory_result someOtherResult =
         pas_segregated_heap_medium_directory_tuple_for_index(
             &heap->segregated_heap,
             pas_segregated_heap_index_for_size(someOtherSize, BMALLOC_HEAP_CONFIG),
             pas_segregated_heap_medium_size_directory_search_within_size_class_progression,
             pas_lock_is_not_held);
 
-    if (someOtherTuple) {
-        cout << "Unexpectedly found a tuple: " << someOtherTuple << "\n";
+    if (someOtherResult.tuple_unsafe_without_lock) {
+        cout << "Unexpectedly found a tuple: " << someOtherResult.tuple_unsafe_without_lock << "\n";
         cout << "It points at directory = "
-             << pas_compact_atomic_segregated_size_directory_ptr_load(&someOtherTuple->directory) << "\n";
+             << someOtherResult.directory << "\n";
         cout << "Our original directory is = " << directory << "\n";
     }
     
-    CHECK(!someOtherTuple);
+    CHECK(!someOtherResult.tuple_unsafe_without_lock);
 }
 
 void testBasicSizeClass(unsigned firstSize, unsigned secondSize)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/Source/bmalloc/libpas/src/test/ExpendableMemoryTests.cpp b/Source/bmalloc/libpas/src/test/ExpendableMemoryTests.cpp
index cf21b49be76a..bd4a33d49e55 100644
--- a/Source/bmalloc/libpas/src/test/ExpendableMemoryTests.cpp
+++ b/Source/bmalloc/libpas/src/test/ExpendableMemoryTests.cpp
@@ -274,36 +274,34 @@ void testRematerializeAfterSearchOfDecommitted()
         reinterpret_cast<uintptr_t>(ptr), &bmalloc_heap_config);
     pas_segregated_size_directory* directory = pas_segregated_view_get_size_directory(view);
 
-    pas_segregated_heap_medium_directory_tuple* tuple =
+    pas_segregated_heap_medium_directory_result result =
         pas_segregated_heap_medium_directory_tuple_for_index(
             &heap->segregated_heap,
             pas_segregated_heap_index_for_size(size, BMALLOC_HEAP_CONFIG),
             pas_segregated_heap_medium_size_directory_search_within_size_class_progression,
             pas_lock_is_not_held);
 
-    CHECK(tuple);
-    CHECK_EQUAL(pas_compact_atomic_segregated_size_directory_ptr_load(&tuple->directory),
-                directory);
+    CHECK_EQUAL(result.directory, directory);
 
     pas_scavenger_fake_decommit_expendable_memory();
 
-    tuple->begin_index = 0;
+    result.tuple_unsafe_without_lock->begin_index = 0;
 
-    pas_segregated_heap_medium_directory_tuple* someOtherTuple =
+    pas_segregated_heap_medium_directory_result someOtherResult =
         pas_segregated_heap_medium_directory_tuple_for_index(
             &heap->segregated_heap,
             pas_segregated_heap_index_for_size(someOtherSize, BMALLOC_HEAP_CONFIG),
             pas_segregated_heap_medium_size_directory_search_within_size_class_progression,
             pas_lock_is_not_held);
 
-    if (someOtherTuple) {
-        cout << "Unexpectedly found a tuple: " << someOtherTuple << "\n";
+    if (someOtherResult.tuple_unsafe_without_lock) {
+        cout << "Unexpectedly found a tuple: " << someOtherResult.tuple_unsafe_without_lock << "\n";
         cout << "It points at directory = "
-             << pas_compact_atomic_segregated_size_directory_ptr_load(&someOtherTuple->directory) << "\n";
+             << someOtherResult.directory << "\n";
         cout << "Our original directory is = " << directory << "\n";
     }
     
-    CHECK(!someOtherTuple);
+    CHECK(!someOtherResult.tuple_unsafe_without_lock);
 }
 
 void testBasicSizeClass(unsigned firstSize, unsigned secondSize)
Loading diff…

Original Bug Report

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