Coverage Report

Created: 2026-08-14 06:27

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/zxc/tests/fuzz_dict.c
Line
Count
Source
1
/*
2
 * ZXC - High-performance lossless compression
3
 *
4
 * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors.
5
 * SPDX-License-Identifier: BSD-3-Clause
6
 */
7
8
/**
9
 * @file fuzz_dict.c
10
 * @brief Fuzzer for the dictionary API in zxc_dict.c.
11
 *
12
 * Strategy (exercises the .zxd format and the trainer, not just the in-memory
13
 * dict bounce path):
14
 *
15
 *  Phase 0 -- Raw .zxd parser. Feed the untrusted bytes straight to
16
 *             zxc_dict_load() / zxc_dict_get_id(). Must never crash on garbage.
17
 *  Phase 1 -- Train. Split the input into samples and call zxc_train_dict().
18
 *  Phase 2 -- Serialize roundtrip. Save the trained dict to .zxd, load it back,
19
 *             and verify content / dict_id agree across save/load/get_id/id.
20
 *             Also drives the DST_TOO_SMALL path and single-byte corruption.
21
 *  Phase 3 -- Use the trained dict for a real compress -> decompress roundtrip.
22
 *
23
 * The control header carries level, sample count, dict capacity, and the
24
 * corruption position/mask, so the same input deterministically reaches the
25
 * deep validation branches that random bytes (which lack the .zxd magic) miss.
26
 */
27
28
#include <assert.h>
29
#include <stddef.h>
30
#include <stdint.h>
31
#include <stdlib.h>
32
#include <string.h>
33
34
#include "../include/zxc_buffer.h"
35
#include "../include/zxc_constants.h"
36
#include "../include/zxc_dict.h"
37
#include "../include/zxc_error.h"
38
39
9.73k
#define FUZZ_DICT_MAX_INPUT (256 << 10) /* 256 KiB */
40
29.1k
#define FUZZ_DICT_CTRL 8                /* control-header bytes consumed below */
41
9.72k
#define FUZZ_DICT_MAX_SAMPLES 8
42
43
9.73k
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
44
9.73k
    static uint8_t* dict_buf = NULL; /* trained dict content (<= ZXC_DICT_SIZE_MAX) */
45
9.73k
    static uint8_t* zxd_buf = NULL;  /* serialized .zxd (header + content)          */
46
9.73k
    static void* comp_buf = NULL;
47
9.73k
    static size_t comp_cap = 0;
48
9.73k
    static void* decomp_buf = NULL;
49
9.73k
    static size_t decomp_cap = 0;
50
51
9.73k
    if (size > FUZZ_DICT_MAX_INPUT) return 0;
52
53
    /* ------------------------------------------------------------------ */
54
    /* Phase 0: raw .zxd parser on untrusted bytes (must not crash).      */
55
    /* zxc_dict_load tolerates buf_size < header, so any size is safe.    */
56
    /* ------------------------------------------------------------------ */
57
9.72k
    {
58
9.72k
        const void* content = NULL;
59
9.72k
        size_t content_size = 0;
60
9.72k
        const void* huf = NULL;
61
9.72k
        uint32_t id = 0;
62
9.72k
        const int rc = zxc_dict_load(data, size, &content, &content_size, &huf, &id);
63
9.72k
        if (rc == ZXC_OK) {
64
0
            assert(content != NULL);
65
0
            assert(content_size > 0 && content_size <= ZXC_DICT_SIZE_MAX);
66
            /* A validated header round-trips through both ID accessors; the
67
             * stored id binds the (content, table) pair. */
68
0
            assert(zxc_dict_get_id(data, size) == id);
69
0
            assert(zxc_dict_id(content, content_size, huf) == id);
70
0
        }
71
9.72k
        (void)zxc_dict_get_id(data, size);
72
9.72k
    }
73
74
    /* ------------------------------------------------------------------ */
75
    /* Parse the control header.                                          */
76
    /* ------------------------------------------------------------------ */
77
9.72k
    if (size < FUZZ_DICT_CTRL) return 0;
78
79
9.72k
    const int level = (int)(data[0] % (unsigned)zxc_max_level()) + 1;
80
9.72k
    const size_t n_samples = (size_t)(data[1] % FUZZ_DICT_MAX_SAMPLES) + 1;
