/src/zxc/tests/fuzz_pstream.c
Line | Count | Source |
1 | | // SPDX-License-Identifier: BSD-3-Clause |
2 | | /* |
3 | | * ZXC - High-performance lossless compression |
4 | | * |
5 | | * Copyright (c) Bertrand Lebonnois and contributors. |
6 | | */ |
7 | | |
8 | | /** |
9 | | * @file fuzz_pstream.c |
10 | | * @brief Fuzzer for the push-based streaming API (zxc_cstream / zxc_dstream). |
11 | | * |
12 | | * The push API is a state machine driven entirely by caller-supplied byte |
13 | | * slices: each call may fully consume input, partially consume it, fill the |
14 | | * output buffer mid-block, or hit an error. These resumption paths are |
15 | | * unique to pstream and are NOT exercised by fuzz_decompress.c (which feeds |
16 | | * the whole blob in a single call). |
17 | | * |
18 | | * Strategy: derive an input-chunk size and an output-drain size from the |
19 | | * fuzzer header bytes so libFuzzer explores byte content AND chunk |
20 | | * boundaries jointly. Phases: |
21 | | * |
22 | | * 1. Compress fuzzed data via zxc_cstream_*, draining the output in small |
23 | | * chunks (forces re-entry into the compressor state machine). |
24 | | * 2. Decompress the result via zxc_dstream_decompress, feeding the input |
25 | | * one chunk at a time, again with a small output drain buffer. Assert |
26 | | * bit-exact roundtrip. |
27 | | * 3. Feed the *raw* fuzzer input directly to a fresh dstream, varying chunk |
28 | | * sizes - this is the parser/state-machine fuzzing surface that catches |
29 | | * malformed-frame, truncation, and sticky-error bugs. |
30 | | */ |
31 | | |
32 | | #include <assert.h> |
33 | | #include <stddef.h> |
34 | | #include <stdint.h> |
35 | | #include <stdlib.h> |
36 | | #include <string.h> |
37 | | |
38 | | #include "../include/zxc_pstream.h" |
39 | | |
40 | | /* Cap aligned with the maximum supported block size (2 MiB). */ |
41 | 8.52k | #define FUZZ_PSTREAM_MAX_INPUT (2 << 20) /* 2 MiB */ |
42 | | |
43 | | /* Tiny output buffer: forces the compressor / decompressor into multi-round |
44 | | * draining, which is precisely the resumption code we want to fuzz. */ |
45 | 147M | #define FUZZ_PSTREAM_OUT_CAP 256 |
46 | | |
47 | 17.0k | static size_t derive_chunk_size(uint8_t b) { |
48 | | /* Map [0..255] -> [1..512] with a non-linear distribution biased toward |
49 | | * small values (small chunks stress the state machine the most). */ |
50 | 17.0k | if (b == 0) return 1; |
51 | 14.7k | if (b < 64) return (size_t)b; /* 1..63 */ |
52 | 12.0k | if (b < 192) return (size_t)b * 2; /* 128..382 */ |
53 | 3.23k | return (size_t)b + 256; /* 448..511 */ |
54 | 12.0k | } |
55 | | |
56 | | /* ---------------------------------------------------------------- * |
57 | | * Decompress `comp` into `out`, feeding input in chunks of `in_chunk` and |
58 | | * draining output in chunks of FUZZ_PSTREAM_OUT_CAP. |
59 | | * |
60 | | * Returns the total number of decompressed bytes written, or a negative |
61 | | * zxc error code on failure. Stops early (no error) if the output buffer |
62 | | * is exhausted before the stream is finished. |
63 | | * ---------------------------------------------------------------- */ |
64 | | static int64_t drip_decompress(const uint8_t* comp, size_t csize, uint8_t* out, size_t out_cap, |
65 | 8.52k | size_t in_chunk, int checksum_enabled) { |
66 | 8.52k | zxc_decompress_opts_t opts = {.checksum_enabled = checksum_enabled}; |
67 | 8.52k | zxc_dstream* ds = zxc_dstream_create(&opts); |
68 | 8.52k | if (!ds) return -1; |
69 | | |
70 | 8.52k | size_t in_pos = 0; |
71 | 8.52k | size_t out_total = 0; |
72 | | |
73 | 20.4M | while (in_pos < csize || !zxc_dstream_finished(ds)) { |
74 | 20.4M | if (out_total >= out_cap) { |
75 | | /* Caller's buffer full - stop without asserting. */ |
76 | 1.11k | zxc_dstream_free(ds); |
77 | 1.11k | return (int64_t)out_total; |
78 | 1.11k | } |
79 | 20.4M | const size_t feed = (csize - in_pos < in_chunk) ? (csize - in_pos) : in_chunk; |
80 | 20.4M | size_t window = out_cap - out_total; |
81 | 20.4M | if (window > FUZZ_PSTREAM_OUT_CAP) window = FUZZ_PSTREAM_OUT_CAP; |
82 | 20.4M | zxc_inbuf_t in = {.src = comp + in_pos, .size = feed, .pos = 0}; |
83 | 20.4M | zxc_outbuf_t ob = {.dst = out + out_total, .size = window, .pos = 0}; |
84 | | |
85 | 20.4M | const size_t before_in = in.pos; |
86 | 20.4M | const size_t before_out = ob.pos; |
87 | 20.4M | const int64_t r = zxc_dstream_decompress(ds, &ob, &in); |
88 | 20.4M | if (r < 0) { |
89 | 0 | zxc_dstream_free(ds); |
90 | 0 | return r; |
91 | 0 | } |
92 | | |
93 | 20.4M | out_total += ob.pos; |
94 | 20.4M | in_pos += in.pos; |
95 | | |
96 | | /* No progress and no more input: prevent infinite loop on truncation. */ |
97 | 20.4M | if (in.pos == before_in && ob.pos == before_out && in_pos == csize) break; |
98 | 20.4M | } |
99 | | |
100 | 7.41k | zxc_dstream_free(ds); |
101 | 7.41k | return (int64_t)out_total; |
102 | 8.52k | } |
103 | | |
104 | 8.55k | int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { |
105 | 8.55k | if (size < 3) return 0; |
106 | | |
107 | | /* Save raw input for the parser-fuzzing phase. */ |
108 | 8.54k | const uint8_t* const raw_data = data; |
109 | 8.54k | const size_t raw_size = size; |
110 | | |
111 | | /* Header bytes drive pstream-specific axes. data[0] is reserved (the |
112 | | * compression level is intentionally fixed to 1, see copts below); it |
113 | | * is still consumed so corpus byte offsets stay stable. */ |
114 | 8.54k | const int checksum_enabled = data[1] & 1; |
115 | 8.54k | const size_t in_chunk = derive_chunk_size(data[2]); |
116 | 8.54k | data += 3; |
117 | 8.54k | size -= 3; |
118 | | |
119 | 8.54k | if (size == 0 || size > FUZZ_PSTREAM_MAX_INPUT) return 0; |
120 | | |
121 | | /* Persistent buffers - reused across iterations to reduce allocator |
122 | | * pressure (same pattern as fuzz_seekable.c). */ |
123 | 8.52k | static uint8_t* comp_buf = NULL; |
124 | 8.52k | static size_t comp_cap = 0; |
125 | 8.52k | static uint8_t* round_buf = NULL; |
126 | 8.52k | static size_t round_cap = 0; |
127 | | |
128 | | /* ------------------------------------------------------------------ */ |
129 | | /* Phase 1: Push compression in chunks */ |
130 | | /* ------------------------------------------------------------------ */ |
131 | | /* Level forced to 1: pstream's state-machine surface (framing, drain, |
132 | | * resumption, sticky errors) is independent of compression strength, |
133 | | * and buffer-API fuzzers (roundtrip, decompress) already exercise the |
134 | | * level dimension. Cheaper compression = more iterations/sec. */ |
135 | 8.52k | zxc_compress_opts_t copts = { |
136 | 8.52k | .level = 1, |
137 | 8.52k | .checksum_enabled = checksum_enabled, |
138 | 8.52k | }; |
139 | 8.52k | zxc_cstream* cs = zxc_cstream_create(&copts); |
140 | 8.52k | if (!cs) return 0; |
141 | | |
142 | | /* Sized so the compressor never runs out - bound is a safe upper limit. */ |
143 | | /* Use the public buffer-API bound function via a local extern declaration |
144 | | * to avoid pulling zxc_buffer.h (kept narrow on purpose). */ |
145 | 8.52k | extern uint64_t zxc_compress_bound(size_t input_size); |
146 | 8.52k | const uint64_t bound64 = zxc_compress_bound(size); |
147 | 8.52k | if (bound64 == 0 || bound64 > SIZE_MAX) { |
148 | 0 | zxc_cstream_free(cs); |
149 | 0 | return 0; |
150 | 0 | } |
151 | 8.52k | const size_t bound = (size_t)bound64; |
152 | 8.52k | if (bound > comp_cap) { |
153 | 3.58k | void* nb = realloc(comp_buf, bound); |
154 | 3.58k | if (!nb) { |
155 | 0 | zxc_cstream_free(cs); |
156 | 0 | return 0; |
157 | 0 | } |
158 | 3.58k | comp_buf = (uint8_t*)nb; |
159 | 3.58k | comp_cap = bound; |
160 | 3.58k | } |
161 | | |
162 | 8.52k | size_t comp_len = 0; |
163 | 8.52k | size_t in_pos = 0; |
164 | | |
165 | | /* Feed input in `in_chunk`-sized slices. */ |
166 | 52.7M | while (in_pos < size) { |
167 | 52.7M | const size_t feed = (size - in_pos < in_chunk) ? (size - in_pos) : in_chunk; |
168 | 52.7M | zxc_inbuf_t in = {.src = data + in_pos, .size = feed, .pos = 0}; |
169 | | |
170 | | /* Drain repeatedly until this slice is fully consumed. */ |
171 | 52.9M | for (;;) { |
172 | 52.9M | size_t window = bound - comp_len; |
173 | 52.9M | if (window > FUZZ_PSTREAM_OUT_CAP) window = FUZZ_PSTREAM_OUT_CAP; |
174 | 52.9M | if (window == 0) { |
175 | | /* Bound exhausted - shouldn't happen with zxc_compress_bound. */ |
176 | 0 | zxc_cstream_free(cs); |
177 | 0 | return 0; |
178 | 0 | } |
179 | 52.9M | zxc_outbuf_t ob = {.dst = comp_buf + comp_len, .size = window, .pos = 0}; |
180 | 52.9M | const int64_t r = zxc_cstream_compress(cs, &ob, &in); |
181 | 52.9M | if (r < 0) { |
182 | 0 | zxc_cstream_free(cs); |
183 | 0 | return 0; |
184 | 0 | } |
185 | 52.9M | comp_len += ob.pos; |
186 | 52.9M | if (in.pos == in.size && r == 0) break; |
187 | 52.9M | } |
188 | 52.7M | in_pos += in.pos; |
189 | 52.7M | } |
190 | | |
191 | | /* Finalize: zxc_cstream_end may report >0 pending bytes; drain until 0. */ |
192 | 551k | for (;;) { |
193 | 551k | size_t window = bound - comp_len; |
194 | 551k | if (window > FUZZ_PSTREAM_OUT_CAP) window = FUZZ_PSTREAM_OUT_CAP; |
195 | 551k | if (window == 0) { |
196 | 0 | zxc_cstream_free(cs); |
197 | 0 | return 0; |
198 | 0 | } |
199 | 551k | zxc_outbuf_t ob = {.dst = comp_buf + comp_len, .size = window, .pos = 0}; |
200 | 551k | const int64_t pending = zxc_cstream_end(cs, &ob); |
201 | 551k | if (pending < 0) { |
202 | 0 | zxc_cstream_free(cs); |
203 | 0 | return 0; |
204 | 0 | } |
205 | 551k | comp_len += ob.pos; |
206 | 551k | if (pending == 0) break; |
207 | 551k | } |
208 | | |
209 | 8.52k | zxc_cstream_free(cs); |
210 | | |
211 | | /* ------------------------------------------------------------------ */ |
212 | | /* Phase 2: Push decompression with chunked feeding + roundtrip */ |
213 | | /* ------------------------------------------------------------------ */ |
214 | 8.52k | if (size > round_cap) { |
215 | 3.58k | void* nb = realloc(round_buf, size); |
216 | 3.58k | if (!nb) return 0; |
217 | 3.58k | round_buf = (uint8_t*)nb; |
218 | 3.58k | round_cap = size; |
219 | 3.58k | } |
220 | | |
221 | 8.52k | const int64_t dsize = |
222 | 8.52k | drip_decompress(comp_buf, comp_len, round_buf, size, in_chunk, checksum_enabled); |
223 | 8.52k | if (dsize >= 0) { |
224 | 8.52k | assert((size_t)dsize == size); |
225 | 8.52k | assert(memcmp(data, round_buf, size) == 0); |
226 | 8.52k | } |
227 | | |
228 | | /* ------------------------------------------------------------------ */ |
229 | | /* Phase 3: Parser fuzzing - feed raw input directly */ |
230 | | /* */ |
231 | | /* This is where malformed-frame and truncation bugs live. We don't */ |
232 | | /* care about the result, only that no UB / crash occurs. */ |
233 | | /* ------------------------------------------------------------------ */ |
234 | 8.52k | if (raw_size > 0) { |
235 | | /* Output is discarded - a single small stack buffer suffices and |
236 | | * keeps the parser's resumption code (small ob.size) under fuzz. */ |
237 | 8.52k | uint8_t scratch[FUZZ_PSTREAM_OUT_CAP]; |
238 | 8.52k | const size_t parser_chunk = derive_chunk_size(raw_data[raw_size - 1]); |
239 | | |
240 | 8.52k | zxc_decompress_opts_t opts = {.checksum_enabled = checksum_enabled}; |
241 | 8.52k | zxc_dstream* ds = zxc_dstream_create(&opts); |
242 | 8.52k | if (ds) { |
243 | 8.52k | size_t pos = 0; |
244 | 8.52k | int rounds = 0; |
245 | 661k | while (pos < raw_size && rounds < 1024) { |
246 | 660k | const size_t feed = |
247 | 660k | (raw_size - pos < parser_chunk) ? (raw_size - pos) : parser_chunk; |
248 | 660k | zxc_inbuf_t in = {.src = raw_data + pos, .size = feed, .pos = 0}; |
249 | 660k | zxc_outbuf_t ob = {.dst = scratch, .size = sizeof scratch, .pos = 0}; |
250 | 660k | const size_t before_in = in.pos; |
251 | 660k | const size_t before_out = ob.pos; |
252 | 660k | const int64_t r = zxc_dstream_decompress(ds, &ob, &in); |
253 | 660k | pos += in.pos; |
254 | 660k | if (r < 0) break; |
255 | 652k | if (in.pos == before_in && ob.pos == before_out) break; |
256 | 652k | rounds++; |
257 | 652k | } |
258 | 8.52k | zxc_dstream_free(ds); |
259 | 8.52k | } |
260 | 8.52k | } |
261 | | |
262 | 8.52k | return 0; |
263 | 8.52k | } |