expand test suite, add CI/CD logic as done in other GNUnet projects

This commit is contained in:
Christian Grothoff
2026-07-28 12:36:36 +02:00
parent b4f2b6583f
commit e34baa3f1a
240 changed files with 21226 additions and 60 deletions
+3
View File
@@ -55,3 +55,6 @@ src/microhttpd_ws/test_websocket
/po/configure.ac
*.exe.manifest
/libmicrohttpd-*.*.*.tar.*
# Reproducers written by the src/fuzz harnesses (MHD_FUZZ_CRASH_DIR)
crashes/
+22 -1
View File
@@ -5832,6 +5832,26 @@ AS_VAR_IF([use_heavy_tests],["yes"],
AM_CONDITIONAL([RUN_ZZUF_TESTS],[test "x$run_zzuf_tests" = "xyes"])
AM_CONDITIONAL([FORCE_USE_ZZUF_SOCAT],[test "x$zzuf_socat_mandatory" = "xyes"])
# Structure-aware in-process fuzzing harnesses (src/fuzz)
AC_MSG_CHECKING([[whether to build the fuzzing harnesses]])
AC_ARG_ENABLE([[fuzzing]],
[AS_HELP_STRING([[--enable-fuzzing]],
[build the in-process fuzzing harnesses in src/fuzz and run them ]
[as part of "make check"; requires a static build of the library ]
[and is only really useful together with --enable-asserts and ]
[--enable-sanitizers=address,undefined [no]])],
[], [enable_fuzzing="no"])
AS_VAR_IF([enable_fuzzing], ["yes"],
[
AS_VAR_IF([enable_static], ["no"],
[AC_MSG_RESULT([[no]])
AC_MSG_ERROR([[--enable-fuzzing requires --enable-static]])],
[AC_MSG_RESULT([[yes]])])
],
[enable_fuzzing="no"
AC_MSG_RESULT([[no]])])
AM_CONDITIONAL([ENABLE_FUZZING], [[test "x$enable_fuzzing" = "xyes"]])
# Final flags that may interfere with autoconf detections
AS_CASE([${enable_build_type}],[debug|debugger],
[ # Debug build or build for walking with debugger
@@ -5933,7 +5953,8 @@ src/examples/Makefile
src/tools/Makefile
src/testcurl/Makefile
src/testcurl/https/Makefile
src/testzzuf/Makefile])
src/testzzuf/Makefile
src/fuzz/Makefile])
AC_OUTPUT
# Finally: summary
+3
View File
@@ -1,2 +1,5 @@
# This Makefile.am is in the public domain
EXTRA_DIST = xcc ascebc mhd.png mhd.svg mhd_logo.png
EXTRA_DIST += ci
EXTRA_DIST += run-option-matrix.sh
EXTRA_DIST += oss-fuzz
+2
View File
@@ -0,0 +1,2 @@
# Output of the 6-coverage job.
/artifacts/
+53
View File
@@ -0,0 +1,53 @@
FROM docker.io/library/debian:trixie
ENV DEBIAN_FRONTEND=noninteractive
# Everything needed to bootstrap, configure, build and "make check"
# GNU libmicrohttpd. Unlike most GNU Taler / GNUnet components MHD has
# no database, no libgnunet and no libjansson dependency; the only hard
# external dependencies are a C compiler and the autotools. Everything
# else is optional and only unlocks parts of the test suite.
RUN apt-get update -yqq && \
apt-get upgrade -yqq && \
apt-get install -yqq \
git \
ca-certificates \
build-essential \
make \
autoconf \
automake \
libtool \
pkg-config \
texinfo \
file \
&& rm -rf /var/lib/apt/lists/*
# Optional dependencies:
# libcurl*-dev src/testcurl and src/testzzuf (RUN_LIBCURL_TESTS)
# libgnutls28-dev + libgcrypt20-dev HTTPS support (--enable-https=auto)
# zlib1g-dev used by some of the example/test code
# zzuf, socat src/testzzuf (--enable-heavy-tests)
# lcov the coverage job
RUN apt-get update -yqq && \
apt-get install -yqq \
curl \
libcurl4-gnutls-dev \
libgnutls28-dev \
libgcrypt20-dev \
zlib1g-dev \
zzuf \
socat \
lcov \
&& rm -rf /var/lib/apt/lists/*
# 32-bit toolchain for the "-m32" half of the build-configuration matrix
# (job 3-build-matrix). Only available on x86; on other architectures the
# matrix job logs a SKIP for the 32-bit combinations instead of failing.
RUN apt-get update -yqq && \
(apt-get install -yqq gcc-multilib libc6-dev-i386 \
|| echo "no 32-bit toolchain for this architecture, -m32 jobs will SKIP") \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /workdir
CMD ["/bin/bash"]
+92
View File
@@ -0,0 +1,92 @@
GNU libmicrohttpd -- containerised CI jobs
==========================================
Each job is a directory under contrib/ci/jobs/. It holds a config.ini
describing the container to use and an executable job.sh that is the
entry point inside that container. contrib/ci/ci.sh is the driver.
Requirements
------------
podman (the driver calls it directly, not docker)
qemu-user-static only for foreign architectures; without it
"podman run --arch ..." fails in creative ways.
See https://wiki.archlinux.org/title/Podman#Foreign_architectures
Nothing else has to be installed on the host: every job builds and runs
inside a debian:trixie container described by contrib/ci/Containerfile
(or contrib/ci/<arch>.Containerfile, if one exists for the architecture
the job asks for).
Running
-------
One job, from the top of the source tree:
contrib/ci/ci.sh 2-test
All of them, in numeric order:
contrib/ci/run-all-jobs
The source tree is bind-mounted into the container at /workdir, so the
jobs build in your checkout and leave their output there.
The jobs
--------
0-codespell spell check. Runs in nixery.dev/shell/codespell, does
not build a container. Advisory only
(HALT_ON_FAILURE = False).
1-build ./bootstrap && ./configure && make. The smoke build:
default configuration, nothing enabled.
2-test --enable-asserts --enable-sanitizers=address,undefined
plus the full "make check". Dumps the logs of every
failing test.
3-build-matrix the build-configuration matrix: asserts on/off x
sanitizers on/off x six digest/auth configurations x
{64-bit, 32-bit}. Clean out-of-tree build and
"make check" per combination, with a summary table at
the end.
4-fuzz a few seconds per harness of the structure-aware
fuzzers in src/fuzz, plus a replay of the whole
committed corpus.
5-zzuf src/testzzuf, the bit-level fuzzing suite, over a
moderate sweep of zzuf seeds.
6-coverage --enable-coverage build, full "make check", lcov +
genhtml. Writes the HTML report to
contrib/ci/artifacts/coverage/$CI_COMMIT_REF/html/ and
prints the path; it uploads nowhere.
Every push, and nightly
-----------------------
Run on every push:
0-codespell, 1-build, 2-test, 4-fuzz
That is the "--enable-asserts --enable-sanitizers" configuration that
TESTING.md proposal P3 asks for on every push, plus a bounded fuzz run so
a regression is caught in the pull request.
Run nightly:
3-build-matrix, 5-zzuf, 6-coverage
These are the expensive ones. 3-build-matrix is 48 builds; 5-zzuf and
6-coverage each run the heavy-test suite.
Why 3-build-matrix exists
-------------------------
MAX_DIGEST in src/microhttpd/digestauth.c is 16 in an MD5-only build and
32 otherwise, so the out-of-bounds write fixed in commit 5a73c1ae
overflows by a different amount - or not at all - depending only on
configure flags. Testing one configuration cannot answer whether a fix
holds in every build a distribution ships. See TESTING.md, proposal P3.
Not part of CI: OSS-Fuzz
------------------------
contrib/oss-fuzz/ holds the OSS-Fuzz integration. It is a separate,
externally driven path and is deliberately not run from these jobs.
+42
View File
@@ -0,0 +1,42 @@
#!/bin/bash
set -exvuo pipefail
# Run a single CI job in a container.
#
# contrib/ci/ci.sh <job-name> [arch]
#
# Requires podman
# Fails if not found in PATH
OCI_RUNTIME=$(which podman)
REPO_NAME=$(basename "${PWD}")
JOB_NAME="${1}"
JOB_ARCH=$((grep CONTAINER_ARCH contrib/ci/jobs/${JOB_NAME}/config.ini | cut -d' ' -f 3) || echo "${2:-amd64}")
JOB_CONTAINER=$((grep CONTAINER_NAME contrib/ci/jobs/${JOB_NAME}/config.ini | cut -d' ' -f 3) || echo "localhost/${REPO_NAME}:${JOB_ARCH}")
CONTAINER_BUILD=$((grep CONTAINER_BUILD contrib/ci/jobs/${JOB_NAME}/config.ini | cut -d' ' -f 3) || echo "True")
CONTAINERFILE="contrib/ci/$JOB_ARCH.Containerfile"
if ! [[ -f "$CONTAINERFILE" ]];
then
CONTAINERFILE="$(dirname "$CONTAINERFILE")/Containerfile"
fi
echo "Image name: ${JOB_CONTAINER}" 2>&1
echo "Containerfile: ${CONTAINERFILE}" 2>&1
if [ "${CONTAINER_BUILD}" = "True" ] ;
then
"${OCI_RUNTIME}" build \
--arch "${JOB_ARCH}" \
-t "${JOB_CONTAINER}" \
-f "$CONTAINERFILE" .
fi
"${OCI_RUNTIME}" run \
--rm \
-ti \
--arch "${JOB_ARCH}" \
--env CI_COMMIT_REF="$(git rev-parse HEAD)" \
--volume "${PWD}":/workdir \
--workdir /workdir \
"${JOB_CONTAINER}" \
contrib/ci/jobs/"${JOB_NAME}"/job.sh
+6
View File
@@ -0,0 +1,6 @@
[build]
HALT_ON_FAILURE = False
WARN_ON_FAILURE = True
CONTAINER_BUILD = False
CONTAINER_NAME = nixery.dev/shell/codespell
CONTAINER_ARCH = amd64
@@ -0,0 +1,40 @@
# List of "words" that codespell should ignore in the libmicrohttpd sources.
#
# Note: The word sensitivity depends on how the to-be-ignored word is
# spelled in codespell_lib/data/dictionary.txt. F.e. if there is a word
# 'foo' and you add 'Foo' _here_, codespell will continue to complain
# about 'Foo'.
#
# Entries here must be *deliberate*: an identifier, a protocol token, a
# truncated string used by a test, or a spelling this project has chosen.
# Real typos belong in a patch, not in this file.
# "TE" is an HTTP header field name (RFC 9110 10.1.4), and the fuzzers
# and the API headers name it.
TE
# HTTP method token registered by RFC 3253 (WebDAV versioning).
CHECKIN
# The RFC 7578 / RFC 2388 spelling used verbatim in postprocessor.c.
IMPLEMENTORS
# Local variable name for "content length" all over connection.c,
# postprocessor.c and the fuzzers.
clen
# Appears inside autoconf quoting in configure.ac.
fo
# Deliberately truncated tokens used by the string-parser unit tests
# (test_str_token*.c) and by src/testcurl/test_tricky.c, which sends
# intentionally malformed requests.
strin
Strin
Thi
# libmicrohttpd consistently hyphenates this in its API documentation.
re-use
re-used
re-uses
re-using
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -exuo pipefail
job_dir=$(dirname "${BASH_SOURCE[0]}")
skip=$(cat "${job_dir}"/skip.txt)
echo "Current directory: $(pwd)"
codespell -d -I "${job_dir}"/dictionary.txt -S ${skip//$'\n'/,} "$@"
+77
View File
@@ -0,0 +1,77 @@
*/.git/**
*.git
ABOUT-NLS
ChangeLog
*build-aux*
autom4te.cache
config.guess
config.log
config.status
config.sub
configure
configure~
configure.lineno
depcomp
install-sh
ltmain.sh
*libtool*
missing
test-driver
compile
*.in
*.m4
*/m4/*
*/doc/*
*.texi
*.info
*.info-*
*/po/*
*.po
*.pot
*.sed
*.sin
*.header
*/contrib/ascebc/*
*/contrib/fixes-autoconf/*
*/contrib/fixes-libtool/*
*.patch
*.png
*.PNG
*.svg
*.jpg
*.jpeg
*.gif
*.pdf
*.crt
*.key
*.pem
*key
*/keys/*
*.o
*.lo
*.la
*.a
*.so
*.so.*
.deps
.libs
*.log
*.trs
*.gcda
*.gcno
*.info.tmp
*.vcxproj
*.filters
*.sln
*.props
*.rpath
*.hint
*.tag
*.gz
*.tgz
*.bz2
*.xz
*.zip
tags
TAGS
*~
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
set -exuo pipefail
# The smoke build: exactly what a user typing the three commands from
# README/INSTALL gets. No asserts, no sanitizers, default everything.
# If this breaks, nothing else in the pipeline is worth running.
#
# "configure" prints a "Configuration Summary:" block at the end; it is
# the quickest way to see in the CI log which optional features (HTTPS,
# digest algorithms, libcurl tests) were actually compiled in.
./bootstrap
./configure
make -j"$(nproc)"
+6
View File
@@ -0,0 +1,6 @@
[build]
HALT_ON_FAILURE = True
WARN_ON_FAILURE = True
CONTAINER_BUILD = True
CONTAINER_NAME = localhost/libmicrohttpd
CONTAINER_ARCH = amd64
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
set -exuo pipefail
job_dir=$(dirname "${BASH_SOURCE[0]}")
. "${job_dir}"/build.sh
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
set -exuo pipefail
# The every-push configuration of TESTING.md P3:
#
# --enable-asserts --enable-sanitizers=address,undefined
#
# Both halves matter. mhd_assert() is compiled out of an ordinary build,
# so the invariants the library documents are only actually checked with
# --enable-asserts; and three of the four v1.0.7 findings were memory
# safety defects that ASAN reports on the spot but that an unsanitised
# build happily runs through. Anything that is not this configuration is
# nightly (see job 3-build-matrix).
./bootstrap
./configure \
--enable-asserts \
--enable-sanitizers=address,undefined
make -j"$(nproc)"
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
set -exuo pipefail
check_command()
{
make -j"$(nproc)" check
}
print_logs()
{
set +e
echo "###############################################################"
echo "## make check FAILED - dumping the logs of every failing test"
echo "###############################################################"
# Automake writes one .log per test plus a per-directory
# test-suite.log; the per-test logs carry the actual diagnostics.
find . -name 'test-suite.log' -print -exec cat {} \;
find . -name '*.log' -path '*/src/*' -newer config.status -print0 |
while IFS= read -r -d '' logfile; do
case "${logfile}" in
*/config.log) continue ;;
esac
# Only the logs of tests that did not pass: automake writes a
# matching .trs with ":test-result: PASS" for successful ones.
trs="${logfile%.log}.trs"
if [ -f "${trs}" ] && grep -q ':test-result: PASS' "${trs}"; then
continue
fi
echo "--------------------------------------------------------"
echo "## ${logfile}"
echo "--------------------------------------------------------"
cat "${logfile}"
done
set -e
}
if ! check_command ; then
print_logs
exit 1
fi
+6
View File
@@ -0,0 +1,6 @@
[build]
HALT_ON_FAILURE = True
WARN_ON_FAILURE = True
CONTAINER_BUILD = True
CONTAINER_NAME = localhost/libmicrohttpd
CONTAINER_ARCH = amd64
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
set -exuo pipefail
job_dir=$(dirname "${BASH_SOURCE[0]}")
. "${job_dir}"/1-build.sh
. "${job_dir}"/2-test.sh
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
set -exuo pipefail
# Every combination of the matrix is a *clean out-of-tree* build, and
# autoconf refuses those while the source directory itself still carries
# a config.status. A CI checkout is clean; a developer running this job
# by hand on a working tree usually is not.
if [ -f config.status ] || [ -f Makefile ]; then
make distclean || true
fi
if [ -f config.status ]; then
echo "ERROR: source directory is still configured;" \
"run 'make distclean' by hand first." >&2
exit 1
fi
./bootstrap
+258
View File
@@ -0,0 +1,258 @@
#!/bin/bash
#
# The build-configuration matrix of TESTING.md, proposal P3.
#
# Why this job exists, concretely:
#
# MAX_DIGEST in src/microhttpd/digestauth.c is the size of the largest
# digest the build supports. It is 16 in an MD5-only build and 32 when
# SHA-256 or SHA-512/256 are compiled in. The out-of-bounds stack
# write fixed in commit 5a73c1ae wrote up to 64 bytes into a
# MAX_DIGEST-sized array, so *the very same request overflows by a
# different amount - or not at all - depending purely on the configure
# flags*. A CI that only ever builds the default configuration cannot
# see that, and cannot tell whether a fix holds in every shipped build.
# Distributions do ship reduced builds, so the reduced builds have to
# be tested.
#
# The same argument applies to --enable-asserts (mhd_assert() is
# compiled out of a normal build, so every invariant the library
# documents is unchecked there), to the sanitizers (they are the only
# oracle that turns a silent memory-safety defect into a test failure)
# and to -m32 (pointer and size_t width change the arithmetic in the
# read-buffer shift-back path of 29eaa56b).
#
# The sweep is
#
# {asserts on, asserts off}
# x {sanitizers on, sanitizers off}
# x {all digests, MD5 only, SHA-256 only, SHA-512/256 only,
# --disable-dauth, --disable-bauth}
# x {native 64-bit, 32-bit via -m32}
#
# = 48 combinations, each a clean out-of-tree build plus a full
# "make check". This is a nightly job; the every-push subset is job
# 2-test.
#
# Combinations that the environment cannot support (no 32-bit toolchain,
# no sanitizer runtime, no 32-bit sanitizer runtime) are reported as SKIP
# with the reason, never as a failure - a missing i386 libasan is a
# property of the runner, not a defect in MHD.
#
# Knobs:
# MHD_CI_MATRIX_FILTER only run combinations whose tag matches this
# extended regular expression
# MHD_CI_MATRIX_JOBS -j level for each build (default: nproc)
# MHD_CI_MATRIX_KEEP set to 1 to keep the build trees for triage
#
# No "set -e": a failing combination must be recorded and the sweep must
# continue. No "set -x" either (job.sh switches it on): with 48 builds
# the trace drowns the summary, and every step prints its own banner.
set +x
set -uo pipefail
srcdir="$(pwd)"
probe_dir="$(mktemp -d)"
trap 'rm -rf "${probe_dir}"' EXIT
jobs="${MHD_CI_MATRIX_JOBS:-$(nproc)}"
filter="${MHD_CI_MATRIX_FILTER:-.}"
keep="${MHD_CI_MATRIX_KEEP:-0}"
# ---------------------------------------------------------------------
# Environment probes
# ---------------------------------------------------------------------
# Compile *and run* a trivial program. Running it matters: a machine can
# have a working 32-bit compiler and 32-bit headers while being unable to
# execute i386 binaries at all (missing loader, restricted container,
# CONFIG_IA32_EMULATION off). Probing with a compile alone would then
# turn every 32-bit combination into a wall of bogus SIGSEGV failures
# instead of one honest SKIP.
probe_cc()
{
printf 'int main(void){return 0;}\n' > "${probe_dir}/probe.c"
# shellcheck disable=SC2086
${CC:-gcc} "$@" -o "${probe_dir}/probe" "${probe_dir}/probe.c" \
> "${probe_dir}/probe.log" 2>&1 || return 1
"${probe_dir}/probe" >> "${probe_dir}/probe.log" 2>&1
}
have_m32=no
have_san=no
have_m32_san=no
have_m32_curl=no
have_m32_gnutls=no
probe_cc -m32 && have_m32=yes
probe_cc -fsanitize=address,undefined && have_san=yes
if [ "${have_m32}" = "yes" ]; then
probe_cc -m32 -fsanitize=address,undefined && have_m32_san=yes
probe_cc -m32 -lcurl && have_m32_curl=yes
probe_cc -m32 -lgnutls && have_m32_gnutls=yes
fi
echo "== environment probes =============================================="
echo " gcc -m32 .............................. ${have_m32}"
echo " gcc -fsanitize=address,undefined ...... ${have_san}"
echo " gcc -m32 -fsanitize=address,undefined . ${have_m32_san}"
echo " 32-bit libcurl ........................ ${have_m32_curl}"
echo " 32-bit gnutls ......................... ${have_m32_gnutls}"
echo "===================================================================="
# ---------------------------------------------------------------------
# The axes. Every option spelling below is the one configure.ac really
# defines; --enable-md5 and --enable-sha256 take a TYPE argument
# (yes/no/builtin/tlslib) while --enable-sha512-256 is a plain
# enable/disable switch, and --enable-sanitizers takes a comma separated
# list.
# ---------------------------------------------------------------------
# tag configure arguments
digest_tags=(all md5 sha256 sha512-256 nodauth nobauth)
digest_args_all=""
digest_args_md5="--enable-md5=builtin --enable-sha256=no --disable-sha512-256"
digest_args_sha256="--enable-md5=no --enable-sha256=builtin --disable-sha512-256"
digest_args_sha512_256="--enable-md5=no --enable-sha256=no --enable-sha512-256"
digest_args_nodauth="--disable-dauth"
digest_args_nobauth="--disable-bauth"
digest_args()
{
case "$1" in
all) echo "${digest_args_all}" ;;
md5) echo "${digest_args_md5}" ;;
sha256) echo "${digest_args_sha256}" ;;
sha512-256) echo "${digest_args_sha512_256}" ;;
nodauth) echo "${digest_args_nodauth}" ;;
nobauth) echo "${digest_args_nobauth}" ;;
esac
}
results=()
failures=0
record()
{
results+=("$(printf '%-34s %-5s %s' "$1" "$2" "${3:-}")")
}
run_combination()
{
local tag="$1" bits="$2" asserts="$3" san="$4" digest="$5"
local builddir="${srcdir}/build-${tag}"
local -a args=()
if ! printf '%s' "${tag}" | grep -Eq -- "${filter}"; then
return 0
fi
# --- skip decisions -------------------------------------------
if [ "${bits}" = "32" ] && [ "${have_m32}" != "yes" ]; then
record "${tag}" SKIP "no 32-bit toolchain (gcc -m32 fails)"
return 0
fi
if [ "${san}" = "on" ] && [ "${bits}" = "64" ] && [ "${have_san}" != "yes" ]; then
record "${tag}" SKIP "no sanitizer runtime for this compiler"
return 0
fi
if [ "${san}" = "on" ] && [ "${bits}" = "32" ] && [ "${have_m32_san}" != "yes" ]; then
record "${tag}" SKIP "no 32-bit sanitizer runtime (i386 libasan)"
return 0
fi
# --- configure arguments --------------------------------------
if [ "${asserts}" = "on" ]; then
args+=(--enable-asserts)
else
args+=(--disable-asserts)
fi
if [ "${san}" = "on" ]; then
# shellcheck disable=SC2054 # one argument, the comma is part of it
args+=(--enable-sanitizers=address,undefined)
fi
# Word splitting is intended here: digest_args() returns a
# whitespace separated list of configure arguments.
# shellcheck disable=SC2207
args+=($(digest_args "${digest}"))
if [ "${bits}" = "32" ]; then
args+=(CC="${CC:-gcc} -m32")
args+=(--build=x86_64-pc-linux-gnu --host=i686-pc-linux-gnu)
# The 32-bit development libraries of libcurl and GnuTLS are
# usually not installed next to a 64-bit toolchain; turn the
# corresponding parts of the suite off explicitly rather than
# letting configure fail a link test and print a warning.
[ "${have_m32_curl}" = "yes" ] || args+=(--disable-curl)
[ "${have_m32_gnutls}" = "yes" ] || args+=(--disable-https)
# src/examples/json_echo links against libjansson, which is
# not part of MHD's own dependency set and is essentially
# never installed for i386 next to an amd64 toolchain. The
# examples are not built by "make check", so dropping them
# costs no coverage and keeps the 32-bit leg buildable.
args+=(--disable-examples)
fi
# --- clean out-of-tree build ----------------------------------
rm -rf "${builddir}"
mkdir -p "${builddir}"
echo "===================================================================="
echo "== ${tag}"
echo "== ../configure ${args[*]}"
echo "===================================================================="
if ! ( cd "${builddir}" && ../configure "${args[@]}" ); then
record "${tag}" FAIL "configure failed"
failures=$((failures + 1))
return 0
fi
if ! ( cd "${builddir}" && make -j"${jobs}" ); then
record "${tag}" FAIL "build failed"
failures=$((failures + 1))
return 0
fi
if ! ( cd "${builddir}" && make -j"${jobs}" check ); then
record "${tag}" FAIL "make check failed"
failures=$((failures + 1))
echo "## test logs for ${tag}:"
find "${builddir}" -name 'test-suite.log' -print -exec cat {} \; || true
return 0
fi
record "${tag}" PASS ""
[ "${keep}" = "1" ] || rm -rf "${builddir}"
return 0
}
for bits in 64 32; do
for asserts in on off; do
for san in on off; do
for digest in "${digest_tags[@]}"; do
run_combination \
"${bits}bit-asserts-${asserts}-san-${san}-${digest}" \
"${bits}" "${asserts}" "${san}" "${digest}"
done
done
done
done
# ---------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------
echo
echo "===================================================================="
echo "== build-configuration matrix summary"
echo "===================================================================="
printf '%-34s %-5s %s\n' "COMBINATION" "STATE" "NOTE"
for line in "${results[@]}"; do
echo "${line}"
done
echo "===================================================================="
echo "== ${failures} failing combination(s)"
echo "===================================================================="
if [ "${failures}" -ne 0 ]; then
exit 1
fi
exit 0
@@ -0,0 +1,6 @@
[build]
HALT_ON_FAILURE = False
WARN_ON_FAILURE = True
CONTAINER_BUILD = True
CONTAINER_NAME = localhost/libmicrohttpd
CONTAINER_ARCH = amd64
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
set -exuo pipefail
job_dir=$(dirname "${BASH_SOURCE[0]}")
. "${job_dir}"/1-bootstrap.sh
. "${job_dir}"/2-matrix.sh
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
set -exuo pipefail
# The in-process fuzzing harnesses of src/fuzz. See src/fuzz/README and
# src/fuzz/BUILD-INTEGRATION.md.
#
# --enable-fuzzing requires --enable-static (configure enforces it):
# fuzz_str and fuzz_auth_header call functions that are internal to the
# library and therefore not exported from the shared object, so the
# harnesses link against the static archive. --enable-static is the
# default, but say it explicitly so the job does not silently break if
# that default ever changes.
#
# The harnesses carry three oracles - the sanitizers, an
# MHD_set_panic_func() tripwire and a body-framing oracle - and only the
# first of those needs the sanitizers to be switched on, so this job is
# always built with them.
./bootstrap
./configure \
--enable-fuzzing \
--enable-static \
--enable-asserts \
--enable-sanitizers=address,undefined
make -j"$(nproc)"
+29
View File
@@ -0,0 +1,29 @@
#!/bin/bash
set -exuo pipefail
# A short, *bounded* fuzz run: this job has to finish in seconds, not
# hours, so that a regression is caught in the pull request rather than
# the next morning. Long, corpus-accumulating sessions are a different
# thing entirely and are not run here.
#
# MHD_FUZZ_ITERATIONS, MHD_FUZZ_SEED, MHD_FUZZ_MIN_DISCIPLINE and
# MHD_FUZZ_MIN_MEM_LIMIT are ordinary make variables of
# src/fuzz/Makefile.am and can be overridden on the command line. The
# defaults cover the full discipline and memory-limit range.
#
# Every run is exactly reproducible from (harness, seed), so the seed is
# pinned here. Point MHD_CI_FUZZ_SEED at $RANDOM in a nightly job if a
# varying seed is wanted; a failure then prints a copy-pasteable replay
# command.
FUZZ_ITERATIONS="${MHD_CI_FUZZ_ITERATIONS:-20000}"
FUZZ_SEED="${MHD_CI_FUZZ_SEED:-1}"
make -C src/fuzz check \
MHD_FUZZ_ITERATIONS="${FUZZ_ITERATIONS}" \
MHD_FUZZ_SEED="${FUZZ_SEED}"
# Replay the whole committed seed corpus - including
# corpus/known-findings/ - through every harness. This is the part that
# proves a fixed crash stays fixed, and it is cheap.
make -C src/fuzz check-corpus
+6
View File
@@ -0,0 +1,6 @@
[build]
HALT_ON_FAILURE = True
WARN_ON_FAILURE = True
CONTAINER_BUILD = True
CONTAINER_NAME = localhost/libmicrohttpd
CONTAINER_ARCH = amd64
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
set -exuo pipefail
job_dir=$(dirname "${BASH_SOURCE[0]}")
. "${job_dir}"/1-build.sh
. "${job_dir}"/2-fuzz.sh
+26
View File
@@ -0,0 +1,26 @@
#!/bin/bash
set -exuo pipefail
# src/testzzuf is only built when RUN_ZZUF_TESTS is true, which requires
# --enable-heavy-tests *and* a usable libcurl (see src/Makefile.am and
# src/testzzuf/README). The option spelling is
#
# --enable-heavy-tests[=SCOPE] with SCOPE in {basic, full}
#
# ("yes" is an alias for "basic", "all" for "full"; anything else is a
# configure error). "basic" runs 10 client iterations per daemon, "full"
# runs 200 - far too slow for a routine job, so CI uses "basic" and a
# seed sweep instead.
#
# The sanitizer build is the one worth running: enabling sanitizers
# automatically forces the socat relay mode, because zzuf and the
# sanitizers cannot both interpose on the C library. socat mode is also
# the only mode in which the deterministic chunk-extension self-check
# runs, since it needs an unfuzzed channel to MHD.
./bootstrap
./configure \
--enable-heavy-tests=basic \
--enable-asserts \
--enable-sanitizers=address,undefined
make -j"$(nproc)"
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
set -exuo pipefail
# Sweep a moderate range of zzuf seeds. A single seed proves almost
# nothing; a huge range takes correspondingly long, so the recommended
# pattern from src/testzzuf/README is a moderate range per job with
# different jobs given different ranges. ZZUF_SEED_START takes
# precedence over ZZUF_SEED.
#
# On failure both runner scripts print a block containing the failing
# seed and a copy-pasteable replay command; grep the job log for
# "FUZZING TEST FAILED".
ZZUF_START="${MHD_CI_ZZUF_SEED_START:-0}"
ZZUF_STOP="${MHD_CI_ZZUF_SEED_STOP:-64}"
make -C src/testzzuf check \
ZZUF_SEED_START="${ZZUF_START}" \
ZZUF_SEED_STOP="${ZZUF_STOP}"
+6
View File
@@ -0,0 +1,6 @@
[build]
HALT_ON_FAILURE = False
WARN_ON_FAILURE = True
CONTAINER_BUILD = True
CONTAINER_NAME = localhost/libmicrohttpd
CONTAINER_ARCH = amd64
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
set -exuo pipefail
job_dir=$(dirname "${BASH_SOURCE[0]}")
. "${job_dir}"/1-build.sh
. "${job_dir}"/2-zzuf.sh
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
set -exuo pipefail
# --enable-coverage adds -fprofile-arcs -ftest-coverage. Optimisation is
# turned off and debug info on so that the line attribution in the report
# is meaningful.
#
# Heavy tests are enabled because the interesting question this report
# answers is "which parts of the parser does the *whole* suite never
# reach" - and src/testzzuf is a large part of that suite. Sanitizers
# are deliberately NOT enabled here: they change the code that is
# generated and slow the run down, and the coverage numbers are what this
# job is for.
./bootstrap
./configure CFLAGS="-ggdb -O0" \
--enable-coverage \
--enable-asserts \
--enable-heavy-tests=basic
make -j"$(nproc)"
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
set -exuo pipefail
# A failing test still produces useful coverage data, so this step does
# not abort the job; the coverage report is the deliverable here and
# job 2-test is the one that gates on test results.
make -j"$(nproc)" check || echo "WARNING: 'make check' failed; the coverage report below is still generated, but it describes a failing run."
+58
View File
@@ -0,0 +1,58 @@
#!/bin/bash
set -exuo pipefail
# Produce an HTML coverage report into a local artifacts directory.
#
# Unlike the GNU Taler jobs this deliberately does *not* upload anywhere:
# there is no rsync target for libmicrohttpd, and a CI job that silently
# depends on a private host is a job that breaks for everyone else. The
# report is left in the working tree; wire it up to whatever artifact
# store the runner has.
ARTIFACT_DIR="${MHD_CI_ARTIFACT_DIR:-contrib/ci/artifacts/coverage/${CI_COMMIT_REF:-local}}"
mkdir -p "${ARTIFACT_DIR}"
# lcov 2.x turns a lot of things that used to be warnings into errors and
# needs them switched off explicitly; lcov 1.x does not know some of
# those keywords at all. Try the strict-lcov invocation first and fall
# back to the plain one, so the job works on either.
lcov_try()
{
lcov "$@" --ignore-errors mismatch,negative,source,empty,unused \
|| lcov "$@"
}
lcov_try --capture \
--directory . \
--output-file "${ARTIFACT_DIR}/coverage.info"
# Only MHD's own sources are interesting; drop system headers and the
# test programs themselves.
lcov_try --remove "${ARTIFACT_DIR}/coverage.info" \
'/usr/*' \
"${PWD}/src/testcurl/*" \
"${PWD}/src/testzzuf/*" \
"${PWD}/src/fuzz/*" \
"${PWD}/src/examples/*" \
--output-file "${ARTIFACT_DIR}/coverage-lib.info" \
|| cp "${ARTIFACT_DIR}/coverage.info" "${ARTIFACT_DIR}/coverage-lib.info"
genhtml "${ARTIFACT_DIR}/coverage-lib.info" \
--output-directory "${ARTIFACT_DIR}/html" \
--title "GNU libmicrohttpd ${CI_COMMIT_REF:-local}" \
--legend \
--ignore-errors source,empty \
|| genhtml "${ARTIFACT_DIR}/coverage-lib.info" \
--output-directory "${ARTIFACT_DIR}/html" \
--title "GNU libmicrohttpd ${CI_COMMIT_REF:-local}" \
--legend
echo "===================================================================="
echo "== coverage report written to:"
echo "== ${PWD}/${ARTIFACT_DIR}/html/index.html"
echo "== raw tracefiles:"
echo "== ${PWD}/${ARTIFACT_DIR}/coverage.info (everything)"
echo "== ${PWD}/${ARTIFACT_DIR}/coverage-lib.info (library only)"
echo "===================================================================="
lcov_try --summary "${ARTIFACT_DIR}/coverage-lib.info" || true
+6
View File
@@ -0,0 +1,6 @@
[build]
HALT_ON_FAILURE = False
WARN_ON_FAILURE = True
CONTAINER_BUILD = True
CONTAINER_NAME = localhost/libmicrohttpd
CONTAINER_ARCH = amd64
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
set -exuo pipefail
job_dir=$(dirname "${BASH_SOURCE[0]}")
. "${job_dir}"/1-build.sh
. "${job_dir}"/2-test.sh
. "${job_dir}"/3-coverage.sh
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
set -eax
for JOB in $(ls $(dirname $0)/jobs | sort -n); do
$(dirname $0)/ci.sh $JOB;
done;
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
set -ex
# This file is in the public domain.
# Determines the current version of our code.
# Shared between various jobs.
BRANCH=$(git name-rev --name-only HEAD)
if [ -z "${BRANCH}" ]; then
exit 1
else
# "Unshallow" our checkout, but only our current branch, and exclude the submodules.
git fetch --no-recurse-submodules --tags --depth=1000 origin "${BRANCH}"
RECENT_VERSION_TAG=$(git describe --tags --match 'v*.*.*' --exclude '*-dev*' --always --abbrev=0 HEAD || exit 1)
commits="$(git rev-list ${RECENT_VERSION_TAG}..HEAD --count)"
if [ "${commits}" = "0" ]; then
git describe --tag HEAD | sed -r 's/^v//' || exit 1
else
echo $(echo ${RECENT_VERSION_TAG} | sed -r 's/^v//')-${commits}-$(git rev-parse --short=8 HEAD)
fi
fi
+44
View File
@@ -0,0 +1,44 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
################################################################################
#
# OSS-Fuzz Dockerfile for GNU libmicrohttpd.
#
# This file becomes projects/libmicrohttpd/Dockerfile in the google/oss-fuzz
# repository; the Apache-2.0 header above is the one OSS-Fuzz requires for
# files living in that repository.
FROM gcr.io/oss-fuzz-base/base-builder
# libmicrohttpd is an autotools project and the git checkout ships no
# 'configure', so the autotools themselves are build dependencies.
# No other dependency is needed: build.sh configures with --disable-https,
# --disable-curl and --disable-doc, which removes GnuTLS, libcurl and
# texinfo from the picture (see build.sh for the reasoning).
RUN apt-get update && \
apt-get install -y --no-install-recommends \
autoconf \
automake \
libtool \
make \
pkg-config \
zip && \
rm -rf /var/lib/apt/lists/*
RUN git clone --depth 1 https://git.gnunet.org/libmicrohttpd.git libmicrohttpd
WORKDIR $SRC/libmicrohttpd
COPY build.sh $SRC/
+361
View File
@@ -0,0 +1,361 @@
GNU libmicrohttpd -- OSS-Fuzz integration
========================================
This directory contains everything needed to run the in-process fuzzing
harnesses of `src/fuzz/` on Google's OSS-Fuzz service: the build script,
the project metadata, the container recipe, the seed-corpus packager,
per-harness dictionaries and per-harness `.options` files.
It is the "continuous" half of `TESTING.md` section **P4**; `src/fuzz/`
is the other half. Read `src/fuzz/README` first -- it explains what the
four harnesses do, what they have already found, and how to run them
without clang.
-------------------------------------------------------------------
0. This is deliberately NOT part of contrib/ci/
-------------------------------------------------------------------
**OSS-Fuzz is not a CI job and nothing here is wired into
`contrib/ci/jobs/`.** That is on purpose, at the maintainer's request:
* an OSS-Fuzz run is a *continuous, hosted, unbounded* campaign owned
by Google's infrastructure, not a bounded per-push check. A CI job
must terminate in minutes and must be reproducible offline; neither
is true here;
* the artifacts in this directory are consumed by the
`google/oss-fuzz` repository (`projects/libmicrohttpd/`), not by
this tree's build system;
* running these builds needs Docker, the OSS-Fuzz base images and
network access.
The bounded, in-tree fuzzing that *does* belong in CI already exists and
is unrelated to this directory: `make -C src/fuzz check` (a few seconds
per harness) and `make -C src/fuzz check-corpus` (corpus replay). Wire
*those* into `contrib/ci/`, never this.
Nothing in this directory is referenced by any `Makefile.am`; adding it
to the build system is not required and not wanted.
-------------------------------------------------------------------
1. Layout
-------------------------------------------------------------------
build.sh the OSS-Fuzz build script
project.yaml OSS-Fuzz project metadata
Dockerfile the base-builder container recipe
make_seed_corpus.sh packages src/fuzz/corpus/ into
$OUT/<fuzzer>_seed_corpus.zip
fuzz_request.options per-harness libFuzzer options (max_len, dict)
fuzz_str.options
fuzz_auth_header.options
fuzz_postprocessor.options
dicts/fuzz_request.dict per-harness fuzzing dictionaries
dicts/fuzz_str.dict
dicts/fuzz_auth_header.dict
dicts/fuzz_postprocessor.dict
README this file
Of these, only `build.sh`, `project.yaml` and `Dockerfile` are copied
into `google/oss-fuzz`; everything else is read out of the cloned
libmicrohttpd checkout at build time, which is why the dictionaries and
the corpus packager live here and not in the OSS-Fuzz repository.
-------------------------------------------------------------------
2. The four fuzz targets
-------------------------------------------------------------------
fuzz_request a real struct MHD_Daemon driven over a
socketpair; consumes daemon options *and*
stream segmentation from the input
fuzz_str the mhd_str.c primitives with exactly-sized
output buffers
fuzz_auth_header MHD_get_rq_dauth_params_() /
MHD_get_rq_bauth_params_()
fuzz_postprocessor MHD_post_process()
All four are single translation units that export
int LLVMFuzzerTestOneInput (const uint8_t *data, size_t size);
unconditionally. `-DFUZZ_NO_MAIN` (which `build.sh` passes) removes only
the standalone driver's `main()` from `fuzz_common.h`; the fuzz target
itself is never conditionally compiled. There is no
`LLVMFuzzerInitialize()` and none is needed: the two tuning knobs of
`fuzz_request` (`MHD_FUZZ_MIN_DISCIPLINE`, `MHD_FUZZ_MIN_MEM_LIMIT`) are
read lazily with `getenv()` on the first call and default to the *full*
range (-3 and 0), which is what a fuzzing service should explore.
### max_len
`max_len` in the `.options` files is set to the point beyond which the
harness ignores the extra bytes, so libFuzzer does not waste its budget:
fuzz_request 8192 4 configuration bytes + length-prefixed send
segments (2-byte header, payload <= 0x3FFF,
at most 96 segments over at most 8
connections). 8 KiB comfortably holds the
two-request %%NONCE%% digest handshake, a
chunked body with extensions and trailers,
and a body-oracle declaration. Larger
inputs are safe -- every segment is bounded
independently -- but buy almost nothing.
fuzz_str 514 2 selector bytes + the payload, which the
harness truncates to 512.
fuzz_auth_header 4097 1 selector byte + the Authorization header
value, truncated to 4096.
fuzz_postprocessor 8196 4 selector bytes + the POST body, truncated
to 8192.
### close_fd_mask
Deliberately **not** set. The harnesses are quiet by default (MHD's
error log is only enabled when `MHD_FUZZ_VERBOSE` is set, which never
happens under libFuzzer), so there is no output to suppress -- while
`fuzz_report_finding()` writes the description of a non-memory-safety
finding straight to fd 2 and `MHD_PANIC()` writes to stderr too. Closing
those fds would throw away exactly the diagnostics that make a report
actionable.
-------------------------------------------------------------------
3. What build.sh configures, and why
-------------------------------------------------------------------
--enable-static --disable-shared --with-pic
fuzz_str and fuzz_auth_header call MHD-internal symbols compiled
with hidden visibility; they are not exported from
libmicrohttpd.so and can only be reached through the static
archive. OSS-Fuzz also requires the binaries in $OUT to be
self-contained.
--enable-fuzzing
configures src/fuzz/Makefile. Not strictly needed (build.sh
compiles the harnesses itself) but it keeps this build equivalent
to the documented developer build and fails loudly if src/fuzz/
ever stops being wired into configure.ac.
--enable-asserts
keeps mhd_assert() alive. Assertions reachable from network input
are remote aborts; findings K1-K6 in `src/fuzz/README` section 6
are all of that kind and are invisible without this.
--disable-https
the harnesses never speak TLS (they hand MHD an already-connected
AF_UNIX socketpair through MHD_add_connection() and never set
MHD_USE_TLS), so HTTPS adds no coverage; it would drag in GnuTLS,
which OSS-Fuzz fuzzes separately, and it would make the
MemorySanitizer build impossible without an MSan-instrumented
GnuTLS. With HTTPS off, libc and libpthread are the only
external dependencies.
--disable-curl --disable-doc --disable-examples --disable-tools
not needed, and they only add build time and dependencies.
--enable-build-type=neutral
the default, stated explicitly: "neutral" is the only build type
that does not inject its own optimisation/debug flags, so the
$CFLAGS supplied by OSS-Fuzz survive unmodified.
`build.sh` never sets `--enable-sanitizers` or `--enable-coverage`:
OSS-Fuzz provides instrumentation through `$CFLAGS`/`$CXXFLAGS`, and a
second, configure-generated `-fsanitize=` set is a classic way to break
an OSS-Fuzz build. `$CFLAGS` is passed through to `configure` and to
every harness compilation verbatim.
The build is out-of-tree (`$WORK/mhd-build`), so `build.sh` never
modifies the checkout. The git checkout ships no `configure`, so
`./bootstrap` runs first; because `bootstrap` ends in an `|| echo ...`
chain and therefore cannot be trusted to return a failure status,
`build.sh` checks for the product and falls back to `autoreconf -fi`.
-------------------------------------------------------------------
4. Building and running locally with infra/helper.py
-------------------------------------------------------------------
Prerequisites: Docker, python3, and a checkout of `google/oss-fuzz`.
git clone --depth 1 https://github.com/google/oss-fuzz
cd oss-fuzz
mkdir -p projects/libmicrohttpd
cp /path/to/libmicrohttpd/contrib/oss-fuzz/build.sh projects/libmicrohttpd/
cp /path/to/libmicrohttpd/contrib/oss-fuzz/project.yaml projects/libmicrohttpd/
cp /path/to/libmicrohttpd/contrib/oss-fuzz/Dockerfile projects/libmicrohttpd/
Build the image and the targets:
python3 infra/helper.py build_image libmicrohttpd
python3 infra/helper.py build_fuzzers --sanitizer address libmicrohttpd
python3 infra/helper.py check_build libmicrohttpd
`build_fuzzers` accepts an optional path to a local source tree as its
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:
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
Run one:
python3 infra/helper.py run_fuzzer libmicrohttpd fuzz_request
python3 infra/helper.py run_fuzzer libmicrohttpd fuzz_str
python3 infra/helper.py run_fuzzer libmicrohttpd fuzz_auth_header
python3 infra/helper.py run_fuzzer libmicrohttpd fuzz_postprocessor
`run_fuzzer` passes anything after the target name to libFuzzer, and
takes `--corpus-dir` for a persistent corpus:
python3 infra/helper.py run_fuzzer --corpus-dir=/tmp/mhd-corpus \
libmicrohttpd fuzz_request -- -max_total_time=600 -rss_limit_mb=4096
Coverage report (needs a coverage build):
python3 infra/helper.py build_fuzzers --sanitizer coverage libmicrohttpd
python3 infra/helper.py coverage libmicrohttpd --fuzz-target fuzz_request
-------------------------------------------------------------------
5. What to do with a report
-------------------------------------------------------------------
An OSS-Fuzz report contains a *testcase* (the raw input bytes) and a
stack trace. Download the testcase from the report, then:
python3 infra/helper.py reproduce libmicrohttpd fuzz_request ./testcase
The same bytes can also be replayed with the in-tree standalone driver,
which needs no Docker and no clang:
./configure --enable-fuzzing --enable-static --enable-asserts \
--enable-sanitizers=address,undefined
make -C src/fuzz check_PROGRAMS # or: make -C src/fuzz check
src/fuzz/fuzz_request --file=./testcase
Minimising a libFuzzer crash:
python3 infra/helper.py shell libmicrohttpd
# inside the container:
/out/fuzz_request -minimize_crash=1 -runs=100000 /testcase
Once fixed, add the minimised input to `src/fuzz/corpus/` (or to
`src/fuzz/corpus/known-findings/` if it stays interesting as a named
regression) and commit it: `make_seed_corpus.sh` picks it up
automatically on the next OSS-Fuzz build, so the case is re-run forever.
Note that `fuzz_request` is **not** perfectly deterministic: MHD's digest
nonces embed a millisecond timestamp, and whether a nonce counts as stale
therefore depends on the wall clock. Finding K6 is of that kind and
reproduces in only a fraction of replays. If ClusterFuzz marks a
testcase "unreproducible" but the trace points at nonce handling, replay
it in a loop before dismissing it.
-------------------------------------------------------------------
6. Seed corpora
-------------------------------------------------------------------
`make_seed_corpus.sh` builds one zip per target:
$OUT/fuzz_request_seed_corpus.zip
$OUT/fuzz_str_seed_corpus.zip
$OUT/fuzz_auth_header_seed_corpus.zip
$OUT/fuzz_postprocessor_seed_corpus.zip
`src/fuzz/corpus/` is organised per harness by file-name prefix
(`fuzz_<harness>-NN.bin`); inputs are *not* interchangeable between
harnesses, because byte 0 selects a different thing in each, so each zip
gets only its own prefix. `src/fuzz/corpus/README` is documentation and
is excluded.
`src/fuzz/corpus/known-findings/K1..K6*.bin` are byte-exact reproducers
for the six findings documented in `src/fuzz/README` section 6. They are
all `fuzz_request` inputs and are added to that target's seed corpus
(prefixed `known-finding-`), which is what turns them into permanent
regression tests: ClusterFuzz keeps every seed in the corpus and replays
it on every run.
Regenerating the corpus from the harnesses' built-in seeds:
make -C src/fuzz refresh-corpus # ./fuzz_<name> --write-corpus=corpus
That only rewrites the `fuzz_<harness>-NN.bin` files. `known-findings/`
is hand-maintained and is never touched by it.
The script can be run by hand:
contrib/oss-fuzz/make_seed_corpus.sh . /tmp/out
unzip -l /tmp/out/fuzz_request_seed_corpus.zip
-------------------------------------------------------------------
7. Dictionaries
-------------------------------------------------------------------
`dicts/*.dict` are libFuzzer/AFL dictionaries (`name="value"`, with only
`\\`, `\"` and `\xAB` as escapes -- CR and LF are written `\x0d`,
`\x0a`). They are installed to `$OUT/<fuzzer>.dict` and referenced from
`$OUT/<fuzzer>.options`, which is how ClusterFuzz picks them up.
They cover: HTTP methods and versions; framing headers
(`Transfer-Encoding`, `Content-Length`, `chunked`, conflicting and
malformed variants); chunk-size lines and chunk-extension syntax
(`;ext`, `;ext=val`, `;ext="quoted"`, unterminated quotes) -- the exact
grammar of commit `c13f4c64`; digest-auth parameters (`algorithm=`,
`qop=`, `nonce=`, `realm=`, `userhash=`, `username*=`, `nc=`,
`response=`) with the algorithm tokens `MD5`, `SHA-256`, `SHA-512-256`
and their `-sess` variants plus deliberately unknown ones, and `auth` /
`auth-int`; over-long hex `response=` values (commit `5a73c1ae`);
percent-encoding, including truncated, invalid, double and overlong
forms; base64; and the `multipart/form-data` and
`application/x-www-form-urlencoded` vocabulary for the post processor.
`fuzz_request.dict` additionally contains the literal `%%NONCE%%`
placeholder, which the harness rewrites at send time into the most recent
nonce the daemon issued. Without it the digest `response=` code path is
statistically unreachable (see `src/fuzz/README` section 2.3), so it is
the single most valuable token in the file.
The token list in `fuzz_common.h` (`fuzz_interesting_str`) is the
generator's equivalent; the two are intentionally similar but not
generated from each other.
-------------------------------------------------------------------
8. Known limitations
-------------------------------------------------------------------
* **The request-body oracle is inactive under libFuzzer.** The
smuggling oracle of `fuzz_request` (see `src/fuzz/README` section
2.4) compares what MHD delivers to the application against ground
truth declared in an `op 1` segment of the input. It is gated on
`fuzz_pristine`, which the standalone driver sets for un-mutated
inputs and which is never set when `FUZZ_NO_MAIN` is defined -- i.e.
it is off for every OSS-Fuzz execution. That is correct and
intentional: libFuzzer mutates the request without mutating the
declaration, so the ground truth would be wrong and every mutated
input would look like a finding. The consequence is that OSS-Fuzz
catches memory-safety bugs, UB, panics and assertion failures, but
*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.
* **Nondeterminism.** See the note about digest nonce timestamps in
section 5.
* The `[libfuzzer]` section of the `.options` files is the only one
used. ClusterFuzz also understands other sections (for sanitizer
options and, in some versions, environment variables), but nothing
here depends on that, and the exact set of supported sections is not
documented in the OSS-Fuzz repository -- if you ever need to pin
`MHD_FUZZ_MIN_DISCIPLINE` or `MHD_FUZZ_MIN_MEM_LIMIT` for the hosted
runs, verify the mechanism against the ClusterFuzz sources first
rather than assuming it works.
+206
View File
@@ -0,0 +1,206 @@
#!/bin/bash -eu
#
# OSS-Fuzz build script for GNU libmicrohttpd.
#
# This file is in the public domain.
#
# It is executed inside the OSS-Fuzz base-builder image, which exports:
#
# $SRC parent directory of the checked-out sources
# ($SRC/libmicrohttpd, see Dockerfile)
# $WORK scratch directory for build artifacts
# $OUT where the finished fuzz targets must be installed
# $CC $CXX the instrumented compilers
# $CFLAGS $CXXFLAGS sanitizer + coverage flags; MUST be honoured and
# MUST NOT be replaced
# $LIB_FUZZING_ENGINE the fuzzing engine to link against ("-fsanitize=fuzzer",
# a path to libFuzzingEngine.a, the AFL driver, ...)
# $SANITIZER address | undefined | memory | coverage
# $FUZZING_ENGINE libfuzzer | afl | honggfuzz | centipede | none
#
# The same script can be run outside OSS-Fuzz for a local smoke test; every
# variable above has a defensive default below.
#
# See contrib/oss-fuzz/README for the full story and for the local
# infra/helper.py recipe.
# ---------------------------------------------------------------------------
# Defaults, so that the script is also runnable by hand
# ---------------------------------------------------------------------------
SRC="${SRC:-$(cd "$(dirname "$0")/../../.." && pwd)}"
WORK="${WORK:-${SRC}/work}"
OUT="${OUT:-${SRC}/out}"
CC="${CC:-clang}"
CXX="${CXX:-clang++}"
CFLAGS="${CFLAGS:--O1 -fno-omit-frame-pointer -gline-tables-only}"
CXXFLAGS="${CXXFLAGS:-${CFLAGS}}"
LIB_FUZZING_ENGINE="${LIB_FUZZING_ENGINE:--fsanitize=fuzzer}"
SANITIZER="${SANITIZER:-address}"
# Directory holding the libmicrohttpd sources. OSS-Fuzz clones them to
# $SRC/libmicrohttpd (see Dockerfile); allow an override for local runs.
MHD_SRC="${MHD_SRC:-${SRC}/libmicrohttpd}"
# Out-of-tree build directory. Keeping the build out of the source tree
# means build.sh never modifies the checkout, which matters for the
# "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"
mkdir -p "${WORK}" "${OUT}" "${BUILD}"
echo "=== libmicrohttpd OSS-Fuzz build ==="
echo " MHD_SRC = ${MHD_SRC}"
echo " BUILD = ${BUILD}"
echo " OUT = ${OUT}"
echo " SANITIZER = ${SANITIZER}"
echo " LIB_FUZZING_ENGINE = ${LIB_FUZZING_ENGINE}"
# ---------------------------------------------------------------------------
# 1. Bootstrap (the git checkout ships no 'configure')
# ---------------------------------------------------------------------------
cd "${MHD_SRC}"
if [ ! -x ./configure ]; then
echo "--- bootstrapping ---"
# ./bootstrap swallows its own failures (it ends in an '|| echo ...'
# chain), so its exit status cannot be trusted; check for the product
# and fall back to autoreconf.
./bootstrap || true
if [ ! -x ./configure ]; then
autoreconf -fi
fi
fi
# ---------------------------------------------------------------------------
# 2. Configure
# ---------------------------------------------------------------------------
# Rationale for each flag:
#
# --enable-static --disable-shared
# fuzz_str and fuzz_auth_header call MHD-internal symbols
# (MHD_hex_to_bin(), MHD_get_rq_dauth_params_(), MHD_pool_create(),
# ...) that are compiled with hidden visibility and are therefore
# NOT exported from libmicrohttpd.so. Only the static archive can
# be linked. Static linking is also what OSS-Fuzz wants: the
# target binaries in $OUT must not depend on anything outside $OUT.
# --with-pic
# keep the static objects position independent so they can be
# linked into the (PIE) fuzz targets regardless of compiler default.
# --enable-fuzzing
# configures src/fuzz/Makefile. Not strictly needed here (the
# harnesses are compiled by hand below) but it keeps this build
# equivalent to the documented developer build, and it makes
# configure fail loudly if src/fuzz/ ever stops being wired up.
# --enable-asserts
# keeps mhd_assert() alive. Assertions on attacker-reachable paths
# are exactly what this campaign is meant to find; without them
# findings K1-K6 (src/fuzz/README section 6) are invisible.
# --disable-https
# deliberate. The harnesses never speak TLS: they hand MHD an
# already-connected AF_UNIX socketpair via MHD_add_connection() and
# never set MHD_USE_TLS. Enabling HTTPS would (a) add nothing to
# coverage, (b) drag GnuTLS - which OSS-Fuzz fuzzes separately -
# into the link, and (c) make the MemorySanitizer build impossible
# without an MSan-instrumented GnuTLS. With HTTPS off the only
# external dependencies are libc and libpthread, so all three
# sanitizers are usable.
# --disable-curl
# the curl-based test suite is not built here and pulling libcurl in
# would create the same uninstrumented-dependency problem.
# --disable-doc --disable-examples --disable-tools
# nothing of that is needed and it only costs build time (and
# texinfo/pandoc dependencies).
# --disable-dependency-tracking
# one-shot build, no need for .deps.
# --enable-build-type=neutral
# the default, stated explicitly: "neutral" is the one build type
# that does NOT inject its own optimisation/debug flags, so the
# $CFLAGS handed to us by OSS-Fuzz survive unmodified.
#
# NOTE: no --enable-sanitizers and no --enable-coverage. OSS-Fuzz supplies
# the sanitizer and coverage instrumentation through $CFLAGS; letting
# configure add a second, possibly conflicting -fsanitize= set is a classic
# way to break an OSS-Fuzz build.
cd "${BUILD}"
"${MHD_SRC}/configure" \
--enable-static \
--disable-shared \
--with-pic \
--enable-fuzzing \
--enable-asserts \
--disable-https \
--disable-curl \
--disable-doc \
--disable-examples \
--disable-tools \
--disable-dependency-tracking \
--enable-build-type=neutral \
CC="${CC}" \
CFLAGS="${CFLAGS}" \
LDFLAGS="${LDFLAGS:-}"
# ---------------------------------------------------------------------------
# 3. Build the library only
# ---------------------------------------------------------------------------
# Building just src/microhttpd avoids compiling the (large) test suite and
# the src/fuzz check_PROGRAMS, which would be built with the standalone
# driver's main() and are useless here.
make -j"$(nproc)" -C src/microhttpd libmicrohttpd.la
MHD_LIB="${BUILD}/src/microhttpd/.libs/libmicrohttpd.a"
test -f "${MHD_LIB}" || {
echo "ERROR: ${MHD_LIB} was not produced" >&2
exit 1
}
# ---------------------------------------------------------------------------
# 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.
#
# Include path:
# -I${BUILD} for the generated MHD_config.h
# -I${MHD_SRC} for the in-tree headers next to configure.ac
# -I${MHD_SRC}/src/include for microhttpd.h
# -I${MHD_SRC}/src/microhttpd for internal.h, mhd_str.h, gen_auth.h, ...
# -I${MHD_SRC}/src/fuzz for fuzz_common.h
MHD_INCLUDES=(
-I"${BUILD}"
-I"${MHD_SRC}"
-I"${MHD_SRC}/src/include"
-I"${MHD_SRC}/src/microhttpd"
-I"${MHD_SRC}/src/fuzz"
)
for fuzzer in ${FUZZERS}; do
echo "--- building ${fuzzer} ---"
# shellcheck disable=SC2086
$CC $CFLAGS \
-DFUZZ_NO_MAIN \
"${MHD_INCLUDES[@]}" \
-c "${MHD_SRC}/src/fuzz/${fuzzer}.c" \
-o "${WORK}/${fuzzer}.o"
# Link with $CXX: $LIB_FUZZING_ENGINE is a C++ archive for most engines.
# shellcheck disable=SC2086
$CXX $CXXFLAGS \
"${WORK}/${fuzzer}.o" \
-o "${OUT}/${fuzzer}" \
$LIB_FUZZING_ENGINE \
"${MHD_LIB}" \
-lpthread
done
# ---------------------------------------------------------------------------
# 5. Seed corpora, dictionaries and .options files
# ---------------------------------------------------------------------------
"${MHD_SRC}/contrib/oss-fuzz/make_seed_corpus.sh" "${MHD_SRC}" "${OUT}"
for fuzzer in ${FUZZERS}; do
cp "${MHD_SRC}/contrib/oss-fuzz/dicts/${fuzzer}.dict" "${OUT}/${fuzzer}.dict"
cp "${MHD_SRC}/contrib/oss-fuzz/${fuzzer}.options" "${OUT}/${fuzzer}.options"
done
echo "=== done; contents of \$OUT ==="
ls -la "${OUT}"
@@ -0,0 +1,85 @@
# libFuzzer dictionary for fuzz_auth_header (GNU libmicrohttpd).
#
# The harness feeds byte 1.. as the raw value of an "Authorization:" header
# into MHD_get_rq_dauth_params_() / MHD_get_rq_bauth_params_(), so every
# token here is a fragment of that header value. Byte 0 is a selector and
# is not covered by the dictionary.
# --- schemes ------------------------------------------------------------
s_digest="Digest "
s_basic="Basic "
s_bearer="Bearer "
s_negotiate="Negotiate "
s_digest_nosp="Digest"
s_digest_tab="Digest\x09"
# --- separators / quoting ------------------------------------------------
comma=","
comma_sp=", "
equals="="
quote="\""
empty_quoted="\"\""
backslash="\\"
escaped_quote="\\\""
escaped_bs="\\\\"
unterminated_quote="\"aaaa"
sp=" "
tab="\x09"
# --- digest parameters ---------------------------------------------------
p_username="username="
p_username_q="username=\"user\""
p_username_star="username*="
p_username_star_v="username*=UTF-8''user"
p_username_star_bad="username*=''"
p_userhash="userhash="
p_userhash_true="userhash=true"
p_userhash_false="userhash=false"
p_userhash_q="userhash=\"true\""
p_realm="realm="
p_realm_q="realm=\"TestRealm\""
p_realm_empty="realm=\"\""
p_nonce="nonce="
p_nonce_q="nonce=\"0000\""
p_uri="uri="
p_uri_q="uri=\"/a\""
p_response="response="
p_response_q="response=\"00\""
p_cnonce="cnonce="
p_cnonce_q="cnonce=\"abcd\""
p_nc="nc="
p_nc_v="nc=00000001"
p_nc_bad="nc=zzzzzzzz"
p_opaque="opaque="
p_qop="qop="
p_algorithm="algorithm="
p_unknown="frobnicate="
# --- algorithm tokens ----------------------------------------------------
a_md5="MD5"
a_md5_sess="MD5-sess"
a_sha256="SHA-256"
a_sha256_sess="SHA-256-sess"
a_sha512_256="SHA-512-256"
a_sha512_256_sess="SHA-512-256-sess"
a_sha512="SHA-512"
a_sha1="SHA-1"
a_case="sha-256"
# --- qop tokens ----------------------------------------------------------
q_auth="auth"
q_auth_int="auth-int"
q_both="auth,auth-int"
q_quoted="qop=\"auth\""
q_unknown="auth-xyz"
# --- values --------------------------------------------------------------
hex16="0123456789abcdef"
hex32="0123456789abcdef0123456789abcdef"
hex64="0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
hex128="00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
b64_user_pass="dXNlcjpwYXNz"
b64_pad="YQ=="
b64_bad="!!!!"
pct="%41"
pct_bad="%zz"
@@ -0,0 +1,61 @@
# libFuzzer dictionary for fuzz_postprocessor (GNU libmicrohttpd).
#
# Bytes 0-3 of the input select the Content-Type shape, the post-processor
# buffer size, the chunking pattern and the boundary length; byte 4.. is the
# POST body. These tokens are the body syntax of the two encodings
# MHD_post_process() understands.
# --- application/x-www-form-urlencoded ----------------------------------
amp="&"
equals="="
plus="+"
semi=";"
kv="a=b"
kv_empty="a="
kv_noeq="a"
kv_only_eq="="
kv_amp_amp="&&"
pct_a="%41"
pct_space="%20"
pct_nul="%00"
pct_cr="%0d"
pct_lf="%0a"
pct_pct="%25"
pct_bad="%zz"
pct_trunc="%4"
pct_utf8="%C3%A4"
pct_overlong="%C0%80"
# --- multipart/form-data -------------------------------------------------
dashdash="--"
crlf="\x0d\x0a"
crlfcrlf="\x0d\x0a\x0d\x0a"
bare_lf="\x0a"
bare_cr="\x0d"
b_open="--b\x0d\x0a"
b_close="--b--\x0d\x0a"
b_close_nocrlf="--b--"
b_long="--boundary-boundary-boundary\x0d\x0a"
cd="Content-Disposition: "
cd_form="Content-Disposition: form-data; name=\"a\"\x0d\x0a"
cd_file="Content-Disposition: form-data; name=\"a\"; filename=\"f\"\x0d\x0a"
cd_attach="Content-Disposition: attachment; filename=\"f\"\x0d\x0a"
cd_noname="Content-Disposition: form-data\x0d\x0a"
cd_empty_name="Content-Disposition: form-data; name=\"\"\x0d\x0a"
p_name="name=\""
p_filename="filename=\""
p_filename_star="filename*=UTF-8''f"
p_boundary="boundary="
p_boundary_q="boundary=\"b\""
p_charset="charset=UTF-8"
h_ct="Content-Type: "
ct_text="Content-Type: text/plain\x0d\x0a"
ct_octet="Content-Type: application/octet-stream\x0d\x0a"
ct_nested="Content-Type: multipart/mixed; boundary=c\x0d\x0a"
h_cte="Content-Transfer-Encoding: "
cte_binary="Content-Transfer-Encoding: binary\x0d\x0a"
cte_b64="Content-Transfer-Encoding: base64\x0d\x0a"
cte_qp="Content-Transfer-Encoding: quoted-printable\x0d\x0a"
quote="\""
escaped_quote="\\\""
semi_sp="; "
+143
View File
@@ -0,0 +1,143 @@
# libFuzzer dictionary for fuzz_request (GNU libmicrohttpd).
#
# Format: libFuzzer / AFL "name=\"value\"" tokens. Only \\, \" and \xAB are
# valid escapes, so CR/LF are written as \x0d / \x0a.
#
# fuzz_request consumes a header of 4 configuration bytes followed by
# length-prefixed send segments (see src/fuzz/README section 2.2), so a
# dictionary token is useful mainly inside the segment payload - i.e. these
# are the HTTP tokens that the request parser reacts to.
# --- line terminators and separators ------------------------------------
crlf="\x0d\x0a"
crlfcrlf="\x0d\x0a\x0d\x0a"
bare_cr="\x0d"
bare_lf="\x0a"
fold="\x0d\x0a\x20"
fold_tab="\x0d\x0a\x09"
colon_sp=": "
colon=":"
semi=";"
comma=","
equals="="
quote="\""
# --- request line -------------------------------------------------------
m_get="GET "
m_head="HEAD "
m_post="POST "
m_put="PUT "
m_delete="DELETE "
m_options="OPTIONS "
m_connect="CONNECT "
m_trace="TRACE "
m_patch="PATCH "
v_11=" HTTP/1.1"
v_10=" HTTP/1.0"
v_09=" HTTP/0.9"
v_20=" HTTP/2.0"
v_bad=" HTTP/1."
tgt_root="/"
tgt_star="*"
tgt_abs="http://example.org/a"
tgt_query="/a?b"
tgt_query_noeq="/a?b&c"
tgt_dotdot="/../"
tgt_pct="%41"
tgt_pct_bad="%zz"
tgt_pct_trunc="%4"
tgt_nul="%00"
# --- framing headers ----------------------------------------------------
h_host="Host: "
h_cl="Content-Length: "
h_te="Transfer-Encoding: "
h_te_chunked="Transfer-Encoding: chunked\x0d\x0a"
h_te_ident="Transfer-Encoding: identity\x0d\x0a"
h_te_double="Transfer-Encoding: chunked, chunked\x0d\x0a"
h_conn="Connection: "
h_conn_close="Connection: close\x0d\x0a"
h_conn_ka="Connection: keep-alive\x0d\x0a"
h_conn_upgrade="Connection: Upgrade\x0d\x0a"
h_upgrade="Upgrade: "
h_expect="Expect: 100-continue\x0d\x0a"
h_ct="Content-Type: "
h_cookie="Cookie: "
h_trailer="Trailer: "
h_te_hdr="TE: "
v_chunked="chunked"
v_identity="identity"
v_gzip="gzip"
v_100="100-continue"
cl_zero="Content-Length: 0\x0d\x0a"
cl_neg="Content-Length: -1\x0d\x0a"
cl_huge="Content-Length: 18446744073709551615\x0d\x0a"
cl_plus="Content-Length: +5\x0d\x0a"
cl_hex="Content-Length: 0x5\x0d\x0a"
cl_dup="Content-Length: 5\x0d\x0aContent-Length: 6\x0d\x0a"
# --- chunked transfer coding & chunk extensions -------------------------
chunk_last="0\x0d\x0a\x0d\x0a"
chunk_last_nl="0\x0d\x0a"
chunk_5="5\x0d\x0a"
chunk_hex="ff\x0d\x0a"
chunk_big="7fffffffffffffff\x0d\x0a"
chunk_ext=";ext"
chunk_ext_val=";ext=val"
chunk_ext_quoted=";ext=\"quoted\""
chunk_ext_multi=";a=1;b=2;c"
chunk_ext_unterminated=";ext=\"qqqqqqqqqqqqqqqq"
chunk_ext_bs=";ext=\"a\\\"b\""
chunk_ext_ws="; ext = val "
trailer_line="X-Trailer: v\x0d\x0a"
# --- digest / basic authentication --------------------------------------
h_authz="Authorization: "
a_digest="Authorization: Digest "
a_basic="Authorization: Basic "
p_username="username=\""
p_userhash="userhash="
p_userhash_t="userhash=true"
p_userhash_f="userhash=false"
p_username_star="username*=UTF-8''a"
p_realm="realm=\""
p_realm_empty="realm=\"\""
p_nonce="nonce=\""
p_nonce_ph="%%NONCE%%"
p_nonce_zero="nonce=\"0000\""
p_uri="uri=\""
p_response="response=\""
p_cnonce="cnonce=\""
p_nc="nc=00000001"
p_opaque="opaque=\""
p_qop="qop="
p_algorithm="algorithm="
a_md5="MD5"
a_md5_sess="MD5-sess"
a_sha256="SHA-256"
a_sha256_sess="SHA-256-sess"
a_sha512_256="SHA-512-256"
a_sha512_256_sess="SHA-512-256-sess"
a_unknown="SHA-1"
q_auth="auth"
q_auth_int="auth-int"
q_auth_both="auth,auth-int"
resp_hex="0123456789abcdef"
resp_long="00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
basic_b64="dXNlcjpwYXNz"
scheme_bearer="Bearer "
scheme_negotiate="Negotiate "
# --- post bodies (the handler may run MHD_post_process) -----------------
ct_urlenc="Content-Type: application/x-www-form-urlencoded\x0d\x0a"
ct_multipart="Content-Type: multipart/form-data; boundary=b\x0d\x0a"
ct_mixed="Content-Type: multipart/mixed; boundary=b\x0d\x0a"
mp_dashes="--"
mp_boundary="--b\x0d\x0a"
mp_end="--b--\x0d\x0a"
cd_form="Content-Disposition: form-data; name=\"a\"\x0d\x0a"
cd_file="Content-Disposition: form-data; name=\"a\"; filename=\"f\"\x0d\x0a"
kv="a=b"
kv_amp="&"
kv_plus="+"
kv_noeq="a"
+95
View File
@@ -0,0 +1,95 @@
# libFuzzer dictionary for fuzz_str (GNU libmicrohttpd).
#
# Byte 0 selects the target primitive in src/microhttpd/mhd_str.c
# (hex_to_bin, bin_to_hex[_z], pct_decode strict/lenient, in-place pct
# decode, unquote, quote, base64, str_to_uint64, token list handling,
# equal_caseless), byte 1 is an auxiliary knob, byte 2.. is the payload
# (truncated to 512 bytes by the harness). These tokens are payload
# vocabulary.
# --- hex (MHD_hex_to_bin / MHD_bin_to_hex) ------------------------------
hex_digits="0123456789abcdef"
hex_upper="0123456789ABCDEF"
hex_odd="abc"
hex_32="0123456789abcdef0123456789abcdef"
hex_64="0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
hex_128="00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
hex_bad="0g"
hex_0x="0x41"
# --- percent-encoding ----------------------------------------------------
pct="%"
pct_a="%41"
pct_lower="%61"
pct_space="%20"
pct_plus="%2B"
pct_pct="%25"
pct_nul="%00"
pct_cr="%0D"
pct_lf="%0A"
pct_slash="%2F"
pct_bad="%zz"
pct_trunc="%4"
pct_trunc2="%"
pct_double="%2525"
pct_utf8="%C3%A4"
pct_overlong="%C0%80"
pct_surrogate="%ED%A0%80"
plus="+"
# --- quoted-string (MHD_str_unquote / MHD_str_quote) --------------------
quote="\""
empty_quoted="\"\""
escaped_quote="\\\""
escaped_bs="\\\\"
bs="\\"
bs_at_end="a\\"
quoted_word="\"abc\""
quoted_ws="\" \""
quoted_ctl="\"\x01\""
quoted_cr="\"\x0d\""
# --- base64 --------------------------------------------------------------
b64_user_pass="dXNlcjpwYXNz"
b64_pad1="YQ=="
b64_pad2="YWI="
b64_nopad="YWJj"
b64_bad="!!!!"
b64_ws="YWJj\x20"
b64_lf="YWJj\x0a"
b64_url="-_"
b64_eq="="
# --- MHD_str_to_uint64 ---------------------------------------------------
n_zero="0"
n_one="1"
n_max64="18446744073709551615"
n_over64="18446744073709551616"
n_neg="-1"
n_plus="+1"
n_lead0="000000000000000000001"
n_hex="0x10"
n_ws=" 1"
n_dot="1.5"
# --- token lists (MHD_str_remove_token_caseless_ etc.) ------------------
comma=","
comma_sp=", "
sp=" "
tab="\x09"
crlf="\x0d\x0a"
semi=";"
equals="="
t_chunked="chunked"
t_identity="identity"
t_gzip="gzip"
t_close="close"
t_keepalive="keep-alive"
t_upgrade="Upgrade"
t_100="100-continue"
t_md5="MD5"
t_sha256="SHA-256"
t_sha512_256="SHA-512-256"
t_auth="auth"
t_auth_int="auth-int"
t_empty_item=",,"
@@ -0,0 +1,3 @@
[libfuzzer]
dict = fuzz_auth_header.dict
max_len = 4097
@@ -0,0 +1,3 @@
[libfuzzer]
dict = fuzz_postprocessor.dict
max_len = 8196
+3
View File
@@ -0,0 +1,3 @@
[libfuzzer]
dict = fuzz_request.dict
max_len = 8192
+3
View File
@@ -0,0 +1,3 @@
[libfuzzer]
dict = fuzz_str.dict
max_len = 514
+85
View File
@@ -0,0 +1,85 @@
#!/bin/bash -eu
#
# Package src/fuzz/corpus/ into the $OUT/<fuzzer>_seed_corpus.zip files that
# OSS-Fuzz/ClusterFuzz picks up automatically.
#
# This file is in the public domain.
#
# Usage: make_seed_corpus.sh [SRCDIR] [OUTDIR]
#
# SRCDIR top of the libmicrohttpd source tree (default: derived from $0)
# OUTDIR where the zips are written (default: $OUT, else ./out)
#
# Corpus layout in src/fuzz/corpus/:
#
# fuzz_<harness>-NN.bin per-harness seeds; the file name prefix is the
# harness the seed belongs to. Inputs are NOT
# interchangeable between harnesses: byte 0 of
# every harness input selects a different thing.
# known-findings/K*.bin byte-exact reproducers for findings K1-K6
# (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.
# README documentation, not an input; excluded.
#
# The corpus itself is regenerated from the harnesses' built-in seeds with
# make -C src/fuzz refresh-corpus
# (which runs "./fuzz_<name> --write-corpus=corpus" for every harness).
# known-findings/ is hand-maintained and is never touched by that.
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
SRCDIR="${1:-$(cd "${SELF_DIR}/../.." && pwd)}"
OUTDIR="${2:-${OUT:-$(pwd)/out}}"
CORPUS="${SRCDIR}/src/fuzz/corpus"
FINDINGS="${CORPUS}/known-findings"
FUZZERS="fuzz_request fuzz_str fuzz_auth_header fuzz_postprocessor"
if [ ! -d "${CORPUS}" ]; then
echo "ERROR: no corpus directory at ${CORPUS}" >&2
exit 1
fi
mkdir -p "${OUTDIR}"
STAGE="$(mktemp -d "${TMPDIR:-/tmp}/mhd-seed-corpus.XXXXXX")"
trap 'rm -rf "${STAGE}"' EXIT
for fuzzer in ${FUZZERS}; do
dir="${STAGE}/${fuzzer}"
mkdir -p "${dir}"
n=0
for f in "${CORPUS}/${fuzzer}"-*.bin; do
[ -f "${f}" ] || continue
cp "${f}" "${dir}/$(basename "${f}")"
n=$((n + 1))
done
# The K1-K6 reproducers are fuzz_request inputs.
if [ "${fuzzer}" = "fuzz_request" ] && [ -d "${FINDINGS}" ]; then
for f in "${FINDINGS}"/*.bin; do
[ -f "${f}" ] || continue
cp "${f}" "${dir}/known-finding-$(basename "${f}")"
n=$((n + 1))
done
fi
if [ "${n}" -eq 0 ]; then
echo "ERROR: no seeds found for ${fuzzer} in ${CORPUS}" >&2
exit 1
fi
zip_path="${OUTDIR}/${fuzzer}_seed_corpus.zip"
rm -f "${zip_path}"
# -j: flat archive, which is what ClusterFuzz expects.
# -X: no extra file attributes, so the zip stays reproducible.
zip -q -j -X "${zip_path}" "${dir}"/* || {
echo "ERROR: failed to create ${zip_path}" >&2
exit 1
}
echo " ${zip_path}: ${n} seed(s)"
done
+54
View File
@@ -0,0 +1,54 @@
#
# OSS-Fuzz project configuration for GNU libmicrohttpd.
#
# This file should become projects/libmicrohttpd/project.yaml in the
# google/oss-fuzz repository.
#
homepage: "https://www.gnu.org/software/libmicrohttpd/"
main_repo: "https://git.gnunet.org/libmicrohttpd.git"
language: c
primary_contact: "grothoff@gmail.com"
# address: the primary target. The bug class that motivated this work
# (commit 5a73c1ae, a stack buffer overflow in the digest-auth hex
# decoder) is exactly what ASan catches, and the harnesses are
# written for it: every output buffer in fuzz_str and
# fuzz_auth_header is an exactly-sized heap allocation so that the
# ASan redzone traps a single-byte overrun.
# undefined: MHD's parsers do a lot of pointer arithmetic and integer width
# juggling on attacker-controlled lengths (the read-buffer
# shift-back underflow of 29eaa56b is of that family). UBSan is
# cheap and catches those before they become memory errors.
# memory: usable here only because build.sh configures --disable-https and
# --disable-curl, which leaves libc and libpthread as the only
# external dependencies; there is no uninstrumented third-party
# library to poison the results. MSan finds the use of
# uninitialised parser state that ASan cannot see.
sanitizers:
- address
- 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.
architectures:
- x86_64
# 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.)
fuzzing_engines:
- libfuzzer
- afl
- honggfuzz
# Documentation a triager should read before filing/handling a report.
help_url: "https://git.gnunet.org/libmicrohttpd.git/tree/src/fuzz/README"
+343
View File
@@ -0,0 +1,343 @@
#!/bin/sh
# This file is in the public domain.
#
# Re-run already built libmicrohttpd test binaries across the daemon option
# matrix defined in src/microhttpd/mhd_opt_matrix.c.
#
# See --help below for the full documentation.
set -u
me=`basename "$0"`
# ---------------------------------------------------------------------------
# Defaults
# ---------------------------------------------------------------------------
# The binaries that honour the matrix (they read the MHD_TEST_* environment
# variables), plus a few libcurl based ones that at least get re-run in every
# threading/polling mode of the daemon they start themselves.
default_tests="src/microhttpd/test_option_matrix
src/microhttpd/test_rq_shift_back
src/microhttpd/test_chunked_ext
src/microhttpd/test_dauth_malformed
src/microhttpd/test_raw_requests
src/testcurl/test_get
src/testcurl/test_post
src/testcurl/test_put_chunked
src/testcurl/test_process_headers"
profiles_arg=""
jobs=1
builddir=""
timeout_sec=600
verbose=0
list_only=0
usage ()
{
cat <<EOF
Usage: $me [OPTION]... [TEST-BINARY]...
Re-runs libmicrohttpd test binaries across the daemon option matrix. The
matrix is defined once, in src/microhttpd/mhd_opt_matrix.c, and is selected
per run through the environment, so nothing has to be recompiled:
MHD_TEST_PROFILE the profile, by name or by index
MHD_TEST_MEM_LIMIT override MHD_OPTION_CONNECTION_MEMORY_LIMIT
MHD_TEST_DISCIPLINE override MHD_OPTION_CLIENT_DISCIPLINE_LVL
MHD_TEST_STRICT_FOR_CLIENT override the same knob via the deprecated
MHD_OPTION_STRICT_FOR_CLIENT
MHD_TEST_THREADING external | internal | per-connection | pool
MHD_TEST_POLL select | poll | epoll
This script only sets MHD_TEST_PROFILE; the other variables stay available
for a manual run.
Options:
--profiles=LIST comma separated profile names or indices to visit
(default: every profile of the matrix)
--jobs=N run up to N test processes in parallel (default: 1)
--builddir=DIR the root of the built tree (default: the directory this
script is called from, or the parent of contrib/)
--timeout=SEC kill a single run after SEC seconds (default: $timeout_sec)
--list-profiles print the profiles of the matrix and exit
--verbose echo the output of every run
--help print this help and exit
Arguments:
TEST-BINARY... the binaries to run, relative to the build directory or
absolute. The default set is:
EOF
echo "$default_tests" | sed 's/^/ /'
cat <<EOF
Only the src/microhttpd binaries listed above read the
MHD_TEST_* variables; the src/testcurl ones ignore them
and are simply re-run, which is still useful as a
stability check but does not vary the configuration.
Exit status:
0 every run passed (or was skipped)
1 at least one run failed
2 usage error or nothing could be run
A run that exits with 77 is reported as SKIP and does not fail the script;
99 is reported as ERROR and does fail it.
EOF
}
# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------
tests=""
while [ $# -gt 0 ] ; do
case "$1" in
--help | -h)
usage ; exit 0 ;;
--profiles=*)
profiles_arg=`expr "x$1" : 'x--profiles=\(.*\)'` ;;
--jobs=*)
jobs=`expr "x$1" : 'x--jobs=\(.*\)'` ;;
-j)
shift ; jobs="${1:-1}" ;;
--builddir=*)
builddir=`expr "x$1" : 'x--builddir=\(.*\)'` ;;
--timeout=*)
timeout_sec=`expr "x$1" : 'x--timeout=\(.*\)'` ;;
--list-profiles)
list_only=1 ;;
--verbose | -v)
verbose=1 ;;
-*)
echo "$me: unknown option '$1'; try '$me --help'." >&2 ; exit 2 ;;
*)
tests="$tests
$1" ;;
esac
shift
done
case "$jobs" in
'' | *[!0-9]*) echo "$me: --jobs needs a number." >&2 ; exit 2 ;;
esac
[ "$jobs" -ge 1 ] || jobs=1
case "$timeout_sec" in
'' | *[!0-9]*) echo "$me: --timeout needs a number." >&2 ; exit 2 ;;
esac
# ---------------------------------------------------------------------------
# Locate the built tree
# ---------------------------------------------------------------------------
if [ -z "$builddir" ] ; then
if [ -x "src/microhttpd/test_option_matrix" ] ; then
builddir="."
else
d=`dirname "$0"`/..
if [ -x "$d/src/microhttpd/test_option_matrix" ] ; then
builddir="$d"
else
builddir="."
fi
fi
fi
if [ ! -d "$builddir" ] ; then
echo "$me: '$builddir' is not a directory." >&2
exit 2
fi
matrix_bin="$builddir/src/microhttpd/test_option_matrix"
# ---------------------------------------------------------------------------
# The list of profiles
# ---------------------------------------------------------------------------
# The matrix is defined in exactly one place: ask the test binary for it.
# The fallback list is only used when the binary has not been built yet, so
# that --list-profiles still says something useful.
fallback_profiles="default
mem-64
mem-128
mem-256
mem-512
mem-1024
mem-2048
mem-3072
mem-4096
strict-1
strict-2
strict-3
lax-1
lax-2
lax-3
legacy-strict
legacy-lax"
if [ -x "$matrix_bin" ] ; then
all_profiles=`"$matrix_bin" --list-profiles 2>/dev/null` || all_profiles=""
fi
if [ -z "${all_profiles:-}" ] ; then
all_profiles="$fallback_profiles"
fi
if [ -n "$profiles_arg" ] ; then
profiles=`echo "$profiles_arg" | tr ',' '\n' | sed '/^$/d'`
else
profiles="$all_profiles"
fi
if [ "$list_only" -eq 1 ] ; then
echo "$all_profiles"
exit 0
fi
if [ -z "$tests" ] ; then
tests="$default_tests"
fi
# ---------------------------------------------------------------------------
# Run
# ---------------------------------------------------------------------------
workdir=`mktemp -d "${TMPDIR:-/tmp}/mhd-option-matrix.XXXXXX"` || {
echo "$me: cannot create a temporary directory." >&2 ; exit 2 ; }
trap 'rm -rf "$workdir"' 0
trap 'rm -rf "$workdir" ; exit 130' 1 2 3 15
timeout_cmd=""
if command -v timeout >/dev/null 2>&1 ; then
timeout_cmd="timeout -k 5 $timeout_sec"
fi
# Run one (profile, binary) pair and record the outcome.
# $1 profile, $2 binary, $3 result file
run_one ()
{
_prof="$1"
_bin="$2"
_out="$3"
_path="$_bin"
case "$_path" in
/*) ;;
*) _path="$builddir/$_bin" ;;
esac
if [ ! -x "$_path" ] ; then
echo "MISSING $_prof $_bin" > "$_out"
return 0
fi
MHD_TEST_PROFILE="$_prof" ; export MHD_TEST_PROFILE
# shellcheck disable=SC2086
$timeout_cmd "$_path" > "$_out.log" 2>&1
_rc=$?
case "$_rc" in
0) echo "PASS $_prof $_bin" > "$_out" ;;
77) echo "SKIP $_prof $_bin" > "$_out" ;;
99) echo "ERROR $_prof $_bin (exit 99)" > "$_out" ;;
124 | 137)
echo "TIMEOUT $_prof $_bin (after ${timeout_sec}s)" > "$_out" ;;
*) echo "FAIL $_prof $_bin (exit $_rc)" > "$_out" ;;
esac
return 0
}
n=0
running=0
for prof in $profiles ; do
for t in $tests ; do
n=`expr $n + 1`
res="$workdir/r$n"
echo "$prof" > "$res.prof"
echo "$t" > "$res.bin"
if [ "$jobs" -gt 1 ] ; then
run_one "$prof" "$t" "$res" &
running=`expr $running + 1`
if [ "$running" -ge "$jobs" ] ; then
wait
running=0
fi
else
run_one "$prof" "$t" "$res"
if [ "$verbose" -eq 1 ] ; then
cat "$res" 2>/dev/null
fi
fi
done
done
wait
if [ "$n" -eq 0 ] ; then
echo "$me: nothing to run." >&2
exit 2
fi
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
total=0
n_pass=0
n_skip=0
n_fail=0
n_missing=0
echo ""
echo "=========================================================================="
echo " option matrix summary"
echo "=========================================================================="
i=0
for prof in $profiles ; do
line=""
bad=0
base=$i
for t in $tests ; do
i=`expr $i + 1`
res="$workdir/r$i"
if [ -f "$res" ] ; then
status=`cut -d' ' -f1 < "$res"`
else
status="ERROR"
fi
total=`expr $total + 1`
case "$status" in
PASS) n_pass=`expr $n_pass + 1` ;;
SKIP) n_skip=`expr $n_skip + 1` ;;
MISSING) n_missing=`expr $n_missing + 1` ;;
*) n_fail=`expr $n_fail + 1` ; bad=1 ;;
esac
line="$line $status"
done
printf '%-16s%s\n' "$prof" "$line"
if [ "$bad" -ne 0 ] || [ "$verbose" -eq 1 ] ; then
j=$base
for t in $tests ; do
j=`expr $j + 1`
res="$workdir/r$j"
[ -f "$res" ] || continue
status=`cut -d' ' -f1 < "$res"`
case "$status" in
PASS | SKIP | MISSING) continue ;;
esac
echo " --- $t: `cat "$res"`"
if [ -f "$res.log" ] ; then
sed 's/^/ /' < "$res.log" | tail -n 25
fi
done
fi
done
echo "--------------------------------------------------------------------------"
echo "binaries, in the order of the columns above:"
for t in $tests ; do
echo " $t"
done
echo "--------------------------------------------------------------------------"
echo "total $total, pass $n_pass, skip $n_skip, fail $n_fail, missing $n_missing"
if [ "$n_fail" -ne 0 ] ; then
echo "RESULT: FAILED"
exit 1
fi
echo "RESULT: OK"
exit 0
+4
View File
@@ -9,6 +9,10 @@ SUBDIRS += testzzuf
endif
endif
if ENABLE_FUZZING
SUBDIRS += fuzz
endif
if BUILD_EXAMPLES
SUBDIRS += examples
endif
+11
View File
@@ -0,0 +1,11 @@
# The built fuzzing harnesses.
/fuzz_request
/fuzz_str
/fuzz_auth_header
/fuzz_postprocessor
# Reproducers written for a failing input (MHD_FUZZ_CRASH_DIR).
/crashes/
# Seed corpora packaged for OSS-Fuzz by contrib/oss-fuzz/make_seed_corpus.sh.
/*_seed_corpus.zip
+229
View File
@@ -0,0 +1,229 @@
# Build integration for `src/fuzz/`
> **Status: applied.** The three changes described below are already
> present in this tree — `configure.ac` carries the `--enable-fuzzing`
> option and the `ENABLE_FUZZING` conditional, `AC_CONFIG_FILES` lists
> `src/fuzz/Makefile`, and `src/Makefile.am` adds `fuzz` to `SUBDIRS`
> under that conditional. Only §1.3 (the optional configuration-summary
> line) is *not* applied. The sections below are kept as the record of
> what was changed and as the recipe for porting `src/fuzz/` to another
> branch.
>
> For running these same harnesses on OSS-Fuzz — a different build path
> that does **not** go through `src/fuzz/Makefile.am` — see
> `../../contrib/oss-fuzz/` and §6 at the end of this file.
`src/fuzz/Makefile.am` is complete. Three files outside `src/fuzz/`
have to be touched. The snippets below are the *exact* text that was
added; nothing else changes.
The harnesses are guarded by a new automake conditional
`ENABLE_FUZZING` (`--enable-fuzzing`, default **no**), because they are
only meaningful in a build with sanitizers and assertions, and because
they need a static archive of the library (they call functions that are
hidden in the shared object).
---
## 1. `configure.ac`
### 1.1 The `--enable-fuzzing` option and the conditional
Add this next to the other feature checks — a good spot is right after
the `AM_CONDITIONAL([RUN_ZZUF_TESTS], ...)` /
`AM_CONDITIONAL([FORCE_USE_ZZUF_SOCAT], ...)` pair (around line 5833 in
the v1.0.7 tree):
```m4
# Fuzzing harnesses (src/fuzz)
AC_MSG_CHECKING([[whether to build the fuzzing harnesses]])
AC_ARG_ENABLE([[fuzzing]],
[AS_HELP_STRING([[--enable-fuzzing]],
[build the in-process fuzzing harnesses in src/fuzz and run them ]
[as part of "make check"; requires a static build of the library ]
[and is only really useful together with --enable-asserts and ]
[--enable-sanitizers=address,undefined [no]])],
[], [enable_fuzzing="no"])
AS_VAR_IF([enable_fuzzing], ["yes"],
[
AS_VAR_IF([enable_static], ["no"],
[AC_MSG_RESULT([[no]])
AC_MSG_ERROR([[--enable-fuzzing requires --enable-static]])],
[AC_MSG_RESULT([[yes]])])
],
[enable_fuzzing="no"
AC_MSG_RESULT([[no]])])
AM_CONDITIONAL([ENABLE_FUZZING], [[test "x$enable_fuzzing" = "xyes"]])
```
### 1.2 Register the new `Makefile`
In the final `AC_CONFIG_FILES([...])` list (around line 5931 of the
v1.0.7 tree) add `src/fuzz/Makefile`, i.e. change
```
src/microhttpd/Makefile
...
src/testzzuf/Makefile])
```
to
```
src/microhttpd/Makefile
...
src/testzzuf/Makefile
src/fuzz/Makefile])
```
### 1.3 (optional, NOT applied) mention it in the configuration summary
If you want the harness state to show up in the final summary, add a
line next to the other `AC_MSG_NOTICE` entries:
```m4
AC_MSG_NOTICE([[ Fuzzing harnesses: ${enable_fuzzing}]])
```
---
## 2. `src/Makefile.am`
Add the conditional `SUBDIRS` entry. The current file reads
```make
SUBDIRS = include microhttpd .
if RUN_LIBCURL_TESTS
SUBDIRS += testcurl
if RUN_ZZUF_TESTS
SUBDIRS += testzzuf
endif
endif
```
Add immediately after that block:
```make
if ENABLE_FUZZING
SUBDIRS += fuzz
endif
```
Automake derives `DIST_SUBDIRS` from all branches of `SUBDIRS`
automatically, so `make dist` keeps working and ships `src/fuzz/`
regardless of the conditional.
---
## 3. `Makefile.am` (top level) — nothing to do
`src/fuzz/` is reached through `src/Makefile.am`; the top-level file
needs no change.
---
## 4. Regenerate and build
```sh
autoreconf -fi # or ./bootstrap
./configure --enable-fuzzing \
--enable-static \
--enable-asserts \
--enable-sanitizers=address,undefined \
--disable-doc --disable-examples
make
make -C src/fuzz check
```
A full fuzzing session:
```sh
make -C src/fuzz check \
MHD_FUZZ_ITERATIONS=5000000 \
MHD_FUZZ_SEED=$RANDOM
```
Replay the checked-in corpus (what CI should do after a fix):
```sh
make -C src/fuzz check-corpus
```
---
## 5. Notes / caveats
* **`--enable-static` is mandatory.** `fuzz_str` and
`fuzz_auth_header` call `MHD_hex_to_bin()`, `MHD_str_quote()`,
`MHD_pool_create()`, `MHD_get_rq_dauth_params_()` etc., which are
built with `$(HIDDEN_VISIBILITY_CFLAGS)` and are therefore not
exported from `libmicrohttpd.so`. The `-static` in the per-program
`_LDFLAGS` makes libtool pick `.libs/libmicrohttpd.a`.
* `fuzz_auth_header` is only built when `HAVE_ANYAUTH` is true (Basic or
Digest authentication enabled). Inside the harness the individual
code paths are additionally guarded by `DAUTH_SUPPORT` /
`BAUTH_SUPPORT`, and `fuzz_str` guards `MHD_str_unquote()`,
`MHD_str_quote()` and `MHD_base64_to_bin_n()` the same way, so a
`--disable-dauth --disable-bauth` build still compiles.
* `fuzz_postprocessor` needs `--enable-postprocessor` (the default); if
you build with `--disable-postprocessor`, additionally guard it with
the existing `HAVE_POSTPROCESSOR` conditional:
```make
if HAVE_POSTPROCESSOR
check_PROGRAMS += fuzz_postprocessor
endif
```
(it is currently listed unconditionally, matching the default build).
* The harnesses include internal headers (`internal.h`,
`memorypool.h`, `gen_auth.h`, `mhd_str.h`), hence the
`-I$(top_srcdir)/src/microhttpd` in `AM_CPPFLAGS`. `MHD_config.h` is
found through automake's default `-I$(top_builddir)`.
* `make check` in `src/fuzz` runs the full discipline / memory-limit
range (`MHD_FUZZ_MIN_DISCIPLINE=-3`, `MHD_FUZZ_MIN_MEM_LIMIT=0`) with
50000 iterations per harness, roughly 3 seconds in total under
ASAN+UBSAN. The findings K1-K6 of `README` section 6 are all fixed on
master, so the full range is clean; their reproducers stay in
`corpus/known-findings/` as regressions.
---
## 6. The OSS-Fuzz build path (does not use this Makefile.am)
`contrib/oss-fuzz/` builds the *same four harness sources* for
libFuzzer/AFL++/honggfuzz without going through `src/fuzz/Makefile.am`
at all. It configures the library out of tree, then compiles each
harness by hand with `-DFUZZ_NO_MAIN` and links it against
`$LIB_FUZZING_ENGINE`:
```sh
$CC $CFLAGS -DFUZZ_NO_MAIN \
-I$BUILD -I$SRCDIR -I$SRCDIR/src/include \
-I$SRCDIR/src/microhttpd -I$SRCDIR/src/fuzz \
-c $SRCDIR/src/fuzz/fuzz_request.c -o $WORK/fuzz_request.o
$CXX $CXXFLAGS $WORK/fuzz_request.o -o $OUT/fuzz_request \
$LIB_FUZZING_ENGINE $BUILD/src/microhttpd/.libs/libmicrohttpd.a -lpthread
```
Two consequences for anyone editing `src/fuzz/`:
* **`LLVMFuzzerTestOneInput()` must stay unconditional.** Only
`main()` may be `#ifndef FUZZ_NO_MAIN`; a harness whose fuzz target is
itself conditional silently produces an empty OSS-Fuzz binary.
* **Anything the fuzz target needs must not live inside the
`#ifndef FUZZ_NO_MAIN` block of `fuzz_common.h`.** Today the split is
right: the PRNG, the crash bookkeeping and `fuzz_report_finding()` are
outside it; the generator loop, the mutator, the corpus walker and
`main()` are inside.
`contrib/oss-fuzz/` also relies on two things this directory provides:
`make -C src/fuzz refresh-corpus` (to regenerate `corpus/`) and the
`corpus/known-findings/` reproducers, which it packages into
`fuzz_request_seed_corpus.zip` so that K1–K6 become permanent
regressions. It is deliberately **not** part of `contrib/ci/jobs/`.
+124
View File
@@ -0,0 +1,124 @@
# This Makefile.am is in the public domain
# In-process fuzzing harnesses. See README in this directory.
#
# The harnesses are built only with --enable-fuzzing (see
# BUILD-INTEGRATION.md). They are ordinary check_PROGRAMS with a
# built-in deterministic driver, so they need neither clang nor
# libFuzzer nor AFL++; with those available the very same sources can be
# compiled into libFuzzer/AFL++ targets (again, see the README).
SUBDIRS = .
# Number of generate/mutate iterations per harness during "make check".
# Raise for a real fuzzing session, e.g.
# make MHD_FUZZ_ITERATIONS=1000000 MHD_FUZZ_SEED=42 check
# The whole suite takes about three seconds under ASAN+UBSAN at this value.
MHD_FUZZ_ITERATIONS = 50000
# PRNG seed; every run is exactly reproducible from (harness, seed).
MHD_FUZZ_SEED = 1
# Lower bound for MHD_OPTION_CLIENT_DISCIPLINE_LVL in fuzz_request.
# -3 is the full range: it includes the deliberately non-conformant
# parsing modes, which is where the most interesting inputs live.
MHD_FUZZ_MIN_DISCIPLINE = -3
# Lower bound for MHD_OPTION_CONNECTION_MEMORY_LIMIT in fuzz_request.
# 0 is the full range, including the tiny pools needed to reach the
# read-buffer "shift back" code path.
MHD_FUZZ_MIN_MEM_LIMIT = 0
# Where reproducers for failing inputs are written.
MHD_FUZZ_CRASH_DIR = crashes
AM_CPPFLAGS = \
-I$(top_srcdir)/src/include \
-I$(top_srcdir)/src/microhttpd \
$(CPPFLAGS_ac)
AM_CFLAGS = $(CFLAGS_ac)
AM_LDFLAGS = $(LDFLAGS_ac)
AM_TESTS_ENVIRONMENT = $(TESTS_ENVIRONMENT_ac) \
MHD_FUZZ_ITERATIONS="$(MHD_FUZZ_ITERATIONS)" ; \
export MHD_FUZZ_ITERATIONS ; \
MHD_FUZZ_SEED="$(MHD_FUZZ_SEED)" ; export MHD_FUZZ_SEED ; \
MHD_FUZZ_MIN_DISCIPLINE="$(MHD_FUZZ_MIN_DISCIPLINE)" ; \
export MHD_FUZZ_MIN_DISCIPLINE ; \
MHD_FUZZ_MIN_MEM_LIMIT="$(MHD_FUZZ_MIN_MEM_LIMIT)" ; \
export MHD_FUZZ_MIN_MEM_LIMIT ; \
MHD_FUZZ_CRASH_DIR="$(MHD_FUZZ_CRASH_DIR)" ; \
export MHD_FUZZ_CRASH_DIR ;
if USE_COVERAGE
AM_CFLAGS += -fprofile-arcs -ftest-coverage
endif
# fuzz_str and fuzz_auth_header call functions that are internal to the
# library (mhd_str.c, gen_auth.c, memorypool.c) and therefore hidden in
# the shared object; link them against the static archive. This
# requires a build with --enable-static (the default).
LDADD = \
$(top_builddir)/src/microhttpd/libmicrohttpd.la
$(top_builddir)/src/microhttpd/libmicrohttpd.la: $(top_builddir)/src/microhttpd/Makefile
@echo ' cd $(top_builddir)/src/microhttpd && $(MAKE) $(AM_MAKEFLAGS) libmicrohttpd.la'; \
$(am__cd) $(top_builddir)/src/microhttpd && $(MAKE) $(AM_MAKEFLAGS) libmicrohttpd.la
check_PROGRAMS = \
fuzz_request \
fuzz_str \
fuzz_postprocessor
if HAVE_ANYAUTH
check_PROGRAMS += \
fuzz_auth_header
endif
.NOTPARALLEL:
TESTS = $(check_PROGRAMS)
fuzz_request_SOURCES = \
fuzz_request.c fuzz_common.h
fuzz_request_LDFLAGS = $(AM_LDFLAGS) -static
fuzz_str_SOURCES = \
fuzz_str.c fuzz_common.h
fuzz_str_LDFLAGS = $(AM_LDFLAGS) -static
fuzz_auth_header_SOURCES = \
fuzz_auth_header.c fuzz_common.h
fuzz_auth_header_LDFLAGS = $(AM_LDFLAGS) -static
fuzz_postprocessor_SOURCES = \
fuzz_postprocessor.c fuzz_common.h
fuzz_postprocessor_LDFLAGS = $(AM_LDFLAGS) -static
# The whole corpus directory is distributed as-is.
EXTRA_DIST = \
README \
BUILD-INTEGRATION.md \
corpus
CLEANFILES = \
$(MHD_FUZZ_CRASH_DIR)/*.bin
# Regenerate the on-disk seed corpus from the built-in one.
.PHONY: refresh-corpus
refresh-corpus: $(check_PROGRAMS)
for p in $(check_PROGRAMS) ; do \
./$$p --write-corpus=$(srcdir)/corpus || exit 1 ; \
done
# Replay the whole on-disk corpus through every harness; this is what a
# CI regression run should do after a crash has been fixed.
.PHONY: check-corpus
check-corpus: $(check_PROGRAMS)
for p in $(check_PROGRAMS) ; do \
MHD_FUZZ_MIN_DISCIPLINE="$(MHD_FUZZ_MIN_DISCIPLINE)" \
MHD_FUZZ_MIN_MEM_LIMIT="$(MHD_FUZZ_MIN_MEM_LIMIT)" \
./$$p --corpus-dir=$(srcdir)/corpus || exit 1 ; \
done
+560
View File
@@ -0,0 +1,560 @@
GNU libmicrohttpd -- in-process fuzzing harnesses
=================================================
This directory contains four in-process fuzzing harnesses for MHD. All
of them are *dual mode*:
* they export the libFuzzer entry point
int LLVMFuzzerTestOneInput (const uint8_t *data, size_t size);
so the very same source can be linked with clang/libFuzzer, AFL++ or
OSS-Fuzz, and
* they ship a **built-in standalone driver** (`fuzz_common.h`) with a
deterministic, seeded generator + mutator loop, so they are useful
with nothing but gcc and `-fsanitize=address,undefined`.
The standalone driver is compiled unless `FUZZ_NO_MAIN` is defined.
-------------------------------------------------------------------
1. The harnesses
-------------------------------------------------------------------
fuzz_request.c the flagship. Feeds arbitrary bytes into a real
`struct MHD_Daemon` through a `socketpair()`,
using MHD_USE_NO_LISTEN_SOCKET +
MHD_add_connection() and external polling
(MHD_run()). Everything runs in one thread, so
the harness is deterministic and fast (~20k
requests/s under ASAN+UBSAN).
fuzz_str.c direct fuzzing of the string primitives in
src/microhttpd/mhd_str.c. Every output buffer is
malloc()ed at *exactly* the documented size so
that ASAN's redzone catches a one-byte overrun.
fuzz_auth_header.c direct fuzzing of the "Authorization:" header
parsers, MHD_get_rq_dauth_params_() and
MHD_get_rq_bauth_params_() (gen_auth.c), through
a minimal fabricated `struct MHD_Connection`.
~250k execs/s.
fuzz_postprocessor.c fuzzing of MHD_post_process() with random
Content-Type (urlencoded / multipart with random
boundaries), random post-processor buffer sizes
and random chunking of the POST data.
Shared code lives in `fuzz_common.h` (header-only, so every harness
stays a single translation unit).
-------------------------------------------------------------------
2. Design of fuzz_request
-------------------------------------------------------------------
2.1 Why a socketpair
--------------------
MHD_add_connection() accepts any already-connected socket, so a
`socketpair(AF_UNIX, SOCK_STREAM)` is enough: no listen socket, no port,
no TCP stack, no second thread. The daemon is started with
MHD_USE_NO_LISTEN_SOCKET and *without* MHD_USE_INTERNAL_POLLING_THREAD,
and the harness pumps it with MHD_run() between sends. A fake
127.0.0.1 `struct sockaddr_in` is passed so that per-IP accounting and
MHD_get_connection_info() see something sane.
2.2 Input format
----------------
byte 0 connection memory limit selector
(index into {default,128,192,256,320,384,512,768,1024,
1400,1500,2048,4096,32768})
byte 1 handler behaviour bitmask
0x01 call MHD_digest_auth_check3() /
MHD_queue_auth_required_response3()
0x02 call MHD_basic_auth_get_username_password3()
0x04 run the request body through MHD_post_process()
0x08 iterate MHD_get_connection_values() over headers,
GET arguments, cookies and footers
0x20 reply with a larger, copied response body
0x40 reply 403 instead of 200
byte 2 low nibble: MHD_OPTION_CLIENT_DISCIPLINE_LVL selector
(index into {-3,-2,-1,0,1,2});
high nibble: reserved for MHD_OPTION_SERVER_INSANITY
(MHD 1.0.7 only defines MHD_DSC_SANE, so the value is 0)
byte 3 digest configuration: bits 0-1 select the algorithm of the
401 challenge {SHA-256, MD5, SHA-512-256, SHA-256},
bit 2 selects the QOP, bits 4-5 select MHD_OPTION_NONCE_NC_SIZE
byte 4.. a sequence of *send segments*. Each segment starts with a
little-endian 16 bit header:
(op << 14) | length length <= 0x3FFF
op 0 send `length` bytes on the current connection
op 1 the payload is the *expected decoded request body*
(ground truth for the body oracle, see 2.4); it is
not sent
op 2 send, then pump the daemon for extra rounds
op 3 close the current connection, open a fresh one on
the same daemon, then send
An explicit length encoding (rather than a magic delimiter) is used so
that the fuzzer can move a split point without having to invent an
escaping scheme. Splitting matters: MHD's parser is incremental and
several bugs only appear for particular split points.
2.3 The %%NONCE%% placeholder
-----------------------------
Interesting parts of digestauth.c are only reached *after* the client
presents a nonce that MHD itself generated. A stateless fuzzer can
never guess one. Therefore the harness rewrites the literal ASCII token
%%NONCE%%
inside a segment, at send time, into the most recent `nonce="..."` value
seen in a response from the daemon. A generated (or hand-written) input
can thus be:
request 1: GET /a -> handler calls MHD_digest_auth_check3(),
gets MHD_DAUTH_WRONG_HEADER and replies
401 + WWW-Authenticate: Digest ... nonce="X"
op 3: new connection
request 2: GET /a with Authorization: Digest ... nonce="%%NONCE%%"
which walks all the way into the 'response' comparison. Without this
the over-long `response=` stack overflow (see 5.4) is unreachable.
2.4 Oracles
-----------
Memory errors are caught by ASAN/UBSAN and aborts by the signal
handlers. In addition fuzz_request installs two behavioural oracles:
a) MHD_set_panic_func() -- any MHD_PANIC() reached from network input
is a finding (a remote abort), not a legitimate "API violation".
b) A request-body oracle. Framing bugs (chunked transfer coding,
Content-Length) do not corrupt memory, they corrupt *data*, which
is exactly what HTTP request smuggling exploits. The input can
therefore declare the expected decoded body in an `op 1` segment.
Every byte that MHD hands to the application must be the next
expected byte, and when MHD completes the request the delivered
body must be complete. MHD is free to reject the request at any
point -- only what it *does* deliver is checked.
The declaration is honoured only for un-mutated inputs (the driver
exposes this as `fuzz_pristine`), because a random mutation would
of course invalidate the ground truth.
2.5 The generator
-----------------
Purely random bytes essentially never form a valid HTTP request, so the
standalone driver uses a small HTTP grammar (`fuzz_generate()`), and
then optionally applies byte-level mutations on top. Shapes:
0 SHAPE_PLAIN random method/target/version + headers
1 SHAPE_NOHDR_QARG *no header lines at all* plus a trailing
query argument without '=' (this is the
exact shape needed for the read-buffer
shift-back bug; the generator also forces a
small connection memory pool for it)
2 SHAPE_CL_BODY Content-Length body + body oracle
3 SHAPE_CHUNKED chunked body with chunk extensions
(";ext", ";ext=val", ";ext=\"quoted\"",
";a=1;b=2;c") + trailers + body oracle
4 SHAPE_DIGEST_SIMPLE Authorization: Digest with a randomised
parameter set, including unknown
`algorithm=` tokens and `response=` values
of every length up to 128 hex digits
5 SHAPE_DIGEST_REPLAY the two-request nonce handshake of 2.3
6 SHAPE_BASIC Authorization: Basic with random base64
7 SHAPE_POST_FORM urlencoded / multipart POST bodies
8 SHAPE_WEIRD folded headers, bare CR, bare LF,
whitespace before the colon, percent
encoding, absolute-form targets, ...
`MHD_FUZZ_SHAPE=<n>` restricts the generator to a single shape, which is
very handy for triage and for regression-testing a specific past bug.
-------------------------------------------------------------------
3. Running the harnesses
-------------------------------------------------------------------
Build (gcc only, no clang required):
SRC=/path/to/libmicrohttpd # configured build tree
gcc -g -O1 -Wall -Wextra \
-fsanitize=address,undefined -fno-sanitize-recover=all \
-I$SRC -I$SRC/src/include -I$SRC/src/microhttpd -I$SRC/src/fuzz \
-o fuzz_request $SRC/src/fuzz/fuzz_request.c \
$SRC/src/microhttpd/.libs/libmicrohttpd.a -lpthread
(the same command line for fuzz_str, fuzz_auth_header and
fuzz_postprocessor; the static archive is required because fuzz_str and
fuzz_auth_header use functions that are hidden in the shared object).
Options of the built-in driver (identical for all harnesses):
--iterations=N number of generate/mutate iterations [3000]
--seed=N PRNG seed; (harness, seed) fully determines a run
--corpus-dir=DIR replay every regular file in DIR and exit
--file=PATH replay a single input and exit (crash repro)
--crash-dir=DIR where reproducers are written [crashes]
--timeout=SEC per-iteration watchdog, 0 disables [20]
--write-corpus=DIR dump the built-in seed corpus to DIR
--skip-seeds do not replay the built-in corpus first
--verbose enable MHD's error log + print statistics
--help
Environment variables (all optional):
MHD_FUZZ_ITERATIONS, MHD_FUZZ_SEED, MHD_FUZZ_TIMEOUT,
MHD_FUZZ_CRASH_DIR, MHD_FUZZ_VERBOSE, MHD_FUZZ_SKIP_SEEDS
MHD_FUZZ_SHAPE=<n> (fuzz_request) restrict the generator
to one grammar shape
MHD_FUZZ_MIN_DISCIPLINE=<n> (fuzz_request) lower bound for
MHD_OPTION_CLIENT_DISCIPLINE_LVL,
default -3 (the full range)
MHD_FUZZ_MIN_MEM_LIMIT=<n> (fuzz_request) lower bound for
MHD_OPTION_CONNECTION_MEMORY_LIMIT,
default 0 (the full range)
MHD_FUZZ_MODEL_DIGEST_SINK=1 (fuzz_str) enable the modelled
digest 'response' call site, see 5.4
Typical use:
# quick smoke test (a couple of seconds)
./fuzz_request
# a real session
./fuzz_request --iterations=5000000 --seed=$RANDOM
# regression: replay the whole checked-in corpus
./fuzz_request --corpus-dir=corpus
./fuzz_str --corpus-dir=corpus # ignores foreign files gracefully
# reproduce a crash
./fuzz_request --file=crashes/crash-fuzz_request-seed3-iter55.bin
-------------------------------------------------------------------
4. Reproducing a failure
-------------------------------------------------------------------
Whenever the process dies -- ASAN error, UBSAN error, `mhd_assert()`,
MHD_PANIC(), a body-oracle finding, or the watchdog -- the input of the
running iteration is written to
$MHD_FUZZ_CRASH_DIR/crash-<harness>-seed<S>-iter<N>.bin
and a line is printed telling you the harness, the seed and the
iteration. The dump is produced from
* `__sanitizer_set_death_callback()` (weakly linked; present whenever
the binary is built with ASAN), and
* SIGABRT/SIGSEGV/SIGBUS/SIGILL/SIGFPE/SIGALRM handlers,
using only async-signal-safe calls. Replay with `--file=...`; the run
is fully deterministic, so `--seed=S --iterations=N+1` reproduces the
whole session as well.
-------------------------------------------------------------------
5. What these harnesses find (regression coverage)
-------------------------------------------------------------------
The four vulnerabilities fixed in MHD 1.0.7+1 are all rediscovered from
scratch. Each has a dedicated seed in `corpus/`, and the generator
finds each of them on its own within a few thousand iterations.
5.1 digestauth.c: unknown `algorithm=` token -> MHD_PANIC()
An `algorithm=` token MHD does not know parses to
MHD_DIGEST_AUTH_ALGO3_INVALID, which is 0, so the allow-mask test
`c_algo == (c_algo & malgo3)` passes for *any* mask; the code then
calls digest_init_one_time() with an invalid algorithm and panics.
Found by: SHAPE_DIGEST_SIMPLE / SHAPE_DIGEST_REPLAY, the panic hook,
corpus seed `digest-unknown-algorithm`.
5.2 connection.c get_req_headers(): read-buffer shift-back underflow
Needs, all at once: a small MHD_OPTION_CONNECTION_MEMORY_LIMIT
(MHD_BUF_INC_SIZE (1500) > read_buffer_size), *no header lines*, and
a trailing query argument without '=' (whose `value` is NULL).
Found by: SHAPE_NOHDR_QARG, corpus seeds
`small-pool-trailing-query-arg[-2]`.
5.3 connection.c process_request_body(): chunk-extension CRLF
`chunk_size_line_len = i` instead of `i + 2` leaves the CRLF of the
chunk-size line in the stream, so the following chunk data is
shifted -- a body desync, i.e. a request-smuggling primitive. This
corrupts no memory, so it is caught by the body oracle (2.4).
Found by: SHAPE_CHUNKED, corpus seeds `chunked-with-extensions`,
`chunked-split`.
5.4 digestauth.c: over-long `response=` -> stack buffer overflow
`response` was accepted up to `digest_size * 4` characters (128 for
SHA-256) and then decoded with MHD_hex_to_bin() into
`uint8_t hash1_bin[MAX_DIGEST]` (32 bytes) -- up to 64 bytes
written, 32 bytes of stack smashed. Reaching it requires a *valid*
nonce, hence the %%NONCE%% mechanism of 2.3.
Found by: SHAPE_DIGEST_REPLAY, corpus seed
`digest-overlong-response`.
fuzz_str additionally reproduces the underlying primitive:
MHD_hex_to_bin() has no output-size parameter and writes len/2
bytes, so any caller with a fixed-size buffer must bound the input
length itself. `MHD_FUZZ_MODEL_DIGEST_SINK=1` enables a target that
replays exactly the pre-fix call site (32 byte heap buffer, input
length bounded only by 4 * 32) and ASAN reports the overflow
immediately. The target models a *caller*, not the library, so it
is off by default.
-------------------------------------------------------------------
6. Findings against MHD 1.0.7 - all fixed, kept as regressions
-------------------------------------------------------------------
Running these harnesses against v1.0.7 built with `--enable-asserts`
reported the following *additional* issues on top of the four
vulnerabilities of section 5. All of them were `mhd_assert()`s reachable
from network input, i.e. a remote abort in builds that keep assertions
enabled, and all of them are fixed on master. They are documented here
because the reproducers are kept as a regression corpus: a failure of one
of them means the corresponding fix has been undone. Byte-exact reproducers are in `corpus/known-findings/`; replay
one with
./fuzz_request --file=corpus/known-findings/K1-digest-empty-realm.bin
K1 digestauth.c:2467 is_param_equal():
mhd_assert (0 != param->value.len) -> fixed in 300a2ab0
Trigger (default daemon configuration!), one request:
GET /a HTTP/1.1
Host: x
Authorization: Digest username="user", realm="", nonce="0000",
uri="/a", response="00"
digest_auth_check_all_inner() rejects a *missing* realm/username but
not an *empty* one, so a zero-length parameter reaches
is_param_equal(), whose documented precondition is a non-empty
value. Requires only that the application calls
MHD_digest_auth_check3(). Real defect (missing validation).
Repro: corpus/known-findings/K1-digest-empty-realm.bin
K2 connection.c:3582 handle_recv_no_space():
mhd_assert ((MHD_PROC_RECV_BODY_CHUNKED != stage) ||
! c->rq.some_payload_processed) -> fixed in 68c83f22
Trigger: small MHD_OPTION_CONNECTION_MEMORY_LIMIT (<= ~400 bytes)
plus a chunked body whose *second* chunk-size line carries a chunk
extension that does not fit into the remaining read buffer. The
flag reflects the last application callback only and survives later
reads, so the assertion is over-strong; the code below it already
handles the situation. Stale assertion.
Repro: corpus/known-findings/K2-chunkext-no-space.bin
K3 connection.c:2881 transmit_error_response_len():
mhd_assert (! connection->stop_with_error) -> fixed in e04eb218
Trigger: small connection memory pool plus an over-long, unterminated
chunk extension:
POST /a HTTP/1.1 / Transfer-Encoding: chunked
d;ext="qqqqqqqq... (longer than the read buffer)
handle_req_chunk_size_line_no_space() is missing a `return` after it
has already queued the "chunk extension too big" response. Real
defect: in a release build the second call forces the connection to
MHD_CONNECTION_CLOSED and the 413 response is never sent.
Repro: corpus/known-findings/K3-chunkext-stop-with-error.bin
K4 connection.c:6099 get_req_header():
mhd_assert ((0 == c->rq.hdrs.hdr.value_start) ||
(0 != c->rq.hdrs.hdr.name_len)) -> fixed in 0b750975
Trigger a) MHD_OPTION_CLIENT_DISCIPLINE_LVL <= -1, first header line
starting with whitespace:
GET /a HTTP/1.1\r\n Host: x\r\n\r\n
Trigger b) MHD_OPTION_CLIENT_DISCIPLINE_LVL <= -2, empty header
name:
GET /a HTTP/1.1\r\n: value\r\nHost: x\r\n\r\n
Both shapes are explicitly allowed by those discipline levels.
Stale assertion.
Repro: corpus/known-findings/K4a-wsp-first-header.bin,
corpus/known-findings/K4b-empty-header-name.bin
K5 connection.c:6394 get_req_header():
mhd_assert ('\r' != chr) -> fixed in 6fcdfd43
Trigger: MHD_OPTION_CLIENT_DISCIPLINE_LVL = -3, which sets
`bare_cr_keep = true`; the branch that keeps a bare CR falls through
into the "not a whitespace, not the end of the line" arm whose
assertion predates that mode.
GET /a HTTP/1.1\r\nHost: x\r\nX: y\r\r\n\r\n
Stale assertion.
Repro: corpus/known-findings/K5-bare-cr-keep.bin
K6 digestauth.c:860 check_nonce_nc():
mhd_assert (0 == nn->nonce[noncelen])
The nonce-nc slot array is indexed by a hash of the nonce, but the
slot content is compared assuming the *stored* nonce has the same
length as the presented one. A client can therefore make MHD read
the terminator of a nonce at the wrong offset by presenting a nonce
whose length belongs to a different digest algorithm.
Note that this one is *timing dependent*: the nonce carries a
millisecond timestamp and whether it counts as stale depends on the
wall clock, so the same input reproduces only in a fraction of the
replays (about 1 in 30 for the corpus file below). Replay it in a
loop.
Trigger (MHD_OPTION_NONCE_NC_SIZE = 1 makes every nonce land in slot
0, which turns the collision into a certainty; larger arrays only
need more attempts):
request 1: GET /a -> 401 with a SHA-256 nonce
(76 chars) stored in the slot
request 2: Authorization: Digest username="user",
realm="TestRealm", nonce="<44 zeros>", uri="/a",
response="e"
(no algorithm parameter -> MD5 -> nonce length 44,
all-zero timestamp -> not stale)
The generator needs ~150k iterations to hit it on its own.
Repro: corpus/known-findings/K6-nonce-length-collision.bin
Status
------
K1-K5 are fixed on master by commits 300a2ab0, 68c83f22, e04eb218,
0b750975 and 6fcdfd43 respectively; K6 by f438804c. All seven
reproducers in `corpus/known-findings/` therefore replay clean, and
`make check-corpus` asserts exactly that.
The `make check` defaults in Makefile.am
MHD_FUZZ_ITERATIONS = 50000
MHD_FUZZ_MIN_DISCIPLINE = -3 (full range)
MHD_FUZZ_MIN_MEM_LIMIT = 0 (full range)
exercise the whole matrix and take about three seconds in total under
ASAN+UBSAN. A real session:
MHD_FUZZ_MIN_DISCIPLINE=-3 MHD_FUZZ_MIN_MEM_LIMIT=0 \
./fuzz_request --iterations=1000000 --seed=1
-------------------------------------------------------------------
7. Building with clang/libFuzzer or AFL++
-------------------------------------------------------------------
Both need `-DFUZZ_NO_MAIN` so that the driver's `main()` is left out.
7.1 libFuzzer
-------------
# build the library itself with the same instrumentation
./configure --enable-static --disable-shared --enable-asserts \
CC=clang \
CFLAGS="-g -O1 -fsanitize=fuzzer-no-link,address,undefined \
-fno-sanitize-recover=all -fprofile-instr-generate \
-fcoverage-mapping"
make -C src/microhttpd
clang -g -O1 -DFUZZ_NO_MAIN \
-fsanitize=fuzzer,address,undefined -fno-sanitize-recover=all \
-I. -Isrc/include -Isrc/microhttpd -Isrc/fuzz \
-o fuzz_request src/fuzz/fuzz_request.c \
src/microhttpd/.libs/libmicrohttpd.a -lpthread
mkdir -p CORPUS && ./fuzz_request --help >/dev/null 2>&1 || true
./fuzz_request CORPUS src/fuzz/corpus \
-max_len=8192 -rss_limit_mb=4096 -timeout=20
# minimise a crash found by libFuzzer
./fuzz_request -minimize_crash=1 -runs=100000 crash-<hash>
7.2 AFL++
---------
export CC=afl-clang-lto AFL_USE_ASAN=1 AFL_USE_UBSAN=1
./configure --enable-static --disable-shared --enable-asserts
make -C src/microhttpd
afl-clang-lto -g -O1 -DFUZZ_NO_MAIN \
-I. -Isrc/include -Isrc/microhttpd -Isrc/fuzz \
-o fuzz_request src/fuzz/fuzz_request.c \
$(afl-config --libdir 2>/dev/null || echo /usr/local/lib/afl)/afl-compiler-rt.o \
/usr/local/lib/afl/libAFLDriver.a \
src/microhttpd/.libs/libmicrohttpd.a -lpthread
afl-fuzz -i src/fuzz/corpus -o findings -- ./fuzz_request @@
(`libAFLDriver.a` provides a `main()` that reads the file named on the
command line and calls LLVMFuzzerTestOneInput(); that is why
`-DFUZZ_NO_MAIN` is required. With `AFL_LLVM_PERSISTENT` /
`__AFL_LOOP` the same binary can be used in persistent mode.)
7.3 OSS-Fuzz
------------
Ready to go: see `../../contrib/oss-fuzz/` and its README. That
directory holds the OSS-Fuzz `build.sh`, `project.yaml` and `Dockerfile`,
per-harness dictionaries (`dicts/fuzz_*.dict`), per-harness `.options`
files (`max_len`, `dict`) and `make_seed_corpus.sh`, which packages
`corpus/` — including `corpus/known-findings/`, so that K1-K6 become
permanent regressions — into the `<fuzzer>_seed_corpus.zip` files
OSS-Fuzz expects.
`build.sh` configures out of tree with
--enable-static --disable-shared --with-pic --enable-fuzzing
--enable-asserts --disable-https --disable-curl --disable-doc
--disable-examples --disable-tools --enable-build-type=neutral
honouring OSS-Fuzz's `$CFLAGS` (no `--enable-sanitizers`: OSS-Fuzz
supplies the instrumentation), and then compiles each harness exactly as
in 7.1 but against `$LIB_FUZZING_ENGINE`. HTTPS is off on purpose — the
harnesses never speak TLS, and leaving GnuTLS out is what makes the
MemorySanitizer build possible.
Two things to know before reading a report from there:
* the request-body oracle of 2.4 is **inactive** under libFuzzer. It
is gated on `fuzz_pristine`, which nothing sets when `FUZZ_NO_MAIN`
is defined — correctly so, since libFuzzer's mutations invalidate the
declared ground truth. Pure framing/desync defects (5.3) therefore
remain the job of the built-in driver, i.e. of `make check`;
* `primary_contact` in `project.yaml` is a placeholder. OSS-Fuzz needs
an address the maintainer controls; it has to be filled in before the
project can be submitted.
The token list in `fuzz_common.h` (`fuzz_interesting_str`) is the
generator's equivalent of those dictionaries; the two are intentionally
similar but are not generated from each other.
OSS-Fuzz is deliberately not part of `contrib/ci/jobs/`; the bounded
in-tree equivalents for CI are `make -C src/fuzz check` and
`make -C src/fuzz check-corpus`.
-------------------------------------------------------------------
8. Adding a harness
-------------------------------------------------------------------
Create `fuzz_<name>.c` with
#define FUZZ_HARNESS_NAME "fuzz_<name>"
#include "fuzz_common.h"
and implement
int LLVMFuzzerTestOneInput (const uint8_t *, size_t);
static size_t fuzz_generate (struct fuzz_rng *, uint8_t *, size_t);
static size_t fuzz_seed_count (void);
static const uint8_t *fuzz_seed_get (size_t, size_t *);
then add it to `check_PROGRAMS` in Makefile.am. Report non-crashing
findings with `fuzz_report_finding("...")`, which dumps the reproducer
and aborts.
Two rules learnt the hard way:
* do not violate documented *preconditions* of the function under
test (e.g. MHD_str_remove_token_caseless_() asserts that the token
contains no space, tab, comma or NUL) -- otherwise the harness only
finds its own bugs;
* never evaluate a macro argument twice when it contains a PRNG call.
+111
View File
@@ -0,0 +1,111 @@
Seed corpus for the src/fuzz harnesses
======================================
`<harness>-NN.bin` is the built-in seed corpus of that harness, dumped
to disk. Regenerate at any time with
./fuzz_<harness> --write-corpus=<this directory>
or, from the build tree,
make -C src/fuzz refresh-corpus
Replay everything (this is what a CI regression run should do):
make -C src/fuzz check-corpus
# or, per harness:
./fuzz_request --corpus-dir=src/fuzz/corpus
Files belonging to another harness are simply uninteresting inputs for
the harness that reads them, so pointing every harness at the whole
directory is fine and gives some extra cross-pollination.
fuzz_request seeds
------------------
00 plain GET / with one header
01 content-length-body body oracle, Content-Length
02 chunked-with-extensions body oracle, chunk extensions
-> chunk-extension CRLF bug
03 chunked-split body oracle, chunk boundary split
across two send() calls
04 small-pool-trailing-query-arg 128..512 byte connection pool, no
header lines, "?novalue"
-> read-buffer shift-back bug
05 small-pool-trailing-query-arg-2 same with "?a=1&b"
06 digest-unknown-algorithm algorithm=BOGUS
-> MHD_DIGEST_AUTH_ALGO3_INVALID
MHD_PANIC()
07 digest-overlong-response 401 challenge, then a replay on a
fresh connection with the
harvested nonce and a 128 hex
digit response=
-> stack buffer overflow
08 digest-userhash userhash=true with a 128 char
username
09 basic-auth Authorization: Basic
10 multipart-post chunked multipart/form-data
11 urlencoded-post application/x-www-form-urlencoded
12 folded-header obs-fold continuation line
13 pipelined two requests in one segment
fuzz_str seeds
--------------
Cover MHD_hex_to_bin (including the 128 character input that
digestauth.c used to allow into a 32 byte buffer), MHD_bin_to_hex[_z],
the percent-decoders (strict, lenient, in place), MHD_str_unquote,
MHD_str_quote, MHD_base64_to_bin_n, MHD_str[x]_to_uint64_n_ and the
token helpers.
fuzz_auth_header seeds
----------------------
Well-formed and broken Digest parameter lists (unknown algorithm,
extended `username*` notation, unterminated quoted strings, empty
parameter list, over-long values) plus Basic `token68` variants.
fuzz_postprocessor seeds
------------------------
urlencoded bodies with broken percent escapes, multipart bodies with
ordinary, degenerate ("-") and quote-containing boundaries, a
multipart Content-Type without a boundary, and a non-POST
Content-Type.
known-findings/
---------------
Reproducers for the issues that this suite found in MHD 1.0.7 itself;
see section 6 of ../README. K1-K5 are fixed by ../../../patches/B17,
B19, B20, B21 and B22; K6 is handled separately and is still open. They
are deliberately kept out of the main corpus directory so that
`check-corpus` stays green on an unpatched tree. Replay one with
./fuzz_request --file=src/fuzz/corpus/known-findings/K1-digest-empty-realm.bin
K1-digest-empty-realm.bin digestauth.c is_param_equal()
mhd_assert (0 != param->value.len)
-> patches/B17.diff
K2-chunkext-no-space.bin connection.c handle_recv_no_space()
-> patches/B19.diff
K3-chunkext-stop-with-error.bin connection.c
transmit_error_response_len()
-> patches/B20.diff
K4a-wsp-first-header.bin connection.c get_req_header(),
CLIENT_DISCIPLINE_LVL <= -1
-> patches/B21.diff
K4b-empty-header-name.bin connection.c get_req_header(),
CLIENT_DISCIPLINE_LVL <= -2
-> patches/B21.diff
K5-bare-cr-keep.bin connection.c get_req_header(),
CLIENT_DISCIPLINE_LVL == -3
-> patches/B22.diff
K6-nonce-length-collision.bin digestauth.c check_nonce_nc()
mhd_assert (0 == nn->nonce[noncelen])
-> still open
Once a patch has landed, move the corresponding file into the main
corpus directory so that `check-corpus` keeps it as a regression seed.
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.
+1
View File
@@ -0,0 +1 @@
Basic dXNlcjpwYXNz
+1
View File
@@ -0,0 +1 @@
Basic
+1
View File
@@ -0,0 +1 @@
Basic ====
+1
View File
@@ -0,0 +1 @@
Basic QQ==QQ==
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,5 @@
XY
--XY
noheaders
--XY--
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