From 78251bcb23128125f7f6a15bcf68eba1189e62d4 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Mon, 20 Jul 2026 21:23:43 +0100 Subject: [PATCH 1/2] ext/standard: stream_filter_register() orphaned user_filter_map on shutdown re-registration. Fix GH-22818 During request shutdown the user_filter_map is torn down by the user_filters RSHUTDOWN before the streams referencing it are flushed. A user filter whose filter() callback re-registers the filter recreated the now-NULL map, then, since the volatile factory was still present in FG(stream_filters), deleted the freshly added entry and left an empty orphaned map behind. The following stream_filter_append() located the factory but no matching fdat, tripping ZEND_ASSERT(fdat), and the recreated map leaked. Register the volatile factory first and only create and populate user_filter_map on success, so a re-registration during the shutdown window fails without recreating the map. The existing NULL-map guard in user_filter_factory_create() then handles the append gracefully. --- ext/standard/tests/gh22818.phpt | 28 ++++++++++++++++++++++++++++ ext/standard/user_filters.c | 20 +++++++++----------- 2 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 ext/standard/tests/gh22818.phpt diff --git a/ext/standard/tests/gh22818.phpt b/ext/standard/tests/gh22818.phpt new file mode 100644 index 000000000000..e6ebaeecce5d --- /dev/null +++ b/ext/standard/tests/gh22818.phpt @@ -0,0 +1,28 @@ +--TEST-- +Bug GH-22818: user_filter_factory_create assertion failure on shutdown re-registration +--FILE-- + +--EXPECTF-- +done + +Warning: stream_filter_append(): Unable to create or locate filter "rotator_notWorking" in %s on line %d diff --git a/ext/standard/user_filters.c b/ext/standard/user_filters.c index 986bbbd5f4d3..9d249ac3abfb 100644 --- a/ext/standard/user_filters.c +++ b/ext/standard/user_filters.c @@ -618,6 +618,13 @@ PHP_FUNCTION(stream_filter_register) RETURN_THROWS(); } + + /* Register the factory first; if that fails, don't (re)create the map, + * which would leak during shutdown re-registration. */ + if (php_stream_filter_register_factory_volatile(filtername, &user_filter_factory) == FAILURE) { + RETURN_FALSE; + } + if (!BG(user_filter_map)) { BG(user_filter_map) = (HashTable*) emalloc(sizeof(HashTable)); zend_hash_init(BG(user_filter_map), 8, NULL, (dtor_func_t) filter_item_dtor, 0); @@ -626,17 +633,8 @@ PHP_FUNCTION(stream_filter_register) fdat = ecalloc(1, sizeof(struct php_user_filter_data)); fdat->classname = zend_string_copy(classname); - if (zend_hash_add_ptr(BG(user_filter_map), filtername, fdat) != NULL) { - if (php_stream_filter_register_factory_volatile(filtername, &user_filter_factory) == SUCCESS) { - RETURN_TRUE; - } - - zend_hash_del(BG(user_filter_map), filtername); - } else { - zend_string_release_ex(classname, 0); - efree(fdat); - } + zend_hash_add_ptr(BG(user_filter_map), filtername, fdat); - RETURN_FALSE; + RETURN_TRUE; } /* }}} */ From 05a1a34d3b47c07f6b4b626054d3f957778d35d7 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Thu, 30 Jul 2026 08:11:23 +0100 Subject: [PATCH 2/2] Use zend_hash_add_new_ptr() for the user_filter_map insertion. The volatile factory registration above already rejects a duplicate filter name, so the name cannot be in the map either. The add_new variant asserts that invariant in debug builds. --- ext/standard/user_filters.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ext/standard/user_filters.c b/ext/standard/user_filters.c index 9d249ac3abfb..e6f203d2006c 100644 --- a/ext/standard/user_filters.c +++ b/ext/standard/user_filters.c @@ -618,7 +618,6 @@ PHP_FUNCTION(stream_filter_register) RETURN_THROWS(); } - /* Register the factory first; if that fails, don't (re)create the map, * which would leak during shutdown re-registration. */ if (php_stream_filter_register_factory_volatile(filtername, &user_filter_factory) == FAILURE) { @@ -633,7 +632,9 @@ PHP_FUNCTION(stream_filter_register) fdat = ecalloc(1, sizeof(struct php_user_filter_data)); fdat->classname = zend_string_copy(classname); - zend_hash_add_ptr(BG(user_filter_map), filtername, fdat); + /* The factory registration above already rejected a duplicate name, so the + * filter name cannot be present in the map either. */ + zend_hash_add_new_ptr(BG(user_filter_map), filtername, fdat); RETURN_TRUE; }