/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 | } |