-comments

This commit is contained in:
Christian Grothoff
2026-07-28 17:18:32 +02:00
parent a1fb82fc3c
commit 56713e94bb
8 changed files with 122 additions and 71 deletions
+8 -8
View File
@@ -23,13 +23,13 @@ make -C src/fuzz check \
MHD_FUZZ_ITERATIONS="${FUZZ_ITERATIONS}" \
MHD_FUZZ_SEED="${FUZZ_SEED}"
# Replay the whole committed seed corpus through every harness. This is
# the part that proves a fixed crash stays fixed, and it is cheap.
# Replay the whole committed seed corpus - including
# corpus/known-findings/, the reproducers of src/fuzz/README section 6 -
# through every harness. This is the part that proves a fixed crash
# stays fixed, and it is cheap.
#
# corpus/known-findings/ is deliberately NOT included: check-corpus does
# not recurse, and the reproducers of the findings that are still open
# (src/fuzz/README section 6, patches/) are expected to fail on a build
# with --enable-asserts. Replay those by hand when working on a fix:
# for f in src/fuzz/corpus/known-findings/*.bin ; do
# ./src/fuzz/fuzz_request --file="$f" ; done
# It only means something because 1-build.sh configures --enable-asserts:
# most of those reproducers trip an mhd_assert(), which compiles away
# without it, so on a default build they would pass whether MHD is fixed
# or not.
make -C src/fuzz check-corpus
+8 -6
View File
@@ -341,12 +341,14 @@ regression tests: ClusterFuzz keeps every seed in the corpus and replays
it on every run.
Reproducers of findings that are still *open* are skipped. A finding is
open exactly when `patches/$ID.diff` exists (see `patches/README`), and
its reproducer crashes the target by construction, so shipping it would
make every ClusterFuzz run open by rediscovering a bug that is already
written down. Committing the fix means deleting the diff, and that
alone promotes the reproducer to a shipped regression seed. At the time
of writing this skips K7.
open exactly when `patches/$ID.diff` exists, and its reproducer crashes
the target by construction, so shipping it would make every ClusterFuzz
run open by rediscovering a bug that is already written down. Committing
the fix means deleting the diff, and that alone promotes the reproducer
to a shipped regression seed.
Nothing is open at the time of writing, so `patches/` does not exist and
all eight reproducers ship.
Regenerating the corpus from the harnesses' built-in seeds:
+10 -7
View File
@@ -64,14 +64,17 @@ for fuzzer in ${FUZZERS}; do
# The K* reproducers are fuzz_request inputs.
#
# Only the ones whose defect is already fixed are shipped. A finding
# that is still open has an unapplied fix in patches/$ID.diff (see
# patches/README), and its reproducer, by construction, crashes the
# target: shipping it would make every ClusterFuzz run start by
# Only the ones whose defect is already fixed are shipped. While a
# finding is open its proposed fix is kept as an unapplied diff in
# patches/$ID.diff, and its reproducer crashes the target by
# construction; shipping it would make every ClusterFuzz run start by
# rediscovering a bug that is already written down, and bury the
# findings that are actually new. Deleting the diff -- which is what
# committing the fix should do -- promotes the reproducer to a
# permanent regression seed here, with no further edit.
# findings that are actually new. Committing the fix deletes the diff,
# and that alone promotes the reproducer to a permanent regression seed
# here, with no further edit.
#
# patches/ therefore does not exist while nothing is open, which is the
# normal state; the test below simply never fires then.
if [ "${fuzzer}" = "fuzz_request" ] && [ -d "${FINDINGS}" ]; then
for f in "${FINDINGS}"/*.bin; do
[ -f "${f}" ] || continue
+6 -5
View File
@@ -188,11 +188,12 @@ make -C src/fuzz check-corpus
* `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
ASAN+UBSAN. The findings K1-K8 of `README` section 6 are all fixed on
master, so the full range is clean; their reproducers stay in
`corpus/known-findings/` as regressions. K7 is open, and its
reproducer lives there too — `check-corpus` does not recurse into that
directory, which is what keeps `make check` green while it is.
`corpus/known-findings/`, and `make check-corpus` replays that
directory alongside the generated corpus. Both only mean anything on a
tree configured with `--enable-asserts`: most of those reproducers trip
an `mhd_assert()`, which compiles away without it.
---
@@ -257,5 +258,5 @@ Two consequences for anyone editing `src/fuzz/`:
`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
`fuzz_request_seed_corpus.zip` so that they become permanent
regressions. It is deliberately **not** part of `contrib/ci/jobs/`.
+20 -3
View File
@@ -115,10 +115,27 @@ refresh-corpus: $(check_PROGRAMS)
# Replay the whole on-disk corpus through every harness; this is what a
# CI regression run should do after a crash has been fixed.
#
# corpus/known-findings/ is listed separately because --corpus-dir does
# not recurse. It holds the reproducers of the findings in README
# section 6; all of them are fixed, so anything failing there is a
# regression. The one intended exception is a reproducer committed while
# its finding is still open: that fails here until the fix lands, which
# is what a regression test for an unfixed bug is supposed to do.
#
# Note that these only bite on a tree configured with --enable-asserts;
# without it mhd_assert() compiles away and most of these reproducers
# pass whether MHD is fixed or not.
CORPUS_DIRS = \
$(srcdir)/corpus \
$(srcdir)/corpus/known-findings
.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 ; \
for d in $(CORPUS_DIRS) ; do \
MHD_FUZZ_MIN_DISCIPLINE="$(MHD_FUZZ_MIN_DISCIPLINE)" \
MHD_FUZZ_MIN_MEM_LIMIT="$(MHD_FUZZ_MIN_MEM_LIMIT)" \
./$$p --corpus-dir=$$d || exit 1 ; \
done ; \
done
+42 -22
View File
@@ -202,11 +202,13 @@ violating either makes MHD abort on input that is perfectly legal:
header naming the upgrade token and not "close"). All three come
off the wire, so the path stays attacker-driven.
Unlike the suspend rule, this one is not sufficient, and is not
supposed to be: it is exactly the check a real application can
perform, and a request that passes it can still abort MHD. That is
finding K7 in section 6. Do not tighten it further to keep the
suite green -- the point is that an application cannot do better.
This is exactly the check a real application can perform, and no
more. It deliberately does not try to predict whether MHD will
accept the upgrade: a request that passes it used to abort MHD, which
is finding K7 in section 6, and MHD_queue_response() now answers
MHD_NO for that case instead. Do not tighten the condition to keep
the suite green -- an application cannot do better than this, so
neither should the harness.
The message-framing headers (Content-Length, Transfer-Encoding) are
deliberately absent from the response-header table: MHD generates them
@@ -529,7 +531,8 @@ K6 digestauth.c:860 check_nonce_nc():
K7 connection.c:2596 build_header_response():
mhd_assert ((NULL == r->upgrade_handler) ||
(MHD_CONN_MUST_UPGRADE == c->keepalive))
Open. Driven entirely from the wire, with default daemon options.
Fixed by commit acef58a0. Was driven entirely from the wire, with
default daemon options.
While parsing the request headers, connection.c sets
c->keepalive = MHD_CONN_MUST_CLOSE
@@ -565,9 +568,10 @@ K7 connection.c:2596 build_header_response():
default-configured server, which is a good deal worse than the
"application misuse" it first looked like.
Fix: patches/K7.diff, which rejects the response in
MHD_queue_response() next to the other upgrade preconditions, so the
application gets MHD_NO instead of an abort.
The fix rejects the response in MHD_queue_response()
(connection.c:8338) next to the other upgrade preconditions, so the
application gets MHD_NO -- which it already has to handle -- instead
of an abort.
Repro: corpus/known-findings/K7-upgrade-after-must-close.bin
GET / HTTP/1.1
Host: x
@@ -580,8 +584,9 @@ K7 connection.c:2596 build_header_response():
K8 digestauth.c MHD_digest_auth_check_digest2():
MHD_PANIC ("Wrong 'malgo3' value, only one base hashing
algorithm ... must be specified, API violation")
Open. An API-contract defect rather than a parsing bug: unlike K7
the offending argument comes from the application, not the network.
Fixed by commit 07c051dd. An API-contract defect rather than a
parsing bug: unlike K7 the offending argument came from the
application, not from the network.
MHD_DIGEST_ALG_AUTO is a documented member of
enum MHD_DigestAuthAlgorithm, and the `algo` parameter of
@@ -605,26 +610,41 @@ K8 digestauth.c MHD_digest_auth_check_digest2():
because the digest length does not identify the algorithm (SHA-256
and SHA-512/256 digests are both 32 bytes).
Fix: patches/K8.diff, which documents the restriction in
microhttpd.h and returns MHD_NO instead of aborting.
The fix documents the restriction in microhttpd.h and returns MHD_NO
instead of aborting. AUTO is left unusable with the
pre-computed-digest entry points, deliberately: there is nothing to
map it to.
Repro: none committed. The argument is the application's, so the
harness never passes AUTO there (see run_digest_check() in
fuzz_request.c); to see it, change the MHD_DIGEST_ALG_* argument of
the variant-4 call to MHD_DIGEST_ALG_AUTO and replay
corpus/fuzz_request-36.bin.
corpus/fuzz_request-37.bin.
Status
------
K1-K5 are fixed on master by commits 300a2ab0, 68c83f22, e04eb218,
0b750975 and 6fcdfd43 respectively; K6 by f438804c. Their seven
reproducers in `corpus/known-findings/` therefore replay clean, and
`make check-corpus` asserts exactly that.
All eight findings are fixed on master:
K7 and K8 are open, and their fixes are unapplied diffs in
`../../patches/`. K7 has a reproducer, which aborts on an
`--enable-asserts` build and passes with patches/K7.diff applied; K8 has
none, because its trigger is an argument the application chooses.
K1 300a2ab0 K4a 0b750975 K7 acef58a0
K2 68c83f22 K4b 0b750975 K8 07c051dd
K3 e04eb218 K5 6fcdfd43
K6 f438804c
The eight reproducers in `corpus/known-findings/` therefore all replay
clean on a build configured with `--enable-asserts`, and
`make check-corpus` asserts exactly that -- it replays that directory
along with the generated corpus. K8 has no reproducer, because its
trigger is an argument the application chooses rather than anything that
comes off the wire.
When the next finding is opened, its reproducer goes into
`corpus/known-findings/` and will make `make check-corpus` fail until the
fix lands. That is the intended behaviour: it is the same arrangement as
the `XFAIL_TESTS` entries in `src/microhttpd/Makefile.am`, except that
here the expectation is not encoded, so the commit that adds a live
reproducer should say so. `contrib/oss-fuzz/make_seed_corpus.sh` has to
skip such a reproducer as well, or every ClusterFuzz run starts by
rediscovering it; see the comment there.
The `make check` defaults in Makefile.am
+18 -10
View File
@@ -132,13 +132,14 @@ known-findings/
---------------
Reproducers for the issues that this suite found in MHD itself; see
section 6 of ../README, which records the status of each. K1-K6 are
fixed on master and therefore replay clean; K7 is open and *aborts* on a
build configured with --enable-asserts.
section 6 of ../README, which records the status of each. All of them
are fixed on master, so all of them replay clean, and `check-corpus`
replays this directory too.
They are kept out of the main corpus directory for exactly that reason:
`check-corpus` does not recurse, so it stays green while an open finding
still has a live reproducer here. Replay one with
They are kept as a separate, explicitly named set rather than being
folded into the main corpus because the files there are *generated*: a
hand-written input would be clobbered or renumbered by the next
`refresh-corpus`. Replay one with
./fuzz_request --file=src/fuzz/corpus/known-findings/K1-digest-empty-realm.bin
@@ -156,17 +157,24 @@ still has a live reproducer here. Replay one with
K6-nonce-length-collision.bin digestauth.c check_nonce_nc()
mhd_assert (0 == nn->nonce[noncelen])
K7-upgrade-after-must-close.bin connection.c build_header_response()
mhd_assert (upgrade -> MUST_UPGRADE)
*** still open, see ../../../patches ***
mhd_assert (upgrade -> MUST_UPGRADE);
a single request carrying both
Content-Length and chunked
Transfer-Encoding, then an upgrade
These files are *not* regenerated by `--write-corpus`; they are edited by
hand. When the input format changes they have to be migrated, and the
migration has to be checked -- replay each one with `--verbose` before
and after and confirm the daemon, handler, body and challenge counts are
unchanged. The last such change made the API-selection block
unconditional, which is six zero bytes inserted at offset 4 for all
seven.
unconditional, which is six zero bytes inserted at offset 4 for K1-K6.
K8 has no reproducer here: its trigger is an argument the application
chooses, not network input, so the harness never performs it. See
section 6 of ../README for how to reach it by hand.
A reproducer added while its finding is still open will make
`make check-corpus` fail, which is intended -- that is what a regression
test for an unfixed bug does. `contrib/oss-fuzz/make_seed_corpus.sh`
skips such a file so that ClusterFuzz does not spend every run
rediscovering it; the rule it uses is whether `patches/$ID.diff` exists.
+10 -10
View File
@@ -1518,18 +1518,18 @@ ahc (void *cls,
application performs, and all three inputs come off the wire, so
the path stays fully attacker-driven.
It is deliberately NOT enough to keep MHD's
It is deliberately NOT enough to predict whether MHD will accept
the upgrade, and it is not meant to be: MHD may have decided during
header parsing that the connection must close, and does not expose
that decision. That used to trip
mhd_assert (NULL == r->upgrade_handler ||
MHD_CONN_MUST_UPGRADE == c->keepalive)
in build_header_response() satisfied, and it is not meant to be:
MHD may have decided during header parsing that the connection must
close, and does not expose that decision. That is finding K7 (see
README section 6); corpus/known-findings/K7-upgrade-after-must-close.bin
is the reproducer, and patches/K7.diff turns the abort into the
MHD_NO that the code below already handles. Do not "fix" this by
weakening the condition here -- the check above is what an
application can actually do, and the harness has to behave like
one. */
in build_header_response() -- finding K7, see README section 6,
reproducer corpus/known-findings/K7-upgrade-after-must-close.bin --
and MHD_queue_response() now returns MHD_NO for it instead, which
the code below already handles. Do not "fix" this by tightening
the condition here: the check above is what an application can
actually do, and the harness has to behave like one. */
if (cfg.allow_upgrade &&
(NULL != version) &&
(0 == strcmp (version, MHD_HTTP_VERSION_1_1)) &&