Released December 12, 2025

CVE-2025-43531

CVSS 3.1 severity low CWE-362 vector CVSS:3.1/AV:N/AC:H/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: Phil Pizlo of Epic Games

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.

Patch Details

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;
-    }

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2025-43531
Bugzilla 301940
Component bmalloc
Bug Class Race
Severity medium
Commit 39a5ac27139893e6…
Advisory Apple Advisory

Terminology

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

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.

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.

Changed Functions

Function File Change Note
medium_directory_tuple_for_index_impl Source/bmalloc/libpas/src/libpas/pas_segregated_heap.c modified Builds a pas_segregated_heap_medium_directory_result capturing directory and allocator_index during the guarded read and folds them into the dependency chain, instead of returning a raw tuple pointer to dereference later.
pas_segregated_heap_medium_directory_tuple_for_index / _with_lock Source/bmalloc/libpas/src/libpas/pas_segregated_heap.c modified Return pas_segregated_heap_medium_directory_result by value; empty result on no rare_data / not found.
pas_segregated_heap_medium_allocator_index_for_index / _medium_size_directory_for_index Source/bmalloc/libpas/src/libpas/pas_segregated_heap.c modified Use result.allocator_index / result.directory directly rather than dereferencing a possibly-freed tuple pointer.
pas_segregated_heap_medium_directory_result (struct + create_empty) Source/bmalloc/libpas/src/libpas/pas_segregated_heap.h added New by-value result holding tuple_unsafe_without_lock (valid only under lock), directory, and allocator_index.
pas_local_allocator_try_allocate Source/bmalloc/libpas/src/libpas/pas_local_allocator_inlines.h modified Adds PAS_TESTING_ASSERT(size <= allocator->object_size) checks to catch allocation into a wrongly-sized class.

Files Changed

Source Files

  • 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

Test Files

  • Source/bmalloc/libpas/src/test/ExpendableMemoryTests.cpp