Browse zlib specs (25 of 62)

Proofs / zlib / The stream interface

api_inflate_deterministic

Two new decompression streams with the same windowBits, fed the same input (any bytes) in the same pieces with the same flush, return the same codes and write the same bytes.

spec
58 lines of C
with its headers
150 lines
status
Proved
25 of 62
On this page

The spec

The spec is the C program harness/api_inflate_deterministic.c, 58 lines, shown whole as it is in the repository at 37189f4. It includes harness/laws_common.h (40 lines) and harness/laws_session.h (52 lines). Its first comment states the property in full, with every bound.

C
// Law api_inflate_deterministic: Two new streams made by inflateInit2 with the same windowBits in zlib.h's ranges (both
// return Z_OK; Z_MEM_ERROR ends the run) and fed the same input (any bytes, any length the allocator provides) in the
// same pieces below 2^32 bytes, with the same output space and the same flush (any int) on each inflate() call (input
// and output each an object of exactly avail_in and avail_out bytes, one per stream), behave the same: each call
// returns the same code on both, reads and writes as many bytes, writes the same bytes, and leaves the same total_in,
// total_out, msg, data_type and (except for raw streams, where zlib does not set it) adler (Z_MEM_ERROR ends the run).
#include "laws_common.h"
#include "laws_session.h"

int main(void) {
	int window_bits = (int) nondet_u32();
	gp_assume(inflate_window_ok(window_bits));
	size_t input_len = upto(PTRDIFF_MAX);
	unsigned char *input = alloc_any_bytes(input_len);
	if (input == NULL)
		return 0;
	z_stream first;
	z_stream second;
	new_stream(&first);
	new_stream(&second);
	int code_first = inflateInit2(&first, window_bits);
	int code_second = inflateInit2(&second, window_bits);
	if (code_first == Z_MEM_ERROR || code_second == Z_MEM_ERROR)
		return 0;
	gp_assert(code_first == Z_OK && code_second == Z_OK);
	size_t read = 0;
	do {
		size_t take = upto(clip(input_len - read));
		size_t out_size = nondet_u32();
		int flush = (int) nondet_u32();
		unsigned char *in_first = copy_of(input + read, take);
		unsigned char *in_second = copy_of(input + read, take);
		unsigned char *out_first = malloc(out_size);
		unsigned char *out_second = malloc(out_size);
		if (in_first == NULL || in_second == NULL || out_first == NULL || out_second == NULL)
			return 0;
		set_buffer(&first, in_first, take, out_first, out_size);
		set_buffer(&second, in_second, take, out_second, out_size);

		code_first = inflate(&first, flush);
		code_second = inflate(&second, flush);

		if (code_first == Z_MEM_ERROR || code_second == Z_MEM_ERROR)
			return 0;
		gp_assert(code_first == code_second);
		gp_assert(first.avail_in == second.avail_in && first.avail_out == second.avail_out);
		gp_assert(bytes_eq(out_first, out_second, out_size - first.avail_out));
		gp_assert(first.total_in == second.total_in && first.total_out == second.total_out);
		gp_assert(first.msg == second.msg && first.data_type == second.data_type);
		gp_assert(window_bits < 0 || first.adler == second.adler);
		read += take - first.avail_in;
		free(in_first);
		free(in_second);
		free(out_first);
		free(out_second);
	} while (nondet_u8() != 0);
	return 0;
}
harness/laws_common.h, 40 lines
C
// laws_common.h: trivial helpers shared by the zlib laws (each law includes it once). No models, no algorithms.
#include <limits.h>
#include <stdlib.h>
#include "quickproof.h"
#include "zlib.h"
static size_t upto(size_t max) {
	size_t value = nondet_size();
	return value < max ? value : max;
}
static uInt clip(size_t n) { return n < UINT_MAX ? (uInt) n : UINT_MAX; }
static unsigned char *alloc_any_bytes(size_t n) {
	unsigned char *bytes = malloc(n);
	if (bytes != NULL)
		nondet_bytes(bytes, n);
	return bytes;
}
static unsigned char *copy_of(const unsigned char *source, size_t n) {
	unsigned char *copy = malloc(n);
	for (size_t i = 0; copy != NULL && i < n; i++)
		copy[i] = source[i];
	return copy;
}
static int bytes_eq(const unsigned char *a, const unsigned char *b, size_t n) {
	for (size_t i = 0; i < n; i++)
		if (a[i] != b[i])
			return 0;
	return 1;
}
static void set_buffer(z_stream *strm, unsigned char *in, size_t in_len, unsigned char *out, size_t out_len) {
	strm->next_in = in;
	strm->avail_in = (uInt) in_len;
	strm->next_out = out;
	strm->avail_out = (uInt) out_len;
}
static void new_stream(z_stream *strm) {
	strm->zalloc = Z_NULL;
	strm->zfree = Z_NULL;
	strm->opaque = Z_NULL;
	set_buffer(strm, Z_NULL, 0, Z_NULL, 0);
}
harness/laws_session.h, 52 lines
C
// laws_session.h: helpers of the laws that drive a z_stream, included after laws_common.h. No models, no algorithms.
static int deflate_params_ok(int level, int window_bits, int mem_level, int strategy) {
	// zlib.h, deflateInit2: level "Z_DEFAULT_COMPRESSION, or between 0 and 9"; memLevel 1..MAX_MEM_LEVEL (9,
	// zconf.h); strategy Z_DEFAULT_STRATEGY (0) .. Z_FIXED (4); windowBits 8..15, "-8..-15 for raw deflate", "Add 16
	// to windowBits to write a simple gzip header", but "For raw deflate or gzip encoding, a request for a 256-byte
	// window is rejected as invalid"
	int zlib_window = window_bits >= 8 && window_bits <= 15;
	int raw_window = window_bits >= -15 && window_bits <= -9;
	int gzip_window = window_bits >= 16 + 9 && window_bits <= 16 + 15;
	int valid = level >= -1 && level <= 9 && mem_level >= 1 && mem_level <= 9 && strategy >= 0 && strategy <= Z_FIXED;
	return valid && (zlib_window || raw_window || gzip_window);
}
static int inflate_window_ok(int window_bits) {
	// zlib.h, inflateInit2: windowBits "should be in the range 8..15", "can also be zero to request that inflate
	// use the window size in the zlib header", "can also be -8..-15 for raw inflate", "Add 32 to windowBits to
	// enable zlib and gzip decoding with automatic header detection, or add 16 to decode only the gzip format"
	int low = window_bits & 15;
	int raw = window_bits >= -15 && window_bits <= -8;
	int wrapped = window_bits >= 0 && window_bits < 48 && (low == 0 || low >= 8);
	return raw || wrapped;
}
struct position {
	unsigned char *next_in;
	uInt avail_in;
	unsigned char *next_out;
	uInt avail_out;
	uLong total_in;
	uLong total_out;
};
static void position_of(z_stream *strm, struct position *at) {
	at->next_in = strm->next_in;
	at->avail_in = strm->avail_in;
	at->next_out = strm->next_out;
	at->avail_out = strm->avail_out;
	at->total_in = strm->total_in;
	at->total_out = strm->total_out;
}
static int unmoved(z_stream *strm, struct position *at) {
	int in = strm->next_in == at->next_in && strm->avail_in == at->avail_in && strm->total_in == at->total_in;
	int out = strm->next_out == at->next_out && strm->avail_out == at->avail_out && strm->total_out == at->total_out;
	return in && out;
}
static int accounted(z_stream *strm, struct position *at) {
	int in = strm->avail_in <= at->avail_in && strm->next_in + strm->avail_in == at->next_in + at->avail_in;
	int out = strm->avail_out <= at->avail_out && strm->next_out + strm->avail_out == at->next_out + at->avail_out;
	return in && out;
}
static int counted(z_stream *strm, struct position *at) {
	int in = strm->total_in == at->total_in + (at->avail_in - strm->avail_in);
	int out = strm->total_out == at->total_out + (at->avail_out - strm->avail_out);
	return in && out;
}

