Coverage Report

Created: 2026-10-17 07:04

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}