From 7ab6d4cb8571316d318332b3a6a814d2c7b5f2f0 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+ndossche@users.noreply.github.com> Date: Sat, 15 Nov 2025 13:40:20 +0100 Subject: [PATCH 1/2] Fix GH-20286: use-after-destroy during userland stream_close() Co-authored-by: David Carlier --- Zend/zend_list.c | 15 +++++++++- ext/standard/tests/streams/gh20286.phpt | 39 +++++++++++++++++++++++++ main/streams/userspace.c | 1 + 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 ext/standard/tests/streams/gh20286.phpt diff --git a/Zend/zend_list.c b/Zend/zend_list.c index 5add19256a691..10aa9174cfccb 100644 --- a/Zend/zend_list.c +++ b/Zend/zend_list.c @@ -214,21 +214,34 @@ void zend_init_rsrc_plist(void) void zend_close_rsrc_list(HashTable *ht) { - /* Reload ht->arData on each iteration, as it may be reallocated. */ uint32_t i = ht->nNumUsed; + uint32_t num = ht->nNumUsed; retry: zend_try { while (i-- > 0) { + /* Reload ht->arData on each iteration, as it may be reallocated. */ zval *p = ZEND_HASH_ELEMENT(ht, i); if (Z_TYPE_P(p) != IS_UNDEF) { zend_resource *res = Z_PTR_P(p); if (res->type >= 0) { zend_resource_dtor(res); + + if (UNEXPECTED(ht->nNumUsed != num)) { + /* New resources were added, reloop from the start. + * We need to keep the top->down order to avoid freeing resources + * in use by the newly created resources. */ + i = num = ht->nNumUsed; + } } } } } zend_catch { + if (UNEXPECTED(ht->nNumUsed != num)) { + /* See above */ + i = num = ht->nNumUsed; + } + /* If we have bailed, we probably executed user code (e.g. user stream * API). Keep closing resources so they don't leak. User handlers must be * called now so they aren't called in zend_deactivate() on diff --git a/ext/standard/tests/streams/gh20286.phpt b/ext/standard/tests/streams/gh20286.phpt new file mode 100644 index 0000000000000..c72a1f1d9ef44 --- /dev/null +++ b/ext/standard/tests/streams/gh20286.phpt @@ -0,0 +1,39 @@ +--TEST-- +GH-20286 use after destroy on userland stream_close +--CREDITS-- +vi3tL0u1s +--FILE-- + +--EXPECTF-- +Fatal error: Cannot redeclare a() (previously declared in %s:%d) in %s on line %d + +Fatal error: Cannot redeclare a() (previously declared in %s on line %d + +Fatal error: Cannot redeclare a() (previously declared in %s on line %d + +Fatal error: Cannot redeclare a() (previously declared in %s on line %d diff --git a/main/streams/userspace.c b/main/streams/userspace.c index 8d15172ef1319..ba66d32465977 100644 --- a/main/streams/userspace.c +++ b/main/streams/userspace.c @@ -254,6 +254,7 @@ typedef struct _php_userstream_data php_userstream_data_t; static zend_result call_method_if_exists( zval *object, zval *method_name, zval *retval, uint32_t param_count, zval *params) { + ZEND_ASSERT(EG(active)); return zend_call_method_if_exists( Z_OBJ_P(object), Z_STR_P(method_name), retval, param_count, params); } From ef9b89ba1e2002737a9aaf3b332b5888543bb1b0 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+ndossche@users.noreply.github.com> Date: Sat, 15 Nov 2025 14:32:03 +0100 Subject: [PATCH 2/2] Use same SKIPIF as GH-19844 --- ext/standard/tests/streams/gh20286.phpt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ext/standard/tests/streams/gh20286.phpt b/ext/standard/tests/streams/gh20286.phpt index c72a1f1d9ef44..b4d340a4390f0 100644 --- a/ext/standard/tests/streams/gh20286.phpt +++ b/ext/standard/tests/streams/gh20286.phpt @@ -2,6 +2,10 @@ GH-20286 use after destroy on userland stream_close --CREDITS-- vi3tL0u1s +--SKIPIF-- + --FILE--