CVE-2025-43531
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.hSource/bmalloc/libpas/src/libpas/pas_segregated_heap.cSource/bmalloc/libpas/src/libpas/pas_segregated_heap.h
Test Files
Source/bmalloc/libpas/src/test/ExpendableMemoryTests.cpp