commit d59f3045248ff848f3e3bb9698f1c4094fc9c8b8 parent 5ae817bd6ac8d0c7830eefc8b8a20b54435c8a23 Author: Christian Grothoff <christian@grothoff.org> Date: Tue, 28 Jul 2026 23:04:02 +0200 expand fuzzing logic Diffstat:
150 files changed, 8821 insertions(+), 42 deletions(-)
diff --git a/contrib/oss-fuzz/README b/contrib/oss-fuzz/README @@ -254,12 +254,61 @@ last argument, which is how you test uncommitted changes: python3 infra/helper.py build_fuzzers --sanitizer address \ libmicrohttpd /path/to/libmicrohttpd -Repeat for the other sanitizers and engines before submitting: +Repeat for the other sanitizers, engines and architectures before +submitting: - python3 infra/helper.py build_fuzzers --sanitizer undefined libmicrohttpd - python3 infra/helper.py build_fuzzers --sanitizer memory libmicrohttpd - python3 infra/helper.py build_fuzzers --engine afl libmicrohttpd - python3 infra/helper.py build_fuzzers --engine honggfuzz libmicrohttpd + python3 infra/helper.py build_fuzzers --sanitizer undefined libmicrohttpd + python3 infra/helper.py build_fuzzers --sanitizer memory libmicrohttpd + python3 infra/helper.py build_fuzzers --engine afl libmicrohttpd + python3 infra/helper.py build_fuzzers --engine honggfuzz libmicrohttpd + python3 infra/helper.py build_fuzzers --architecture i386 libmicrohttpd + + +### 4.1 Without Docker + +`build.sh` also runs directly, and it reads the same four variables +OSS-Fuzz sets, so the whole matrix is reachable on a developer machine: + + SANITIZER address | undefined | memory | coverage | none + FUZZING_ENGINE libfuzzer | afl | honggfuzz | none + ARCHITECTURE x86_64 | i386 + +Every combination has been built and driven locally. The recipes: + + # libFuzzer, x86_64, ASan+UBSan (the default) + MHD_SRC=/path/to/src WORK=/tmp/w OUT=/tmp/o ./contrib/oss-fuzz/build.sh + + # i386. Needs gcc-multilib and a 32 bit libstdc++; build.sh adds + # -m32 -no-pie itself. + ARCHITECTURE=i386 MHD_SRC=... WORK=... OUT=... ./build.sh + + # AFL++ (Debian: apt install afl++) + FUZZING_ENGINE=afl MHD_SRC=... WORK=... OUT=... ./build.sh + afl-fuzz -i seeds -o findings -- $OUT/fuzz_request + + # honggfuzz. Not packaged by Debian; build it and put its compiler + # wrappers on $PATH: + # git clone https://github.com/google/honggfuzz /tmp/honggfuzz + # make -C /tmp/honggfuzz + PATH=/tmp/honggfuzz/hfuzz_cc:$PATH FUZZING_ENGINE=honggfuzz \ + MHD_SRC=... WORK=... OUT=... ./build.sh + /tmp/honggfuzz/honggfuzz -i seeds -o findings -- $OUT/fuzz_request + + # no engine at all: keeps the harnesses' own deterministic driver, so + # this is a sanitizer smoke test that needs nothing installed. + FUZZING_ENGINE=none MHD_SRC=... WORK=... OUT=... ./build.sh + $OUT/fuzz_request --iterations=100000 --seed=1 + +Local toolchain quirk, not a property of the build: Debian's clang picks +a gcc installation that may have no matching libstdc++, and the link then +fails with `cannot find -lstdc++`. Pin it, choosing a gcc that has the +word size you are building for: + + CXX="clang++ --gcc-install-dir=/usr/lib/gcc/x86_64-linux-gnu/15" # 64 bit + CXX="clang++ --gcc-install-dir=/usr/lib/gcc/x86_64-linux-gnu/14" # 32 bit + +Not set inside build.sh on purpose: in the OSS-Fuzz image clang uses its +own libc++ and any such pin would be wrong. Run one: @@ -414,13 +463,15 @@ generated from each other. *not* pure framing/desync defects of the `c13f4c64` kind. Those stay the job of the in-tree driver (`make -C src/fuzz check`), which is another reason to keep running it in CI. - * **i386 is not claimed.** MHD's digest buffer sizes depend on the - word size (`TESTING.md` section P3), so a 32-bit build is genuinely - interesting, but OSS-Fuzz supports i386 only for ASan + libFuzzer and - that configuration has not been verified here. Add - `- i386` to `architectures:` once it has been. - * **centipede is not claimed** for the same reason: it is a supported - OSS-Fuzz engine but these targets have not been tried with it. + * **i386 runs ASan + libFuzzer only.** That is an OSS-Fuzz + restriction, not one of this build: locally the 32 bit targets build + and run under every engine. Worth knowing because the word size is + exactly what makes some of these bugs interesting (`TESTING.md` + section P3). + * **centipede is not claimed.** It is a supported OSS-Fuzz engine but + these targets have not been tried with it; unlike afl and honggfuzz, + nobody has built it here. Do not add it to `project.yaml` on the + assumption that a plain `LLVMFuzzerTestOneInput()` target must work. * **Nondeterminism.** See the note about digest nonce timestamps in section 5. * The `[libfuzzer]` section of the `.options` files is the only one diff --git a/contrib/oss-fuzz/build.sh b/contrib/oss-fuzz/build.sh @@ -27,13 +27,100 @@ # --------------------------------------------------------------------------- # Defaults, so that the script is also runnable by hand # --------------------------------------------------------------------------- +# The shebang already carries -eu, but only when the script is executed +# directly; "bash build.sh" silently drops it, and a harness that fails to +# link then leaves $OUT without that target and the script still exits 0. +set -eu + SRC="${SRC:-$(cd "$(dirname "$0")/../../.." && pwd)}" WORK="${WORK:-${SRC}/work}" OUT="${OUT:-${SRC}/out}" -CC="${CC:-clang}" -CXX="${CXX:-clang++}" -LIB_FUZZING_ENGINE="${LIB_FUZZING_ENGINE:--fsanitize=fuzzer}" SANITIZER="${SANITIZER:-address}" +FUZZING_ENGINE="${FUZZING_ENGINE:-libfuzzer}" +ARCHITECTURE="${ARCHITECTURE:-x86_64}" + +# --- engine ------------------------------------------------------------------ +# +# Under OSS-Fuzz $CC, $CXX and $LIB_FUZZING_ENGINE are exported by the +# base-builder image for the engine being built and MUST be used as given, +# so everything here is dead code there. It fires only on a local run, +# and its job is to make "FUZZING_ENGINE=afl ./build.sh" produce a real +# AFL++ target rather than a libFuzzer one that happens to link. +# +# _mhd_cov_cflags is the coverage instrumentation the engine needs at +# compile time. Getting it wrong is the failure mode that matters: the +# build succeeds, the target runs, and it finds nothing, because the +# engine has no feedback signal. +case "${FUZZING_ENGINE}" in + libfuzzer) + CC="${CC:-clang}" + CXX="${CXX:-clang++}" + LIB_FUZZING_ENGINE="${LIB_FUZZING_ENGINE:--fsanitize=fuzzer}" + _mhd_cov_cflags="-fsanitize=fuzzer-no-link" + _mhd_main="engine" + ;; + afl) + # afl-clang-fast inserts AFL++'s own instrumentation, so libFuzzer's + # must not be added on top; libAFLDriver.a supplies a main() that + # feeds AFL++ input to LLVMFuzzerTestOneInput(). + CC="${CC:-afl-clang-fast}" + CXX="${CXX:-afl-clang-fast++}" + LIB_FUZZING_ENGINE="${LIB_FUZZING_ENGINE:-/usr/lib/afl/libAFLDriver.a}" + _mhd_cov_cflags="" + _mhd_main="engine" + ;; + honggfuzz) + # hfuzz-clang does the same job for honggfuzz. It is not packaged by + # Debian; build it from https://github.com/google/honggfuzz and put + # its directory on $PATH (see contrib/oss-fuzz/README). + CC="${CC:-hfuzz-clang}" + CXX="${CXX:-hfuzz-clang++}" + # hfuzz-clang links libhfuzz/libhfcommon and supplies main() itself, + # so $LIB_FUZZING_ENGINE stays empty. + LIB_FUZZING_ENGINE="${LIB_FUZZING_ENGINE:-}" + _mhd_cov_cflags="" + _mhd_main="engine" + ;; + none) + # No engine: link the harnesses' own deterministic driver instead. + # This is what makes a sanitizer-only smoke test possible without any + # fuzzing engine installed at all. + CC="${CC:-clang}" + CXX="${CXX:-clang++}" + LIB_FUZZING_ENGINE="${LIB_FUZZING_ENGINE:-}" + _mhd_cov_cflags="" + _mhd_main="builtin" + ;; + *) + echo "ERROR: unknown FUZZING_ENGINE='${FUZZING_ENGINE}'" >&2 + echo " expected: libfuzzer | afl | honggfuzz | none" >&2 + exit 1 + ;; +esac + +# --- architecture ------------------------------------------------------------ +# +# OSS-Fuzz exports $ARCHITECTURE and already has -m32 in $CFLAGS for i386, +# so this too only fires locally. +# +# -no-pie is needed on i386 and only there: a position independent +# executable built with -m32 and any sanitizer dies with SEGV at address +# 0 before main() on current Linux/clang. It is harmless on x86_64 and +# is therefore not applied there, to keep that build byte-comparable with +# what OSS-Fuzz produces. +case "${ARCHITECTURE}" in + x86_64) + _mhd_arch_cflags="" + ;; + i386) + _mhd_arch_cflags="-m32 -no-pie" + ;; + *) + echo "ERROR: unknown ARCHITECTURE='${ARCHITECTURE}'" >&2 + echo " expected: x86_64 | i386" >&2 + exit 1 + ;; +esac # --- $CFLAGS / $CXXFLAGS ----------------------------------------------------- # @@ -100,6 +187,11 @@ if [ -z "${CFLAGS:-}" ]; then # instrumented too -- true inside the OSS-Fuzz image, essentially # never true on a distro toolchain. Expect false positives in libc # frames locally; use the OSS-Fuzz container for a real MSan run. + # + # This is also why build.sh configures --disable-https and + # --disable-curl: an uninstrumented GnuTLS would poison every run. + # Any harness that needs TLS is skipped in this configuration; see + # the $FUZZERS selection below. _mhd_san_cflags="-fsanitize=memory -fsanitize-memory-track-origins" ;; coverage) @@ -119,13 +211,20 @@ if [ -z "${CFLAGS:-}" ]; then esac if [ "${SANITIZER}" = "coverage" ]; then - CFLAGS="${_mhd_base_cflags} ${_mhd_san_cflags}" + CFLAGS="${_mhd_arch_cflags} ${_mhd_base_cflags} ${_mhd_san_cflags}" else - CFLAGS="${_mhd_base_cflags} ${_mhd_san_cflags} -fsanitize=fuzzer-no-link" + CFLAGS="${_mhd_arch_cflags} ${_mhd_base_cflags} ${_mhd_san_cflags}" + CFLAGS="${CFLAGS} ${_mhd_cov_cflags}" fi unset _mhd_base_cflags _mhd_san_cflags fi CXXFLAGS="${CXXFLAGS:-${CFLAGS}}" +if [ "${_mhd_main}" = "builtin" ]; then + _mhd_no_main="" +else + _mhd_no_main="-DFUZZ_NO_MAIN" +fi +unset _mhd_arch_cflags _mhd_cov_cflags _mhd_main # Directory holding the libmicrohttpd sources. OSS-Fuzz clones them to # $SRC/libmicrohttpd (see Dockerfile); allow an override for local runs. @@ -136,7 +235,15 @@ MHD_SRC="${MHD_SRC:-${SRC}/libmicrohttpd}" # "run build.sh twice" and "reproduce against a pristine tree" cases. BUILD="${WORK}/mhd-build" -FUZZERS="fuzz_request fuzz_str fuzz_auth_header fuzz_postprocessor" +FUZZERS="fuzz_request fuzz_options fuzz_eventloop fuzz_str fuzz_memorypool fuzz_auth_header fuzz_postprocessor" + +# fuzz_tls is deliberately absent: it needs a TLS backend, and this build +# configures --disable-https on purpose (see the rationale below), so the +# target would be an empty shell on all three sanitizers. Shipping it +# would need a second, HTTPS-enabled build variant, which also gives up +# the MemorySanitizer configuration -- an uninstrumented GnuTLS poisons +# every MSan run. It is built and tested in tree by "make -C src/fuzz +# check" instead. mkdir -p "${WORK}" "${OUT}" "${BUILD}" @@ -145,10 +252,13 @@ echo " MHD_SRC = ${MHD_SRC}" echo " BUILD = ${BUILD}" echo " OUT = ${OUT}" echo " SANITIZER = ${SANITIZER}" +echo " FUZZING_ENGINE = ${FUZZING_ENGINE}" +echo " ARCHITECTURE = ${ARCHITECTURE}" echo " LIB_FUZZING_ENGINE = ${LIB_FUZZING_ENGINE}" echo " CC / CXX = ${CC} / ${CXX}" echo " CFLAGS = ${CFLAGS}" echo " CXXFLAGS = ${CXXFLAGS}" +echo " FUZZERS = ${FUZZERS}" # --------------------------------------------------------------------------- # 1. Bootstrap (the git checkout ships no 'configure') @@ -250,8 +360,12 @@ test -f "${MHD_LIB}" || { # --------------------------------------------------------------------------- # 4. Compile the harnesses as libFuzzer translation units # --------------------------------------------------------------------------- -# -DFUZZ_NO_MAIN drops the standalone driver's main() from fuzz_common.h; -# LLVMFuzzerTestOneInput() itself is unconditional in every harness. +# -DFUZZ_NO_MAIN drops the standalone driver's main() from fuzz_common.h, +# because the engine supplies its own. With FUZZING_ENGINE=none there is +# no engine, so the harnesses' built-in driver is kept instead and the +# result is a self-contained, deterministic, seeded fuzzer that needs no +# engine at all. LLVMFuzzerTestOneInput() itself is unconditional in +# every harness either way. # # Include path: # -I${BUILD} for the generated MHD_config.h @@ -271,7 +385,7 @@ for fuzzer in ${FUZZERS}; do echo "--- building ${fuzzer} ---" # shellcheck disable=SC2086 $CC $CFLAGS \ - -DFUZZ_NO_MAIN \ + ${_mhd_no_main} \ "${MHD_INCLUDES[@]}" \ -c "${MHD_SRC}/src/fuzz/${fuzzer}.c" \ -o "${WORK}/${fuzzer}.o" @@ -295,5 +409,23 @@ for fuzzer in ${FUZZERS}; do cp "${MHD_SRC}/contrib/oss-fuzz/${fuzzer}.options" "${OUT}/${fuzzer}.options" done +# --------------------------------------------------------------------------- +# 6. Verify: every requested target must actually be in $OUT +# --------------------------------------------------------------------------- +# Belt and braces for the failure that matters most -- a build that +# reports success but ships nothing, which on OSS-Fuzz shows up only as a +# target that never runs. +_mhd_missing="" +for fuzzer in ${FUZZERS}; do + [ -x "${OUT}/${fuzzer}" ] || _mhd_missing="${_mhd_missing} ${fuzzer}" + [ -f "${OUT}/${fuzzer}_seed_corpus.zip" ] || + _mhd_missing="${_mhd_missing} ${fuzzer}_seed_corpus.zip" +done +if [ -n "${_mhd_missing}" ]; then + echo "ERROR: build did not produce:${_mhd_missing}" >&2 + exit 1 +fi +unset _mhd_missing + echo "=== done; contents of \$OUT ===" ls -la "${OUT}" diff --git a/contrib/oss-fuzz/dicts/fuzz_eventloop.dict b/contrib/oss-fuzz/dicts/fuzz_eventloop.dict @@ -0,0 +1,75 @@ +# libFuzzer dictionary for fuzz_eventloop (GNU libmicrohttpd). +# +# The input is not HTTP: bytes 0-4 configure the daemon, the access handler +# and the event-loop defaults, and byte 5.. is a program of one-byte +# operations, "(opcode << 4) | argument". See src/fuzz/fuzz_eventloop.c for +# the full encoding. The tokens below are therefore *operations* and short +# operation sequences, not protocol syntax; single interesting bytes are left +# to libFuzzer's own mutators. + +# --- one full external-event-loop round --------------------------------- +# MHD_get_fdset2() / select() / MHD_run_from_select2() +round_v2="\x21\xf0\x31" +# MHD_get_fdset() / select() / MHD_run_from_select() (the v1 pair) +round_v1="\x20\xf0\x30" +# ... with the timeout queried in between, which is what the API asks for +round_timeout="\x21\xf0\x40\x31" + +# --- individual event-loop entry points --------------------------------- +get_fdset_v1="\x20" +get_fdset2="\x21" +get_fdset2_small="\x22" +get_fdset2_no_maxfd="\x23" +get_fdset2_no_except="\x24" +poll="\xf0" +run_from_select_v1="\x30" +run_from_select2="\x31" +run="\x32" +run_wait="\x33" +get_timeout="\x40" +quiesce="\xb0" + +# --- descriptor sets that do not match what MHD asked for --------------- +run_zero_sets="\x34" +run_all_ones="\x38" +run_read_only="\x3c" +run2_zero_sets="\x35" +run2_all_ones="\x39" + +# --- connection life cycle ---------------------------------------------- +new_connection="\x80" +close_connection="\x90" +close_abrupt="\x98" +switch_connection="\xa0" +drain_all="\xc8" +set_conn_timeout="\xd0" +set_conn_timeout_0="\xd8" +suspend="\x60" +resume="\x70" +suspend_resume="\x60\x70" +suspend_run_resume="\x60\x32\x70\x32" + +# --- the artificial clock ------------------------------------------------ +clock_step="\x50" +clock_expiry="\x5f" + +# --- request fragments --------------------------------------------------- +send_full_get="\x00" +send_partial_get="\x01" +send_crlf="\x02" +send_post_cl="\x03" +send_body="\x04" +send_post_chunked="\x05" +send_chunk="\x06" +send_last_chunk="\x07" +send_close="\x08" +send_http10="\x09" +send_head="\x0a" +send_pipelined="\x0b" +send_raw="\x10" + +# --- whole schedules that reach the interesting states ------------------- +request_then_loop="\x00\x21\xf0\x31\x40" +park_then_stop="\x00\x32\x60\x40\x32" +quiesce_then_fdset="\xb0\x21\x40" +expiry="\x01\x32\x40\x5f\x40\x32" diff --git a/contrib/oss-fuzz/dicts/fuzz_memorypool.dict b/contrib/oss-fuzz/dicts/fuzz_memorypool.dict @@ -0,0 +1,63 @@ +# libFuzzer dictionary for fuzz_memorypool (GNU libmicrohttpd). +# +# The input is byte 0 = pool size selector, byte 1 = flags, then a +# stream of 3-byte operation records (opcode, block selector, size +# selector); see src/fuzz/README section 2.6 and the header comment of +# src/fuzz/fuzz_memorypool.c. A dictionary of *whole records* is what +# helps here: it lets the mutator splice in a well-formed operation +# instead of having to discover the opcode numbering byte by byte. +# +# Size selector encoding (low two bits select the class): +# b & 3 == 0 tiny, b >> 2 bytes (0..63) +# b & 3 == 1 ((b >> 2) + 1) / 64 of the pool +# b & 3 == 2 the currently free amount, +- ((b >> 2) & 7) +# (bit 5 selects the sign) +# b & 3 == 3 SIZE_MAX - (b >> 2), i.e. the value-wrap guards + +# --- headers: pool size selector + flags -------------------------------- +hdr_tiny_pool="\x00\x01" +hdr_pool_128="\x08\x01" +hdr_pool_256="\x0b\x01" +hdr_pool_1024="\x10\x01" +hdr_pool_1500="\x12\x01" +hdr_pool_32768="\x17\x01" +hdr_pool_mmap="\x19\x00" +hdr_prefer_end="\x08\x03" + +# --- allocation ---------------------------------------------------------- +alloc_tiny="\x00\x00\x40" +alloc_zero="\x00\x00\x00" +alloc_from_end="\x00\x01\x40" +alloc_half="\x00\x00\x7d" +alloc_free="\x00\x00\x02" +alloc_free_plus="\x00\x00\x26" +alloc_free_minus="\x00\x00\x06" +alloc_wrap="\x00\x00\x03" +alloc_all_free="\x01\x00\x00" +alloc_all_free_end="\x01\x01\x00" + +# --- try_alloc / the connection.c squeeze -------------------------------- +try_alloc_tiny="\x02\x00\x30" +try_alloc_free="\x02\x00\x02" +try_alloc_wrap="\x02\x00\x03" +squeeze_tiny="\x03\x00\x30" +squeeze_frac="\x03\x00\x21" + +# --- reallocation -------------------------------------------------------- +realloc_grow="\x04\x00\xfc" +realloc_shrink="\x04\x00\x04" +realloc_to_free="\x04\x00\x02" +realloc_wrap="\x04\x00\x03" +realloc_fresh="\x05\x00\x7d" + +# --- deallocation, reset, queries --------------------------------------- +dealloc="\x06\x00\x00" +dealloc_other="\x06\x01\x00" +reset_keep_all="\x07\xf0\x7d" +reset_keep_none="\x07\x00\x00" +reset_to_free="\x07\xf0\x02" +get_free="\x08\x00\x00" +resizable="\x09\x00\x00" +verify="\x0a\x00\x00" +recreate_small="\x0b\x08\x00" +recreate_large="\x0b\x17\x00" diff --git a/contrib/oss-fuzz/dicts/fuzz_options.dict b/contrib/oss-fuzz/dicts/fuzz_options.dict @@ -0,0 +1,53 @@ +# libFuzzer dictionary for fuzz_options (GNU libmicrohttpd). +# +# The first 16 bytes of the input configure the daemon -- the MHD_FLAG +# combination, the MHD_OPTION values and which callbacks are installed -- +# and the rest is fuzz_request's segment stream, "(op << 14) | length" +# followed by that many bytes of wire data. +# +# So the useful tokens are of two kinds: request fragments for the segment +# stream, and the two-byte segment headers that frame them. The daemon +# configuration itself is a dense bit field and needs no dictionary: every +# byte value means something, which is exactly the case a mutator handles +# well on its own. + +# --- segment headers: (op << 14) | length, little endian --------------- +# op 0 "send", short payloads +seg_send_16="\x10\x00" +seg_send_32="\x20\x00" +seg_send_64="\x40\x00" +# op 2 "send and pump extra rounds" +seg_pump_0="\x00\x80" +seg_pump_16="\x10\x80" +# op 3 "close, reopen, send" +seg_newconn_16="\x10\xc0" +seg_newconn_32="\x20\xc0" + +# --- request lines ----------------------------------------------------- +get="GET / HTTP/1.1\x0d\x0a" +post="POST / HTTP/1.1\x0d\x0a" +head="HEAD / HTTP/1.1\x0d\x0a" +options_star="OPTIONS * HTTP/1.1\x0d\x0a" +http10="GET / HTTP/1.0\x0d\x0a" + +# --- headers that interact with daemon options ------------------------- +host="Host: x\x0d\x0a" +conn_close="Connection: close\x0d\x0a" +conn_keepalive="Connection: keep-alive\x0d\x0a" +conn_upgrade="Connection: Upgrade\x0d\x0a" +upgrade="Upgrade: fuzz-protocol\x0d\x0a" +cl0="Content-Length: 0\x0d\x0a" +cl_big="Content-Length: 1048576\x0d\x0a" +te_chunked="Transfer-Encoding: chunked\x0d\x0a" +expect100="Expect: 100-continue\x0d\x0a" +ctype_form="Content-Type: application/x-www-form-urlencoded\x0d\x0a" +ctype_multipart="Content-Type: multipart/form-data; boundary=b\x0d\x0a" +cookie="Cookie: a=b\x0d\x0a" +auth_basic="Authorization: Basic dXNlcjpwYXNz\x0d\x0a" +auth_digest="Authorization: Digest username=\"user\"\x0d\x0a" +crlf="\x0d\x0a" + +# --- bodies ------------------------------------------------------------ +chunk_end="0\x0d\x0a\x0d\x0a" +chunk_4="4\x0d\x0aabcd\x0d\x0a" +form="a=1&b=2" diff --git a/contrib/oss-fuzz/fuzz_eventloop.options b/contrib/oss-fuzz/fuzz_eventloop.options @@ -0,0 +1,3 @@ +[libfuzzer] +dict = fuzz_eventloop.dict +max_len = 512 diff --git a/contrib/oss-fuzz/fuzz_memorypool.options b/contrib/oss-fuzz/fuzz_memorypool.options @@ -0,0 +1,3 @@ +[libfuzzer] +dict = fuzz_memorypool.dict +max_len = 200 diff --git a/contrib/oss-fuzz/fuzz_options.options b/contrib/oss-fuzz/fuzz_options.options @@ -0,0 +1,3 @@ +[libfuzzer] +dict = fuzz_options.dict +max_len = 8192 diff --git a/contrib/oss-fuzz/make_seed_corpus.sh b/contrib/oss-fuzz/make_seed_corpus.sh @@ -17,11 +17,14 @@ # interchangeable between harnesses: byte 0 of # every harness input selects a different thing. # known-findings/K*.bin byte-exact reproducers for the findings in -# src/fuzz/README section 6. All of them are -# fuzz_request inputs, so they go into that -# harness' seed corpus, where OSS-Fuzz will keep -# re-running them forever - i.e. they become -# permanent regression tests. Reproducers of +# src/fuzz/README section 6. Each goes into the +# seed corpus of the harness that found it, where +# OSS-Fuzz keeps re-running it forever - i.e. it +# becomes a permanent regression test. The owning +# harness is named in the file, "K<n>-<harness>- +# <what>.bin"; a name without one means +# fuzz_request, which is what the reproducers +# predating the convention are. Reproducers of # findings that are still open are skipped; see # the loop below. # README documentation, not an input; excluded. @@ -39,7 +42,7 @@ CORPUS="${SRCDIR}/src/fuzz/corpus" FINDINGS="${CORPUS}/known-findings" PATCHES="${SRCDIR}/patches" -FUZZERS="fuzz_request fuzz_str fuzz_auth_header fuzz_postprocessor" +FUZZERS="fuzz_request fuzz_options fuzz_eventloop fuzz_str fuzz_memorypool fuzz_auth_header fuzz_postprocessor" if [ ! -d "${CORPUS}" ]; then echo "ERROR: no corpus directory at ${CORPUS}" >&2 @@ -62,8 +65,6 @@ for fuzzer in ${FUZZERS}; do n=$((n + 1)) done - # The K* reproducers are fuzz_request inputs. - # # Only the ones whose defect is already fixed are shipped. While a # finding is open its proposed fix is kept as an unapplied diff in # patches/$ID.diff, and its reproducer crashes the target by @@ -75,9 +76,17 @@ for fuzzer in ${FUZZERS}; do # # patches/ therefore does not exist while nothing is open, which is the # normal state; the test below simply never fires then. - if [ "${fuzzer}" = "fuzz_request" ] && [ -d "${FINDINGS}" ]; then + # + # A reproducer belongs to the harness named in its file name, + # "K<n>-<harness>-<what>.bin"; the older ones predate that convention + # and are all fuzz_request inputs, so a name without a harness means + # fuzz_request. + if [ -d "${FINDINGS}" ]; then for f in "${FINDINGS}"/*.bin; do [ -f "${f}" ] || continue + owner="$(basename "${f}" | sed -n 's/^K[0-9]*-\(fuzz_[a-z_]*\)-.*/\1/p')" + [ -n "${owner}" ] || owner="fuzz_request" + [ "${owner}" = "${fuzzer}" ] || continue id="$(basename "${f}" | sed -n 's/^\(K[0-9]*\).*/\1/p')" if [ -n "${id}" ] && [ -f "${PATCHES}/${id}.diff" ]; then echo " skipping ${fuzzer} seed $(basename "${f}"): ${id} is open" \ diff --git a/contrib/oss-fuzz/project.yaml b/contrib/oss-fuzz/project.yaml @@ -31,19 +31,33 @@ sanitizers: - undefined - memory -# Only x86_64 is claimed here because it is the only architecture this -# configuration has been exercised on. i386 is interesting for MHD (the -# severity of the MAX_DIGEST overflow depends on the word size, see -# TESTING.md section P3) and can be added later, but OSS-Fuzz supports i386 -# only for the address sanitizer with libFuzzer, and the build has to be -# verified first. +# i386 matters for MHD because the severity of several of the length bugs +# this suite found depends on the word size (see TESTING.md section P3): +# a size_t underflow that is merely a huge number on x86_64 can be a +# wrap-around on a 32 bit target. +# +# Verified locally: the whole library and all harnesses build with -m32, +# and the entire committed corpus, including every corpus/known-findings/ +# reproducer, replays clean on the 32 bit targets. Note that OSS-Fuzz +# runs i386 only with the address sanitizer under libFuzzer. +# +# One i386 quirk is handled in build.sh rather than here: a -m32 PIE +# linked against any sanitizer runtime dies with SEGV at address 0 before +# main() on current Linux/clang, so the local default adds -no-pie. architectures: - x86_64 + - i386 # The harnesses are plain LLVMFuzzerTestOneInput() targets with no engine -# specific code, so every in-process engine works. ("centipede" is -# deliberately not listed: it is supported by OSS-Fuzz but has not been -# tried against these targets.) +# specific code, so every in-process engine works. All three listed here +# have been built and driven against the targets locally -- see section 4 +# of the README for the exact recipe -- rather than merely assumed to +# work; an engine that is claimed but broken shows up as a dead target on +# the OSS-Fuzz dashboard, not as an error anyone notices. +# +# "centipede" is deliberately not listed: it is supported by OSS-Fuzz but +# has not been tried against these targets. Do not add it without +# building and running it first. fuzzing_engines: - libfuzzer - afl diff --git a/src/fuzz/.gitignore b/src/fuzz/.gitignore @@ -9,3 +9,7 @@ # Seed corpora packaged for OSS-Fuzz by contrib/oss-fuzz/make_seed_corpus.sh. /*_seed_corpus.zip +fuzz_eventloop +fuzz_memorypool +fuzz_options +fuzz_tls diff --git a/src/fuzz/Makefile.am b/src/fuzz/Makefile.am @@ -69,7 +69,10 @@ $(top_builddir)/src/microhttpd/libmicrohttpd.la: $(top_builddir)/src/microhttpd/ check_PROGRAMS = \ fuzz_request \ + fuzz_options \ + fuzz_eventloop \ fuzz_str \ + fuzz_memorypool \ fuzz_postprocessor if HAVE_ANYAUTH @@ -77,6 +80,15 @@ check_PROGRAMS += \ fuzz_auth_header endif +# fuzz_tls needs a TLS backend, and it links GnuTLS directly of its own +# accord: it drives a real client through the handshake rather than +# feeding bytes at MHD, which is what makes MHD's own TLS plumbing -- +# rather than GnuTLS's record parser -- the thing under test. +if ENABLE_HTTPS +check_PROGRAMS += \ + fuzz_tls +endif + .NOTPARALLEL: TESTS = $(check_PROGRAMS) @@ -85,6 +97,20 @@ fuzz_request_SOURCES = \ fuzz_request.c fuzz_common.h fuzz_request_LDFLAGS = $(AM_LDFLAGS) -static +fuzz_options_SOURCES = \ + fuzz_options.c fuzz_common.h +fuzz_options_LDFLAGS = $(AM_LDFLAGS) -static + +fuzz_eventloop_SOURCES = \ + fuzz_eventloop.c fuzz_common.h +fuzz_eventloop_LDFLAGS = $(AM_LDFLAGS) -static + +fuzz_tls_SOURCES = \ + fuzz_tls.c fuzz_common.h +fuzz_tls_CPPFLAGS = $(AM_CPPFLAGS) $(MHD_TLS_LIB_CPPFLAGS) +fuzz_tls_LDFLAGS = $(AM_LDFLAGS) $(MHD_TLS_LIB_LDFLAGS) -static +fuzz_tls_LDADD = $(LDADD) $(MHD_TLS_LIBDEPS) + fuzz_str_SOURCES = \ fuzz_str.c fuzz_common.h fuzz_str_LDFLAGS = $(AM_LDFLAGS) -static @@ -93,6 +119,10 @@ fuzz_auth_header_SOURCES = \ fuzz_auth_header.c fuzz_common.h fuzz_auth_header_LDFLAGS = $(AM_LDFLAGS) -static +fuzz_memorypool_SOURCES = \ + fuzz_memorypool.c fuzz_common.h +fuzz_memorypool_LDFLAGS = $(AM_LDFLAGS) -static + fuzz_postprocessor_SOURCES = \ fuzz_postprocessor.c fuzz_common.h fuzz_postprocessor_LDFLAGS = $(AM_LDFLAGS) -static diff --git a/src/fuzz/README b/src/fuzz/README @@ -620,23 +620,61 @@ K8 digestauth.c MHD_digest_auth_check_digest2(): the variant-4 call to MHD_DIGEST_ALG_AUTO and replay corpus/fuzz_request-37.bin. +K9 postprocessor.c:1119 post_process_multipart(): + LeakSanitizer: direct leak, strdup() from MHD_post_process() + Found by fuzz_postprocessor, not fuzz_request -- the first finding + that did not come from the daemon harness. Remotely triggerable, + default configuration, no assertions needed. + + On entering PP_PerformCheckMultipart the code did + + pp->nested_boundary = strstr (pp->content_type, "boundary="); + ... + pp->nested_boundary = strdup (&pp->nested_boundary[9]); + + The first assignment stores an *interior pointer into + pp->content_type* over whatever pp->nested_boundary held. If the + post processor already owned a boundary -- which it does for every + nested "multipart/mixed" part after the first, unless the state + machine happened to pass through PP_PerformCleanup in between -- that + allocation is lost. MHD_destroy_post_processor() frees only the last + one. + + So a body with N nested multipart/mixed parts, each carrying its own + "boundary=", leaks N-1 blocks, and the client picks how long each one + is. That makes it a memory-exhaustion vector against any application + that calls MHD_post_process() on multipart input, not the one byte + the reproducer happens to leak (its boundary is the empty string). + + Fixed by copying into a local first and releasing any previously + owned boundary before taking ownership of the new one; the error + path no longer overwrites the old pointer either. + Repro: corpus/known-findings/K9-fuzz_postprocessor-nested-boundary-leak.bin + Status ------ -All eight findings are fixed on master: +All nine findings are fixed on master: K1 300a2ab0 K4a 0b750975 K7 acef58a0 K2 68c83f22 K4b 0b750975 K8 07c051dd - K3 e04eb218 K5 6fcdfd43 + K3 e04eb218 K5 6fcdfd43 K9 (this tree) K6 f438804c -The eight reproducers in `corpus/known-findings/` therefore all replay +The nine reproducers in `corpus/known-findings/` therefore all replay clean on a build configured with `--enable-asserts`, and `make check-corpus` asserts exactly that -- it replays that directory along with the generated corpus. K8 has no reproducer, because its trigger is an argument the application chooses rather than anything that comes off the wire. +A reproducer belongs to the harness that found it, and says so in its +name: `K<n>-<harness>-<what>.bin`. The ones without a harness in the +name predate the convention and are all fuzz_request inputs. +`contrib/oss-fuzz/make_seed_corpus.sh` routes each one into the right +target's seed corpus on that basis -- a fuzz_postprocessor reproducer in +fuzz_request's corpus would just be an uninteresting input. + When the next finding is opened, its reproducer goes into `corpus/known-findings/` and will make `make check-corpus` fail until the fix lands. That is the intended behaviour: it is the same arrangement as diff --git a/src/fuzz/corpus/README b/src/fuzz/corpus/README @@ -136,6 +136,11 @@ section 6 of ../README, which records the status of each. All of them are fixed on master, so all of them replay clean, and `check-corpus` replays this directory too. +A file is named `K<n>-<harness>-<what>.bin` for the harness that found +it; the ones without a harness in the name predate that convention and +are all fuzz_request inputs. `contrib/oss-fuzz/make_seed_corpus.sh` +routes each into the seed corpus of its own harness on that basis. + They are kept as a separate, explicitly named set rather than being folded into the main corpus because the files there are *generated*: a hand-written input would be clobbered or renumbered by the next @@ -161,6 +166,10 @@ hand-written input would be clobbered or renumbered by the next a single request carrying both Content-Length and chunked Transfer-Encoding, then an upgrade + K9-fuzz_postprocessor- postprocessor.c post_process_multipart() + nested-boundary-leak.bin leaked the previous nested boundary + on every extra nested + multipart/mixed part These files are *not* regenerated by `--write-corpus`; they are edited by hand. When the input format changes they have to be migrated, and the diff --git a/src/fuzz/corpus/fuzz_eventloop-00.bin b/src/fuzz/corpus/fuzz_eventloop-00.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_eventloop-01.bin b/src/fuzz/corpus/fuzz_eventloop-01.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_eventloop-02.bin b/src/fuzz/corpus/fuzz_eventloop-02.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_eventloop-03.bin b/src/fuzz/corpus/fuzz_eventloop-03.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_eventloop-04.bin b/src/fuzz/corpus/fuzz_eventloop-04.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_eventloop-05.bin b/src/fuzz/corpus/fuzz_eventloop-05.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_eventloop-06.bin b/src/fuzz/corpus/fuzz_eventloop-06.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_eventloop-07.bin b/src/fuzz/corpus/fuzz_eventloop-07.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_eventloop-08.bin b/src/fuzz/corpus/fuzz_eventloop-08.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_eventloop-09.bin b/src/fuzz/corpus/fuzz_eventloop-09.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_eventloop-10.bin b/src/fuzz/corpus/fuzz_eventloop-10.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_eventloop-11.bin b/src/fuzz/corpus/fuzz_eventloop-11.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_eventloop-12.bin b/src/fuzz/corpus/fuzz_eventloop-12.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_eventloop-13.bin b/src/fuzz/corpus/fuzz_eventloop-13.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_eventloop-14.bin b/src/fuzz/corpus/fuzz_eventloop-14.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_eventloop-15.bin b/src/fuzz/corpus/fuzz_eventloop-15.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_eventloop-16.bin b/src/fuzz/corpus/fuzz_eventloop-16.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_eventloop-17.bin b/src/fuzz/corpus/fuzz_eventloop-17.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_eventloop-18.bin b/src/fuzz/corpus/fuzz_eventloop-18.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_memorypool-00.bin b/src/fuzz/corpus/fuzz_memorypool-00.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_memorypool-01.bin b/src/fuzz/corpus/fuzz_memorypool-01.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_memorypool-02.bin b/src/fuzz/corpus/fuzz_memorypool-02.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_memorypool-03.bin b/src/fuzz/corpus/fuzz_memorypool-03.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_memorypool-04.bin b/src/fuzz/corpus/fuzz_memorypool-04.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_memorypool-05.bin b/src/fuzz/corpus/fuzz_memorypool-05.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_memorypool-06.bin b/src/fuzz/corpus/fuzz_memorypool-06.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_memorypool-07.bin b/src/fuzz/corpus/fuzz_memorypool-07.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_memorypool-08.bin b/src/fuzz/corpus/fuzz_memorypool-08.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_memorypool-09.bin b/src/fuzz/corpus/fuzz_memorypool-09.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_memorypool-10.bin b/src/fuzz/corpus/fuzz_memorypool-10.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_memorypool-11.bin b/src/fuzz/corpus/fuzz_memorypool-11.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_memorypool-12.bin b/src/fuzz/corpus/fuzz_memorypool-12.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_memorypool-13.bin b/src/fuzz/corpus/fuzz_memorypool-13.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_memorypool-14.bin b/src/fuzz/corpus/fuzz_memorypool-14.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_memorypool-15.bin b/src/fuzz/corpus/fuzz_memorypool-15.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_memorypool-16.bin b/src/fuzz/corpus/fuzz_memorypool-16.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-00.bin b/src/fuzz/corpus/fuzz_options-00.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-01.bin b/src/fuzz/corpus/fuzz_options-01.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-02.bin b/src/fuzz/corpus/fuzz_options-02.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-03.bin b/src/fuzz/corpus/fuzz_options-03.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-04.bin b/src/fuzz/corpus/fuzz_options-04.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-05.bin b/src/fuzz/corpus/fuzz_options-05.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-06.bin b/src/fuzz/corpus/fuzz_options-06.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-07.bin b/src/fuzz/corpus/fuzz_options-07.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-08.bin b/src/fuzz/corpus/fuzz_options-08.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-09.bin b/src/fuzz/corpus/fuzz_options-09.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-10.bin b/src/fuzz/corpus/fuzz_options-10.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-11.bin b/src/fuzz/corpus/fuzz_options-11.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-12.bin b/src/fuzz/corpus/fuzz_options-12.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-13.bin b/src/fuzz/corpus/fuzz_options-13.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-14.bin b/src/fuzz/corpus/fuzz_options-14.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-15.bin b/src/fuzz/corpus/fuzz_options-15.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-16.bin b/src/fuzz/corpus/fuzz_options-16.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-17.bin b/src/fuzz/corpus/fuzz_options-17.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-18.bin b/src/fuzz/corpus/fuzz_options-18.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-19.bin b/src/fuzz/corpus/fuzz_options-19.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-20.bin b/src/fuzz/corpus/fuzz_options-20.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-21.bin b/src/fuzz/corpus/fuzz_options-21.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-22.bin b/src/fuzz/corpus/fuzz_options-22.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-23.bin b/src/fuzz/corpus/fuzz_options-23.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-24.bin b/src/fuzz/corpus/fuzz_options-24.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-25.bin b/src/fuzz/corpus/fuzz_options-25.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-26.bin b/src/fuzz/corpus/fuzz_options-26.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-27.bin b/src/fuzz/corpus/fuzz_options-27.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-28.bin b/src/fuzz/corpus/fuzz_options-28.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-29.bin b/src/fuzz/corpus/fuzz_options-29.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-30.bin b/src/fuzz/corpus/fuzz_options-30.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-31.bin b/src/fuzz/corpus/fuzz_options-31.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-32.bin b/src/fuzz/corpus/fuzz_options-32.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-33.bin b/src/fuzz/corpus/fuzz_options-33.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-34.bin b/src/fuzz/corpus/fuzz_options-34.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-35.bin b/src/fuzz/corpus/fuzz_options-35.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-36.bin b/src/fuzz/corpus/fuzz_options-36.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-37.bin b/src/fuzz/corpus/fuzz_options-37.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-38.bin b/src/fuzz/corpus/fuzz_options-38.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-39.bin b/src/fuzz/corpus/fuzz_options-39.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-40.bin b/src/fuzz/corpus/fuzz_options-40.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-41.bin b/src/fuzz/corpus/fuzz_options-41.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-42.bin b/src/fuzz/corpus/fuzz_options-42.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-43.bin b/src/fuzz/corpus/fuzz_options-43.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-44.bin b/src/fuzz/corpus/fuzz_options-44.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-45.bin b/src/fuzz/corpus/fuzz_options-45.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-46.bin b/src/fuzz/corpus/fuzz_options-46.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-47.bin b/src/fuzz/corpus/fuzz_options-47.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-48.bin b/src/fuzz/corpus/fuzz_options-48.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-49.bin b/src/fuzz/corpus/fuzz_options-49.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-50.bin b/src/fuzz/corpus/fuzz_options-50.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-51.bin b/src/fuzz/corpus/fuzz_options-51.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-52.bin b/src/fuzz/corpus/fuzz_options-52.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-53.bin b/src/fuzz/corpus/fuzz_options-53.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-54.bin b/src/fuzz/corpus/fuzz_options-54.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-55.bin b/src/fuzz/corpus/fuzz_options-55.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-56.bin b/src/fuzz/corpus/fuzz_options-56.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-57.bin b/src/fuzz/corpus/fuzz_options-57.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-58.bin b/src/fuzz/corpus/fuzz_options-58.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-59.bin b/src/fuzz/corpus/fuzz_options-59.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-60.bin b/src/fuzz/corpus/fuzz_options-60.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-61.bin b/src/fuzz/corpus/fuzz_options-61.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_options-62.bin b/src/fuzz/corpus/fuzz_options-62.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-00.bin b/src/fuzz/corpus/fuzz_tls-00.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-01.bin b/src/fuzz/corpus/fuzz_tls-01.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-02.bin b/src/fuzz/corpus/fuzz_tls-02.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-03.bin b/src/fuzz/corpus/fuzz_tls-03.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-04.bin b/src/fuzz/corpus/fuzz_tls-04.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-05.bin b/src/fuzz/corpus/fuzz_tls-05.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-06.bin b/src/fuzz/corpus/fuzz_tls-06.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-07.bin b/src/fuzz/corpus/fuzz_tls-07.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-08.bin b/src/fuzz/corpus/fuzz_tls-08.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-09.bin b/src/fuzz/corpus/fuzz_tls-09.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-10.bin b/src/fuzz/corpus/fuzz_tls-10.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-11.bin b/src/fuzz/corpus/fuzz_tls-11.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-12.bin b/src/fuzz/corpus/fuzz_tls-12.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-13.bin b/src/fuzz/corpus/fuzz_tls-13.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-14.bin b/src/fuzz/corpus/fuzz_tls-14.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-15.bin b/src/fuzz/corpus/fuzz_tls-15.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-16.bin b/src/fuzz/corpus/fuzz_tls-16.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-17.bin b/src/fuzz/corpus/fuzz_tls-17.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-18.bin b/src/fuzz/corpus/fuzz_tls-18.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-19.bin b/src/fuzz/corpus/fuzz_tls-19.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-20.bin b/src/fuzz/corpus/fuzz_tls-20.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-21.bin b/src/fuzz/corpus/fuzz_tls-21.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-22.bin b/src/fuzz/corpus/fuzz_tls-22.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-23.bin b/src/fuzz/corpus/fuzz_tls-23.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-24.bin b/src/fuzz/corpus/fuzz_tls-24.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-25.bin b/src/fuzz/corpus/fuzz_tls-25.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-26.bin b/src/fuzz/corpus/fuzz_tls-26.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-27.bin b/src/fuzz/corpus/fuzz_tls-27.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-28.bin b/src/fuzz/corpus/fuzz_tls-28.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-29.bin b/src/fuzz/corpus/fuzz_tls-29.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tls-30.bin b/src/fuzz/corpus/fuzz_tls-30.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/K14-fuzz_eventloop-force-close-not-closed.bin b/src/fuzz/corpus/known-findings/K14-fuzz_eventloop-force-close-not-closed.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/K9-fuzz_postprocessor-nested-boundary-leak.bin b/src/fuzz/corpus/known-findings/K9-fuzz_postprocessor-nested-boundary-leak.bin Binary files differ. diff --git a/src/fuzz/fuzz_eventloop.c b/src/fuzz/fuzz_eventloop.c @@ -0,0 +1,1985 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. + If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * @file fuzz/fuzz_eventloop.c + * @brief In-process fuzzer for MHD's external event loop and for the + * scheduling of the connection life cycle. + * @author Christian Grothoff + * + * fuzz_request.c already touches these entry points, but only as a side + * channel: it picks one event-loop mode per iteration and then always + * pumps in the same rhythm. The bugs this harness is after are in the + * *scheduling* -- what MHD does when the application polls at adversarial + * times, ignores the timeout it was given, calls MHD_run_from_select() + * with descriptor sets that do not match what MHD asked for, or + * suspends/resumes connections across those calls. + * + * The input is therefore not an HTTP request but a *schedule*: a short + * configuration block followed by a program of one-byte opcodes that is + * interpreted against a live `struct MHD_Daemon` with up to + * #MAX_CONNS socketpair connections. Request bytes enter the program + * through two of those opcodes (a fragment table and a raw literal), so + * the parser is still driven, just not fuzzed. + * + * Input format + * ------------ + * + * byte 0 daemon configuration + * bits 0-1 MHD_OPTION_CONNECTION_TIMEOUT selector, + * index into {1, 2, 0, 1} seconds + * bit 2 give the daemon a real listening socket (port 0) + * instead of MHD_USE_NO_LISTEN_SOCKET, so that + * MHD_quiesce_daemon() has something to do and the + * listen FD shows up in the descriptor sets + * bit 3 pass MHD_OPTION_APP_FD_SETSIZE + * bits 4-5 MHD_OPTION_CONNECTION_MEMORY_LIMIT selector, + * index into {default, 256, 1024, 4096} + * bit 6 MHD_OPTION_CONNECTION_LIMIT = 2, so that + * MHD_add_connection() starts failing + * bit 7 shrink SO_SNDBUF/SO_RCVBUF of the socketpair, so + * that responses do not fit and connections stay + * blocked on write + * byte 1 access-handler behaviour + * bits 0-1 response kind: 0 small static buffer, + * 1 chunked callback (MHD_SIZE_UNKNOWN), + * 2 large (128 KiB) static buffer, 3 empty + * bit 2 answer 500 instead of 200 + * bit 3 suspend in the handler and leave it parked + * bit 4 suspend and resume immediately in the handler + * bit 5 call MHD_set_connection_option() from the handler + * bit 6 call MHD_get_connection_info() from the handler + * bit 7 drain the client side after every run + * byte 2 event-loop defaults + * bits 0-2 default MHD_get_fdset*() variant + * bits 3-4 default run entry point + * bit 5 honour the timeout MHD returns + * bit 6 query the timeouts after every run + * bit 7 MHD_quiesce_daemon() before stopping + * byte 3 schedule knobs + * bits 0-1 number of connections opened up front (1-4) + * bit 2 run the timeout oracle after every operation + * bit 3 allow a real-time wait for a connection timeout + * to expire (globally budgeted, see below) + * bits 4-7 unused + * byte 4 artificial-clock step base + * byte 5. the operation program. Each operation is one byte, + * `(opcode << 4) | argument`; see enum op below. OP_SEND_RAW + * is the only one with an operand: a length byte followed by + * that many payload bytes. + * + * An input shorter than five bytes is rejected: the configuration block + * is mandatory and an input that short carries no program either. + * + * Oracles + * ------- + * + * Beyond ASAN/UBSAN and the MHD_set_panic_func() tripwire (any + * MHD_PANIC() reached this way is a finding, including the + * "MHD_stop_daemon() called while we have suspended connections" one + * that answers the "the daemon must always be stoppable" requirement): + * + * a) the four timeout accessors must agree. MHD_get_timeout(), + * MHD_get_timeout64(), MHD_get_timeout64s() and MHD_get_timeout_i() + * read the same state, so either all of them report a timeout or + * none of them does. Their values are read in sequence, so the + * clock may advance between two of them and the value may only + * *decrease*; connection_get_wait() has a documented 100 ms floor + * for the "exact match" case, hence the 100 ms slack. + * + * b) a reported timeout must never exceed the largest connection + * timeout in effect. MHD only ever derives it from + * `last_activity + connection_timeout_ms`, so a larger value would + * mean the deadline is somewhere the application cannot reach. + * + * c) MHD must never ask the application to wait forever while it has a + * live, not-suspended connection with a non-zero timeout: that is + * precisely the "timeout in the past" situation, i.e. a connection + * that can never be reaped. Live connections are tracked through + * MHD_OPTION_NOTIFY_CONNECTION, which brackets exactly the interval + * in which MHD owns the connection object. + * + * Both inputs of (b) and (c) -- whether a connection is suspended and + * what its timeout is -- are read back from MHD rather than modelled, + * because both diverge from what the application asked for: + * MHD_set_connection_option() silently does nothing while a + * connection is suspended. A harness that models them ends up + * reporting findings against its own model. + * + * d) MHD_get_fdset*() must not set a descriptor at or above the + * FD_SETSIZE limit it was given, `*max_fd` must be at least as large + * as every descriptor that was added, and `*max_fd` itself must be + * one of them. + * + * e) after MHD_quiesce_daemon() the listening socket must be gone from + * the descriptor sets. + * + * Two contract rules the harness has to respect (violating either makes + * MHD abort on a schedule that is perfectly legal, so the harness would + * only be finding its own bugs): + * + * * a connection is only suspended when the access handler is about to + * return MHD_YES. MHD_NO asks MHD to terminate the connection, and + * terminating one that the same callback just suspended trips + * mhd_assert (! connection->suspended) in MHD_connection_close_(); + * + * * every suspended connection is resumed before MHD_stop_daemon(). + * Several connections can be parked at once, so this is a set + * (@e lives[i].susp), not a single slot, and @e tearing_down stops + * the handler from parking anything new while the set is drained. + * + * MHD_suspend_connection() is additionally only called when MHD does not + * already consider the connection suspended, *unless* a resume of it is + * still pending -- that is the one case internal_suspend_connection_() + * handles itself (it cancels the resume), and the harness mirrors the + * resulting state. Calling it on an already-suspended connection would + * unlink it from a list it is not on. + * + * One more trap: with MHD_USE_ITC -- which MHD_ALLOW_SUSPEND_RESUME + * implies, so this harness always has it -- MHD_add_connection() only + * *queues* the socket, and the `struct MHD_Connection` is created later, + * from inside a run. There is therefore no moment at which the + * application could pair its own socket up with the connection object, + * and the registry of live connections has to be built purely from + * MHD_OPTION_NOTIFY_CONNECTION. This is why the client sockets + * (@e csock) and the connections (@e lives) are two separate arrays. + * + * Note on MHD_get_timeout_expiration(): no such function exists in the + * MHD 1.x API. The four accessors above are the complete family; the + * absolute-deadline form is a MHD 2.x addition. + */ + +#define FUZZ_HARNESS_NAME "fuzz_eventloop" +#include "fuzz_common.h" + +#include <microhttpd.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <sys/select.h> +#include <limits.h> + +/** Largest number of connections a single iteration may have open. */ +#define MAX_CONNS 8 + +/** Upper bound on the number of operations interpreted per iteration. */ +#define MAX_OPS 400 + +/** Size of the "large response" body. */ +#define BIG_BODY_LEN 131072 + +/** Bytes read back from the daemon are counted, never inspected. */ +#define DRAIN_BUF 4096 + + +/* ------------------------------------------------------------------ */ +/* Per-iteration configuration */ +/* ------------------------------------------------------------------ */ + +struct el_cfg +{ + unsigned int timeout_s; /**< MHD_OPTION_CONNECTION_TIMEOUT */ + int listen_sock; /**< real listening socket wanted */ + int app_fd_setsize; /**< pass MHD_OPTION_APP_FD_SETSIZE */ + size_t mem_limit; /**< MHD_OPTION_CONNECTION_MEMORY_LIMIT */ + int conn_limit; /**< MHD_OPTION_CONNECTION_LIMIT = 2 */ + int small_sockbuf; /**< shrink the socketpair buffers */ + + unsigned int resp_kind; /**< which response the handler queues */ + int error_reply; /**< answer 500 instead of 200 */ + int susp_park; /**< handler suspends and leaves it parked */ + int susp_now; /**< handler suspends and resumes at once */ + int hnd_connopt; /**< handler calls set_connection_option */ + int hnd_info; /**< handler calls get_connection_info */ + int auto_drain; /**< drain the client sockets after a run */ + + unsigned int fdset_var; /**< default MHD_get_fdset*() variant */ + unsigned int run_var; /**< default run entry point */ + int honour_timeout; /**< act on the timeout MHD returns */ + int timeout_every_run; /**< query the timeouts after every run */ + int quiesce_end; /**< quiesce before stopping */ + + unsigned int nconn_up_front; /**< connections opened before the program */ + int check_always; /**< run the timeout oracle after every op */ + int allow_real_wait; /**< may burn real time on a timeout expiry */ + uint8_t clock_seed; /**< artificial-clock step base */ +}; + +static struct el_cfg cfg; + +/** Connection memory limits selectable by byte 0. */ +static const size_t mem_limit_tbl[] = { 0, 256, 1024, 4096 }; + +/** Connection timeouts (seconds) selectable by byte 0. */ +static const unsigned int timeout_tbl[] = { 1, 2, 0, 1 }; + +/** + * FD_SETSIZE values handed to MHD_get_fdset2()/MHD_run_from_select2(). + * + * All of them are <= FD_SETSIZE on purpose. Passing a *larger* value + * than the `fd_set` objects actually hold would let MHD write past them, + * which is the application lying about its own buffers rather than + * anything MHD could defend against; the interesting direction is the + * smaller one, where MHD has to refuse descriptors that do not fit. + */ +static const unsigned int setsize_tbl[] = { + (unsigned int) FD_SETSIZE, + (unsigned int) FD_SETSIZE, + (unsigned int) FD_SETSIZE / 2, + 64, 16, 4, 1, (unsigned int) FD_SETSIZE +}; + + +/* ------------------------------------------------------------------ */ +/* Per-iteration state */ +/* ------------------------------------------------------------------ */ + +/** + * The client end of one socketpair. Deliberately *not* tied to a + * `struct MHD_Connection`: with MHD_USE_ITC (which + * MHD_ALLOW_SUSPEND_RESUME implies) MHD_add_connection() only queues the + * socket and the connection object is created later, from inside a run. + * There is therefore no moment at which the harness could pair the two + * up, and trying to would silently mistrack every connection. + */ +static int csock[MAX_CONNS]; +static unsigned int nconns; +static unsigned int cur_conn; + +/** + * A connection MHD currently owns. + * + * The registry is maintained purely from MHD_OPTION_NOTIFY_CONNECTION, + * which brackets exactly the interval in which the object exists: after + * MHD_CONNECTION_NOTIFY_CLOSED nothing may reference it any more, in + * particular not a deferred resume and not the "is there a live + * connection" oracle. + */ +struct live_conn +{ + struct MHD_Connection *mc; /**< NULL if the slot is free */ + int susp; /**< suspended by us, not resumed yet */ +}; + +/** A few more slots than connections, so the registry cannot overflow. */ +#define MAX_LIVE (MAX_CONNS + 4) + +static struct live_conn lives[MAX_LIVE]; + +/** Daemon of the current iteration. */ +static struct MHD_Daemon *cur_daemon; + +/** + * Set once the iteration only wants to drain the daemon. The handler + * then stops parking connections, which is what makes the flush loop in + * the teardown terminate. + */ +static int tearing_down; + +/** Listening socket returned by MHD_quiesce_daemon(), ours to close. */ +static MHD_socket quiesced_fd = MHD_INVALID_SOCKET; + +/** Artificial clock; only the harness's own scheduling depends on it. */ +static uint64_t clk_ms; +static uint64_t deadline_ms; +static int deadline_valid; + +/** Descriptor sets collected by the last OP_FDSET. */ +static fd_set g_rs; +static fd_set g_ws; +static fd_set g_es; +static MHD_socket g_max_fd = MHD_INVALID_SOCKET; +static unsigned int g_setsize = (unsigned int) FD_SETSIZE; +static int g_have_sets; + +/** The large response body, filled once. */ +static char big_body[BIG_BODY_LEN]; +static int big_body_ready; + +/** + * Budget for iterations that are allowed to wait in real time for a + * connection timeout to expire. MHD_OPTION_CONNECTION_TIMEOUT has a + * resolution of one second, so the expiry path cannot be reached without + * actually burning about that much wall clock; the budget keeps the + * whole run in the "couple of seconds" range while still exercising it. + * Override with MHD_FUZZ_EXPIRY_BUDGET. + */ +static int expiry_budget = 2; +static int expiry_budget_read; + +/** Statistics, printed at exit with --verbose. */ +static unsigned long stat_daemons; +static unsigned long stat_handler_calls; +static unsigned long stat_fdset_v1; +static unsigned long stat_fdset_v2; +static unsigned long stat_rfs_v1; +static unsigned long stat_rfs_v2; +static unsigned long stat_run; +static unsigned long stat_run_wait; +static unsigned long stat_timeouts; +static unsigned long stat_quiesce; +static unsigned long stat_suspend; +static unsigned long stat_resume; +static unsigned long stat_expiry_waits; +static int stats_registered; + + +static void +print_stats (void) +{ + if (! fuzz_verbose) + return; + fprintf (stderr, + "%s: daemons=%lu handler=%lu get_fdset=%lu get_fdset2=%lu " + "run_from_select=%lu run_from_select2=%lu run=%lu run_wait=%lu " + "timeout queries=%lu quiesce=%lu suspend=%lu resume=%lu " + "expiry waits=%lu\n", + FUZZ_HARNESS_NAME, + stat_daemons, stat_handler_calls, stat_fdset_v1, stat_fdset_v2, + stat_rfs_v1, stat_rfs_v2, stat_run, stat_run_wait, + stat_timeouts, stat_quiesce, stat_suspend, stat_resume, + stat_expiry_waits); +} + + +/* ------------------------------------------------------------------ */ +/* Callbacks */ +/* ------------------------------------------------------------------ */ + +static void +panic_cb (void *cls, + const char *file, + unsigned int line, + const char *reason) +{ + char msg[512]; + + (void) cls; + (void) snprintf (msg, sizeof (msg), + "MHD_PANIC() reached from an event-loop schedule " + "at %s:%u: %s", + (NULL != file) ? file : "?", + line, + (NULL != reason) ? reason : "?"); + fuzz_report_finding (msg); +} + + +/** + * Find the registry slot of @a c, or -1. + */ +static int +slot_of (const struct MHD_Connection *c) +{ + unsigned int i; + + for (i = 0; i < MAX_LIVE; i++) + if ( (NULL != lives[i].mc) && + (lives[i].mc == c) ) + return (int) i; + return -1; +} + + +static void +notify_conn_cb (void *cls, + struct MHD_Connection *connection, + void **socket_context, + enum MHD_ConnectionNotificationCode toe) +{ + unsigned int i; + int idx; + + (void) cls; + (void) socket_context; + if (MHD_CONNECTION_NOTIFY_STARTED == toe) + { + for (i = 0; i < MAX_LIVE; i++) + { + if (NULL != lives[i].mc) + continue; + lives[i].mc = connection; + lives[i].susp = 0; + return; + } + return; + } + /* MHD_CONNECTION_NOTIFY_CLOSED: the object is about to be freed. */ + idx = slot_of (connection); + if (0 <= idx) + { + lives[idx].mc = NULL; + lives[idx].susp = 0; + } +} + + +/* ------------------------------------------------------------------ */ +/* Suspend / resume bookkeeping */ +/* ------------------------------------------------------------------ */ + +/** + * Ask MHD whether it considers slot @a i suspended. + */ +static int +mhd_thinks_suspended (unsigned int i) +{ + const union MHD_ConnectionInfo *ci; + + if (NULL == lives[i].mc) + return 0; + ci = MHD_get_connection_info (lives[i].mc, + MHD_CONNECTION_INFO_CONNECTION_SUSPENDED); + if (NULL == ci) + return 0; + return (MHD_YES == ci->suspended); +} + + +/** + * Suspend slot @a i, if that is legal right now. + * + * MHD_suspend_connection() unlinks the connection from the timeout and + * connection lists, so calling it on a connection MHD already has on the + * suspended list corrupts those lists. The one exception is a + * connection with a resume still pending: internal_suspend_connection_() + * detects that itself and merely cancels the resume, leaving the + * connection suspended -- which is why @e susp is set in both branches. + */ +static void +do_suspend (unsigned int i) +{ + int mhd_susp; + + if ( (i >= MAX_LIVE) || + (NULL == lives[i].mc) ) + return; + mhd_susp = mhd_thinks_suspended (i); + if (mhd_susp && lives[i].susp) + return; /* really suspended already */ + MHD_suspend_connection (lives[i].mc); + lives[i].susp = 1; + stat_suspend++; +} + + +static void +do_resume (unsigned int i) +{ + if ( (i >= MAX_LIVE) || + (NULL == lives[i].mc) || + (! lives[i].susp) ) + return; + /* Clear first: MHD_resume_connection() may end up running the handler + later, which is allowed to suspend the very same connection again. */ + lives[i].susp = 0; + MHD_resume_connection (lives[i].mc); + stat_resume++; +} + + +/** + * Resume everything still parked. + * + * @return non-zero if at least one connection was resumed, so that the + * caller knows the daemon needs another round to act on it + */ +static int +resume_all (void) +{ + unsigned int i; + int any = 0; + + for (i = 0; i < MAX_LIVE; i++) + { + if ( (NULL == lives[i].mc) || + (! lives[i].susp) ) + continue; + lives[i].susp = 0; + MHD_resume_connection (lives[i].mc); + stat_resume++; + any = 1; + } + return any; +} + + +/** + * Summarise the connections MHD currently owns. + * + * Both properties are read back from MHD rather than modelled: a + * connection leaves the timeout lists exactly when MHD sets + * `connection->suspended`, and its timeout is exactly what + * MHD_set_connection_option() left there -- which is *not* what the + * application passed if the connection happened to be suspended at the + * time. Modelling either of them means the oracle eventually fires on + * the model rather than on MHD. + * + * @param[out] max_ms largest timeout, in milliseconds, of the + * connections that are in a timeout list right now + * @return non-zero if at least one such connection exists, i.e. if MHD + * must report a timeout + */ +static int +scan_live_timeouts (uint64_t *max_ms) +{ + unsigned int i; + int any = 0; + + *max_ms = 0; + for (i = 0; i < MAX_LIVE; i++) + { + const union MHD_ConnectionInfo *ci; + uint64_t ms; + + if (NULL == lives[i].mc) + continue; + ci = MHD_get_connection_info (lives[i].mc, + MHD_CONNECTION_INFO_CONNECTION_SUSPENDED); + if ( (NULL != ci) && + (MHD_YES == ci->suspended) ) + continue; /* not in any timeout list */ + ci = MHD_get_connection_info (lives[i].mc, + MHD_CONNECTION_INFO_CONNECTION_TIMEOUT); + if (NULL == ci) + continue; + ms = ((uint64_t) ci->connection_timeout) * 1000; + if (0 == ms) + continue; /* in a list, but exempt from timeouts */ + any = 1; + if (ms > *max_ms) + *max_ms = ms; + } + return any; +} + + +/* ------------------------------------------------------------------ */ +/* The access handler */ +/* ------------------------------------------------------------------ */ + +struct crc_state +{ + uint64_t total; + unsigned int pattern; +}; + + +static ssize_t +crc_cb (void *cls, + uint64_t pos, + char *buf, + size_t max) +{ + struct crc_state *st = (struct crc_state *) cls; + size_t n; + size_t i; + + if (pos >= st->total) + return MHD_CONTENT_READER_END_OF_STREAM; + n = (size_t) (st->total - pos); + if (n > max) + n = max; + if (0 == n) + return MHD_CONTENT_READER_END_OF_STREAM; + for (i = 0; i < n; i++) + buf[i] = (char) ('a' + (int) ((pos + i + st->pattern) % 26)); + return (ssize_t) n; +} + + +static void +crc_free (void *cls) +{ + free (cls); +} + + +static struct MHD_Response * +make_response (void) +{ + struct crc_state *st; + struct MHD_Response *r; + + switch (cfg.resp_kind) + { + case 1: + st = (struct crc_state *) calloc (1, sizeof (struct crc_state)); + if (NULL == st) + return NULL; + st->total = 700; + st->pattern = cfg.clock_seed; + r = MHD_create_response_from_callback (MHD_SIZE_UNKNOWN, + 128, + &crc_cb, + st, + &crc_free); + if (NULL == r) + free (st); + return r; + case 2: + /* Large enough not to fit into the socket buffers, so that the + connection stays blocked on write and MHD has to schedule it + through the write descriptor set over several rounds. */ + return MHD_create_response_from_buffer_static (BIG_BODY_LEN, big_body); + case 3: + return MHD_create_response_empty (MHD_RF_NONE); + default: + return MHD_create_response_from_buffer_static (2, "ok"); + } +} + + +static enum MHD_Result +ahc (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **req_cls) +{ + struct MHD_Response *resp; + enum MHD_Result ret; + int idx; + + (void) cls; + (void) url; + (void) method; + (void) version; + (void) upload_data; + if (NULL == *req_cls) + { + *req_cls = (void *) (intptr_t) 1; + return MHD_YES; + } + stat_handler_calls++; + if (0 != *upload_data_size) + { + *upload_data_size = 0; + return MHD_YES; + } + + idx = slot_of (connection); + if (cfg.hnd_connopt) + { + /* Moves the connection between the "normal" and the "manual" timeout + list, i.e. changes which connection MHD_get_timeout*() reports. */ + unsigned int nt = (0 == cfg.timeout_s) ? 1u : (cfg.timeout_s + 1u); + + (void) MHD_set_connection_option (connection, + MHD_CONNECTION_OPTION_TIMEOUT, + nt); + } + if (cfg.hnd_info) + { + volatile size_t sink = 0; + const union MHD_ConnectionInfo *ci; + + ci = MHD_get_connection_info (connection, + MHD_CONNECTION_INFO_CONNECTION_FD); + if (NULL != ci) + sink += (size_t) (ci->connect_fd + 1); + ci = MHD_get_connection_info (connection, + MHD_CONNECTION_INFO_CONNECTION_TIMEOUT); + if (NULL != ci) + sink += (size_t) ci->connection_timeout; + (void) sink; + } + + resp = make_response (); + if (NULL == resp) + return MHD_NO; + ret = MHD_queue_response (connection, + cfg.error_reply + ? MHD_HTTP_INTERNAL_SERVER_ERROR : MHD_HTTP_OK, + resp); + MHD_destroy_response (resp); + if (MHD_YES != ret) + return ret; + /* Only once the response was accepted: returning MHD_NO tells MHD to + terminate the connection, and terminating one that this very callback + suspended trips mhd_assert (! connection->suspended) in + MHD_connection_close_(). */ + if (tearing_down || + (0 > idx)) + return ret; + if (cfg.susp_now) + { + MHD_suspend_connection (connection); + MHD_resume_connection (connection); + stat_suspend++; + stat_resume++; + } + else if (cfg.susp_park && + (! lives[idx].susp)) + { + MHD_suspend_connection (connection); + lives[idx].susp = 1; + stat_suspend++; + } + return ret; +} + + +/* ------------------------------------------------------------------ */ +/* Oracles */ +/* ------------------------------------------------------------------ */ + +/** + * Query all four timeout accessors and cross-check them. + * + * The values are read one after the other, so the monotonic clock may + * advance in between and a later reading may be *smaller*. It may never + * be larger, except for the 100 ms floor connection_get_wait() falls back + * to when the elapsed time exactly matches the timeout. + */ +static void +check_timeouts (struct MHD_Daemon *d) +{ + MHD_UNSIGNED_LONG_LONG tl = 0; + uint64_t t64 = 0; + int64_t t64s; + int ti; + enum MHD_Result r1; + enum MHD_Result r2; + int have[4]; + uint64_t val[4]; + unsigned int k; + uint64_t max_ms; + int any_timed; + char msg[256]; + + stat_timeouts++; + r1 = MHD_get_timeout (d, &tl); + r2 = MHD_get_timeout64 (d, &t64); + t64s = MHD_get_timeout64s (d); + ti = MHD_get_timeout_i (d); + + have[0] = (MHD_YES == r1); + have[1] = (MHD_YES == r2); + have[2] = (0 <= t64s); + have[3] = (0 <= ti); + val[0] = (uint64_t) tl; + val[1] = t64; + val[2] = (uint64_t) (t64s < 0 ? 0 : t64s); + val[3] = (uint64_t) (ti < 0 ? 0 : ti); + + for (k = 1; k < 4; k++) + if (have[k] != have[0]) + fuzz_report_finding ( + "MHD_get_timeout*(): the four accessors disagree about whether " + "a timeout is in effect"); + + any_timed = scan_live_timeouts (&max_ms); + if (! have[0]) + { + /* An indefinite wait while a live, not-suspended connection has a + non-zero timeout means that connection can never be reaped: its + deadline is already unreachable for the application. */ + if (any_timed) + fuzz_report_finding ( + "MHD_get_timeout*() reported no timeout although the daemon has a " + "live, not suspended connection with a non-zero timeout"); + deadline_valid = 0; + return; + } + + for (k = 0; k < 4; k++) + { + if (val[k] <= max_ms) + continue; + (void) snprintf (msg, sizeof (msg), + "MHD_get_timeout*() [accessor %u] returned %llu ms, " + "larger than the largest connection timeout in effect " + "(%llu ms)", + k, + (unsigned long long) val[k], + (unsigned long long) max_ms); + fuzz_report_finding (msg); + } + for (k = 1; k < 4; k++) + if ( (val[k] > val[k - 1]) && + (val[k] > 100) ) + fuzz_report_finding ( + "MHD_get_timeout*(): a later accessor reported a larger timeout " + "than an earlier one, although the deadline cannot have moved"); + + deadline_ms = clk_ms + val[1]; + deadline_valid = 1; +} + + +/** + * Check the descriptor sets that the last MHD_get_fdset*() produced. + * + * @param setsize the FD_SETSIZE limit that was passed to MHD + * @param max_fd the reported maximum, #MHD_INVALID_SOCKET if none + * @param had_max whether a @a max_fd pointer was passed at all + * @param with_es whether an except set was passed + */ +static void +check_fdsets (unsigned int setsize, + MHD_socket max_fd, + int had_max, + int with_es) +{ + int f; + int hi = -1; + int max_seen = 0; + + for (f = 0; f < (int) FD_SETSIZE; f++) + { + int in_set = FD_ISSET (f, &g_rs) || FD_ISSET (f, &g_ws); + + if (with_es && FD_ISSET (f, &g_es)) + in_set = 1; + if (! in_set) + continue; + if ((unsigned int) f >= setsize) + fuzz_report_finding ( + "MHD_get_fdset*() added a descriptor at or above the FD_SETSIZE " + "limit it was given"); + if (f > hi) + hi = f; + if ( (had_max) && + (MHD_INVALID_SOCKET != max_fd) && + (f == (int) max_fd) ) + max_seen = 1; + } + if (! had_max) + return; + if (0 > hi) + { + if (MHD_INVALID_SOCKET != max_fd) + fuzz_report_finding ( + "MHD_get_fdset*() set max_fd although it added no descriptor"); + return; + } + if (MHD_INVALID_SOCKET == max_fd) + fuzz_report_finding ( + "MHD_get_fdset*() added descriptors but left max_fd unset"); + if (hi > (int) max_fd) + fuzz_report_finding ( + "MHD_get_fdset*() added a descriptor larger than the max_fd it " + "reported"); + if ((unsigned int) max_fd >= setsize) + fuzz_report_finding ( + "MHD_get_fdset*() reported a max_fd at or above the FD_SETSIZE " + "limit it was given"); + if (! max_seen) + fuzz_report_finding ( + "MHD_get_fdset*() reported a max_fd that is not in any of the sets"); +} + + +/* ------------------------------------------------------------------ */ +/* Event-loop primitives */ +/* ------------------------------------------------------------------ */ + +/** + * Collect the descriptor sets. + * + * Variant 0 goes through the *real* v1 entry point. microhttpd.h also + * defines MHD_get_fdset as a macro forwarding to MHD_get_fdset2 with + * FD_SETSIZE, so the name has to be parenthesised or the v1 function is + * never reached at all. Same trick for MHD_run_from_select below. + */ +static void +op_fdset (struct MHD_Daemon *d, + unsigned int var) +{ + MHD_socket max_fd = MHD_INVALID_SOCKET; + unsigned int setsize = (unsigned int) FD_SETSIZE; + int had_max = 1; + int with_es = 1; + + FD_ZERO (&g_rs); + FD_ZERO (&g_ws); + FD_ZERO (&g_es); + switch (var % 5) + { + case 0: + stat_fdset_v1++; + (void) (MHD_get_fdset) (d, &g_rs, &g_ws, &g_es, &max_fd); + break; + case 1: + stat_fdset_v2++; + (void) MHD_get_fdset2 (d, &g_rs, &g_ws, &g_es, &max_fd, + (unsigned int) FD_SETSIZE); + break; + case 2: + stat_fdset_v2++; + setsize = setsize_tbl[(var + cfg.clock_seed) + % (sizeof (setsize_tbl) + / sizeof (setsize_tbl[0]))]; + (void) MHD_get_fdset2 (d, &g_rs, &g_ws, &g_es, &max_fd, setsize); + break; + case 3: + /* max_fd is documented as optional */ + stat_fdset_v2++; + had_max = 0; + (void) MHD_get_fdset2 (d, &g_rs, &g_ws, &g_es, NULL, + (unsigned int) FD_SETSIZE); + break; + default: + /* no except set: deprecated, but shipped API */ + stat_fdset_v2++; + with_es = 0; + (void) MHD_get_fdset2 (d, &g_rs, &g_ws, NULL, &max_fd, + (unsigned int) FD_SETSIZE); + break; + } + check_fdsets (setsize, max_fd, had_max, with_es); + g_max_fd = max_fd; + g_setsize = setsize; + g_have_sets = 1; + if ( (MHD_INVALID_SOCKET != quiesced_fd) && + ((unsigned int) quiesced_fd < (unsigned int) FD_SETSIZE) && + FD_ISSET (quiesced_fd, &g_rs) ) + fuzz_report_finding ( + "MHD_get_fdset*() still watches the listening socket after " + "MHD_quiesce_daemon() handed it back to the application"); +} + + +/** + * select() on the sets collected last, with a zero timeout. + * + * The harness is single threaded and everything MHD could be waiting for + * has already been written into the socketpairs, so blocking would only + * burn wall clock; select() is called purely to fill in the readiness. + */ +static void +op_poll (void) +{ + struct timeval tv; + + if ( (! g_have_sets) || + (MHD_INVALID_SOCKET == g_max_fd) ) + return; + tv.tv_sec = 0; + tv.tv_usec = 0; + (void) select ((int) g_max_fd + 1, &g_rs, &g_ws, &g_es, &tv); +} + + +/** + * Drain whatever the daemon has produced on slot @a i, so that a large + * response can make progress. + */ +static void +drain_conn (unsigned int i) +{ + char tmp[DRAIN_BUF]; + + if ( (i >= MAX_CONNS) || + (0 > csock[i]) ) + return; + for (;;) + { + ssize_t n = recv (csock[i], tmp, sizeof (tmp), MSG_DONTWAIT); + + if (0 >= n) + break; + } +} + + +static void +drain_all (void) +{ + unsigned int i; + + for (i = 0; i < MAX_CONNS; i++) + drain_conn (i); +} + + +/** + * Advance the daemon once. + * + * @param d the daemon + * @param var which entry point to use + * @param flavour which descriptor sets to hand over: + * 0 the ones MHD asked for, 1 all-zero, 2 all-ones, + * 3 only the read set as collected + */ +static void +op_run (struct MHD_Daemon *d, + unsigned int var, + unsigned int flavour) +{ + fd_set rs; + fd_set ws; + fd_set es; + + switch (flavour % 4) + { + case 1: + FD_ZERO (&rs); + FD_ZERO (&ws); + FD_ZERO (&es); + break; + case 2: + /* Every descriptor number reported ready. MHD only tests the bits of + the sockets it owns, so this is an application that lies about + readiness, not an invalid descriptor. */ + memset (&rs, 0xFF, sizeof (rs)); + memset (&ws, 0xFF, sizeof (ws)); + memset (&es, 0xFF, sizeof (es)); + break; + case 3: + rs = g_rs; + FD_ZERO (&ws); + FD_ZERO (&es); + break; + default: + rs = g_rs; + ws = g_ws; + es = g_es; + break; + } + switch (var % 4) + { + case 0: + stat_rfs_v1++; + (void) (MHD_run_from_select) (d, &rs, &ws, &es); + break; + case 1: + stat_rfs_v2++; + (void) MHD_run_from_select2 (d, &rs, &ws, &es, + (g_setsize > (unsigned int) FD_SETSIZE) + ? (unsigned int) FD_SETSIZE : g_setsize); + break; + case 2: + stat_run++; + (void) MHD_run (d); + break; + default: + stat_run_wait++; + (void) MHD_run_wait (d, 0); + break; + } + deadline_valid = 0; + if (cfg.auto_drain) + drain_all (); + if (cfg.timeout_every_run) + check_timeouts (d); +} + + +/* ------------------------------------------------------------------ */ +/* Connections */ +/* ------------------------------------------------------------------ */ + +static int +new_connection (struct MHD_Daemon *d) +{ + int sv[2]; + struct sockaddr_in sa; + unsigned int idx; + + if (nconns >= MAX_CONNS) + return -1; + idx = nconns; + if (0 != socketpair (AF_UNIX, SOCK_STREAM, 0, sv)) + return -1; + if (cfg.small_sockbuf) + { + int bs = 2048; + + (void) setsockopt (sv[0], SOL_SOCKET, SO_RCVBUF, &bs, sizeof (bs)); + (void) setsockopt (sv[1], SOL_SOCKET, SO_SNDBUF, &bs, sizeof (bs)); + } + memset (&sa, 0, sizeof (sa)); + sa.sin_family = AF_INET; + sa.sin_port = htons (44444); + sa.sin_addr.s_addr = htonl (INADDR_LOOPBACK); + csock[idx] = sv[0]; + if (MHD_YES != MHD_add_connection (d, + (MHD_socket) sv[1], + (const struct sockaddr *) &sa, + (socklen_t) sizeof (sa))) + { + /* MHD has already closed sv[1] in that case. */ + (void) close (sv[0]); + csock[idx] = -1; + return -1; + } + nconns++; + cur_conn = idx; + return (int) idx; +} + + +/** + * Drop our end of connection @a i. + * + * @param graceful non-zero to shut the write side down first (an orderly + * client close), zero to just close (an abrupt one, which MHD sees + * as a reset) + */ +static void +close_conn (unsigned int i, + int graceful) +{ + if ( (i >= MAX_CONNS) || + (0 > csock[i]) ) + return; + if (graceful) + (void) shutdown (csock[i], SHUT_WR); + (void) close (csock[i]); + csock[i] = -1; +} + + +static void +send_bytes (unsigned int i, + const void *buf, + size_t len) +{ + const char *p = (const char *) buf; + size_t off = 0; + unsigned int tries = 0; + + if ( (i >= MAX_CONNS) || + (0 > csock[i]) || + (0 == len) ) + return; + while ( (off < len) && + (tries < 4) ) + { + ssize_t s = send (csock[i], p + off, len - off, MSG_DONTWAIT); + + if (0 < s) + { + off += (size_t) s; + continue; + } + tries++; + /* The peer's receive buffer is full because MHD has not run yet, or + MHD is gone. One round is enough to tell the two apart; the rest + of the fragment is simply dropped, which is a split point like any + other. */ + if (NULL != cur_daemon) + (void) MHD_run (cur_daemon); + if ( (0 > s) && + (EAGAIN != errno) && + (EWOULDBLOCK != errno) && + (EINTR != errno) ) + break; + } +} + + +/** + * Request fragments. Whole requests, halves of requests and pipelines, + * so that a schedule can leave a connection in any parser state while it + * plays with the event loop. + */ +static const char *const frag_tbl[16] = { + "GET / HTTP/1.1\r\nHost: x\r\n\r\n", + "GET /a HTTP/1.1\r\nHost: x\r\n", + "\r\n", + "POST /p HTTP/1.1\r\nHost: x\r\nContent-Length: 10\r\n\r\n", + "0123456789", + "POST /c HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\n\r\n", + "5\r\nabcde\r\n", + "0\r\n\r\n", + "GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", + "GET / HTTP/1.0\r\n\r\n", + "HEAD / HTTP/1.1\r\nHost: x\r\n\r\n", + "GET / HTTP/1.1\r\nHost: x\r\n\r\nGET /2 HTTP/1.1\r\nHost: x\r\n\r\n", + "G", + "ET / HTTP/1.1\r\n", + "Host: x\r\n\r\n", + "\r\n\r\n" +}; + + +/* ------------------------------------------------------------------ */ +/* The operation program */ +/* ------------------------------------------------------------------ */ + +enum op +{ + OP_SEND_FRAG = 0, + OP_SEND_RAW = 1, + OP_FDSET = 2, + OP_RUN = 3, + OP_TIMEOUT = 4, + OP_CLOCK = 5, + OP_SUSPEND = 6, + OP_RESUME = 7, + OP_NEWCONN = 8, + OP_CLOSECONN = 9, + OP_SWITCH = 10, + OP_QUIESCE = 11, + OP_DRAIN = 12, + OP_CONNOPT = 13, + OP_INFO = 14, + OP_POLL = 15 +}; + + +/** + * Wait in real time until the connection timeout of the current daemon + * has certainly expired, so that the expiry path is actually reached. + * + * MHD_OPTION_CONNECTION_TIMEOUT has a resolution of one second, so this + * costs about that much wall clock and is therefore globally budgeted. + */ +static void +wait_for_expiry (struct MHD_Daemon *d) +{ + struct timeval tv; + + if ( (! cfg.allow_real_wait) || + (1 != cfg.timeout_s) || + (0 >= expiry_budget) ) + return; + expiry_budget--; + stat_expiry_waits++; + tv.tv_sec = 1; + tv.tv_usec = 50000; + (void) select (0, NULL, NULL, NULL, &tv); + clk_ms += 1050; + /* Everything parked has to come back first, or the expiry cannot be + observed for it at all. */ + (void) resume_all (); + (void) MHD_run (d); + (void) MHD_run (d); +} + + +static void +op_quiesce (struct MHD_Daemon *d) +{ + MHD_socket ls; + + stat_quiesce++; + ls = MHD_quiesce_daemon (d); + if (MHD_INVALID_SOCKET == ls) + return; + if (MHD_INVALID_SOCKET != quiesced_fd) + { + /* MHD_quiesce_daemon() is documented to hand the socket over once + and to answer MHD_INVALID_SOCKET afterwards. */ + (void) close (ls); + fuzz_report_finding ( + "MHD_quiesce_daemon() handed out the listening socket twice"); + return; + } + quiesced_fd = ls; +} + + +static void +op_info (struct MHD_Daemon *d, + unsigned int arg) +{ + const union MHD_DaemonInfo *di; + const union MHD_ConnectionInfo *ci; + volatile size_t sink = 0; + unsigned int i = arg % MAX_LIVE; + + di = MHD_get_daemon_info (d, MHD_DAEMON_INFO_CURRENT_CONNECTIONS); + if (NULL != di) + sink += (size_t) di->num_connections; + di = MHD_get_daemon_info (d, MHD_DAEMON_INFO_FLAGS); + if (NULL != di) + sink += (size_t) di->flags; + if (NULL != lives[i].mc) + { + ci = MHD_get_connection_info (lives[i].mc, + MHD_CONNECTION_INFO_CONNECTION_SUSPENDED); + if (NULL != ci) + sink += (size_t) ci->suspended; + ci = MHD_get_connection_info (lives[i].mc, + MHD_CONNECTION_INFO_DAEMON); + if (NULL != ci) + sink += (NULL != ci->daemon) ? 1u : 0u; + } + (void) sink; +} + + +static void +op_connopt (unsigned int i, + unsigned int val) +{ + if ( (i >= MAX_LIVE) || + (NULL == lives[i].mc) ) + return; + /* Note that MHD skips the whole update while connection->suspended is + set, so this is not necessarily the timeout the connection ends up + with -- which is why the oracle reads it back instead. */ + (void) MHD_set_connection_option (lives[i].mc, + MHD_CONNECTION_OPTION_TIMEOUT, + val); +} + + +/* ------------------------------------------------------------------ */ +/* The fuzz target */ +/* ------------------------------------------------------------------ */ + +int +LLVMFuzzerTestOneInput (const uint8_t *data, + size_t size) +{ + struct MHD_Daemon *d; + struct MHD_OptionItem opts[8]; + unsigned int nopt = 0; + unsigned int flags; + size_t pos; + unsigned int nops = 0; + unsigned int i; + + /* Must happen before the first write() into a socketpair. The built-in + driver also does this, but that code is compiled out under + -DFUZZ_NO_MAIN, which is exactly the build every external fuzzing + engine uses; the call is idempotent. See fuzz_ignore_sigpipe() in + fuzz_common.h for why the process dies without it. */ + fuzz_ignore_sigpipe (); + + if (size < 5) + return 0; + + if (! big_body_ready) + { + big_body_ready = 1; + for (i = 0; i < BIG_BODY_LEN; i++) + big_body[i] = (char) ('a' + (i % 26)); + } + if (! expiry_budget_read) + { + const char *e = getenv ("MHD_FUZZ_EXPIRY_BUDGET"); + + expiry_budget_read = 1; + if (NULL != e) + expiry_budget = atoi (e); + } + + memset (&cfg, 0, sizeof (cfg)); + /* Must never carry over: everything these point at belonged to the + previous iteration's daemon and is long gone. */ + memset (lives, 0, sizeof (lives)); + for (i = 0; i < MAX_CONNS; i++) + csock[i] = -1; + nconns = 0; + cur_conn = 0; + tearing_down = 0; + quiesced_fd = MHD_INVALID_SOCKET; + clk_ms = 0; + deadline_ms = 0; + deadline_valid = 0; + g_have_sets = 0; + g_max_fd = MHD_INVALID_SOCKET; + g_setsize = (unsigned int) FD_SETSIZE; + + cfg.timeout_s = timeout_tbl[data[0] & 0x03]; + cfg.listen_sock = (0 != (data[0] & 0x04)); + cfg.app_fd_setsize = (0 != (data[0] & 0x08)); + cfg.mem_limit = mem_limit_tbl[(data[0] >> 4) & 0x03]; + cfg.conn_limit = (0 != (data[0] & 0x40)); + cfg.small_sockbuf = (0 != (data[0] & 0x80)); + + cfg.resp_kind = (unsigned int) (data[1] & 0x03); + cfg.error_reply = (0 != (data[1] & 0x04)); + cfg.susp_park = (0 != (data[1] & 0x08)); + cfg.susp_now = (0 != (data[1] & 0x10)); + cfg.hnd_connopt = (0 != (data[1] & 0x20)); + cfg.hnd_info = (0 != (data[1] & 0x40)); + cfg.auto_drain = (0 != (data[1] & 0x80)); + + cfg.fdset_var = (unsigned int) (data[2] & 0x07); + cfg.run_var = (unsigned int) ((data[2] >> 3) & 0x03); + cfg.honour_timeout = (0 != (data[2] & 0x20)); + cfg.timeout_every_run = (0 != (data[2] & 0x40)); + cfg.quiesce_end = (0 != (data[2] & 0x80)); + + cfg.nconn_up_front = 1u + (unsigned int) (data[3] & 0x03); + cfg.check_always = (0 != (data[3] & 0x04)); + cfg.allow_real_wait = (0 != (data[3] & 0x08)); + cfg.clock_seed = data[4]; + + if (0 != cfg.mem_limit) + { + opts[nopt].option = MHD_OPTION_CONNECTION_MEMORY_LIMIT; + opts[nopt].value = (intptr_t) cfg.mem_limit; + opts[nopt].ptr_value = NULL; + nopt++; + } + opts[nopt].option = MHD_OPTION_CONNECTION_TIMEOUT; + opts[nopt].value = (intptr_t) cfg.timeout_s; + opts[nopt].ptr_value = NULL; + nopt++; + if (cfg.conn_limit) + { + opts[nopt].option = MHD_OPTION_CONNECTION_LIMIT; + opts[nopt].value = (intptr_t) 2; + opts[nopt].ptr_value = NULL; + nopt++; + } + if (cfg.app_fd_setsize) + { + opts[nopt].option = MHD_OPTION_APP_FD_SETSIZE; + opts[nopt].value = (intptr_t) FD_SETSIZE; + opts[nopt].ptr_value = NULL; + nopt++; + } + opts[nopt].option = MHD_OPTION_END; + opts[nopt].value = 0; + opts[nopt].ptr_value = NULL; + + /* MHD_ALLOW_SUSPEND_RESUME is always on: it is what this harness is + about, and it implies MHD_USE_ITC, whose descriptor is one more thing + MHD_get_fdset*() has to report correctly. */ + flags = MHD_ALLOW_SUSPEND_RESUME; + if (! cfg.listen_sock) + flags |= MHD_USE_NO_LISTEN_SOCKET; + if (fuzz_verbose) + flags |= MHD_USE_ERROR_LOG; + + MHD_set_panic_func (&panic_cb, NULL); + d = MHD_start_daemon (flags, + 0, + NULL, NULL, + &ahc, NULL, + MHD_OPTION_ARRAY, opts, + /* through the varargs rather than the option + array: storing a function pointer in the + array's intptr_t member is not strictly + conforming C */ + MHD_OPTION_NOTIFY_CONNECTION, ¬ify_conn_cb, NULL, + MHD_OPTION_END); + if ( (NULL == d) && + cfg.listen_sock) + { + /* No networking available: fall back to the socketpair-only daemon + rather than losing the whole iteration. */ + cfg.listen_sock = 0; + flags |= MHD_USE_NO_LISTEN_SOCKET; + d = MHD_start_daemon (flags, + 0, + NULL, NULL, + &ahc, NULL, + MHD_OPTION_ARRAY, opts, + MHD_OPTION_NOTIFY_CONNECTION, ¬ify_conn_cb, NULL, + MHD_OPTION_END); + } + if (NULL == d) + return 0; + cur_daemon = d; + stat_daemons++; + if (! stats_registered) + { + stats_registered = 1; + (void) atexit (&print_stats); + } + + for (i = 0; i < cfg.nconn_up_front; i++) + if (0 > new_connection (d)) + break; + + pos = 5; + while ( (pos < size) && + (nops < MAX_OPS) ) + { + const unsigned int b = data[pos++]; + const unsigned int opc = b >> 4; + const unsigned int arg = b & 0x0F; + + nops++; + switch (opc) + { + case OP_SEND_FRAG: + { + const char *f = frag_tbl[arg]; + + send_bytes (cur_conn, f, strlen (f)); + break; + } + case OP_SEND_RAW: + { + size_t l; + + if (pos >= size) + break; + l = data[pos++]; + if (l > size - pos) + l = size - pos; + send_bytes (cur_conn, data + pos, l); + pos += l; + break; + } + case OP_FDSET: + op_fdset (d, (0 != (arg & 0x08)) ? cfg.fdset_var : arg); + break; + case OP_RUN: + op_run (d, arg & 0x03, (arg >> 2) & 0x03); + break; + case OP_TIMEOUT: + check_timeouts (d); + if ( (cfg.honour_timeout) && + (deadline_valid) && + (deadline_ms <= clk_ms) ) + op_run (d, cfg.run_var, 0); + break; + case OP_CLOCK: + if (15 == arg) + wait_for_expiry (d); + else + { + clk_ms += (uint64_t) (1u + arg) * (uint64_t) (1u + cfg.clock_seed % 64u); + if ( (cfg.honour_timeout) && + (deadline_valid) && + (deadline_ms <= clk_ms) ) + op_run (d, cfg.run_var, 0); + } + break; + case OP_SUSPEND: + do_suspend (arg % MAX_LIVE); + break; + case OP_RESUME: + do_resume (arg % MAX_LIVE); + break; + case OP_NEWCONN: + (void) new_connection (d); + break; + case OP_CLOSECONN: + close_conn ((0 != (arg & 0x08)) ? cur_conn : (arg % MAX_CONNS), + (0 != (arg & 0x04))); + break; + case OP_SWITCH: + if ( (arg % MAX_CONNS) < nconns) + cur_conn = arg % MAX_CONNS; + break; + case OP_QUIESCE: + op_quiesce (d); + break; + case OP_DRAIN: + if (0 != (arg & 0x08)) + drain_all (); + else + drain_conn (arg % MAX_CONNS); + break; + case OP_CONNOPT: + op_connopt (arg % MAX_LIVE, (arg >> 3) & 0x01); + break; + case OP_INFO: + op_info (d, arg); + break; + default: + op_poll (); + break; + } + if (cfg.check_always) + check_timeouts (d); + } + + /* ---- teardown ---- */ + tearing_down = 1; + for (i = 0; i < MAX_CONNS; i++) + close_conn (i, 1); + /* A connection left suspended makes MHD_stop_daemon() MHD_PANIC() + ("called while we have suspended connections"), and + MHD_resume_connection() alone is not enough: it only raises a flag, + and the connection leaves the suspended list in MHD_run(). So flush + and run until nothing is parked any more. tearing_down keeps the + handler from parking anything new, which is what bounds this loop; + the cap is only a backstop. */ + for (i = 0; i < MAX_CONNS + 2u; i++) + { + if (! resume_all ()) + break; + (void) MHD_run (d); + } + (void) MHD_run (d); + if (cfg.quiesce_end) + op_quiesce (d); + MHD_stop_daemon (d); + cur_daemon = NULL; + if (MHD_INVALID_SOCKET != quiesced_fd) + { + (void) close (quiesced_fd); + quiesced_fd = MHD_INVALID_SOCKET; + } + for (i = 0; i < MAX_CONNS; i++) + close_conn (i, 0); + memset (lives, 0, sizeof (lives)); + return 0; +} + + +/* ------------------------------------------------------------------ */ +/* Structure-aware schedule generator */ +/* ------------------------------------------------------------------ */ + +struct sbuf +{ + uint8_t *p; + size_t len; + size_t cap; +}; + + +static void +sb_byte (struct sbuf *b, + uint8_t v) +{ + if (b->len < b->cap) + b->p[b->len++] = v; +} + + +static void +sb_op (struct sbuf *b, + unsigned int opc, + unsigned int arg) +{ + sb_byte (b, (uint8_t) ((opc << 4) | (arg & 0x0F))); +} + + +/** + * Fragment indices, weighted towards the ones that complete a request: + * a schedule that never reaches the access handler exercises very little + * of the connection life cycle. + */ +static const unsigned char gen_frag_bias[] = { + 0, 0, 0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 11, 1, 2, 12, 13, 14, 15 +}; + + +static unsigned int +gen_frag (struct fuzz_rng *rng) +{ + if (fuzz_chance (rng, 4)) + return fuzz_below (rng, 16); + return gen_frag_bias[fuzz_below (rng, + (uint32_t) sizeof (gen_frag_bias))]; +} + + +/** + * Emit "collect the descriptors, poll, run" -- the shape a real external + * event loop has -- with a randomly chosen variant of each step. + */ +static void +gen_loop_round (struct fuzz_rng *rng, + struct sbuf *b) +{ + unsigned int v; + unsigned int fl; + + if (! fuzz_chance (rng, 5)) + sb_op (b, OP_FDSET, fuzz_below (rng, 16)); + if (! fuzz_chance (rng, 3)) + sb_op (b, OP_POLL, 0); + if (fuzz_chance (rng, 3)) + sb_op (b, OP_TIMEOUT, fuzz_below (rng, 16)); + v = fuzz_below (rng, 4); + /* Half the runs use the descriptor sets MHD actually asked for, so + that requests make progress; the other half are the adversarial + ones (all-zero, all-ones, read set only). */ + fl = fuzz_chance (rng, 2) ? 0u : fuzz_below (rng, 4); + sb_op (b, OP_RUN, v | (fl << 2)); +} + + +static size_t +fuzz_generate (struct fuzz_rng *rng, + uint8_t *buf, + size_t cap) +{ + struct sbuf b; + unsigned int nrounds; + unsigned int i; + unsigned int r; + + b.p = buf; + b.len = 0; + b.cap = cap; + + /* --- configuration block --- */ + sb_byte (&b, fuzz_byte (rng)); + sb_byte (&b, fuzz_byte (rng)); + sb_byte (&b, fuzz_byte (rng)); + sb_byte (&b, fuzz_byte (rng)); + sb_byte (&b, fuzz_byte (rng)); + + nrounds = 3u + fuzz_below (rng, 40); + for (i = 0; i < nrounds; i++) + { + switch (fuzz_below (rng, 24)) + { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + /* feed the current connection */ + sb_op (&b, OP_SEND_FRAG, gen_frag (rng)); + gen_loop_round (rng, &b); + break; + case 6: + /* raw literal, so that a mutator has somewhere to put bytes */ + { + unsigned int n = fuzz_below (rng, 24); + unsigned int k; + + sb_op (&b, OP_SEND_RAW, 0); + sb_byte (&b, (uint8_t) n); + for (k = 0; k < n; k++) + sb_byte (&b, fuzz_byte (rng)); + break; + } + case 7: + case 8: + gen_loop_round (rng, &b); + break; + case 9: + r = fuzz_below (rng, 16); + sb_op (&b, OP_SUSPEND, r); + break; + case 10: + r = fuzz_below (rng, 16); + sb_op (&b, OP_RESUME, r); + break; + case 11: + sb_op (&b, OP_NEWCONN, 0); + sb_op (&b, OP_SEND_FRAG, gen_frag (rng)); + break; + case 12: + r = fuzz_below (rng, 16); + sb_op (&b, OP_CLOSECONN, r); + break; + case 13: + r = fuzz_below (rng, 16); + sb_op (&b, OP_SWITCH, r); + break; + case 14: + sb_op (&b, OP_QUIESCE, 0); + break; + case 15: + r = fuzz_below (rng, 16); + sb_op (&b, OP_DRAIN, r); + break; + case 16: + r = fuzz_below (rng, 16); + sb_op (&b, OP_CONNOPT, r); + break; + case 17: + r = fuzz_below (rng, 16); + sb_op (&b, OP_INFO, r); + break; + case 18: + r = fuzz_below (rng, 16); + sb_op (&b, OP_TIMEOUT, r); + break; + case 19: + /* an artificial clock jump, occasionally the real one that lets a + connection time out */ + r = fuzz_chance (rng, 40) ? 15u : fuzz_below (rng, 15); + sb_op (&b, OP_CLOCK, r); + break; + case 20: + /* a run without ever asking MHD what it wanted */ + r = fuzz_below (rng, 16); + sb_op (&b, OP_RUN, r); + break; + case 21: + sb_op (&b, OP_POLL, 0); + break; + default: + sb_op (&b, OP_SEND_FRAG, gen_frag (rng)); + break; + } + } + /* Make sure most schedules end with the daemon actually run, so that + the interesting states are reached rather than only set up. */ + gen_loop_round (rng, &b); + return b.len; +} + + +/* ------------------------------------------------------------------ */ +/* Built-in seed corpus */ +/* ------------------------------------------------------------------ */ + +/** + * A seed is the five configuration bytes plus a literal operation + * program. Programs are short on purpose: each one is meant to pin down + * one entry point or one life-cycle transition, and libFuzzer's -merge + * keeps the shortest input reaching a given edge. + */ +struct seed_def +{ + const char *name; + unsigned char cfg[5]; + unsigned char ops[24]; + unsigned char nops; +}; + +#define OPB(o,a) (unsigned char) (((o) << 4) | (a)) + +static const struct seed_def seeds[] = { + /* MHD_get_fdset() (the real v1 entry point) + select() + + MHD_run_from_select() (also v1) */ + { "fdset-v1-run-from-select-v1", { 0x00, 0x80, 0x00, 0x00, 0x00 }, + { OPB (OP_SEND_FRAG, 0), OPB (OP_FDSET, 0), OPB (OP_POLL, 0), + OPB (OP_RUN, 0), OPB (OP_TIMEOUT, 0), OPB (OP_FDSET, 0), + OPB (OP_POLL, 0), OPB (OP_RUN, 0) }, 8 }, + + /* MHD_get_fdset2() + MHD_run_from_select2() */ + { "fdset2-run-from-select2", { 0x00, 0x80, 0x09, 0x00, 0x00 }, + { OPB (OP_SEND_FRAG, 0), OPB (OP_FDSET, 1), OPB (OP_POLL, 0), + OPB (OP_RUN, 1), OPB (OP_TIMEOUT, 0), OPB (OP_FDSET, 1), + OPB (OP_POLL, 0), OPB (OP_RUN, 1) }, 8 }, + + /* a small FD_SETSIZE limit, which MHD has to refuse descriptors for */ + { "fdset2-small-setsize", { 0x00, 0x80, 0x02, 0x00, 0x04 }, + { OPB (OP_SEND_FRAG, 0), OPB (OP_FDSET, 2), OPB (OP_RUN, 1), + OPB (OP_FDSET, 2), OPB (OP_TIMEOUT, 0), OPB (OP_RUN, 1) }, 6 }, + + /* max_fd == NULL and except_fd_set == NULL, both documented shapes */ + { "fdset2-null-args", { 0x00, 0x80, 0x00, 0x00, 0x00 }, + { OPB (OP_SEND_FRAG, 0), OPB (OP_FDSET, 3), OPB (OP_RUN, 1), + OPB (OP_FDSET, 4), OPB (OP_RUN, 1), OPB (OP_TIMEOUT, 0) }, 6 }, + + /* MHD_run() and MHD_run_wait() */ + { "run-and-run-wait", { 0x00, 0x80, 0x10, 0x00, 0x00 }, + { OPB (OP_SEND_FRAG, 0), OPB (OP_RUN, 2), OPB (OP_RUN, 3), + OPB (OP_TIMEOUT, 0), OPB (OP_RUN, 2) }, 5 }, + + /* descriptor sets MHD never asked for: all-zero and all-ones */ + { "run-from-select-bogus-sets", { 0x00, 0x80, 0x00, 0x00, 0x00 }, + { OPB (OP_SEND_FRAG, 0), OPB (OP_RUN, 0x04), OPB (OP_RUN, 0x08), + OPB (OP_RUN, 0x05), OPB (OP_RUN, 0x09), OPB (OP_TIMEOUT, 0), + OPB (OP_RUN, 0x0C) }, 7 }, + + /* stale sets: collect, close the connection, then run with them */ + { "run-from-select-stale-sets", { 0x00, 0x80, 0x00, 0x00, 0x00 }, + { OPB (OP_SEND_FRAG, 1), OPB (OP_FDSET, 1), OPB (OP_CLOSECONN, 0), + OPB (OP_RUN, 0), OPB (OP_RUN, 1), OPB (OP_TIMEOUT, 0) }, 6 }, + + /* suspend from the pump loop, across a full fdset/poll/run round */ + { "suspend-across-loop", { 0x00, 0x80, 0x00, 0x00, 0x00 }, + { OPB (OP_SEND_FRAG, 1), OPB (OP_RUN, 2), OPB (OP_SUSPEND, 0), + OPB (OP_FDSET, 1), OPB (OP_TIMEOUT, 0), OPB (OP_POLL, 0), + OPB (OP_RUN, 1), OPB (OP_RESUME, 0), OPB (OP_RUN, 2) }, 9 }, + + /* the handler parks the connection; several are parked at once */ + { "handler-parks-two-connections", { 0x00, 0x88, 0x00, 0x00, 0x00 }, + { OPB (OP_SEND_FRAG, 0), OPB (OP_RUN, 2), OPB (OP_NEWCONN, 0), + OPB (OP_SEND_FRAG, 0), OPB (OP_RUN, 2), OPB (OP_TIMEOUT, 0), + OPB (OP_RESUME, 0), OPB (OP_RUN, 2) }, 8 }, + + /* suspend while a resume is still pending (cancels the resume) */ + { "suspend-cancels-pending-resume", { 0x00, 0x80, 0x00, 0x00, 0x00 }, + { OPB (OP_SEND_FRAG, 1), OPB (OP_RUN, 2), OPB (OP_SUSPEND, 0), + OPB (OP_RESUME, 0), OPB (OP_SUSPEND, 0), OPB (OP_TIMEOUT, 0), + OPB (OP_RESUME, 0), OPB (OP_RUN, 2) }, 8 }, + + /* quiesce a daemon that really has a listening socket, then check that + it is gone from the descriptor sets */ + { "quiesce-listening-daemon", { 0x04, 0x80, 0x00, 0x00, 0x00 }, + { OPB (OP_SEND_FRAG, 0), OPB (OP_FDSET, 1), OPB (OP_RUN, 1), + OPB (OP_QUIESCE, 0), OPB (OP_FDSET, 1), OPB (OP_TIMEOUT, 0), + OPB (OP_RUN, 1), OPB (OP_QUIESCE, 0) }, 8 }, + + /* a large response on a small socket buffer: the connection stays + blocked on write over many rounds */ + { "blocked-write-scheduling", { 0x80, 0x02, 0x00, 0x00, 0x00 }, + { OPB (OP_SEND_FRAG, 0), OPB (OP_FDSET, 1), OPB (OP_POLL, 0), + OPB (OP_RUN, 1), OPB (OP_FDSET, 1), OPB (OP_POLL, 0), + OPB (OP_RUN, 1), OPB (OP_DRAIN, 8), OPB (OP_FDSET, 1), + OPB (OP_RUN, 1), OPB (OP_TIMEOUT, 0) }, 11 }, + + /* a real connection-timeout expiry */ + { "connection-timeout-expiry", { 0x00, 0x80, 0x00, 0x08, 0x00 }, + { OPB (OP_SEND_FRAG, 1), OPB (OP_RUN, 2), OPB (OP_TIMEOUT, 0), + OPB (OP_CLOCK, 15), OPB (OP_TIMEOUT, 0), OPB (OP_RUN, 2) }, 6 }, + + /* per-connection timeouts, which move the connection to the "manual" + timeout list that MHD_get_timeout*() has to scan separately */ + { "manual-timeout-list", { 0x00, 0x80, 0x00, 0x03, 0x00 }, + { OPB (OP_SEND_FRAG, 1), OPB (OP_CONNOPT, 8), OPB (OP_TIMEOUT, 0), + OPB (OP_CONNOPT, 1), OPB (OP_TIMEOUT, 0), OPB (OP_RUN, 2), + OPB (OP_TIMEOUT, 0) }, 7 }, + + /* pipelined requests plus a chunked upload, driven one run at a time */ + { "pipelined-and-chunked", { 0x00, 0x81, 0x00, 0x00, 0x00 }, + { OPB (OP_SEND_FRAG, 11), OPB (OP_RUN, 2), OPB (OP_SEND_FRAG, 5), + OPB (OP_RUN, 2), OPB (OP_SEND_FRAG, 6), OPB (OP_RUN, 2), + OPB (OP_SEND_FRAG, 7), OPB (OP_RUN, 2), OPB (OP_TIMEOUT, 0) }, 9 }, + + /* never poll, never ask for descriptors, only run from empty sets */ + { "never-poll", { 0x00, 0x80, 0x00, 0x00, 0x00 }, + { OPB (OP_SEND_FRAG, 0), OPB (OP_RUN, 0x04), OPB (OP_RUN, 0x04), + OPB (OP_RUN, 0x04), OPB (OP_RUN, 0x04), OPB (OP_TIMEOUT, 0) }, 6 }, + + /* abrupt close in the middle of a request, with the descriptor sets + collected before it */ + { "abrupt-close-mid-request", { 0x00, 0x80, 0x00, 0x00, 0x00 }, + { OPB (OP_SEND_FRAG, 3), OPB (OP_FDSET, 1), OPB (OP_CLOSECONN, 0), + OPB (OP_RUN, 1), OPB (OP_TIMEOUT, 0), OPB (OP_RUN, 2) }, 6 }, + + /* connection limit of two, so that MHD_add_connection() starts to fail */ + { "connection-limit", { 0x40, 0x80, 0x00, 0x03, 0x00 }, + { OPB (OP_NEWCONN, 0), OPB (OP_NEWCONN, 0), OPB (OP_SEND_FRAG, 0), + OPB (OP_RUN, 2), OPB (OP_TIMEOUT, 0) }, 5 }, + + /* no connection timeout at all: MHD_get_timeout*() must report that an + indefinite wait is fine */ + { "no-timeout-configured", { 0x02, 0x80, 0x00, 0x04, 0x00 }, + { OPB (OP_SEND_FRAG, 1), OPB (OP_TIMEOUT, 0), OPB (OP_RUN, 2), + OPB (OP_TIMEOUT, 0), OPB (OP_SEND_FRAG, 2), OPB (OP_RUN, 2) }, 6 } +}; + +static uint8_t seed_render_buf[64]; + + +static size_t +fuzz_seed_count (void) +{ + return sizeof (seeds) / sizeof (seeds[0]); +} + + +static const uint8_t * +fuzz_seed_get (size_t idx, + size_t *len) +{ + const struct seed_def *sd = &seeds[idx]; + size_t n = sd->nops; + + if (n > sizeof (sd->ops)) + n = sizeof (sd->ops); + if (n + sizeof (sd->cfg) > sizeof (seed_render_buf)) + n = sizeof (seed_render_buf) - sizeof (sd->cfg); + memcpy (seed_render_buf, sd->cfg, sizeof (sd->cfg)); + memcpy (seed_render_buf + sizeof (sd->cfg), sd->ops, n); + *len = sizeof (sd->cfg) + n; + return seed_render_buf; +} diff --git a/src/fuzz/fuzz_memorypool.c b/src/fuzz/fuzz_memorypool.c @@ -0,0 +1,1613 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. + If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * @file fuzz/fuzz_memorypool.c + * @brief Direct in-process fuzzer for src/microhttpd/memorypool.c + * @author Christian Grothoff + * + * memorypool.c is the per-connection allocator: a bump allocator that + * hands out "normal" blocks from the start of the pool and small, + * never-reallocated blocks from its end, plus an in-place resize and a + * reset that moves one surviving block back to the beginning. Every + * request buffer, every header name and value of every connection lives + * in it, so a single mis-computed offset here is a cross-request + * information leak or a heap overflow -- and one that ASAN alone cannot + * see, because the whole pool is one malloc()ed object. (It does see it + * in a build where MHD_ASAN_POISON_ACTIVE is defined, i.e. where + * `sanitizer/asan_interface.h` was found and memorypool.c poisons the + * unallocated parts of the pool itself. The harness works either way + * and knows the red zone size that mode adds between two blocks.) + * + * The harness therefore keeps its own model of the pool and checks it + * against the allocator after every single operation: + * + * - every returned pointer lies inside the pool, is aligned, and does + * not overlap any other live allocation; + * - every live allocation carries a position-dependent byte pattern + * that is re-verified later; the allocator moves data around in + * MHD_pool_reallocate() and MHD_pool_reset(), and the pattern is what + * proves the move was correct; + * - the sum of the live allocations plus MHD_pool_get_free() never + * exceeds the size of the pool, MHD_pool_get_free() never grows + * across an allocation and never shrinks across a deallocation; + * - the implications the rest of MHD relies on hold: + * * MHD_pool_allocate (pool, MHD_pool_get_free (pool), false) must + * succeed (response.c:2203 does exactly this), and more generally + * an allocation must not fail while get_free() reports room for + * its rounded-up size; + * * if MHD_pool_is_resizable_inplace() says yes, a reallocation + * that fits must succeed *and must not move the block* + * (connection.c:729/750/2082/2121 assert precisely that); + * * after freeing the number of bytes MHD_pool_try_alloc() asked + * for in @a required_bytes, the same call must succeed -- this is + * the whole point of that out parameter and the logic of + * MHD_connection_alloc_memory_(). + * + * Documented preconditions are respected, since violating them would + * only produce findings about the harness: blocks allocated "from the + * end" are never passed to MHD_pool_reallocate(), MHD_pool_reset() is + * never asked for more than the pool size nor for more copy_bytes than + * the kept block holds, and a block is only ever deallocated with the + * size it was allocated with. + * + * Two *open* defects of memorypool.c are reachable and would otherwise + * make every run abort; each is gated behind an environment variable, + * documented in full at full_dealloc_enabled and wrap_realloc_enabled + * below and in README section 6.1, with a byte-exact reproducer in the + * built-in seed corpus. Nothing else is gated. + * + * Input format: + * byte 0 pool size selector (index into fz_pool_sizes[]) + * byte 1 flags + * 0x01 verify every live block after every operation + * 0x02 prefer allocating "from the end" + * rest unused + * byte 2.. a stream of 3-byte operation records + * [0] opcode (see enum fz_op) + * [1] block selector / auxiliary nibbles + * [2] size selector (see fz_size()) + */ + +#define FUZZ_HARNESS_NAME "fuzz_memorypool" +#include "fuzz_common.h" + +/* internal.h pulls in MHD_config.h and <microhttpd.h> in the right + order; including <microhttpd.h> first would redefine _MHD_EXTERN. */ +#include "internal.h" +#include "memorypool.h" + + +/** + * Alignment of every block the pool hands out; memorypool.c aligns to + * two words "as GNU libc does". + */ +#define FZ_ALIGN_SIZE (2 * sizeof (void *)) + +/** + * Size of the red zone memorypool.c keeps between two blocks. It only + * exists in a build with user memory poisoning, i.e. when the pool + * itself is instrumented; without it the pool is packed tight. + */ +#ifdef MHD_ASAN_POISON_ACTIVE +#define FZ_RED_ZONE_SIZE FZ_ALIGN_SIZE +#else /* ! MHD_ASAN_POISON_ACTIVE */ +#define FZ_RED_ZONE_SIZE ((size_t) 0) +#endif /* ! MHD_ASAN_POISON_ACTIVE */ + +/** + * Maximum number of live allocations the model tracks. An operation + * that would exceed this is skipped, so the model never loses a block. + */ +#define FZ_MAX_BLOCKS 24 + +/** + * Maximum number of operations executed for one input. + */ +#define FZ_MAX_OPS 64 + +/** + * Number of bytes of each allocation that carry the check pattern. + * Bounding this keeps the harness fast; the interesting bugs of a bump + * allocator are all at the start of a block or at its (ASAN-guarded) + * end. + */ +#define FZ_PAT_MAX 96 + +/** + * Pool sizes to choose from. MHD uses 128..32768 in practice (the + * default is 32768 and MHD_OPTION_CONNECTION_MEMORY_LIMIT can set + * anything); everything at or below 32k is malloc()ed by + * MHD_pool_create(), so the harness knows the exact usable size and + * ASAN's redzone sits right behind the pool. The last two entries are + * larger than that on purpose: they take the mmap() path of + * MHD_pool_create() and the munmap() path of MHD_pool_destroy(). Both + * are powers of two and therefore a multiple of any page size, so the + * resulting pool is exactly that large either way and the check below + * stays exact. + */ +static const size_t fz_pool_sizes[] = { + 1, 15, 16, 17, 32, 48, 64, 96, 128, 129, 192, 256, 320, 384, 512, + 768, 1024, 1400, 1500, 2048, 4096, 8192, 16384, 32768, 65536, 131072 +}; + +#define FZ_NUM_POOL_SIZES \ + (sizeof (fz_pool_sizes) / sizeof (fz_pool_sizes[0])) + + +enum fz_op +{ + FZ_OP_ALLOC = 0, /**< MHD_pool_allocate() */ + FZ_OP_ALLOC_ALL, /**< allocate exactly MHD_pool_get_free() bytes */ + FZ_OP_TRY_ALLOC, /**< MHD_pool_try_alloc() */ + FZ_OP_SQUEEZE, /**< try_alloc + shrink + retry */ + FZ_OP_REALLOC, /**< MHD_pool_reallocate() of a live block */ + FZ_OP_REALLOC_NEW, /**< MHD_pool_reallocate() with old == NULL */ + FZ_OP_DEALLOC, /**< MHD_pool_deallocate() */ + FZ_OP_RESET, /**< MHD_pool_reset() */ + FZ_OP_GET_FREE, /**< MHD_pool_get_free() */ + FZ_OP_RESIZABLE, /**< MHD_pool_is_resizable_inplace() */ + FZ_OP_VERIFY, /**< re-check every live block */ + FZ_OP_RECREATE, /**< MHD_pool_destroy() + MHD_pool_create() */ + FZ_OP_COUNT +}; + + +/** + * One live allocation. + */ +struct fz_block +{ + /** + * Address returned by the pool. + */ + uint8_t *ptr; + + /** + * Current size of the block, i.e. the number of bytes the harness is + * allowed to touch and the size it will pass to the pool again. + */ + size_t size; + + /** + * Seed of the check pattern of this block. + */ + uint8_t tag; + + /** + * True if the block was allocated "from the end" (MHD_pool_allocate() + * with @a from_end, or MHD_pool_try_alloc()). Such blocks must never + * be passed to MHD_pool_reallocate(). + */ + bool from_end; +}; + + +/** + * The model of one pool. + */ +struct fz_pool +{ + struct MemoryPool *pool; + + /** + * Address of the first byte of the pool, as returned by + * MHD_pool_reset() for an empty pool -- that is a real MHD call site + * (connection.c:3021 uses the result as the new read buffer). + */ + uint8_t *base; + + /** + * Total size of the pool in bytes. + */ + size_t cap; + + struct fz_block blk[FZ_MAX_BLOCKS]; + + unsigned int nblk; + + uint8_t next_tag; + + /** + * Verify every live block after every operation. + */ + bool paranoid; +}; + + +/** + * F1 -- MHD_pool_deallocate() of a "from the end" block of a pool that + * is *exactly* full. This is a live defect in memorypool.c and is + * therefore off by default; set MHD_FUZZ_POOL_FULL_DEALLOC=1 to reach + * it (seed fuzz_memorypool-15.bin is the byte-exact reproducer). + * + * MHD_pool_deallocate() decides whether a block came from the front or + * from the end with + * + * if (block_offset <= pool->pos) memorypool.c:666 + * + * which is ambiguous when pool->pos == pool->end, i.e. when the pool has + * no free space left: a "from the end" block then starts exactly at + * pool->pos and is treated as a front block. With --enable-asserts + * + * mhd_assert ((block_offset != pool->pos) || (block_size == 0)); :655 + * mhd_assert (alg_end <= pool->pos); :671 + * + * both fail, i.e. the process aborts; without them the block is simply + * not returned to the pool (pool->end is left alone), so the memory is + * lost until the pool is reset or destroyed. Reproducer: + * + * p = MHD_pool_create (128); + * e = MHD_pool_allocate (p, 16, true); + * (void) MHD_pool_allocate (p, MHD_pool_get_free (p), false); + * MHD_pool_deallocate (p, e, 16); <- abort / 16 bytes lost + * + * Nothing in memorypool.h forbids this; MHD_pool_deallocate() documents + * only that "the NULL is tolerated" and has an explicit branch for + * blocks allocated from the end. MHD's own call sites never hit it, + * because the three MHD_pool_deallocate() calls in connection.c and + * response.c all pass the read or the write buffer, which are front + * allocations -- so this is a latent defect of a supported branch of the + * API rather than something reachable from the network today. A + * dispatch on `block_offset < pool->end` would resolve the ambiguity. + */ +static int full_dealloc_enabled; + +/** + * F2 -- MHD_pool_reallocate() reports success for a @a new_size that + * wrapped. Also a live defect, off by default; set + * MHD_FUZZ_POOL_WRAP_REALLOC=1 to reach it (seed + * fuzz_memorypool-16.bin is the byte-exact reproducer). + * + * Every other entry point of memorypool.c detects a size that is too + * close to SIZE_MAX explicitly: + * + * asize = ROUND_TO_ALIGN_PLUS_RED_ZONE (size); + * if ( (0 == asize) && (0 != size) ) + * return NULL; // "size too close to SIZE_MAX" + * + * -- MHD_pool_allocate(), MHD_pool_try_alloc() and the "need to + * allocate a new block" tail of MHD_pool_reallocate() all do it. The + * in-place branch of MHD_pool_reallocate() instead relies on + * + * const size_t new_apos = + * ROUND_TO_ALIGN_PLUS_RED_ZONE (old_offset + new_size); + * if ( (new_apos > pool->end) || + * (new_apos < pool->pos) ) // "Value wrap" + * return NULL; + * + * which does not catch the wrap when the wrapped value happens to land + * on pool->pos. For a zero-length block sitting at the front boundary + * (old_size == 0, old_offset == pool->pos, which is exactly what + * MHD_pool_reset (pool, NULL, 0, 0) returns -- see connection.c:3021 -- + * or MHD_pool_allocate (pool, 0, false)) and any new_size in + * [SIZE_MAX - 14, SIZE_MAX], old_offset + new_size wraps to + * old_offset - 1 - k, which rounds back up to exactly old_offset, i.e. + * to pool->pos: neither test fires, pool->pos is left alone and the + * function returns @a old, i.e. it reports success. + * + * p = MHD_pool_create (1024); + * b = MHD_pool_reset (p, NULL, 0, 0); + * r = MHD_pool_reallocate (p, b, 0, SIZE_MAX - 4); + * // r == b, not NULL: the caller now believes it owns + * // SIZE_MAX - 4 bytes at the start of a 1024 byte pool, + * // while MHD_pool_get_free() still reports the full 1024. + * + * The documented contract is "@return new address of the block, or NULL + * if the pool cannot support @a new_size bytes", so this is a plain + * contract violation, and one that hands the caller a buffer size the + * allocation does not have. No MHD call site can reach it today: every + * new_size MHD passes is derived from the pool size (connection.c:1636, + * 2031, 2082, 2121, 2170, 6935), so it is a latent defect rather than + * something reachable from the network. Checking the addition itself, + * e.g. `if (new_size > pool->size - old_offset) return NULL;`, would + * close it. + */ +static int wrap_realloc_enabled; + +static int known_bugs_read; + +/** Statistics, printed at exit with --verbose. */ +static unsigned long stat_ops; +static unsigned long stat_alloc_ok; +static unsigned long stat_alloc_fail; +static unsigned long stat_realloc_moved; +static unsigned long stat_resets; + + +static void +print_stats (void) +{ + if (! fuzz_verbose) + return; + fprintf (stderr, + "%s: ops=%lu allocations=%lu (failed %lu) " + "moved reallocations=%lu resets=%lu\n", + FUZZ_HARNESS_NAME, + stat_ops, stat_alloc_ok, stat_alloc_fail, + stat_realloc_moved, stat_resets); +} + + +/** + * Report a finding, prefixed with the name of the operation that was + * running. + */ +static void +fz_report (const char *op, + const char *what) +{ + char msg[256]; + + (void) snprintf (msg, sizeof (msg), "%s: %s", op, what); + fuzz_report_finding (msg); +} + + +/** + * The ROUND_TO_ALIGN() of memorypool.c, wrap-around included: a size + * that is too close to SIZE_MAX rounds to 0, which is how the allocator + * detects it. + */ +static size_t +fz_round (size_t n) +{ + return (n + (FZ_ALIGN_SIZE - 1)) / FZ_ALIGN_SIZE * FZ_ALIGN_SIZE; +} + + +/** + * Byte @a idx of the check pattern of a block with seed @a tag. + */ +static uint8_t +fz_pat (uint8_t tag, + size_t idx) +{ + return (uint8_t) ((((unsigned int) tag) * 131u) + + ((unsigned int) (idx * 17u)) + + 0x5Au); +} + + +/** + * Write the check pattern into @a ptr[from..to). + */ +static void +fz_fill (uint8_t *ptr, + uint8_t tag, + size_t from, + size_t to) +{ + size_t i; + + if (to > FZ_PAT_MAX) + to = FZ_PAT_MAX; + for (i = from; i < to; i++) + ptr[i] = fz_pat (tag, i); +} + + +/** + * Verify that the first bytes of @a ptr still carry the pattern of + * @a tag. The expected bytes are built in an exactly sized malloc()ed + * buffer, so that a comparison that runs off either end is caught by + * ASAN rather than silently passing. + */ +static void +fz_verify (const uint8_t *ptr, + size_t size, + uint8_t tag, + const char *op, + const char *what) +{ + size_t n = (size < FZ_PAT_MAX) ? size : (size_t) FZ_PAT_MAX; + uint8_t *expect; + size_t i; + + if (0 == n) + return; + expect = (uint8_t *) malloc (n); + if (NULL == expect) + abort (); + for (i = 0; i < n; i++) + expect[i] = fz_pat (tag, i); + if (0 != memcmp (expect, ptr, n)) + fz_report (op, what); + free (expect); +} + + +/** + * Verify every live block. + */ +static void +fz_verify_all (struct fz_pool *st, + const char *op) +{ + unsigned int i; + + for (i = 0; i < st->nblk; i++) + fz_verify (st->blk[i].ptr, + st->blk[i].size, + st->blk[i].tag, + op, + "the contents of a live allocation changed"); +} + + +/** + * Check a pointer the pool just returned: inside the pool, aligned, and + * not overlapping any block that is still live. The caller has already + * removed the block being replaced (if any) from the model. + */ +static void +fz_check_ptr (struct fz_pool *st, + void *p, + size_t size, + const char *op) +{ + const uint8_t *up = (const uint8_t *) p; + size_t off; + unsigned int i; + + if ( (up < st->base) || + (up > st->base + st->cap) ) + { + fz_report (op, "returned a pointer outside of the pool"); + return; + } + off = (size_t) (up - st->base); + if ( (size > st->cap) || + (off + size > st->cap) ) + fz_report (op, "returned a block that extends past the end of the pool"); + if (0 != (off % FZ_ALIGN_SIZE)) + fz_report (op, "returned a misaligned pointer"); + if (0 == size) + return; + for (i = 0; i < st->nblk; i++) + { + const uint8_t *q = st->blk[i].ptr; + + if (0 == st->blk[i].size) + continue; + if ( (up < q + st->blk[i].size) && + (q < up + size) ) + { + fz_report (op, "returned a block overlapping another live allocation"); + return; + } + } +} + + +/** + * Invariants that must hold after every single operation. + */ +static void +fz_check_invariants (struct fz_pool *st, + const char *op) +{ + size_t total = 0; + size_t freem; + unsigned int i; + + freem = MHD_pool_get_free (st->pool); + if (freem > st->cap) + fz_report (op, "MHD_pool_get_free() exceeds the size of the pool"); + for (i = 0; i < st->nblk; i++) + total += st->blk[i].size; + if (total > st->cap) + fz_report (op, "the live allocations alone exceed the size of the pool"); + else if (total + freem > st->cap) + fz_report (op, + "the live allocations plus the free space exceed the size " + "of the pool"); + if (st->paranoid) + fz_verify_all (st, op); +} + + +static struct fz_block * +fz_add (struct fz_pool *st, + void *ptr, + size_t size, + bool from_end) +{ + struct fz_block *b = &st->blk[st->nblk++]; + + b->ptr = (uint8_t *) ptr; + b->size = size; + b->tag = st->next_tag++; + b->from_end = from_end; + fz_fill (b->ptr, b->tag, 0, size); + return b; +} + + +static void +fz_del (struct fz_pool *st, + struct fz_block *b) +{ + *b = st->blk[--st->nblk]; +} + + +/** + * Pick a live block. + * + * @param st the model + * @param sel selector byte + * @param front_only only consider blocks that may be reallocated + * @return NULL if there is no such block + */ +static struct fz_block * +fz_pick (struct fz_pool *st, + uint8_t sel, + bool front_only) +{ + unsigned int i; + unsigned int n = 0; + unsigned int idx; + + for (i = 0; i < st->nblk; i++) + { + if ( (! front_only) || + (! st->blk[i].from_end) ) + n++; + } + if (0 == n) + return NULL; + idx = ((unsigned int) sel) % n; + for (i = 0; i < st->nblk; i++) + { + if ( (front_only) && + (st->blk[i].from_end) ) + continue; + if (0 == idx) + return &st->blk[i]; + idx--; + } + return NULL; /* unreachable */ +} + + +/** + * Turn a selector byte into a size. The four classes matter: tiny + * sizes are what headers use, fractions of the pool are what the + * request buffers use, sizes around the currently free amount are where + * the off-by-one-block bugs live, and sizes close to SIZE_MAX exercise + * the "value wrap" guards of the allocator. + */ +static size_t +fz_size (uint8_t sel, + size_t cap, + size_t freem) +{ + size_t d; + + switch (sel & 0x03) + { + case 0: + return (size_t) (sel >> 2); /* 0 .. 63 */ + case 1: + return (((size_t) ((sel >> 2) + 1)) * cap) / 64u; + case 2: + d = (size_t) ((sel >> 2) & 0x07u); + if (0 != (sel & 0x20)) + return freem + d; + return (freem > d) ? (freem - d) : 0; + default: + return SIZE_MAX - (size_t) (sel >> 2); + } +} + + +static void +fz_pool_close (struct fz_pool *st) +{ + MHD_pool_destroy (st->pool); + st->pool = NULL; + st->nblk = 0; +} + + +/** + * Create a pool and learn its base address and its exact size. + * + * @return false if the pool could not be created + */ +static bool +fz_pool_open (struct fz_pool *st, + uint8_t sel) +{ + size_t max = fz_pool_sizes[((size_t) sel) % FZ_NUM_POOL_SIZES]; + void *base; + + st->nblk = 0; + st->pool = MHD_pool_create (max); + if (NULL == st->pool) + return false; + st->cap = MHD_pool_get_free (st->pool) + FZ_RED_ZONE_SIZE; + if (st->cap != fz_round (max)) + fz_report ("MHD_pool_create", + "a fresh pool does not offer the requested size rounded " + "up to the alignment"); + /* MHD_pool_reset() with nothing to keep returns the start of the + pool; connection.c:3021 relies on exactly this call. It is the + only way to learn the base address through the public interface. */ + base = MHD_pool_reset (st->pool, NULL, 0, 0); + if (NULL == base) + { + fz_report ("MHD_pool_reset", "returned NULL for an empty pool"); + fz_pool_close (st); + return false; + } + st->base = (uint8_t *) base; + return true; +} + + +/* ------------------------------------------------------------------ */ +/* The individual operations */ +/* ------------------------------------------------------------------ */ + +/** + * MHD_pool_allocate(), from the front or from the end. + */ +static void +fz_op_alloc (struct fz_pool *st, + const uint8_t *op, + bool prefer_end) +{ + static const char *const nm[2] = { "MHD_pool_allocate", + "MHD_pool_allocate (from_end)" }; + const bool from_end = (0 != (op[1] & 0x01)) || prefer_end; + const char *name = nm[from_end ? 1 : 0]; + size_t freem; + size_t after; + size_t size; + void *p; + + if (st->nblk >= FZ_MAX_BLOCKS) + return; + freem = MHD_pool_get_free (st->pool); + size = fz_size (op[2], st->cap, freem); + p = MHD_pool_allocate (st->pool, size, from_end); + after = MHD_pool_get_free (st->pool); + if (NULL == p) + { + stat_alloc_fail++; + if (after != freem) + fz_report (name, "a failed allocation changed the amount of free space"); + if ( (0 != fz_round (size)) && + (fz_round (size) <= freem) ) + fz_report (name, + "allocation failed although MHD_pool_get_free() reported " + "room for it"); + return; + } + stat_alloc_ok++; + if (after > freem) + fz_report (name, "MHD_pool_get_free() increased across an allocation"); + else if (freem - after < size) + fz_report (name, + "MHD_pool_get_free() dropped by less than the allocated size"); + fz_check_ptr (st, p, size, name); + (void) fz_add (st, p, size, from_end); +} + + +/** + * Allocate exactly MHD_pool_get_free() bytes. response.c:2203 does + * this and dereferences the result without a NULL check, so it must + * always succeed, and it must leave the pool without any free space. + */ +static void +fz_op_alloc_all (struct fz_pool *st, + const uint8_t *op) +{ + static const char name[] = "MHD_pool_allocate (all free)"; + const bool from_end = (0 != (op[1] & 0x01)); + size_t freem; + void *p; + + if (st->nblk >= FZ_MAX_BLOCKS) + return; + freem = MHD_pool_get_free (st->pool); + if (0 == freem) + return; + p = MHD_pool_allocate (st->pool, freem, from_end); + if (NULL == p) + { + stat_alloc_fail++; + fz_report (name, + "allocating exactly MHD_pool_get_free() bytes failed"); + return; + } + stat_alloc_ok++; + fz_check_ptr (st, p, freem, name); + if (0 != MHD_pool_get_free (st->pool)) + fz_report (name, + "the pool still reports free space after all of it was " + "allocated"); + (void) fz_add (st, p, freem, from_end); +} + + +/** + * MHD_pool_try_alloc(). Checks the contract of @a required_bytes. + */ +static void +fz_op_try_alloc (struct fz_pool *st, + const uint8_t *op) +{ + static const char name[] = "MHD_pool_try_alloc"; + size_t freem; + size_t after; + size_t size; + size_t need; + void *p; + + if (st->nblk >= FZ_MAX_BLOCKS) + return; + freem = MHD_pool_get_free (st->pool); + size = fz_size (op[2], st->cap, freem); + need = 0x5A5A5A5Au; /* poor man's "was it written at all" */ + p = MHD_pool_try_alloc (st->pool, size, &need); + after = MHD_pool_get_free (st->pool); + if (NULL == p) + { + stat_alloc_fail++; + if (after != freem) + fz_report (name, "a failed allocation changed the amount of free space"); + if (0 == need) + fz_report (name, + "the allocation failed but no additional memory is " + "reported to be required"); + else if ( (SIZE_MAX != need) && + (need > st->cap) ) + fz_report (name, + "more memory is reported to be required than the pool " + "has in total"); + if ( (0 != fz_round (size)) && + (fz_round (size) <= freem) ) + fz_report (name, + "allocation failed although MHD_pool_get_free() reported " + "room for it"); + return; + } + stat_alloc_ok++; + if (0 != need) + fz_report (name, + "the allocation succeeded but required_bytes was not " + "cleared"); + if (after > freem) + fz_report (name, "MHD_pool_get_free() increased across an allocation"); + else if (freem - after < size) + fz_report (name, + "MHD_pool_get_free() dropped by less than the allocated size"); + fz_check_ptr (st, p, size, name); + /* try_alloc always allocates "from the end" */ + (void) fz_add (st, p, size, true); +} + + +/** + * The logic of MHD_connection_alloc_memory_() (connection.c:706): when + * MHD_pool_try_alloc() fails it names the number of bytes that have to + * be freed in the relocatable area; freeing exactly that many bytes by + * shrinking a block that is resizable in-place must make the very same + * allocation succeed. If it does not, MHD returns "out of memory" for + * a request it has the memory for. + */ +static void +fz_op_squeeze (struct fz_pool *st, + const uint8_t *op) +{ + static const char name[] = "MHD_pool_try_alloc (squeeze)"; + size_t size; + size_t need = 0; + size_t need2 = 0; + void *p; + unsigned int i; + struct fz_block *victim = NULL; + + if (st->nblk >= FZ_MAX_BLOCKS) + return; + size = fz_size (op[2], st->cap, MHD_pool_get_free (st->pool)); + p = MHD_pool_try_alloc (st->pool, size, &need); + if (NULL != p) + { + fz_check_ptr (st, p, size, name); + (void) fz_add (st, p, size, true); + return; + } + if ( (0 == need) || + (SIZE_MAX == need) ) + return; + for (i = 0; i < st->nblk; i++) + { + if (st->blk[i].from_end) + continue; + if (st->blk[i].size < need) + continue; + if (! MHD_pool_is_resizable_inplace (st->pool, + st->blk[i].ptr, + st->blk[i].size)) + continue; + victim = &st->blk[i]; + break; + } + if (NULL == victim) + return; + { + const size_t new_size = victim->size - need; + void *shrunk = MHD_pool_reallocate (st->pool, + victim->ptr, + victim->size, + new_size); + + if (NULL == shrunk) + { + fz_report (name, + "shrinking a block that is resizable in-place failed"); + return; + } + if (shrunk != victim->ptr) + fz_report (name, + "shrinking a block that is resizable in-place moved it"); + victim->ptr = (uint8_t *) shrunk; + victim->size = new_size; + fz_verify (victim->ptr, victim->size, victim->tag, name, + "shrinking a block damaged its contents"); + } + p = MHD_pool_try_alloc (st->pool, size, &need2); + if (NULL == p) + { + fz_report (name, + "the allocation still fails after freeing the number of " + "bytes that required_bytes asked for"); + return; + } + fz_check_ptr (st, p, size, name); + (void) fz_add (st, p, size, true); +} + + +/** + * MHD_pool_reallocate() of a live block that was allocated from the + * front. Blocks allocated "from the end" are excluded: memorypool.c + * documents ("Blocks 'from the end' must not be reallocated") and + * asserts that. + */ +static void +fz_op_realloc (struct fz_pool *st, + const uint8_t *op) +{ + static const char name[] = "MHD_pool_reallocate"; + struct fz_block *b = fz_pick (st, op[1], true); + struct fz_block saved; + size_t freem; + size_t after; + size_t new_size; + bool inplace; + void *p; + + if (NULL == b) + return; + freem = MHD_pool_get_free (st->pool); + new_size = fz_size (op[2], st->cap, freem); + if ( (0 == b->size) && + (new_size > SIZE_MAX - FZ_ALIGN_SIZE) && + (! wrap_realloc_enabled) ) + return; /* open finding F2, see above */ + inplace = MHD_pool_is_resizable_inplace (st->pool, b->ptr, b->size); + saved = *b; + p = MHD_pool_reallocate (st->pool, b->ptr, b->size, new_size); + after = MHD_pool_get_free (st->pool); + if (NULL == p) + { + /* "old continues to be valid for old_size" */ + if (after != freem) + fz_report (name, + "a failed reallocation changed the amount of free space"); + fz_verify (saved.ptr, saved.size, saved.tag, name, + "a failed reallocation damaged the old block"); + if ( (inplace) && + (new_size <= saved.size + freem) ) + fz_report (name, + "reallocation failed although the block is resizable " + "in-place and the pool has the required free space"); + return; + } + if ( (inplace) && + (p != saved.ptr) ) + fz_report (name, + "a block that MHD_pool_is_resizable_inplace() accepted was " + "moved"); + if ( (new_size >= saved.size) && + (after > freem) ) + fz_report (name, + "MHD_pool_get_free() increased across a growing " + "reallocation"); + /* the old location is gone as far as the model is concerned */ + fz_del (st, b); + if (p != saved.ptr) + stat_realloc_moved++; + fz_check_ptr (st, p, new_size, name); + fz_verify ((const uint8_t *) p, + (new_size < saved.size) ? new_size : saved.size, + saved.tag, + name, + "reallocation did not preserve the contents of the block"); + b = &st->blk[st->nblk++]; + b->ptr = (uint8_t *) p; + b->size = new_size; + b->tag = saved.tag; + b->from_end = false; + if (new_size > saved.size) + fz_fill (b->ptr, b->tag, saved.size, new_size); +} + + +/** + * MHD_pool_reallocate() with @a old == NULL, which is the documented + * way to obtain a fresh relocatable block (connection.c:2121 uses it + * for the very first write buffer). + */ +static void +fz_op_realloc_new (struct fz_pool *st, + const uint8_t *op) +{ + static const char name[] = "MHD_pool_reallocate (fresh)"; + size_t freem; + size_t size; + void *p; + + if (st->nblk >= FZ_MAX_BLOCKS) + return; + freem = MHD_pool_get_free (st->pool); + size = fz_size (op[2], st->cap, freem); + p = MHD_pool_reallocate (st->pool, NULL, 0, size); + if (NULL == p) + { + stat_alloc_fail++; + if ( (0 != fz_round (size)) && + (fz_round (size) <= freem) ) + fz_report (name, + "allocation failed although MHD_pool_get_free() reported " + "room for it"); + return; + } + stat_alloc_ok++; + if (MHD_pool_get_free (st->pool) > freem) + fz_report (name, "MHD_pool_get_free() increased across an allocation"); + fz_check_ptr (st, p, size, name); + (void) fz_add (st, p, size, false); +} + + +/** + * MHD_pool_deallocate() of a live block, or of NULL ("the NULL is + * tolerated"). A block is always deallocated with the size it + * currently has: memorypool.c asserts that the block lies inside the + * allocated area, and splitting a block is explicitly disallowed. + */ +static void +fz_op_dealloc (struct fz_pool *st, + const uint8_t *op) +{ + static const char name[] = "MHD_pool_deallocate"; + struct fz_block *b = fz_pick (st, op[1], false); + size_t freem; + size_t after; + + freem = MHD_pool_get_free (st->pool); + if ( (NULL != b) && + (b->from_end) && + (0 != b->size) && + (0 == freem) && + (! full_dealloc_enabled) ) + { + /* See the comment on full_dealloc_enabled: this is an open finding, + not a precondition of MHD_pool_deallocate(). The lowest-addressed + live "from the end" block of non-zero size starts exactly at + pool->end, which for a full pool is also pool->pos -- the + ambiguous case. Any block above it is unaffected, so only that + one block is spared. Zero-sized blocks do not count: they consume + nothing, so they can sit below pool->end once a later + deallocation has moved pool->end back up past them. */ + unsigned int i; + + for (i = 0; i < st->nblk; i++) + { + if ( (st->blk[i].from_end) && + (0 != st->blk[i].size) && + (st->blk[i].ptr < b->ptr) ) + break; + } + if (i == st->nblk) + return; + } + if (NULL == b) + { + MHD_pool_deallocate (st->pool, NULL, 0); + } + else + { + fz_verify (b->ptr, b->size, b->tag, name, + "the contents of the block changed before it was freed"); + MHD_pool_deallocate (st->pool, b->ptr, b->size); + fz_del (st, b); + } + after = MHD_pool_get_free (st->pool); + if (after < freem) + fz_report (name, "deallocation reduced the amount of free space"); +} + + +/** + * MHD_pool_reset(). Preconditions: copy_bytes <= new_size, + * new_size <= size of the pool, and the kept block must really hold + * copy_bytes bytes. + */ +static void +fz_op_reset (struct fz_pool *st, + const uint8_t *op) +{ + static const char name[] = "MHD_pool_reset"; + struct fz_block *b = fz_pick (st, (uint8_t) (op[1] & 0x0F), false); + size_t new_size; + size_t copy; + uint8_t tag = 0; + void *p; + + new_size = fz_size (op[2], st->cap, MHD_pool_get_free (st->pool)); + if (new_size > st->cap) + new_size = st->cap; + copy = 0; + if (NULL != b) + { + copy = (b->size * ((size_t) (op[1] >> 4))) / 15u; + if (copy > b->size) + copy = b->size; + if (copy > new_size) + copy = new_size; + tag = b->tag; + fz_verify (b->ptr, b->size, b->tag, name, + "the contents of the kept block changed before the reset"); + } + p = MHD_pool_reset (st->pool, + (NULL != b) ? b->ptr : NULL, + copy, + new_size); + stat_resets++; + /* every allocation is gone now */ + st->nblk = 0; + if (NULL == p) + { + fz_report (name, "returned NULL"); + return; + } + fz_check_ptr (st, p, new_size, name); + fz_verify ((const uint8_t *) p, copy, tag, name, + "the kept bytes did not survive the reset"); + b = fz_add (st, p, new_size, false); + /* fz_add() has just re-tagged and re-filled the whole block */ +} + + +/** + * MHD_pool_is_resizable_inplace() on a live block, or on NULL. + */ +static void +fz_op_resizable (struct fz_pool *st, + const uint8_t *op) +{ + static const char name[] = "MHD_pool_is_resizable_inplace"; + struct fz_block *b = fz_pick (st, op[1], false); + bool r; + + if ( (NULL == b) || + (0 != (op[1] & 0x80)) ) + { + if (MHD_pool_is_resizable_inplace (st->pool, NULL, 0)) + fz_report (name, "an unallocated block is reported as resizable"); + return; + } + r = MHD_pool_is_resizable_inplace (st->pool, b->ptr, b->size); + if ( (r) && + (b->from_end) && + (0 != b->size) ) + fz_report (name, + "a block allocated from the end is reported as resizable " + "in-place"); + if (! r) + return; + /* A block that is resizable in-place must survive a resize to its own + size without moving; connection.c asserts this after every such + reallocation. */ + { + void *p = MHD_pool_reallocate (st->pool, b->ptr, b->size, b->size); + + if (NULL == p) + fz_report (name, + "reallocating a resizable block to its own size failed"); + else if (p != b->ptr) + fz_report (name, + "reallocating a resizable block to its own size moved it"); + } +} + + +static void +fz_do_op (struct fz_pool *st, + const uint8_t *op, + bool prefer_end) +{ + const char *name = "operation"; + + stat_ops++; + switch ((enum fz_op) (op[0] % (uint8_t) FZ_OP_COUNT)) + { + case FZ_OP_ALLOC: + fz_op_alloc (st, op, prefer_end); + break; + case FZ_OP_ALLOC_ALL: + fz_op_alloc_all (st, op); + break; + case FZ_OP_TRY_ALLOC: + fz_op_try_alloc (st, op); + break; + case FZ_OP_SQUEEZE: + fz_op_squeeze (st, op); + break; + case FZ_OP_REALLOC: + fz_op_realloc (st, op); + break; + case FZ_OP_REALLOC_NEW: + fz_op_realloc_new (st, op); + break; + case FZ_OP_DEALLOC: + fz_op_dealloc (st, op); + break; + case FZ_OP_RESET: + fz_op_reset (st, op); + break; + case FZ_OP_GET_FREE: + name = "MHD_pool_get_free"; + break; + case FZ_OP_RESIZABLE: + fz_op_resizable (st, op); + break; + case FZ_OP_VERIFY: + name = "verify"; + fz_verify_all (st, name); + break; + case FZ_OP_RECREATE: + fz_pool_close (st); + if (! fz_pool_open (st, op[1])) + return; + break; + case FZ_OP_COUNT: + default: + break; + } + fz_check_invariants (st, name); +} + + +int +LLVMFuzzerTestOneInput (const uint8_t *data, + size_t size) +{ + static bool inited = false; + struct fz_pool st; + size_t nops; + size_t i; + + if (size < 5) + return 0; + if (! inited) + { + inited = true; + /* Sets the page size used by MHD_pool_create(); MHD does this once + from MHD_start_daemon(). */ + MHD_init_mem_pools_ (); + (void) atexit (&print_stats); + } + if (! known_bugs_read) + { + const char *e; + + known_bugs_read = 1; + e = getenv ("MHD_FUZZ_POOL_FULL_DEALLOC"); + if (NULL != e) + full_dealloc_enabled = (0 != atoi (e)); + e = getenv ("MHD_FUZZ_POOL_WRAP_REALLOC"); + if (NULL != e) + wrap_realloc_enabled = (0 != atoi (e)); + } + memset (&st, 0, sizeof (st)); + st.paranoid = (0 != (data[1] & 0x01)); + if (! fz_pool_open (&st, data[0])) + return 0; + nops = (size - 2) / 3; + if (nops > FZ_MAX_OPS) + nops = FZ_MAX_OPS; + for (i = 0; i < nops; i++) + { + if (NULL == st.pool) + break; + fz_do_op (&st, data + 2 + 3 * i, (0 != (data[1] & 0x02))); + } + fz_verify_all (&st, "final"); + fz_pool_close (&st); /* MHD_pool_destroy() tolerates a NULL pool */ + return 0; +} + + +/* ------------------------------------------------------------------ */ +/* Generator */ +/* ------------------------------------------------------------------ */ + +/** + * Opcodes, weighted: the allocating operations and the reallocation are + * what move the pool's internal offsets around, so they get most of the + * probability mass, while the read-only queries and the destructive + * reset and re-create are rarer. + */ +static const uint8_t gen_ops[] = { + FZ_OP_ALLOC, FZ_OP_ALLOC, FZ_OP_ALLOC, FZ_OP_ALLOC, + FZ_OP_ALLOC_ALL, + FZ_OP_TRY_ALLOC, FZ_OP_TRY_ALLOC, + FZ_OP_SQUEEZE, FZ_OP_SQUEEZE, + FZ_OP_REALLOC, FZ_OP_REALLOC, FZ_OP_REALLOC, FZ_OP_REALLOC, + FZ_OP_REALLOC_NEW, + FZ_OP_DEALLOC, FZ_OP_DEALLOC, FZ_OP_DEALLOC, + FZ_OP_RESET, + FZ_OP_GET_FREE, + FZ_OP_RESIZABLE, FZ_OP_RESIZABLE, + FZ_OP_VERIFY, + FZ_OP_RECREATE +}; + + +/** + * Size selector bytes that are worth trying often: the boundary cases + * (exactly the free space, one alignment unit more or less) and the + * value-wrap guards. + */ +static uint8_t +gen_size_sel (struct fuzz_rng *rng) +{ + const uint32_t k = fuzz_below (rng, 100); + uint32_t d; + uint32_t sign; + + if (k < 35) /* tiny */ + return (uint8_t) ((fuzz_below (rng, 64) << 2) | 0u); + if (k < 60) /* fraction of the pool */ + return (uint8_t) ((fuzz_below (rng, 64) << 2) | 1u); + if (k < 95) + { /* around the free space */ + /* Two statements, not one expression: the order in which the + operands of '|' are evaluated is unspecified, and both calls + advance the PRNG, so a single expression would make the generated + stream compiler-dependent. */ + d = fuzz_below (rng, 8); + sign = fuzz_below (rng, 2); + return (uint8_t) ((d << 2) | (sign << 5) | 2u); + } + return (uint8_t) ((fuzz_below (rng, 64) << 2) | 3u); /* value wrap */ +} + + +static size_t +fuzz_generate (struct fuzz_rng *rng, + uint8_t *buf, + size_t cap) +{ + size_t len = 0; + unsigned int nops; + unsigned int i; + unsigned int psel; + uint8_t flags = 0; + + if (cap < 8) + return 0; + /* Small pools are where the interesting paths are, so bias towards + them, but keep the whole table reachable. */ + if (fuzz_chance (rng, 3)) + psel = fuzz_below (rng, (uint32_t) FZ_NUM_POOL_SIZES); + else + psel = fuzz_below (rng, 16); + buf[len++] = (uint8_t) psel; + /* One PRNG call per statement, see gen_size_sel(). */ + if (fuzz_chance (rng, 4)) + flags |= 0x01; + if (fuzz_chance (rng, 8)) + flags |= 0x02; + buf[len++] = flags; + nops = 1 + fuzz_below (rng, FZ_MAX_OPS); + for (i = 0; i < nops; i++) + { + if (len + 3 > cap) + break; + buf[len++] = gen_ops[fuzz_below (rng, (uint32_t) (sizeof (gen_ops)))]; + buf[len++] = fuzz_byte (rng); + buf[len++] = gen_size_sel (rng); + } + return len; +} + + +/* ------------------------------------------------------------------ */ +/* Seed corpus */ +/* ------------------------------------------------------------------ */ + +/* Size selectors, see fz_size() */ +#define SZ_TINY(n) ((uint8_t) ((((unsigned int) (n)) << 2) | 0u)) +#define SZ_FRAC(k) ((uint8_t) ((((unsigned int) (k)) << 2) | 1u)) +#define SZ_FREE ((uint8_t) 2u) +#define SZ_FREE_M(d) ((uint8_t) ((((unsigned int) (d)) << 2) | 2u)) +#define SZ_FREE_P(d) ((uint8_t) ((((unsigned int) (d)) << 2) | 0x20u | 2u)) +#define SZ_HUGE ((uint8_t) 3u) + +/* Pool size selectors, indices into fz_pool_sizes[] */ +#define PS_1 0 +#define PS_16 2 +#define PS_32 4 +#define PS_64 6 +#define PS_128 8 +#define PS_256 11 +#define PS_512 14 +#define PS_1024 16 +#define PS_1500 18 +#define PS_32768 23 + +#define OPR(o,b,s) ((uint8_t) (o)), ((uint8_t) (b)), (s) + +static const uint8_t seed_grow_shrink[] = { + PS_512, 0x01, + OPR (FZ_OP_ALLOC, 0x00, SZ_TINY (32)), + OPR (FZ_OP_REALLOC, 0x00, SZ_TINY (63)), + OPR (FZ_OP_REALLOC, 0x00, SZ_TINY (8)), + OPR (FZ_OP_REALLOC, 0x00, SZ_FRAC (32)), + OPR (FZ_OP_VERIFY, 0x00, SZ_TINY (0)), + OPR (FZ_OP_REALLOC, 0x00, SZ_TINY (1)), + OPR (FZ_OP_DEALLOC, 0x00, SZ_TINY (0)) +}; + +static const uint8_t seed_two_blocks_realloc[] = { + PS_256, 0x01, + OPR (FZ_OP_ALLOC, 0x00, SZ_TINY (17)), + OPR (FZ_OP_ALLOC, 0x00, SZ_TINY (23)), + /* the first block is no longer the last one: it has to move */ + OPR (FZ_OP_REALLOC, 0x00, SZ_TINY (60)), + OPR (FZ_OP_VERIFY, 0x00, SZ_TINY (0)), + OPR (FZ_OP_REALLOC, 0x01, SZ_TINY (40)), + OPR (FZ_OP_VERIFY, 0x00, SZ_TINY (0)) +}; + +static const uint8_t seed_from_end[] = { + PS_128, 0x01, + OPR (FZ_OP_ALLOC, 0x01, SZ_TINY (16)), + OPR (FZ_OP_ALLOC, 0x01, SZ_TINY (1)), + OPR (FZ_OP_ALLOC, 0x00, SZ_TINY (16)), + OPR (FZ_OP_RESIZABLE, 0x00, SZ_TINY (0)), + OPR (FZ_OP_DEALLOC, 0x00, SZ_TINY (0)), + OPR (FZ_OP_ALLOC, 0x01, SZ_TINY (16)) +}; + +static const uint8_t seed_fill_exactly[] = { + PS_128, 0x01, + OPR (FZ_OP_ALLOC, 0x01, SZ_TINY (16)), + OPR (FZ_OP_ALLOC_ALL, 0x00, SZ_TINY (0)), + OPR (FZ_OP_GET_FREE, 0x00, SZ_TINY (0)), + OPR (FZ_OP_ALLOC, 0x00, SZ_TINY (1)), + OPR (FZ_OP_TRY_ALLOC, 0x00, SZ_TINY (1)) +}; + +static const uint8_t seed_squeeze[] = { + PS_512, 0x01, + OPR (FZ_OP_ALLOC, 0x00, SZ_FREE), + OPR (FZ_OP_SQUEEZE, 0x00, SZ_TINY (40)), + OPR (FZ_OP_SQUEEZE, 0x00, SZ_TINY (7)), + OPR (FZ_OP_VERIFY, 0x00, SZ_TINY (0)), + OPR (FZ_OP_SQUEEZE, 0x00, SZ_FRAC (8)) +}; + +static const uint8_t seed_squeeze_small[] = { + PS_128, 0x01, + OPR (FZ_OP_REALLOC_NEW, 0x00, SZ_FREE_M (2)), + OPR (FZ_OP_SQUEEZE, 0x00, SZ_TINY (17)), + OPR (FZ_OP_SQUEEZE, 0x00, SZ_TINY (17)), + OPR (FZ_OP_SQUEEZE, 0x00, SZ_TINY (17)) +}; + +static const uint8_t seed_reset_keep[] = { + PS_1500, 0x01, + OPR (FZ_OP_ALLOC, 0x00, SZ_TINY (60)), + OPR (FZ_OP_ALLOC, 0x01, SZ_TINY (12)), + OPR (FZ_OP_RESET, 0xF0, SZ_FRAC (31)), + OPR (FZ_OP_VERIFY, 0x00, SZ_TINY (0)), + OPR (FZ_OP_RESET, 0xF0, SZ_TINY (4)), + OPR (FZ_OP_RESET, 0x00, SZ_TINY (0)) +}; + +static const uint8_t seed_reset_full[] = { + PS_256, 0x01, + OPR (FZ_OP_ALLOC, 0x00, SZ_FRAC (63)), + OPR (FZ_OP_RESET, 0xF0, SZ_FRAC (63)), + OPR (FZ_OP_RESET, 0xF0, SZ_FREE_P (1)), + OPR (FZ_OP_VERIFY, 0x00, SZ_TINY (0)) +}; + +static const uint8_t seed_boundary[] = { + PS_64, 0x01, + OPR (FZ_OP_ALLOC, 0x00, SZ_FREE_M (1)), + OPR (FZ_OP_ALLOC, 0x00, SZ_FREE), + OPR (FZ_OP_ALLOC, 0x01, SZ_FREE_P (1)), + OPR (FZ_OP_TRY_ALLOC, 0x00, SZ_FREE), + OPR (FZ_OP_DEALLOC, 0x00, SZ_TINY (0)), + OPR (FZ_OP_ALLOC, 0x00, SZ_FREE) +}; + +static const uint8_t seed_wrap[] = { + PS_256, 0x00, + OPR (FZ_OP_ALLOC, 0x00, SZ_HUGE), + OPR (FZ_OP_ALLOC, 0x01, SZ_HUGE), + OPR (FZ_OP_TRY_ALLOC, 0x00, SZ_HUGE), + OPR (FZ_OP_REALLOC_NEW, 0x00, SZ_HUGE), + OPR (FZ_OP_ALLOC, 0x00, SZ_TINY (16)), + OPR (FZ_OP_REALLOC, 0x00, SZ_HUGE), + OPR (FZ_OP_VERIFY, 0x00, SZ_TINY (0)) +}; + +static const uint8_t seed_tiny_pool[] = { + PS_1, 0x01, + OPR (FZ_OP_ALLOC, 0x00, SZ_TINY (1)), + OPR (FZ_OP_ALLOC, 0x01, SZ_TINY (1)), + OPR (FZ_OP_GET_FREE, 0x00, SZ_TINY (0)), + OPR (FZ_OP_RESET, 0xF0, SZ_TINY (1)), + OPR (FZ_OP_DEALLOC, 0x00, SZ_TINY (0)) +}; + +static const uint8_t seed_zero_sizes[] = { + PS_32, 0x01, + OPR (FZ_OP_ALLOC, 0x00, SZ_TINY (0)), + OPR (FZ_OP_ALLOC, 0x01, SZ_TINY (0)), + OPR (FZ_OP_REALLOC, 0x00, SZ_TINY (0)), + OPR (FZ_OP_REALLOC, 0x00, SZ_TINY (4)), + OPR (FZ_OP_DEALLOC, 0x00, SZ_TINY (0)), + OPR (FZ_OP_RESET, 0x00, SZ_TINY (0)) +}; + +static const uint8_t seed_many_small[] = { + PS_1500, 0x01, + OPR (FZ_OP_ALLOC, 0x00, SZ_TINY (5)), + OPR (FZ_OP_ALLOC, 0x01, SZ_TINY (5)), + OPR (FZ_OP_ALLOC, 0x00, SZ_TINY (5)), + OPR (FZ_OP_ALLOC, 0x01, SZ_TINY (5)), + OPR (FZ_OP_ALLOC, 0x00, SZ_TINY (5)), + OPR (FZ_OP_ALLOC, 0x01, SZ_TINY (5)), + OPR (FZ_OP_REALLOC, 0x00, SZ_TINY (33)), + OPR (FZ_OP_REALLOC, 0x01, SZ_TINY (33)), + OPR (FZ_OP_DEALLOC, 0x02, SZ_TINY (0)), + OPR (FZ_OP_VERIFY, 0x00, SZ_TINY (0)), + OPR (FZ_OP_ALLOC, 0x00, SZ_TINY (5)) +}; + +static const uint8_t seed_recreate[] = { + PS_32768, 0x00, + OPR (FZ_OP_ALLOC, 0x00, SZ_FRAC (60)), + OPR (FZ_OP_RECREATE, PS_16, SZ_TINY (0)), + OPR (FZ_OP_ALLOC, 0x00, SZ_FREE), + OPR (FZ_OP_RECREATE, PS_512, SZ_TINY (0)), + OPR (FZ_OP_ALLOC, 0x01, SZ_TINY (24)), + OPR (FZ_OP_RESET, 0xF0, SZ_FRAC (2)) +}; + +/* The read-buffer life cycle of a connection: allocate the read buffer, + grow it to everything that is free, put the parsed headers "at the + end", shrink the read buffer to what was actually received, then + reset the pool keeping the received bytes -- connection.c does + exactly this for every keep-alive request. */ +static const uint8_t seed_connection_cycle[] = { + PS_1500, 0x01, + OPR (FZ_OP_REALLOC_NEW, 0x00, SZ_FRAC (31)), + OPR (FZ_OP_REALLOC, 0x00, SZ_FREE), + OPR (FZ_OP_TRY_ALLOC, 0x00, SZ_TINY (12)), + OPR (FZ_OP_TRY_ALLOC, 0x00, SZ_TINY (20)), + OPR (FZ_OP_SQUEEZE, 0x00, SZ_TINY (40)), + OPR (FZ_OP_REALLOC, 0x00, SZ_TINY (60)), + OPR (FZ_OP_VERIFY, 0x00, SZ_TINY (0)), + OPR (FZ_OP_RESET, 0xF0, SZ_FRAC (31)), + OPR (FZ_OP_REALLOC, 0x00, SZ_FREE), + OPR (FZ_OP_VERIFY, 0x00, SZ_TINY (0)) +}; + + +/* The reproducer of the open finding described at full_dealloc_enabled: + a 128 byte pool, one 16 byte block "from the end", the whole rest of + the pool allocated from the front, then that first block released. + Inert unless MHD_FUZZ_POOL_FULL_DEALLOC=1 is set. */ +static const uint8_t seed_dealloc_end_full[] = { + PS_128, 0x01, + OPR (FZ_OP_ALLOC, 0x01, SZ_TINY (16)), + OPR (FZ_OP_ALLOC_ALL, 0x00, SZ_TINY (0)), + OPR (FZ_OP_DEALLOC, 0x00, SZ_TINY (0)) +}; + + +/* The reproducer of the open finding F2: MHD_pool_reset() with nothing + to keep leaves a zero-length block at the front boundary, and + reallocating that to SIZE_MAX wraps. Inert unless + MHD_FUZZ_POOL_WRAP_REALLOC=1 is set. */ +static const uint8_t seed_realloc_wrap[] = { + PS_1024, 0x01, + OPR (FZ_OP_RESET, 0x00, SZ_TINY (0)), + OPR (FZ_OP_REALLOC, 0x00, SZ_HUGE) +}; + + +struct mp_seed +{ + const uint8_t *bytes; + size_t len; +}; + +#define MSEED(a) { (a), sizeof (a) } + +static const struct mp_seed mp_seeds[] = { + MSEED (seed_grow_shrink), + MSEED (seed_two_blocks_realloc), + MSEED (seed_from_end), + MSEED (seed_fill_exactly), + MSEED (seed_squeeze), + MSEED (seed_squeeze_small), + MSEED (seed_reset_keep), + MSEED (seed_reset_full), + MSEED (seed_boundary), + MSEED (seed_wrap), + MSEED (seed_tiny_pool), + MSEED (seed_zero_sizes), + MSEED (seed_many_small), + MSEED (seed_recreate), + MSEED (seed_connection_cycle), + MSEED (seed_dealloc_end_full), + MSEED (seed_realloc_wrap) +}; + + +static size_t +fuzz_seed_count (void) +{ + return sizeof (mp_seeds) / sizeof (mp_seeds[0]); +} + + +static const uint8_t * +fuzz_seed_get (size_t idx, + size_t *len) +{ + *len = mp_seeds[idx].len; + return mp_seeds[idx].bytes; +} diff --git a/src/fuzz/fuzz_options.c b/src/fuzz/fuzz_options.c @@ -0,0 +1,2313 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. + If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * @file fuzz/fuzz_options.c + * @brief In-process fuzzer for MHD's daemon option handling and for the + * daemon start-up / shutdown paths. + * @author Christian Grothoff + * + * fuzz_request.c fuzzes what MHD does with the *bytes on the wire*, but + * it always starts the daemon in one single shape: external polling, no + * listen socket, five fixed options. Everything in daemon.c that is + * reached through any other configuration -- the flag validation in + * MHD_start_daemon(), parse_options_va(), the internal polling thread, + * the thread pool, epoll/poll/select, the listen socket, the per-IP and + * per-daemon connection limits, quiesce -- is therefore dead code as far + * as the fuzzing suite is concerned. This harness closes that gap: the + * input bytes pick a *combination* of MHD_FLAG bits and an MHD_OPTION + * array, the daemon is started with them, one short HTTP request is + * driven through it over a socketpair, and the daemon is stopped again. + * + * The request-driving machinery is the one from fuzz_request.c: + * MHD_USE_NO_LISTEN_SOCKET plus MHD_add_connection() on an AF_UNIX + * socketpair(), with a fake 127.0.0.1 peer address so that the per-IP + * accounting sees something sane, and MHD_set_panic_func() installed as + * a tripwire. What is new here is that the *daemon* is a variable. + * + * Two shapes of daemon exist, and the harness drives them differently: + * + * - "external polling" (no MHD_USE_INTERNAL_POLLING_THREAD and no + * MHD_USE_THREAD_PER_CONNECTION). Single threaded and fully + * deterministic: the segments are fed in one at a time and the + * daemon is pumped with MHD_run() / MHD_get_fdset2() + select() + + * MHD_run_from_select2() / MHD_run_wait() between them. This is the + * majority of the iterations and the only shape in which the harness + * suspends a connection. + * + * - "internal thread" (an internal polling thread, a thread pool, or + * thread-per-connection). MHD_add_connection() is explicitly + * supported in these modes -- it is how an application with its own + * accept() loop hands sockets over -- but the harness must not also + * poll the daemon, and it cannot know when the worker is done. So + * the whole request is written at once, the write side is shut down + * (which makes MHD finish the request immediately rather than + * waiting for more data), and the answer is awaited with a short + * bounded poll() before the daemon is stopped. MHD_USE_ITC is + * forced on in this shape so that the worker picks the new + * connection up at once instead of waiting for its poll timeout. + * No connection is ever suspended here. + * + * Input format (see README section 2.2 for the fuzz_request one, which + * the segment stream below is deliberately identical to): + * + * byte 0 daemon shape: index into mode_tbl[] (event loop and + * threading; the entries are the combinations MHD documents + * as valid, so that most iterations get a live daemon) + * byte 1 MHD_FLAG bits, group A (see flags_from_input()) + * byte 2 MHD_FLAG bits, group B; 0xC0 in the top two bits switches + * on the "raw" escape, which feeds bytes 1, 2 and 15 + * straight into the flags word. That is what fuzzes + * MHD_start_daemon()'s own combination checks; MHD answering + * NULL is a normal outcome and simply ends the iteration. + * byte 3 option presence mask A (sizes and limits) + * byte 4 option presence mask B (discipline, insanity, fd_setsize) + * byte 5 option presence mask C (the callbacks and the pointers) + * byte 6 value selector: connection memory limit / increment + * byte 7 value selector: connection limit / per-IP limit + * byte 8 value selector: connection timeout / pool size / stack + * byte 9 value selector: nonce-nc size / digest random / bind type + * byte 10 value selector: discipline / strict / insanity / bzero URI + * byte 11 value selector: backlog / fd_setsize / reuse / fastopen + * byte 12 driver behaviour: event loop variant, quiesce, + * introspection, suspend mode, number of connections + * byte 13 handler behaviour (which response, MHD_NO, per-connection + * option) and the accept path: whether an accept policy + * callback is installed, what it answers, and whether the + * iteration additionally connect()s to the daemon's real + * listening socket + * byte 14 option presence mask D (the digest and TLS-adjacent + * options, MHD_OPTION_LISTEN_SOCKET, and the deliberately + * invalid option number) + * byte 15 spare entropy; also supplies flag bits 14+ in raw mode + * byte 16. a sequence of send segments, each introduced by a little + * endian 16 bit header (op << 14) | length + * op 0 send the payload on the current connection + * op 1 send the payload, then pump extra rounds + * op 2 send the payload, then pump extra rounds + * op 3 close the current connection, open a fresh one on + * the same daemon, then send + * + * The sixteen configuration bytes are mandatory; a shorter input is + * rejected. An input of exactly sixteen bytes is meaningful and is not + * a degenerate case: it starts and stops a daemon with one connection + * and no traffic at all, which is precisely the start-up/shutdown path + * this harness is about. + * + * Two rules the harness obeys, both of them application contract rather + * than anything worth fuzzing: + * + * - every daemon that starts is stopped, and no connection is left + * suspended when that happens, because MHD_stop_daemon() answers a + * suspended connection with MHD_PANIC(). Suspended connections are + * tracked in pending_resume[] and flushed during teardown, exactly + * as in fuzz_request.c; + * - the socket that MHD_quiesce_daemon() returns belongs to the + * caller. It is closed after MHD_stop_daemon() (not before: with + * internal threads a worker may still be using it), otherwise the + * harness runs out of file descriptors within a few thousand + * iterations. + * + * Environment: + * + * MHD_FUZZ_QUIESCE_EPOLL_RACE=1 let the harness call + * MHD_quiesce_daemon() on an epoll daemon that runs its own + * thread(s). Off by default because that combination reaches an + * open MHD defect; see quiesce_epoll_race_allowed() below. + */ + +#define FUZZ_HARNESS_NAME "fuzz_options" +#include "fuzz_common.h" + +#include <microhttpd.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <sys/select.h> +#include <poll.h> +#include <limits.h> + +/* MHD_OPTION_STRICT_FOR_CLIENT is superseded by + MHD_OPTION_CLIENT_DISCIPLINE_LVL but is still shipped API, and its + mapping onto the discipline level is exactly the kind of thing this + harness is for. */ +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + +/** Number of mandatory configuration bytes at the start of the input. */ +#define CFG_BYTES 16 + +#define MAX_SEGMENTS 48 +#define MAX_CONNECTIONS 6 +#define MAX_OPTS 40 +#define RESP_DRAIN_BUF 4096 +#define GEN_BUF_SIZE 4096 + +/** + * Milliseconds the internal-thread shape waits for the daemon to answer. + * The write side of the socketpair is shut down before the wait, so the + * daemon never sits on an incomplete request: either it answers (the + * common case, tens of microseconds) or it closes the connection, and + * both wake poll() up immediately. The timeout is therefore only paid + * by inputs that make MHD drop the connection without a reply. + */ +#define THREAD_WAIT_MS 2 + +/** Rounds of MHD_run() after each segment in the external shape. */ +#define PUMP_ROUNDS 3 +/** Rounds of MHD_run() after a segment sent with op 1 or 2. */ +#define PUMP_ROUNDS_LONG 10 + + +/* ------------------------------------------------------------------ */ +/* Per-iteration configuration */ +/* ------------------------------------------------------------------ */ + +struct fuzz_cfg +{ + /** Daemon runs its own thread(s); the harness must not call MHD_run(). */ + int uses_threads; + /** A real listening socket was requested (no MHD_USE_NO_LISTEN_SOCKET). */ + int listen_sock; + /** MHD_USE_ERROR_LOG was requested, so an external logger is mandatory. */ + int err_log; + /** 0 none, 1 suspend+resume in the handler, 2 resume from the pump. */ + int suspend_mode; + /** 0 MHD_run(), 1 MHD_get_fdset2()+select()+MHD_run_from_select2(), + 2 MHD_run_wait(0), 3 the v1 MHD_get_fdset()/MHD_run_from_select(). */ + unsigned int loop_mode; + /** Call MHD_quiesce_daemon() before stopping. */ + int quiesce; + /** Query MHD_get_daemon_info() / MHD_get_timeout*() while pumping. */ + int daemon_info; + /** Call MHD_set_connection_option() from the handler. */ + int conn_option; + /** Which response constructor the handler uses. */ + unsigned int resp_kind; + /** Handler answers MHD_NO instead of queueing a response. */ + int handler_no; + /** Upper bound on the number of connections the iteration opens. */ + unsigned int nconn_max; + /** Really connect() to the daemon's listening socket. */ + int real_connect; + /** Install an accept policy callback, and what it should answer. */ + int accept_policy; + int accept_policy_deny; +}; + +static struct fuzz_cfg cfg; + +/** + * Connections suspended by suspend mode 2, which the pump loop still has + * to resume. Cleared by completed_cb() so that a connection MHD has + * finished with is never resumed afterwards. A connection that is still + * suspended when MHD_stop_daemon() runs makes MHD answer with + * MHD_PANIC ("MHD_stop_daemon() called while we have suspended + * connections"), which would look exactly like an MHD bug. + */ +static struct MHD_Connection *pending_resume[MAX_CONNECTIONS]; + +/** + * Set once the iteration only wants to drain the daemon. The handler + * then stops parking new connections, which is what makes the flush loop + * in the teardown provably terminate. + */ +static int tearing_down; + +/** Statistics, printed at exit with --verbose. */ +static unsigned long stat_daemons; +static unsigned long stat_daemons_failed; +static unsigned long stat_threaded; +static unsigned long stat_handler_calls; +static unsigned long stat_conns_added; +static unsigned long stat_conns_refused; +static unsigned long stat_real_conns; +static int stats_registered; + + +static void +print_stats (void) +{ + if (! fuzz_verbose) + return; + fprintf (stderr, + "%s: daemons=%lu (failed to start=%lu, threaded=%lu) " + "connections=%lu (refused=%lu, accepted from a real socket=%lu) " + "handler calls=%lu\n", + FUZZ_HARNESS_NAME, + stat_daemons, stat_daemons_failed, stat_threaded, + stat_conns_added, stat_conns_refused, stat_real_conns, + stat_handler_calls); +} + + +/* ------------------------------------------------------------------ */ +/* Flags */ +/* ------------------------------------------------------------------ */ + +/** + * The event-loop / threading core of the flags word. Every entry is a + * combination MHD documents as valid, so that the ordinary path through + * this harness gets a live daemon and actually exercises the option + * handling; the deliberately invalid combinations are reached through + * the raw escape of byte 2 instead. + * + * The distribution is on purpose: entries 0..17 are the external polling + * modes, 18..23 the ones with internal threads. An iteration with + * internal threads costs a thread creation, a join and a short wait for + * the answer, i.e. roughly an order of magnitude more than an external + * one, so they are a quarter of the iterations rather than a half. + */ +static const unsigned int mode_tbl[] = { + 0, 0, 0, 0, 0, 0, /* external, select() */ + 0, 0, 0, 0, 0, 0, + MHD_USE_EPOLL, MHD_USE_EPOLL, /* external, epoll */ + MHD_USE_EPOLL, MHD_USE_EPOLL, + MHD_USE_AUTO, MHD_USE_AUTO, /* external, MHD picks */ + MHD_USE_INTERNAL_POLLING_THREAD, + MHD_USE_INTERNAL_POLLING_THREAD | MHD_USE_POLL, + MHD_USE_INTERNAL_POLLING_THREAD | MHD_USE_EPOLL, + MHD_USE_THREAD_PER_CONNECTION | MHD_USE_INTERNAL_POLLING_THREAD, + MHD_USE_THREAD_PER_CONNECTION | MHD_USE_INTERNAL_POLLING_THREAD + | MHD_USE_POLL, + MHD_USE_AUTO | MHD_USE_INTERNAL_POLLING_THREAD +}; + +#define MODE_COUNT (sizeof (mode_tbl) / sizeof (mode_tbl[0])) + +/** + * Flag bits the raw escape is allowed to set. MHD_USE_TLS is masked + * out: without a certificate the daemon merely fails to start, so it + * would only waste iterations, and fuzzing GnuTLS is not what this + * harness is for. Everything else, including the bits above + * MHD_USE_NO_THREAD_SAFETY that no MHD_FLAG uses, is fair game. + */ +#define RAW_FLAG_MASK (~((unsigned int) MHD_USE_TLS)) + +/** + * The bit that MHD_suspend_connection() actually tests. + * + * MHD_ALLOW_SUSPEND_RESUME is a *compound* value (8192 | MHD_USE_ITC), + * so "0 != (flags & MHD_ALLOW_SUSPEND_RESUME)" is already true for a + * daemon that merely asked for MHD_USE_ITC -- and suspending such a + * connection is answered with + * MHD_PANIC ("Cannot suspend connections without enabling + * MHD_ALLOW_SUSPEND_RESUME"), which is the harness violating the API + * rather than a finding. internal.h calls the single bit + * MHD_TEST_ALLOW_SUSPEND_RESUME; this is the same value, expressed + * without reaching into a private header. + */ +#define SUSPEND_BIT \ + (((unsigned int) MHD_ALLOW_SUSPEND_RESUME) \ + & ~((unsigned int) MHD_USE_ITC)) + + +/** + * Build the flags word from bytes 0, 1, 2 and 15 of the input. + * + * @param data the input + * @return the flags to pass to MHD_start_daemon() + */ +static unsigned int +flags_from_input (const uint8_t *data) +{ + unsigned int flags; + + if (0xC0 == (data[2] & 0xC0)) + { + /* Raw escape: the flags word comes straight off the input. This is + what exercises MHD_start_daemon()'s combination checks (POLL with + EPOLL, EPOLL with thread-per-connection, AUTO with either, ...), + every one of which answers NULL. */ + flags = ((unsigned int) data[1]) + | (((unsigned int) (data[2] & 0x3F)) << 8) + | (((unsigned int) data[15]) << 14); + flags &= RAW_FLAG_MASK; + cfg.listen_sock = (0 == (flags & MHD_USE_NO_LISTEN_SOCKET)); + cfg.err_log = (0 != (flags & MHD_USE_ERROR_LOG)); + return flags; + } + + flags = mode_tbl[data[0] % MODE_COUNT]; + + if (0 != (data[1] & 0x01)) + flags |= MHD_USE_PEDANTIC_CHECKS; + if (0 != (data[1] & 0x02)) + flags |= MHD_USE_SUPPRESS_DATE_NO_CLOCK; + if (0 != (data[1] & 0x04)) + flags |= MHD_ALLOW_SUSPEND_RESUME; + if ( (0 != (data[1] & 0x08)) && + (MHD_YES == MHD_is_feature_supported (MHD_FEATURE_UPGRADE)) ) + flags |= MHD_ALLOW_UPGRADE; + if (0 != (data[1] & 0x10)) + flags |= MHD_USE_TURBO; + if (0 != (data[1] & 0x20)) + flags |= MHD_USE_ITC; + if (0 != (data[1] & 0x40)) + cfg.err_log = 1; + if (0 != (data[1] & 0x80)) + flags |= MHD_USE_POST_HANDSHAKE_AUTH_SUPPORT; /* ignored without TLS */ + + if (0 != (data[2] & 0x01)) + cfg.listen_sock = 1; + if (0 != (data[2] & 0x02)) + flags |= MHD_USE_IPv6; + if (0 != (data[2] & 0x04)) + flags |= MHD_USE_DUAL_STACK; + if (0 != (data[2] & 0x08)) + flags |= MHD_USE_TCP_FASTOPEN; + if (0 != (data[2] & 0x10)) + flags |= MHD_USE_NO_THREAD_SAFETY; + if (0 != (data[2] & 0x20)) + flags |= MHD_USE_INSECURE_TLS_EARLY_DATA; /* ignored without TLS */ + + /* MHD_USE_NO_THREAD_SAFETY together with any internal thread is + documented as unsupported and answered with NULL. Drop it here + rather than burning the iteration; the raw escape above still + reaches that check. */ + if (0 != (flags & (MHD_USE_INTERNAL_POLLING_THREAD + | MHD_USE_THREAD_PER_CONNECTION))) + flags &= ~((unsigned int) MHD_USE_NO_THREAD_SAFETY); + return flags; +} + + +/** + * @return non-zero if @a flags make MHD use epoll + * + * MHD_USE_AUTO resolves to epoll for everything except + * thread-per-connection, which it resolves to poll. + */ +static int +uses_epoll (unsigned int flags) +{ + if (0 != (flags & MHD_USE_EPOLL)) + return 1; + return (0 != (flags & MHD_USE_AUTO)) && + (0 == (flags & MHD_USE_THREAD_PER_CONNECTION)); +} + + +/** + * Is the harness allowed to call MHD_quiesce_daemon() on an epoll + * daemon that runs its own thread(s)? + * + * It is not, by default, because that combination reaches an *open* MHD + * defect and would make "make check" fail at random: + * + * MHD_quiesce_daemon() sets daemon->was_quiesced and then removes the + * listen FD from the epoll set itself (daemon.c:6208), tolerating + * ENOENT with the comment "can happen due to race with MHD_epoll()". + * MHD_epoll() removes the very same FD in two places. The first + * (daemon.c:5556) mirrors that and tolerates ENOENT. The second + * (daemon.c:5593), the "at the connection limit, disable listen + * socket" branch, also fires when daemon->was_quiesced is set -- and + * it does not tolerate anything: + * + * if (0 != epoll_ctl (daemon->epoll_fd, EPOLL_CTL_DEL, ls, NULL)) + * MHD_PANIC (_ ("Failed to remove listen FD from epoll set.")); + * + * The worker can read daemon->was_quiesced as already true and + * daemon->listen_socket_in_epoll as still true -- neither is atomic + * and neither is under a lock -- and then lose the race for the + * removal, so epoll_ctl() answers ENOENT and MHD aborts the process. + * Confirmed with errno == ENOENT, was_quiesced == 1, connections == 0. + * The same unguarded MHD_PANIC() sits in the worker-pool loop of + * MHD_quiesce_daemon() itself (daemon.c:6189). + * + * Set MHD_FUZZ_QUIESCE_EPOLL_RACE=1 to reach it. Quiescing is left + * enabled everywhere else, so the poll()/select() side of + * MHD_quiesce_daemon(), including its worker-pool loop, keeps being + * exercised. + */ +static int +quiesce_epoll_race_allowed (void) +{ + static int val = -1; + + if (0 > val) + { + const char *e = getenv ("MHD_FUZZ_QUIESCE_EPOLL_RACE"); + + val = ((NULL != e) && (0 != atoi (e))) ? 1 : 0; + } + return val; +} + + +/* ------------------------------------------------------------------ */ +/* Option values */ +/* ------------------------------------------------------------------ */ + +static const size_t mem_limit_tbl[] = { + 0 /* MHD default */, 128, 192, 256, 384, 512, 1024, 1400, + 1500, 2048, 4096, 8192, 32768, 64, 0, 0 +}; + +static const size_t mem_increment_tbl[] = { + 0 /* MHD default */, 1, 16, 64, 128, 256, 1024, 4096, + 1500, 32768, 0, 0, 0, 0, 0, 0 +}; + +static const unsigned int conn_limit_tbl[] = { + 0, 1, 2, 3, 8, 64, 1024, 100000 +}; + +static const unsigned int per_ip_limit_tbl[] = { + 0, 1, 2, 3, 4, 8, 64, 1000 +}; + +static const unsigned int timeout_tbl[] = { + 0, 1, 2, 5, 60, 3600, 86400, UINT_MAX +}; + +static const unsigned int pool_size_tbl[] = { 2, 3, 4, 2 }; + +/* Small stacks make pthread_create() fail outright under ASAN, which + only produces a NULL daemon; keep the values plausible. */ +static const size_t stack_size_tbl[] = { + 0, 1024u * 1024u, 2048u * 1024u, 8192u * 1024u +}; + +/* Bounded on purpose: the nonce-nc array is nonce_nc_size * + sizeof(struct MHD_NonceNc) (about 130 bytes per slot) and is + allocated and freed once per iteration. */ +static const unsigned int nonce_nc_tbl[] = { 0, 1, 4, 32 }; + +static const int discipline_tbl[] = { -3, -2, -1, 0, 1, 2, 3, -4 }; + +static const int strict_tbl[] = { -1, 0, 1, 2 }; + +static const unsigned int backlog_tbl[] = { 0, 1, 5, 511 }; + +static const unsigned int fastopen_tbl[] = { 0, 1, 5, 10 }; + +/* Anything but the platform's own FD_SETSIZE is rejected unless MHD was + built with HAS_FD_SETSIZE_OVERRIDABLE, so the odd values are a small + minority: they cost a whole iteration each. */ +static const int fd_setsize_tbl[] = { + (int) FD_SETSIZE, (int) FD_SETSIZE, (int) FD_SETSIZE, (int) FD_SETSIZE, + (int) FD_SETSIZE, (int) FD_SETSIZE, 64, 0 +}; + +/** Fixed entropy, so that a digest nonce is reproducible across runs. */ +static const char digest_rnd[32] = + "\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67\x89\xab\xcd\xef" + "\xfe\xdc\xba\x98\x76\x54\x32\x10\xfe\xdc\xba\x98\x76\x54\x32\x10"; + +/* Bind addresses for MHD_OPTION_SOCK_ADDR / MHD_OPTION_SOCK_ADDR_LEN. + Filled in by prepare_sock_addrs() because htons()/htonl() are not + constant expressions. */ +static struct sockaddr_in bind4; +#ifdef AF_INET6 +static struct sockaddr_in6 bind6; +#endif +static int bind_addrs_ready; + + +static void +prepare_sock_addrs (void) +{ + if (bind_addrs_ready) + return; + bind_addrs_ready = 1; + memset (&bind4, 0, sizeof (bind4)); + bind4.sin_family = AF_INET; + bind4.sin_port = htons (0); /* ephemeral */ + bind4.sin_addr.s_addr = htonl (INADDR_LOOPBACK); +#ifdef AF_INET6 + memset (&bind6, 0, sizeof (bind6)); + bind6.sin6_family = AF_INET6; + bind6.sin6_port = htons (0); + bind6.sin6_addr = in6addr_loopback; +#endif +} + + +/* ------------------------------------------------------------------ */ +/* The daemon callbacks */ +/* ------------------------------------------------------------------ */ + +/** + * MHD_OPTION_EXTERNAL_LOGGER. Swallows the message unless --verbose is + * in effect. This is what makes MHD_USE_ERROR_LOG affordable: with the + * flag set, MHD executes several hundred MHD_DLOG() call sites in + * daemon.c that are otherwise dead, and without a logger of our own all + * of that would land on stderr. + */ +static void +logger_cb (void *cls, + const char *fmt, + va_list ap) +{ + (void) cls; + if (! fuzz_verbose) + return; + (void) vfprintf (stderr, fmt, ap); +} + + +/** + * MHD_OPTION_URI_LOG_CALLBACK. The return value becomes the initial + * `*req_cls` of the request, so it has to stay NULL: the access handler + * uses "req_cls is still NULL" to recognise its first invocation, and a + * non-NULL value here would also have to be freed somewhere, which is + * not possible when MHD_OPTION_NOTIFY_COMPLETED is not set. + */ +static void * +uri_log_cb (void *cls, + const char *uri, + struct MHD_Connection *con) +{ + volatile size_t sink; + + (void) cls; + (void) con; + sink = (NULL != uri) ? strlen (uri) : 0; + (void) sink; + return NULL; +} + + +/** + * MHD_OPTION_UNESCAPE_CALLBACK. Replaces MHD's own unescaping, so it + * must honour the contract: unescape @a s in place and return the new + * length. Leaving the string alone is a legal implementation and keeps + * the harness out of the business of re-testing mhd_str.c, which + * fuzz_str does. + */ +static size_t +unescape_cb (void *cls, + struct MHD_Connection *conn, + char *s) +{ + (void) cls; + (void) conn; + return strlen (s); +} + + +static void +completed_cb (void *cls, + struct MHD_Connection *connection, + void **req_cls, + enum MHD_RequestTerminationCode toe) +{ + (void) cls; + (void) toe; + /* MHD is done with this connection, so the deferred resume of suspend + mode 2 must not fire for it any more. */ + if (! cfg.uses_threads) + { + unsigned int i; + + for (i = 0; i < MAX_CONNECTIONS; i++) + if (pending_resume[i] == connection) + pending_resume[i] = NULL; + } + *req_cls = NULL; +} + + +static void +notify_connection_cb (void *cls, + struct MHD_Connection *connection, + void **socket_context, + enum MHD_ConnectionNotificationCode toe) +{ + (void) cls; + (void) connection; + if (MHD_CONNECTION_NOTIFY_STARTED == toe) + *socket_context = NULL; +} + + +static void +panic_cb (void *cls, + const char *file, + unsigned int line, + const char *reason) +{ + char msg[512]; + + (void) cls; + (void) snprintf (msg, sizeof (msg), + "MHD_PANIC() reached at %s:%u: %s", + (NULL != file) ? file : "?", + line, + (NULL != reason) ? reason : "?"); + fuzz_report_finding (msg); +} + + +/* ------------------------------------------------------------------ */ +/* Suspend bookkeeping */ +/* ------------------------------------------------------------------ */ + +static void +pending_resume_add (struct MHD_Connection *c) +{ + unsigned int i; + + for (i = 0; i < MAX_CONNECTIONS; i++) + { + if (NULL == pending_resume[i]) + { + pending_resume[i] = c; + return; + } + } +} + + +/** + * Resume everything still parked. + * + * @return non-zero if at least one connection was resumed, so that the + * caller knows the daemon needs another round to act on it + */ +static int +pending_resume_flush (void) +{ + unsigned int i; + int any = 0; + + for (i = 0; i < MAX_CONNECTIONS; i++) + { + struct MHD_Connection *c = pending_resume[i]; + + if (NULL == c) + continue; + /* Clear first: MHD_resume_connection() can make MHD run the handler, + which may suspend the very same connection again. */ + pending_resume[i] = NULL; + MHD_resume_connection (c); + any = 1; + } + return any; +} + + +/** + * Suspend the connection if the input asks for it. Only ever called in + * the external-polling shape: resuming from the harness thread while an + * internal worker owns the connection is legal but untimed, and this + * harness has no way to tell whether the resume happened before or after + * the worker looked at the connection. + */ +static void +suspend_maybe (struct MHD_Connection *connection) +{ + if ( (0 == cfg.suspend_mode) || + cfg.uses_threads || + tearing_down) + return; + MHD_suspend_connection (connection); + if (1 == cfg.suspend_mode) + { + MHD_resume_connection (connection); + return; + } + pending_resume_add (connection); +} + + +/* ------------------------------------------------------------------ */ +/* The access handler */ +/* ------------------------------------------------------------------ */ + +static const char resp_body[] = "hello"; + +static enum MHD_Result +ahc (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **req_cls) +{ + /* The address of this object is the "request already started" marker. + Nothing is allocated per request on purpose: this harness runs + millions of iterations in one process and the daemon shapes with + internal threads cannot guarantee that MHD_OPTION_NOTIFY_COMPLETED + is even set, so anything malloc()ed here could leak. */ + static int req_marker; + struct MHD_Response *resp; + enum MHD_Result ret; + unsigned int code; + volatile size_t sink = 0; + + (void) cls; + (void) upload_data; + + if (&req_marker != *req_cls) + { + *req_cls = &req_marker; + return MHD_YES; + } + stat_handler_calls++; + + /* Touch the parsed request line the way a real application would. */ + if (NULL != url) + sink += strlen (url); + if (NULL != method) + sink += strlen (method); + if (NULL != version) + sink += strlen (version); + (void) sink; + + if (0 != *upload_data_size) + { + *upload_data_size = 0; + return MHD_YES; + } + + if (cfg.conn_option) + (void) MHD_set_connection_option (connection, + MHD_CONNECTION_OPTION_TIMEOUT, + (unsigned int) 30); + if (cfg.handler_no) + return MHD_NO; /* never suspend on this path: MHD_NO terminates the + connection and terminating a suspended one trips + mhd_assert (! connection->suspended) */ + + code = MHD_HTTP_OK; + switch (cfg.resp_kind) + { + case 1: + resp = MHD_create_response_empty (MHD_RF_NONE); + code = MHD_HTTP_NO_CONTENT; + break; + case 2: + resp = MHD_create_response_from_buffer (sizeof (resp_body) - 1, + (void *) (intptr_t) resp_body, + MHD_RESPMEM_MUST_COPY); + code = MHD_HTTP_FORBIDDEN; + break; + case 3: + resp = MHD_create_response_from_buffer_static (0, ""); + code = MHD_HTTP_INTERNAL_SERVER_ERROR; + break; + default: + resp = MHD_create_response_from_buffer_static (sizeof (resp_body) - 1, + resp_body); + break; + } + if (NULL == resp) + return MHD_NO; + ret = MHD_queue_response (connection, code, resp); + MHD_destroy_response (resp); + if (MHD_YES == ret) + suspend_maybe (connection); + return ret; +} + + +/* ------------------------------------------------------------------ */ +/* Driving the daemon */ +/* ------------------------------------------------------------------ */ + +/** + * Read and discard whatever the daemon has produced so far. The harness + * has no response oracle -- fuzz_request owns that job -- but the data + * has to be taken off the socket so that MHD's writes keep succeeding. + */ +static void +drain (int sock) +{ + char tmp[RESP_DRAIN_BUF]; + + if (0 > sock) + return; + while (0 < recv (sock, tmp, sizeof (tmp), MSG_DONTWAIT)) + /* nothing */; +} + + +/** + * Advance an externally polled daemon by one cycle, through whichever of + * the four event-loop entry points byte 12 selected. + * + * The select() timeout is always zero: the harness is single threaded + * and everything the daemon could be waiting for has already been + * written into the socketpair, so blocking would only burn wall clock. + */ +static void +run_once (struct MHD_Daemon *d) +{ + fd_set rs; + fd_set ws; + fd_set es; + MHD_socket max_fd = MHD_INVALID_SOCKET; + struct timeval tv; + + if (cfg.daemon_info) + { + MHD_UNSIGNED_LONG_LONG tl = 0; + volatile int64_t sink; + + (void) MHD_get_timeout (d, &tl); + sink = MHD_get_timeout64s (d); + sink += (int64_t) MHD_get_timeout_i (d); + (void) sink; + } + switch (cfg.loop_mode) + { + case 1: + case 3: + FD_ZERO (&rs); + FD_ZERO (&ws); + FD_ZERO (&es); + /* MHD_get_fdset and MHD_run_from_select are also macros forwarding + to the *2 variants, so the names have to be parenthesised for the + v1 entry points to be reached at all. */ + if (1 == cfg.loop_mode) + { + if (MHD_YES != MHD_get_fdset2 (d, &rs, &ws, &es, &max_fd, + (unsigned int) FD_SETSIZE)) + { + (void) MHD_run (d); + return; + } + } + else + { + if (MHD_YES != (MHD_get_fdset) (d, &rs, &ws, &es, &max_fd)) + { + (void) MHD_run (d); + return; + } + } + tv.tv_sec = 0; + tv.tv_usec = 0; + if (MHD_INVALID_SOCKET != max_fd) + (void) select ((int) max_fd + 1, &rs, &ws, &es, &tv); + if (1 == cfg.loop_mode) + (void) MHD_run_from_select2 (d, &rs, &ws, &es, (unsigned int) FD_SETSIZE); + else + (void) (MHD_run_from_select) (d, &rs, &ws, &es); + break; + case 2: + (void) MHD_run_wait (d, 0); + break; + default: + (void) MHD_run (d); + break; + } +} + + +static void +pump (struct MHD_Daemon *d, + int sock, + unsigned int rounds) +{ + unsigned int i; + + if (cfg.uses_threads) + return; + for (i = 0; i < rounds; i++) + { + /* Deferred resume of suspend mode 2. Each slot is cleared before + its connection is resumed, which stays correct even when the + resume makes MHD complete (and forget) the connection. */ + (void) pending_resume_flush (); + run_once (d); + drain (sock); + } +} + + +/** + * Wait, briefly and with a hard bound, for a daemon with internal + * threads to answer. The caller has already shut the write side down, + * so MHD sees end-of-stream and either answers or closes; both wake + * poll() immediately. An input that makes MHD do neither pays the full + * timeout, which is why it is only a couple of milliseconds. + */ +static void +wait_threaded (int sock) +{ + unsigned int round; + + if (0 > sock) + return; + for (round = 0; round < 4; round++) + { + struct pollfd p; + char tmp[RESP_DRAIN_BUF]; + ssize_t n; + + p.fd = sock; + p.events = POLLIN; + p.revents = 0; + if (0 >= poll (&p, 1, THREAD_WAIT_MS)) + return; + if (0 == (p.revents & POLLIN)) + return; /* POLLHUP/POLLERR only: peer is gone */ + n = recv (sock, tmp, sizeof (tmp), MSG_DONTWAIT); + if (0 >= n) + return; + } +} + + +static void +send_all (struct MHD_Daemon *d, + int sock, + const uint8_t *data, + size_t len) +{ + size_t off = 0; + unsigned int stall = 0; + + if (0 > sock) + return; + while ( (off < len) && + (stall < 64) ) + { + ssize_t s = send (sock, data + off, len - off, MSG_DONTWAIT); + + if (0 < s) + { + off += (size_t) s; + stall = 0; + continue; + } + stall++; + if (cfg.uses_threads) + { + struct pollfd p; + + p.fd = sock; + p.events = POLLOUT; + p.revents = 0; + if (0 >= poll (&p, 1, THREAD_WAIT_MS)) + break; + } + else + { + pump (d, sock, 2); + } + if ( (0 > s) && + (EAGAIN != errno) && + (EWOULDBLOCK != errno) && + (EINTR != errno) ) + break; + } +} + + +/** + * Hand a fresh socketpair to the daemon. + * + * @param d the daemon + * @param[out] sock set to the harness side of the pair + * @return 0 on success, -1 if MHD refused the connection (which is a + * perfectly ordinary outcome: MHD_OPTION_CONNECTION_LIMIT and + * MHD_OPTION_PER_IP_CONNECTION_LIMIT are part of what is fuzzed) + */ +static int +new_connection (struct MHD_Daemon *d, + int *sock) +{ + int sv[2]; + struct sockaddr_in sa; + + *sock = -1; + if (0 != socketpair (AF_UNIX, SOCK_STREAM, 0, sv)) + return -1; + memset (&sa, 0, sizeof (sa)); + sa.sin_family = AF_INET; + sa.sin_port = htons (44444); + sa.sin_addr.s_addr = htonl (INADDR_LOOPBACK); + if (MHD_YES != MHD_add_connection (d, + (MHD_socket) sv[1], + (const struct sockaddr *) &sa, + (socklen_t) sizeof (sa))) + { + /* MHD has already closed sv[1] in that case */ + (void) close (sv[0]); + stat_conns_refused++; + return -1; + } + stat_conns_added++; + *sock = sv[0]; + return 0; +} + + +static void +close_connection (struct MHD_Daemon *d, + int *sock) +{ + if (0 > *sock) + return; + (void) shutdown (*sock, SHUT_WR); + if (cfg.uses_threads) + wait_threaded (*sock); + else + pump (d, *sock, 4); + (void) close (*sock); + *sock = -1; +} + + +/* ------------------------------------------------------------------ */ +/* Building the option array */ +/* ------------------------------------------------------------------ */ + +#define PICK(tbl, idx) ((tbl)[(idx) % (sizeof (tbl) / sizeof ((tbl)[0]))]) + +static void +add_opt (struct MHD_OptionItem *opts, + unsigned int *nopt, + enum MHD_OPTION option, + intptr_t value, + void *ptr_value) +{ + if (*nopt + 1 >= MAX_OPTS) + return; + opts[*nopt].option = option; + opts[*nopt].value = value; + opts[*nopt].ptr_value = ptr_value; + (*nopt)++; +} + + +/** + * Translate the presence masks (bytes 3, 4, 14) and the value selectors + * (bytes 6..11, 15) into an MHD_OptionItem array. + * + * Only the options that can be expressed through MHD_OPTION_ARRAY are + * built here. The five callback options take two pointers, and putting + * a function pointer into the array's `intptr_t value` member is not + * strictly conforming C, so those go through the varargs of + * MHD_start_daemon() instead (see start_daemon_variant()). + */ +static unsigned int +build_options (const uint8_t *data, + struct MHD_OptionItem *opts, + unsigned int flags) +{ + unsigned int nopt = 0; + const uint8_t mask_a = data[3]; + const uint8_t mask_b = data[4]; + const uint8_t mask_d = data[14]; + /* A thread pool needs an internal polling thread and is incompatible + with thread-per-connection. */ + const int pool_ok = + (0 != (flags & MHD_USE_INTERNAL_POLLING_THREAD)) && + (0 == (flags & MHD_USE_THREAD_PER_CONNECTION)); + /* Offer options in configurations where MHD rejects them. One input + in eight: the rejection branches are worth reaching, but each one + costs the whole rest of the iteration, because the daemon does not + start at all. */ + const int misfit = (0xE0 == (data[15] & 0xE0)); + + if (0 != (mask_a & 0x01)) + add_opt (opts, &nopt, MHD_OPTION_CONNECTION_MEMORY_LIMIT, + (intptr_t) PICK (mem_limit_tbl, data[6] & 0x0F), NULL); + if (0 != (mask_a & 0x02)) + add_opt (opts, &nopt, MHD_OPTION_CONNECTION_MEMORY_INCREMENT, + (intptr_t) PICK (mem_increment_tbl, (data[6] >> 4) & 0x0F), NULL); + if (0 != (mask_a & 0x04)) + add_opt (opts, &nopt, MHD_OPTION_CONNECTION_LIMIT, + (intptr_t) PICK (conn_limit_tbl, data[7] & 0x07), NULL); + if (0 != (mask_a & 0x08)) + add_opt (opts, &nopt, MHD_OPTION_PER_IP_CONNECTION_LIMIT, + (intptr_t) PICK (per_ip_limit_tbl, (data[7] >> 4) & 0x07), NULL); + if (0 != (mask_a & 0x10)) + add_opt (opts, &nopt, MHD_OPTION_CONNECTION_TIMEOUT, + (intptr_t) PICK (timeout_tbl, data[8] & 0x07), NULL); + /* MHD_OPTION_THREAD_POOL_SIZE is rejected outright without an + internal polling thread, and again when combined with + MHD_USE_THREAD_PER_CONNECTION. Both branches are worth reaching, + but a daemon that fails to start exercises nothing else, so the + mismatched combinations are gated behind an extra bit instead of + being half of all the inputs that name the option. */ + if ( (0 != (mask_a & 0x20)) && + (pool_ok || misfit) ) + add_opt (opts, &nopt, MHD_OPTION_THREAD_POOL_SIZE, + (intptr_t) PICK (pool_size_tbl, (data[8] >> 3) & 0x03), NULL); + if (0 != (mask_a & 0x40)) + add_opt (opts, &nopt, MHD_OPTION_THREAD_STACK_SIZE, + (intptr_t) PICK (stack_size_tbl, (data[8] >> 5) & 0x03), NULL); + if (0 != (mask_a & 0x80)) + add_opt (opts, &nopt, MHD_OPTION_LISTENING_ADDRESS_REUSE, + (intptr_t) (unsigned int) (data[11] & 0x01), NULL); + + if (0 != (mask_b & 0x01)) + add_opt (opts, &nopt, MHD_OPTION_LISTEN_BACKLOG_SIZE, + (intptr_t) PICK (backlog_tbl, (data[11] >> 1) & 0x03), NULL); + if (0 != (mask_b & 0x02)) + add_opt (opts, &nopt, MHD_OPTION_NONCE_NC_SIZE, + (intptr_t) PICK (nonce_nc_tbl, data[9] & 0x03), NULL); + if (0 != (mask_b & 0x04)) + add_opt (opts, &nopt, MHD_OPTION_SERVER_INSANITY, + (intptr_t) (unsigned int) ((data[10] >> 6) & 0x03), NULL); + if (0 != (mask_b & 0x08)) + add_opt (opts, &nopt, MHD_OPTION_STRICT_FOR_CLIENT, + (intptr_t) PICK (strict_tbl, (data[10] >> 4) & 0x03), NULL); + if (0 != (mask_b & 0x10)) + add_opt (opts, &nopt, MHD_OPTION_CLIENT_DISCIPLINE_LVL, + (intptr_t) PICK (discipline_tbl, data[10] & 0x07), NULL); + if (0 != (mask_b & 0x20)) + add_opt (opts, &nopt, MHD_OPTION_SIGPIPE_HANDLED_BY_APP, + /* Truthful: LLVMFuzzerTestOneInput() ignores SIGPIPE + process-wide before anything else happens. */ + (intptr_t) 1, NULL); + if (0 != (mask_b & 0x40)) + add_opt (opts, &nopt, MHD_OPTION_APP_FD_SETSIZE, + (intptr_t) PICK (fd_setsize_tbl, (data[11] >> 3) & 0x07), NULL); + if (0 != (mask_b & 0x80)) + add_opt (opts, &nopt, MHD_OPTION_ALLOW_BIN_ZERO_IN_URI_PATH, + (intptr_t) (int) ((data[11] >> 5) & 0x03), NULL); + + if (0 != (mask_d & 0x01)) + add_opt (opts, &nopt, MHD_OPTION_DIGEST_AUTH_NONCE_BIND_TYPE, + (intptr_t) (unsigned int) ((data[9] >> 2) & 0x0F), NULL); + if (0 != (mask_d & 0x02)) + add_opt (opts, &nopt, MHD_OPTION_DIGEST_AUTH_DEFAULT_NONCE_TIMEOUT, + (intptr_t) (unsigned int) (data[15] & 0x7F), NULL); + if (0 != (mask_d & 0x04)) + add_opt (opts, &nopt, MHD_OPTION_DIGEST_AUTH_DEFAULT_MAX_NC, + (intptr_t) (uint32_t) data[15], NULL); + if (0 != (mask_d & 0x08)) + add_opt (opts, &nopt, MHD_OPTION_TCP_FASTOPEN_QUEUE_SIZE, + (intptr_t) PICK (fastopen_tbl, (data[11] >> 6) & 0x03), NULL); + if (0 != (mask_d & 0x10)) + add_opt (opts, &nopt, MHD_OPTION_TLS_NO_ALPN, + (intptr_t) (int) (data[15] & 0x01), NULL); + if (0 != (mask_d & 0x20)) + /* MHD_INVALID_SOCKET means "use the socket MHD creates itself"; a + real descriptor is deliberately not offered, because MHD takes + ownership of it and the harness would have to track that. */ + add_opt (opts, &nopt, MHD_OPTION_LISTEN_SOCKET, + (intptr_t) MHD_INVALID_SOCKET, NULL); + + /* MHD_OPTION_DIGEST_AUTH_RANDOM keeps the caller's buffer, the _COPY + variant makes MHD malloc() a copy that MHD_stop_daemon() has to free + again -- a leak this harness would notice immediately. */ + if (0 != (data[5] & 0x20)) + add_opt (opts, &nopt, + (0 != (data[5] & 0x40)) + ? MHD_OPTION_DIGEST_AUTH_RANDOM_COPY + : MHD_OPTION_DIGEST_AUTH_RANDOM, + (intptr_t) sizeof (digest_rnd), + (void *) (intptr_t) digest_rnd); + + /* A bind address is rejected together with MHD_USE_NO_LISTEN_SOCKET; + same reasoning as for the thread pool above. */ + if ( (0 != (data[5] & 0x80)) && + (cfg.listen_sock || misfit) ) + { + prepare_sock_addrs (); + /* The address family has to match MHD_USE_IPv6, otherwise MHD + rejects the daemon; that check is reached through the raw flag + escape rather than by feeding a mismatched address on purpose. */ +#ifdef AF_INET6 + if (0 != (flags & MHD_USE_IPv6)) + { + if (0 != (data[15] & 0x02)) + add_opt (opts, &nopt, MHD_OPTION_SOCK_ADDR_LEN, + (intptr_t) (socklen_t) sizeof (bind6), &bind6); + else + add_opt (opts, &nopt, MHD_OPTION_SOCK_ADDR, 0, &bind6); + } + else +#endif + if (0 != (data[15] & 0x02)) + add_opt (opts, &nopt, MHD_OPTION_SOCK_ADDR_LEN, + (intptr_t) (socklen_t) sizeof (bind4), &bind4); + else + add_opt (opts, &nopt, MHD_OPTION_SOCK_ADDR, 0, &bind4); + } + + /* An option number MHD does not know at all. parse_options_va() + answers MHD_NO for it and MHD_start_daemon() returns NULL, which is + the branch this reaches; keep it last so that everything above has + already been parsed. */ + if (0 != (mask_d & 0x40)) + add_opt (opts, &nopt, (enum MHD_OPTION) (200 + (data[15] & 0x0F)), + 0, NULL); + + opts[nopt].option = MHD_OPTION_END; + opts[nopt].value = 0; + opts[nopt].ptr_value = NULL; + return nopt; +} + + +/* The three callback options whose "not set" state is representable as a + NULL function pointer; MHD checks all three for NULL before calling + them, so passing NULL is exactly equivalent to omitting the option. */ +typedef void *(*fuzz_uri_log_cb)(void *, const char *, + struct MHD_Connection *); + +#define VARARG_CALLBACKS \ + MHD_OPTION_NOTIFY_COMPLETED, cb_completed, NULL, \ + MHD_OPTION_NOTIFY_CONNECTION, cb_notify, NULL, \ + MHD_OPTION_URI_LOG_CALLBACK, cb_uri_log, NULL + +/* The accept policy callback, defined with the accept path further + down. */ +static enum MHD_Result +apc_cb (void *cls, + const struct sockaddr *addr, + socklen_t addrlen); + + +/** + * Start the daemon. + * + * MHD_OPTION_EXTERNAL_LOGGER and MHD_OPTION_UNESCAPE_CALLBACK cannot be + * "passed as NULL": MHD calls both unconditionally, so their absence has + * to be expressed by leaving the option out of the varargs, which is why + * there are four spellings of the same call. The logger comes first on + * purpose -- MHD_OPTION_EXTERNAL_LOGGER only catches the messages + * emitted after it has been parsed, and parsing the option array emits + * several. + */ +static struct MHD_Daemon * +start_daemon_variant (unsigned int flags, + struct MHD_OptionItem *opts, + unsigned int cbsel) +{ + MHD_AcceptPolicyCallback apc = cfg.accept_policy ? &apc_cb : NULL; + MHD_RequestCompletedCallback cb_completed = + (0 != (cbsel & 0x01)) ? &completed_cb : NULL; + MHD_NotifyConnectionCallback cb_notify = + (0 != (cbsel & 0x02)) ? ¬ify_connection_cb : NULL; + fuzz_uri_log_cb cb_uri_log = + (0 != (cbsel & 0x04)) ? &uri_log_cb : NULL; + + switch ((cbsel >> 3) & 0x03) + { + case 1: + return MHD_start_daemon (flags, 0, apc, NULL, &ahc, NULL, + MHD_OPTION_ARRAY, opts, + VARARG_CALLBACKS, + MHD_OPTION_UNESCAPE_CALLBACK, &unescape_cb, NULL, + MHD_OPTION_END); + case 2: + return MHD_start_daemon (flags, 0, apc, NULL, &ahc, NULL, + MHD_OPTION_EXTERNAL_LOGGER, &logger_cb, NULL, + MHD_OPTION_ARRAY, opts, + VARARG_CALLBACKS, + MHD_OPTION_END); + case 3: + return MHD_start_daemon (flags, 0, apc, NULL, &ahc, NULL, + MHD_OPTION_EXTERNAL_LOGGER, &logger_cb, NULL, + MHD_OPTION_ARRAY, opts, + VARARG_CALLBACKS, + MHD_OPTION_UNESCAPE_CALLBACK, &unescape_cb, NULL, + MHD_OPTION_END); + default: + return MHD_start_daemon (flags, 0, apc, NULL, &ahc, NULL, + MHD_OPTION_ARRAY, opts, + VARARG_CALLBACKS, + MHD_OPTION_END); + } +} + + +/** + * The accept policy callback. MHD_accept_connection() calls it with the + * peer address of every socket it accepts and closes the connection + * again when it answers MHD_NO, which is a code path (and a whole + * function, new_connection_close_()) that nothing else here reaches. + */ +static enum MHD_Result +apc_cb (void *cls, + const struct sockaddr *addr, + socklen_t addrlen) +{ + volatile size_t sink; + + (void) cls; + sink = (size_t) addrlen + ((NULL != addr) ? (size_t) addr->sa_family : 0u); + (void) sink; + return cfg.accept_policy_deny ? MHD_NO : MHD_YES; +} + + +/** + * Really connect to the daemon's listening socket. + * + * MHD_add_connection() bypasses accept(), so without this the whole + * accept path -- MHD_accept_connection(), the accept policy callback, + * the listen-socket branches of the three event loops -- is unreachable. + * The client end is given SO_LINGER {1, 0} so that close() sends a RST + * and neither side ends up in TIME_WAIT: at fuzzing rates the ephemeral + * port range would otherwise be exhausted within a couple of minutes. + * + * @param d the daemon, which must have a listening socket + * @param flags the flags it was started with, to pick the address family + * @return the connected socket, or -1 + */ +static int +connect_real (struct MHD_Daemon *d, + unsigned int flags) +{ + const union MHD_DaemonInfo *di; + struct linger lg; + int s; + uint16_t port; + + di = MHD_get_daemon_info (d, MHD_DAEMON_INFO_BIND_PORT); + if ( (NULL == di) || + (0 == di->port) ) + return -1; + port = (uint16_t) di->port; + prepare_sock_addrs (); +#ifdef AF_INET6 + if (0 != (flags & MHD_USE_IPv6)) + { + struct sockaddr_in6 to = bind6; + + to.sin6_port = htons (port); + s = socket (AF_INET6, SOCK_STREAM, 0); + if (0 > s) + return -1; + if (0 != connect (s, (const struct sockaddr *) &to, sizeof (to))) + { + (void) close (s); + return -1; + } + } + else +#endif + { + struct sockaddr_in to = bind4; + + to.sin_port = htons (port); + s = socket (AF_INET, SOCK_STREAM, 0); + if (0 > s) + return -1; + if (0 != connect (s, (const struct sockaddr *) &to, sizeof (to))) + { + (void) close (s); + return -1; + } + } + lg.l_onoff = 1; + lg.l_linger = 0; + (void) setsockopt (s, SOL_SOCKET, SO_LINGER, &lg, sizeof (lg)); + stat_real_conns++; + return s; +} + + +/** + * Ask MHD_is_feature_supported() about one feature. The function is a + * large switch in daemon.c that no other harness touches; the index + * comes off the input so that invalid values reach its default branch + * as well. + */ +static void +query_feature (uint8_t sel) +{ + volatile int sink; + + sink = (int) MHD_is_feature_supported ((enum MHD_FEATURE) (sel % 40u)); + (void) sink; +} + + +/** + * Read everything MHD_get_daemon_info() offers. All of it lives in + * daemon.c and none of it is reachable from the other harnesses. + */ +static void +query_daemon_info (struct MHD_Daemon *d) +{ + volatile unsigned int sink = 0; + const union MHD_DaemonInfo *di; + + sink += (unsigned int) (NULL != MHD_get_version ()); + sink += MHD_get_version_bin (); + + di = MHD_get_daemon_info (d, MHD_DAEMON_INFO_CURRENT_CONNECTIONS); + if (NULL != di) + sink += di->num_connections; + di = MHD_get_daemon_info (d, MHD_DAEMON_INFO_FLAGS); + if (NULL != di) + sink += (unsigned int) di->flags; + di = MHD_get_daemon_info (d, MHD_DAEMON_INFO_BIND_PORT); + if (NULL != di) + sink += di->port; + di = MHD_get_daemon_info (d, MHD_DAEMON_INFO_LISTEN_FD); + if (NULL != di) + sink += (unsigned int) (di->listen_fd + 1); + di = MHD_get_daemon_info (d, MHD_DAEMON_INFO_EPOLL_FD); + if (NULL != di) + sink += (unsigned int) (di->listen_fd + 1); + (void) sink; +} + + +/* ------------------------------------------------------------------ */ +/* The fuzz target */ +/* ------------------------------------------------------------------ */ + +int +LLVMFuzzerTestOneInput (const uint8_t *data, + size_t size) +{ + struct MHD_Daemon *d; + struct MHD_OptionItem opts[MAX_OPTS]; + unsigned int flags; + int sock = -1; + int rsock = -1; + size_t pos; + unsigned int nseg = 0; + unsigned int nconn = 1; + unsigned int cbsel; + MHD_socket quiesced = MHD_INVALID_SOCKET; + + /* Must happen before the first write() into the socketpair, and must + not be left to the built-in driver: fuzz_install_handlers() is + compiled out under -DFUZZ_NO_MAIN, which is exactly the build every + external fuzzing engine uses. The call is idempotent. See + fuzz_ignore_sigpipe() in fuzz_common.h for what happens without it. */ + fuzz_ignore_sigpipe (); + + /* The sixteen configuration bytes are mandatory. An input of exactly + that length is fine and starts a daemon with no traffic. */ + if (size < CFG_BYTES) + return 0; + + memset (&cfg, 0, sizeof (cfg)); + memset (pending_resume, 0, sizeof (pending_resume)); + tearing_down = 0; + + flags = flags_from_input (data); + if (cfg.listen_sock) + flags &= ~((unsigned int) MHD_USE_NO_LISTEN_SOCKET); + else + flags |= MHD_USE_NO_LISTEN_SOCKET; + if (cfg.err_log) + flags |= MHD_USE_ERROR_LOG; + else + flags &= ~((unsigned int) MHD_USE_ERROR_LOG); + + cfg.uses_threads = (0 != (flags & (MHD_USE_INTERNAL_POLLING_THREAD + | MHD_USE_THREAD_PER_CONNECTION))); + if (cfg.uses_threads && + (0 == (flags & MHD_USE_NO_THREAD_SAFETY))) + { + /* Without the inter-thread communication channel a worker only + notices a connection handed to it by MHD_add_connection() when its + own poll times out, so every threaded iteration would pay the full + wait. MHD forces ITC on anyway whenever there is no listen + socket; this extends that to the daemons that have one. */ + flags |= MHD_USE_ITC; + } + + cfg.loop_mode = (unsigned int) (data[12] & 0x03); + cfg.quiesce = (0 != (data[12] & 0x04)); + if (cfg.quiesce && + cfg.uses_threads && + uses_epoll (flags) && + (! quiesce_epoll_race_allowed ())) + cfg.quiesce = 0; + cfg.daemon_info = (0 != (data[12] & 0x08)); + cfg.suspend_mode = (0 != (flags & SUSPEND_BIT)) + ? (int) ((data[12] >> 4) & 0x03) + : 0; + if (3 == cfg.suspend_mode) + cfg.suspend_mode = 1; + cfg.nconn_max = 1u + (unsigned int) ((data[12] >> 6) & 0x03); + if (cfg.nconn_max > MAX_CONNECTIONS) + cfg.nconn_max = MAX_CONNECTIONS; + + cfg.resp_kind = (unsigned int) (data[13] & 0x03); + cfg.handler_no = (0 != (data[13] & 0x04)); + cfg.conn_option = (0 != (data[13] & 0x08)); + cfg.accept_policy = (0 != (data[13] & 0x10)); + cfg.accept_policy_deny = (0 != (data[13] & 0x20)); + /* One iteration in four of those that have a listening socket at all; + a real connect()/accept() pair is much more expensive than + MHD_add_connection() on a socketpair. */ + cfg.real_connect = cfg.listen_sock && (0xC0 == (data[13] & 0xC0)); + + (void) build_options (data, opts, flags); + + cbsel = (unsigned int) data[5]; + /* MHD_USE_ERROR_LOG without MHD_OPTION_EXTERNAL_LOGGER sends every + message MHD produces to stderr through MHD_default_logger_, which + at fuzzing rates is tens of megabytes of noise. The flag is far too + valuable to drop -- with it set, several hundred MHD_DLOG() call + sites in daemon.c become live -- so the logger is forced on instead + (bit 4 of the callback selector). */ + if (cfg.err_log) + cbsel |= 0x10u; + + MHD_set_panic_func (&panic_cb, NULL); + d = start_daemon_variant (flags, opts, cbsel); + if (NULL == d) + { + /* Entirely normal: an unsupported flag combination, an option MHD + rejects, a bind() failure, ... */ + stat_daemons_failed++; + return 0; + } + stat_daemons++; + if (cfg.uses_threads) + stat_threaded++; + if (! stats_registered) + { + stats_registered = 1; + (void) atexit (&print_stats); + } + + if (cfg.daemon_info) + query_daemon_info (d); + query_feature (data[15]); + + if (cfg.real_connect) + { + rsock = connect_real (d, flags); + if (0 <= rsock) + { + static const char req[] = "GET / HTTP/1.1\r\nHost: x\r\n\r\n"; + + (void) send (rsock, req, sizeof (req) - 1, MSG_DONTWAIT); + if (cfg.uses_threads) + wait_threaded (rsock); + else + pump (d, rsock, PUMP_ROUNDS_LONG); + } + } + + (void) new_connection (d, &sock); + + pos = CFG_BYTES; + while ( (pos + 2 <= size) && + (nseg < MAX_SEGMENTS) ) + { + unsigned int hdr = (unsigned int) data[pos] + | ((unsigned int) data[pos + 1] << 8); + unsigned int op = hdr >> 14; + size_t slen = (size_t) (hdr & 0x3FFF); + + pos += 2; + nseg++; + if (slen > size - pos) + slen = size - pos; + + if ( (3 == op) && + (nconn < cfg.nconn_max) ) + { + close_connection (d, &sock); + (void) new_connection (d, &sock); + nconn++; + } + if (0 != slen) + send_all (d, sock, data + pos, slen); + pos += slen; + if (cfg.uses_threads) + continue; /* the threaded shape drains at close */ + pump (d, sock, (0 == op) ? PUMP_ROUNDS : PUMP_ROUNDS_LONG); + } + + close_connection (d, &sock); + pump (d, sock, 4); + if (0 <= rsock) + { + (void) shutdown (rsock, SHUT_WR); + if (cfg.uses_threads) + wait_threaded (rsock); + else + pump (d, rsock, 4); + (void) close (rsock); + rsock = -1; + } + + /* A connection left suspended makes MHD_stop_daemon() MHD_PANIC(), and + MHD_resume_connection() alone is not enough: it only raises a flag, + and the connection is taken off the daemon's suspended list by + MHD_run(). So flush and run until nothing is parked any more. + tearing_down keeps the handler from parking anything new, which is + what bounds this loop; the cap is only a backstop. */ + tearing_down = 1; + if (! cfg.uses_threads) + { + unsigned int i; + + for (i = 0; i < MAX_CONNECTIONS + 2u; i++) + { + if (! pending_resume_flush ()) + break; + (void) MHD_run (d); + } + } + + if (cfg.quiesce) + { + /* The returned socket belongs to the caller from here on, and with + internal threads it must not be closed before MHD_stop_daemon() + has joined them. */ + quiesced = MHD_quiesce_daemon (d); + } + MHD_stop_daemon (d); + if (MHD_INVALID_SOCKET != quiesced) + (void) close ((int) quiesced); + return 0; +} + + +/* ------------------------------------------------------------------ */ +/* Structure-aware generator */ +/* ------------------------------------------------------------------ */ + +/* Defined unconditionally, exactly like the rest of the harness API: + only main() may live behind #ifndef FUZZ_NO_MAIN, and fuzz_common.h + already declares these three with FUZZ_UNUSED so that the libFuzzer + and AFL++ builds, which never call them, compile without a warning. */ + +struct sbuf +{ + uint8_t *p; + size_t len; + size_t cap; +}; + + +static void +sb_raw (struct sbuf *b, + const void *s, + size_t n) +{ + if (b->len + n > b->cap) + n = b->cap - b->len; + memcpy (b->p + b->len, s, n); + b->len += n; +} + + +static void +sb_str (struct sbuf *b, + const char *s) +{ + sb_raw (b, s, strlen (s)); +} + + +static void +sb_u64 (struct sbuf *b, + uint64_t v, + int hex) +{ + char tmp[32]; + + (void) snprintf (tmp, sizeof (tmp), + hex ? "%llx" : "%llu", + (unsigned long long) v); + sb_str (b, tmp); +} + + +static const char *const gen_methods[] = { + "GET", "POST", "HEAD", "PUT", "OPTIONS", "DELETE", "TRACE", "CONNECT", + "PATCH", "get", "BREW", "" +}; + +static const char *const gen_targets[] = { + "/", "/a", "/a/b/c", "*", "http://x/a", "/%41%42", "/a?b=c&d", + "/very/long/path/that/does/not/fit/into/a/small/connection/memory/pool", + "/a?novalue", "//", "/.%2e/", "/\x01" +}; + +static const char *const gen_versions[] = { + "HTTP/1.1", "HTTP/1.0", "HTTP/1.2", "HTTP/0.9", "HTTP/1", "" +}; + +static const char *const gen_hdr_names[] = { + "Host", "Connection", "Accept", "User-Agent", "Cookie", "Expect", + "Content-Type", "X-Fuzz", "Upgrade", "Accept-Encoding", "Range", + "If-Modified-Since" +}; + +static const char *const gen_hdr_values[] = { + "x", "keep-alive", "close", "*/*", "a=b; c=d", "100-continue", + "text/plain", "1", "fuzz-protocol", "gzip", "bytes=0-1", "chunked" +}; + + +/** + * Emit one HTTP request into @a b. + * + * The shapes are deliberately unambitious -- the request parser is + * fuzz_request's subject, not this harness's. What matters here is that + * the bytes form something MHD will actually take through its state + * machine under whatever daemon configuration the configuration bytes + * describe, so that the option handling is exercised against a live + * connection rather than against a connection MHD drops on the first + * byte. + */ +static void +gen_request (struct fuzz_rng *rng, + struct sbuf *b, + unsigned int shape) +{ + unsigned int nhdr; + unsigned int i; + + switch (shape % 10) + { + case 9: + /* Not HTTP at all: MHD has to reject it, which is its own path. */ + for (i = 0; i < 24; i++) + { + uint8_t c = fuzz_byte (rng); + + sb_raw (b, &c, 1); + } + return; + case 6: + /* Two pipelined requests on one connection. */ + sb_str (b, "GET /a HTTP/1.1\r\nHost: x\r\n\r\n"); + sb_str (b, "GET /b HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n"); + return; + default: + break; + } + + sb_str (b, gen_methods[fuzz_below (rng, (uint32_t) (sizeof (gen_methods) + / sizeof (gen_methods[0] + )))]); + sb_str (b, " "); + sb_str (b, gen_targets[fuzz_below (rng, (uint32_t) (sizeof (gen_targets) + / sizeof (gen_targets[0] + )))]); + sb_str (b, " "); + sb_str (b, gen_versions[fuzz_below (rng, + (uint32_t) (sizeof (gen_versions) + / sizeof (gen_versions[0]))) + ]); + sb_str (b, "\r\n"); + + nhdr = (5 == (shape % 10)) ? (8 + fuzz_below (rng, 24)) : fuzz_below (rng, 5); + for (i = 0; i < nhdr; i++) + { + sb_str (b, gen_hdr_names[fuzz_below (rng, + (uint32_t) (sizeof (gen_hdr_names) + / sizeof (gen_hdr_names[0] + )))]); + sb_str (b, ": "); + sb_str (b, gen_hdr_values[fuzz_below (rng, + (uint32_t) (sizeof (gen_hdr_values) + / sizeof (gen_hdr_values[ + 0])))]); + sb_str (b, "\r\n"); + } + + switch (shape % 10) + { + case 2: /* Content-Length body */ + { + uint32_t n = fuzz_below (rng, 64); + + sb_str (b, "Content-Length: "); + sb_u64 (b, n, 0); + sb_str (b, "\r\n\r\n"); + for (i = 0; i < n; i++) + sb_str (b, "A"); + break; + } + case 3: /* chunked body */ + { + unsigned int nch = 1 + fuzz_below (rng, 3); + + sb_str (b, "Transfer-Encoding: chunked\r\n\r\n"); + for (i = 0; i < nch; i++) + { + uint32_t n = 1 + fuzz_below (rng, 16); + uint32_t k; + + sb_u64 (b, n, 1); + sb_str (b, "\r\n"); + for (k = 0; k < n; k++) + sb_str (b, "B"); + sb_str (b, "\r\n"); + } + sb_str (b, "0\r\n\r\n"); + break; + } + case 4: /* expect 100-continue */ + sb_str (b, "Expect: 100-continue\r\nContent-Length: 4\r\n\r\nabcd"); + break; + case 7: /* upgrade request */ + sb_str (b, "Connection: Upgrade\r\nUpgrade: fuzz-protocol\r\n\r\n"); + break; + case 8: /* truncated: MHD keeps waiting for more */ + sb_str (b, "X-Trunc: "); + break; + default: + sb_str (b, "\r\n"); + break; + } +} + + +/** + * Serialise @a body into the segment stream understood by + * LLVMFuzzerTestOneInput(). + */ +static void +emit_segments (struct fuzz_rng *rng, + struct sbuf *out, + const uint8_t *body, + size_t body_len, + int new_conn_first) +{ + size_t off = 0; + int first = 1; + + while (off < body_len) + { + size_t chunk; + unsigned int op; + uint8_t hdr[2]; + unsigned int hv; + + switch (fuzz_below (rng, 5)) + { + case 0: + chunk = 1; + break; + case 1: + chunk = 2 + fuzz_below (rng, 8); + break; + default: + chunk = body_len - off; + break; + } + if (chunk > body_len - off) + chunk = body_len - off; + if (chunk > 0x3FFF) + chunk = 0x3FFF; + op = (first && new_conn_first) ? 3u : (fuzz_chance (rng, 5) ? 2u : 0u); + hv = (op << 14) | (unsigned int) chunk; + hdr[0] = (uint8_t) (hv & 0xFF); + hdr[1] = (uint8_t) (hv >> 8); + if (out->len + 2 + chunk > out->cap) + return; + sb_raw (out, hdr, 2); + sb_raw (out, body + off, chunk); + off += chunk; + first = 0; + } +} + + +/** + * The generator. + * + * Purely random configuration bytes are already useful for this harness + * -- unlike an HTTP request, an option array has no grammar to get wrong + * -- but two things still need help. Byte 0 is biased into the range of + * mode_tbl[] so that most iterations get a daemon that actually starts, + * and the tail of the input has to look like HTTP, or MHD closes every + * connection before any of the configured behaviour has a chance to + * matter. + */ +static size_t +fuzz_generate (struct fuzz_rng *rng, + uint8_t *buf, + size_t cap) +{ + struct sbuf out; + uint8_t cfg_bytes[CFG_BYTES]; + uint8_t req[GEN_BUF_SIZE]; + struct sbuf rb; + unsigned int nreq; + unsigned int i; + + out.p = buf; + out.len = 0; + out.cap = cap; + + for (i = 0; i < CFG_BYTES; i++) + cfg_bytes[i] = fuzz_byte (rng); + + /* Byte 0 selects the daemon shape; keep it inside the table so that + the value is not folded by the modulo in an uneven way. */ + cfg_bytes[0] = (uint8_t) fuzz_below (rng, (uint32_t) MODE_COUNT); + /* The raw flag escape is 1 in 4 of the byte-2 values, which is far too + often: those daemons mostly fail to start and never reach the option + handling. Clear the escape unless it is explicitly rolled. */ + if (! fuzz_chance (rng, 10)) + cfg_bytes[2] &= (uint8_t) ~0xC0u; + else + cfg_bytes[2] |= (uint8_t) 0xC0u; + /* Likewise the deliberately invalid option number: useful, but every + input carrying it ends in MHD_start_daemon() == NULL. */ + if (! fuzz_chance (rng, 20)) + cfg_bytes[14] &= (uint8_t) ~0x40u; + + sb_raw (&out, cfg_bytes, CFG_BYTES); + + nreq = 1u + (fuzz_chance (rng, 4) ? 1u : 0u); + for (i = 0; i < nreq; i++) + { + rb.p = req; + rb.len = 0; + rb.cap = sizeof (req); + gen_request (rng, &rb, fuzz_below (rng, 10)); + emit_segments (rng, &out, req, rb.len, + (0 != i) || fuzz_chance (rng, 6)); + } + return out.len; +} + + +/* ------------------------------------------------------------------ */ +/* Built-in seed corpus */ +/* ------------------------------------------------------------------ */ + +/** + * A seed is sixteen configuration bytes plus one request, rendered into + * the wire format at run time so that segment lengths never have to be + * spelled out by hand. + * + * Every seed turns on exactly one area, so that a corpus minimiser keeps + * them distinguishable, and the configuration bytes of a seed that does + * not care about an area are zero -- which is the plainest setting: + * external polling with select(), no listen socket, no options at all, + * one connection, MHD_run() as the event loop. + */ +struct seed_def +{ + const char *name; + unsigned char cfg[CFG_BYTES]; + const char *req; +}; + +/* Byte positions inside seed_def::cfg, for readability. */ +#define C_MODE 0 +#define C_FLAGA 1 +#define C_FLAGB 2 +#define C_OPTA 3 +#define C_OPTB 4 +#define C_OPTC 5 +#define C_VAL_MEM 6 +#define C_VAL_CONN 7 +#define C_VAL_THR 8 +#define C_VAL_DAUTH 9 +#define C_VAL_DISC 10 +#define C_VAL_SOCK 11 +#define C_DRIVE 12 +#define C_HANDLER 13 +#define C_OPTD 14 +#define C_SPARE 15 + +#define REQ_PLAIN "GET /a HTTP/1.1\r\nHost: x\r\n\r\n" +#define REQ_CLOSE "GET /a HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n" +#define REQ_POST \ + "POST /a HTTP/1.1\r\nHost: x\r\nContent-Length: 4\r\n\r\nabcd" +#define REQ_CHUNKED \ + "POST /a HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\n" \ + "\r\n3\r\nabc\r\n0\r\n\r\n" + +static const struct seed_def seeds[] = { + /* ---- the event loops, all external ---- */ + { "plain-external-run", + { 0 }, REQ_PLAIN }, + { "external-fdset2", + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01 | 0x08, 0, 0, 0 }, + REQ_PLAIN }, + { "external-run-wait", + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x02, 0, 0, 0 }, + REQ_PLAIN }, + { "external-fdset-v1", + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x03, 0, 0, 0 }, + REQ_PLAIN }, + { "external-epoll", + { 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + { "external-auto", + { 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + + /* ---- the daemon shapes that run their own threads ---- */ + { "internal-thread-select", + { 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_CLOSE }, + { "internal-thread-poll", + { 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_CLOSE }, + { "internal-thread-epoll", + { 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_CLOSE }, + { "thread-per-connection", + { 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_CLOSE }, + { "thread-per-connection-poll", + { 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_CLOSE }, + /* A thread pool needs MHD_OPTION_THREAD_POOL_SIZE (option mask A bit + 5) on top of an internal polling thread, and is rejected outright + with thread-per-connection. */ + { "thread-pool", + { 18, 0, 0, 0x20, 0, 0, 0, 0, 0x00, 0, 0, 0, 0, 0, 0, 0 }, + REQ_CLOSE }, + { "thread-pool-epoll-stack", + { 20, 0, 0, 0x20 | 0x40, 0, 0, 0, 0, 0x10 | 0x20, 0, 0, 0, 0, 0, 0, 0 }, + REQ_CLOSE }, + + /* ---- a real listening socket ---- */ + { "listen-socket", + { 0, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + { "listen-socket-reuse-backlog", + { 0, 0, 0x01, 0x80, 0x01, 0, 0, 0, 0, 0, 0, 0x01 | 0x06, 0, 0, 0, 0 }, + REQ_PLAIN }, + { "listen-socket-sockaddr", + { 0, 0, 0x01, 0, 0, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + { "listen-socket-ipv6-dual", + { 0, 0, 0x01 | 0x02 | 0x04, 0, 0, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + { "listen-socket-quiesce", + { 0, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x04 | 0x08, 0, 0, 0 }, + REQ_PLAIN }, + { "listen-socket-internal-thread", + { 18, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x04, 0, 0, 0 }, + REQ_CLOSE }, + + /* ---- the accept() path: a real client on the listening socket ---- + Byte 13 bit 0x40|0x80 asks for the connect(), bit 0x10 installs the + accept policy callback and bit 0x20 makes it say no. Without these + MHD_accept_connection() and the listen-socket branches of the three + event loops are never entered at all: MHD_add_connection() bypasses + accept() completely. */ + { "real-connect", + { 0, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xC0, 0, 0 }, + REQ_PLAIN }, + { "real-connect-accept-policy", + { 0, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xC0 | 0x10, 0, 0 }, + REQ_PLAIN }, + { "real-connect-accept-denied", + { 0, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xC0 | 0x10 | 0x20, 0, 0 }, + REQ_PLAIN }, + { "real-connect-internal-thread", + { 18, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xC0, 0, 0 }, + REQ_CLOSE }, + { "real-connect-external-epoll", + { 12, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xC0, 0, 0 }, + REQ_PLAIN }, + + /* ---- the memory options ---- */ + { "tiny-pool", + { 0, 0, 0, 0x01 | 0x02, 0, 0, 0x01 | 0x10, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + { "big-pool-big-increment", + { 0, 0, 0, 0x01 | 0x02, 0, 0, 0x0C | 0x60, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_POST }, + + /* ---- the connection limits ---- */ + { "connection-limit-one", + { 0, 0, 0, 0x04, 0, 0, 0, 0x01, 0, 0, 0, 0, 0xC0, 0, 0, 0 }, + REQ_PLAIN }, + { "per-ip-limit-one", + { 0, 0, 0, 0x08, 0, 0, 0, 0x10, 0, 0, 0, 0, 0xC0, 0, 0, 0 }, + REQ_PLAIN }, + { "connection-limit-zero", + { 0, 0, 0, 0x04, 0, 0, 0, 0x00, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + { "connection-timeout", + { 0, 0, 0, 0x10, 0, 0, 0, 0, 0x01, 0, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + + /* ---- parsing discipline and insanity ---- */ + { "discipline-lowest", + { 0, 0, 0, 0, 0x10, 0, 0, 0, 0, 0, 0x00, 0, 0, 0, 0, 0 }, + "GET /a HTTP/1.1\r\n Host: x\r\n\r\n" }, + { "discipline-highest-pedantic", + { 0, 0x01, 0, 0, 0x10, 0, 0, 0, 0, 0, 0x05, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + { "strict-for-client", + { 0, 0, 0, 0, 0x08, 0, 0, 0, 0, 0, 0x00, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + { "server-insanity", + { 0, 0, 0, 0, 0x04, 0, 0, 0, 0, 0, 0x40, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + { "bin-zero-in-uri-path", + { 0, 0, 0, 0, 0x80, 0, 0, 0, 0, 0, 0, 0x20, 0, 0, 0, 0 }, + "GET /a%00b HTTP/1.1\r\nHost: x\r\n\r\n" }, + { "app-fd-setsize", + { 0, 0, 0, 0, 0x40, 0, 0, 0, 0, 0, 0, 0x00, 0x01, 0, 0, 0 }, + REQ_PLAIN }, + + /* ---- the callbacks ---- */ + { "notify-completed", + { 0, 0, 0, 0, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + { "notify-connection", + { 0, 0, 0, 0, 0, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + { "uri-log-callback", + { 0, 0, 0, 0, 0, 0x04, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + "GET /a?q=%41 HTTP/1.1\r\nHost: x\r\n\r\n" }, + { "external-logger", + { 0, 0x40, 0, 0, 0, 0x10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + { "unescape-callback", + { 0, 0, 0, 0, 0, 0x08, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + "GET /%41%42?a=%43 HTTP/1.1\r\nHost: x\r\n\r\n" }, + { "all-callbacks-error-log", + { 0, 0x40, 0, 0, 0, 0x01 | 0x02 | 0x04 | 0x08 | 0x10, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + + /* ---- digest-auth related options ---- */ + { "digest-random", + { 0, 0, 0, 0, 0x02, 0x20, 0, 0, 0, 0x02, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + { "digest-random-copy", + { 0, 0, 0, 0, 0x02, 0x20 | 0x40, 0, 0, 0, 0x01, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + { "digest-nonce-bind-and-defaults", + { 0, 0, 0, 0, 0, 0x20, 0, 0, 0, 0x3C, 0, 0, 0, 0, + 0x01 | 0x02 | 0x04, 0x11 }, + REQ_PLAIN }, + + /* ---- suspend / resume ---- */ + { "suspend-immediate", + { 0, 0x04, 0, 0, 0, 0x01, 0, 0, 0, 0, 0, 0, 0x10, 0, 0, 0 }, + REQ_PLAIN }, + { "suspend-deferred", + { 0, 0x04, 0, 0, 0, 0x01, 0, 0, 0, 0, 0, 0, 0x20, 0, 0, 0 }, + REQ_PLAIN }, + { "suspend-deferred-two-connections", + { 0, 0x04, 0, 0, 0, 0x01, 0, 0, 0, 0, 0, 0, 0x20 | 0x40, 0, 0, 0 }, + REQ_PLAIN }, + + /* ---- the remaining flag bits ---- */ + { "turbo-itc-suppress-date", + { 0, 0x02 | 0x10 | 0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + { "allow-upgrade", + { 0, 0x08, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + "GET / HTTP/1.1\r\nHost: x\r\nConnection: Upgrade\r\n" + "Upgrade: fuzz-protocol\r\n\r\n" }, + { "no-thread-safety", + { 0, 0, 0x10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + { "tcp-fastopen-listen", + { 0, 0, 0x01 | 0x08, 0, 0, 0, 0, 0, 0, 0, 0, 0x40, 0, 0, 0x08, 0 }, + REQ_PLAIN }, + { "sigpipe-handled-by-app", + { 0, 0, 0, 0, 0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + { "listen-socket-option-invalid-fd", + { 0, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x20, 0 }, + REQ_PLAIN }, + + /* ---- inputs whose daemon must not start ---- */ + /* Raw flag escape: MHD_USE_POLL together with MHD_USE_EPOLL. */ + { "raw-flags-poll-and-epoll", + { 0, 0x40, 0xC0 | 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + /* Raw flag escape: MHD_USE_EPOLL with MHD_USE_THREAD_PER_CONNECTION. */ + { "raw-flags-epoll-thread-per-conn", + { 0, 0x04 | 0x08, 0xC0 | 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + /* An option number MHD does not know. */ + { "invalid-option-number", + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x40, 0 }, + REQ_PLAIN }, + /* MHD_OPTION_THREAD_POOL_SIZE without an internal polling thread. */ + { "thread-pool-without-threads", + { 0, 0, 0, 0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + REQ_PLAIN }, + + /* ---- the handler behaviours ---- */ + { "handler-returns-no", + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x04, 0, 0 }, + REQ_PLAIN }, + { "empty-and-copied-responses", + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01 | 0x08, 0, 0 }, + REQ_CHUNKED }, + { "error-response", + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x03, 0, 0 }, + REQ_POST }, + + /* ---- everything at once ---- */ + { "kitchen-sink-external", + { 0, 0x02 | 0x04 | 0x10 | 0x20 | 0x40, 0x01, + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x80, + 0x02 | 0x10 | 0x20 | 0x40 | 0x80, + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20, + 0x63, 0x45, 0x21, 0x13, 0x02, 0x0A, + 0x01 | 0x04 | 0x08 | 0x20, 0x08, 0x01 | 0x02 | 0x04 | 0x10, 0x33 }, + REQ_POST }, + { "kitchen-sink-thread-pool", + { 18, 0x02 | 0x10 | 0x20 | 0x40, 0x01, + 0x01 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40 | 0x80, + 0x01 | 0x02 | 0x80, + 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20, + 0x03, 0x46, 0x39, 0x02, 0x00, 0x0A, + 0x04 | 0x08, 0x00, 0x01 | 0x08, 0x22 }, + REQ_CLOSE } +}; + +static uint8_t seed_render_buf[1024]; + + +static size_t +fuzz_seed_count (void) +{ + return sizeof (seeds) / sizeof (seeds[0]); +} + + +static const uint8_t * +fuzz_seed_get (size_t idx, + size_t *len) +{ + const struct seed_def *sd = &seeds[idx]; + struct sbuf b; + + b.p = seed_render_buf; + b.len = 0; + b.cap = sizeof (seed_render_buf); + sb_raw (&b, sd->cfg, CFG_BYTES); + if (NULL != sd->req) + { + size_t n = strlen (sd->req); + unsigned int hv; + uint8_t hdr[2]; + + if (n > 0x3FFF) + n = 0x3FFF; + hv = (0u << 14) | (unsigned int) n; + hdr[0] = (uint8_t) (hv & 0xFF); + hdr[1] = (uint8_t) (hv >> 8); + sb_raw (&b, hdr, 2); + sb_raw (&b, sd->req, n); + } + *len = b.len; + return seed_render_buf; +} diff --git a/src/fuzz/fuzz_tls.c b/src/fuzz/fuzz_tls.c @@ -0,0 +1,2381 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. + If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * @file fuzz/fuzz_tls.c + * @brief In-process fuzzer for MHD's HTTPS/TLS integration layer. + * @author Christian Grothoff + * + * The target is **MHD's own TLS plumbing**, not GnuTLS. GnuTLS has its + * own OSS-Fuzz project and its record parser is not our bug surface, so + * only a small part of the input budget is spent throwing raw bytes at a + * TLS socket. What this harness really exercises is + * + * - the @c MHD_USE_TLS daemon option surface: #MHD_OPTION_HTTPS_MEM_KEY, + * #MHD_OPTION_HTTPS_MEM_CERT, #MHD_OPTION_HTTPS_MEM_TRUST, + * #MHD_OPTION_HTTPS_MEM_DHPARAMS, #MHD_OPTION_HTTPS_PRIORITIES, + * #MHD_OPTION_HTTPS_PRIORITIES_APPEND, #MHD_OPTION_HTTPS_CRED_TYPE, + * #MHD_OPTION_HTTPS_KEY_PASSWORD, #MHD_OPTION_TLS_NO_ALPN and the SNI + * callback #MHD_OPTION_HTTPS_CERT_CALLBACK -- with malformed PEM + * blobs, mismatched key/certificate pairs, bogus priority strings, + * credential types MHD does not support, and an SNI callback that + * fails or answers with garbage; + * - the handshake state machine of connection_https.c + * (#MHD_TLS_CONN_INIT -> HANDSHAKING -> CONNECTED / TLS_FAILED) and + * what MHD does when the handshake fails, is abandoned half way, or + * the peer disappears while MHD is still in it; + * - the transition from the handshake to the ordinary HTTP parser, and + * the TLS receive/send adapters and @c gnutls_bye() shutdown path that + * the connection uses afterwards. + * + * To reach that last group the harness contains a real, in-process GnuTLS + * *client*. Both ends of an @c AF_UNIX @c socketpair() are non-blocking + * and everything runs in one thread: the client's @c gnutls_handshake() + * is called until it answers @c GNUTLS_E_AGAIN, then the daemon is pumped + * with MHD_run(), and so on. No threads, no ports, no TCP stack, and -- + * because the client's handshake timeout is set to + * @c GNUTLS_INDEFINITE_TIMEOUT -- no wall clock either. + * + * Input format (see README): + * + * byte 0 certificate/key pair selector (valid, mismatched, truncated, + * garbage, absent, or built from the fuzzer's own bytes) + * byte 1 which TLS daemon options to pass at all (bitmask) + * 0x01 MHD_OPTION_HTTPS_MEM_TRUST + * 0x02 MHD_OPTION_HTTPS_MEM_DHPARAMS + * 0x04 MHD_OPTION_HTTPS_CERT_CALLBACK (the SNI callback) + * 0x08 MHD_OPTION_HTTPS_PRIORITIES[_APPEND] + * 0x10 MHD_OPTION_HTTPS_CRED_TYPE + * 0x20 MHD_OPTION_TLS_NO_ALPN + * 0x40 MHD_OPTION_HTTPS_KEY_PASSWORD + * 0x80 use _PRIORITIES_APPEND instead of _PRIORITIES + * byte 2 priority string selector + * byte 3 bits 0-2 credential type (certificate, PSK, anon, SRP, IA, + * and two values GnuTLS does not define) + * bits 3-5 SNI callback behaviour + * bit 6 trust blob: CA certificate or garbage + * bit 7 DH parameters: valid or garbage + * byte 4 bits 0-1 client mode + * 0 raw bytes, 1 real TLS client, + * 2 real client, handshake abandoned half way, + * 3 raw bytes shaped like TLS records + * bit 2 client sends a server name (SNI) + * bit 3 client presents a client certificate + * bits 4-5 client priority string selector + * bit 6 gnutls_bye() before closing + * bit 7 shutdown(SHUT_WR) before closing + * byte 5 low nibble MHD_OPTION_CONNECTION_MEMORY_LIMIT selector + * high nibble event loop: MHD_run() vs MHD_get_fdset*() + + * MHD_run_from_select*() + * byte 6 handler behaviour and introspection + * bits 0-1 response constructor + * bit 2 MHD_get_connection_info() for the TLS members + * bit 3 MHD_get_daemon_info() + * bit 4 MHD_set_connection_option() + * bit 5 answer 403 instead of 200 + * bit 6 add a response header + * bit 7 MHD_quiesce_daemon() before stopping + * byte 7 bits 0-2 handshake round budget (client mode 2) + * bit 3 pass the HTTPS options to a daemon started *without* + * MHD_USE_TLS + * bit 4 MHD_ALLOW_UPGRADE + * bits 5-7 extra pump rounds + * byte 8 how many bytes of the segment stream are spliced into the + * fuzzer-built PEM blobs (see cred_tbl entries 12 and 13) + * byte 9 bits 0-2 the server name the client presents, as an index + * into a small built-in table; an op 1 segment overrides it + * byte 10. a sequence of segments, each introduced by a little-endian + * 16 bit header (op << 14) | length + * op 0 send the payload + * op 1 the payload is the server name the *next* client + * connection presents; nothing is sent + * op 2 send the payload and pump the daemon extra rounds + * op 3 close the connection, open a fresh one (which means + * a fresh handshake), then send + * + * All ten configuration bytes are mandatory; a shorter input is + * rejected. + * + * The segment encoding is byte-for-byte the one fuzz_request uses, and + * `corpus/` is shared by every harness in this directory (see README + * section 3), so the seeds below deliberately contain **no op 1 + * segment**: fuzz_request reads op 1 as the declared decoded request + * body of its own body oracle, and would report a spurious finding when + * it replays a fuzz_tls seed. That is what byte 9 is for. Inputs the + * generator or a mutator produces may use op 1 freely -- they never end + * up in `corpus/`. + * + * The certificates and keys are the ones from + * `src/testcurl/https/tls_test_keys.h` (the CA certificate, the + * CA-signed server certificate with its key, and the self-signed server + * certificate with its key), reproduced verbatim so that this harness + * stays a single translation unit. The DH parameters are RFC 3526 + * group 14, as emitted by `certtool --get-dh-params --sec-param medium`. + * + * The whole harness is guarded by #HTTPS_SUPPORT: `contrib/oss-fuzz/ + * build.sh` configures `--disable-https` (that is what makes the + * MemorySanitizer build possible), and in such a build this file must + * still compile to a valid, trivially passing fuzz target. + */ + +#define FUZZ_HARNESS_NAME "fuzz_tls" +#include "fuzz_common.h" + +/* Pulls in MHD_config.h, which is where HTTPS_SUPPORT is defined. It + must come before <microhttpd.h>: it also settles FD_SETSIZE. */ +#include "mhd_options.h" + +#ifdef HTTPS_SUPPORT + +#include <microhttpd.h> +#include <gnutls/gnutls.h> +#include <gnutls/abstract.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <sys/select.h> +#include <arpa/inet.h> + +/** + * LeakSanitizer suppressions, compiled into the harness so that they + * cannot be forgotten on the command line. + * + * There is exactly one entry and it is *not* about MHD. GnuTLS 3.8.9 + * leaks the partially parsed certificate when a PEM certificate blob + * ends before its "-----END CERTIFICATE-----" line: + * + * gnutls_certificate_allocate_credentials (&cred); + * gnutls_certificate_set_x509_key_mem (cred, &truncated_cert, + * &good_key, + * GNUTLS_X509_FMT_PEM); + * -> GNUTLS_E_BASE64_UNEXPECTED_HEADER_ERROR (-207) + * gnutls_certificate_free_credentials (cred); + * + * leaks 9672 bytes in 57 allocations with no libmicrohttpd in the + * picture at all (reproduced with a 40 line program). MHD passes the + * blob straight through and frees the credentials on every path, so + * there is nothing for MHD to fix and nothing for this harness to find; + * GnuTLS is fuzzed separately by its own OSS-Fuzz project. + * + * The suppression matches only allocations made *inside* GnuTLS's + * certificate parser. A credential object leaked by MHD itself is a + * direct leak from gnutls_certificate_allocate_credentials() and is + * still reported, as is every other MHD allocation. + */ +const char * +__lsan_default_suppressions (void); + +const char * +__lsan_default_suppressions (void) +{ + return "leak:gnutls_x509_crt_init\n"; +} + + +#define MAX_SEGMENTS 48 +#define MAX_CONNECTIONS 3 +#define RESP_BUF_SIZE 16384 +#define GEN_BUF_SIZE 4096 +#define MAX_SNI_LEN 255 +#define FUZZ_PEM_BODY_MAX 1024 + +/** Number of client priority strings offered to the input. */ +#define CLIENT_PRIO_COUNT 4 + +/** + * Upper bound on the number of client/server round trips spent on one + * handshake. A TLS 1.3 handshake over a socketpair needs a handful; the + * cap only has to guarantee termination. + */ +#define HANDSHAKE_ROUNDS 64 + + +/* ------------------------------------------------------------------ */ +/* Test credentials */ +/* ------------------------------------------------------------------ */ + +/* + * Taken verbatim from src/testcurl/https/tls_test_keys.h (the CA + * certificate, the CA-signed server certificate and its key, and the + * self-signed server certificate and its key), so that this harness + * stays a single translation unit. dh_params_pem is RFC 3526 group 14, + * as emitted by "certtool --get-dh-params --sec-param medium". + */ + +static const char ca_cert_pem[] = + "-----BEGIN CERTIFICATE-----\n" + "MIIGITCCBAmgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgTELMAkGA1UEBhMCUlUx\n" + "DzANBgNVBAgMBk1vc2NvdzEPMA0GA1UEBwwGTW9zY293MRswGQYDVQQKDBJ0ZXN0\n" + "LWxpYm1pY3JvaHR0cGQxITAfBgkqhkiG9w0BCQEWEm5vYm9keUBleGFtcGxlLm9y\n" + "ZzEQMA4GA1UEAwwHdGVzdC1DQTAgFw0yMTA0MDcxNzM2MThaGA8yMTIxMDMxNDE3\n" + "MzYxOFowgYExCzAJBgNVBAYTAlJVMQ8wDQYDVQQIDAZNb3Njb3cxDzANBgNVBAcM\n" + "Bk1vc2NvdzEbMBkGA1UECgwSdGVzdC1saWJtaWNyb2h0dHBkMSEwHwYJKoZIhvcN\n" + "AQkBFhJub2JvZHlAZXhhbXBsZS5vcmcxEDAOBgNVBAMMB3Rlc3QtQ0EwggIiMA0G\n" + "CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDdaWupA4qZjCBNkJoJOm5xnCaizl36\n" + "ZLUwp4xBL/YfXPWE3LkmAREiVI/YnAb8l6G7CJnz8dTsOJWkNXG6T1KVP5/2RvBI\n" + "IaaaufRIAl7hEnj1j9E2hQlV2fxF2ZNhz+nqi0LqKV4LJSpclkXADf2FA9HsVRP/\n" + "B7zYh+DP0fSU8V6bsu8XCeRGshroAPrc8rH8lFEEXpNLNIqQr8yKx6SmdB6hfja6\n" + "6SQ0++qBhl0aJtn4LHWZohgjBmkIaGFPYIJLgxQ/xyp2Grz2q7lGKJ+zBkBF8iOP\n" + "t3x+F1hSCBnr/DGYWmjEm5tYm+7pyuriPddXdCc8+qa2LxMZo3EXxLo5YISpPCyw\n" + "Z7V3YAOZTr3m1C24LiYvPehCq1CTIkhhmqtlVJXU7ISD48cx9y+5Pi34wtbTI/gN\n" + "x4voyTLAfyavKMmIpxxIRsWldiF2n06HdvCRVdihDQUad10ygTmWf1J/s2ZETAtH\n" + "QaSd7MD389t6nQFtTIXigsNKnnDPlrtxt7rOLvLQeR0K04Gzrf/scheOanRAfOXH\n" + "KNBFU7YkDFG8rqizlC65rx9qeXFYXQcHZTuqxK7tgZnSgJat3E70VbTSCsEEG7eR\n" + "bNX/fChUKAIIpWaiW6HDlKLl6m2y+BzM91umBsKOqTvntMVFBSF9pVYlXK854aIR\n" + "q8A2Xujd012seQIDAQABo4GfMIGcMAsGA1UdDwQEAwICpDASBgNVHRMBAf8ECDAG\n" + "AQH/AgEBMB0GA1UdDgQWBBRYdUPApWoxw4U13Rqsjf9AHdbpLDATBgNVHSUEDDAK\n" + "BggrBgEFBQcDATAkBglghkgBhvhCAQ0EFxYVVGVzdCBsaWJtaWNyb2h0dHBkIENB\n" + "MB8GA1UdIwQYMBaAFFh1Q8ClajHDhTXdGqyN/0Ad1uksMA0GCSqGSIb3DQEBCwUA\n" + "A4ICAQBvrrcTKVeI1EYnXo4BQD4oCvf9z1fYQmL21EbHwgjg1nmaPkvStgWAc5p1\n" + "kKwySrpEMKXfu68X76RccXZyWWIamEjz2OCWYZgjX6d6FpjhLphL8WxXDy5C9eay\n" + "ixN7+URz2XQoi22wqR+tCPDhrIzcMPyMkx/6gRgcYeDnaFrkdSeSsKsID4plfcIj\n" + "ISWJDvv+IAgrtsG1NVHnGwpAv0od3A8/4/fR6PPyewaU3aydvjZ7Au8O9DGDjlU9\n" + "9HdlOkkY6GVJ1pfGZib7cV7lhy0D2kj1g9xZh97YjpoUfppPl9r+6A8gDm0hXlAD\n" + "TlzNYlwTb681ZEoSd9PiLEY8HETssHlays2dYXdcNwAEp69iIHz8q1Q98Be9LScl\n" + "WEzgaOT9U7lpIw/MWbELoMsC+Ecs1cVWBIuiIq8aSG2kRr1x3S8yVXbAohAXif2s\n" + "E6puieM/VJ25iaNhkbLmDkk58QVVmn9NZNv6ETxuSQMp9e0EwbVlj68vzClQ91Y/\n" + "nmAiGcLFUEwB9G0szv9+vR+oDW4IkvdFZSUbcICd2cnynnwAD395onqS4hEZO1xM\n" + "Gy5ZldbTMTjgn7fChNopz15ChPBnwFIjhm+S0CyiLRQAowfknRVq2IBkj7/5kOWg\n" + "4mcxcq76HoQWK/8X/8RFL1eFVAvY7TNHYJ0RS51DMuwCNQictA==\n" + "-----END CERTIFICATE-----\n"; + + +static const char srv_signed_key_pem[] = + "-----BEGIN PRIVATE KEY-----\n" + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCff7amw9zNSE+h\n" + "rOMhBrzbbsJluUP3gmd8nOKY5MUimoPkxmAXfp2L0il+MPZT/ZEmo11q0k6J2jfG\n" + "UBQ+oZW9ahNZ9gCDjbYlBblo/mqTai+LdeLO3qk53d0zrZKXvCO6sA3uKpG2WR+g\n" + "+sNKxfYpIHCpanqBU6O+degIV/+WKy3nQ2Fwp7K5HUNj1u0pg0QQ18yf68LTnKFU\n" + "HFjZmmaaopWki5wKSBieHivzQy6w+04HSTogHHRK/y/UcoJNSG7xnHmoPPo1vLT8\n" + "CMRIYnSSgU3wJ43XBJ80WxrC2dcoZjV2XZz+XdQwCD4ZrC1ihykcAmiQA+sauNm7\n" + "dztOMkGzAgMBAAECggEAIbKDzlvXDG/YkxnJqrKXt+yAmak4mNQuNP+YSCEdHSBz\n" + "+SOILa6MbnvqVETX5grOXdFp7SWdfjZiTj2g6VKOJkSA7iKxHRoVf2DkOTB3J8np\n" + "XZd8YaRdMGKVV1O2guQ20Dxd1RGdU18k9YfFNsj4Jtw5sTFTzHr1P0n9ybV9xCXp\n" + "znSxVfRg8U6TcMHoRDJR9EMKQMO4W3OQEmreEPoGt2/+kMuiHjclxLtbwDxKXTLP\n" + "pD0gdg3ibvlufk/ccKl/yAglDmd0dfW22oS7NgvRKUve7tzDxY1Q6O5v8BCnLFSW\n" + "D+z4hS1PzooYRXRkM0xYudvPkryPyu+1kEpw3fNsoQKBgQDRfXJo82XQvlX8WPdZ\n" + "Ts3PfBKKMVu3Wf8J3SYpuvYT816qR3ot6e4Ivv5ZCQkdDwzzBKe2jAv6JddMJIhx\n" + "pkGHc0KKOodd9HoBewOd8Td++hapJAGaGblhL5beIidLKjXDjLqtgoHRGlv5Cojo\n" + "zHa7Viel1eOPPcBumhp83oJ+mQKBgQDC6PmdETZdrW3QPm7ZXxRzF1vvpC55wmPg\n" + "pRfTRM059jzRzAk0QiBgVp3yk2a6Ob3mB2MLfQVDgzGf37h2oO07s5nspSFZTFnM\n" + "KgSjFy0xVOAVDLe+0VpbmLp1YUTYvdCNowaoTE7++5rpePUDu3BjAifx07/yaSB+\n" + "W+YPOfOuKwKBgQCGK6g5G5qcJSuBIaHZ6yTZvIdLRu2M8vDral5k3793a6m3uWvB\n" + "OFAh/eF9ONJDcD5E7zhTLEMHhXDs7YEN+QODMwjs6yuDu27gv97DK5j1lEsrLUpx\n" + "XgRjAE3KG2m7NF+WzO1K74khWZaKXHrvTvTEaxudlO3X8h7rN3u7ee9uEQKBgQC2\n" + "wI1zeTUZhsiFTlTPWfgppchdHPs6zUqq0wFQ5Zzr8Pa72+zxY+NJkU2NqinTCNsG\n" + "ePykQ/gQgk2gUrt595AYv2De40IuoYk9BlTMuql0LNniwsbykwd/BOgnsSlFdEy8\n" + "0RQn70zOhgmNSg2qDzDklJvxghLi7zE5aV9//V1/ewKBgFRHHZN1a8q/v8AAOeoB\n" + "ROuXfgDDpxNNUKbzLL5MO5odgZGi61PBZlxffrSOqyZoJkzawXycNtoBP47tcVzT\n" + "QPq5ZOB3kjHTcN7dRLmPWjji9h4O3eHCX67XaPVMSWiMuNtOZIg2an06+jxGFhLE\n" + "qdJNJ1DkyUc9dN2cliX4R+rG\n" + "-----END PRIVATE KEY-----\n"; + + +static const char srv_signed_cert_pem[] = + "-----BEGIN CERTIFICATE-----\n" + "MIIFSzCCAzOgAwIBAgIBBDANBgkqhkiG9w0BAQsFADCBgTELMAkGA1UEBhMCUlUx\n" + "DzANBgNVBAgMBk1vc2NvdzEPMA0GA1UEBwwGTW9zY293MRswGQYDVQQKDBJ0ZXN0\n" + "LWxpYm1pY3JvaHR0cGQxITAfBgkqhkiG9w0BCQEWEm5vYm9keUBleGFtcGxlLm9y\n" + "ZzEQMA4GA1UEAwwHdGVzdC1DQTAgFw0yMjA0MjAxODQzMDJaGA8yMTIyMDMyNjE4\n" + "NDMwMlowZTELMAkGA1UEBhMCUlUxDzANBgNVBAgMBk1vc2NvdzEPMA0GA1UEBwwG\n" + "TW9zY293MRswGQYDVQQKDBJ0ZXN0LWxpYm1pY3JvaHR0cGQxFzAVBgNVBAMMDnRl\n" + "c3QtbWhkc2VydmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn3+2\n" + "psPczUhPoazjIQa8227CZblD94JnfJzimOTFIpqD5MZgF36di9IpfjD2U/2RJqNd\n" + "atJOido3xlAUPqGVvWoTWfYAg422JQW5aP5qk2ovi3Xizt6pOd3dM62Sl7wjurAN\n" + "7iqRtlkfoPrDSsX2KSBwqWp6gVOjvnXoCFf/list50NhcKeyuR1DY9btKYNEENfM\n" + "n+vC05yhVBxY2ZpmmqKVpIucCkgYnh4r80MusPtOB0k6IBx0Sv8v1HKCTUhu8Zx5\n" + "qDz6Nby0/AjESGJ0koFN8CeN1wSfNFsawtnXKGY1dl2c/l3UMAg+GawtYocpHAJo\n" + "kAPrGrjZu3c7TjJBswIDAQABo4HmMIHjMAsGA1UdDwQEAwIFoDAMBgNVHRMBAf8E\n" + "AjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMBMDEGA1UdEQQqMCiCDnRlc3QtbWhk\n" + "c2VydmVyhwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMB0GA1UdDgQWBBQ57Z06WJae\n" + "8fJIHId4QGx/HsRgDDAoBglghkgBhvhCAQ0EGxYZVGVzdCBsaWJtaWNyb2h0dHBk\n" + "IHNlcnZlcjARBglghkgBhvhCAQEEBAMCBkAwHwYDVR0jBBgwFoAUWHVDwKVqMcOF\n" + "Nd0arI3/QB3W6SwwDQYJKoZIhvcNAQELBQADggIBAI7Lggm/XzpugV93H5+KV48x\n" + "X+Ct8unNmPCSzCaI5hAHGeBBJpvD0KME5oiJ5p2wfCtK5Dt9zzf0S0xYdRKqU8+N\n" + "aKIvPoU1hFixXLwTte1qOp6TviGvA9Xn2Fc4n36dLt6e9aiqDnqPbJgBwcVO82ll\n" + "HJxVr3WbrAcQTB3irFUMqgAke/Cva9Bw79VZgX4ghb5EnejDzuyup4pHGzV10Myv\n" + "hdg+VWZbAxpCe0S4eKmstZC7mWsFCLeoRTf/9Pk1kQ6+azbTuV/9QOBNfFi8QNyb\n" + "18jUjmm8sc2HKo8miCGqb2sFqaGD918hfkWmR+fFkzQ3DZQrT+eYbKq2un3k0pMy\n" + "UySy8SRn1eadfab+GwBVb68I9TrPRMrJsIzysNXMX4iKYl2fFE/RSNnaHtPw0C8y\n" + "B7memyxPRl+H2xg6UjpoKYh3+8e44/XKm0rNIzXjrwA8f8gnw2TbqmMDkj1YqGnC\n" + "SCj5A27zUzaf2pT/YsnQXIWOJjVvbEI+YKj34wKWyTrXA093y8YI8T3mal7Kr9YM\n" + "WiIyPts0/aVeziM0Gunglz+8Rj1VesL52FTurobqusPgM/AME82+qb/qnxuPaCKj\n" + "OT1qAbIblaRuWqCsid8BzP7ZQiAnAWgMRSUg1gzDwSwRhrYQRRWAyn/Qipzec+27\n" + "/w0gW9EVWzFhsFeGEssi\n" + "-----END CERTIFICATE-----\n"; + + +static const char srv_self_signed_cert_pem[] = + "-----BEGIN CERTIFICATE-----\n" + "MIIDJzCCAg+gAwIBAgIUOKf6e6Heee2XA+yF5St3t+fVM40wDQYJKoZIhvcNAQEF\n" + "BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTIyMTAxMDA4MzQ0N1oYDzIxMjIw\n" + "OTE2MDgzNDQ3WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB\n" + "AQUAA4IBDwAwggEKAoIBAQClivgF8Xq0ekQli++0l7Q5JFwJCuLf04Cb1UKIS80U\n" + "CfphFd1ILJepNw4bWR3OV1sRI1vFiw6LnCz53vOwVNyiZ+sMGi4bDX4AV9Xd+F83\n" + "xhG8AjOmKTayW0TxSIvt47Qd5S/4fgraxMtvqrRRBen30iKOwX7uNF/4dYb9vdin\n" + "OldV/e8uzbqSurMGkNDznOeSaNBmdO/7x0VMFZM2hwmHyiiw75/j4BhUlLCcMEvK\n" + "oN+YHNCNcTt3Qm1vVuiGXmh9QreOV09Gc1SzAltxF2gmI0jzw8r/duz18QXMNsMw\n" + "El/Ah4+02gR70L7qlgttN1NPU3RJpK/L34J7yg649wHTAgMBAAGjbzBtMB0GA1Ud\n" + "DgQWBBROVferD+YYcV1YEnFgC0jYm5X9BjAfBgNVHSMEGDAWgBROVferD+YYcV1Y\n" + "EnFgC0jYm5X9BjAPBgNVHRMBAf8EBTADAQH/MBoGA1UdEQQTMBGCCWxvY2FsaG9z\n" + "dIcEfwAAATANBgkqhkiG9w0BAQUFAAOCAQEAoRbozsm5xXdNX3VO++s2LMzw5KM9\n" + "RpIInHNkMJbnyLJFKJ8DF7nTxSGCA38YMkX3tphPNKZXbg+V64Dqr/XpzOVyiinU\n" + "7hIwyUdSSKKyErZxIWR97lY6Q3SOyPAg8ZElbtvSsSzmd772VE23VTXGDi7AW0PQ\n" + "hag9N2EEnHURMvID15O+UXyFpDdyUyQIbx3HuswsGDH9xBTm4irLyrZwO0KwKg5a\n" + "JBeUiPs0SYRRfn9/MoE6VwAnmOCg3LLR6ZPU3hQtTPLHj2Op1g5fey3X3X6lC+JC\n" + "K6dNZc1zBFPz8KANGUsFYbmoP2bvAAA+6KwCnZZEflUgE7/HFEmQhVOezw==\n" + "-----END CERTIFICATE-----\n"; + + +static const char srv_self_signed_key_pem[] = + "-----BEGIN PRIVATE KEY-----\n" + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQClivgF8Xq0ekQl\n" + "i++0l7Q5JFwJCuLf04Cb1UKIS80UCfphFd1ILJepNw4bWR3OV1sRI1vFiw6LnCz5\n" + "3vOwVNyiZ+sMGi4bDX4AV9Xd+F83xhG8AjOmKTayW0TxSIvt47Qd5S/4fgraxMtv\n" + "qrRRBen30iKOwX7uNF/4dYb9vdinOldV/e8uzbqSurMGkNDznOeSaNBmdO/7x0VM\n" + "FZM2hwmHyiiw75/j4BhUlLCcMEvKoN+YHNCNcTt3Qm1vVuiGXmh9QreOV09Gc1Sz\n" + "AltxF2gmI0jzw8r/duz18QXMNsMwEl/Ah4+02gR70L7qlgttN1NPU3RJpK/L34J7\n" + "yg649wHTAgMBAAECggEAERbbCtYGakoy7cNX8Ac3Kiz4OVC/4gZWAQBPeX2FwrtS\n" + "9yHIMbK0x1mxIZ6eBpabBpZlW2vDCSOKuxLKiloAWt2qdJnhR5apesSWhe8leT7/\n" + "xq5dgZpAlMH6SIRKObknd2yY+qicW0A0licDrVeUcypkueL8xP9wJtiPInOuQXkI\n" + "QROhB13eStRuRKYwOn5gtwAHJ+J1DFKKiqpBOkrSYf4625StGegJO9+bjK0ei+0W\n" + "tp6unpiwA/lXTgz6Xim1Z3fzWs4XjFgVKzK5s/6yBJjr8spHX6lv7QsahP4w6HZ/\n" + "VcRxP6cJNd/otiTEtJXpbxiiyccwXm/AOcOn22P1cQKBgQDAnY/0G/ap/G98pneE\n" + "suzNXhWOQ8JoL8d66Io8vwTvfiJggfgUcwblI7pPCrSlaZMR7/q6JImE53lZtPk8\n" + "eI3c9lN0ocr8E7+huDpYdk7cMYj9SuxySsXoMLiMqzHFi+NcIhKMF56kk6a5CFCt\n" + "yP1Ofy76LVweGE3XvTwpwE7wUQKBgQDcBLyH1cC71s0I0Gz28AyELV9hPhasjAKO\n" + "12CVbeBVTPd+28uk/3o80wSrTksc6H5ehAA2aTvrb4OhwssWNL+D0fS8YK2cJ3V0\n" + "FJxGAM266+vC4d/8jRTHJnc+6PP3ix5t6vAt+K2Y0fePtefLqf4ebgXx/ODAj3J2\n" + "aZKBldjK4wKBgGIRFpTLk/eR/dUyEBHw4x3gdAsdtqJDCUYrlQ4+ly20Q55tLbiD\n" + "pBQP77CEm9rH+MgeLcKODbIsBB3HRUojet7wTydHpMhY6a1V1ebqPVZgpgWIGwBJ\n" + "z59bBusf0lRo15Y2Bslq0SurvSvh7um8NjO8D1fytj7gUumvgC0lq0sxAoGBAI1+\n" + "kkx9IBTtIDER8XGhkTsT/uoHxwcyh5abVmbjIclZ1TUFX2L+Vft17ePJVy8BKfvY\n" + "wlY7uShBMBNAteDTDXNV/CGFv0DUc4myk4nFjIkwng9XufeuN3WX/Eo+AF/rXSdt\n" + "VwcJjYLhTWdjoe1tppqlQTeN3HCaEA+s92ZVGvXnAoGAMCXGS6WZl1e5wsHRq0Yy\n" + "8Ef2Wrk620bBjKHolkTfvgfhlvxeZM1sv1ioZGsOeQ0z7O7wdJhvL0M/WAG+3yQj\n" + "HSXp81T1vOICPwNYZf8xcvbLKmvj7rHFt6ZAZF2o4EK8ReZTRyA3DUpBCDY+s3FN\n" + "GmBv0D7N3QP0CT3SzfQrPkc=\n" + "-----END PRIVATE KEY-----\n"; + + +static const char dh_params_pem[] = + "-----BEGIN DH PARAMETERS-----\n" + "MIIBDAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz\n" + "+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a\n" + "87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7\n" + "YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi\n" + "7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD\n" + "ssbzSibBsu/6iGtCOGEoXJf//////////wIBAgICAQA=\n" + "-----END DH PARAMETERS-----\n"; + +/** A PEM blob whose armour is fine but whose payload is not base64. */ +static const char garbage_cert_pem[] = + "-----BEGIN CERTIFICATE-----\n" + "this is not base64 at all, not even close !!!! ????\n" + "-----END CERTIFICATE-----\n"; + +static const char garbage_key_pem[] = + "-----BEGIN PRIVATE KEY-----\n" + "@@@@ neither is this @@@@\n" + "-----END PRIVATE KEY-----\n"; + +/** Correct base64, but the armour never ends. */ +static const char truncated_cert_pem[] = + "-----BEGIN CERTIFICATE-----\n" + "MIIFSzCCAzOgAwIBAgIBBDANBgkqhkiG9w0BAQsFADCBgTELMAkGA1UEBhMCUlUx\n" + "DzANBgNVBAgMBk1vc2NvdzEPMA0GA1UEBwwGTW9zY293MRswGQYDVQQKDBJ0ZXN0\n"; + +static const char truncated_key_pem[] = + "-----BEGIN PRIVATE KEY-----\n" + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCff7amw9zNSE+h\n"; + +/** Base64 of a certificate, but with no PEM armour at all. */ +static const char no_armour_pem[] = + "MIIFSzCCAzOgAwIBAgIBBDANBgkqhkiG9w0BAQsFADCBgTELMAkGA1UEBhMCUlUx\n" + "DzANBgNVBAgMBk1vc2NvdzEPMA0GA1UEBwwGTW9zY293MRswGQYDVQQKDBJ0ZXN0\n"; + +static const char garbage_dh_pem[] = + "-----BEGIN DH PARAMETERS-----\n" + "not-dh-parameters\n" + "-----END DH PARAMETERS-----\n"; + +/** + * PEM blobs assembled from the fuzzer's own bytes, see cred_tbl entries + * 12 and 13. Static so that the pointer handed to MHD stays valid for + * the whole lifetime of the daemon. + */ +static char fuzz_cert_pem[FUZZ_PEM_BODY_MAX + 128]; +static char fuzz_key_pem[FUZZ_PEM_BODY_MAX + 128]; + + +/** + * A certificate/key pair for #MHD_OPTION_HTTPS_MEM_CERT and + * #MHD_OPTION_HTTPS_MEM_KEY. A NULL member means "do not pass the + * option at all", which is a configuration MHD has to reject cleanly. + */ +struct cred_pair +{ + const char *cert; + const char *key; +}; + +static const struct cred_pair cred_tbl[] = { + { srv_signed_cert_pem, srv_signed_key_pem }, /* 0 valid, CA signed */ + { srv_self_signed_cert_pem, srv_self_signed_key_pem }, /* 1 valid, self signed */ + { srv_signed_cert_pem, srv_self_signed_key_pem }, /* 2 key does not match */ + { srv_self_signed_cert_pem, srv_signed_key_pem }, /* 3 key does not match */ + { srv_signed_cert_pem, NULL }, /* 4 certificate only */ + { NULL, srv_signed_key_pem }, /* 5 key only */ + { NULL, NULL }, /* 6 neither */ + { "", "" }, /* 7 empty strings */ + { truncated_cert_pem, srv_signed_key_pem }, /* 8 truncated armour */ + { srv_signed_cert_pem, truncated_key_pem }, /* 9 truncated armour */ + { garbage_cert_pem, garbage_key_pem }, /* 10 armour, no base64 */ + { no_armour_pem, srv_signed_key_pem }, /* 11 base64, no armour */ + { fuzz_cert_pem, srv_signed_key_pem }, /* 12 PEM from the input */ + { srv_signed_cert_pem, fuzz_key_pem }, /* 13 PEM from the input */ + { fuzz_cert_pem, fuzz_key_pem }, /* 14 PEM from the input */ + { srv_signed_cert_pem, srv_signed_key_pem } /* 15 valid (bias) */ +}; + +#define CRED_COUNT (sizeof (cred_tbl) / sizeof (cred_tbl[0])) + +/** + * Priority strings. The first entries are the ones a real server would + * use, the rest are what MHD has to reject without falling over. MHD + * hands these to gnutls_priority_init() (or appends them to its own + * base string) and fails daemon startup if GnuTLS does not like them. + */ +static const char *const prio_tbl[] = { + "NORMAL", + "NORMAL:-VERS-ALL:+VERS-TLS1.3", + "NORMAL:-VERS-ALL:+VERS-TLS1.2", + "SECURE128", + "PERFORMANCE", + "@LIBMICROHTTPD", + "@SYSTEM", + "NONE", + "", + "BOGUS-PRIORITY-STRING", + "NORMAL:!!!", + "NORMAL:-VERS-ALL", + "NORMAL:+ANON-ECDH", + "NORMAL:%SERVER_PRECEDENCE", + "SECURE256:-CIPHER-ALL", + ":::::" +}; + +#define PRIO_COUNT (sizeof (prio_tbl) / sizeof (prio_tbl[0])) + +/** Priority strings offered to the *client* side. */ +static const char *const client_prio_tbl[CLIENT_PRIO_COUNT] = { + "NORMAL", + "NORMAL:-VERS-ALL:+VERS-TLS1.3", + "NORMAL:-VERS-ALL:+VERS-TLS1.2", + "PERFORMANCE" +}; + +/** + * Credential types. Only GNUTLS_CRD_CERTIFICATE and GNUTLS_CRD_PSK are + * accepted by MHD_TLS_init(); everything else must make daemon startup + * fail rather than reach the MHD_PANIC() in new_connection_prepare_(). + */ +static const int cred_type_tbl[] = { + (int) GNUTLS_CRD_CERTIFICATE, + (int) GNUTLS_CRD_PSK, + (int) GNUTLS_CRD_ANON, + (int) GNUTLS_CRD_SRP, + (int) GNUTLS_CRD_IA, + (int) GNUTLS_CRD_CERTIFICATE, + 99, + -1 +}; + +#define CRED_TYPE_COUNT (sizeof (cred_type_tbl) / sizeof (cred_type_tbl[0])) + +static const size_t mem_limit_tbl[] = { + 0 /* MHD default */, 256, 512, 1024, 1400, 1500, 2048, 4096, 8192, 32768, + 0, 1024, 2048, 4096, 16384, 0 +}; + +#define MEM_LIMIT_COUNT (sizeof (mem_limit_tbl) / sizeof (mem_limit_tbl[0])) + +static const char https_key_password[] = "not-the-password"; + + +/* ------------------------------------------------------------------ */ +/* Per-iteration configuration */ +/* ------------------------------------------------------------------ */ + +/** Behaviour of the SNI callback, selected by bits 3-5 of byte 3. */ +enum sni_behaviour +{ + SNI_ALWAYS_OK = 0, /**< always answer with the signed pair */ + SNI_FAIL, /**< always answer -1 */ + SNI_EMPTY, /**< answer 0 with an empty certificate list */ + SNI_BY_NAME, /**< look the server name up, -1 if unknown */ + SNI_MISMATCH, /**< answer with a key that is not the cert's */ + SNI_NO_KEY, /**< answer 0, certificate but no key */ + SNI_SELF_SIGNED, /**< always answer with the self-signed pair */ + SNI_BEHAVIOUR_COUNT +}; + +/** Client behaviour, selected by bits 0-1 of byte 4. */ +enum client_mode +{ + CLIENT_RAW = 0, /**< raw bytes straight at the TLS socket */ + CLIENT_TLS, /**< a real GnuTLS client */ + CLIENT_TLS_ABANDON, /**< a real client that stops mid-handshake */ + CLIENT_RECORDS /**< raw bytes shaped like TLS records */ +}; + +struct fuzz_cfg +{ + /* ---- daemon ---- */ + const char *cert; + const char *key; + int use_tls; /**< set MHD_USE_TLS at all */ + int use_trust; + int trust_garbage; + int use_dhparams; + int dh_garbage; + int use_sni; + int use_prio; + int prio_append; + unsigned int prio_idx; + int use_cred_type; + unsigned int cred_type_idx; + int no_alpn; + int key_password; + int allow_upgrade; + size_t mem_limit; + unsigned int loop_mode; + + /* ---- SNI callback ---- */ + enum sni_behaviour sni_mode; + + /* ---- client ---- */ + enum client_mode mode; + int client_sni; + int client_cert; + unsigned int client_prio_idx; + int client_bye; + int client_shut_wr; + unsigned int hs_budget; /**< handshake rounds in CLIENT_TLS_ABANDON */ + unsigned int extra_pump; + + /* ---- handler ---- */ + unsigned int resp_kind; + int conn_info; + int daemon_info; + int conn_option; + int error_reply; + int resp_header; + int quiesce; +}; + +static struct fuzz_cfg cfg; + +/** + * Server names the client can present. Which one is used comes from + * byte 9 of the input; an op 1 segment overrides it. MHD itself never + * looks at the name -- GnuTLS parses the extension and the application + * callback reads it back -- so a fixed table costs no MHD coverage. + */ +static const char *const sni_name_tbl[] = { + "test-mhdserver", + "localhost", + "mhdhost1", + "nobody.example.org", + "", + "example.org", + "TEST-MHDSERVER", + "a.very.long.name.that.nobody.has.a.certificate.for.example.org" +}; + +#define SNI_NAME_COUNT (sizeof (sni_name_tbl) / sizeof (sni_name_tbl[0])) + +/** Server name the next client connection presents. */ +static char sni_name[MAX_SNI_LEN + 1]; +static size_t sni_name_len; + +/** Bytes received from the daemon during the current iteration. */ +static char resp_buf[RESP_BUF_SIZE]; +static size_t resp_len; + +/** Statistics, printed at exit with --verbose. */ +static unsigned long stat_daemons; +static unsigned long stat_daemons_failed; +static unsigned long stat_connections; +static unsigned long stat_handshakes_ok; +static unsigned long stat_handshakes_failed; +static unsigned long stat_handler_calls; +static unsigned long stat_sni_calls; +static int stats_registered; + + +static void +print_stats (void) +{ + if (! fuzz_verbose) + return; + fprintf (stderr, + "%s: daemons=%lu (start failed=%lu) connections=%lu " + "handshakes ok=%lu failed=%lu handler calls=%lu SNI calls=%lu\n", + FUZZ_HARNESS_NAME, + stat_daemons, stat_daemons_failed, stat_connections, + stat_handshakes_ok, stat_handshakes_failed, stat_handler_calls, + stat_sni_calls); +} + + +/* ------------------------------------------------------------------ */ +/* The SNI (certificate retrieve) callback */ +/* ------------------------------------------------------------------ */ + +/** + * Certificates for the SNI callback, parsed once and then kept in these + * statics for the whole life of the process: they are configuration, not + * per-iteration state, and re-parsing them on every execution would cost + * more than everything else the harness does. They are reachable from + * globals, so LeakSanitizer does not count them. + */ +struct sni_host +{ + const char *name; + const char *cert_pem; + const char *key_pem; + gnutls_pcert_st pcrt; + gnutls_privkey_t key; + int loaded; +}; + +static struct sni_host sni_hosts[2] = { + { "test-mhdserver", NULL, NULL, { 0, { NULL, 0 }, 0 }, NULL, 0 }, + { "localhost", NULL, NULL, { 0, { NULL, 0 }, 0 }, NULL, 0 } +}; + +static int sni_hosts_ready; + + +/** + * @return 0 if the certificates are usable + */ +static int +sni_hosts_init (void) +{ + unsigned int i; + + if (0 != sni_hosts_ready) + return (1 == sni_hosts_ready) ? 0 : -1; + sni_hosts[0].cert_pem = srv_signed_cert_pem; + sni_hosts[0].key_pem = srv_signed_key_pem; + sni_hosts[1].cert_pem = srv_self_signed_cert_pem; + sni_hosts[1].key_pem = srv_self_signed_key_pem; + sni_hosts_ready = 1; + for (i = 0; i < sizeof (sni_hosts) / sizeof (sni_hosts[0]); i++) + { + struct sni_host *h = &sni_hosts[i]; + gnutls_datum_t d; + + d.data = (unsigned char *) (intptr_t) h->cert_pem; + d.size = (unsigned int) strlen (h->cert_pem); + if (GNUTLS_E_SUCCESS != + gnutls_pcert_import_x509_raw (&h->pcrt, &d, GNUTLS_X509_FMT_PEM, 0)) + { + sni_hosts_ready = -1; + continue; + } + if (GNUTLS_E_SUCCESS != gnutls_privkey_init (&h->key)) + { + gnutls_pcert_deinit (&h->pcrt); + sni_hosts_ready = -1; + continue; + } + d.data = (unsigned char *) (intptr_t) h->key_pem; + d.size = (unsigned int) strlen (h->key_pem); + if (GNUTLS_E_SUCCESS != + gnutls_privkey_import_x509_raw (h->key, &d, GNUTLS_X509_FMT_PEM, + NULL, 0)) + { + gnutls_privkey_deinit (h->key); + h->key = NULL; + gnutls_pcert_deinit (&h->pcrt); + sni_hosts_ready = -1; + continue; + } + h->loaded = 1; + } + return (1 == sni_hosts_ready) ? 0 : -1; +} + + +/** + * #MHD_OPTION_HTTPS_CERT_CALLBACK. Deliberately badly behaved for most + * of the settings of @e cfg.sni_mode: an application callback that fails + * or answers with nothing is exactly the case MHD has to survive. + */ +static int +sni_callback (gnutls_session_t session, + const gnutls_datum_t *req_ca_dn, + int nreqs, + const gnutls_pk_algorithm_t *pk_algos, + int pk_algos_length, + gnutls_pcert_st **pcert, + unsigned int *pcert_length, + gnutls_privkey_t *pkey) +{ + char name[MAX_SNI_LEN + 1]; + size_t name_len = sizeof (name); + unsigned int type; + unsigned int i; + + (void) req_ca_dn; + (void) nreqs; + (void) pk_algos; + (void) pk_algos_length; + stat_sni_calls++; + if (! sni_hosts[0].loaded) + return -1; + switch (cfg.sni_mode) + { + case SNI_FAIL: + return -1; + case SNI_EMPTY: + *pcert = NULL; + *pcert_length = 0; + *pkey = NULL; + return 0; + case SNI_NO_KEY: + *pcert = &sni_hosts[0].pcrt; + *pcert_length = 1; + *pkey = NULL; + return 0; + case SNI_MISMATCH: + if (! sni_hosts[1].loaded) + return -1; + *pcert = &sni_hosts[0].pcrt; + *pcert_length = 1; + *pkey = sni_hosts[1].key; + return 0; + case SNI_SELF_SIGNED: + if (! sni_hosts[1].loaded) + return -1; + *pcert = &sni_hosts[1].pcrt; + *pcert_length = 1; + *pkey = sni_hosts[1].key; + return 0; + case SNI_BY_NAME: + if (GNUTLS_E_SUCCESS != + gnutls_server_name_get (session, name, &name_len, &type, 0)) + return -1; + for (i = 0; i < sizeof (sni_hosts) / sizeof (sni_hosts[0]); i++) + if ( (sni_hosts[i].loaded) && + (0 == strncmp (name, sni_hosts[i].name, name_len)) ) + { + *pcert = &sni_hosts[i].pcrt; + *pcert_length = 1; + *pkey = sni_hosts[i].key; + return 0; + } + return -1; + case SNI_ALWAYS_OK: + case SNI_BEHAVIOUR_COUNT: + default: + break; + } + *pcert = &sni_hosts[0].pcrt; + *pcert_length = 1; + *pkey = sni_hosts[0].key; + return 0; +} + + +/* ------------------------------------------------------------------ */ +/* The application */ +/* ------------------------------------------------------------------ */ + +static const char resp_body[] = "hello world over TLS"; + + +static enum MHD_Result +ahc (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **req_cls) +{ + static int marker; + struct MHD_Response *r; + enum MHD_Result ret; + + (void) cls; + (void) url; + (void) method; + (void) version; + (void) upload_data; + stat_handler_calls++; + if (NULL == *req_cls) + { + /* first call: MHD only wants to know that we are interested */ + *req_cls = ▮ + return MHD_YES; + } + if (0 != *upload_data_size) + { + /* discard the request body */ + *upload_data_size = 0; + return MHD_YES; + } + if (cfg.conn_info) + { + /* The TLS-specific members of MHD_ConnectionInfo; all three answer + NULL on a connection without a session, which is a state this + harness can produce. */ + (void) MHD_get_connection_info (connection, + MHD_CONNECTION_INFO_CIPHER_ALGO); + (void) MHD_get_connection_info (connection, + MHD_CONNECTION_INFO_PROTOCOL); + (void) MHD_get_connection_info (connection, + MHD_CONNECTION_INFO_GNUTLS_SESSION); + (void) MHD_get_connection_info (connection, + MHD_CONNECTION_INFO_GNUTLS_CLIENT_CERT); + (void) MHD_get_connection_info (connection, + MHD_CONNECTION_INFO_CLIENT_ADDRESS); + (void) MHD_get_connection_info (connection, + MHD_CONNECTION_INFO_CONNECTION_FD); + } + if (cfg.conn_option) + (void) MHD_set_connection_option (connection, + MHD_CONNECTION_OPTION_TIMEOUT, + (unsigned int) 0); + switch (cfg.resp_kind) + { + case 1: + r = MHD_create_response_from_buffer_copy (sizeof (resp_body) - 1, + resp_body); + break; + case 2: + r = MHD_create_response_empty (MHD_RF_NONE); + break; + case 3: + r = MHD_create_response_from_buffer_static (0, ""); + break; + case 0: + default: + r = MHD_create_response_from_buffer_static (sizeof (resp_body) - 1, + resp_body); + break; + } + if (NULL == r) + return MHD_NO; + if (cfg.resp_header) + (void) MHD_add_response_header (r, "X-Fuzz", "tls"); + ret = MHD_queue_response (connection, + cfg.error_reply + ? MHD_HTTP_FORBIDDEN + : MHD_HTTP_OK, + r); + MHD_destroy_response (r); + return ret; +} + + +static void +panic_cb (void *cls, + const char *file, + unsigned int line, + const char *reason) +{ + char msg[512]; + + (void) cls; + (void) snprintf (msg, sizeof (msg), + "MHD_PANIC() reached at %s:%u: %s", + (NULL != file) ? file : "?", + line, + (NULL != reason) ? reason : "?"); + fuzz_report_finding (msg); +} + + +/* ------------------------------------------------------------------ */ +/* Driving the daemon */ +/* ------------------------------------------------------------------ */ + +/** + * Advance the daemon by one cycle, through the event-loop API selected + * by byte 5 of the input. The select() timeout is always zero: the + * harness is single threaded, whatever the daemon is waiting for has + * already been written into the socketpair, and blocking would only burn + * wall clock. + */ +static void +run_once (struct MHD_Daemon *d) +{ + fd_set rs; + fd_set ws; + fd_set es; + MHD_socket max_fd = MHD_INVALID_SOCKET; + struct timeval tv; + + if (0 == cfg.loop_mode) + { + (void) MHD_run (d); + return; + } + FD_ZERO (&rs); + FD_ZERO (&ws); + FD_ZERO (&es); + if (1 == cfg.loop_mode) + { + /* Parenthesised so that the real v1 function is called: microhttpd.h + also defines MHD_get_fdset as a macro forwarding to + MHD_get_fdset2. Same trick for MHD_run_from_select below. */ + if (MHD_YES != (MHD_get_fdset) (d, &rs, &ws, &es, &max_fd)) + { + (void) MHD_run (d); + return; + } + } + else + { + if (MHD_YES != MHD_get_fdset2 (d, &rs, &ws, &es, &max_fd, + (unsigned int) FD_SETSIZE)) + { + (void) MHD_run (d); + return; + } + } + tv.tv_sec = 0; + tv.tv_usec = 0; + if (MHD_INVALID_SOCKET != max_fd) + (void) select ((int) max_fd + 1, &rs, &ws, &es, &tv); + if (1 == cfg.loop_mode) + (void) (MHD_run_from_select) (d, &rs, &ws, &es); + else + (void) MHD_run_from_select2 (d, &rs, &ws, &es, (unsigned int) FD_SETSIZE); +} + + +/* ------------------------------------------------------------------ */ +/* The in-process TLS client */ +/* ------------------------------------------------------------------ */ + +struct tls_client +{ + gnutls_session_t sess; /**< NULL in the raw modes */ + gnutls_certificate_credentials_t cred; + int fd; /**< our end of the socketpair */ + int hs_done; + int dead; +}; + + +static void +pump (struct MHD_Daemon *d, + unsigned int rounds) +{ + unsigned int i; + + for (i = 0; i < rounds; i++) + run_once (d); +} + + +/** + * Read whatever the daemon has produced so far. In the raw modes this + * is a plain recv(); with a real client it goes through GnuTLS, which is + * what actually drives MHD's send path and its TLS shutdown handling. + */ +static void +tc_drain (struct tls_client *tc) +{ + unsigned int i; + char tmp[4096]; + + if (0 > tc->fd) + return; + if (NULL == tc->sess) + { + for (;;) + { + ssize_t n = recv (tc->fd, tmp, sizeof (tmp), MSG_DONTWAIT); + + if (0 >= n) + break; + if (resp_len + (size_t) n < RESP_BUF_SIZE) + { + memcpy (resp_buf + resp_len, tmp, (size_t) n); + resp_len += (size_t) n; + } + } + return; + } + if (tc->dead || (! tc->hs_done)) + return; + for (i = 0; i < 8; i++) + { + ssize_t n = gnutls_record_recv (tc->sess, tmp, sizeof (tmp)); + + if (0 < n) + { + if (resp_len + (size_t) n < RESP_BUF_SIZE) + { + memcpy (resp_buf + resp_len, tmp, (size_t) n); + resp_len += (size_t) n; + } + continue; + } + if (0 == n) + break; /* peer closed the TLS connection */ + if ( (GNUTLS_E_AGAIN == n) || + (GNUTLS_E_INTERRUPTED == n) ) + break; + if (0 != gnutls_error_is_fatal ((int) n)) + tc->dead = 1; + break; + } +} + + +static void +pump_and_drain (struct MHD_Daemon *d, + struct tls_client *tc, + unsigned int rounds) +{ + unsigned int i; + + for (i = 0; i < rounds; i++) + { + run_once (d); + tc_drain (tc); + } +} + + +/** + * Let the client and the daemon take turns at the handshake until it + * completes, fails, or @a rounds is exhausted. Exhausting @a rounds on + * purpose (client mode 2) is what leaves MHD's connection parked in + * #MHD_TLS_CONN_HANDSHAKING. + */ +static void +tc_handshake (struct MHD_Daemon *d, + struct tls_client *tc, + unsigned int rounds) +{ + unsigned int i; + + if ( (NULL == tc->sess) || + (0 != tc->dead) || + (0 != tc->hs_done) ) + return; + for (i = 0; i < rounds; i++) + { + int ret = gnutls_handshake (tc->sess); + + if (GNUTLS_E_SUCCESS == ret) + { + tc->hs_done = 1; + stat_handshakes_ok++; + return; + } + if (0 != gnutls_error_is_fatal (ret)) + { + tc->dead = 1; + stat_handshakes_failed++; + return; + } + /* GNUTLS_E_AGAIN / GNUTLS_E_INTERRUPTED / a warning alert: give the + daemon a chance to answer. */ + run_once (d); + } +} + + +/** + * Open a fresh connection: a socketpair, one end handed to MHD with + * MHD_add_connection(), the other end ours. With a real client the + * GnuTLS session is set up here too, but the handshake itself is driven + * by tc_handshake(). + * + * @return 0 on success + */ +static int +tc_open (struct MHD_Daemon *d, + struct tls_client *tc) +{ + int sv[2]; + struct sockaddr_in sa; + int fl; +#if GNUTLS_VERSION_NUMBER >= 0x030500 + gnutls_init_flags_t flags; +#else + unsigned int flags; +#endif + + memset (tc, 0, sizeof (*tc)); + tc->fd = -1; + if (0 != socketpair (AF_UNIX, SOCK_STREAM, 0, sv)) + return -1; + memset (&sa, 0, sizeof (sa)); + sa.sin_family = AF_INET; + sa.sin_port = htons (44444); + sa.sin_addr.s_addr = htonl (INADDR_LOOPBACK); + if (MHD_YES != MHD_add_connection (d, + (MHD_socket) sv[1], + (const struct sockaddr *) &sa, + (socklen_t) sizeof (sa))) + { + /* MHD has already closed sv[1] in that case */ + (void) close (sv[0]); + return -1; + } + tc->fd = sv[0]; + stat_connections++; + /* Our end must never block: the daemon is pumped from this very + thread, so a blocking write would deadlock the process. */ + fl = fcntl (tc->fd, F_GETFL, 0); + if (0 <= fl) + (void) fcntl (tc->fd, F_SETFL, fl | O_NONBLOCK); + if ( (CLIENT_TLS != cfg.mode) && + (CLIENT_TLS_ABANDON != cfg.mode) ) + return 0; + + if (GNUTLS_E_SUCCESS != + gnutls_certificate_allocate_credentials (&tc->cred)) + { + tc->cred = NULL; + tc->dead = 1; + return 0; + } + if (cfg.client_cert) + { + gnutls_datum_t c; + gnutls_datum_t k; + + c.data = (unsigned char *) (intptr_t) srv_self_signed_cert_pem; + c.size = (unsigned int) strlen (srv_self_signed_cert_pem); + k.data = (unsigned char *) (intptr_t) srv_self_signed_key_pem; + k.size = (unsigned int) strlen (srv_self_signed_key_pem); + (void) gnutls_certificate_set_x509_key_mem (tc->cred, &c, &k, + GNUTLS_X509_FMT_PEM); + } + flags = GNUTLS_CLIENT; +#if GNUTLS_VERSION_MAJOR >= 3 + flags |= GNUTLS_NONBLOCK; +#endif +#if GNUTLS_VERSION_NUMBER >= 0x030402 + flags |= GNUTLS_NO_SIGNAL; +#endif + if (GNUTLS_E_SUCCESS != gnutls_init (&tc->sess, flags)) + { + tc->sess = NULL; + tc->dead = 1; + return 0; + } + if (GNUTLS_E_SUCCESS != + gnutls_priority_set_direct (tc->sess, + client_prio_tbl[cfg.client_prio_idx], + NULL)) + (void) gnutls_priority_set_direct (tc->sess, "NORMAL", NULL); + if (GNUTLS_E_SUCCESS != + gnutls_credentials_set (tc->sess, GNUTLS_CRD_CERTIFICATE, tc->cred)) + tc->dead = 1; + if ( (cfg.client_sni) && + (0 != sni_name_len) ) + (void) gnutls_server_name_set (tc->sess, + GNUTLS_NAME_DNS, + sni_name, + sni_name_len); + gnutls_transport_set_int (tc->sess, tc->fd); + /* GNUTLS_INDEFINITE_TIMEOUT: the default handshake timeout is a wall + clock deadline, and this harness must not depend on the clock. */ + gnutls_handshake_set_timeout (tc->sess, 0); + return 0; +} + + +static void +tc_send (struct MHD_Daemon *d, + struct tls_client *tc, + const uint8_t *buf, + size_t len) +{ + size_t off = 0; + unsigned int stall = 0; + + if ( (0 > tc->fd) || + (0 == len) ) + return; + if (NULL == tc->sess) + { + while ( (off < len) && + (stall < 64) ) + { + ssize_t s = send (tc->fd, buf + off, len - off, MSG_DONTWAIT); + + if (0 < s) + { + off += (size_t) s; + stall = 0; + continue; + } + stall++; + pump_and_drain (d, tc, 2); + if ( (0 > s) && + (EAGAIN != errno) && + (EWOULDBLOCK != errno) && + (EINTR != errno) ) + break; + } + return; + } + if ( (0 != tc->dead) || + (0 == tc->hs_done) ) + return; + while ( (off < len) && + (stall < 64) ) + { + /* On GNUTLS_E_AGAIN the call has to be repeated with exactly the + same arguments, which is why @a off is only advanced on success. */ + ssize_t s = gnutls_record_send (tc->sess, buf + off, len - off); + + if (0 < s) + { + off += (size_t) s; + stall = 0; + continue; + } + if ( (GNUTLS_E_AGAIN == s) || + (GNUTLS_E_INTERRUPTED == s) ) + { + stall++; + pump_and_drain (d, tc, 2); + continue; + } + if (0 != gnutls_error_is_fatal ((int) s)) + tc->dead = 1; + break; + } +} + + +/** + * Tear the client end down. Every GnuTLS object is released on every + * path, including the ones where the session never got off the ground. + */ +static void +tc_close (struct MHD_Daemon *d, + struct tls_client *tc) +{ + if (NULL != tc->sess) + { + if ( (cfg.client_bye) && + (0 == tc->dead) && + (0 != tc->hs_done) ) + { + unsigned int i; + + for (i = 0; i < 8; i++) + { + int ret = gnutls_bye (tc->sess, GNUTLS_SHUT_WR); + + if ( (GNUTLS_E_AGAIN != ret) && + (GNUTLS_E_INTERRUPTED != ret) ) + break; + run_once (d); + } + } + gnutls_deinit (tc->sess); + tc->sess = NULL; + } + if (NULL != tc->cred) + { + gnutls_certificate_free_credentials (tc->cred); + tc->cred = NULL; + } + if (0 <= tc->fd) + { + if (cfg.client_shut_wr) + (void) shutdown (tc->fd, SHUT_WR); + pump (d, 2); + (void) close (tc->fd); + tc->fd = -1; + } + tc->hs_done = 0; + tc->dead = 1; + pump (d, 4); +} + + +/** + * Bring a fresh connection up to the point where payload can be sent. + */ +static void +tc_start (struct MHD_Daemon *d, + struct tls_client *tc) +{ + if (0 != tc_open (d, tc)) + { + tc->fd = -1; + return; + } + if (CLIENT_TLS == cfg.mode) + tc_handshake (d, tc, HANDSHAKE_ROUNDS); + else if (CLIENT_TLS_ABANDON == cfg.mode) + tc_handshake (d, tc, cfg.hs_budget); + else + pump (d, 1); +} + + +/* ------------------------------------------------------------------ */ +/* PEM blobs assembled from the input */ +/* ------------------------------------------------------------------ */ + +/** + * Build @a out as "-----BEGIN @a label-----\n<body>\n-----END @a + * label-----\n", with @a body_len bytes of @a body as the payload. The + * result is always NUL terminated, because MHD calls strlen() on it. + */ +static void +build_fuzz_pem (char *out, + size_t out_size, + const char *label, + const uint8_t *body, + size_t body_len) +{ + size_t o = 0; + size_t i; + int n; + + n = snprintf (out, out_size, "-----BEGIN %s-----\n", label); + if ( (0 > n) || + ((size_t) n >= out_size) ) + { + out[0] = '\0'; + return; + } + o = (size_t) n; + if (body_len > FUZZ_PEM_BODY_MAX) + body_len = FUZZ_PEM_BODY_MAX; + for (i = 0; (i < body_len) && (o + 32 < out_size); i++) + { + /* Keep the blob a C string; a NUL inside would simply truncate it + for MHD's strlen(), which is a less interesting shape. */ + out[o++] = (char) ((0 == body[i]) ? 'A' : body[i]); + } + n = snprintf (out + o, out_size - o, "\n-----END %s-----\n", label); + if (0 > n) + out[o] = '\0'; +} + + +/* ------------------------------------------------------------------ */ +/* The fuzz target */ +/* ------------------------------------------------------------------ */ + +int +LLVMFuzzerTestOneInput (const uint8_t *data, + size_t size) +{ + struct MHD_Daemon *d; + struct MHD_OptionItem opts[12]; + unsigned int nopt = 0; + unsigned int flags; + struct tls_client tc; + size_t pos; + unsigned int nseg = 0; + unsigned int nconn = 1; + + /* Must happen before the first write() into the socketpair; see + fuzz_ignore_sigpipe() in fuzz_common.h for why the process dies + without it. Idempotent. */ + fuzz_ignore_sigpipe (); + + /* The ten configuration bytes are mandatory. */ + if (size < 10) + return 0; + + resp_len = 0; + memset (&cfg, 0, sizeof (cfg)); + + { + const char *nm = sni_name_tbl[(data[9] & 0x07) % SNI_NAME_COUNT]; + + sni_name_len = strlen (nm); + memcpy (sni_name, nm, sni_name_len + 1); + } + + cfg.cert = cred_tbl[data[0] % CRED_COUNT].cert; + cfg.key = cred_tbl[data[0] % CRED_COUNT].key; + + cfg.use_trust = (0 != (data[1] & 0x01)); + cfg.use_dhparams = (0 != (data[1] & 0x02)); + cfg.use_sni = (0 != (data[1] & 0x04)); + cfg.use_prio = (0 != (data[1] & 0x08)); + cfg.use_cred_type = (0 != (data[1] & 0x10)); + cfg.no_alpn = (0 != (data[1] & 0x20)); + cfg.key_password = (0 != (data[1] & 0x40)); + cfg.prio_append = (0 != (data[1] & 0x80)); + + cfg.prio_idx = (unsigned int) (data[2] % PRIO_COUNT); + + cfg.cred_type_idx = (unsigned int) (data[3] & 0x07) % CRED_TYPE_COUNT; + cfg.sni_mode = + (enum sni_behaviour) (((unsigned int) (data[3] >> 3) & 0x07) + % (unsigned int) SNI_BEHAVIOUR_COUNT); + cfg.trust_garbage = (0 != (data[3] & 0x40)); + cfg.dh_garbage = (0 != (data[3] & 0x80)); + + cfg.mode = (enum client_mode) (data[4] & 0x03); + cfg.client_sni = (0 != (data[4] & 0x04)); + cfg.client_cert = (0 != (data[4] & 0x08)); + cfg.client_prio_idx = (unsigned int) ((data[4] >> 4) & 0x03); + cfg.client_bye = (0 != (data[4] & 0x40)); + cfg.client_shut_wr = (0 != (data[4] & 0x80)); + + cfg.mem_limit = mem_limit_tbl[(data[5] & 0x0F) % MEM_LIMIT_COUNT]; + cfg.loop_mode = (unsigned int) ((data[5] >> 4) & 0x03); + + cfg.resp_kind = (unsigned int) (data[6] & 0x03); + cfg.conn_info = (0 != (data[6] & 0x04)); + cfg.daemon_info = (0 != (data[6] & 0x08)); + cfg.conn_option = (0 != (data[6] & 0x10)); + cfg.error_reply = (0 != (data[6] & 0x20)); + cfg.resp_header = (0 != (data[6] & 0x40)); + cfg.quiesce = (0 != (data[6] & 0x80)); + + cfg.hs_budget = 1u + (unsigned int) (data[7] & 0x07); + cfg.use_tls = (0 == (data[7] & 0x08)); + cfg.allow_upgrade = + (0 != (data[7] & 0x10)) && + (MHD_YES == MHD_is_feature_supported (MHD_FEATURE_UPGRADE)); + cfg.extra_pump = (unsigned int) ((data[7] >> 5) & 0x07); + + /* A real handshake is pointless without a working credential setup on + our own side; fall back to the raw modes if the SNI certificates + could not be parsed. */ + if ( (cfg.use_sni) && + (0 != sni_hosts_init ()) ) + cfg.use_sni = 0; + + /* The PEM blobs built from the input. They are only referenced by + cred_tbl entries 12-14, but filling them unconditionally keeps this + out of the option assembly below. */ + { + size_t body = (size_t) data[8] * 4u; + + if (body > size - 10) + body = size - 10; + build_fuzz_pem (fuzz_cert_pem, sizeof (fuzz_cert_pem), + "CERTIFICATE", data + 10, body); + build_fuzz_pem (fuzz_key_pem, sizeof (fuzz_key_pem), + "PRIVATE KEY", data + 10, body); + } + + if (0 != cfg.mem_limit) + { + opts[nopt].option = MHD_OPTION_CONNECTION_MEMORY_LIMIT; + opts[nopt].value = (intptr_t) cfg.mem_limit; + opts[nopt].ptr_value = NULL; + nopt++; + } + if (NULL != cfg.key) + { + opts[nopt].option = MHD_OPTION_HTTPS_MEM_KEY; + opts[nopt].value = 0; + opts[nopt].ptr_value = (void *) (intptr_t) cfg.key; + nopt++; + } + if (NULL != cfg.cert) + { + opts[nopt].option = MHD_OPTION_HTTPS_MEM_CERT; + opts[nopt].value = 0; + opts[nopt].ptr_value = (void *) (intptr_t) cfg.cert; + nopt++; + } + if (cfg.key_password) + { + opts[nopt].option = MHD_OPTION_HTTPS_KEY_PASSWORD; + opts[nopt].value = 0; + opts[nopt].ptr_value = (void *) (intptr_t) https_key_password; + nopt++; + } + if (cfg.use_trust) + { + opts[nopt].option = MHD_OPTION_HTTPS_MEM_TRUST; + opts[nopt].value = 0; + opts[nopt].ptr_value = (void *) (intptr_t) + (cfg.trust_garbage ? garbage_cert_pem + : ca_cert_pem); + nopt++; + } + if (cfg.use_dhparams) + { + opts[nopt].option = MHD_OPTION_HTTPS_MEM_DHPARAMS; + opts[nopt].value = 0; + opts[nopt].ptr_value = (void *) (intptr_t) + (cfg.dh_garbage ? garbage_dh_pem : dh_params_pem); + nopt++; + } + if (cfg.use_prio) + { + opts[nopt].option = cfg.prio_append + ? MHD_OPTION_HTTPS_PRIORITIES_APPEND + : MHD_OPTION_HTTPS_PRIORITIES; + opts[nopt].value = 0; + opts[nopt].ptr_value = (void *) (intptr_t) prio_tbl[cfg.prio_idx]; + nopt++; + } + if (cfg.use_cred_type) + { + opts[nopt].option = MHD_OPTION_HTTPS_CRED_TYPE; + opts[nopt].value = (intptr_t) cred_type_tbl[cfg.cred_type_idx]; + opts[nopt].ptr_value = NULL; + nopt++; + } + if (cfg.no_alpn) + { + opts[nopt].option = MHD_OPTION_TLS_NO_ALPN; + opts[nopt].value = 1; + opts[nopt].ptr_value = NULL; + nopt++; + } + opts[nopt].option = MHD_OPTION_END; + opts[nopt].value = 0; + opts[nopt].ptr_value = NULL; + + flags = MHD_USE_NO_LISTEN_SOCKET; + if (cfg.use_tls) + flags |= MHD_USE_TLS; + if (fuzz_verbose) + flags |= MHD_USE_ERROR_LOG; + if (cfg.allow_upgrade) + flags |= MHD_ALLOW_UPGRADE; + + MHD_set_panic_func (&panic_cb, NULL); + d = MHD_start_daemon (flags, + 0, + NULL, NULL, + &ahc, NULL, + MHD_OPTION_ARRAY, opts, + /* Passed through the varargs rather than through + the option array: the array's ptr_value is a + void *, and a function pointer does not + portably fit in one. A NULL here is exactly + equivalent to not passing the option. */ + MHD_OPTION_HTTPS_CERT_CALLBACK, + cfg.use_sni ? &sni_callback : NULL, + MHD_OPTION_END); + if (NULL == d) + { + stat_daemons_failed++; + return 0; + } + stat_daemons++; + if (! stats_registered) + { + stats_registered = 1; + (void) atexit (&print_stats); + } + if (cfg.daemon_info) + { + (void) MHD_get_daemon_info (d, MHD_DAEMON_INFO_LISTEN_FD); + (void) MHD_get_daemon_info (d, MHD_DAEMON_INFO_FLAGS); + (void) MHD_get_daemon_info (d, MHD_DAEMON_INFO_CURRENT_CONNECTIONS); + (void) MHD_get_daemon_info (d, MHD_DAEMON_INFO_BIND_PORT); + } + + /* The connection is opened lazily, when the first segment that carries + wire data is reached, so that a leading op 1 segment can still + override the server name byte 9 selected. */ + tc.sess = NULL; + tc.cred = NULL; + tc.fd = -1; + tc.hs_done = 0; + tc.dead = 1; + + pos = 10u; + while ( (pos + 2 <= size) && + (nseg < MAX_SEGMENTS) ) + { + unsigned int hdr = (unsigned int) data[pos] + | ((unsigned int) data[pos + 1] << 8); + unsigned int op = hdr >> 14; + size_t slen = (size_t) (hdr & 0x3FFF); + + pos += 2; + nseg++; + if (slen > size - pos) + slen = size - pos; + + if (1 == op) + { + /* Server name declaration for the next connection, not wire data. */ + size_t n = slen; + + if (n > MAX_SNI_LEN) + n = MAX_SNI_LEN; + memcpy (sni_name, data + pos, n); + /* GnuTLS wants a plain host name, and an embedded NUL would only + shorten it behind our back. */ + while ( (0 != n) && + ('\0' == sni_name[n - 1]) ) + n--; + sni_name[n] = '\0'; + sni_name_len = n; + pos += slen; + continue; + } + if (0 > tc.fd) + { + tc_start (d, &tc); + if (0 > tc.fd) + break; + } + else if ( (3 == op) && + (nconn < MAX_CONNECTIONS) ) + { + tc_close (d, &tc); + tc_start (d, &tc); + nconn++; + if (0 > tc.fd) + break; + } + if (0 != slen) + tc_send (d, &tc, data + pos, slen); + pos += slen; + pump_and_drain (d, &tc, (2 == op) ? (6u + cfg.extra_pump) : 3u); + } + if (0 > tc.fd) + tc_start (d, &tc); + pump_and_drain (d, &tc, 2u + cfg.extra_pump); + tc_close (d, &tc); + + if (cfg.quiesce) + (void) MHD_quiesce_daemon (d); + MHD_stop_daemon (d); + return 0; +} + + +/* ------------------------------------------------------------------ */ +/* Structure-aware generator */ +/* ------------------------------------------------------------------ */ + +struct sbuf +{ + uint8_t *p; + size_t len; + size_t cap; +}; + + +static void +sb_raw (struct sbuf *b, + const void *v, + size_t n) +{ + if (b->len + n > b->cap) + n = b->cap - b->len; + memcpy (b->p + b->len, v, n); + b->len += n; +} + + +static void +sb_str (struct sbuf *b, + const char *s) +{ + sb_raw (b, s, strlen (s)); +} + + +static void +sb_u32 (struct sbuf *b, + uint32_t v) +{ + char tmp[16]; + unsigned int n = 0; + + do + { + tmp[n++] = (char) ('0' + (v % 10u)); + v /= 10u; + } + while ( (0 != v) && + (n < sizeof (tmp)) ); + while (0 != n) + { + char c = tmp[--n]; + + sb_raw (b, &c, 1); + } +} + + +enum gen_shape +{ + SHAPE_TLS_HTTP = 0, /**< good credentials, real handshake, HTTP over TLS */ + SHAPE_RAW_BYTES, /**< good credentials, junk at the TLS socket */ + SHAPE_ABANDON, /**< a real client that walks away mid-handshake */ + SHAPE_BAD_CREDS, /**< mismatched/garbage/absent key or certificate */ + SHAPE_BAD_OPTIONS, /**< bogus priorities, credential types, no MHD_USE_TLS */ + SHAPE_SNI, /**< the certificate callback, all behaviours */ + SHAPE_PEM_FUZZ, /**< PEM blobs built from the generator's own bytes */ + SHAPE_RECORDS, /**< hand-built TLS records */ + SHAPE_COUNT +}; + +static int forced_shape = -1; +static int forced_shape_read; + +static const char *const gen_methods[] = { + "GET", "POST", "HEAD", "PUT", "OPTIONS", "BREW" +}; + +static const char *const gen_targets[] = { + "/", "/a", "/index.html", "/a?b=c", "/%41%42", "*", "//" +}; + +static const char *const gen_versions[] = { + "HTTP/1.1", "HTTP/1.0", "HTTP/1.2", "HTTP/9.9" +}; + +static const char *const gen_hdr_names[] = { + "Host", "User-Agent", "Accept", "Connection", "X-Fuzz", "Cookie", + "Content-Type", "Expect" +}; + +static const char *const gen_hdr_values[] = { + "x", "localhost", "*/*", "keep-alive", "close", "100-continue", + "text/plain", "a=b" +}; + + +/** + * A plain HTTP request; over TLS this is what makes MHD leave the + * handshake state machine and enter the ordinary parser. + */ +static void +gen_http_request (struct fuzz_rng *rng, + struct sbuf *b) +{ + unsigned int nh; + unsigned int i; + int with_body; + + sb_str (b, gen_methods[fuzz_below (rng, (uint32_t) + (sizeof (gen_methods) + / sizeof (gen_methods[0])))]); + sb_str (b, " "); + sb_str (b, gen_targets[fuzz_below (rng, (uint32_t) + (sizeof (gen_targets) + / sizeof (gen_targets[0])))]); + sb_str (b, " "); + sb_str (b, gen_versions[fuzz_below (rng, (uint32_t) + (sizeof (gen_versions) + / sizeof (gen_versions[0])))]); + sb_str (b, "\r\n"); + with_body = fuzz_chance (rng, 3); + nh = fuzz_below (rng, 4); + for (i = 0; i < nh; i++) + { + sb_str (b, gen_hdr_names[fuzz_below (rng, (uint32_t) + (sizeof (gen_hdr_names) + / sizeof (gen_hdr_names[0])))]); + sb_str (b, ": "); + sb_str (b, gen_hdr_values[fuzz_below (rng, (uint32_t) + (sizeof (gen_hdr_values) + / sizeof (gen_hdr_values[0])))]); + sb_str (b, "\r\n"); + } + if (with_body) + { + unsigned int blen = fuzz_below (rng, 64); + + sb_str (b, "Content-Length: "); + sb_u32 (b, blen); + sb_str (b, "\r\n\r\n"); + for (i = 0; i < blen; i++) + { + char c = (char) ('a' + (int) fuzz_below (rng, 26)); + + sb_raw (b, &c, 1); + } + return; + } + sb_str (b, "\r\n"); +} + + +/** + * Something that looks like a TLS record: a content type, a version, a + * length and a payload. Most of this lands in GnuTLS's record parser + * rather than in MHD, which is why the generator spends only one shape + * on it. + */ +static void +gen_tls_records (struct fuzz_rng *rng, + struct sbuf *b) +{ + unsigned int n = 1 + fuzz_below (rng, 4); + unsigned int i; + unsigned int j; + + for (i = 0; i < n; i++) + { + uint8_t hdr[5]; + unsigned int plen = fuzz_below (rng, 96); + unsigned int declared = fuzz_chance (rng, 3) + ? fuzz_below (rng, 0x4000) + : plen; + + hdr[0] = fuzz_chance (rng, 4) + ? fuzz_byte (rng) + : (uint8_t) (20 + fuzz_below (rng, 4)); + hdr[1] = 0x03; + hdr[2] = (uint8_t) fuzz_below (rng, 5); + hdr[3] = (uint8_t) ((declared >> 8) & 0xFF); + hdr[4] = (uint8_t) (declared & 0xFF); + sb_raw (b, hdr, sizeof (hdr)); + for (j = 0; j < plen; j++) + { + uint8_t v = fuzz_byte (rng); + + sb_raw (b, &v, 1); + } + } +} + + +/** + * Number of leading entries of #prio_tbl that GnuTLS accepts. Beyond + * that the daemon refuses to start, which is a fine thing to fuzz but a + * poor way to reach the handshake. + */ +#define PRIO_VALID_COUNT 7 + + +/** + * Constrain the configuration bytes so that MHD_start_daemon() actually + * succeeds. Used by the shapes whose point is what happens *after* the + * daemon is up; the option surface itself is fuzzed by the shapes that + * do not call this. + */ +static void +make_daemon_startable (struct fuzz_rng *rng, + uint8_t *cfg_bytes) +{ + cfg_bytes[1] &= (uint8_t) ~0x10u; /* no credential type override */ + cfg_bytes[3] &= (uint8_t) ~0xC0u; /* valid trust store, valid DH */ + if (0 != (cfg_bytes[1] & 0x08u)) + cfg_bytes[2] = (uint8_t) fuzz_below (rng, PRIO_VALID_COUNT); + cfg_bytes[7] &= (uint8_t) ~0x08u; /* keep MHD_USE_TLS */ +} + + +/** + * Emit one segment with the given op code. + */ +static void +emit_segment (struct sbuf *out, + unsigned int op, + const uint8_t *payload, + size_t len) +{ + unsigned int hv; + uint8_t hdr[2]; + + if (len > 0x3FFF) + len = 0x3FFF; + hv = (op << 14) | (unsigned int) len; + hdr[0] = (uint8_t) (hv & 0xFF); + hdr[1] = (uint8_t) (hv >> 8); + sb_raw (out, hdr, 2); + sb_raw (out, payload, len); +} + + +static size_t +fuzz_generate (struct fuzz_rng *rng, + uint8_t *buf, + size_t cap) +{ + struct sbuf out; + struct sbuf rb; + uint8_t req[GEN_BUF_SIZE]; + uint8_t cfg_bytes[10]; + enum gen_shape shape; + unsigned int nreq; + unsigned int i; + + out.p = buf; + out.len = 0; + out.cap = cap; + + if (! forced_shape_read) + { + const char *e = getenv ("MHD_FUZZ_SHAPE"); + + forced_shape_read = 1; + if (NULL != e) + forced_shape = atoi (e); + } + if (0 <= forced_shape) + shape = (enum gen_shape) (forced_shape % (int) SHAPE_COUNT); + else + shape = (enum gen_shape) fuzz_below (rng, (uint32_t) SHAPE_COUNT); + + for (i = 0; i < sizeof (cfg_bytes); i++) + cfg_bytes[i] = fuzz_byte (rng); + + /* Byte 0 picks the credentials, byte 4 (bits 0-1) the client, and byte + 1 which options are passed at all; every shape below narrows exactly + those and leaves the rest of the configuration space random. */ + cfg_bytes[7] &= (uint8_t) ~0x08u; /* keep MHD_USE_TLS by default */ + switch (shape) + { + case SHAPE_TLS_HTTP: + cfg_bytes[0] = (uint8_t) (fuzz_chance (rng, 2) ? 0 : 1); + cfg_bytes[4] = (uint8_t) ((cfg_bytes[4] & ~0x03u) | CLIENT_TLS); + cfg_bytes[1] &= (uint8_t) ~0x04u; /* no SNI callback */ + make_daemon_startable (rng, cfg_bytes); + break; + case SHAPE_RAW_BYTES: + cfg_bytes[0] = 0; + cfg_bytes[4] = (uint8_t) ((cfg_bytes[4] & ~0x03u) | CLIENT_RAW); + make_daemon_startable (rng, cfg_bytes); + break; + case SHAPE_ABANDON: + cfg_bytes[0] = (uint8_t) (fuzz_chance (rng, 2) ? 0 : 1); + cfg_bytes[4] = (uint8_t) ((cfg_bytes[4] & ~0x03u) | CLIENT_TLS_ABANDON); + cfg_bytes[7] = (uint8_t) ((cfg_bytes[7] & ~0x07u) + + fuzz_below (rng, 6)); + make_daemon_startable (rng, cfg_bytes); + break; + case SHAPE_BAD_CREDS: + /* entries 2-11 of cred_tbl are the broken ones */ + cfg_bytes[0] = (uint8_t) (2 + fuzz_below (rng, 10)); + cfg_bytes[4] = (uint8_t) ((cfg_bytes[4] & ~0x03u) + | (fuzz_chance (rng, 2) ? CLIENT_TLS + : CLIENT_RAW)); + break; + case SHAPE_BAD_OPTIONS: + cfg_bytes[1] |= 0x18u; /* priorities + credential type */ + cfg_bytes[2] = (uint8_t) fuzz_below (rng, (uint32_t) PRIO_COUNT); + cfg_bytes[3] = fuzz_byte (rng); + if (fuzz_chance (rng, 4)) + cfg_bytes[7] |= 0x08u; /* HTTPS options, but no MHD_USE_TLS */ + cfg_bytes[4] = (uint8_t) ((cfg_bytes[4] & ~0x03u) + | (fuzz_chance (rng, 2) ? CLIENT_TLS + : CLIENT_RAW)); + break; + case SHAPE_SNI: + cfg_bytes[0] = (uint8_t) (fuzz_chance (rng, 3) ? 6 : 0); + cfg_bytes[3] = (uint8_t) + ((cfg_bytes[3] & ~0x38u) + | (fuzz_below (rng, (uint32_t) SNI_BEHAVIOUR_COUNT) << 3)); + cfg_bytes[4] = (uint8_t) ((cfg_bytes[4] & ~0x03u) | CLIENT_TLS | 0x04u); + cfg_bytes[9] = (uint8_t) fuzz_below (rng, (uint32_t) SNI_NAME_COUNT); + make_daemon_startable (rng, cfg_bytes); + cfg_bytes[1] |= 0x04u; /* the certificate callback */ + break; + case SHAPE_PEM_FUZZ: + cfg_bytes[0] = (uint8_t) (12 + fuzz_below (rng, 3)); + cfg_bytes[8] = (uint8_t) (1 + fuzz_below (rng, 64)); + cfg_bytes[4] = (uint8_t) ((cfg_bytes[4] & ~0x03u) + | (fuzz_chance (rng, 2) ? CLIENT_TLS + : CLIENT_RAW)); + break; + case SHAPE_RECORDS: + cfg_bytes[0] = 0; + cfg_bytes[4] = (uint8_t) ((cfg_bytes[4] & ~0x03u) | CLIENT_RECORDS); + make_daemon_startable (rng, cfg_bytes); + break; + case SHAPE_COUNT: + default: + break; + } + /* Byte 9 already carries a server name; now and then hand the same + one over as an op 1 segment instead, so that the segment form is + exercised too. Generated inputs are never written to corpus/, so + unlike the seeds they may use op 1 (see the note at the top). */ + sb_raw (&out, cfg_bytes, sizeof (cfg_bytes)); + if (fuzz_chance (rng, 4)) + { + const char *nm = sni_name_tbl[cfg_bytes[9] & 0x07]; + + emit_segment (&out, 1u, (const uint8_t *) nm, strlen (nm)); + } + + nreq = 1u + (fuzz_chance (rng, 4) ? 1u : 0u); + for (i = 0; i < nreq; i++) + { + unsigned int op; + + rb.p = req; + rb.len = 0; + rb.cap = sizeof (req); + switch (shape) + { + case SHAPE_RECORDS: + gen_tls_records (rng, &rb); + break; + case SHAPE_RAW_BYTES: + { + unsigned int n = 1 + fuzz_below (rng, 256); + unsigned int j; + + for (j = 0; j < n; j++) + { + uint8_t v = fuzz_byte (rng); + + sb_raw (&rb, &v, 1); + } + break; + } + default: + gen_http_request (rng, &rb); + break; + } + if (0 != i) + op = 3u; /* a fresh connection */ + else + op = fuzz_chance (rng, 3) ? 2u : 0u; + /* Split the payload now and then: MHD's parser is incremental and + the TLS record boundaries move with the split. */ + if ( (rb.len > 8) && + fuzz_chance (rng, 3) ) + { + size_t cut = 1 + fuzz_below (rng, (uint32_t) (rb.len - 1)); + + emit_segment (&out, op, req, cut); + emit_segment (&out, 2u, req + cut, rb.len - cut); + } + else + { + emit_segment (&out, op, req, rb.len); + } + } + return out.len; +} + + +/* ------------------------------------------------------------------ */ +/* Built-in seed corpus */ +/* ------------------------------------------------------------------ */ + +struct seed_part +{ + unsigned int op; /**< 0 send, 1 server name, 2 send+pump, 3 new */ + const char *txt; /**< NUL terminated payload */ +}; + +struct seed_def +{ + const char *name; + unsigned char cfg[10]; + struct seed_part parts[4]; +}; + +#define P_END { 0, NULL } + +/* The configuration bytes are written out in full so that a seed can be + read without decoding: see the input format at the top of this file. */ +static const struct seed_def seeds[] = { + /* A complete TLS 1.3 handshake followed by an ordinary request: the + only shape that reaches the handshake -> HTTP parser transition. */ + { "tls-handshake-get", + { 0, 0x00, 0, 0, 0x41, 0x00, 0x04, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END } }, + + /* The same, but the request arrives in two TLS records. */ + { "tls-handshake-split", + { 0, 0x00, 0, 0, 0x41, 0x00, 0x04, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHo" }, + { 2, "st: x\r\n\r\n" }, P_END, P_END } }, + + /* Keep-alive over TLS: two requests on one session, then gnutls_bye(). */ + { "tls-keepalive", + { 0, 0x00, 0, 0, 0x41, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "GET /a HTTP/1.1\r\nHost: x\r\n\r\n" }, + { 2, "GET /b HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n" }, + P_END, P_END } }, + + /* A second connection, i.e. a second handshake on the same daemon. */ + { "tls-second-connection", + { 0, 0x00, 0, 0, 0x41, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "GET /a HTTP/1.1\r\nHost: x\r\n\r\n" }, + { 3, "GET /b HTTP/1.1\r\nHost: x\r\n\r\n" }, + P_END, P_END } }, + + /* Plain HTTP at a TLS port: the record layer sees "GET ..." and the + handshake fails, which is MHD_run_tls_handshake_()'s error arm. */ + { "plain-http-at-tls-port", + { 0, 0x00, 0, 0, 0x00, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END } }, + + /* Nothing at all, then EOF: the connection dies in MHD_TLS_CONN_INIT. */ + { "eof-in-init", + { 0, 0x00, 0, 0, 0x80, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "" }, P_END, P_END, P_END } }, + + /* A real client that walks away after a single handshake round: MHD is + left in MHD_TLS_CONN_HANDSHAKING when the socket closes. */ + { "abandon-handshake", + { 0, 0x00, 0, 0, 0x02, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "" }, P_END, P_END, P_END } }, + + /* Certificate and key do not belong together. */ + { "mismatched-key", + { 2, 0x00, 0, 0, 0x01, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END } }, + + /* No certificate at all: MHD_start_daemon() has to fail cleanly. */ + { "no-certificate", + { 6, 0x00, 0, 0, 0x01, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END } }, + + /* PEM armour with a payload that is not base64. */ + { "garbage-pem", + { 10, 0x00, 0, 0, 0x01, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END } }, + + /* A priority string GnuTLS rejects (index 9 of prio_tbl). */ + { "bogus-priorities", + { 0, 0x08, 9, 0, 0x01, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END } }, + + /* The same string through MHD_OPTION_HTTPS_PRIORITIES_APPEND. */ + { "bogus-priorities-append", + { 0, 0x88, 10, 0, 0x01, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END } }, + + /* TLS 1.2 only on both ends, with the trust store and a client + certificate request. */ + { "tls12-with-trust", + { 0, 0x01, 2, 0, 0x29, 0x00, 0x04, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END } }, + + /* Valid Diffie-Hellman parameters (RFC 3526 group 14). */ + { "valid-dhparams", + { 0, 0x02, 0, 0, 0x21, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END } }, + + /* Malformed Diffie-Hellman parameters: MHD_start_daemon() must fail. */ + { "garbage-dhparams", + { 0, 0x02, 0, 0x80, 0x01, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END } }, + + /* A credential type MHD does not support (GNUTLS_CRD_ANON). */ + { "cred-type-anon", + { 0, 0x10, 0, 0x02, 0x01, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END } }, + + /* GNUTLS_CRD_PSK without a PSK callback: the daemon starts, the + handshake cannot. */ + { "cred-type-psk", + { 0, 0x10, 0, 0x01, 0x01, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END } }, + + /* The SNI callback, answering for the presented name (byte 9 = 0, + "test-mhdserver"). No op 1 segment here or below: see the note on + the shared corpus at the top of this file. */ + { "sni-by-name", + { 6, 0x04, 0, 0x18, 0x05, 0x00, 0x04, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: test-mhdserver\r\n\r\n" }, + P_END, P_END, P_END } }, + + /* The SNI callback failing outright. */ + { "sni-callback-fails", + { 6, 0x04, 0, 0x08, 0x05, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, + P_END, P_END, P_END } }, + + /* The SNI callback answering with an empty certificate list. */ + { "sni-callback-empty", + { 6, 0x04, 0, 0x10, 0x05, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, + P_END, P_END, P_END } }, + + /* The SNI callback answering with a key that is not the certificate's + (byte 9 = 1, "localhost"). */ + { "sni-callback-mismatch", + { 6, 0x04, 0, 0x20, 0x05, 0x00, 0x00, 0x00, 0, 1 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, + P_END, P_END, P_END } }, + + /* The SNI callback answering with a certificate but no key. */ + { "sni-callback-no-key", + { 6, 0x04, 0, 0x28, 0x05, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, + P_END, P_END, P_END } }, + + /* An unknown server name with the by-name callback (byte 9 = 3, + "nobody.example.org"): no certificate. */ + { "sni-unknown-name", + { 6, 0x04, 0, 0x18, 0x05, 0x00, 0x00, 0x00, 0, 3 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, + P_END, P_END, P_END } }, + + /* A PEM blob assembled from the payload below (cred_tbl entry 14). */ + { "fuzzed-pem", + { 14, 0x00, 0, 0, 0x01, 0x00, 0x00, 0x00, 32, 0 }, + { { 0, "MIIFSzCCAzOgAwIBAgIBBDANBgkqhkiG9w0BAQsFADCBgTELMAkGA1UEBhMCUlUx" }, + P_END, P_END, P_END } }, + + /* A truncated TLS record header, then EOF. */ + { "short-record", + { 0, 0x00, 0, 0, 0x00, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "\x16\x03\x01" }, P_END, P_END, P_END } }, + + /* A record that promises far more data than it delivers. */ + { "record-length-lie", + { 0, 0x00, 0, 0, 0x00, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "\x16\x03\x01\x3f\xff\x01\x02\x03\x04" }, P_END, P_END, P_END } }, + + /* The HTTPS options on a daemon started without MHD_USE_TLS. */ + { "no-use-tls-flag", + { 0, 0x0F, 0, 0, 0x00, 0x00, 0x00, 0x08, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END } }, + + /* A tiny connection memory pool with a real TLS session. */ + { "small-pool-tls", + { 0, 0x00, 0, 0, 0x41, 0x01, 0x00, 0x00, 0, 0 }, + { { 0, "GET /aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa HTTP/1.1\r\n" + "Host: x\r\nX-Long: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n\r\n" }, + P_END, P_END, P_END } }, + + /* A request body over TLS, so that the receive adapter is used for + more than the request line. */ + { "tls-request-body", + { 0, 0x00, 0, 0, 0x41, 0x00, 0x00, 0x00, 0, 0 }, + { { 0, "POST /a HTTP/1.1\r\nHost: x\r\nContent-Length: 11\r\n\r\n" + "hello world" }, + P_END, P_END, P_END } }, + + /* The external event loop (MHD_get_fdset() + MHD_run_from_select()). */ + { "tls-external-loop", + { 0, 0x00, 0, 0, 0x41, 0x10, 0x04, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END } }, + + /* MHD_quiesce_daemon() with a live TLS connection. */ + { "tls-quiesce", + { 0, 0x00, 0, 0, 0x41, 0x00, 0x80, 0x00, 0, 0 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END } } +}; + +static uint8_t seed_render_buf[2048]; + + +static size_t +fuzz_seed_count (void) +{ + return sizeof (seeds) / sizeof (seeds[0]); +} + + +static const uint8_t * +fuzz_seed_get (size_t idx, + size_t *len) +{ + const struct seed_def *sd = &seeds[idx]; + struct sbuf b; + unsigned int i; + + b.p = seed_render_buf; + b.len = 0; + b.cap = sizeof (seed_render_buf); + sb_raw (&b, sd->cfg, sizeof (sd->cfg)); + for (i = 0; i < sizeof (sd->parts) / sizeof (sd->parts[0]); i++) + { + if (NULL == sd->parts[i].txt) + break; + emit_segment (&b, sd->parts[i].op, + (const uint8_t *) sd->parts[i].txt, + strlen (sd->parts[i].txt)); + } + *len = b.len; + return seed_render_buf; +} + + +#else /* ! HTTPS_SUPPORT */ + +/* + * MHD was configured without HTTPS (which is what + * contrib/oss-fuzz/build.sh does for the MemorySanitizer build), so + * there is no TLS layer to fuzz. The file still has to produce a valid + * fuzz target: LLVMFuzzerTestOneInput() must exist unconditionally, or + * an OSS-Fuzz build of this harness would silently be an empty binary. + */ + +int +LLVMFuzzerTestOneInput (const uint8_t *data, + size_t size) +{ + fuzz_ignore_sigpipe (); + (void) data; + (void) size; + return 0; +} + + +static size_t +fuzz_generate (struct fuzz_rng *rng, + uint8_t *buf, + size_t cap) +{ + (void) rng; + if (0 == cap) + return 0; + buf[0] = 0; + return 1; +} + + +static const uint8_t no_https_seed[] = { 0 }; + + +static size_t +fuzz_seed_count (void) +{ + return 1; +} + + +static const uint8_t * +fuzz_seed_get (size_t idx, + size_t *len) +{ + (void) idx; + *len = sizeof (no_https_seed); + return no_https_seed; +} + + +#endif /* ! HTTPS_SUPPORT */ + +/* end of fuzz_tls.c */