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 */