From 6b8839bdf7829f568d7f8fbb23eaa36eff013ba6 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sun, 21 Jun 2026 07:53:02 -0400 Subject: [PATCH] Fix stream filter flush corrupting a partially-read buffer When a read filter is flushed (for example via stream_filter_remove()) while the stream has already been partially read, _php_stream_filter_flush() backs the unconsumed tail of the read buffer up to offset 0. Two defects in that block diverged from the equivalent code in streams.c. The copy used memcpy() on source and destination ranges that overlap whenever writepos - readpos exceeds readpos, which is undefined behavior (ASAN reports memcpy-param-overlap). And writepos was shrunk with writepos -= readpos after readpos had already been zeroed, making the subtraction a no-op, so writepos stayed inflated by readpos bytes: the stale tail was kept as live data and later flushed buckets were appended past the real end, duplicating bytes and risking an out-of-bounds write. Use memmove() and subtract before zeroing, matching streams.c. --- .../stream_filter_flush_partial_read.phpt | 33 +++++++++++++++++++ main/streams/filter.c | 4 +-- 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 ext/standard/tests/streams/stream_filter_flush_partial_read.phpt diff --git a/ext/standard/tests/streams/stream_filter_flush_partial_read.phpt b/ext/standard/tests/streams/stream_filter_flush_partial_read.phpt new file mode 100644 index 000000000000..b8abc9f4b3de --- /dev/null +++ b/ext/standard/tests/streams/stream_filter_flush_partial_read.phpt @@ -0,0 +1,33 @@ +--TEST-- +Flushing a read filter on a partially-read stream must not duplicate buffered bytes +--FILE-- +datalen; + stream_bucket_append($out, $bucket); + } + if ($closing) { + stream_bucket_append($out, stream_bucket_new($this->stream, "")); + } + return PSFS_PASS_ON; + } +} + +stream_filter_register("flushemit", "FlushEmitFilter"); + +$fp = fopen("php://memory", "r+"); +fwrite($fp, "ABCDEFGHIJKLMNOP"); +rewind($fp); + +$filter = stream_filter_append($fp, "flushemit", STREAM_FILTER_READ); +var_dump(fread($fp, 5)); +stream_filter_remove($filter); +var_dump(fread($fp, 100)); +?> +--EXPECT-- +string(5) "ABCDE" +string(18) "FGHIJKLMNOP" 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 */