[php-src] PHP-8.4: Fix read buffer compaction in stream filter flush (#23439)

Georgij Tsarin via Weilin Du <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Georgij Tsarin (crystarm)
Committer: Weilin Du (LamentXU123)
Date: 2026-08-28T00:52:16+08:00

Commit: https://github.com/php/php-src/commit/79e4cd1efe9a6e17a3954d83a88d4d525d911e06
Raw diff: https://github.com/php/php-src/commit/79e4cd1efe9a6e17a3954d83a88d4d525d911e06.diff

Fix read buffer compaction in stream filter flush (#23439)

php_stream_filter_flush() compacts unread data before appending buckets
produced by a read filter. The source and destination ranges may overlap,
making memcpy() undefined behavior. Additionally, resetting readpos before
subtracting it from writepos leaves the buffer size unadjusted and stale
data visible.

Use memmove() and adjust writepos before resetting readpos, matching the
existing buffer compaction logic in php_stream_fill_read_buffer().

Closes #23439

Changed paths:
  A  ext/standard/tests/filters/stream_filter_remove_compact_read_buffer.phpt
  M  NEWS
  M  main/streams/filter.c


Diff:

diff --git a/NEWS b/NEWS
index 59517a0dd355..1a9f52b2ab91 100644
--- a/NEWS
+++ b/NEWS
@@ -63,6 +63,7 @@ PHP                                                                        NEWS
     empty Location header. (iliaal)
   . Fixed a memory leak in array_merge_recursive() when the recursive merge of
     an object converted to an array fails. (David Carlier)
+  . Fixed read buffer compaction in php_stream_filter_flush(). (crystarm)
 
 - Zip:
   . Fixed bug GH-23276 (ZipArchive subclass storing its own stream cannot be
diff --git a/ext/standard/tests/filters/stream_filter_remove_compact_read_buffer.phpt b/ext/standard/tests/filters/stream_filter_remove_compact_read_buffer.phpt
new file mode 100644
index 000000000000..7cfef971e7e7
--- /dev/null
+++ b/ext/standard/tests/filters/stream_filter_remove_compact_read_buffer.phpt
@@ -0,0 +1,31 @@
+--TEST--
+stream_filter_remove() compacts unread data before appending flushed data
+--FILE--
+<?php
+class ClosingSuffixFilter extends php_user_filter
+{
+    public function filter($in, $out, &$consumed, $closing): int
+    {
+        while ($bucket = stream_bucket_make_writeable($in)) {
+            $consumed += $bucket->datalen;
+            stream_bucket_append($out, $bucket);
+        }
+        if ($closing) {
+            stream_bucket_append($out, stream_bucket_new($this->stream, 'END'));
+        }
+        return PSFS_PASS_ON;
+    }
+}
+stream_filter_register('closing-suffix', ClosingSuffixFilter::class);
+$stream = fopen('php://memory', 'w+');
+fwrite($stream, 'abcdef');
+rewind($stream);
+$filter = stream_filter_append($stream, 'closing-suffix', STREAM_FILTER_READ);
+var_dump(fread($stream, 2));
+var_dump(stream_filter_remove($filter));
+var_dump(stream_get_contents($stream));
+?>
+--EXPECT--
+string(2) "ab"
+bool(true)
+string(7) "cdefEND"
diff --git a/main/streams/filter.c b/main/streams/filter.c
index 967be5d7f724..edf0a01e46fc 100644
--- a/main/streams/filter.c
+++ b/main/streams/filter.c
@@ -459,9 +459,9 @@ PHPAPI int _php_stream_filter_flush(php_stream_filter *filter, int finish)
 		/* Dump any newly flushed data to the read buffer */
 		if (stream->readpos > 0) {
 			/* Back the buffer up */
-			memcpy(stream->readbuf, stream->readbuf + stream->readpos, stream->writepos - stream->readpos);
-			stream->readpos = 0;
+			memmove(stream->readbuf, stream->readbuf + stream->readpos, stream->writepos - stream->readpos);
 			stream->writepos -= stream->readpos;
+			stream->readpos = 0;
 		}
 		if (flushed_size > (stream->readbuflen - stream->writepos)) {
 			/* Grow the buffer */
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.