libextractor

GNU libextractor
Log | Files | Refs | Submodules | README | LICENSE

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 }