Commit 79e4cd1efe9 for php.net
commit 79e4cd1efe9a6e17a3954d83a88d4d525d911e06
Author: Georgij Tsarin <nullderef@duck.com>
Date: Fri Aug 28 00:44:10 2026 +0800
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
diff --git a/NEWS b/NEWS
index 59517a0dd35..1a9f52b2ab9 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 00000000000..7cfef971e7e
--- /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 967be5d7f72..edf0a01e46f 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 */