From 863ed297346e9cfe72594cb1f658c47415f7c770 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 9 Apr 2026 08:08:46 -0400 Subject: [PATCH] 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 --- Zend/zend_list.c | 8 ++++++- ext/standard/tests/filters/gh16321.phpt | 32 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 ext/standard/tests/filters/gh16321.phpt diff --git a/Zend/zend_list.c b/Zend/zend_list.c index 10aa9174cfccb..3a60eb0fa8434 100644 --- a/Zend/zend_list.c +++ b/Zend/zend_list.c @@ -55,7 +55,9 @@ ZEND_API zend_result ZEND_FASTCALL zend_list_delete(zend_resource *res) ZEND_API void ZEND_FASTCALL zend_list_free(zend_resource *res) { ZEND_ASSERT(GC_REFCOUNT(res) == 0); - zend_hash_index_del(&EG(regular_list), res->handle); + if (zend_hash_index_del(&EG(regular_list), res->handle) == FAILURE) { + efree_size(res, sizeof(zend_resource)); + } } static void zend_resource_dtor(zend_resource *res) @@ -177,7 +179,11 @@ void list_entry_destructor(zval *zv) ZVAL_UNDEF(zv); if (res->type >= 0) { + GC_ADDREF(res); zend_resource_dtor(res); + if (GC_DELREF(res) != 0) { + return; + } } efree_size(res, sizeof(zend_resource)); } diff --git a/ext/standard/tests/filters/gh16321.phpt b/ext/standard/tests/filters/gh16321.phpt new file mode 100644 index 0000000000000..59e8e3ef35294 --- /dev/null +++ b/ext/standard/tests/filters/gh16321.phpt @@ -0,0 +1,32 @@ +--TEST-- +GH-16321 (UAF when stream filter throws during stream close) +--FILE-- +stream, "42"); + return PSFS_ERR_FATAL; + } +} + +stream_filter_register("test_filter", "TestFilter"); + +function test() { + $stream = fopen('php://memory', 'wb+'); + fwrite($stream, "data"); + fseek($stream, 0, SEEK_SET); + stream_filter_append($stream, "test_filter"); + stream_get_contents($stream); +} + +test(); +?> +--EXPECTF-- +Warning: stream_get_contents(): Unprocessed filter buckets remaining on input brigade in %s on line %d + +Fatal error: Uncaught TypeError: stream_bucket_new(): Argument #1 ($stream) must be an open stream resource in %s:%d +Stack trace: +#0 %s(%d): stream_bucket_new(Resource id #%d, '42') +#1 %s(%d): TestFilter->filter(Resource id #%d, Resource id #%d, 0, true) +#2 {main} + thrown in %s on line %d