81
9.72k
    size_t dict_cap = (size_t)(data[2] | (data[3] << 8));
82
9.72k
    const size_t corrupt_pos = (size_t)(data[4] | (data[5] << 8));
83
9.72k
    const uint8_t corrupt_mask = data[6];
84
9.72k
    const int use_checksum = data[7] & 1;
85
9.72k
    data += FUZZ_DICT_CTRL;
86
9.72k
    size -= FUZZ_DICT_CTRL;
87
88
9.72k
    if (size == 0) return 0;
89
90
9.72k
    if (dict_cap == 0) dict_cap = 1;
91
9.72k
    if (dict_cap > ZXC_DICT_SIZE_MAX) dict_cap = ZXC_DICT_SIZE_MAX;
92
93
    /* ------------------------------------------------------------------ */
94
    /* Phase 1: split input into samples and train.                       */
95
    /* ------------------------------------------------------------------ */
96
9.72k
    const void* samples[FUZZ_DICT_MAX_SAMPLES];
97
9.72k
    size_t sample_sizes[FUZZ_DICT_MAX_SAMPLES];
98
9.72k
    const size_t chunk = size / n_samples;
99
50.8k
    for (size_t i = 0; i < n_samples; i++) {
100
41.0k
        samples[i] = data + chunk * i;
101
41.0k
        sample_sizes[i] = (i + 1 < n_samples) ? chunk : (size - chunk * (n_samples - 1));
102
41.0k
    }
103
104
9.72k
    if (!dict_buf) {
105
1
        dict_buf = (uint8_t*)malloc(ZXC_DICT_SIZE_MAX);
106
1
        if (!dict_buf) return 0;
107
1
    }
108
109
9.72k
    const int64_t dict_sz = zxc_train_dict(samples, sample_sizes, n_samples, dict_buf, dict_cap);
110
9.72k
    if (dict_sz <= 0) return 0; /* corpus too small / no patterns: nothing to serialize */
111
9.72k
    assert((size_t)dict_sz <= dict_cap);
112
113
    /* The .zxd format requires the shared literal table; train it on the same
114
     * samples (it needs the trained content for the post-LZ literal stats). */
115
9.69k
    uint8_t huf[ZXC_HUF_TABLE_SIZE];
116
9.69k
    if (zxc_train_dict_huf(samples, sample_sizes, n_samples, dict_buf, (size_t)dict_sz, huf) !=
117
9.69k
        ZXC_OK)
118
0
        return 0;
119
120
    /* ------------------------------------------------------------------ */
121
    /* Phase 2: .zxd save / load roundtrip + corruption.                  */
122
    /* ------------------------------------------------------------------ */
123
9.69k
    const size_t zxd_bound = zxc_dict_save_bound((size_t)dict_sz);
124
9.69k
    if (!zxd_buf) {
125
1
        zxd_buf = (uint8_t*)malloc(ZXC_DICT_HEADER_SIZE + ZXC_DICT_SIZE_MAX + ZXC_HUF_TABLE_SIZE);
126
1
        if (!zxd_buf) return 0;
127
1
    }
128
129
9.69k
    const int64_t zxd_sz = zxc_dict_save(dict_buf, (size_t)dict_sz, huf, zxd_buf, zxd_bound);
130
9.69k
    assert(zxd_sz == (int64_t)zxd_bound);
131
132
9.69k
    {
133
9.69k
        const void* lc = NULL;
134
9.69k
        size_t lcs = 0;
135
9.69k
        const void* lh = NULL;
136
9.69k
        uint32_t lid = 0;
137
9.69k
        const int rc = zxc_dict_load(zxd_buf, (size_t)zxd_sz, &lc, &lcs, &lh, &lid);
138
9.69k
        assert(rc == ZXC_OK);
139
9.69k
        (void)rc;
140
9.69k
        assert(lcs == (size_t)dict_sz);
141
9.69k
        assert(memcmp(lc, dict_buf, (size_t)dict_sz) == 0);
142
9.69k
        assert(lh != NULL && memcmp(lh, huf, ZXC_HUF_TABLE_SIZE) == 0);
143
        /* The stored id binds the (content, table) pair, not the content alone. */
144
9.69k
        assert(zxc_dict_get_id(zxd_buf, (size_t)zxd_sz) == lid);
145
9.69k
    }
