Commit b63b651bdfd for php

commit b63b651bdfd2221dfd352f23dd7357b6b011c557
Merge: 0e66515c856 cb7612f76d2
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Fri Nov 6 17:04:43 2026 -0500

    Merge branch 'PHP-8.5' into PHP-8.6

    * PHP-8.5:
      Fix GH-22063: Stream filter chain UAF on self-removal during callback

    PHP-8.6 added filter seek(), so php_stream_filters_seek() gets the same
    removal bracket as the filter chain walks.

diff --cc NEWS
index 6b0564e2f0c,ac9a1213090..3680d673729
--- a/NEWS
+++ b/NEWS
@@@ -28,15 -25,33 +28,19 @@@ PH
      or never terminates). (Ilia Alshanetsky)
    . Fixed memory leak in JIT ASSIGN_DIM when the assignment fails. (Jorg Sowa)

 +- OpenSSL:
 +  . Fixed bug GH-25172 (TLS session reuse shares request-lifetime callback
 +    structures with persistent streams). (cxxz16, David Carlier)
 +  . Fixed an exception thrown by the session_get_cb stream context callback
 +    being replaced with a TypeError. (Ilia Alshanetsky)
 +
+ - Streams:
+   . Fixed bug GH-22063 (Stream filter chain UAF on self-removal during
+     callback). (Ilia Alshanetsky)
+
 -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/streams/gh22063_seek.phpt
index 00000000000,00000000000..d65e211cbac
new file mode 100644
--- /dev/null
+++ b/ext/standard/tests/streams/gh22063_seek.phpt
@@@ -1,0 -1,0 +1,33 @@@
++--TEST--
++GH-22063 (stream filter self-removal denied during a seek() callback)
++--FILE--
++<?php
++class SeekRemover extends php_user_filter {
++    public function filter($in, $out, &$consumed, $closing): int {
++        while ($b = stream_bucket_make_writeable($in)) {
++            stream_bucket_append($out, $b);
++        }
++        return PSFS_PASS_ON;
++    }
++    public function seek(int $offset, int $whence, int $chain): bool {
++        var_dump(stream_filter_remove($GLOBALS['f']));
++        return true;
++    }
++}
++stream_filter_register('seekremover', SeekRemover::class);
++
++$fp = fopen('php://memory', 'r+');
++fwrite($fp, 'hello world data here');
++rewind($fp);
++$GLOBALS['f'] = stream_filter_append($fp, 'seekremover', STREAM_FILTER_READ);
++var_dump(fread($fp, 4));
++var_dump(fseek($fp, 0));
++echo "done\n";
++?>
++--EXPECTF--
++string(4) "hell"
++
++Warning: stream_filter_remove(): Cannot remove filter while it is being applied in %s on line %d
++bool(false)
++int(0)
++done
diff --cc main/streams/streams.c
index 3c482f4cd52,648e1bda3d8..a83e5d6042b
--- a/main/streams/streams.c
+++ b/main/streams/streams.c
@@@ -1302,64 -1415,7 +1310,72 @@@ PHPAPI zend_off_t php_stream_tell(cons
  	return stream->position;
  }

 -PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence)
 +static bool php_stream_are_filters_seekable(php_stream_filter *filter, bool is_start_seeking, int chain_type)
 +{
 +	while (filter) {
 +		php_stream_filter_seekable_t seekable = (chain_type == PHP_STREAM_FILTER_READ) ?
 +				filter->read_seekable : filter->write_seekable;
 +		if (seekable == PSFS_SEEKABLE_NEVER) {
 +			php_error_docref(NULL, E_WARNING, "Stream filter %s is never seekable", filter->fops->label);
 +			return false;
 +		}
 +		if (!is_start_seeking && seekable == PSFS_SEEKABLE_START) {
 +			php_error_docref(NULL, E_WARNING, "Stream filter %s is seekable only to start position", filter->fops->label);
 +			return false;
 +		}
 +		filter = filter->next;
 +	}
 +	return true;
 +}
 +
 +static zend_result php_stream_filters_seek(php_stream *stream, php_stream_filter *filter,
 +		bool is_start_seeking, zend_off_t offset, int whence, int chain_type)
 +{
++	uint32_t no_remove_flag = chain_type == PHP_STREAM_FILTER_READ ?
++			PHP_STREAM_FLAG_NO_READ_FILTER_REMOVE : PHP_STREAM_FLAG_NO_WRITE_FILTER_REMOVE;
++	uint32_t orig_no_remove = stream->flags & no_remove_flag;
++	stream->flags |= no_remove_flag;
 +	while (filter) {
 +		php_stream_filter_seekable_t seekable = (chain_type == PHP_STREAM_FILTER_READ) ?
 +				filter->read_seekable : filter->write_seekable;
 +		if (((seekable == PSFS_SEEKABLE_START && is_start_seeking) ||
 +				seekable == PSFS_SEEKABLE_CHECK) &&
 +				filter->fops->seek(stream, filter, offset, whence) == FAILURE) {
 +			php_error_docref(NULL, E_WARNING, "Stream filter seeking for %s failed", filter->fops->label);
++			stream->flags &= ~no_remove_flag;
++			stream->flags |= orig_no_remove;
 +			return FAILURE;
 +		}
 +		filter = filter->next;
 +	}
++	stream->flags &= ~no_remove_flag;
++	stream->flags |= orig_no_remove;
 +	return SUCCESS;
 +}
 +
 +static zend_result php_stream_filters_seek_all(php_stream *stream, bool is_start_seeking,
 +		zend_off_t offset, int whence)
 +{
 +	if (php_stream_filters_seek(stream, stream->writefilters.head, is_start_seeking,
 +			offset, whence, PHP_STREAM_FILTER_WRITE) == FAILURE) {
 +		return FAILURE;
 +	}
 +	if (php_stream_filters_seek(stream, stream->readfilters.head, is_start_seeking,
 +			offset, whence, PHP_STREAM_FILTER_READ) == FAILURE) {
 +		return FAILURE;
 +	}
 +
 +	return SUCCESS;
 +}
 +
 +static bool php_stream_has_notifier(php_stream *stream)
 +{
 +	php_stream_context *context = PHP_STREAM_CONTEXT(stream);
 +	/* The fd-level copy cannot emit progress notifications. */
 +	return context && context->notifier;
 +}
 +
 +PHPAPI int php_stream_seek(php_stream *stream, zend_off_t offset, int whence)
  {
  	if (stream->fclose_stdiocast == PHP_STREAM_FCLOSE_FOPENCOOKIE) {
  		/* flush can call seek internally so we need to prevent an infinite loop */