Coverage Report

Created: 2026-08-14 06:27

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