Coverage Report

Created: 2026-09-04 07:03

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libzip/ossfuzz/zip_write_roundtrip_fuzzer.c
Line
Count
Source
1
/*
2
  zip_write_roundtrip_fuzzer.c -- fuzz the archive write/modify/serialize paths
3
  Copyright (C) 2026 The libzip Authors
4
5
  SPDX-License-Identifier: BSD-3-Clause
6
7
  Coverage gap addressed:
8
    The existing fuzzers (zip_read_fuzzer, zip_read_file_fuzzer,
9
    zip_read_metadata_fuzzer) only ever OPEN an archive read-only and
10
    consume it.  They never exercise the *write* side of libzip:
11
12
      - zip_file_add() / zip_dir_add() / zip_delete() / zip_file_rename()
13
      - zip_set_file_compression() (deflate/store encode)
14
      - zip_file_set_encryption()  (WinZip-AES / PKWARE encode)
15
      - zip_file_set_comment() / zip_set_archive_comment()
16
      - zip_file_extra_field_set()
17
      - zip_close() serialization: _zip_dirent_write(), extra-field
18
        sizing/merging, Zip64 promotion, central-directory + EOCD writing.
19
20
    These paths build ZIP structures from caller-influenced sizes and
21
    counts and are not reached by any read-only harness.
22
23
  Strategy:
24
    Drive a sequence of mutation operations from the fuzz input against a
25
    fresh, writable in-memory archive, then serialize it with zip_close().
26
    The fuzz bytes choose the operation, entry names, payload slices,
27
    compression methods, encryption methods, comments and extra fields.
28
    Finally, reopen the freshly written buffer and read every entry back
29
    (the encode -> decode round trip), so a malformed structure produced
30
    by the write path is also exercised on the read path.
31
*/
32
33
#include <stdint.h>
34
#include <stddef.h>
35
#include <stdlib.h>
36
#include <string.h>
37
#include <zip.h>
38
39
/* Simple cursor over the fuzz input used to drive the operations. */
40
typedef struct {
41
    const uint8_t *p;
42
    size_t len;
43
    size_t pos;
44
} stream_t;
45
46
static uint8_t
47
728k
u8(stream_t *s) {
48
728k
    return s->pos < s->len ? s->p[s->pos++] : 0;
49
728k
}
50
51
static uint16_t
52
26.8k
u16(stream_t *s) {
53
26.8k
    uint16_t v = u8(s);
54
26.8k
    v = (uint16_t)(v | ((uint16_t)u8(s) << 8));
55
26.8k
    return v;
56
26.8k
}
57
58
/* Consume up to max bytes from the stream, returning a pointer and length. */
59
static const uint8_t *
60
25.4k
take(stream_t *s, size_t max, size_t *out_len) {
61
25.4k
    size_t avail = s->len - s->pos;
62
25.4k
    size_t n = max;
63
25.4k
    if (n > avail) {
64
3.64k
        n = avail;
65
3.64k
    }
66
25.4k
    const uint8_t *r = s->p + s->pos;
67
25.4k
    s->pos += n;
68
25.4k
    *out_len = n;
69
25.4k
    return r;
70
25.4k
}
71
72
static const char *PASSWORD = "fuzzpw";
73
74
#ifdef __cplusplus
75
extern "C" {
76
#endif
77
78
int
79
5.45k
LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
80
5.45k
    zip_error_t error;
81
5.45k
    zip_source_t *src;
82
5.45k
    zip_t *za;
83
5.45k
    stream_t st;
84
5.45k
    int nops, i;
85
86
5.45k
    zip_error_init(&error);
87
88
    /* Empty, growable, writable in-memory archive. */
89
5.45k
    src = zip_source_buffer_create(NULL, 0, 0, &error);
90
5.45k
    if (src == NULL) {
91
0
        zip_error_fini(&error);
92
0
        return 0;
93
0
    }
94
    /* Keep an extra reference so the buffer survives the write zip_close()
95
       and we can reopen it for the read-back round trip. */
96
5.45k
    zip_source_keep(src);
97
98
5.45k
    za = zip_open_from_source(src, ZIP_TRUNCATE, &error);
99
5.45k
    if (za == NULL) {
100
0
        zip_source_free(src); /* release our keep reference */
101
0
        zip_error_fini(&error);
102
0
        return 0;
103
0
    }
104
5.45k
    zip_error_fini(&error);
105
106
5.45k
    zip_set_default_password(za, PASSWORD);
107
108
5.45k
    st.p = data;
109
5.45k
    st.len = size;
110
5.45k
    st.pos = 0;
111
112
5.45k
    nops = (int)(u8(&st) % 32) + 1;
113
40.5k
    for (i = 0; i < nops && st.pos < st.len; i++) {
114
35.1k
        uint8_t op = u8(&st) % 7;
115
35.1k
        char name[64];
116
35.1k
        size_t nlen = u8(&st) % (sizeof(name) - 1);
117
35.1k
        size_t k;
118
565k
        for (k = 0; k < nlen; k++) {
119
            /* avoid embedded NUL so it stays a valid C string */
120
530k
            name[k] = (char)(u8(&st) | 0x01);
121
530k
        }
122
35.1k
        name[nlen] = '\0';
123
35.1k
        if (name[0] == '\0') {
124
13.6k
            name[0] = 'f';
125
13.6k
            name[1] = '\0';
126
13.6k
        }
127
128
35.1k
        switch (op) {
129
15.7k
        case 0:
130
18.9k
        case 1: {
131
            /* add a file whose data is a slice of the input */
132
18.9k
            size_t dlen;
133
18.9k
            const uint8_t *slice = take(&st, u16(&st) % 4096u, &dlen);
134
18.9k
            void *buf = malloc(dlen ? dlen : 1);
135
18.9k
            zip_source_t *fs;
136
18.9k
            zip_int64_t idx;
137
18.9k
            zip_int32_t methods[3];
138
18.9k
            zip_uint16_t ems[4];
139
140
18.9k
            if (buf == NULL) {
141
0
                break;
142
0
            }
143
18.9k
            if (dlen) {
144
11.8k
                memcpy(buf, slice, dlen);
145
11.8k
            }
146
            /* freep = 1: libzip owns buf and frees it with the source */
147
18.9k
            fs = zip_source_buffer(za, buf, dlen, 1);
148
18.9k
            if (fs == NULL) {
149
0
                free(buf);
150
0
                break;
151
0
            }
152
18.9k
            idx = zip_file_add(za, name, fs, ZIP_FL_OVERWRITE | ZIP_FL_ENC_GUESS);
153
18.9k
            if (idx < 0) {
154
0
                zip_source_free(fs);
155
0
                break;
156
0
            }
157
18.9k
            methods[0] = ZIP_CM_STORE;
158
18.9k
            methods[1] = ZIP_CM_DEFLATE;
159
18.9k
            methods[2] = ZIP_CM_DEFAULT;
160
18.9k
            zip_set_file_compression(za, (zip_uint64_t)idx, methods[u8(&st) % 3], u8(&st) % 10);
161
162
18.9k
            ems[0] = ZIP_EM_NONE;
163
18.9k
            ems[1] = ZIP_EM_AES_128;
164
18.9k
            ems[2] = ZIP_EM_AES_256;
165
18.9k
            ems[3] = ZIP_EM_TRAD_PKWARE;
166
18.9k
            zip_file_set_encryption(za, (zip_uint64_t)idx, ems[u8(&st) % 4], PASSWORD);
167
18.9k
            break;
168
18.9k
        }
169
170
7.95k
        case 2:
171
7.95k
            zip_dir_add(za, name, ZIP_FL_ENC_GUESS);
172
7.95k
            break;
173
174
1.49k
        case 3: {
175
1.49k
            size_t clen;
176
1.49k
            const uint8_t *c = take(&st, u8(&st) % 200u, &clen);
177
1.49k
            zip_set_archive_comment(za, (const char *)c, (zip_uint16_t)clen);
178
1.49k
            break;
179
18.9k
        }
180
181
5.15k
        case 4: {
182
5.15k
            zip_int64_t n = zip_get_num_entries(za, 0);
183
5.15k
            zip_uint64_t idx;
184
5.15k
            if (n <= 0) {
185
107
                break;
186
107
            }
187
5.05k
            idx = u16(&st) % (zip_uint64_t)n;
188
5.05k
            if (u8(&st) & 1) {
189
3.48k
                size_t clen;
190
3.48k
                const uint8_t *c = take(&st, u8(&st) % 200u, &clen);
191
3.48k
                zip_file_set_comment(za, idx, (const char *)c, (zip_uint16_t)clen, ZIP_FL_ENC_GUESS);
192
3.48k
            }
193
1.56k
            else {
194
1.56k
                zip_uint16_t efid = u16(&st);
195
1.56k
                size_t eflen;
196
1.56k
                const uint8_t *ef = take(&st, u8(&st) % 200u, &eflen);
197
1.56k
                zip_file_extra_field_set(za, idx, efid, ZIP_EXTRA_FIELD_NEW, ef, (zip_uint16_t)eflen, ZIP_FL_LOCAL);
198
1.56k
            }
199
5.05k
            break;
200
5.15k
        }
201
202
860
        case 5: {
203
860
            zip_int64_t n = zip_get_num_entries(za, 0);
204
860
            if (n <= 0) {
205
101
                break;
206
101
            }
207
759
            zip_file_rename(za, u16(&st) % (zip_uint64_t)n, name, ZIP_FL_ENC_GUESS);
208
759
            break;
209
860
        }
210
211
726
        case 6: {
212
726
            zip_int64_t n = zip_get_num_entries(za, 0);
213
726
            if (n <= 0) {
214
178
                break;
215
178
            }
216
548
            zip_delete(za, u16(&st) % (zip_uint64_t)n);
217
548
            break;
218
726
        }
219
35.1k
        }
220
35.1k
    }
221
222
    /* Serialize: the write/encode path under test. */
223
5.45k
    if (zip_close(za) < 0) {
224
0
        zip_discard(za);
225
0
        zip_source_free(src); /* release keep reference */
226
0
        return 0;
227
0
    }
228
229
    /* Round trip: reopen the freshly written archive and read it back.
230
       zip_open_from_source() takes ownership of one source reference and
231
       releases it on zip_close()/zip_discard(); keep one more reference so
232
       our own reference survives this second open/close cycle. */
233
5.45k
    zip_source_keep(src);
234
5.45k
    zip_error_init(&error);
235
5.45k
    za = zip_open_from_source(src, 0, &error);
236
5.45k
    if (za == NULL) {
237
2.22k
        zip_source_free(src); /* undo the keep above */
238
2.22k
        zip_source_free(src); /* release our original reference */
239
2.22k
        zip_error_fini(&error);
240
2.22k
        return 0;
241
2.22k
    }
242
3.22k
    zip_error_fini(&error);
243
244
3.22k
    zip_set_default_password(za, PASSWORD);
245
3.22k
    {
246
3.22k
        zip_int64_t n = zip_get_num_entries(za, 0);
247
3.22k
        zip_int64_t j;
248
3.22k
        char rbuf[8192];
249
22.6k
        for (j = 0; j < n; j++) {
250
19.4k
            zip_file_t *f = zip_fopen_index(za, (zip_uint64_t)j, 0);
251
19.4k
            if (f == NULL) {
252
512
                continue;
253
512
            }
254
27.7k
            while (zip_fread(f, rbuf, sizeof(rbuf)) > 0) {
255
8.86k
                ;
256
8.86k
            }
257
18.9k
            zip_fclose(f);
258
18.9k
        }
259
3.22k
    }
260
261
3.22k
    if (zip_close(za) < 0) {
262
0
        zip_discard(za);
263
0
    }
264
3.22k
    zip_source_free(src); /* release keep reference */
265
3.22k
    return 0;
266
5.45k
}
267
268
#ifdef __cplusplus
269
}
270
#endif