Commit 46352f7eadd for php.net

commit 46352f7eadd0efd30d3eb3dcae17081d01ca0057
Author: David Carlier <devnexen@gmail.com>
Date:   Mon Jul 20 21:23:43 2026 +0100

    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.

diff --git a/ext/standard/tests/gh22818.phpt b/ext/standard/tests/gh22818.phpt
new file mode 100644
index 00000000000..e6ebaeecce5
--- /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--
+<?php
+
+class rotate_filter_nw extends php_user_filter
+{
+    public function filter($in, $out, &$consumed, $closing): int
+    {
+        $stream = fopen('php://memory', 'w+');
+        stream_filter_register("rotator_notWorking", rotate_filter_nw::class);
+        stream_filter_append($stream, "rotator_notWorking");
+
+        return PSFS_PASS_ON;
+    }
+}
+
+stream_filter_register("rotator_notWorking", rotate_filter_nw::class);
+
+$stream = fopen('php://memory', 'w+');
+stream_filter_append($stream, "rotator_notWorking");
+
+echo "done\n";
+?>
+--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 9711ad1f83f..d69af26912c 100644
--- a/ext/standard/user_filters.c
+++ b/ext/standard/user_filters.c
@@ -601,20 +601,21 @@ 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));
 		/* We don't need a destructor as we are only storing a CE which should be never modified */
 		zend_hash_init(BG(user_filter_map), 8, NULL, NULL, 0);
 	}

-	if (zend_hash_add_ptr(BG(user_filter_map), filtername, ce) != NULL) {
-		if (php_stream_filter_register_factory_volatile(filtername, &user_filter_factory) == SUCCESS) {
-			RETURN_TRUE;
-		}
-
-		zend_hash_del(BG(user_filter_map), filtername);
-	}
+	/* The factory has just been (re)registered, so keep the map in sync. */
+	zend_hash_update_ptr(BG(user_filter_map), filtername, ce);

-	RETURN_FALSE;
+	RETURN_TRUE;
 }
 /* }}} */