Skip to content

Commit 9ac7f4e

Browse files
committed
Fix heap overflow when a read filter changes the stream's chunk size
_php_stream_fill_read_buffer() allocates chunk_buf from stream->chunk_size once, but read each chunk with the current stream->chunk_size. A read filter that calls stream_set_chunk_size() on its stream, directly or from code that runs while the filter's Fiber is suspended, makes the next read write past chunk_buf. The size is now taken once for the whole loop.
1 parent 357fcf1 commit 9ac7f4e

2 files changed

Lines changed: 43 additions & 3 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
--TEST--
2+
stream_set_chunk_size() called by a read filter
3+
--FILE--
4+
<?php
5+
class Grow extends php_user_filter {
6+
private int $seen = 0;
7+
8+
public function filter($in, $out, &$consumed, bool $closing): int {
9+
stream_set_chunk_size($this->stream, 4000000);
10+
11+
while ($bucket = stream_bucket_make_writeable($in)) {
12+
$consumed += $bucket->datalen;
13+
$this->seen += $bucket->datalen;
14+
}
15+
16+
if ($closing) {
17+
stream_bucket_append($out, stream_bucket_new($this->stream, (string) $this->seen));
18+
return PSFS_PASS_ON;
19+
}
20+
21+
return PSFS_FEED_ME;
22+
}
23+
}
24+
25+
stream_filter_register('grow', 'Grow');
26+
27+
$file = __DIR__ . '/stream_set_chunk_size_in_read_filter.tmp';
28+
file_put_contents($file, str_repeat('x', 65536));
29+
30+
$handle = fopen($file, 'r');
31+
stream_filter_append($handle, 'grow', STREAM_FILTER_READ);
32+
var_dump(fread($handle, 4096));
33+
?>
34+
--CLEAN--
35+
<?php
36+
@unlink(__DIR__ . '/stream_set_chunk_size_in_read_filter.tmp');
37+
?>
38+
--EXPECT--
39+
string(5) "65536"

‎main/streams/streams.c‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -565,13 +565,14 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size)
565565
bool old_eof = stream->eof;
566566

567567
if (stream->readfilters.head) {
568-
size_t to_read_now = MIN(size, stream->chunk_size);
568+
const size_t chunk_size = stream->chunk_size;
569+
size_t to_read_now = MIN(size, chunk_size);
569570
char *chunk_buf;
570571
php_stream_bucket_brigade brig_in = { NULL, NULL }, brig_out = { NULL, NULL };
571572
php_stream_bucket_brigade *brig_inp = &brig_in, *brig_outp = &brig_out, *brig_swap;
572573

573574
/* allocate a buffer for reading chunks */
574-
chunk_buf = emalloc(stream->chunk_size);
575+
chunk_buf = emalloc(chunk_size);
575576

576577
while (!stream->eof && (stream->writepos - stream->readpos < (zend_off_t)to_read_now)) {
577578
ssize_t justread = 0;
@@ -581,7 +582,7 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size)
581582
php_stream_filter *filter;
582583

583584
/* read a chunk into a bucket */
584-
justread = stream->ops->read(stream, chunk_buf, stream->chunk_size);
585+
justread = stream->ops->read(stream, chunk_buf, chunk_size);
585586
if (justread < 0 && stream->writepos == stream->readpos) {
586587
efree(chunk_buf);
587588
retval = FAILURE;

0 commit comments

Comments
 (0)