-
Notifications
You must be signed in to change notification settings - Fork 8k
Description
Description
| Field | Value |
|---|---|
| Component | ext/spl/spl_heap.c |
| Affects | PHP 8.4.x, 8.5.x (all versions since GH-16337 fix) |
| Type | Use-After-Free / Heap Corruption |
| Requires | PHP code execution, Fibers |
| Tested on | PHP 8.5.2, x86_64 Linux, debug + release builds |
| Related | GH-16337, commit a56ff4f |
Summary
Commit a56ff4fec71 (Oct 2024) introduced SPL_HEAP_WRITE_LOCKED and spl_heap_consistency_validations() to prevent concurrent modification of SplHeap internals. The fix was applied to insert(), extract(), and top(), but two code paths that also call spl_ptr_heap_delete_top() were missed:
PHP_METHOD(SplHeap, next)at line 962spl_heap_it_move_forward()at line 937 validates withwrite=false(should bewrite=true)
Both call spl_ptr_heap_delete_top(), which is a write operation that modifies the heap's internal array and calls the user-supplied compare() function during sift-down. When a Fiber suspends inside compare() during a write-locked operation (e.g., extract()), calling next() or iterating with foreach performs a concurrent delete_top() on the same heap, corrupting its internal state.
Root Cause
ext/spl/spl_heap.c lines 962-969:
PHP_METHOD(SplHeap, next)
{
spl_heap_object *intern = Z_SPLHEAP_P(ZEND_THIS);
ZEND_PARSE_PARAMETERS_NONE();
// BUG: No call to spl_heap_consistency_validations(intern, true)
// extract() and insert() both perform this check.
spl_ptr_heap_delete_top(intern->heap, NULL, ZEND_THIS);
}Compare with extract() at line 635, which correctly checks:
PHP_METHOD(SplHeap, extract)
{
// ...
if (UNEXPECTED(spl_heap_consistency_validations(intern, true) != SUCCESS)) {
RETURN_THROWS();
}
// ...
}And spl_heap_it_move_forward() at line 937 has the wrong flag:
static void spl_heap_it_move_forward(zend_object_iterator *iter)
{
spl_heap_object *object = Z_SPLHEAP_P(&iter->data);
// BUG: write=false, but this calls delete_top (a write operation)
if (UNEXPECTED(spl_heap_consistency_validations(object, false) != SUCCESS)) {
return;
}
spl_ptr_heap_delete_top(object->heap, NULL, &iter->data);
// ...
}Technique
- Subclass
SplMinHeapwith acompare()that callsFiber::suspend()on a specific comparison count - Start
extract()inside a Fiber - it suspends mid-sift-down while the heap isWRITE_LOCKED - Call
$heap->next()- bypasses the lock, performs a concurrentspl_ptr_heap_delete_top() - Each
next()call frees an element viazval_ptr_dtor(), decrementsheap->count, runs its own sift-down, and clearsWRITE_LOCKED- all while the original extract's sift-down is still suspended - Resume the Fiber - the original sift-down continues with stale
limitandbottompointers, accessing positions beyond the now-reduced valid range
Confirmed impact
Debug build (ZTS DEBUG):
- Assertion failure:
ht=0x... is already destroyedatzend_hash.c:2692 - Crash via SIGABRT (exit code 134)
Release build (NTS, default ZMM allocator):
extract()returns NULL- Subsequent extractions return corrupted data
- Elements are duplicated
- Elements are lost
- Queue ordering is destroyed
zend_mm_heap corrupted- heap allocator metadata destroyed
Valgrind (release build, USE_ZEND_ALLOC=0):
- 48 errors from 28 contexts
- Multiple
Invalid read of size 4andInvalid writeon freed blocks of size 56 (zend_array)
Memory-level issue path
The freed zend_array structures are 56 bytes (ZMM bin 6). On release builds with the default allocator:
- UAF frees
zend_array(56 bytes) back to ZMM bin 6 free list - Attacker allocates
zend_stringobjects of length 24-31 (header 24 + data + null = 56 bytes, same bin) - ZMM's LIFO free list guarantees the string lands in the freed slot
- The dangling zval (
type=IS_ARRAY,value.arrpointing to the now-reallocated memory) interprets the string data as azend_arraystruct - String bytes at offset 24-31 overlap
zend_array.pDestructor(a function pointer at offset 48) - When the fake array is destroyed,
pDestructor(zval*)is called with the attacker-controlled address
PHP Version
PHP 8.5.2 (cli) (built: Mar 9 2026 15:05:25) (ZTS DEBUG)
Copyright (c) The PHP Group
Zend Engine v4.5.2, Copyright (c) Zend Technologies
with Zend OPcache v8.5.2, Copyright (c), by Zend Technologies
Operating System
No response