The theorem

QuickProof generated this statement from the spec, in src/contracts/api_inflate_deterministic.v:

Rocq
Theorem api_inflate_deterministic : harness_safe whole_prog.

src/contracts/laws.v states it on the spec's own program, whole_prog (the spec linked with the code under test), with its proof:

Rocq
Theorem api_inflate_deterministic : harness_safe Zlib.contracts.api_inflate_deterministic.whole_prog.
Proof. exact (proj1 (proj2 (proj2 (proj2 (proj2 (proj2 Zlib.contracts.suite.suite)))))). Qed.

Read as a statement about runs, it says that no run of the program has undefined behaviour or a failing check, for every value the spec can draw. That reading rests on VST's adequacy theorem, which is not yet instantiated in Rocq (formerly Coq) for QuickProof's environment.

The C files (units) its program links
  • adler32.c
  • crc32.c
  • inflate.c
  • inffast.c
  • inftrees.c
  • zutil.c
  • quickproof/libc/libc.c

What it leaves out

  • What the first comment of the spec excludes, and its bounds.
  • Like every spec of this proof: termination (a run that never returns is not a counterexample), and binaries built by a compiler (the theorem is about the code as CompCert defines C for x86_64, LP64), one thread.
  • Where zlib reports Z_MEM_ERROR, most specs end the run and claim nothing about it; the first comment of the spec says so.
  • The edits to zlib's code, listed in the repository, are tested, not proved.

The summary page says what the proof as a whole covers.

How it was proved

AI agents wrote the proof, and Rocq checks it. qp verify passes on this repository: this statement is the one generated from the spec at the contract commit, and its proof uses only Rocq's standard axioms and VST's abstract malloc library.