libextractor

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

fuzz_ec.h (16251B)


      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_ec.h
     22  * @brief contract-exact model of `struct EXTRACTOR_ExtractContext`
     23  * @author Christian Grothoff
     24  *
     25  * This is what makes the plugin harnesses find things that running
     26  * `extract` over a mutated file does not.  Two properties matter:
     27  *
     28  * 1. **The read window is exact.**  `plugin_env_read()` in
     29  *    extractor_plugin_main.c hands the plugin a pointer *into the shared
     30  *    memory window* and returns how many bytes are valid there -- which
     31  *    is at most `shm_map_size` (16 KiB by default), and at most what is
     32  *    left of the file.  A plugin that asks for 100 KiB and gets 16 KiB
     33  *    but then dereferences all 100 KiB reads memory it was never given.
     34  *    In production that memory is a live mmap, so nothing crashes and
     35  *    the bug is invisible; here every window is a fresh `malloc()` of
     36  *    *exactly* the returned byte count, so AddressSanitizer's redzone
     37  *    turns the same access into a hard error.
     38  *
     39  *    The window size is fuzzer-controlled, because "read() returned less
     40  *    than I asked for" is the single most productive precondition in
     41  *    this library and a 16 KiB default hides it for every file smaller
     42  *    than that.
     43  *
     44  * 2. **The window slides.**  The pointer a plugin got from `read()` stays
     45  *    valid only for as long as the shared memory window it points into
     46  *    still covers that part of the file.  A `read()` or `seek()` that
     47  *    needs data outside the window makes the core refill it, and the
     48  *    plugin's old pointer then addresses *different file bytes* than the
     49  *    plugin believes it holds.  The model tracks the same window as
     50  *    `plugin_env_read()`/`plugin_env_seek()` do -- so a `seek(0,
     51  *    SEEK_CUR)`, which never leaves the window, does not invalidate
     52  *    anything, exactly as in production -- and frees every slice handed
     53  *    out from a window when that window slides, so ASAN reports a
     54  *    retained pointer as a use-after-free.
     55  *
     56  *    Set `LE_FUZZ_STRICT_WINDOW=1` to invalidate on *every* call instead.
     57  *    That models the in-process implementation, whose single `ctx->buf`
     58  *    is overwritten by each read; it finds more, at the price of also
     59  *    flagging plugins that are correct under the default out-of-process
     60  *    policy.
     61  *
     62  * On top of that the metadata processor is an oracle.  The plugin-side
     63  * `transmit_reply()` writes `data_len` bytes from `data` to a pipe and
     64  * calls `strlen()` on the mime type, so this harness touches exactly the
     65  * same bytes -- an over-long `data_len` is a real out-of-bounds read in
     66  * production, not an artefact of the harness.
     67  */
     68 #ifndef LE_FUZZ_EC_H
     69 #define LE_FUZZ_EC_H 1
     70 
     71 #include "platform.h"
     72 #include "extractor.h"
     73 #include "fuzz_common.h"
     74 
     75 /**
     76  * Size of the shared memory window the core actually uses; must be kept
     77  * in sync with #DEFAULT_SHM_SIZE in src/main/extractor.c.
     78  */
     79 #define LE_FUZZ_DEFAULT_SHM (16 * 1024)
     80 
     81 /**
     82  * Upper bound the plugin side enforces on a single meta data item; see
     83  * #MAX_META_DATA in src/main/extractor_ipc.h.
     84  */
     85 #define LE_FUZZ_MAX_META_DATA (32 * 1024)
     86 
     87 /**
     88  * Number of configuration bytes that precede the file image in every
     89  * plugin-harness input.  An all-zero prefix means "no fault injection,
     90  * real 16 KiB window", i.e. exactly what production does, so a corpus
     91  * entry is just four zero bytes followed by the file.
     92  */
     93 #define LE_FUZZ_EC_PREFIX 4
     94 
     95 /**
     96  * Candidate read-window sizes.  Index 0 is the production value; the
     97  * rest deliberately include sizes that force a short read on almost
     98  * every call, and sizes just below/above a power of two.
     99  */
    100 static const size_t le_fuzz_windows[] = {
    101   LE_FUZZ_DEFAULT_SHM, 1, 2, 3, 5, 7, 15, 16, 31, 64, 127, 255, 256,
    102   511, 1024, 4095, 8192, 16383, 16385, 32768, 65536
    103 };
    104 
    105 /**
    106  * State behind the #EXTRACTOR_ExtractContext handed to the plugin.
    107  */
    108 struct fuzz_ec_state
    109 {
    110   /**
    111    * The file image.  Owned by the caller.
    112    */
    113   const uint8_t *img;
    114 
    115   /**
    116    * Number of bytes in #img.
    117    */
    118   size_t img_len;
    119 
    120   /**
    121    * Current read position, in [0, #img_len].
    122    */
    123   uint64_t pos;
    124 
    125   /**
    126    * Size of the shared memory window, i.e. the largest number of bytes
    127    * a single read() may return.  Models `shm_map_size`.
    128    */
    129   size_t window;
    130 
    131   /**
    132    * File offset the current window starts at.  Models `shm_off`.
    133    */
    134   uint64_t win_off;
    135 
    136   /**
    137    * Number of bytes of the file the current window holds.  Models
    138    * `shm_ready_bytes`.
    139    */
    140   size_t win_len;
    141 
    142   /**
    143    * Non-zero once a window has been established at all.
    144    */
    145   int win_valid;
    146 
    147   /**
    148    * Slices handed out from the current window.  Each is an exact-size
    149    * allocation, so ASAN's redzone starts right after the last byte the
    150    * plugin was promised; all of them are freed when the window slides.
    151    */
    152   uint8_t **slices;
    153 
    154   /**
    155    * Number of entries in #slices.
    156    */
    157   size_t slices_len;
    158 
    159   /**
    160    * Capacity of #slices.  Grown geometrically: a plugin can issue
    161    * millions of one-byte reads inside a single window, and a
    162    * grow-by-one realloc() would make the *harness* quadratic and turn
    163    * every such input into a bogus "slow unit".
    164    */
    165   size_t slices_cap;
    166 
    167   /**
    168    * Number of read() calls so far.
    169    */
    170   unsigned int reads;
    171 
    172   /**
    173    * Number of seek() calls so far.
    174    */
    175   unsigned int seeks;
    176 
    177   /**
    178    * Number of proc() calls so far.
    179    */
    180   unsigned int procs;
    181 
    182   /**
    183    * Fault-injection bitmask, see #LE_FUZZ_FAULT_*.
    184    */
    185   unsigned int faults;
    186 
    187   /**
    188    * Call index at which an injected fault fires.
    189    */
    190   unsigned int fault_at;
    191 
    192   /**
    193    * Set once get_size() has returned UINT64_MAX; the failure is
    194    * transient in production (one failed IPC round), so it only fires
    195    * once here too.
    196    */
    197   int size_failed;
    198 };
    199 
    200 #define LE_FUZZ_FAULT_SIZE     0x01
    201 #define LE_FUZZ_FAULT_READ     0x02
    202 #define LE_FUZZ_FAULT_SEEK     0x04
    203 #define LE_FUZZ_FAULT_STOP     0x08
    204 #define LE_FUZZ_RUN_TWICE      0x10
    205 #define LE_FUZZ_ODD_ALIGN      0x20
    206 
    207 /**
    208  * Cached value of $LE_FUZZ_STRICT_WINDOW.
    209  */
    210 static int le_fuzz_strict_window = -1;
    211 
    212 /**
    213  * Cached value of $LE_FUZZ_STRICT_PROC.
    214  */
    215 static int le_fuzz_strict_proc = -1;
    216 
    217 
    218 /**
    219  * Free every slice handed out from the window that is about to be
    220  * replaced.  In production those pointers keep addressing live shared
    221  * memory, but the bytes behind them are no longer the ones the plugin
    222  * read, so any use of them is a defect; freeing turns that into an ASAN
    223  * report.
    224  *
    225  * @param st harness state
    226  */
    227 static void
    228 le_fuzz_drop_slices (struct fuzz_ec_state *st)
    229 {
    230   size_t i;
    231 
    232   for (i = 0; i < st->slices_len; i++)
    233     free (st->slices[i]);
    234   free (st->slices);
    235   st->slices = NULL;
    236   st->slices_len = 0;
    237   st->slices_cap = 0;
    238 }
    239 
    240 
    241 /**
    242  * Slide the window so that it starts at @a off.
    243  *
    244  * @param st harness state
    245  * @param off new window start
    246  */
    247 static void
    248 le_fuzz_slide (struct fuzz_ec_state *st,
    249                uint64_t off)
    250 {
    251   le_fuzz_drop_slices (st);
    252   st->win_off = off;
    253   st->win_len = (size_t) (st->img_len - off);
    254   if (st->win_len > st->window)
    255     st->win_len = st->window;
    256   st->win_valid = 1;
    257 }
    258 
    259 
    260 /**
    261  * @return non-zero if @a off is covered by the current window
    262  */
    263 static int
    264 le_fuzz_in_window (const struct fuzz_ec_state *st,
    265                    uint64_t off)
    266 {
    267   if (! st->win_valid)
    268     return 0;
    269   if (le_fuzz_strict_window)
    270     return 0;
    271   return ( (st->win_off <= off) &&
    272            (off < st->win_off + st->win_len) );
    273 }
    274 
    275 
    276 /**
    277  * Hand out an exact-size copy of @a n bytes starting at file offset
    278  * @a off.  The allocation is remembered so that it can be freed when the
    279  * window slides.
    280  *
    281  * @param st harness state
    282  * @param off file offset
    283  * @param n number of bytes
    284  * @return pointer the plugin may read @a n bytes from
    285  */
    286 static uint8_t *
    287 le_fuzz_slice (struct fuzz_ec_state *st,
    288                uint64_t off,
    289                size_t n)
    290 {
    291   uint8_t *p = (uint8_t *) malloc ((0 == n) ? 1 : n);
    292 
    293   if (NULL == p)
    294     abort ();
    295   if (0 != n)
    296     memcpy (p, st->img + off, n);
    297   if (st->slices_len == st->slices_cap)
    298   {
    299     size_t cap = (0 == st->slices_cap) ? 64 : st->slices_cap * 2;
    300     uint8_t **v = (uint8_t **) realloc (st->slices,
    301                                         sizeof (uint8_t *) * cap);
    302 
    303     if (NULL == v)
    304       abort ();
    305     st->slices = v;
    306     st->slices_cap = cap;
    307   }
    308   st->slices[st->slices_len++] = p;
    309   return p;
    310 }
    311 
    312 
    313 /**
    314  * Model of `plugin_env_read()` from src/main/extractor_plugin_main.c.
    315  *
    316  * @param cls a `struct fuzz_ec_state`
    317  * @param[out] data set to the start of the window
    318  * @param count number of bytes requested
    319  * @return number of bytes available at @a data, -1 on error
    320  */
    321 static ssize_t
    322 le_fuzz_read (void *cls,
    323               void **data,
    324               size_t count)
    325 {
    326   struct fuzz_ec_state *st = cls;
    327 
    328   *data = NULL;
    329   st->reads++;
    330   if ( (0 != (st->faults & LE_FUZZ_FAULT_READ)) &&
    331        (st->reads == st->fault_at) )
    332     return -1;                  /* models a failed IPC round */
    333   /* clamp to what is left of the file, exactly as plugin_env_read()
    334      does (including its overflow guard) */
    335   if ( (count + st->pos > st->img_len) ||
    336        (count + st->pos < st->pos) )
    337     count = (size_t) (st->img_len - st->pos);
    338   /* refill the window if the read starts outside it, as
    339      plugin_env_read() does via its plugin_env_seek() call */
    340   if (! le_fuzz_in_window (st, st->pos))
    341     le_fuzz_slide (st, st->pos);
    342   /* clamp to what the window holds */
    343   if (st->pos + count > st->win_off + st->win_len)
    344     count = (size_t) (st->win_off + st->win_len - st->pos);
    345   *data = le_fuzz_slice (st, st->pos, count);
    346   st->pos += count;
    347   return (ssize_t) count;
    348 }
    349 
    350 
    351 /**
    352  * Model of `plugin_env_seek()` from src/main/extractor_plugin_main.c.
    353  *
    354  * @param cls a `struct fuzz_ec_state`
    355  * @param pos offset to seek to
    356  * @param whence SEEK_SET, SEEK_CUR or SEEK_END
    357  * @return new absolute position, -1 on error
    358  */
    359 static int64_t
    360 le_fuzz_seek (void *cls,
    361               int64_t pos,
    362               int whence)
    363 {
    364   struct fuzz_ec_state *st = cls;
    365   uint64_t npos;
    366 
    367   st->seeks++;
    368   if ( (0 != (st->faults & LE_FUZZ_FAULT_SEEK)) &&
    369        (st->seeks == st->fault_at) )
    370     return -1;
    371   switch (whence)
    372   {
    373   case SEEK_CUR:
    374     if ( (pos < 0) &&
    375          (st->pos < (uint64_t) (-pos)) )
    376       return -1;
    377     pos = (int64_t) (st->pos + pos);
    378     break;
    379   case SEEK_END:
    380     if (pos > 0)
    381       return -1;
    382     pos = (int64_t) (st->img_len + pos);
    383     break;
    384   case SEEK_SET:
    385     break;
    386   default:
    387     return -1;
    388   }
    389   if ( (pos < 0) ||
    390        ((uint64_t) pos > st->img_len) )
    391     return -1;
    392   npos = (uint64_t) pos;
    393   /* plugin_env_seek() returns without touching the window whenever the
    394      target is already inside it; otherwise the core refills it and every
    395      pointer into the old window goes stale */
    396   if (! le_fuzz_in_window (st, npos))
    397     le_fuzz_slide (st, npos);
    398   st->pos = npos;
    399   return (int64_t) npos;
    400 }
    401 
    402 
    403 /**
    404  * Model of `plugin_env_get_size()`.
    405  *
    406  * @param cls a `struct fuzz_ec_state`
    407  * @return file size, UINT64_MAX when the injected IPC failure fires
    408  */
    409 static uint64_t
    410 le_fuzz_get_size (void *cls)
    411 {
    412   struct fuzz_ec_state *st = cls;
    413 
    414   if ( (0 != (st->faults & LE_FUZZ_FAULT_SIZE)) &&
    415        (! st->size_failed) )
    416   {
    417     st->size_failed = 1;
    418     return UINT64_MAX;          /* documented "IPC failure" return */
    419   }
    420   return (uint64_t) st->img_len;
    421 }
    422 
    423 
    424 /**
    425  * Metadata processor used as an oracle.  Touches precisely the bytes
    426  * that `transmit_reply()` in extractor_plugin_main.c touches.
    427  *
    428  * @param cls a `struct fuzz_ec_state`
    429  * @param plugin_name name of the reporting plugin
    430  * @param type metadata type
    431  * @param format metadata format
    432  * @param data_mime_type mime type of @a data, may be NULL
    433  * @param data the metadata
    434  * @param data_len number of bytes in @a data
    435  * @return 0 to continue, 1 to abort
    436  */
    437 static int
    438 le_fuzz_proc (void *cls,
    439               const char *plugin_name,
    440               enum EXTRACTOR_MetaType type,
    441               enum EXTRACTOR_MetaFormat format,
    442               const char *data_mime_type,
    443               const char *data,
    444               size_t data_len)
    445 {
    446   struct fuzz_ec_state *st = cls;
    447   volatile unsigned int sink = 0;
    448   size_t i;
    449 
    450   st->procs++;
    451   if (NULL == plugin_name)
    452     fuzz_report_finding ("plugin reported a NULL plugin name");
    453   /* transmit_reply() does strlen() on both of these */
    454   sink += (unsigned int) strlen (plugin_name);
    455   if (NULL != data_mime_type)
    456     sink += (unsigned int) strlen (data_mime_type);
    457   if ( (NULL == data) &&
    458        (0 != data_len) )
    459     fuzz_report_finding ("plugin reported NULL data with a non-zero length");
    460   if (NULL != data)
    461   {
    462     /* transmit_reply() writes exactly data_len bytes starting at data;
    463        reading them here is what production does */
    464     for (i = 0; i < data_len; i++)
    465       sink += (unsigned char) data[i];
    466     if ( (le_fuzz_strict_proc) &&
    467          (0 != data_len) &&
    468          ( (EXTRACTOR_METAFORMAT_UTF8 == format) ||
    469            (EXTRACTOR_METAFORMAT_C_STRING == format) ) &&
    470          ('\0' != data[data_len - 1]) )
    471       fuzz_report_finding ("plugin reported a string metadata value that is "
    472                            "not 0-terminated");
    473   }
    474   (void) sink;
    475   (void) type;
    476   if ( (0 != (st->faults & LE_FUZZ_FAULT_STOP)) &&
    477        (st->procs > st->fault_at) )
    478     return 1;                   /* application asked us to stop */
    479   return 0;
    480 }
    481 
    482 
    483 /**
    484  * Read the harness-wide environment knobs once.
    485  */
    486 static void
    487 le_fuzz_ec_init_env (void)
    488 {
    489   if (-1 != le_fuzz_strict_window)
    490     return;
    491   le_fuzz_strict_window = (0 != fuzz_env_ulong ("LE_FUZZ_STRICT_WINDOW", 0));
    492   le_fuzz_strict_proc = (0 != fuzz_env_ulong ("LE_FUZZ_STRICT_PROC", 0));
    493 }
    494 
    495 
    496 /**
    497  * Set up @a ec and @a st from a fuzz input.  The first
    498  * #LE_FUZZ_EC_PREFIX bytes are configuration, the rest is the file
    499  * image.
    500  *
    501  * @param[out] ec context to fill in
    502  * @param[out] st state to fill in
    503  * @param data the fuzz input
    504  * @param size number of bytes in @a data
    505  * @return 0 if the input was too short to be usable
    506  */
    507 static int
    508 le_fuzz_ec_setup (struct EXTRACTOR_ExtractContext *ec,
    509                   struct fuzz_ec_state *st,
    510                   const uint8_t *data,
    511                   size_t size)
    512 {
    513   size_t off;
    514 
    515   le_fuzz_ec_init_env ();
    516   if (size < LE_FUZZ_EC_PREFIX)
    517     return 0;
    518   memset (st, 0, sizeof (*st));
    519   st->window = le_fuzz_windows[data[0]
    520                                % (sizeof (le_fuzz_windows)
    521                                   / sizeof (le_fuzz_windows[0]))];
    522   st->faults = data[1];
    523   st->fault_at = data[2];
    524   off = LE_FUZZ_EC_PREFIX;
    525   if ( (0 != (st->faults & LE_FUZZ_ODD_ALIGN)) &&
    526        (off < size) )
    527     off++;
    528   st->img = data + off;
    529   st->img_len = size - off;
    530   memset (ec, 0, sizeof (*ec));
    531   ec->cls = st;
    532   ec->config = NULL;
    533   ec->read = &le_fuzz_read;
    534   ec->seek = &le_fuzz_seek;
    535   ec->get_size = &le_fuzz_get_size;
    536   ec->proc = &le_fuzz_proc;
    537   return 1;
    538 }
    539 
    540 
    541 /**
    542  * Release everything @a st still owns.
    543  *
    544  * @param st state to clean up
    545  */
    546 static void
    547 le_fuzz_ec_cleanup (struct fuzz_ec_state *st)
    548 {
    549   le_fuzz_drop_slices (st);
    550   st->win_valid = 0;
    551   st->win_off = 0;
    552   st->win_len = 0;
    553 }
    554 
    555 
    556 /**
    557  * Rewind @a st so that the same context can be handed to the extract
    558  * method a second time.  Plugins must be stateless across files.
    559  *
    560  * @param st state to reset
    561  */
    562 static void
    563 le_fuzz_ec_rewind (struct fuzz_ec_state *st)
    564 {
    565   le_fuzz_ec_cleanup (st);
    566   st->pos = 0;
    567   st->reads = 0;
    568   st->seeks = 0;
    569   st->procs = 0;
    570   st->size_failed = 0;
    571 }
    572 
    573 
    574 #endif /* LE_FUZZ_EC_H */