commit d2e286352ade71d30a9839734d7f1178fba22cef
parent fd16f9d2cbd085e84bc903326d9434aff74ea890
Author: Christian Grothoff <christian@grothoff.org>
Date: Tue, 28 Jul 2026 23:31:29 +0200
update fuzzer docs
Diffstat:
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