Commit d48602bccf3 for php
commit d48602bccf3c070e93f8dea1c9589656d05daa0d
Merge: 9c48082e26c dfe6d76ac5c
Author: Jakub Zelenka <bukka@php.net>
Date: Sat Nov 7 10:29:28 2026 +0100
Merge branch 'PHP-8.5' into PHP-8.6
* PHP-8.5:
Fix leaks and data loss when user filter leaves buckets on input brigade
diff --cc NEWS
index 3680d673729,4f3a6607e76..f80adaa2217
--- a/NEWS
+++ b/NEWS
@@@ -37,10 -28,32 +37,12 @@@ PH
- Streams:
. Fixed bug GH-22063 (Stream filter chain UAF on self-removal during
callback). (Ilia Alshanetsky)
+ . Fixed leaks and data loss when user filter leaves buckets on input
+ brigade. (ndossche, Matthias Goergens, Jakub Zelenka)
-19 Nov 2026, PHP 8.5.13
+05 Nov 2026, PHP 8.6.0RC5
- Core:
- . Fixed bug GH-24184 (System ID not being reinitialized after an Apache
- graceful restart, potentially causing incompatible OPcache file cache
- entries to be reused). (Levi Morrison)
- . Fixed memory leak when an exception already contains the pending exception
- in its previous chain. (Edmond)
- . Fixed bug GH-23979 (Nullsafe operator must not flush delayed oplines of an
- enclosing function). (ndossche)
- . Fixed memory manager keeping a block reallocated to exactly the size of
- the next smaller bin in its larger bin, which efree_size() then freed
- into the wrong one. (Marc Bennewitz)
- . Fixed bug GH-24081 (User opcode handlers resume execution against a stale
- frame under ZEND_VM_KIND_TAILCALL). (devnexen)
- . Fixed bug GH-24218 (Invalid opcode for count() of a literal array
- without SCCP). (lazerg)
- . Fix throwing behaviour with recursive arrays in VM. (ndossche)
- . Fixed skipped catch block when switching fibers during exception unwinding
- (Nicolas Grekas)
- . Fix escape analysis for objects implementing ArrayAccess. (ndossche, Arnaud)
- . Fix UAF for resolved callable zval. (EdmondDantes)
- . Fix memory leak for nested HTTP calls. (EdmondDantes)
. Fix function and constant imports in compile_to_ast and add support
for grouping. (Klaas Skelte van der Werf)
. Fixed bug GH-24668 (zend_set_local_var() leaks the previous value of an
diff --cc ext/standard/tests/filters/stream_filter_register_class_coerce_consumed_by_ref_param.phpt
index f5f0ecca797,00000000000..6133cc2c775
mode 100644,000000..100644
--- a/ext/standard/tests/filters/stream_filter_register_class_coerce_consumed_by_ref_param.phpt
+++ b/ext/standard/tests/filters/stream_filter_register_class_coerce_consumed_by_ref_param.phpt
@@@ -1,32 -1,0 +1,30 @@@
+--TEST--
+stream_filter_register() with a class that coerces the $consumed parameter of filter method
- --XFAIL--
- This leaks memory
+--FILE--
+<?php
+
+class foo extends php_user_filter {
+ public function filter($in, $out, &$consumed, bool $closing): int {
+ $consumed = new stdClass();
+ return PSFS_PASS_ON;
+ }
+}
+
+var_dump(stream_filter_register("invalid_filter", "foo"));
+
+var_dump(stream_filter_append(STDOUT, "invalid_filter"));
+
+$out = fwrite(STDOUT, "Hello\n");
+var_dump($out);
+
+?>
+--EXPECTF--
+bool(true)
+resource(4) of type (stream filter)
+
+Warning: Object of class stdClass could not be converted to int in %s on line %d
+
+Warning: fwrite(): Unprocessed filter buckets remaining on input brigade in %s on line %d
+int(1)
+
+Warning: Object of class stdClass could not be converted to int in Unknown on line 0
diff --cc ext/standard/tests/filters/stream_filter_register_filter_always_feed.phpt
index 7a3d3f88999,00000000000..89d3f75489c
mode 100644,000000..100644
--- a/ext/standard/tests/filters/stream_filter_register_filter_always_feed.phpt
+++ b/ext/standard/tests/filters/stream_filter_register_filter_always_feed.phpt
@@@ -1,24 -1,0 +1,24 @@@
+--TEST--
+stream_filter_register() with a filter method always returning PSFS_FEED_ME
+--FILE--
+<?php
+class foo extends php_user_filter {
+ public function filter($in, $out, &$consumed, bool $closing): int {
+ return PSFS_FEED_ME;
+ }
+}
+
+var_dump(stream_filter_register("invalid_filter", "foo"));
+
+var_dump(stream_filter_append(STDOUT, "invalid_filter"));
+
+$out = fwrite(STDOUT, "Hello\n");
+var_dump($out);
+
+?>
+--EXPECTF--
+bool(true)
+resource(4) of type (stream filter)
-
- Warning: fwrite(): Unprocessed filter buckets remaining on input brigade in %s on line %d
+int(0)
++
++Warning: PHP Request Shutdown: Unprocessed filter buckets remaining on input brigade in Unknown on line 0
diff --cc ext/standard/tests/filters/stream_filter_register_mock_class_filter.phpt
index 4d2309a718e,00000000000..71fcd7f0d2e
mode 100644,000000..100644
--- a/ext/standard/tests/filters/stream_filter_register_mock_class_filter.phpt
+++ b/ext/standard/tests/filters/stream_filter_register_mock_class_filter.phpt
@@@ -1,30 -1,0 +1,28 @@@
+--TEST--
+stream_filter_register() with a class name exist that mocks php_user_filter with a filter method
- --XFAIL--
- This leaks memory
+--FILE--
+<?php
+
+class foo {
+ public $filtername;
+ public $params;
+
+ public function filter($in, $out, &$consumed, bool $closing): int {
+ return PSFS_PASS_ON;
+ }
+}
+
+var_dump(stream_filter_register("invalid_filter", "foo"));
+
+var_dump(stream_filter_append(STDOUT, "invalid_filter"));
+
+$out = fwrite(STDOUT, "Hello\n");
+var_dump($out);
+
+?>
+--EXPECTF--
+bool(true)
+resource(4) of type (stream filter)
+
+Warning: fwrite(): Unprocessed filter buckets remaining on input brigade in %s on line %d
+int(0)
diff --cc ext/standard/tests/filters/stream_filter_register_mock_class_filter_incorect_return_type.phpt
index b5ec8da3dde,00000000000..93d608b0252
mode 100644,000000..100644
--- a/ext/standard/tests/filters/stream_filter_register_mock_class_filter_incorect_return_type.phpt
+++ b/ext/standard/tests/filters/stream_filter_register_mock_class_filter_incorect_return_type.phpt
@@@ -1,32 -1,0 +1,32 @@@
+--TEST--
+stream_filter_register() with a class name exist that mocks php_user_filter with a filter method returning not an int
+--FILE--
+<?php
+
+class foo {
+ public $filtername;
+ public $params;
+
+ public function filter($in, $out, &$consumed, bool $closing) {
+ return new stdClass();
+ }
+}
+
+var_dump(stream_filter_register("invalid_filter", "foo"));
+
+var_dump(stream_filter_append(STDOUT, "invalid_filter"));
+
+$out = fwrite(STDOUT, "Hello\n");
+var_dump($out);
+
+?>
+--EXPECTF--
+bool(true)
+resource(4) of type (stream filter)
+
+Warning: Object of class stdClass could not be converted to int in %s on line %d
-
- Warning: fwrite(): Unprocessed filter buckets remaining on input brigade in %s on line %d
+int(0)
+
+Warning: Object of class stdClass could not be converted to int in Unknown on line 0
++
++Warning: PHP Request Shutdown: Unprocessed filter buckets remaining on input brigade in Unknown on line 0
diff --cc ext/standard/user_filters.c
index b699c12ed9b,d34b4f770b1..d9d65aba5e9
--- a/ext/standard/user_filters.c
+++ b/ext/standard/user_filters.c
@@@ -185,14 -160,47 +197,26 @@@ static php_stream_filter_status_t userf
uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;
- /* Give the userfilter class a hook back to the stream */
- const zend_class_entry *old_scope = EG(fake_scope);
- EG(fake_scope) = Z_OBJCE_P(obj);
-
- zend_string *stream_name = ZSTR_INIT_LITERAL("stream", 0);
- 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;
- 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)) {
- EG(fake_scope) = old_scope;
- if (buckets_in->head) {
- php_error_docref(NULL, E_WARNING, "Unprocessed filter buckets remaining on input brigade");
- userfilter_release_brigade(buckets_in);
- }
- zend_string_release(stream_name);
- stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
- stream->flags |= orig_no_fclose;
- return PSFS_ERR_FATAL;
+ zend_string *stream_name = NULL;
+ if (userfilter_assign_stream(stream, obj, &stream_name, orig_no_fclose) == FAILURE) {
+ if (buckets_in->head) {
+ php_error_docref(NULL, E_WARNING, "Unprocessed filter buckets remaining on input brigade");
++ userfilter_release_brigade(buckets_in);
}
+ return PSFS_ERR_FATAL;
}
- EG(fake_scope) = old_scope;
-
+ /* Re-present input kept back by a previous PSFS_FEED_ME */
+ if (thisfilter->buffer.head) {
+ php_stream_bucket *bucket = thisfilter->buffer.tail;
+ while (bucket) {
+ php_stream_bucket *prev = bucket->prev;
+ php_stream_bucket_unlink(bucket);
+ php_stream_bucket_prepend(buckets_in, bucket);
+ bucket = prev;
+ }
+ }
+
ZVAL_STRINGL(&func_name, "filter", sizeof("filter")-1);
/* Setup calling arguments */