CVE-2025-43531
Overview
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.
Attack Path
- 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.
- 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.
- Read stale metadata The caller dereferences the freed tuple and gets a wrong size directory / allocator_index.
- Allocator confusion Allocating with the wrong index places an object into a mismatched size class, corrupting heap metadata in the WebContent process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifSource/bmalloc/libpas/src/libpas/pas_local_allocator_inlines.h |
modified | |
ifSource/bmalloc/libpas/src/libpas/pas_segregated_heap.c |
modified | |
switchSource/bmalloc/libpas/src/libpas/pas_segregated_heap.c |
modified | |
ifSource/bmalloc/libpas/src/test/ExpendableMemoryTests.cpp |
modified |
Files Changed
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.hSource/bmalloc/libpas/src/test/ExpendableMemoryTests.cpp
Audit Directions
- Same file: post-read dereferencesIn pas_segregated_heap.c audit remaining .tuple_unsafe_without_lock uses to confirm they only occur under pas_lock_is_held.
- Lock-free result plumbingGrep libpas for lock-free lookups returning pointers into structures the scavenger can decommit/reallocate; values must be snapshotted inside the dependency chain.
Patch
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)
Regression Test / PoC
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)