libextractor

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

README (12583B)


      1 GNU libextractor -- in-process fuzzing harnesses
      2 ================================================
      3 
      4 This directory replaces the old `src/plugins/fuzz_default.sh`, which ran
      5 `zzuf` over the test corpus and invoked the `extract` *binary* once per
      6 mutation.  That approach had four problems, and each of them is what one
      7 of the design decisions below is answering:
      8 
      9   * it forked a process per input, so it managed a few hundred
     10     executions per second where an in-process harness manages hundreds of
     11     thousands;
     12   * `extract` runs plugins **out of process**, so a plugin that crashed
     13     produced a dead child that the library dutifully restarted -- the
     14     script saw a clean exit status and reported success;
     15   * it had no sanitizer, so only a crash the kernel delivered was
     16     noticed.  A read of 200 bytes past the end of a 16 KiB shared memory
     17     window is invisible without one, and that is precisely the shape of
     18     almost every bug in this library;
     19   * it had no coverage feedback, so the hundredth mutation of a file was
     20     no more likely to reach new code than the first.
     21 
     22 Every harness here is *dual mode*:
     23 
     24   * it exports the libFuzzer entry point
     25 
     26         int LLVMFuzzerTestOneInput (const uint8_t *data, size_t size);
     27 
     28     so the same source links against clang/libFuzzer, AFL++ or
     29     honggfuzz (see `../../contrib/oss-fuzz/`), and
     30 
     31   * it ships a **built-in standalone driver** (`fuzz_common.h`) with a
     32     deterministic, seeded generator + mutator loop, so it is useful with
     33     nothing but gcc and `-fsanitize=address,undefined`.
     34 
     35 The standalone driver is compiled unless `FUZZ_NO_MAIN` is defined.
     36 
     37 
     38 -------------------------------------------------------------------
     39 1. The harnesses
     40 -------------------------------------------------------------------
     41 
     42 1.1 Per-plugin targets
     43 ----------------------
     44 
     45 `fuzz_plugin.c` is compiled once per plugin, producing `fuzz_gif`,
     46 `fuzz_png`, `fuzz_rtf` and so on -- 21 in-tree parsers plus one per
     47 plugin that wraps a third-party library, when that library is present.
     48 Each target links the plugin's own `.c` files, so every line of the
     49 parser is instrumented and neither `dlopen()` nor `fork()` nor the IPC
     50 layer is in the way.
     51 
     52 One target per format rather than one target for all of them, because
     53 the corpus is what makes a format fuzzer work: `fuzz_png` starts from
     54 PNG files and mutates PNGs, and its coverage feedback is not diluted by
     55 inputs that only ever exercise the RTF tokeniser.
     56 
     57 1.2 Core targets
     58 ----------------
     59 
     60 fuzz_extract        end-to-end: `EXTRACTOR_plugin_add_defaults()` +
     61                     `EXTRACTOR_extract()`, plugins in-process.  This is
     62                     the only target that covers plugin discovery, the
     63                     dispatch loop, and the plugins that wrap a
     64                     third-party library, all against the real plugin set.
     65 
     66 fuzz_datasource     `src/main/extractor_datasource.c`: the first code in
     67                     the library to touch attacker bytes, and the only one
     68                     that runs *before* any plugin is consulted.  The gzip
     69                     header walker (FEXTRA / FNAME / FCOMMENT / FHCRC) and
     70                     the "seek backwards through a decompressed stream"
     71                     path are the interesting parts.
     72 
     73 fuzz_unzip          `src/common/unzip.c`, the in-tree ZIP reader shared
     74                     by the odf, msoffice and zip plugins -- hand-written
     75                     code descended from unzip 1.00, reachable from three
     76                     formats at once.  The highest-value single target
     77                     here.
     78 
     79 fuzz_ipc            `EXTRACTOR_IPC_process_reply_()`, the parser in the
     80                     *trusted parent* for a byte stream produced by an
     81                     *untrusted child*.  That is the security boundary of
     82                     the whole out-of-process design, so it is fuzzed on
     83                     its own even though the function is short.
     84 
     85 fuzz_convert        `EXTRACTOR_common_convert_to_utf8()` and the metatype
     86                     tables.  The helper is called by nsfe, rtf, msoffice
     87                     and png with a length *and* a charset name that both
     88                     come out of the file being parsed.
     89 
     90 
     91 -------------------------------------------------------------------
     92 2. Why the harness finds things `extract` does not
     93 -------------------------------------------------------------------
     94 
     95 The substance is in `fuzz_ec.h`, which models
     96 `struct EXTRACTOR_ExtractContext` the way `extractor_plugin_main.c`
     97 implements it, and then makes two of its properties enforceable.
     98 
     99 2.1 The read window is exact
    100 ----------------------------
    101 
    102 `plugin_env_read()` hands the plugin a pointer *into a shared memory
    103 window* and returns how many bytes are valid there.  That is at most
    104 `shm_map_size` -- 16 KiB by default -- and at most what is left of the
    105 file.  A plugin that asks for 100 KiB, gets 16 KiB, and then walks all
    106 100 KiB is reading memory it was never given.  In production that memory
    107 is a live mmap, so nothing crashes, no test fails, and the bug is
    108 invisible.
    109 
    110 Here every window is a fresh `malloc()` of *exactly* the returned byte
    111 count, so ASAN's redzone starts at the first byte the plugin was not
    112 promised.  The window size is fuzzer-controlled (byte 0 of the input),
    113 because "the read returned less than I asked for" is the single most
    114 productive precondition in this library, and the 16 KiB default hides it
    115 for every file smaller than that.
    116 
    117 2.2 The window slides
    118 ---------------------
    119 
    120 A pointer from `read()` stays valid only while the window still covers
    121 that part of the file.  A `read()` or `seek()` that needs data outside
    122 the window makes the core refill it, and the plugin's old pointer then
    123 addresses *different file bytes* than it believes it holds.
    124 
    125 The model tracks the same window as `plugin_env_read()` and
    126 `plugin_env_seek()` do, so a `seek (0, SEEK_CUR)` -- which never leaves
    127 the window -- invalidates nothing, exactly as in production.  When the
    128 window really does slide, every slice handed out from it is freed, so a
    129 retained pointer becomes an ASAN use-after-free.
    130 
    131 Getting this boundary right is the difference between a report worth
    132 reading and noise: an earlier version invalidated on *every* seek and
    133 immediately "found" a bug in png_extractor.c that does not exist under
    134 either shipped implementation.  `LE_FUZZ_STRICT_WINDOW=1` restores that
    135 stricter behaviour on purpose, since it models the in-process
    136 implementation, whose single `ctx->buf` really is overwritten by every
    137 read.
    138 
    139 2.3 The metadata processor is an oracle
    140 ---------------------------------------
    141 
    142 `transmit_reply()` writes `data_len` bytes starting at `data` to a pipe
    143 and calls `strlen()` on the mime type.  The harness touches exactly those
    144 bytes, so a plugin that reports a length longer than its buffer is an
    145 ASAN report here and a real out-of-bounds read in production.
    146 
    147 `LE_FUZZ_STRICT_PROC=1` additionally flags string metadata that is not
    148 0-terminated.  Off by default: that is a contract violation rather than a
    149 memory error, and it is common enough in the older plugins to drown
    150 everything else.
    151 
    152 2.4 Fault injection
    153 -------------------
    154 
    155 Byte 1 of the input arms the failures that really can happen and that
    156 plugins rarely handle: `get_size()` returning `UINT64_MAX` after a failed
    157 IPC round, `read()` or `seek()` returning -1, the application asking to
    158 stop after N items, and running the extract method twice against the same
    159 context (plugins must not carry state from one file to the next -- the
    160 same process handles every file in a directory walk).
    161 
    162 
    163 -------------------------------------------------------------------
    164 3. Input format
    165 -------------------------------------------------------------------
    166 
    167 Every `fuzz_<plugin>` target and `fuzz_unzip` take:
    168 
    169     byte 0    read window size selector; 0 selects the production 16 KiB
    170     byte 1    fault-injection bitmask (LE_FUZZ_FAULT_* in fuzz_ec.h)
    171     byte 2    call index at which the injected fault fires
    172     byte 3    auxiliary parameter
    173     byte 4..  the file image
    174 
    175 An all-zero prefix is exactly what production does, so a corpus entry is
    176 four zero bytes followed by a real file.  That is what
    177 `contrib/oss-fuzz/make_seed_corpus.sh` builds out of
    178 `src/plugins/testdata/`.
    179 
    180 `fuzz_extract` uses a two-byte prefix, `fuzz_convert` a two-byte prefix,
    181 `fuzz_datasource` four bytes, and `fuzz_ipc` none (its input is the raw
    182 message stream).  Each is documented at the top of its own source file.
    183 
    184 
    185 -------------------------------------------------------------------
    186 4. Building and running
    187 -------------------------------------------------------------------
    188 
    189     ./bootstrap
    190     ./configure --enable-fuzzing --enable-static \
    191                 CC=clang \
    192                 CFLAGS="-g -O1 -fno-omit-frame-pointer \
    193                         -fsanitize=address,undefined" \
    194                 LDFLAGS="-fsanitize=address,undefined"
    195     make
    196     make -C src/fuzz check
    197 
    198 `make check` runs every harness under its built-in driver for
    199 `LE_FUZZ_ITERATIONS` iterations (20000 by default).
    200 
    201 Note that on a tree that does not carry the fixes from `issues.txt` this
    202 `make check` *fails*, by design: the harnesses find those bugs within a
    203 few thousand iterations.  It cannot affect an ordinary build, because
    204 `--enable-fuzzing` defaults to no and `src/fuzz/` is not even configured
    205 without it.
    206 
    207 A longer session:
    208 
    209     make -C src/fuzz check LE_FUZZ_ITERATIONS=5000000 LE_FUZZ_SEED=$RANDOM
    210 
    211 Every run is fully reproducible from (harness, seed).  A failing input is
    212 dumped to `$LE_FUZZ_CRASH_DIR` and replayed with:
    213 
    214     ./fuzz_png --file=crashes/crash-fuzz_png-seed1-iter28.bin
    215 
    216 Replay the whole checked-in corpus, which is what CI should do after a
    217 fix:
    218 
    219     make -C src/fuzz check-corpus
    220 
    221 For a real campaign use libFuzzer via `contrib/oss-fuzz/build.sh`; see
    222 `../../contrib/oss-fuzz/README`.
    223 
    224 `CAMPAIGN.md` answers the two questions that come up as soon as the
    225 campaign is longer than a coffee break -- how to split the budget across
    226 the targets, and how long it is worth running -- from a measured
    227 12-core hour rather than from intuition.  Both answers are unobvious:
    228 half the targets reach 99% of their final coverage within 93 seconds,
    229 and a flat allocation spends most of its budget on targets that finished
    230 in the first minute.  `contrib/oss-fuzz/run_campaign.sh` implements the
    231 conclusion:
    232 
    233     contrib/oss-fuzz/run_campaign.sh -b /path/to/build.sh-output -p nightly
    234 
    235 Environment knobs, all read once at startup:
    236 
    237     LE_FUZZ_ITERATIONS    iterations of the built-in driver
    238     LE_FUZZ_SEED          PRNG seed
    239     LE_FUZZ_TIMEOUT       per-iteration watchdog, seconds (0 disables)
    240     LE_FUZZ_CRASH_DIR     where reproducers are written
    241     LE_FUZZ_SKIP_SEEDS    skip the built-in seed corpus at the start
    242     LE_FUZZ_VERBOSE       be chatty
    243     LE_FUZZ_STRICT_WINDOW invalidate the read window on every call
    244     LE_FUZZ_STRICT_PROC   flag non-0-terminated string metadata
    245 
    246 `fuzz_extract` additionally needs `LIBEXTRACTOR_PREFIX` pointing at the
    247 directory holding the built plugin modules, normally
    248 `src/plugins/.libs`; `make check` sets it.
    249 
    250 
    251 -------------------------------------------------------------------
    252 5. The corpus
    253 -------------------------------------------------------------------
    254 
    255 `corpus/<target>/` holds the harnesses' own built-in seeds and is
    256 regenerated by
    257 
    258     make -C src/fuzz refresh-corpus
    259 
    260 It deliberately does *not* contain copies of `src/plugins/testdata/`:
    261 those files are already in the tree, and
    262 `contrib/oss-fuzz/make_seed_corpus.sh` prepends the configuration prefix
    263 to them at build time instead.  Run that script by hand to materialise
    264 the full corpus for a local campaign.
    265 
    266 `corpus/known-findings/` holds the reproducers for the entries in
    267 `issues.txt`.  `make check-corpus` replays it, so each one stays a
    268 permanent regression test.
    269 
    270 
    271 -------------------------------------------------------------------
    272 6. Adding a plugin target
    273 -------------------------------------------------------------------
    274 
    275 1. Give the plugin an `LE_ID_*` number in `fuzz_plugin_name.h` and add
    276    the `#elif` block with its magic bytes and body shape.  This only
    277    feeds the *generator*; getting it wrong costs coverage, never
    278    soundness.
    279 2. Add the target to `Makefile.am`, following one of the existing
    280    blocks: `_SOURCES = fuzz_plugin.c`, `_CPPFLAGS` with the two `-D`
    281    flags, `nodist_..._SOURCES` with the plugin's own sources, and the
    282    libraries it needs.
    283 3. Add it to `PLUGIN_FUZZERS` in `../../contrib/oss-fuzz/build.sh` and
    284    to the `testdata_glob` table in
    285    `../../contrib/oss-fuzz/make_seed_corpus.sh`.