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`.