From 96fd23d1392e8764cd8b71eb06e1f25d1bad9688 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 5 Oct 2026 16:30:45 +0200 Subject: [PATCH] Fix stale read buffer length in stream filter flush When a read filter is flushed (e.g. via stream_filter_remove()) and the flushed data exceeds the remaining read buffer capacity, _php_stream_filter_flush() reallocates stream->readbuf but leaves stream->readbuflen at its old value. Once writepos exceeds the stale readbuflen, the next refill in _php_stream_fill_read_buffer() computes readbuflen - writepos as a wrapped size_t and passes it to the read op, causing EFAULT on plain files and out-of-bounds writes on memory streams. Keep readbuflen in sync with the reallocation, as streams.c does. --- NEWS | 4 ++ ...stream_filter_remove_grow_read_buffer.phpt | 50 +++++++++++++++++++ main/streams/filter.c | 3 +- 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 ext/standard/tests/filters/stream_filter_remove_grow_read_buffer.phpt diff --git a/NEWS b/NEWS index 9ccefd5fddbc..3bbd250f78dc 100644 --- a/NEWS +++ b/NEWS @@ -215,6 +215,10 @@ PHP NEWS lookup, and an unchecked CreateFileA() failure. (Ilia Alshanetsky) . Fix persistent stream context lifetime during shutdown (Levi Morrison) +- Streams: + . Fixed read buffer size not being updated when a flushed read filter grows + the buffer in stream_filter_remove(), causing invalid reads. (Jakub Zelenka) + - Tidy: . Fixed a use-after-free when a tidyNode is used after its document is reparsed. (Ilia Alshanetsky) diff --git a/ext/standard/tests/filters/stream_filter_remove_grow_read_buffer.phpt b/ext/standard/tests/filters/stream_filter_remove_grow_read_buffer.phpt new file mode 100644 index 000000000000..89ef04526fc1 --- /dev/null +++ b/ext/standard/tests/filters/stream_filter_remove_grow_read_buffer.phpt @@ -0,0 +1,50 @@ +--TEST-- +stream_filter_remove() keeps readbuflen in sync when growing the read buffer +--FILE-- +datalen; + stream_bucket_append($out, $bucket); + } + if ($closing) { + stream_bucket_append($out, stream_bucket_new($this->stream, str_repeat('F', 20000))); + } + return PSFS_PASS_ON; + } +} +stream_filter_register('closing-suffix', ClosingSuffixFilter::class); + +/* The file must be larger than one chunk so that the stream is not at EOF + * when the filter is removed and a refill of the read buffer happens later. */ +$file = __DIR__ . '/stream_filter_remove_grow_read_buffer.tmp'; +$content = str_repeat('a', 100) . str_repeat('b', 100000); +file_put_contents($file, $content); + +$stream = fopen($file, 'r'); +$filter = stream_filter_append($stream, 'closing-suffix', STREAM_FILTER_READ); +var_dump(fread($stream, 100) === str_repeat('a', 100)); +/* The flushed 20000 bytes exceed the remaining read buffer capacity, so the buffer is grown */ +var_dump(stream_filter_remove($filter)); +$rest = stream_get_contents($stream); +var_dump(strlen($rest)); +/* Data already buffered before the flush comes first, then the flushed data, then the rest of the file */ +var_dump(preg_match('/^b*F{20000}b*$/', $rest)); +var_dump(substr_count($rest, 'b')); +var_dump(feof($stream)); +fclose($stream); +?> +--CLEAN-- + +--EXPECT-- +bool(true) +bool(true) +int(120000) +int(1) +int(100000) +bool(true) diff --git a/main/streams/filter.c b/main/streams/filter.c index edf0a01e46fc..66c82d8a3d53 100644 --- a/main/streams/filter.c +++ b/main/streams/filter.c @@ -465,7 +465,8 @@ PHPAPI int _php_stream_filter_flush(php_stream_filter *filter, int finish) } if (flushed_size > (stream->readbuflen - stream->writepos)) { /* Grow the buffer */ - stream->readbuf = perealloc(stream->readbuf, stream->writepos + flushed_size + stream->chunk_size, stream->is_persistent); + stream->readbuflen = stream->writepos + flushed_size + stream->chunk_size; + stream->readbuf = perealloc(stream->readbuf, stream->readbuflen, stream->is_persistent); } while ((bucket = inp->head)) { memcpy(stream->readbuf + stream->writepos, bucket->buf, bucket->buflen);