libmicrohttpd

HTTP/1.x server C library (MHD 1.x, stable)
Log | Files | Refs | Submodules | README | LICENSE

commit d2e286352ade71d30a9839734d7f1178fba22cef
parent fd16f9d2cbd085e84bc903326d9434aff74ea890
Author: Christian Grothoff <christian@grothoff.org>
Date:   Tue, 28 Jul 2026 23:31:29 +0200

update fuzzer docs

Diffstat:
Mcontrib/oss-fuzz/README | 27+++++++++++++++++++++++++++
Msrc/fuzz/README | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/fuzz/corpus/README | 9+++++++++
3 files changed, 92 insertions(+), 6 deletions(-)

diff --git a/contrib/oss-fuzz/README b/contrib/oss-fuzz/README @@ -114,6 +114,33 @@ harness ignores the extra bytes, so libFuzzer does not waste its budget: fuzz_postprocessor 8196 4 selector bytes + the POST body, truncated to 8192. +### rss_limit_mb, and why a long run creeps + +libFuzzer's default `-rss_limit_mb=2048` counts the whole process, and an +ASan-instrumented target grows slowly over hundreds of millions of +executions even with no leak at all: ASan's allocator keeps a quarantine +of freed chunks and does not return memory to the OS eagerly. A local +two-hour campaign hit exactly this -- `fuzz_options` crept from 686 MB to +1804 MB over 26M executions and was killed as an OOM, while +LeakSanitizer stayed silent across 3M-execution runs and +`MALLOC_ARENA_MAX` changed nothing (it is glibc's knob, and none of these +allocations go through glibc). + +Three arms of the same target over the same corpus, 4M executions each, +settle it: + + ASan, default quarantine 608 MB + ASan, ASAN_OPTIONS=quarantine_size_mb=1 266 MB + no sanitizer at all (glibc allocator) 36 MB + +17x between the first and the last, with no code difference: the growth +is the sanitizer's allocator holding freed memory, not the harness and +not MHD. + +So treat a slow, monotonic RSS climb as allocator retention rather than a +finding. ClusterFuzz restarts its fuzzers periodically, which bounds it; +do the same locally rather than raising the limit forever. + ### close_fd_mask Deliberately **not** set. The harnesses are quiet by default (MHD's diff --git a/src/fuzz/README b/src/fuzz/README @@ -651,18 +651,68 @@ K9 postprocessor.c:1119 post_process_multipart(): path no longer overwrites the old pointer either. Repro: corpus/known-findings/K9-fuzz_postprocessor-nested-boundary-leak.bin +K10 memorypool.c MHD_pool_deallocate(): end block of an exactly-full + pool was never returned; the front/end dispatch tested pool->pos, + where the two ranges meet. Fixed. + +K11 memorypool.c MHD_pool_reallocate(): returned non-NULL for a + wrapping @a new_size, handing the caller a block it believed was + nearly SIZE_MAX bytes long. Fixed. + +K12 daemon.c MHD_start_daemon_va(): leaked the GnuTLS DH parameters + built from MHD_OPTION_HTTPS_MEM_DHPARAMS on every startup-failure + exit. The application cannot free them. Fixed. + +K13 daemon.c MHD_epoll() / MHD_quiesce_daemon(): both threads remove + the listening socket from the epoll set, and the loser aborts on + ENOENT. Open; two alternative fixes are proposed as + ../../patches/K13.diff (tolerate ENOENT everywhere) and + ../../patches/K13b.diff (take a lock, keep the strict check). + +K14 daemon.c:1358 call_handlers(): + mhd_assert (! force_close || MHD_CONNECTION_CLOSED == con->state) + Open, fix proposed in ../../patches/K14.diff. + + MHD_connection_handle_read() closes the connection when its + @a socket_error argument is set -- but only once it gets far enough + to try. It returns early, leaving the state untouched, when the + connection is suspended, when a TLS handshake is in progress, and + when the read buffer has no free space at all. The caller asserts + the post-condition unconditionally. + + Instrumenting the assertion site with the reproducer gives + + state=12 (HEADERS_PROCESSED) suspended=0 + rb_size=2021 rb_off=2021 evinfo=READ + + i.e. the read buffer is exactly full. A client that fills the + connection memory pool without the application consuming the body + gets there; no unusual API use is involved. Observed through + MHD_run_from_select2(), i.e. from the external event loop. + + The reproducer does NOT fire on a plain in-tree gcc build: ASan's + pool redzones change the buffer arithmetic enough to matter, so + confirm it against the OSS-Fuzz build configuration. + Repro: corpus/known-findings/K14-fuzz_eventloop-force-close-not-closed.bin + Status ------ -All nine findings are fixed on master: +K1-K12 are fixed on master: - K1 300a2ab0 K4a 0b750975 K7 acef58a0 - K2 68c83f22 K4b 0b750975 K8 07c051dd - K3 e04eb218 K5 6fcdfd43 K9 (this tree) + K1 300a2ab0 K4a 0b750975 K7 acef58a0 K10 (memorypool) + K2 68c83f22 K4b 0b750975 K8 07c051dd K11 (memorypool) + K3 e04eb218 K5 6fcdfd43 K9 (postproc) K12 (daemon/TLS) K6 f438804c -The nine reproducers in `corpus/known-findings/` therefore all replay -clean on a build configured with `--enable-asserts`, and +K13 and K14 are open, with proposed fixes in ../../patches/. Until K14 +lands, `make check-corpus` fails on an `--enable-asserts` build, because +its reproducer is committed and still reproduces -- which is the +documented behaviour for an open finding, but worth knowing before a CI +run. + +The reproducers in `corpus/known-findings/` otherwise 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 diff --git a/src/fuzz/corpus/README b/src/fuzz/corpus/README @@ -170,6 +170,15 @@ hand-written input would be clobbered or renumbered by the next nested-boundary-leak.bin leaked the previous nested boundary on every extra nested multipart/mixed part + K14-fuzz_eventloop- daemon.c call_handlers() asserted a + force-close-not-closed.bin post-condition that + MHD_connection_handle_read() does + not guarantee when the read buffer + is full + *** still open, see ../../../patches *** + Only fires on an --enable-asserts + *ASan* build: the redzones change + the pool arithmetic that reaches it. 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