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