[php-src] master: Merge branch 'PHP-8.5'

Ilia Alshanetsky <[email protected]> Fri, 24 Jul 2026 15:35:17 +0000
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Date: 2026-07-24T11:30:37-04:00

Commit: https://github.com/php/php-src/commit/e9ee4c0b9f5b6f6028187ab87236f2f5fee6fc65
Raw diff: https://github.com/php/php-src/commit/e9ee4c0b9f5b6f6028187ab87236f2f5fee6fc65.diff

Merge branch 'PHP-8.5'

* PHP-8.5:
  Don't expose a freed stream resource to user filters

Changed paths:
  A  ext/standard/tests/filters/gh15836.phpt
  M  NEWS
  M  ext/standard/tests/filters/bug54350.phpt
  M  ext/standard/user_filters.c


Diff:

diff --git a/NEWS b/NEWS
index b2f7d0220a86..244ea8ea94f6 100644
--- a/NEWS
+++ b/NEWS
@@ -67,6 +67,8 @@ PHP                                                                        NEWS
     TransmitFile) for faster stream copying. (Jakub Zelenka, David Carlier)
   . Fixed bug GH-22841 (php_stream_copy_to_stream_ex() drops progress
     notifications when using the copy fast path). (David Carlier)
+  . Fixed bug GH-15836 (Use-after-free when a user stream filter accesses
+    $this->stream during the close flush). (iliaal)
 
 16 Jul 2026, PHP 8.6.0alpha2
 
diff --git a/ext/standard/tests/filters/bug54350.phpt b/ext/standard/tests/filters/bug54350.phpt
index 046db0483a9a..ec3cd857f1a0 100644
--- a/ext/standard/tests/filters/bug54350.phpt
+++ b/ext/standard/tests/filters/bug54350.phpt
@@ -23,4 +23,4 @@ fwrite($fd, "foo");
 ?>
 --EXPECTF--
 Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d
-fclose(): Argument #1 ($stream) must be an open stream resource
+fclose(): Argument #1 ($stream) must be of type resource, null given
diff --git a/ext/standard/tests/filters/gh15836.phpt b/ext/standard/tests/filters/gh15836.phpt
new file mode 100644
index 000000000000..593b93dcdc70
--- /dev/null
+++ b/ext/standard/tests/filters/gh15836.phpt
@@ -0,0 +1,34 @@
+--TEST--
+GH-15836 (use-after-free when a user filter reads $this->stream during the close flush)
+--FILE--
+<?php
+class my_filter extends php_user_filter {
+    public static ?Throwable $e = null;
+    function filter($in, $out, &$consumed, $closing): int {
+        if ($closing) {
+            try {
+                stream_bucket_new($this->stream, "x");
+            } catch (TypeError $e) {
+                self::$e = $e;
+            }
+        }
+        return PSFS_PASS_ON;
+    }
+}
+var_dump(stream_filter_register("my_filter", "my_filter"));
+
+function run() {
+    $s = fopen("php://memory", "wb+");
+    stream_filter_append($s, "my_filter", STREAM_FILTER_WRITE);
+}
+run();
+
+echo my_filter::$e->getTraceAsString(), "\n";
+echo "done\n";
+?>
+--EXPECTF--
+bool(true)
+#0 %s(%d): stream_bucket_new(NULL, 'x')
+#1 %s(%d): my_filter->filter(Resource id #%d, Resource id #%d, 0, true)
+#2 {main}
+done
diff --git a/ext/standard/user_filters.c b/ext/standard/user_filters.c
index 986bbbd5f4d3..126b434713ad 100644
--- a/ext/standard/user_filters.c
+++ b/ext/standard/user_filters.c
@@ -143,7 +143,11 @@ static zend_result userfilter_assign_stream(php_stream *stream, zval *obj,
 	bool stream_property_exists = Z_OBJ_HT_P(obj)->has_property(Z_OBJ_P(obj), stream_name, ZEND_PROPERTY_EXISTS, NULL);
 	if (stream_property_exists) {
 		zval stream_zval;
-		php_stream_to_zval(stream, &stream_zval);
+		if (EXPECTED(stream->res && stream->res->type >= 0)) {
+			php_stream_to_zval(stream, &stream_zval);
+		} else {
+			ZVAL_NULL(&stream_zval);
+		}
 		zend_update_property_ex(Z_OBJCE_P(obj), Z_OBJ_P(obj), stream_name, &stream_zval);
 		/* If property update threw an exception, skip filter execution */
 		if (EG(exception)) {