From 10f928b24fd2123834a22ae8c51f1d718258ade1 Mon Sep 17 00:00:00 2001 From: zeynallow Date: Fri, 23 Oct 2026 17:16:57 +0400 Subject: [PATCH] Fix leak of stream buckets left behind by misbehaving user filters When a filter returns PSFS_PASS_ON without consuming its input brigade, the read and write filter chains reset that brigade with memset() and leak the unconsumed buckets. When a write filter returns PSFS_FEED_ME or PSFS_ERR_FATAL after appending output buckets, those were leaked too. Discard leftover buckets in both cases. --- NEWS | 3 + .../user_filter_unconsumed_buckets_leak.phpt | 55 +++++++++++++++++++ main/streams/streams.c | 32 +++++++---- 3 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 ext/standard/tests/filters/user_filter_unconsumed_buckets_leak.phpt diff --git a/NEWS b/NEWS index 8bd02dc6d6ac..64d039baf8e4 100644 --- a/NEWS +++ b/NEWS @@ -141,6 +141,9 @@ PHP NEWS - Streams: . Fixed bug GH-24508 (HTTP wrapper leaks the dechunk filter when header parsing fails after Transfer-Encoding). (bercedev) + . Fixed memory leak of stream buckets left behind by user filters that do + not consume their input brigade or return PSFS_FEED_ME with output. + (zeynallow) - Tidy: . Fixed tidy_repair_string(), tidy_repair_file(), tidy::repairString() and diff --git a/ext/standard/tests/filters/user_filter_unconsumed_buckets_leak.phpt b/ext/standard/tests/filters/user_filter_unconsumed_buckets_leak.phpt new file mode 100644 index 000000000000..acf328c53c29 --- /dev/null +++ b/ext/standard/tests/filters/user_filter_unconsumed_buckets_leak.phpt @@ -0,0 +1,55 @@ +--TEST-- +User filters leaving buckets behind on the input or output brigade must not leak them +--FILE-- + +--EXPECTF-- +Read filter, PSFS_PASS_ON without consuming input: + +Warning: fread(): Unprocessed filter buckets remaining on input brigade in %s on line %d +string(0) "" +Write filter, PSFS_PASS_ON without consuming input: + +Warning: fwrite(): Unprocessed filter buckets remaining on input brigade in %s on line %d +int(0) +Write filter, PSFS_FEED_ME with buckets on output: +int(0) diff --git a/main/streams/streams.c b/main/streams/streams.c index b76c2531d943..eb57c131fc2d 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -557,6 +557,16 @@ fprintf(stderr, "stream_free: %s:%p[%s] preserve_handle=%d release_cast=%d remov /* {{{ generic stream operations */ +static void php_stream_bucket_brigade_discard(php_stream_bucket_brigade *brigade) +{ + php_stream_bucket *bucket; + + while ((bucket = brigade->head)) { + php_stream_bucket_unlink(bucket); + php_stream_bucket_delref(bucket); + } +} + PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size) { /* allocate/fill the buffer */ @@ -609,8 +619,10 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size) } /* brig_out becomes brig_in. - * brig_in will always be empty here, as the filter MUST attach any un-consumed buckets - * to its own brigade */ + * brig_in should be empty here, as the filter MUST attach any un-consumed buckets + * to its own brigade. A misbehaving filter may leave buckets behind; discard them + * so they are not leaked when the brigade is reset below. */ + php_stream_bucket_brigade_discard(brig_inp); brig_swap = brig_inp; brig_inp = brig_outp; brig_outp = brig_swap; @@ -1289,8 +1301,10 @@ static ssize_t _php_stream_write_filtered(php_stream *stream, const char *buf, s break; } /* brig_out becomes brig_in. - * brig_in will always be empty here, as the filter MUST attach any un-consumed buckets - * to its own brigade */ + * brig_in should be empty here, as the filter MUST attach any un-consumed buckets + * to its own brigade. A misbehaving filter may leave buckets behind; discard them + * so they are not leaked when the brigade is reset below. */ + php_stream_bucket_brigade_discard(brig_inp); brig_swap = brig_inp; brig_inp = brig_outp; brig_outp = brig_swap; @@ -1325,12 +1339,10 @@ static ssize_t _php_stream_write_filtered(php_stream *stream, const char *buf, s case PSFS_FEED_ME: /* need more data before we can push data through to the stream */ - /* Filter could have added buckets anyway, but signalled that it did not return any. Discard them. */ - while (brig_inp->head) { - bucket = brig_inp->head; - php_stream_bucket_unlink(bucket); - php_stream_bucket_delref(bucket); - } + /* Filter could have added buckets anyway, but signalled that it did not return any. Discard them, + * together with any input buckets it did not consume. */ + php_stream_bucket_brigade_discard(brig_inp); + php_stream_bucket_brigade_discard(brig_outp); break; }