Browse zstd specs
Proofs / zstd
zstd's decoder
- specs
- 44
- zstd
- 01b7154f
- lines of Rocq
- ~430,000
- qp verify
- PASS
On this page
We proved 44 C specs of zstd's decoder. Each spec states a property, and each has a theorem in Rocq (formerly Coq). Twenty-five are
short specs that quote the text they come from: RFC 8878, zstd.h or the xxHash specification. On any input the
decoder is safe and keeps within its buffers, valid frames decode to exactly their content, invalid frames are
refused, and memory stays within zstd's own estimates and is all given back. The other 19 are the longer specs of
our first zstd proof, which draw compliant frames, compressed blocks and dictionaries included, within stated
bounds. AI agents wrote the proof, about 430,000 lines of Rocq, and Rocq checks it. The specs, the proof and every
change to the code are in genproof/zstd-auto-proofs-public.
The short specs
Each spec links to its page, which shows the spec whole, as it is in the repository, with its theorem. A short spec
is 10 to 78 lines of C. In a spec, nondet_*, upto(max), any_bytes(n) and pick(x) stand for any value, and
gp_assert states what must hold. The first comment of each file states the property in full; it starts with
Law, the older word for a spec.
The valid frames of these specs hold only Raw and RLE blocks, which harness/frames.h (115 lines) writes field by
field as RFC 8878 lays them out. zstd's compressor is not part of the code under test, so valid frames are written
by hand. In the short specs, compressed blocks (Huffman literals, FSE sequences) appear only as any bytes.
The standard on valid input
Frames as RFC 8878 lays them out decode to exactly their content. 3 specs
A frame of Raw and RLE blocks in any header form, with or without a checksum, decodes with
ZSTD_decompress, given room for its content, to exactly its content.Any number of such frames one after the other decode to their contents one after the other.
Skippable frames (any magic of the 16, any user data up to 2^32 - 1 bytes) anywhere among them add nothing.
Rejecting bad input
Any input is safe, and invalid frames are refused. 8 specs
On any bytes, with any capacity,
ZSTD_decompressand a reused context'sZSTD_decompressDCtxreturn an error or a size at most the capacity. With the theorem's "no undefined behaviour", no input makes these calls access memory outside their objects.On any bytes in any pieces, with any buffers and start positions, the stream reset after each error,
ZSTD_decompressStreamkeeps both positions within their buffers and never moves them back.On any dictionary bytes and any input,
ZSTD_decompress_usingDictreturns an error or a size at most the capacity. The dictionary and the output are one object (the spec explains why).A frame whose checksum is changed to any other value is refused:
ZSTD_error_checksum_wrongone-shot, never complete when streamed.A frame whose
Frame_Content_Sizedeclares any other size than its content's (2^64 - 1, which means "unknown", is left out) is refused, one-shot and streamed.A frame with the header's
Reserved_bitset is refused, one-shot and streamed.A frame where any one block's type is changed to 3 (Reserved) is refused, one-shot and streamed.
Any part of a frame that stops before its end is never taken for a complete frame, one-shot or streamed.
The interface
The functions of zstd.h return and write what zstd.h says. 10 specs
For any accepted
ZSTD_d_windowLogMax, a series of frames streamed in any pieces with any output room ends as one-shot decoding ends: when one-shot decoding fails, the stream never completes after reading everything; when it succeeds, the stream completes with exactly the same bytes.On any input in any pieces, the stream reset after each error, a stream call never changes the output before the position it is given, and a call that returns no error writes nothing after the position it returns.
A context used for one-shot and streaming decoding of any inputs, with dictionaries loaded by copy and any
ZSTD_d_windowLogMax, then reset withZSTD_reset_session_and_parameters, behaves like a new one on any input (same results, positions and bytes; both fail or neither).A frame given less capacity than its content is refused with
ZSTD_error_dstSize_tooSmall. The spec checks the error code only, not what is indst.On any input, the stream reset after each error, a stream call given no input writes at most
ZSTD_BLOCKSIZE_MAX(128 KiB).ZSTD_getFrameContentSizereturns the declared size (any 64-bit value),ZSTD_CONTENTSIZE_UNKNOWNwithout the field, 0 for a skippable frame.ZSTD_findFrameCompressedSizereturns exactly the size of the first frame, Zstandard or skippable, whatever follows.ZSTD_getDictID_fromFramereturns the frame'sDictionary_ID(any value, any field size), 0 for a skippable frame.With
ZSTD_d_forceIgnoreChecksum, a frame with any checksum decodes to exactly its content.ZSTD_freeDCtx,ZSTD_freeDStream,ZSTD_freeDDictaccept NULL.
Memory
Counted through the context's allocator: bounded, and all given back. 3 specs
After any use (one-shot and streaming decoding, dictionaries loaded by copy, one referenced dictionary with or without
ZSTD_d_refMultipleDDicts, anyZSTD_d_windowLogMax, resets),ZSTD_freeDCtxgives back every byte.Decoding any inputs one-shot never holds more than
ZSTD_estimateDCtxSize().With any accepted
ZSTD_d_windowLogMax, streaming any input, the stream reset after each error, never holds more thanZSTD_estimateDStreamSize(2^windowLogMax).
Checksum
zstd's XXH64 is the xxHash specification's. 1 spec
ZSTD_XXH64equals XXH64 as the xxHash specification defines it, written out in the spec, for any bytes and seed.
Earlier, longer properties
These are the 19 specs of our first zstd proof, renamed x_<name> with their statements unchanged. Ten draw
compliant frames, compressed blocks and dictionaries included, from the frame writer harness/writer.h; eight take
any bytes; x_bound is about a macro. They are bounded: on any bytes, for example, input of at most 4096 bytes and
output of at most 65536. The repository's SPECS.md gives every bound.
One-shot decompression of any bytes into any capacity: an error or at most the capacity;
ZSTD_freeDCtxaccepts NULL.Streaming any bytes over any chunking:
0 <= pos <= sizefor input and output; at mostZSTD_BLOCKSIZE_MAXflushed without new input; resets after errors.Any dictionary bytes and any frame bytes through the by-reference dictionary APIs: an error or at most the capacity.
The same through the APIs that copy the dictionary.
The frame-inspection functions on any bytes return what
zstd.hdocuments (sizes within the input, "wanted" sizes, magic tests).ZSTD_getDecompressedSizeandZSTD_getFrameHeader_advancedon any bytes, as documented.Any values of the decoding parameters (window limit, format, checksum, several dictionaries, block size): one-shot and streaming stay safe and within their buffers.
Buffer-less streaming (
ZSTD_decompressContinue) on any bytes in any pieces, with or without a dictionary: an error or at most the capacity.ZSTD_COMPRESSBOUND(A) + ZSTD_COMPRESSBOUND(B) <= ZSTD_COMPRESSBOUND(A + B)for A, B of at least 128 KB with A + B <ZSTD_MAX_INPUT_SIZE.Any series of compliant frames (every block, literal and table mode, header form, checksum, skippable frames) decodes one-shot to exactly its content; with room for it, it succeeds unless
zlimits.hallows a refusal.The same streamed in any pieces with any output room; the hint never asks for more than the frame holds.
Any dictionary (raw or formatted) and frames written with it, through the by-reference dictionary APIs: exactly the content.
The same through the APIs that copy the dictionary.
Several dictionaries selected by their ID (
ZSTD_d_refMultipleDDicts), the_advancedloaders, the deprecated stream initialisation: exactly the content.On compliant frames, the inspection functions report exactly the frames' sizes, content sizes and IDs.
Compliant frames under any accepted decoding parameters decode to exactly their content.
Buffer-less streaming of compliant frames, each call given exactly what it asks for: exactly the content, 0 through skippable frames.
A compliant series with one frame's checksum changed or reserved bit set is refused, one-shot and streamed.
ZSTD_d_stableOutBuffer: memory safe on any bytes; on compliant frames, every byte written is the content.
Limits
Each theorem is harness_safe whole_prog, where whole_prog is the spec linked with the decoder. By VST's
adequacy theorem it means that for every value the spec draws, no run has undefined behaviour in CompCert's
semantics of C and every check in the spec holds. An access outside an object or a use after free is undefined
behaviour, so memory safety is part of every theorem. That adequacy theorem is not yet instantiated in Rocq for
QuickProof's environment.
- Termination is not proved. A run that never returns is not a counterexample.
- The theorems are about the C code as CompCert 3.15 defines it for
x86_64-linux, in one thread. A binary built by another compiler is outside them. Running out of stack is not covered. - Any
mallocmay returnNULL. When zstd reportsZSTD_error_memory_allocation, or the spec cannot allocate, the spec claims nothing more about that run. - In the short specs, data has any length the allocator provides. Only frame windows are limited, to zstd's documented limits: 2 GiB in one call, 128 MiB for a stream by default.
- Error codes are not checked, except where a spec names one.
- The configuration has no assembly, no BMI2 and no legacy formats, and uses the plain Huffman loops. In-place decoding is left out, and so are four public functions that take a struct by value. The compressor is not covered.
- The theorems are about the decoder with the edits listed below. Nothing is proved about the unmodified code.
- Each theorem rests on 14 axioms: eight standard axioms of Rocq and six declarations of VST's
malloclibrary. The result also trusts Rocq, VST, CompCert's semantics of C,clightgen, QuickProof's translation scripts, its model of the environment andqp verify(What a proof means).
How to check it
You need 64-bit x86 Linux with Coq 8.20.1, CompCert 3.15 and VST 2.15 (Install), or QuickProof's Docker image. Check the proof with a QuickProof checkout of your own at v0.1.0, the release the repository pins:
git clone --branch v0.1.0 https://github.com/genproof/quickproof-public.git quickproof
git clone --recursive https://github.com/genproof/zstd-auto-proofs-public.git
quickproof/tools/qp verify zstd-auto-proofs-public --base contractThe tag contract holds the specs, the code and the 44 theorems before any proof. qp verify rebuilds every
proof from a fresh clone, checks that the statements are those of that tag and that nothing is admitted, and lists
the axioms each theorem uses. A full check takes about 7 hours with 8 build jobs on 16 CPUs.
The code under test
The code is zstd's decoder at commit
01b7154f1172 on the dev
branch (2026-09-18), after release v1.5.7: lib/common and lib/decompress, compiled as one unit. The
repository's patch/GENPROOF-PATCH.md describes every edit, and every changed line carries a comment with the
original. Idiom edits rewrite C that CompCert's semantics or VST cannot handle; differential tests against the
original check most of them. Other edits fix places where the code differed from zstd's documentation, or where
it reached expressions that CompCert's semantics leaves undefined. One added function passes the allocator by
pointer for the memory specs. The edits are tested, not proved.
Bugs found
Some specs were false on zstd's decoder at 01b7154f; the edits that make them hold are in the repository's
patch/GENPROOF-PATCH.md. Reported upstream so far, on 2026-11-01:
ZSTD_getDictID_fromFramereturns the magic variant (0-15) of a skippable frame as its dictionary ID, not 0 (api_dict_id; meta/zstd#4881).- One-shot decoding refuses skippable frames whose
Frame_Sizeis 2^32 - 8 or more, which RFC 8878 allows; streaming decoding skips them (rfc_skippable; meta/zstd#4882). - Streaming decoding does not compare the decoded size with
Frame_Content_Sizewhen a frame ends with an empty last block, so it reports success on a frame shorter than it declares (rj_content_size; meta/zstd#4880).
Maintained by Vladimir Kukharenko.