You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
zlib.decompressobj: decompress(b"", n) and flush() never return output that inflate still holds (raw deflate loses its last bytes under max_length) #8907
crates/common/src/compression/zlib.rs on main (a7d75d25a7b2c291720411dc65445accd972e69d,
2026-09-29) is byte-identical to d31ccae; the line numbers below hold for both.
all input consumed: 1100 of 1168, eof False
decompress(b'', 100): 68
flush(): 0 total: 1168
RustPython d31ccae:
all input consumed: 1100 of 1168, eof False
decompress(b'', 100): 0
flush(): 0 total: 1100
The last 68 bytes stay inside the inflate state. The input is all consumed, so unconsumed_tail is
empty, and neither decompress(b"", n) nor flush() runs inflate again.
Frequency, the same drain loop plus flush(), sizes n in range(1000, 3000, 7), max_length
in (64, 100, 256, 1024) (1 144 cases):
raw deflate (wbits=-15) short
zlib (15) and gzip (31) short
CPython 3.13.5
0 / 1 144
0 / 2 288
RustPython d31ccae
196 / 1 144
0 / 2 288
With a zlib or gzip wrapper the trailer bytes are still unconsumed while output is pending, so unconsumed_tail is non-empty and the loop calls inflate again. Raw deflate has no trailer, so
the pending output can outlive the last input byte.
Decompressor::decompress (line 690) passes b"" through decompress_inner → decompress_all → decompress_chunks, which returns before it calls inflate. Decompressor::flush (lines 701–709) takes unconsumed_tail, which is empty here, and reaches
the same early return, so the InflateFlush::Finish call never runs either. CPython calls inflate
for an empty input and returns the pending output.
A fix has to keep the existing unit test empty_input_does_not_finish_a_stream (line 1000):
an empty input before any compressed data MUST NOT set eof. #8752 aligned lzma to this same early
return for that reason.
Impact
Any consumer that decodes raw deflate with a bounded max_length and drains with decompress(b"", n) or flush() gets a truncated result with no error. One example is HTTP Content-Encoding: deflate sent as raw deflate, which urllib3's DeflateDecoder accepts after its
first zlib-header attempt fails. I have not measured urllib3 end to end for this case.
The reproducers above were run locally on RustPython and CPython.
🤖 Written by Claude Code (2.1.283) using model claude-opus-5-5
Activity
zlib: return output inflate still holds after the last input byte
zlib: return output inflate still holds after the last input byte