Skip to content

Make deflateBound() account for a preset dictionary set after the call - #1308

Open
XananasX7 wants to merge 1 commit into
madler:developfrom
XananasX7:fix-deflatebound-dict
Open

Make deflateBound() account for a preset dictionary set after the call#1308
XananasX7 wants to merge 1 commit into
madler:developfrom
XananasX7:fix-deflatebound-dict

Conversation

@XananasX7

Copy link
Copy Markdown

Summary

deflateBound() can return a bound that is four bytes too small, breaking the single-pass guarantee documented in zlib.h, when deflateSetDictionary() is called after deflateBound() and before the first deflate().

The documented contract (zlib.h)

deflateBound() returns an upper bound on the compressed size after deflation of sourceLen bytes. It must be called after deflateInit() or deflateInit2(), and after deflateSetHeader(), if used. ... If that first deflate() call is provided the sourceLen input bytes, an output buffer allocated to the size returned by deflateBound(), and the flush value Z_FINISH, then deflate() is guaranteed to return Z_STREAM_END.

deflateSetDictionary()'s documentation only requires that it be called after deflateInit()/deflateInit2() and before deflate() — no ordering relative to deflateBound() is stated. So init → bound → setDictionary → single-pass finish is a fully legal sequence.

Root cause

When a preset dictionary is in use, the emitted zlib header carries the PRESET_DICT flag followed by the four bytes of the dictionary's Adler-32 (deflate.c header emission). deflateBound_z() accounts for this only via wraplen = 6 + (s->strstart ? 4 : 0):

  • dictionary set before deflateBound()s->strstart is non-zero → the +4 is included. Correct.
  • dictionary set after deflateBound() (legal per the docs) → at bound time s->strstart == 0 and the stream has not emitted its header → +4 is not included → the bound is short by exactly the four dictionary-Adler bytes that the later header will carry.

Impact

A caller that allocates exactly deflateBound() bytes and relies on the documented one-call Z_STREAM_END guarantee gets Z_OK with avail_out == 0 instead — the output buffer fills up four bytes early, and the stream is silently truncated (missing the dictionary Adler-32 and the first bytes of compressed data). Downstream decoders either fail or, for streams where the truncation lands on a block boundary, produce a subtly different stream.

Verified on current develop (e3dc0a8): for incompressible input of 1–100,000 bytes with dictionary lengths 1–30000, every case returned Z_OK with the buffer full instead of Z_STREAM_END (35/35 configurations).

Fix

Include the four preset-dictionary bytes whenever the header has not yet been emitted — i.e. when no data has been compressed yet and the stream status is still INIT_STATE — not only when a dictionary has already been loaded:

wraplen = 6 + (s->strstart || s->status == INIT_STATE ? 4 : 0);

Once the header has been emitted (status past INIT_STATE), the dictionary Adler-32 is already part of the accounted output, so no double counting occurs. The bound becomes conservative by at most four bytes for streams that never use a preset dictionary.

Verification

  • Reproducer: deflateInitdeflateBound(n)deflateSetDictionary → one-shot deflate(Z_FINISH) with avail_out = bound. Before: Z_OK, buffer full (35/35 cases). After: Z_STREAM_END in all cases.
  • make test passes on the patched tree.

zlib.h documents that deflateBound() returns an upper bound on the
compressed size such that a first deflate() call given all of the input
at once, Z_FINISH, and an output buffer of that bound, is guaranteed to
return Z_STREAM_END.  deflateSetDictionary() may legally be called after
deflateBound() and before that first deflate() (its only requirement is
that it precede any deflate() call).  When it is, the emitted zlib header
carries the PRESET_DICT flag followed by the four bytes of the
dictionary's Adler-32 (deflate.c, header emission), but deflateBound()
only included those four bytes when s->strstart was already non-zero at
bound time, i.e. when the dictionary had been set before deflateBound().

Callers following the documented single-pass pattern therefore received a
bound four bytes too small: the first deflate() returned Z_OK with the
output buffer full instead of Z_STREAM_END, truncating the stream (the
dictionary Adler-32 and the first bytes of compressed data were lost).
The compressed stream was still syntactically valid deflate but was no
longer the Adler-32-verified stream the caller expected.

Fix: include the four-byte preset-dictionary Adler-32 whenever the zlib
header has not yet been emitted (stream status is still INIT_STATE), not
only when a dictionary has already been loaded.  Once the header has been
written the bytes are already accounted for in the output.  This makes
the bound conservative by at most four bytes for streams that never use a
dictionary.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant