fuzz_unzip.c (14868B)
1 /* 2 This file is part of libextractor. 3 Copyright (C) 2026 Christian Grothoff 4 5 libextractor is free software; you can redistribute it and/or modify 6 it under the terms of the GNU General Public License as published 7 by the Free Software Foundation; either version 3, or (at your 8 option) any later version. 9 10 libextractor is distributed in the hope that it will be useful, but 11 WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 General Public License for more details. 14 15 You should have received a copy of the GNU General Public License 16 along with libextractor; see the file COPYING. If not, write to the 17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 18 Boston, MA 02110-1301, USA. 19 */ 20 /** 21 * @file fuzz/fuzz_unzip.c 22 * @brief direct fuzzer for the in-tree ZIP reader, src/common/unzip.c 23 * @author Christian Grothoff 24 * 25 * The ZIP reader is shared by the odf, msoffice and zip plugins, so a 26 * bug here is reachable from three formats at once, and it is 27 * hand-written code descended from unzip 1.00 rather than a maintained 28 * third-party library. That combination makes it the highest-value 29 * single target in the tree. 30 * 31 * Every output buffer this harness passes is `malloc()`ed at *exactly* 32 * the size the API was told about, so ASAN's redzone turns a one-byte 33 * over-write into a hard error. Each call that takes a buffer size is 34 * additionally repeated with a deliberately too-small buffer: an API 35 * that is given a size must honour it. 36 * 37 * Input format: the fuzz_ec.h configuration prefix, then the ZIP image. 38 */ 39 40 #define FUZZ_HARNESS_NAME "fuzz_unzip" 41 42 #include "fuzz_common.h" 43 #include "fuzz_ec.h" 44 #include "unzip.h" 45 46 /** 47 * Upper bound on the number of archive members walked per input; a 48 * malformed central directory can otherwise be made to loop for a very 49 * long time, which is a finding of its own but not one worth 50 * rediscovering on every execution. 51 */ 52 #define MAX_MEMBERS 64 53 54 /** 55 * Upper bound on the number of bytes decompressed per member. 56 */ 57 #define MAX_INFLATE (1024 * 1024) 58 59 60 static void * 61 xalloc (size_t n) 62 { 63 void *p = malloc ((0 == n) ? 1 : n); 64 65 if (NULL == p) 66 abort (); 67 return p; 68 } 69 70 71 /** 72 * Ask for the current member's metadata twice: once with buffers sized 73 * exactly as the first (size-query) call reported, and once with buffers 74 * that are deliberately too small. 75 * 76 * @param uf the open archive 77 * @param aux fuzzer-chosen shrink factor for the too-small pass 78 */ 79 static void 80 probe_current_file_info (struct EXTRACTOR_UnzipFile *uf, 81 unsigned int aux) 82 { 83 struct EXTRACTOR_UnzipFileInfo fi; 84 char *name; 85 char *comment; 86 void *extra; 87 size_t small; 88 89 memset (&fi, 0, sizeof (fi)); 90 if (EXTRACTOR_UNZIP_OK != 91 EXTRACTOR_common_unzip_get_current_file_info (uf, 92 &fi, 93 NULL, 0, 94 NULL, 0, 95 NULL, 0)) 96 return; 97 /* Contract-exact buffers. Note that the API is told the buffer size, 98 so it must not write more than that even if the archive declares a 99 longer name. */ 100 name = (char *) xalloc (fi.size_filename); 101 extra = xalloc (fi.size_file_extra); 102 comment = (char *) xalloc (fi.size_file_comment); 103 if (EXTRACTOR_UNZIP_OK != 104 EXTRACTOR_common_unzip_get_current_file_info (uf, 105 &fi, 106 name, 107 (size_t) fi.size_filename, 108 extra, 109 (size_t) fi.size_file_extra, 110 comment, 111 (size_t) 112 fi.size_file_comment)) 113 { 114 free (name); 115 free (extra); 116 free (comment); 117 return; 118 } 119 free (name); 120 free (extra); 121 free (comment); 122 123 /* Too-small pass. */ 124 small = (0 == fi.size_filename) 125 ? 0 126 : ((size_t) fi.size_filename * (aux % 100u)) / 100u; 127 name = (char *) xalloc (small); 128 (void) EXTRACTOR_common_unzip_get_current_file_info (uf, 129 &fi, 130 name, 131 (size_t) small, 132 NULL, 0, 133 NULL, 0); 134 free (name); 135 small = (0 == fi.size_file_comment) 136 ? 0 137 : ((size_t) fi.size_file_comment * (aux % 100u)) / 100u; 138 comment = (char *) xalloc (small); 139 (void) EXTRACTOR_common_unzip_get_current_file_info (uf, 140 &fi, 141 NULL, 0, 142 NULL, 0, 143 comment, 144 (size_t) small); 145 free (comment); 146 small = (0 == fi.size_file_extra) 147 ? 0 148 : ((size_t) fi.size_file_extra * (aux % 100u)) / 100u; 149 extra = xalloc (small); 150 (void) EXTRACTOR_common_unzip_get_current_file_info (uf, 151 &fi, 152 NULL, 0, 153 extra, (size_t) small, 154 NULL, 0); 155 free (extra); 156 } 157 158 159 /** 160 * Decompress the current member into exactly-sized chunks. 161 * 162 * @param uf the open archive 163 * @param chunk number of bytes to request per read 164 */ 165 static void 166 probe_read_current (struct EXTRACTOR_UnzipFile *uf, 167 size_t chunk) 168 { 169 size_t total = 0; 170 171 if (EXTRACTOR_UNZIP_OK != 172 EXTRACTOR_common_unzip_open_current_file (uf)) 173 return; 174 if (0 == chunk) 175 chunk = 1; 176 while (total < MAX_INFLATE) 177 { 178 /* exact-size destination: a decompressor that writes one byte too 179 many lands in the redzone */ 180 void *buf = xalloc (chunk); 181 ssize_t got = EXTRACTOR_common_unzip_read_current_file (uf, buf, chunk); 182 183 if (0 < got) 184 { 185 if ((size_t) got > chunk) 186 { 187 free (buf); 188 fuzz_report_finding ("EXTRACTOR_common_unzip_read_current_file() " 189 "reported more bytes than the buffer size it " 190 "was given"); 191 } 192 total += (size_t) got; 193 } 194 free (buf); 195 if (0 >= got) 196 break; 197 } 198 (void) EXTRACTOR_common_unzip_close_current_file (uf); 199 } 200 201 202 int 203 LLVMFuzzerTestOneInput (const uint8_t *data, 204 size_t size) 205 { 206 struct EXTRACTOR_ExtractContext ec; 207 struct fuzz_ec_state st; 208 struct EXTRACTOR_UnzipFile *uf; 209 unsigned int members; 210 unsigned int aux; 211 size_t chunk; 212 213 fuzz_ignore_sigpipe (); 214 if (! le_fuzz_ec_setup (&ec, &st, data, size)) 215 return 0; 216 aux = data[3]; 217 chunk = (size_t) 1 << (aux % 17u); 218 uf = EXTRACTOR_common_unzip_open (&ec); 219 if (NULL == uf) 220 { 221 le_fuzz_ec_cleanup (&st); 222 return 0; 223 } 224 { 225 /* The global comment length is not exposed, so the only contract we 226 can check is that the function honours the size we hand it. */ 227 size_t clen = 1 + (aux % 512u); 228 char *c = (char *) xalloc (clen); 229 230 (void) EXTRACTOR_common_unzip_get_global_comment (uf, c, clen); 231 free (c); 232 c = (char *) xalloc (1); 233 (void) EXTRACTOR_common_unzip_get_global_comment (uf, c, 1); 234 free (c); 235 } 236 if (EXTRACTOR_UNZIP_OK == 237 EXTRACTOR_common_unzip_go_to_first_file (uf)) 238 { 239 for (members = 0; members < MAX_MEMBERS; members++) 240 { 241 probe_current_file_info (uf, aux); 242 probe_read_current (uf, chunk); 243 if (EXTRACTOR_UNZIP_OK != 244 EXTRACTOR_common_unzip_go_to_next_file (uf)) 245 break; 246 } 247 } 248 /* Name lookup: the comparison walks the central directory again, and 249 the name comes straight from the input. */ 250 { 251 static const char *const names[] = { 252 "mimetype", "meta.xml", "content.xml", "docProps/core.xml", 253 "word/document.xml", "", "a" 254 }; 255 256 (void) EXTRACTOR_common_unzip_go_find_local_file (uf, 257 names[aux 258 % (sizeof (names) 259 / sizeof (char *) 260 )], 261 (int) (aux % 3u)); 262 } 263 (void) EXTRACTOR_common_unzip_close (uf); 264 le_fuzz_ec_cleanup (&st); 265 return 0; 266 } 267 268 269 /* ------------------------------------------------------------------ */ 270 /* Generator */ 271 /* ------------------------------------------------------------------ */ 272 273 /** 274 * Build a ZIP archive with @a nmem members. Sizes, offsets and counts 275 * are frequently made inconsistent on purpose: a central directory that 276 * disagrees with the local headers is the classic way into this parser. 277 */ 278 static size_t 279 fuzz_generate (struct fuzz_rng *rng, 280 uint8_t *buf, 281 size_t cap) 282 { 283 static const char *const names[] = { 284 "mimetype", "meta.xml", "content.xml", "docProps/core.xml", 285 "word/document.xml", "xl/workbook.xml", "META-INF/manifest.xml", 286 "a", "", "../../etc/passwd", 287 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 288 }; 289 size_t len = 0; 290 size_t cd_off; 291 unsigned int nmem = 1 + fuzz_below (rng, 6); 292 unsigned int i; 293 size_t lfh[8]; 294 size_t nmlen[8]; 295 const char *nm[8]; 296 uint32_t csize[8]; 297 298 if (cap < 256) 299 return 0; 300 buf[len++] = fuzz_chance (rng, 2) ? 0 : fuzz_byte (rng); 301 buf[len++] = fuzz_chance (rng, 3) ? 0 : (uint8_t) fuzz_below (rng, 0x40); 302 buf[len++] = fuzz_byte (rng); 303 buf[len++] = fuzz_byte (rng); 304 305 for (i = 0; i < nmem; i++) 306 { 307 unsigned int k; 308 uint32_t payload = fuzz_below (rng, 128); 309 310 nm[i] = names[fuzz_below (rng, 311 (uint32_t) (sizeof (names) / sizeof (char *)))]; 312 nmlen[i] = strlen (nm[i]); 313 lfh[i] = len; 314 csize[i] = payload; 315 fuzz_put_mem (buf, &len, cap, "PK\x03\x04", 4); 316 fuzz_put_le (buf, &len, cap, 20, 2); 317 fuzz_put_le (buf, &len, cap, 318 fuzz_chance (rng, 4) ? fuzz_byte (rng) : 0, 2); 319 fuzz_put_le (buf, &len, cap, fuzz_chance (rng, 2) ? 0 : 8, 2); 320 fuzz_put_le (buf, &len, cap, fuzz_next (rng), 4); 321 fuzz_put_le (buf, &len, cap, fuzz_next (rng), 4); /* crc */ 322 fuzz_put_le (buf, &len, cap, 323 fuzz_chance (rng, 4) ? fuzz_next (rng) : payload, 4); 324 fuzz_put_le (buf, &len, cap, 325 fuzz_chance (rng, 4) ? fuzz_next (rng) : payload, 4); 326 fuzz_put_le (buf, &len, cap, 327 fuzz_chance (rng, 6) ? fuzz_next (rng) : nmlen[i], 2); 328 fuzz_put_le (buf, &len, cap, 329 fuzz_chance (rng, 4) ? fuzz_below (rng, 4096) : 0, 2); 330 fuzz_put_mem (buf, &len, cap, nm[i], nmlen[i]); 331 if (len + payload > cap) 332 break; 333 for (k = 0; k < payload; k++) 334 buf[len++] = fuzz_chance (rng, 3) ? 0 : fuzz_byte (rng); 335 } 336 nmem = i; 337 cd_off = len; 338 for (i = 0; i < nmem; i++) 339 { 340 fuzz_put_mem (buf, &len, cap, "PK\x01\x02", 4); 341 fuzz_put_le (buf, &len, cap, 20, 2); 342 fuzz_put_le (buf, &len, cap, 20, 2); 343 fuzz_put_le (buf, &len, cap, 344 fuzz_chance (rng, 4) ? fuzz_byte (rng) : 0, 2); 345 fuzz_put_le (buf, &len, cap, fuzz_chance (rng, 2) ? 0 : 8, 2); 346 fuzz_put_le (buf, &len, cap, 0, 4); 347 fuzz_put_le (buf, &len, cap, fuzz_next (rng), 4); 348 fuzz_put_le (buf, &len, cap, 349 fuzz_chance (rng, 4) ? fuzz_next (rng) : csize[i], 4); 350 fuzz_put_le (buf, &len, cap, 351 fuzz_chance (rng, 4) ? fuzz_next (rng) : csize[i], 4); 352 fuzz_put_le (buf, &len, cap, 353 fuzz_chance (rng, 6) ? fuzz_next (rng) : nmlen[i], 2); 354 fuzz_put_le (buf, &len, cap, 355 fuzz_chance (rng, 5) ? fuzz_next (rng) : 0, 2); 356 fuzz_put_le (buf, &len, cap, 357 fuzz_chance (rng, 5) ? fuzz_next (rng) : 0, 2); 358 fuzz_put_le (buf, &len, cap, 0, 2); 359 fuzz_put_le (buf, &len, cap, 0, 2); 360 fuzz_put_le (buf, &len, cap, 0, 4); 361 fuzz_put_le (buf, &len, cap, 362 fuzz_chance (rng, 5) ? fuzz_next (rng) : lfh[i], 4); 363 fuzz_put_mem (buf, &len, cap, nm[i], nmlen[i]); 364 } 365 fuzz_put_mem (buf, &len, cap, "PK\x05\x06", 4); 366 fuzz_put_le (buf, &len, cap, 0, 2); 367 fuzz_put_le (buf, &len, cap, 0, 2); 368 fuzz_put_le (buf, &len, cap, 369 fuzz_chance (rng, 5) ? fuzz_next (rng) : nmem, 2); 370 fuzz_put_le (buf, &len, cap, 371 fuzz_chance (rng, 5) ? fuzz_next (rng) : nmem, 2); 372 fuzz_put_le (buf, &len, cap, 373 fuzz_chance (rng, 5) ? fuzz_next (rng) : len - cd_off, 4); 374 fuzz_put_le (buf, &len, cap, 375 fuzz_chance (rng, 5) ? fuzz_next (rng) : cd_off, 4); 376 { 377 /* An end-of-central-directory comment whose declared length runs 378 past the end of the file is the shortest path to a read past the 379 buffer. */ 380 size_t clen = fuzz_chance (rng, 3) ? fuzz_below (rng, 0x10000) : 0; 381 382 fuzz_put_le (buf, &len, cap, clen, 2); 383 if (! fuzz_chance (rng, 2)) 384 { 385 size_t k; 386 387 for (k = 0; (k < clen) && (len < cap); k++) 388 buf[len++] = fuzz_byte (rng); 389 } 390 } 391 return len; 392 } 393 394 395 /* ------------------------------------------------------------------ */ 396 /* Seed corpus */ 397 /* ------------------------------------------------------------------ */ 398 399 #define LE_UNZIP_NSEEDS 8 400 401 static uint8_t unzip_seed_buf[LE_UNZIP_NSEEDS][1024]; 402 static size_t unzip_seed_len[LE_UNZIP_NSEEDS]; 403 static int unzip_seeds_ready; 404 405 406 static void 407 unzip_build_seeds (void) 408 { 409 struct fuzz_rng rng; 410 unsigned int i; 411 412 if (unzip_seeds_ready) 413 return; 414 unzip_seeds_ready = 1; 415 for (i = 0; i < LE_UNZIP_NSEEDS; i++) 416 { 417 fuzz_rng_seed (&rng, 0x21D0u + i); 418 unzip_seed_len[i] = fuzz_generate (&rng, 419 unzip_seed_buf[i], 420 sizeof (unzip_seed_buf[i])); 421 if (unzip_seed_len[i] >= LE_FUZZ_EC_PREFIX) 422 { 423 unzip_seed_buf[i][0] = (uint8_t) i; 424 unzip_seed_buf[i][1] = 0; 425 } 426 } 427 } 428 429 430 static size_t 431 fuzz_seed_count (void) 432 { 433 unzip_build_seeds (); 434 return LE_UNZIP_NSEEDS; 435 } 436 437 438 static const uint8_t * 439 fuzz_seed_get (size_t idx, 440 size_t *len) 441 { 442 unzip_build_seeds (); 443 *len = unzip_seed_len[idx]; 444 return unzip_seed_buf[idx]; 445 }