Skip to content

gh-160001: Honor the zdict of zlib._ZlibDecompressor on zlib-wrapped streams - #161365

Open
compiler-segfault wants to merge 1 commit into
python:mainfrom
compiler-segfault:fix/zlib-decompressor-zdict
Open

compiler-segfault wants to merge 1 commit into
python:mainfrom
compiler-segfault:fix/zlib-decompressor-zdict

Conversation

@compiler-segfault

Copy link
Copy Markdown

zlib._ZlibDecompressor(zdict=...) only installs its dictionary in the
constructor, and the constructor only does that for raw deflate (wbits < 0).
For a zlib-wrapped stream zlib reports Z_NEED_DICT from inflate() after it
has read the header, and decompress_buf() handled that return code by
jumping to error without setting an exception. The method therefore returned
NULL with no exception set:

>>> import zlib
>>> zd = b"hello world " * 10
>>> c = zlib.compressobj(zdict=zd)
>>> s = c.compress(b"hello world hello") + c.flush()
>>> zlib._ZlibDecompressor(zdict=zd).decompress(s)
SystemError: <method 'decompress' of 'zlib._ZlibDecompressor' objects> returned NULL without setting an exception

This applies the dictionary from the decompression loop when one was supplied,
and raises zlib.error when the stream asks for a dictionary the caller never
provided -- which is what zlib.Decompress and zlib.decompressobj already
do for the same input.

The error path also covers the FDICT-only trigger from the comments: an 8-byte
zlib header with the FDICT flag set used to raise SystemError (and trip the
fatal assertion in ceval.c on a debug build) even when no zdict was passed,
because self->zdict == NULL was not distinguished from a dictionary that
still has to be installed.

Tests added to Lib/test/test_zlib.py:

  • testDecompressZdictWrapped and testDecompressZdictWrappedChunks: the
    zdict is honored for zlib-wrapped streams, including input fed three bytes
    at a time so the Z_NEED_DICT return happens on a later call than the first.
  • testDecompressZdictRaw: raw deflate continues to work.
  • testDecompressFdictHeaderWithoutZdict: the FDICT header without a zdict
    raises zlib.error instead of SystemError.

./python -m test test_zlib passes. With the C change reverted,
testDecompressZdictWrapped, testDecompressZdictWrappedChunks and
testDecompressFdictHeaderWithoutZdict fail, while testDecompressZdictRaw
still passes.

@python-cla-bot

python-cla-bot Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

… streams

_ZlibDecompressor only installed its dictionary in the constructor, which
covers raw deflate (wbits < 0).  For a zlib-wrapped stream zlib reports
Z_NEED_DICT from inflate() once it has read the header, and decompress_buf()
treated that as an error path that never set an exception, so the method
returned NULL and the caller got a SystemError.

Install the dictionary from the decompression loop when one was supplied, and
raise zlib.error when the stream requires a dictionary that was never given,
matching what zlib.Decompress and zlib.decompressobj do for the same input.
@compiler-segfault
compiler-segfault force-pushed the fix/zlib-decompressor-zdict branch from be1f5d6 to 30b2efc Compare November 4, 2026 23:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant