[php-src] master: [standard] Fix crash when filter callback unsets StreamBucket::$data

Ilia Alshanetsky <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Date: 2026-08-27T17:00:44-04:00

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

[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

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


Diff:

diff --git a/NEWS b/NEWS
index 31021de07015..cf28751877fa 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 000000000000..043ea5953e92
--- /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 735dd8390de8..f5e58041ca80 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);
 		}
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.