libmicrohttpd

HTTP/1.x server C library (MHD 1.x, stable)
Log | Files | Refs | Submodules | README | LICENSE

README (26451B)


      1 GNU libmicrohttpd -- OSS-Fuzz integration
      2 ========================================
      3 
      4 This directory contains everything needed to run the in-process fuzzing
      5 harnesses of `src/fuzz/` on Google's OSS-Fuzz service: the build script,
      6 the project metadata, the container recipe, the seed-corpus packager,
      7 per-harness dictionaries and per-harness `.options` files.
      8 
      9 It is the "continuous" half of `TESTING.md` section **P4**; `src/fuzz/`
     10 is the other half.  Read `src/fuzz/README` first -- it explains what the
     11 four harnesses do, what they have already found, and how to run them
     12 without clang.
     13 
     14 
     15 -------------------------------------------------------------------
     16 0. This is deliberately NOT part of contrib/ci/
     17 -------------------------------------------------------------------
     18 
     19 **OSS-Fuzz is not a CI job and nothing here is wired into
     20 `contrib/ci/jobs/`.**  That is on purpose, at the maintainer's request:
     21 
     22   * an OSS-Fuzz run is a *continuous, hosted, unbounded* campaign owned
     23     by Google's infrastructure, not a bounded per-push check.  A CI job
     24     must terminate in minutes and must be reproducible offline; neither
     25     is true here;
     26   * the artifacts in this directory are consumed by the
     27     `google/oss-fuzz` repository (`projects/libmicrohttpd/`), not by
     28     this tree's build system;
     29   * running these builds needs Docker, the OSS-Fuzz base images and
     30     network access.
     31 
     32 The bounded, in-tree fuzzing that *does* belong in CI already exists and
     33 is unrelated to this directory: `make -C src/fuzz check` (a few seconds
     34 per harness) and `make -C src/fuzz check-corpus` (corpus replay).  Wire
     35 *those* into `contrib/ci/`, never this.
     36 
     37 Nothing in this directory is referenced by any `Makefile.am`; adding it
     38 to the build system is not required and not wanted.
     39 
     40 
     41 -------------------------------------------------------------------
     42 1. Layout
     43 -------------------------------------------------------------------
     44 
     45     build.sh                    the OSS-Fuzz build script
     46     project.yaml                OSS-Fuzz project metadata
     47     Dockerfile                  the base-builder container recipe
     48     make_seed_corpus.sh         packages src/fuzz/corpus/ into
     49                                 $OUT/<fuzzer>_seed_corpus.zip
     50     fuzz_request.options        per-harness libFuzzer options (max_len, dict)
     51     fuzz_options.options
     52     fuzz_eventloop.options
     53     fuzz_str.options
     54     fuzz_memorypool.options
     55     fuzz_auth_header.options
     56     fuzz_postprocessor.options
     57     dicts/fuzz_request.dict     per-harness fuzzing dictionaries
     58     dicts/fuzz_options.dict
     59     dicts/fuzz_eventloop.dict
     60     dicts/fuzz_str.dict
     61     dicts/fuzz_memorypool.dict
     62     dicts/fuzz_auth_header.dict
     63     dicts/fuzz_postprocessor.dict
     64     README                      this file
     65 
     66 Of these, only `build.sh`, `project.yaml` and `Dockerfile` are copied
     67 into `google/oss-fuzz`; everything else is read out of the cloned
     68 libmicrohttpd checkout at build time, which is why the dictionaries and
     69 the corpus packager live here and not in the OSS-Fuzz repository.
     70 
     71 
     72 -------------------------------------------------------------------
     73 2. The fuzz targets
     74 -------------------------------------------------------------------
     75 
     76     fuzz_request            a real struct MHD_Daemon driven over a
     77                             socketpair; consumes daemon options *and*
     78                             stream segmentation from the input.  Bytes
     79                             4-9 (see src/fuzz/README section 2.2.1)
     80                             additionally select the response
     81                             constructor, the authentication entry point,
     82                             the event-loop API, and whether the
     83                             connection is suspended or upgraded
     84     fuzz_options            the daemon configuration surface: the input
     85                             picks the MHD_FLAG bits and the MHD_OPTION
     86                             array, reaching the flag validation, the
     87                             thread pool, the internal polling modes, the
     88                             listen socket and the connection limits
     89     fuzz_eventloop          the external event loop: the input is a
     90                             program of opcodes that polls, ignores
     91                             timeouts, mismatches descriptor sets and
     92                             suspends/resumes against a live daemon
     93     fuzz_str                the mhd_str.c primitives with exactly-sized
     94                             output buffers
     95     fuzz_memorypool         memorypool.c, the per-connection bump
     96                             allocator, with the harness's own oracles
     97     fuzz_auth_header        MHD_get_rq_dauth_params_() /
     98                             MHD_get_rq_bauth_params_()
     99     fuzz_postprocessor      MHD_post_process()
    100 
    101 src/fuzz/ has one more harness, `fuzz_tls`, which is deliberately absent
    102 here: it needs a TLS backend and this build is --disable-https (see
    103 section 3).
    104 
    105 All seven are single translation units that export
    106 
    107     int LLVMFuzzerTestOneInput (const uint8_t *data, size_t size);
    108 
    109 unconditionally.  `-DFUZZ_NO_MAIN` (which `build.sh` passes) removes only
    110 the standalone driver's `main()` from `fuzz_common.h`; the fuzz target
    111 itself is never conditionally compiled.  There is no
    112 `LLVMFuzzerInitialize()` and none is needed: the two tuning knobs of
    113 `fuzz_request` (`MHD_FUZZ_MIN_DISCIPLINE`, `MHD_FUZZ_MIN_MEM_LIMIT`) are
    114 read lazily with `getenv()` on the first call and default to the *full*
    115 range (-3 and 0), which is what a fuzzing service should explore.
    116 
    117 ### max_len
    118 
    119 `max_len` in the `.options` files is set to the point beyond which the
    120 harness ignores the extra bytes, so libFuzzer does not waste its budget:
    121 
    122     fuzz_request        8192   10 configuration bytes + length-prefixed
    123                                send segments (2-byte header, payload <= 0x3FFF,
    124                                at most 96 segments over at most 8
    125                                connections).  8 KiB comfortably holds the
    126                                two-request %%NONCE%% digest handshake, a
    127                                chunked body with extensions and trailers,
    128                                and a body-oracle declaration.  Larger
    129                                inputs are safe -- every segment is bounded
    130                                independently -- but buy almost nothing.
    131     fuzz_options        8192   the flag/option block plus one short
    132                                request; the request half is what makes
    133                                the tail of the input worth anything, so
    134                                this tracks fuzz_request rather than the
    135                                (much smaller) option block alone.
    136     fuzz_eventloop       512   configuration byte + the opcode program.
    137                                Opcodes are one byte each and the program
    138                                is bounded, so a longer input only adds
    139                                opcodes that are never interpreted.
    140     fuzz_str             514   2 selector bytes + the payload, which the
    141                                harness truncates to 512.
    142     fuzz_memorypool      200   the smallest of the set: a pool size and a
    143                                short program of allocate/reallocate/reset
    144                                operations, all bounded.
    145     fuzz_auth_header    4097   1 selector byte + the Authorization header
    146                                value, truncated to 4096.
    147     fuzz_postprocessor  8196   4 selector bytes + the POST body, truncated
    148                                to 8192.
    149 
    150 ### rss_limit_mb, and why a long run creeps
    151 
    152 libFuzzer's default `-rss_limit_mb=2048` counts the whole process, and an
    153 ASan-instrumented target grows slowly over hundreds of millions of
    154 executions even with no leak at all: ASan's allocator keeps a quarantine
    155 of freed chunks and does not return memory to the OS eagerly.  A local
    156 two-hour campaign hit exactly this -- `fuzz_options` crept from 686 MB to
    157 1804 MB over 26M executions and was killed as an OOM, while
    158 LeakSanitizer stayed silent across 3M-execution runs and
    159 `MALLOC_ARENA_MAX` changed nothing (it is glibc's knob, and none of these
    160 allocations go through glibc).
    161 
    162 Three arms of the same target over the same corpus, 4M executions each,
    163 settle it:
    164 
    165     ASan, default quarantine                  608 MB
    166     ASan, ASAN_OPTIONS=quarantine_size_mb=1   266 MB
    167     no sanitizer at all (glibc allocator)      36 MB
    168 
    169 17x between the first and the last, with no code difference: the growth
    170 is the sanitizer's allocator holding freed memory, not the harness and
    171 not MHD.
    172 
    173 So treat a slow, monotonic RSS climb as allocator retention rather than a
    174 finding.  ClusterFuzz restarts its fuzzers periodically, which bounds it;
    175 do the same locally rather than raising the limit forever.
    176 
    177 ### close_fd_mask
    178 
    179 Deliberately **not** set.  The harnesses are quiet by default (MHD's
    180 error log is only enabled when `MHD_FUZZ_VERBOSE` is set, which never
    181 happens under libFuzzer), so there is no output to suppress -- while
    182 `fuzz_report_finding()` writes the description of a non-memory-safety
    183 finding straight to fd 2 and `MHD_PANIC()` writes to stderr too.  Closing
    184 those fds would throw away exactly the diagnostics that make a report
    185 actionable.
    186 
    187 
    188 -------------------------------------------------------------------
    189 3. What build.sh configures, and why
    190 -------------------------------------------------------------------
    191 
    192     --enable-static --disable-shared --with-pic
    193         fuzz_str and fuzz_auth_header call MHD-internal symbols compiled
    194         with hidden visibility; they are not exported from
    195         libmicrohttpd.so and can only be reached through the static
    196         archive.  OSS-Fuzz also requires the binaries in $OUT to be
    197         self-contained.
    198     --enable-fuzzing
    199         configures src/fuzz/Makefile.  Not strictly needed (build.sh
    200         compiles the harnesses itself) but it keeps this build equivalent
    201         to the documented developer build and fails loudly if src/fuzz/
    202         ever stops being wired into configure.ac.
    203     --enable-asserts
    204         keeps mhd_assert() alive.  Assertions reachable from network input
    205         are remote aborts; findings K1-K7 in `src/fuzz/README` section 6
    206         are all of that kind and are invisible without this.
    207     --disable-https
    208         the harnesses never speak TLS (they hand MHD an already-connected
    209         AF_UNIX socketpair through MHD_add_connection() and never set
    210         MHD_USE_TLS), so HTTPS adds no coverage; it would drag in GnuTLS,
    211         which OSS-Fuzz fuzzes separately, and it would make the
    212         MemorySanitizer build impossible without an MSan-instrumented
    213         GnuTLS.  With HTTPS off, libc and libpthread are the only
    214         external dependencies.
    215     --disable-curl --disable-doc --disable-examples --disable-tools
    216         not needed, and they only add build time and dependencies.
    217     --enable-build-type=neutral
    218         the default, stated explicitly: "neutral" is the only build type
    219         that does not inject its own optimisation/debug flags, so the
    220         $CFLAGS supplied by OSS-Fuzz survive unmodified.
    221 
    222 `build.sh` never sets `--enable-sanitizers` or `--enable-coverage`:
    223 OSS-Fuzz provides instrumentation through `$CFLAGS`/`$CXXFLAGS`, and a
    224 second, configure-generated `-fsanitize=` set is a classic way to break
    225 an OSS-Fuzz build.  `$CFLAGS` is passed through to `configure` and to
    226 every harness compilation verbatim.
    227 
    228 The build is out-of-tree (`$WORK/mhd-build`), so `build.sh` never
    229 modifies the checkout.  The git checkout ships no `configure`, so
    230 `./bootstrap` runs first; because `bootstrap` ends in an `|| echo ...`
    231 chain and therefore cannot be trusted to return a failure status,
    232 `build.sh` checks for the product and falls back to `autoreconf -fi`.
    233 
    234 Note that out-of-tree does *not* mean the checkout can be a tree you
    235 have already configured in place: an in-tree `config.status` makes any
    236 subsequent out-of-tree `configure` stop with "source directory already
    237 configured; run \"make distclean\" there first".  Either `make
    238 distclean` the checkout or -- better, and closer to what OSS-Fuzz
    239 actually does -- point `$MHD_SRC` at a pristine clone:
    240 
    241     git clone --shared /path/to/libmicrohttpd /tmp/mhd-fuzz-src
    242 
    243 
    244 ### $CFLAGS / $CXXFLAGS on a local run
    245 
    246 Under OSS-Fuzz these are always exported by the base-builder image and
    247 `build.sh` uses them verbatim.  When they are unset -- i.e. only on a
    248 local run -- `build.sh` derives them from `$SANITIZER`.  Two flags are
    249 what make such a run a real fuzzing run rather than a build that merely
    250 succeeds:
    251 
    252   * `-fsanitize=fuzzer-no-link` installs libFuzzer's coverage
    253     instrumentation into every translation unit of the library.  Without
    254     it there is no feedback signal at all: libFuzzer degenerates to blind
    255     random generation, `cov:` never moves and the corpus never grows.
    256     This is the single easiest thing to leave out, and nothing about the
    257     resulting build looks wrong.
    258   * a sanitizer, because libFuzzer on its own only notices crashes the
    259     kernel delivers.  (MHD's own oracles -- the `MHD_set_panic_func()`
    260     tripwire and `mhd_assert()` -- do fire without one, which is why
    261     `SANITIZER=none` is still worth something.)
    262 
    263 The `address` default folds UBSan into the ASan build, with
    264 `-fno-sanitize-recover=undefined` so that UB actually kills the process
    265 instead of printing and continuing.  That differs deliberately from
    266 OSS-Fuzz, which runs `address` and `undefined` as two separate
    267 campaigns: it has unlimited machine time and wants each report
    268 attributed to one sanitizer, whereas a local run has an afternoon and is
    269 better off with two oracles per CPU-hour.  Pass `SANITIZER=undefined`
    270 explicitly to get the split OSS-Fuzz shape.
    271 
    272 `SANITIZER=memory` is accepted but is only meaningful inside the
    273 OSS-Fuzz image, where the C library is instrumented too; on a distro
    274 toolchain it reports uninitialised reads in libc frames.
    275 
    276 A complete local run then needs nothing but clang and the compiler-rt
    277 runtimes (on Debian: `clang`, `libclang-rt-dev`, and `llvm` for
    278 `llvm-symbolizer`, without which every trace is bare addresses):
    279 
    280     git clone --shared . /tmp/mhd-fuzz-src
    281     WORK=/tmp/mhd-fuzz-work OUT=/tmp/mhd-fuzz-out MHD_SRC=/tmp/mhd-fuzz-src \
    282       /tmp/mhd-fuzz-src/contrib/oss-fuzz/build.sh
    283     mkdir /tmp/c && unzip -q /tmp/mhd-fuzz-out/fuzz_request_seed_corpus.zip -d /tmp/c
    284     /tmp/mhd-fuzz-out/fuzz_request /tmp/c \
    285         -dict=/tmp/mhd-fuzz-out/fuzz_request.dict -max_len=8192 \
    286         -jobs=8 -workers=8 -max_total_time=3600
    287 
    288 `build.sh` prints the resolved `CC`, `CXX`, `CFLAGS` and `CXXFLAGS` in
    289 its banner; check there first if a local run finds nothing.
    290 
    291 
    292 -------------------------------------------------------------------
    293 4. Building and running locally with infra/helper.py
    294 -------------------------------------------------------------------
    295 
    296 Prerequisites: Docker, python3, and a checkout of `google/oss-fuzz`.
    297 
    298     git clone --depth 1 https://github.com/google/oss-fuzz
    299     cd oss-fuzz
    300     mkdir -p projects/libmicrohttpd
    301     cp /path/to/libmicrohttpd/contrib/oss-fuzz/build.sh      projects/libmicrohttpd/
    302     cp /path/to/libmicrohttpd/contrib/oss-fuzz/project.yaml  projects/libmicrohttpd/
    303     cp /path/to/libmicrohttpd/contrib/oss-fuzz/Dockerfile    projects/libmicrohttpd/
    304 
    305 Build the image and the targets:
    306 
    307     python3 infra/helper.py build_image libmicrohttpd
    308     python3 infra/helper.py build_fuzzers --sanitizer address libmicrohttpd
    309     python3 infra/helper.py check_build libmicrohttpd
    310 
    311 `build_fuzzers` accepts an optional path to a local source tree as its
    312 last argument, which is how you test uncommitted changes:
    313 
    314     python3 infra/helper.py build_fuzzers --sanitizer address \
    315         libmicrohttpd /path/to/libmicrohttpd
    316 
    317 Repeat for the other sanitizers, engines and architectures before
    318 submitting:
    319 
    320     python3 infra/helper.py build_fuzzers --sanitizer undefined    libmicrohttpd
    321     python3 infra/helper.py build_fuzzers --sanitizer memory       libmicrohttpd
    322     python3 infra/helper.py build_fuzzers --engine afl             libmicrohttpd
    323     python3 infra/helper.py build_fuzzers --engine honggfuzz       libmicrohttpd
    324     python3 infra/helper.py build_fuzzers --architecture i386      libmicrohttpd
    325 
    326 
    327 ### 4.1 Without Docker
    328 
    329 `build.sh` also runs directly, and it reads the same four variables
    330 OSS-Fuzz sets, so the whole matrix is reachable on a developer machine:
    331 
    332     SANITIZER        address | undefined | memory | coverage | none
    333     FUZZING_ENGINE   libfuzzer | afl | honggfuzz | none
    334     ARCHITECTURE     x86_64 | i386
    335 
    336 Every combination has been built and driven locally.  The recipes:
    337 
    338     # libFuzzer, x86_64, ASan+UBSan  (the default)
    339     MHD_SRC=/path/to/src WORK=/tmp/w OUT=/tmp/o ./contrib/oss-fuzz/build.sh
    340 
    341     # i386.  Needs gcc-multilib and a 32 bit libstdc++; build.sh adds
    342     # -m32 -no-pie itself.
    343     ARCHITECTURE=i386 MHD_SRC=... WORK=... OUT=... ./build.sh
    344 
    345     # AFL++  (Debian: apt install afl++)
    346     FUZZING_ENGINE=afl MHD_SRC=... WORK=... OUT=... ./build.sh
    347     afl-fuzz -i seeds -o findings -- $OUT/fuzz_request
    348 
    349     # honggfuzz.  Not packaged by Debian; build it and put its compiler
    350     # wrappers on $PATH:
    351     #   git clone https://github.com/google/honggfuzz /tmp/honggfuzz
    352     #   make -C /tmp/honggfuzz
    353     PATH=/tmp/honggfuzz/hfuzz_cc:$PATH FUZZING_ENGINE=honggfuzz \
    354       MHD_SRC=... WORK=... OUT=... ./build.sh
    355     /tmp/honggfuzz/honggfuzz -i seeds -o findings -- $OUT/fuzz_request
    356 
    357     # no engine at all: keeps the harnesses' own deterministic driver, so
    358     # this is a sanitizer smoke test that needs nothing installed.
    359     FUZZING_ENGINE=none MHD_SRC=... WORK=... OUT=... ./build.sh
    360     $OUT/fuzz_request --iterations=100000 --seed=1
    361 
    362 Local toolchain quirk, not a property of the build: Debian's clang picks
    363 a gcc installation that may have no matching libstdc++, and the link then
    364 fails with `cannot find -lstdc++`.  Pin it, choosing a gcc that has the
    365 word size you are building for:
    366 
    367     CXX="clang++ --gcc-install-dir=/usr/lib/gcc/x86_64-linux-gnu/15"   # 64 bit
    368     CXX="clang++ --gcc-install-dir=/usr/lib/gcc/x86_64-linux-gnu/14"   # 32 bit
    369 
    370 Not set inside build.sh on purpose: in the OSS-Fuzz image clang uses its
    371 own libc++ and any such pin would be wrong.
    372 
    373 Run one:
    374 
    375     python3 infra/helper.py run_fuzzer libmicrohttpd fuzz_request
    376     python3 infra/helper.py run_fuzzer libmicrohttpd fuzz_str
    377     python3 infra/helper.py run_fuzzer libmicrohttpd fuzz_auth_header
    378     python3 infra/helper.py run_fuzzer libmicrohttpd fuzz_postprocessor
    379 
    380 `run_fuzzer` passes anything after the target name to libFuzzer, and
    381 takes `--corpus-dir` for a persistent corpus:
    382 
    383     python3 infra/helper.py run_fuzzer --corpus-dir=/tmp/mhd-corpus \
    384         libmicrohttpd fuzz_request -- -max_total_time=600 -rss_limit_mb=4096
    385 
    386 Coverage report (needs a coverage build):
    387 
    388     python3 infra/helper.py build_fuzzers --sanitizer coverage libmicrohttpd
    389     python3 infra/helper.py coverage libmicrohttpd --fuzz-target fuzz_request
    390 
    391 
    392 -------------------------------------------------------------------
    393 5. What to do with a report
    394 -------------------------------------------------------------------
    395 
    396 An OSS-Fuzz report contains a *testcase* (the raw input bytes) and a
    397 stack trace.  Download the testcase from the report, then:
    398 
    399     python3 infra/helper.py reproduce libmicrohttpd fuzz_request ./testcase
    400 
    401 The same bytes can also be replayed with the in-tree standalone driver,
    402 which needs no Docker and no clang:
    403 
    404     ./configure --enable-fuzzing --enable-static --enable-asserts \
    405                 --enable-sanitizers=address,undefined
    406     make -C src/fuzz check_PROGRAMS      # or: make -C src/fuzz check
    407     src/fuzz/fuzz_request --file=./testcase
    408 
    409 Minimising a libFuzzer crash:
    410 
    411     python3 infra/helper.py shell libmicrohttpd
    412     # inside the container:
    413     /out/fuzz_request -minimize_crash=1 -runs=100000 /testcase
    414 
    415 Once fixed, add the minimised input to `src/fuzz/corpus/` (or to
    416 `src/fuzz/corpus/known-findings/` if it stays interesting as a named
    417 regression) and commit it: `make_seed_corpus.sh` picks it up
    418 automatically on the next OSS-Fuzz build, so the case is re-run forever.
    419 
    420 Note that `fuzz_request` is **not** perfectly deterministic: MHD's digest
    421 nonces embed a millisecond timestamp, and whether a nonce counts as stale
    422 therefore depends on the wall clock.  Finding K6 is of that kind and
    423 reproduces in only a fraction of replays.  If ClusterFuzz marks a
    424 testcase "unreproducible" but the trace points at nonce handling, replay
    425 it in a loop before dismissing it.
    426 
    427 
    428 -------------------------------------------------------------------
    429 6. Seed corpora
    430 -------------------------------------------------------------------
    431 
    432 `make_seed_corpus.sh` builds one zip per target:
    433 
    434     $OUT/fuzz_request_seed_corpus.zip
    435     $OUT/fuzz_str_seed_corpus.zip
    436     $OUT/fuzz_auth_header_seed_corpus.zip
    437     $OUT/fuzz_postprocessor_seed_corpus.zip
    438 
    439 `src/fuzz/corpus/` is organised per harness by file-name prefix
    440 (`fuzz_<harness>-NN.bin`); inputs are *not* interchangeable between
    441 harnesses, because byte 0 selects a different thing in each, so each zip
    442 gets only its own prefix.  `src/fuzz/corpus/README` is documentation and
    443 is excluded.
    444 
    445 `src/fuzz/corpus/known-findings/K*.bin` are byte-exact reproducers for
    446 the findings documented in `src/fuzz/README` section 6.  They are all
    447 `fuzz_request` inputs and are added to that target's seed corpus
    448 (prefixed `known-finding-`), which is what turns them into permanent
    449 regression tests: ClusterFuzz keeps every seed in the corpus and replays
    450 it on every run.
    451 
    452 Reproducers of findings that are still *open* are skipped.  A finding is
    453 open exactly when `patches/$ID.diff` exists, and its reproducer crashes
    454 the target by construction, so shipping it would make every ClusterFuzz
    455 run open by rediscovering a bug that is already written down.  Committing
    456 the fix means deleting the diff, and that alone promotes the reproducer
    457 to a shipped regression seed.
    458 
    459 Nothing is open at the time of writing, so `patches/` does not exist and
    460 all eight reproducers ship.
    461 
    462 Regenerating the corpus from the harnesses' built-in seeds:
    463 
    464     make -C src/fuzz refresh-corpus     # ./fuzz_<name> --write-corpus=corpus
    465 
    466 That only rewrites the `fuzz_<harness>-NN.bin` files.  `known-findings/`
    467 is hand-maintained and is never touched by it.
    468 
    469 The script can be run by hand:
    470 
    471     contrib/oss-fuzz/make_seed_corpus.sh . /tmp/out
    472     unzip -l /tmp/out/fuzz_request_seed_corpus.zip
    473 
    474 
    475 -------------------------------------------------------------------
    476 7. Dictionaries
    477 -------------------------------------------------------------------
    478 
    479 `dicts/*.dict` are libFuzzer/AFL dictionaries (`name="value"`, with only
    480 `\\`, `\"` and `\xAB` as escapes -- CR and LF are written `\x0d`,
    481 `\x0a`).  They are installed to `$OUT/<fuzzer>.dict` and referenced from
    482 `$OUT/<fuzzer>.options`, which is how ClusterFuzz picks them up.
    483 
    484 They cover: HTTP methods and versions; framing headers
    485 (`Transfer-Encoding`, `Content-Length`, `chunked`, conflicting and
    486 malformed variants); chunk-size lines and chunk-extension syntax
    487 (`;ext`, `;ext=val`, `;ext="quoted"`, unterminated quotes) -- the exact
    488 grammar of commit `c13f4c64`; digest-auth parameters (`algorithm=`,
    489 `qop=`, `nonce=`, `realm=`, `userhash=`, `username*=`, `nc=`,
    490 `response=`) with the algorithm tokens `MD5`, `SHA-256`, `SHA-512-256`
    491 and their `-sess` variants plus deliberately unknown ones, and `auth` /
    492 `auth-int`; over-long hex `response=` values (commit `5a73c1ae`);
    493 percent-encoding, including truncated, invalid, double and overlong
    494 forms; base64; and the `multipart/form-data` and
    495 `application/x-www-form-urlencoded` vocabulary for the post processor.
    496 
    497 `fuzz_request.dict` additionally contains the literal `%%NONCE%%`
    498 placeholder, which the harness rewrites at send time into the most recent
    499 nonce the daemon issued.  Without it the digest `response=` code path is
    500 statistically unreachable (see `src/fuzz/README` section 2.3), so it is
    501 the single most valuable token in the file.
    502 
    503 The token list in `fuzz_common.h` (`fuzz_interesting_str`) is the
    504 generator's equivalent; the two are intentionally similar but not
    505 generated from each other.
    506 
    507 
    508 -------------------------------------------------------------------
    509 8. Known limitations
    510 -------------------------------------------------------------------
    511 
    512  *  **The request-body oracle is inactive under libFuzzer.**  The
    513     smuggling oracle of `fuzz_request` (see `src/fuzz/README` section
    514     2.4) compares what MHD delivers to the application against ground
    515     truth declared in an `op 1` segment of the input.  It is gated on
    516     `fuzz_pristine`, which the standalone driver sets for un-mutated
    517     inputs and which is never set when `FUZZ_NO_MAIN` is defined -- i.e.
    518     it is off for every OSS-Fuzz execution.  That is correct and
    519     intentional: libFuzzer mutates the request without mutating the
    520     declaration, so the ground truth would be wrong and every mutated
    521     input would look like a finding.  The consequence is that OSS-Fuzz
    522     catches memory-safety bugs, UB, panics and assertion failures, but
    523     *not* pure framing/desync defects of the `c13f4c64` kind.  Those stay
    524     the job of the in-tree driver (`make -C src/fuzz check`), which is
    525     another reason to keep running it in CI.
    526  *  **i386 runs ASan + libFuzzer only.**  That is an OSS-Fuzz
    527     restriction, not one of this build: locally the 32 bit targets build
    528     and run under every engine.  Worth knowing because the word size is
    529     exactly what makes some of these bugs interesting (`TESTING.md`
    530     section P3).
    531  *  **centipede is not claimed.**  It is a supported OSS-Fuzz engine but
    532     these targets have not been tried with it; unlike afl and honggfuzz,
    533     nobody has built it here.  Do not add it to `project.yaml` on the
    534     assumption that a plain `LLVMFuzzerTestOneInput()` target must work.
    535  *  **Nondeterminism.**  See the note about digest nonce timestamps in
    536     section 5.
    537  *  The `[libfuzzer]` section of the `.options` files is the only one
    538     used.  ClusterFuzz also understands other sections (for sanitizer
    539     options and, in some versions, environment variables), but nothing
    540     here depends on that, and the exact set of supported sections is not
    541     documented in the OSS-Fuzz repository -- if you ever need to pin
    542     `MHD_FUZZ_MIN_DISCIPLINE` or `MHD_FUZZ_MIN_MEM_LIMIT` for the hosted
    543     runs, verify the mechanism against the ClusterFuzz sources first
    544     rather than assuming it works.