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