From 05aaa7deb8debbece572c24834b4bf340e6e3b6f Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Sun, 20 Sep 2026 13:42:33 +0000 Subject: [PATCH 1/4] Reject trailing Brotli data to prevent infinite drain loops --- HISTORY.rst | 6 ++++++ src/brotlicffi/_api.py | 4 ++++ test/test_simple_decompression.py | 13 +++++++++++++ 3 files changed, 23 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index ecad247..4eef9ab 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,12 @@ Changelog ========= +Unreleased +---------- + +- Fixed infinite loops in streaming consumers by rejecting trailing data + after a complete Brotli stream. + 1.2.0.2 (2026-08-21) -------------------- diff --git a/src/brotlicffi/_api.py b/src/brotlicffi/_api.py index 75a5182..afcbc41 100644 --- a/src/brotlicffi/_api.py +++ b/src/brotlicffi/_api.py @@ -492,6 +492,10 @@ def _decompress(self, data, output_buffer_limit): b"Decompression error: %s" % ffi.string(error_message) ) + # Reject bytes remaining after the Brotli stream finishes. + if rc == lib.BROTLI_DECODER_RESULT_SUCCESS and available_in[0]: + raise error("Decompression error: trailing data after stream.") + # Next, copy the result out. chunk = ffi.buffer(out_buffer, buffer_size - available_out[0])[:] chunks.append(chunk) diff --git a/test/test_simple_decompression.py b/test/test_simple_decompression.py index dea8c96..2d0cc52 100644 --- a/test/test_simple_decompression.py +++ b/test/test_simple_decompression.py @@ -108,6 +108,19 @@ def test_high_expansion_prefix_without_output_buffer_limit(): assert result == uncompressed +@pytest.mark.parametrize('output_buffer_limit', [None, 50]) +def test_decompressobj_rejects_trailing_data(output_buffer_limit): + o = brotlicffi.Decompressor() + data = brotlicffi.compress(b'A' * 100) + b'tail' + if output_buffer_limit is not None: + assert o.decompress(data, output_buffer_limit=50) == b'A' * 50 + assert not o.can_accept_more_data() + data = b'' + + with pytest.raises(brotlicffi.error, match='trailing data'): + o.decompress(data, output_buffer_limit=output_buffer_limit) + + def test_drip_feed(simple_compressed_file): """ Sending in the data one byte at a time still works. From 323e43da3a38ba9785525207c24feb22e5cbaeee Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Thu, 24 Sep 2026 23:18:37 +0000 Subject: [PATCH 2/4] Clear buffered input before rejecting trailing data --- src/brotlicffi/_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/brotlicffi/_api.py b/src/brotlicffi/_api.py index afcbc41..b24a361 100644 --- a/src/brotlicffi/_api.py +++ b/src/brotlicffi/_api.py @@ -494,6 +494,7 @@ def _decompress(self, data, output_buffer_limit): # Reject bytes remaining after the Brotli stream finishes. if rc == lib.BROTLI_DECODER_RESULT_SUCCESS and available_in[0]: + self._unconsumed_data = b'' raise error("Decompression error: trailing data after stream.") # Next, copy the result out. From a19c2b96433a537e08a2f85949465873b4426b32 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Thu, 24 Sep 2026 23:18:41 +0000 Subject: [PATCH 3/4] Expand trailing data regression coverage --- test/test_simple_decompression.py | 32 ++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/test/test_simple_decompression.py b/test/test_simple_decompression.py index 2d0cc52..2276a37 100644 --- a/test/test_simple_decompression.py +++ b/test/test_simple_decompression.py @@ -108,17 +108,43 @@ def test_high_expansion_prefix_without_output_buffer_limit(): assert result == uncompressed +def test_decompression_rejects_trailing_data(): + data = brotlicffi.compress(b'A' * 100) + b'x' + with pytest.raises(brotlicffi.error, match='trailing data'): + brotlicffi.decompress(data) + + +@pytest.mark.parametrize('method', ['decompress', 'process']) @pytest.mark.parametrize('output_buffer_limit', [None, 50]) -def test_decompressobj_rejects_trailing_data(output_buffer_limit): +def test_decompressobj_rejects_trailing_data(method, output_buffer_limit): o = brotlicffi.Decompressor() + decompress = getattr(o, method) + # Without a limit, decoding needs multiple internal buffer iterations. data = brotlicffi.compress(b'A' * 100) + b'tail' if output_buffer_limit is not None: - assert o.decompress(data, output_buffer_limit=50) == b'A' * 50 + assert decompress(data, output_buffer_limit=50) == b'A' * 50 assert not o.can_accept_more_data() data = b'' with pytest.raises(brotlicffi.error, match='trailing data'): - o.decompress(data, output_buffer_limit=output_buffer_limit) + decompress(data, output_buffer_limit=output_buffer_limit) + + assert o._unconsumed_data == b'' + with pytest.raises(brotlicffi.error, match='trailing data'): + decompress(b'x', output_buffer_limit=output_buffer_limit) + + +@pytest.mark.parametrize('output_buffer_limit', [None, 100]) +def test_decompressobj_rejects_data_after_finished(output_buffer_limit): + o = brotlicffi.Decompressor() + data = brotlicffi.compress(b'A' * 100) + assert ( + o.process(data, output_buffer_limit=output_buffer_limit) == b'A' * 100 + ) + assert o.is_finished() + + with pytest.raises(brotlicffi.error, match='trailing data'): + o.process(b'x', output_buffer_limit=output_buffer_limit) def test_drip_feed(simple_compressed_file): From aad82b116d8366c8708dddabc26a6571dc2f2082 Mon Sep 17 00:00:00 2001 From: Illia Volochii Date: Thu, 24 Sep 2026 23:18:47 +0000 Subject: [PATCH 4/4] Clarify trailing data behavior in changelog --- HISTORY.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 4eef9ab..757ac7e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,8 +4,12 @@ Changelog Unreleased ---------- -- Fixed infinite loops in streaming consumers by rejecting trailing data - after a complete Brotli stream. +- Changed one-shot ``decompress()`` and streaming ``Decompressor.process()`` / + ``Decompressor.decompress()`` to raise ``brotlicffi.error`` for trailing bytes + after a complete Brotli stream, including calls without an output limit. + Previously tolerated input is now rejected, and any output produced during + the failing call is discarded. This prevents infinite drain loops in + streaming consumers. 1.2.0.2 (2026-08-21) --------------------