mirror of
https://git.gnunet.org/libmicrohttpd.git
synced 2026-09-25 04:09:31 +03:00
expand fuzzing logic
This commit is contained in:
+63
-12
@@ -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
@@ -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"
|
||||
@@ -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"
|
||||
@@ -0,0 +1,3 @@
|
||||
[libfuzzer]
|
||||
dict = fuzz_eventloop.dict
|
||||
max_len = 512
|
||||
@@ -0,0 +1,3 @@
|
||||
[libfuzzer]
|
||||
dict = fuzz_memorypool.dict
|
||||
max_len = 200
|
||||
@@ -0,0 +1,3 @@
|
||||
[libfuzzer]
|
||||
dict = fuzz_options.dict
|
||||
max_len = 8192
|
||||
@@ -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" \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user