Commit 47bebf87a04 for php.net

commit 47bebf87a04485b3e7c5f22c84f5c6ae6004626d
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Mon Aug 24 11:16:48 2026 -0400

    [standard] Fix crash when filter callback unsets StreamBucket::$data

    stream_bucket_prepend()/append() assumed a successful zend_read_property()
    of StreamBucket::$data always yields a string, but for an unset typed
    property it throws while returning &EG(uninitialized_zval), so
    Z_STRLEN_P() dereferenced a NULL string pointer and crashed when the
    brigade was consumed. Reject non-string reads up front (rethrowing any
    pending exception) so the bucket is never re-attached with undefined
    data; the sibling $bucket property path is already safe because
    zend_fetch_resource_ex() rejects non-resources.

    Closes GH-23466

diff --git a/NEWS b/NEWS
index 31021de0701..cf28751877f 100644
--- a/NEWS
+++ b/NEWS
@@ -61,6 +61,8 @@ PHP                                                                        NEWS
     (Weilin Du)

 - Standard:
+  . Fixed a segfault when a stream filter callback unsets StreamBucket::$data
+    before re-attaching the bucket. (iliaal)
   . Fixed an out-of-bounds read when following a redirect response with an
     empty Location header. (iliaal)
   . Fixed a memory leak in array_merge_recursive() when the recursive merge of
diff --git a/ext/standard/tests/filters/bucket_data_unset.phpt b/ext/standard/tests/filters/bucket_data_unset.phpt
new file mode 100644
index 00000000000..043ea5953e9
--- /dev/null
+++ b/ext/standard/tests/filters/bucket_data_unset.phpt
@@ -0,0 +1,27 @@
+--TEST--
+unset(StreamBucket::$data) in filter callback must not crash when bucket is re-attached
+--FILE--
+<?php
+class MyFilter extends php_user_filter {
+    public function filter($in, $out, &$consumed, bool $closing): int {
+        while ($bucket = stream_bucket_make_writeable($in)) {
+            unset($bucket->data);
+            stream_bucket_prepend($out, $bucket);
+        }
+        return PSFS_PASS_ON;
+    }
+}
+stream_filter_register("myfilter", "MyFilter");
+$fp = fopen("php://temp", "w+");
+fwrite($fp, str_repeat("A", 100));
+rewind($fp);
+stream_filter_append($fp, "myfilter");
+try {
+    var_dump(stream_get_contents($fp));
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), "\n";
+}
+echo "DONE\n";
+--EXPECT--
+Error: Typed property StreamBucket::$data must not be accessed before initialization
+DONE
diff --git a/ext/standard/user_filters.c b/ext/standard/user_filters.c
index 735dd8390de..f5e58041ca8 100644
--- a/ext/standard/user_filters.c
+++ b/ext/standard/user_filters.c
@@ -423,7 +423,11 @@ static void php_stream_bucket_attach(int append, INTERNAL_FUNCTION_PARAMETERS)
 	}

 	if (NULL != (pzdata = zend_read_property(NULL, Z_OBJ_P(zobject), "data", sizeof("data")-1, false, &rv))) {
+		if (EG(exception)) {
+			RETURN_THROWS();
+		}
 		ZVAL_DEREF(pzdata);
+		ZEND_ASSERT(Z_TYPE_P(pzdata) == IS_STRING);
 		if (!bucket->own_buf) {
 			bucket = php_stream_bucket_make_writeable(bucket);
 		}