/src/zxc/tests/fuzz_seekable.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_seekable.c |
10 | | * @brief Fuzzer for the seekable random-access decompression API. |
11 | | * |
12 | | * Strategy: compress fuzzed input with seekable=1, then exercise the full |
13 | | * seekable read path (open, metadata getters, single-threaded decompress, |
14 | | * multi-threaded decompress) and verify data integrity. |
15 | | */ |
16 | | |
17 | | #include <assert.h> |
18 | | #include <stddef.h> |
19 | | #include <stdint.h> |
20 | | #include <stdlib.h> |
21 | | #include <string.h> |
22 | | |
23 | | #include "../include/zxc_buffer.h" |
24 | | #include "../include/zxc_seekable.h" |
25 | | |
26 | 9.45k | #define FUZZ_SEEKABLE_MAX_INPUT (4 << 20) /* 4 MiB */ |
27 | | |
28 | 9.45k | int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { |
29 | 9.45k | if (size < 2) return 0; |
30 | | |
31 | | /* Save original input for phase 7 (raw parser fuzzing) */ |
32 | 9.45k | const uint8_t* const raw_data = data; |
33 | 9.45k | const size_t raw_size = size; |
34 | | |
35 | | /* Use first byte as level, second as flags */ |
36 | 9.45k | const int level = (data[0] % 5) + 1; |
37 | 9.45k | const int use_checksum = data[1] & 1; |
38 | 9.45k | const int use_mt = data[1] & 2; |
39 | 9.45k | data += 2; |
40 | 9.45k | size -= 2; |
41 | | |
42 | 9.45k | if (size == 0 || size > FUZZ_SEEKABLE_MAX_INPUT) return 0; |
43 | | |
44 | | /* Persistent buffers - reused across iterations to reduce allocator pressure */ |
45 | 9.45k | static uint8_t* comp_buf = NULL; |
46 | 9.45k | static size_t comp_cap = 0; |
47 | 9.45k | static uint8_t* decomp_buf = NULL; |
48 | 9.45k | static size_t decomp_cap = 0; |
49 | | |
50 | | /* ------------------------------------------------------------------ */ |
51 | | /* Phase 1: Compress with seekable=1 */ |
52 | | /* ------------------------------------------------------------------ */ |
53 | 9.45k | const uint64_t bound64 = zxc_compress_bound(size); |
54 | 9.45k | if (bound64 == 0 || bound64 > SIZE_MAX) return 0; |
55 | 9.45k | const size_t bound = (size_t)bound64; |
56 | 9.45k | if (bound > comp_cap) { |
57 | 5.12k | void* new_buf = realloc(comp_buf, bound); |
58 | 5.12k | if (!new_buf) return 0; |
59 | 5.12k | comp_buf = (uint8_t*)new_buf; |
60 | 5.12k | comp_cap = bound; |
61 | 5.12k | } |
62 | | |
63 | 9.45k | zxc_compress_opts_t copts = { |
64 | 9.45k | .level = level, |
65 | 9.45k | .checksum_enabled = use_checksum, |
66 | 9.45k | .seekable = 1, |
67 | 9.45k | }; |
68 | 9.45k | const int64_t csize = zxc_compress(data, size, comp_buf, bound, &copts); |
69 | 9.45k | if (csize < 0) return 0; |
70 | | |
71 | | /* ------------------------------------------------------------------ */ |
72 | | /* Phase 2: Open seekable handle */ |
73 | | /* ------------------------------------------------------------------ */ |
74 | 9.45k | zxc_seekable* s = zxc_seekable_open(comp_buf, (size_t)csize); |
75 | 9.45k | if (!s) return 0; |
76 | | |
77 | | /* ------------------------------------------------------------------ */ |
78 | | /* Phase 3: Exercise metadata getters */ |
79 | | /* ------------------------------------------------------------------ */ |
80 | 9.45k | const uint32_t num_blocks = zxc_seekable_get_num_blocks(s); |
81 | 9.45k | const uint64_t total_decomp = zxc_seekable_get_decompressed_size(s); |
82 | 9.45k | assert(total_decomp == size); |
83 | | |
84 | 20.1k | for (uint32_t i = 0; i < num_blocks; i++) { |
85 | 10.6k | const uint32_t csz = zxc_seekable_get_block_comp_size(s, i); |
86 | 10.6k | const uint32_t dsz = zxc_seekable_get_block_decomp_size(s, i); |
87 | 10.6k | assert(csz > 0); |
88 | 10.6k | assert(dsz > 0); |
89 | 10.6k | (void)csz; |
90 | 10.6k | (void)dsz; |
91 | 10.6k | } |
92 | | /* Out-of-range access should return 0 */ |
93 | 9.45k | assert(zxc_seekable_get_block_comp_size(s, num_blocks) == 0); |
94 | 9.45k | assert(zxc_seekable_get_block_decomp_size(s, num_blocks) == 0); |
95 | | |
96 | | /* ------------------------------------------------------------------ */ |
97 | | /* Phase 4: Full decompression via seekable range */ |
98 | | /* ------------------------------------------------------------------ */ |
99 | 9.45k | if (size > decomp_cap) { |
100 | 5.12k | void* new_buf = realloc(decomp_buf, size); |
101 | 5.12k | if (!new_buf) { |
102 | 0 | zxc_seekable_free(s); |
103 | 0 | return 0; |
104 | 0 | } |
105 | 5.12k | decomp_buf = (uint8_t*)new_buf; |
106 | 5.12k | decomp_cap = size; |
107 | 5.12k | } |
108 | | |
109 | 9.45k | int64_t dec_result; |
110 | 9.45k | if (use_mt && size > 4096) { |
111 | 1.72k | dec_result = zxc_seekable_decompress_range_mt(s, decomp_buf, size, 0, size, 2); |
112 | 7.72k | } else { |
113 | 7.72k | dec_result = zxc_seekable_decompress_range(s, decomp_buf, size, 0, size); |
114 | 7.72k | } |
115 | 9.45k | assert(dec_result == (int64_t)size); |
116 | 9.45k | (void)dec_result; |
117 | 9.45k | assert(memcmp(data, decomp_buf, size) == 0); |
118 | | |
119 | | /* ------------------------------------------------------------------ */ |
120 | | /* Phase 5: Partial range decompression (sub-block extraction) */ |
121 | | /* ------------------------------------------------------------------ */ |
122 | 9.45k | if (size >= 4) { |
123 | | /* Extract a range from the middle */ |
124 | 9.43k | const size_t off = size / 4; |
125 | 9.43k | const size_t len = size / 2; |
126 | 9.43k | dec_result = zxc_seekable_decompress_range(s, decomp_buf, len, off, len); |
127 | 9.43k | assert(dec_result == (int64_t)len); |
128 | 9.43k | assert(memcmp(data + off, decomp_buf, len) == 0); |
129 | 9.43k | } |
130 | | |
131 | | /* ------------------------------------------------------------------ */ |
132 | | /* Phase 6: Edge cases */ |
133 | | /* ------------------------------------------------------------------ */ |
134 | | /* Zero-length read */ |
135 | 9.45k | dec_result = zxc_seekable_decompress_range(s, decomp_buf, size, 0, 0); |
136 | 9.45k | assert(dec_result == 0); |
137 | | |
138 | | /* Out-of-bounds read */ |
139 | 9.45k | dec_result = zxc_seekable_decompress_range(s, decomp_buf, size, total_decomp, 1); |
140 | 9.45k | assert(dec_result < 0); |
141 | | |
142 | | /* NULL handle */ |
143 | 9.45k | assert(zxc_seekable_get_num_blocks(NULL) == 0); |
144 | 9.45k | assert(zxc_seekable_get_decompressed_size(NULL) == 0); |
145 | | |
146 | | /* ------------------------------------------------------------------ */ |
147 | | /* Cleanup */ |
148 | | /* ------------------------------------------------------------------ */ |
149 | 9.45k | zxc_seekable_free(s); |
150 | | |
151 | | /* ------------------------------------------------------------------ */ |
152 | | /* Phase 7: Fuzz the parser with raw data (malformed seekable archives) */ |
153 | | /* ------------------------------------------------------------------ */ |
154 | 9.45k | zxc_seekable* s2 = zxc_seekable_open(raw_data, raw_size); |
155 | 9.45k | if (s2) { |
156 | | /* If it parsed, try a read - should not crash */ |
157 | 0 | const uint64_t td = zxc_seekable_get_decompressed_size(s2); |
158 | 0 | if (td > 0 && td <= FUZZ_SEEKABLE_MAX_INPUT) { |
159 | 0 | uint8_t* tmp = (uint8_t*)malloc((size_t)td); |
160 | 0 | if (tmp) { |
161 | 0 | zxc_seekable_decompress_range(s2, tmp, (size_t)td, 0, (size_t)td); |
162 | 0 | free(tmp); |
163 | 0 | } |
164 | 0 | } |
165 | 0 | zxc_seekable_free(s2); |
166 | 0 | } |
167 | | |
168 | 9.45k | return 0; |
169 | 9.45k | } |