expand fuzzing logic

This commit is contained in:
Christian Grothoff
2026-07-28 23:04:14 +02:00
parent 5ae817bd6a
commit d59f304524
150 changed files with 8821 additions and 42 deletions
+63 -12
View File
@@ -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
+141 -9
View File
@@ -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}"
@@ -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"
@@ -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"
+53
View File
@@ -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"
+3
View File
@@ -0,0 +1,3 @@
[libfuzzer]
dict = fuzz_eventloop.dict
max_len = 512
+3
View File
@@ -0,0 +1,3 @@
[libfuzzer]
dict = fuzz_memorypool.dict
max_len = 200
+3
View File
@@ -0,0 +1,3 @@
[libfuzzer]
dict = fuzz_options.dict
max_len = 8192
+18 -9
View File
@@ -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" \
+23 -9
View File
@@ -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
+4
View File
@@ -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
+30
View File
@@ -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
+41 -3
View File
@@ -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
+9
View File
@@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 B

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More