Skip to content

Commit 863ed29

Browse files
committed
Fix GH-16321: UAF in list_entry_destructor when resource dtor adds references
list_entry_destructor() freed the resource struct unconditionally after calling its destructor. If user code ran during the destructor (e.g. a stream filter callback) and something captured a new reference to the resource (e.g. an exception backtrace), the struct was freed while still referenced, causing a heap-use-after-free in _build_trace_args(). Bump the resource refcount before calling the destructor so reentrant calls to zend_list_free() can't trigger a premature free. After the destructor returns, skip the free if the refcount is still elevated. Add a fallback path in zend_list_free() to free directly when the resource has already been removed from the list. Closes GH-16321
1 parent 0f3e741 commit 863ed29

File tree

2 files changed

+39
-1
lines changed

2 files changed

+39
-1
lines changed

Zend/zend_list.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ ZEND_API zend_result ZEND_FASTCALL zend_list_delete(zend_resource *res)
5555
ZEND_API void ZEND_FASTCALL zend_list_free(zend_resource *res)
5656
{
5757
ZEND_ASSERT(GC_REFCOUNT(res) == 0);
58-
zend_hash_index_del(&EG(regular_list), res->handle);
58+
if (zend_hash_index_del(&EG(regular_list), res->handle) == FAILURE) {
59+
efree_size(res, sizeof(zend_resource));
60+
}
5961
}
6062

6163
static void zend_resource_dtor(zend_resource *res)
@@ -177,7 +179,11 @@ void list_entry_destructor(zval *zv)
177179

178180
ZVAL_UNDEF(zv);
179181
if (res->type >= 0) {
182+
GC_ADDREF(res);
180183
zend_resource_dtor(res);
184+
if (GC_DELREF(res) != 0) {
185+
return;
186+
}
181187
}
182188
efree_size(res, sizeof(zend_resource));
183189
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
GH-16321 (UAF when stream filter throws during stream close)
3+
--FILE--
4+
<?php
5+
class TestFilter extends php_user_filter {
6+
function filter($in, $out, &$consumed, $closing): int {
7+
stream_bucket_new($this->stream, "42");
8+
return PSFS_ERR_FATAL;
9+
}
10+
}
11+
12+
stream_filter_register("test_filter", "TestFilter");
13+
14+
function test() {
15+
$stream = fopen('php://memory', 'wb+');
16+
fwrite($stream, "data");
17+
fseek($stream, 0, SEEK_SET);
18+
stream_filter_append($stream, "test_filter");
19+
stream_get_contents($stream);
20+
}
21+
22+
test();
23+
?>
24+
--EXPECTF--
25+
Warning: stream_get_contents(): Unprocessed filter buckets remaining on input brigade in %s on line %d
26+
27+
Fatal error: Uncaught TypeError: stream_bucket_new(): Argument #1 ($stream) must be an open stream resource in %s:%d
28+
Stack trace:
29+
#0 %s(%d): stream_bucket_new(Resource id #%d, '42')
30+
#1 %s(%d): TestFilter->filter(Resource id #%d, Resource id #%d, 0, true)
31+
#2 {main}
32+
thrown in %s on line %d

0 commit comments

Comments
 (0)