146
147
    /* DST_TOO_SMALL: any capacity below the full file must be rejected. */
148
9.69k
    {
149
9.69k
        const size_t small_cap = corrupt_pos % (size_t)zxd_sz; /* in [0, zxd_sz) */
150
9.69k
        const int64_t r = zxc_dict_save(dict_buf, (size_t)dict_sz, huf, zxd_buf, small_cap);
151
9.69k
        assert(r < 0);
152
9.69k
        (void)r;
153
9.69k
    }
154
155
    /* Re-save (the DST_TOO_SMALL attempt left zxd_buf untouched, but be safe). */
156
9.69k
    assert(zxc_dict_save(dict_buf, (size_t)dict_sz, huf, zxd_buf, zxd_bound) == (int64_t)zxd_sz);
157
158
    /* Flip one byte and re-load: must not crash. A surviving ZXC_OK can only
159
     * come from a reserved-byte flip (offsets 12-13, zeroed before the CRC),
160
     * which cannot change the recovered content. */
161
9.69k
    {
162
9.69k
        const size_t pos = corrupt_pos % (size_t)zxd_sz;
163
9.69k
        const uint8_t saved = zxd_buf[pos];
164
9.69k
        zxd_buf[pos] ^= (uint8_t)(corrupt_mask | 1u); /* guaranteed to differ */
165
166
9.69k
        const void* cc = NULL;
167
9.69k
        size_t ccs = 0;
168
9.69k
        uint32_t cid = 0;
169
9.69k
        const int rc = zxc_dict_load(zxd_buf, (size_t)zxd_sz, &cc, &ccs, NULL, &cid);
170
9.69k
        if (rc == ZXC_OK) {
171
29
            assert(ccs == (size_t)dict_sz);
172
29
            assert(memcmp(cc, dict_buf, (size_t)dict_sz) == 0);
173
29
        }
174
9.69k
        zxd_buf[pos] = saved;
175
9.69k
    }
176
177
    /* ------------------------------------------------------------------ */
178
    /* Phase 3: real compress -> decompress roundtrip with the trained    */
179
    /* dict (also keeps the in-memory dict bounce path covered).          */
180
    /* ------------------------------------------------------------------ */
181
9.69k
    const uint64_t bound64 = zxc_compress_bound(size);
182
9.69k
    if (bound64 == 0 || bound64 > SIZE_MAX) return 0;
183
9.69k
    const size_t bound = (size_t)bound64;
184
9.69k
    if (bound > comp_cap) {
185
4.96k
        void* nb = realloc(comp_buf, bound);
186
4.96k
        if (!nb) return 0;
187
4.96k
        comp_buf = nb;
188
4.96k
        comp_cap = bound;
189
4.96k
    }
190
191
9.69k
    zxc_compress_opts_t copts = {
192
9.69k
        .level = level,
193
9.69k
        .checksum_enabled = use_checksum,
194
9.69k
        .dict = dict_buf,
195
9.69k
        .dict_size = (size_t)dict_sz,
196
9.69k
    };
197
9.69k
    const int64_t csize = zxc_compress(data, size, comp_buf, bound, &copts);
198
9.69k
    if (csize < 0) return 0;
199
200
9.69k
    if (size > decomp_cap) {
201
4.96k
        void* nb = realloc(decomp_buf, size);
202
4.96k
        if (!nb) return 0;
203
4.96k
        decomp_buf = nb;
204
4.96k
        decomp_cap = size;
205
4.96k
    }
206
207
9.69k
    zxc_decompress_opts_t dopts = {
208
9.69k
        .checksum_enabled = use_checksum,
209
9.69k
        .dict = dict_buf,
210
9.69k
        .dict_size = (size_t)dict_sz,
211
9.69k
    };
212
9.69k
    const int64_t dsize = zxc_decompress(comp_buf, (size_t)csize, decomp_buf, size, &dopts);
213
214
9.69k
    if (dsize >= 0) {
215
9.69k
        assert((size_t)dsize == size);
216
9.69k
        assert(memcmp(data, decomp_buf, size) == 0);
217
9.69k
    }
218
219
9.69k
    return 0;
220
9.69k
}