libmicrohttpd

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

commit e34baa3f1a05bdd811f7caa3c71946c5593a8382
parent b4f2b6583f65ebeae01a102ba68292fd546b2a89
Author: Christian Grothoff <christian@grothoff.org>
Date:   Tue, 28 Jul 2026 12:36:24 +0200

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

Diffstat:
M.gitignore | 3+++
Mconfigure.ac | 23++++++++++++++++++++++-
Mcontrib/Makefile.am | 3+++
Acontrib/ci/.gitignore | 2++
Acontrib/ci/Containerfile | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/ci/README | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/ci/ci.sh | 42++++++++++++++++++++++++++++++++++++++++++
Acontrib/ci/jobs/0-codespell/config.ini | 6++++++
Acontrib/ci/jobs/0-codespell/dictionary.txt | 40++++++++++++++++++++++++++++++++++++++++
Acontrib/ci/jobs/0-codespell/job.sh | 9+++++++++
Acontrib/ci/jobs/0-codespell/skip.txt | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/ci/jobs/1-build/build.sh | 14++++++++++++++
Acontrib/ci/jobs/1-build/config.ini | 6++++++
Acontrib/ci/jobs/1-build/job.sh | 6++++++
Acontrib/ci/jobs/2-test/1-build.sh | 19+++++++++++++++++++
Acontrib/ci/jobs/2-test/2-test.sh | 40++++++++++++++++++++++++++++++++++++++++
Acontrib/ci/jobs/2-test/config.ini | 6++++++
Acontrib/ci/jobs/2-test/job.sh | 7+++++++
Acontrib/ci/jobs/3-build-matrix/1-bootstrap.sh | 17+++++++++++++++++
Acontrib/ci/jobs/3-build-matrix/2-matrix.sh | 258+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/ci/jobs/3-build-matrix/config.ini | 6++++++
Acontrib/ci/jobs/3-build-matrix/job.sh | 7+++++++
Acontrib/ci/jobs/4-fuzz/1-build.sh | 25+++++++++++++++++++++++++
Acontrib/ci/jobs/4-fuzz/2-fuzz.sh | 29+++++++++++++++++++++++++++++
Acontrib/ci/jobs/4-fuzz/config.ini | 6++++++
Acontrib/ci/jobs/4-fuzz/job.sh | 7+++++++
Acontrib/ci/jobs/5-zzuf/1-build.sh | 26++++++++++++++++++++++++++
Acontrib/ci/jobs/5-zzuf/2-zzuf.sh | 19+++++++++++++++++++
Acontrib/ci/jobs/5-zzuf/config.ini | 6++++++
Acontrib/ci/jobs/5-zzuf/job.sh | 7+++++++
Acontrib/ci/jobs/6-coverage/1-build.sh | 20++++++++++++++++++++
Acontrib/ci/jobs/6-coverage/2-test.sh | 7+++++++
Acontrib/ci/jobs/6-coverage/3-coverage.sh | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/ci/jobs/6-coverage/config.ini | 6++++++
Acontrib/ci/jobs/6-coverage/job.sh | 8++++++++
Acontrib/ci/run-all-jobs | 5+++++
Acontrib/ci/version.sh | 20++++++++++++++++++++
Acontrib/oss-fuzz/Dockerfile | 44++++++++++++++++++++++++++++++++++++++++++++
Acontrib/oss-fuzz/README | 361+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/oss-fuzz/build.sh | 206+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/oss-fuzz/dicts/fuzz_auth_header.dict | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/oss-fuzz/dicts/fuzz_postprocessor.dict | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/oss-fuzz/dicts/fuzz_request.dict | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/oss-fuzz/dicts/fuzz_str.dict | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/oss-fuzz/fuzz_auth_header.options | 3+++
Acontrib/oss-fuzz/fuzz_postprocessor.options | 3+++
Acontrib/oss-fuzz/fuzz_request.options | 3+++
Acontrib/oss-fuzz/fuzz_str.options | 3+++
Acontrib/oss-fuzz/make_seed_corpus.sh | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/oss-fuzz/project.yaml | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/run-option-matrix.sh | 343+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Makefile.am | 4++++
Asrc/fuzz/.gitignore | 11+++++++++++
Asrc/fuzz/BUILD-INTEGRATION.md | 229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/fuzz/Makefile.am | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/fuzz/README | 560+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/fuzz/corpus/README | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/fuzz/corpus/fuzz_auth_header-00.bin | 0
Asrc/fuzz/corpus/fuzz_auth_header-01.bin | 0
Asrc/fuzz/corpus/fuzz_auth_header-02.bin | 0
Asrc/fuzz/corpus/fuzz_auth_header-03.bin | 0
Asrc/fuzz/corpus/fuzz_auth_header-04.bin | 0
Asrc/fuzz/corpus/fuzz_auth_header-05.bin | 0
Asrc/fuzz/corpus/fuzz_auth_header-06.bin | 0
Asrc/fuzz/corpus/fuzz_auth_header-07.bin | 0
Asrc/fuzz/corpus/fuzz_auth_header-08.bin | 0
Asrc/fuzz/corpus/fuzz_auth_header-09.bin | 0
Asrc/fuzz/corpus/fuzz_auth_header-10.bin | 0
Asrc/fuzz/corpus/fuzz_auth_header-11.bin | 0
Asrc/fuzz/corpus/fuzz_auth_header-12.bin | 2++
Asrc/fuzz/corpus/fuzz_auth_header-13.bin | 2++
Asrc/fuzz/corpus/fuzz_auth_header-14.bin | 2++
Asrc/fuzz/corpus/fuzz_auth_header-15.bin | 2++
Asrc/fuzz/corpus/fuzz_postprocessor-00.bin | 0
Asrc/fuzz/corpus/fuzz_postprocessor-01.bin | 0
Asrc/fuzz/corpus/fuzz_postprocessor-02.bin | 0
Asrc/fuzz/corpus/fuzz_postprocessor-03.bin | 0
Asrc/fuzz/corpus/fuzz_postprocessor-04.bin | 0
Asrc/fuzz/corpus/fuzz_postprocessor-05.bin | 0
Asrc/fuzz/corpus/fuzz_postprocessor-06.bin | 5+++++
Asrc/fuzz/corpus/fuzz_postprocessor-07.bin | 0
Asrc/fuzz/corpus/fuzz_postprocessor-08.bin | 0
Asrc/fuzz/corpus/fuzz_postprocessor-09.bin | 0
Asrc/fuzz/corpus/fuzz_postprocessor-10.bin | 0
Asrc/fuzz/corpus/fuzz_request-00.bin | 0
Asrc/fuzz/corpus/fuzz_request-01.bin | 0
Asrc/fuzz/corpus/fuzz_request-02.bin | 0
Asrc/fuzz/corpus/fuzz_request-03.bin | 0
Asrc/fuzz/corpus/fuzz_request-04.bin | 0
Asrc/fuzz/corpus/fuzz_request-05.bin | 0
Asrc/fuzz/corpus/fuzz_request-06.bin | 0
Asrc/fuzz/corpus/fuzz_request-07.bin | 0
Asrc/fuzz/corpus/fuzz_request-08.bin | 0
Asrc/fuzz/corpus/fuzz_request-09.bin | 0
Asrc/fuzz/corpus/fuzz_request-10.bin | 0
Asrc/fuzz/corpus/fuzz_request-11.bin | 0
Asrc/fuzz/corpus/fuzz_request-12.bin | 0
Asrc/fuzz/corpus/fuzz_request-13.bin | 0
Asrc/fuzz/corpus/fuzz_str-00.bin | 0
Asrc/fuzz/corpus/fuzz_str-01.bin | 0
Asrc/fuzz/corpus/fuzz_str-02.bin | 0
Asrc/fuzz/corpus/fuzz_str-03.bin | 0
Asrc/fuzz/corpus/fuzz_str-04.bin | 0
Asrc/fuzz/corpus/fuzz_str-05.bin | 0
Asrc/fuzz/corpus/fuzz_str-06.bin | 0
Asrc/fuzz/corpus/fuzz_str-07.bin | 2++
Asrc/fuzz/corpus/fuzz_str-08.bin | 0
Asrc/fuzz/corpus/fuzz_str-09.bin | 0
Asrc/fuzz/corpus/fuzz_str-10.bin | 0
Asrc/fuzz/corpus/fuzz_str-11.bin | 0
Asrc/fuzz/corpus/fuzz_str-12.bin | 0
Asrc/fuzz/corpus/fuzz_str-13.bin | 0
Asrc/fuzz/corpus/fuzz_str-14.bin | 0
Asrc/fuzz/corpus/fuzz_str-15.bin | 0
Asrc/fuzz/corpus/fuzz_str-16.bin | 2++
Asrc/fuzz/corpus/fuzz_str-17.bin | 2++
Asrc/fuzz/corpus/fuzz_str-18.bin | 0
Asrc/fuzz/corpus/known-findings/K1-digest-empty-realm.bin | 0
Asrc/fuzz/corpus/known-findings/K2-chunkext-no-space.bin | 0
Asrc/fuzz/corpus/known-findings/K3-chunkext-stop-with-error.bin | 0
Asrc/fuzz/corpus/known-findings/K4a-wsp-first-header.bin | 0
Asrc/fuzz/corpus/known-findings/K4b-empty-header-name.bin | 0
Asrc/fuzz/corpus/known-findings/K5-bare-cr-keep.bin | 0
Asrc/fuzz/corpus/known-findings/K6-nonce-length-collision.bin | 0
Asrc/fuzz/fuzz_auth_header.c | 437+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/fuzz/fuzz_common.h | 894+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/fuzz/fuzz_postprocessor.c | 477+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/fuzz/fuzz_request.c | 1555+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/fuzz/fuzz_str.c | 544+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/microhttpd/Makefile.am | 51+++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/microhttpd/connection.c | 38++++++++++++++++++++++++++++++++++++--
Msrc/microhttpd/digestauth.c | 12++++++++++--
Msrc/microhttpd/md5_ext.c | 2++
Msrc/microhttpd/memorypool.c | 17++++++++++++++++-
Asrc/microhttpd/mhd_check.h | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/microhttpd/mhd_opt_matrix.c | 532+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/microhttpd/mhd_opt_matrix.h | 387+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/microhttpd/mhd_panic_tripwire.h | 477+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/microhttpd/mhd_str.c | 3++-
Msrc/microhttpd/sha256_ext.c | 2++
Msrc/microhttpd/test_auth_parse.c | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/microhttpd/test_chunked_ext.c | 1032+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/microhttpd/test_client_put_stop.c | 4++++
Msrc/microhttpd/test_daemon.c | 4++++
Asrc/microhttpd/test_dauth_malformed.c | 1248+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/microhttpd/test_dauth_userdigest.c | 4++++
Msrc/microhttpd/test_dauth_userhash.c | 4++++
Msrc/microhttpd/test_http_reasons.c | 4++++
Msrc/microhttpd/test_md5.c | 4++++
Msrc/microhttpd/test_mhd_version.c | 4++++
Asrc/microhttpd/test_option_matrix.c | 1575+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/microhttpd/test_options.c | 4++++
Msrc/microhttpd/test_postprocessor.c | 4++++
Msrc/microhttpd/test_postprocessor_amp.c | 4++++
Msrc/microhttpd/test_postprocessor_large.c | 4++++
Msrc/microhttpd/test_postprocessor_md.c | 4++++
Asrc/microhttpd/test_raw_requests.c | 2123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/microhttpd/test_response_entries.c | 10++++++++--
Asrc/microhttpd/test_rq_shift_back.c | 651+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/microhttpd/test_sha1.c | 4++++
Msrc/microhttpd/test_sha256.c | 4++++
Msrc/microhttpd/test_sha512_256.c | 4++++
Msrc/microhttpd/test_shutdown_select.c | 4++++
Msrc/microhttpd/test_start_stop.c | 4++++
Msrc/microhttpd/test_str.c | 4++++
Msrc/microhttpd/test_str_base64.c | 4++++
Msrc/microhttpd/test_str_bin_hex.c | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/microhttpd/test_str_pct.c | 4++++
Msrc/microhttpd/test_str_quote.c | 16++++++++++++----
Msrc/microhttpd/test_str_token.c | 4++++
Msrc/microhttpd/test_str_token_remove.c | 4++++
Msrc/microhttpd/test_str_tokens_remove.c | 4++++
Msrc/microhttpd/test_upgrade.c | 4++++
Msrc/testcurl/https/test_empty_response.c | 4++++
Msrc/testcurl/https/test_https_get.c | 4++++
Msrc/testcurl/https/test_https_get_iovec.c | 4++++
Msrc/testcurl/https/test_https_get_parallel.c | 4++++
Msrc/testcurl/https/test_https_get_parallel_threads.c | 4++++
Msrc/testcurl/https/test_https_get_select.c | 4++++
Msrc/testcurl/https/test_https_multi_daemon.c | 4++++
Msrc/testcurl/https/test_https_session_info.c | 4++++
Msrc/testcurl/https/test_https_sni.c | 4++++
Msrc/testcurl/https/test_https_time_out.c | 4++++
Msrc/testcurl/https/test_tls_authentication.c | 4++++
Msrc/testcurl/https/test_tls_options.c | 4++++
Msrc/testcurl/perf_get.c | 4++++
Msrc/testcurl/perf_get_concurrent.c | 4++++
Msrc/testcurl/test_add_conn.c | 4++++
Msrc/testcurl/test_basicauth.c | 4++++
Msrc/testcurl/test_callback.c | 4++++
Msrc/testcurl/test_concurrent_stop.c | 4++++
Msrc/testcurl/test_delete.c | 4++++
Msrc/testcurl/test_digestauth.c | 4++++
Msrc/testcurl/test_digestauth2.c | 4++++
Msrc/testcurl/test_digestauth_concurrent.c | 4++++
Msrc/testcurl/test_digestauth_emu_ext.c | 4++++
Msrc/testcurl/test_digestauth_sha256.c | 4++++
Msrc/testcurl/test_digestauth_with_arguments.c | 4++++
Msrc/testcurl/test_get.c | 4++++
Msrc/testcurl/test_get_chunked.c | 4++++
Msrc/testcurl/test_get_close_keep_alive.c | 4++++
Msrc/testcurl/test_get_empty.c | 4++++
Msrc/testcurl/test_get_iovec.c | 4++++
Msrc/testcurl/test_get_response_cleanup.c | 4++++
Msrc/testcurl/test_get_sendfile.c | 4++++
Msrc/testcurl/test_get_wait.c | 4++++
Msrc/testcurl/test_head.c | 4++++
Msrc/testcurl/test_iplimit.c | 4++++
Msrc/testcurl/test_large_put.c | 4++++
Msrc/testcurl/test_long_header.c | 4++++
Msrc/testcurl/test_parse_cookies.c | 4++++
Msrc/testcurl/test_patch.c | 4++++
Msrc/testcurl/test_post.c | 4++++
Msrc/testcurl/test_post_loop.c | 4++++
Msrc/testcurl/test_postform.c | 4++++
Msrc/testcurl/test_process_arguments.c | 4++++
Msrc/testcurl/test_process_headers.c | 4++++
Msrc/testcurl/test_put.c | 4++++
Msrc/testcurl/test_put_broken_len.c | 4++++
Msrc/testcurl/test_put_chunked.c | 4++++
Msrc/testcurl/test_put_header_fold.c | 4++++
Msrc/testcurl/test_quiesce.c | 4++++
Msrc/testcurl/test_quiesce_stream.c | 4++++
Msrc/testcurl/test_termination.c | 4++++
Msrc/testcurl/test_timeout.c | 4++++
Msrc/testcurl/test_toolarge.c | 4++++
Msrc/testcurl/test_tricky.c | 4++++
Msrc/testcurl/test_urlparse.c | 4++++
Msrc/testzzuf/Makefile.am | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/testzzuf/README | 320++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Asrc/testzzuf/mhd_zzuf_common.c | 972+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/testzzuf/mhd_zzuf_common.h | 572+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/testzzuf/test_basic_auth.c | 436+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/testzzuf/test_digest_auth.c | 642+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/testzzuf/test_get.c | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Asrc/testzzuf/test_get_args.c | 439+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/testzzuf/test_get_chunked_ext.c | 742+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/testzzuf/test_pipelined.c | 301+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/testzzuf/zzuf_socat_test_runner.sh | 98++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/testzzuf/zzuf_test_runner.sh | 132++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
240 files changed, 21235 insertions(+), 62 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -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/ diff --git a/configure.ac b/configure.ac @@ -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 diff --git a/contrib/Makefile.am b/contrib/Makefile.am @@ -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 diff --git a/contrib/ci/.gitignore b/contrib/ci/.gitignore @@ -0,0 +1,2 @@ +# Output of the 6-coverage job. +/artifacts/ diff --git a/contrib/ci/Containerfile b/contrib/ci/Containerfile @@ -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"] diff --git a/contrib/ci/README b/contrib/ci/README @@ -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. diff --git a/contrib/ci/ci.sh b/contrib/ci/ci.sh @@ -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 diff --git a/contrib/ci/jobs/0-codespell/config.ini b/contrib/ci/jobs/0-codespell/config.ini @@ -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 diff --git a/contrib/ci/jobs/0-codespell/dictionary.txt b/contrib/ci/jobs/0-codespell/dictionary.txt @@ -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 diff --git a/contrib/ci/jobs/0-codespell/job.sh b/contrib/ci/jobs/0-codespell/job.sh @@ -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'/,} "$@" diff --git a/contrib/ci/jobs/0-codespell/skip.txt b/contrib/ci/jobs/0-codespell/skip.txt @@ -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 +*~ diff --git a/contrib/ci/jobs/1-build/build.sh b/contrib/ci/jobs/1-build/build.sh @@ -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)" diff --git a/contrib/ci/jobs/1-build/config.ini b/contrib/ci/jobs/1-build/config.ini @@ -0,0 +1,6 @@ +[build] +HALT_ON_FAILURE = True +WARN_ON_FAILURE = True +CONTAINER_BUILD = True +CONTAINER_NAME = localhost/libmicrohttpd +CONTAINER_ARCH = amd64 diff --git a/contrib/ci/jobs/1-build/job.sh b/contrib/ci/jobs/1-build/job.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -exuo pipefail + +job_dir=$(dirname "${BASH_SOURCE[0]}") + +. "${job_dir}"/build.sh diff --git a/contrib/ci/jobs/2-test/1-build.sh b/contrib/ci/jobs/2-test/1-build.sh @@ -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)" diff --git a/contrib/ci/jobs/2-test/2-test.sh b/contrib/ci/jobs/2-test/2-test.sh @@ -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 diff --git a/contrib/ci/jobs/2-test/config.ini b/contrib/ci/jobs/2-test/config.ini @@ -0,0 +1,6 @@ +[build] +HALT_ON_FAILURE = True +WARN_ON_FAILURE = True +CONTAINER_BUILD = True +CONTAINER_NAME = localhost/libmicrohttpd +CONTAINER_ARCH = amd64 diff --git a/contrib/ci/jobs/2-test/job.sh b/contrib/ci/jobs/2-test/job.sh @@ -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 diff --git a/contrib/ci/jobs/3-build-matrix/1-bootstrap.sh b/contrib/ci/jobs/3-build-matrix/1-bootstrap.sh @@ -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 diff --git a/contrib/ci/jobs/3-build-matrix/2-matrix.sh b/contrib/ci/jobs/3-build-matrix/2-matrix.sh @@ -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 diff --git a/contrib/ci/jobs/3-build-matrix/config.ini b/contrib/ci/jobs/3-build-matrix/config.ini @@ -0,0 +1,6 @@ +[build] +HALT_ON_FAILURE = False +WARN_ON_FAILURE = True +CONTAINER_BUILD = True +CONTAINER_NAME = localhost/libmicrohttpd +CONTAINER_ARCH = amd64 diff --git a/contrib/ci/jobs/3-build-matrix/job.sh b/contrib/ci/jobs/3-build-matrix/job.sh @@ -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 diff --git a/contrib/ci/jobs/4-fuzz/1-build.sh b/contrib/ci/jobs/4-fuzz/1-build.sh @@ -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)" diff --git a/contrib/ci/jobs/4-fuzz/2-fuzz.sh b/contrib/ci/jobs/4-fuzz/2-fuzz.sh @@ -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 diff --git a/contrib/ci/jobs/4-fuzz/config.ini b/contrib/ci/jobs/4-fuzz/config.ini @@ -0,0 +1,6 @@ +[build] +HALT_ON_FAILURE = True +WARN_ON_FAILURE = True +CONTAINER_BUILD = True +CONTAINER_NAME = localhost/libmicrohttpd +CONTAINER_ARCH = amd64 diff --git a/contrib/ci/jobs/4-fuzz/job.sh b/contrib/ci/jobs/4-fuzz/job.sh @@ -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 diff --git a/contrib/ci/jobs/5-zzuf/1-build.sh b/contrib/ci/jobs/5-zzuf/1-build.sh @@ -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)" diff --git a/contrib/ci/jobs/5-zzuf/2-zzuf.sh b/contrib/ci/jobs/5-zzuf/2-zzuf.sh @@ -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}" diff --git a/contrib/ci/jobs/5-zzuf/config.ini b/contrib/ci/jobs/5-zzuf/config.ini @@ -0,0 +1,6 @@ +[build] +HALT_ON_FAILURE = False +WARN_ON_FAILURE = True +CONTAINER_BUILD = True +CONTAINER_NAME = localhost/libmicrohttpd +CONTAINER_ARCH = amd64 diff --git a/contrib/ci/jobs/5-zzuf/job.sh b/contrib/ci/jobs/5-zzuf/job.sh @@ -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 diff --git a/contrib/ci/jobs/6-coverage/1-build.sh b/contrib/ci/jobs/6-coverage/1-build.sh @@ -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)" diff --git a/contrib/ci/jobs/6-coverage/2-test.sh b/contrib/ci/jobs/6-coverage/2-test.sh @@ -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." diff --git a/contrib/ci/jobs/6-coverage/3-coverage.sh b/contrib/ci/jobs/6-coverage/3-coverage.sh @@ -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 diff --git a/contrib/ci/jobs/6-coverage/config.ini b/contrib/ci/jobs/6-coverage/config.ini @@ -0,0 +1,6 @@ +[build] +HALT_ON_FAILURE = False +WARN_ON_FAILURE = True +CONTAINER_BUILD = True +CONTAINER_NAME = localhost/libmicrohttpd +CONTAINER_ARCH = amd64 diff --git a/contrib/ci/jobs/6-coverage/job.sh b/contrib/ci/jobs/6-coverage/job.sh @@ -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 diff --git a/contrib/ci/run-all-jobs b/contrib/ci/run-all-jobs @@ -0,0 +1,5 @@ +#!/bin/bash +set -eax +for JOB in $(ls $(dirname $0)/jobs | sort -n); do + $(dirname $0)/ci.sh $JOB; +done; diff --git a/contrib/ci/version.sh b/contrib/ci/version.sh @@ -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 diff --git a/contrib/oss-fuzz/Dockerfile b/contrib/oss-fuzz/Dockerfile @@ -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/ diff --git a/contrib/oss-fuzz/README b/contrib/oss-fuzz/README @@ -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. diff --git a/contrib/oss-fuzz/build.sh b/contrib/oss-fuzz/build.sh @@ -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}" diff --git a/contrib/oss-fuzz/dicts/fuzz_auth_header.dict b/contrib/oss-fuzz/dicts/fuzz_auth_header.dict @@ -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" diff --git a/contrib/oss-fuzz/dicts/fuzz_postprocessor.dict b/contrib/oss-fuzz/dicts/fuzz_postprocessor.dict @@ -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="; " diff --git a/contrib/oss-fuzz/dicts/fuzz_request.dict b/contrib/oss-fuzz/dicts/fuzz_request.dict @@ -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" diff --git a/contrib/oss-fuzz/dicts/fuzz_str.dict b/contrib/oss-fuzz/dicts/fuzz_str.dict @@ -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=",," diff --git a/contrib/oss-fuzz/fuzz_auth_header.options b/contrib/oss-fuzz/fuzz_auth_header.options @@ -0,0 +1,3 @@ +[libfuzzer] +dict = fuzz_auth_header.dict +max_len = 4097 diff --git a/contrib/oss-fuzz/fuzz_postprocessor.options b/contrib/oss-fuzz/fuzz_postprocessor.options @@ -0,0 +1,3 @@ +[libfuzzer] +dict = fuzz_postprocessor.dict +max_len = 8196 diff --git a/contrib/oss-fuzz/fuzz_request.options b/contrib/oss-fuzz/fuzz_request.options @@ -0,0 +1,3 @@ +[libfuzzer] +dict = fuzz_request.dict +max_len = 8192 diff --git a/contrib/oss-fuzz/fuzz_str.options b/contrib/oss-fuzz/fuzz_str.options @@ -0,0 +1,3 @@ +[libfuzzer] +dict = fuzz_str.dict +max_len = 514 diff --git a/contrib/oss-fuzz/make_seed_corpus.sh b/contrib/oss-fuzz/make_seed_corpus.sh @@ -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 diff --git a/contrib/oss-fuzz/project.yaml b/contrib/oss-fuzz/project.yaml @@ -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" + diff --git a/contrib/run-option-matrix.sh b/contrib/run-option-matrix.sh @@ -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 diff --git a/src/Makefile.am b/src/Makefile.am @@ -9,6 +9,10 @@ SUBDIRS += testzzuf endif endif +if ENABLE_FUZZING +SUBDIRS += fuzz +endif + if BUILD_EXAMPLES SUBDIRS += examples endif diff --git a/src/fuzz/.gitignore b/src/fuzz/.gitignore @@ -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 diff --git a/src/fuzz/BUILD-INTEGRATION.md b/src/fuzz/BUILD-INTEGRATION.md @@ -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/`. diff --git a/src/fuzz/Makefile.am b/src/fuzz/Makefile.am @@ -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 diff --git a/src/fuzz/README b/src/fuzz/README @@ -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. diff --git a/src/fuzz/corpus/README b/src/fuzz/corpus/README @@ -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. diff --git a/src/fuzz/corpus/fuzz_auth_header-00.bin b/src/fuzz/corpus/fuzz_auth_header-00.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_auth_header-01.bin b/src/fuzz/corpus/fuzz_auth_header-01.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_auth_header-02.bin b/src/fuzz/corpus/fuzz_auth_header-02.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_auth_header-03.bin b/src/fuzz/corpus/fuzz_auth_header-03.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_auth_header-04.bin b/src/fuzz/corpus/fuzz_auth_header-04.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_auth_header-05.bin b/src/fuzz/corpus/fuzz_auth_header-05.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_auth_header-06.bin b/src/fuzz/corpus/fuzz_auth_header-06.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_auth_header-07.bin b/src/fuzz/corpus/fuzz_auth_header-07.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_auth_header-08.bin b/src/fuzz/corpus/fuzz_auth_header-08.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_auth_header-09.bin b/src/fuzz/corpus/fuzz_auth_header-09.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_auth_header-10.bin b/src/fuzz/corpus/fuzz_auth_header-10.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_auth_header-11.bin b/src/fuzz/corpus/fuzz_auth_header-11.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_auth_header-12.bin b/src/fuzz/corpus/fuzz_auth_header-12.bin @@ -0,0 +1 @@ +Basic dXNlcjpwYXNz +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_auth_header-13.bin b/src/fuzz/corpus/fuzz_auth_header-13.bin @@ -0,0 +1 @@ +Basic +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_auth_header-14.bin b/src/fuzz/corpus/fuzz_auth_header-14.bin @@ -0,0 +1 @@ +Basic ==== +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_auth_header-15.bin b/src/fuzz/corpus/fuzz_auth_header-15.bin @@ -0,0 +1 @@ +Basic QQ==QQ== +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_postprocessor-00.bin b/src/fuzz/corpus/fuzz_postprocessor-00.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_postprocessor-01.bin b/src/fuzz/corpus/fuzz_postprocessor-01.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_postprocessor-02.bin b/src/fuzz/corpus/fuzz_postprocessor-02.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_postprocessor-03.bin b/src/fuzz/corpus/fuzz_postprocessor-03.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_postprocessor-04.bin b/src/fuzz/corpus/fuzz_postprocessor-04.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_postprocessor-05.bin b/src/fuzz/corpus/fuzz_postprocessor-05.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_postprocessor-06.bin b/src/fuzz/corpus/fuzz_postprocessor-06.bin @@ -0,0 +1,5 @@ +XY +--XY + +noheaders +--XY-- diff --git a/src/fuzz/corpus/fuzz_postprocessor-07.bin b/src/fuzz/corpus/fuzz_postprocessor-07.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_postprocessor-08.bin b/src/fuzz/corpus/fuzz_postprocessor-08.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_postprocessor-09.bin b/src/fuzz/corpus/fuzz_postprocessor-09.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_postprocessor-10.bin b/src/fuzz/corpus/fuzz_postprocessor-10.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-00.bin b/src/fuzz/corpus/fuzz_request-00.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-01.bin b/src/fuzz/corpus/fuzz_request-01.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-02.bin b/src/fuzz/corpus/fuzz_request-02.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-03.bin b/src/fuzz/corpus/fuzz_request-03.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-04.bin b/src/fuzz/corpus/fuzz_request-04.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-05.bin b/src/fuzz/corpus/fuzz_request-05.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-06.bin b/src/fuzz/corpus/fuzz_request-06.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-07.bin b/src/fuzz/corpus/fuzz_request-07.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-08.bin b/src/fuzz/corpus/fuzz_request-08.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-09.bin b/src/fuzz/corpus/fuzz_request-09.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-10.bin b/src/fuzz/corpus/fuzz_request-10.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-11.bin b/src/fuzz/corpus/fuzz_request-11.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-12.bin b/src/fuzz/corpus/fuzz_request-12.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_request-13.bin b/src/fuzz/corpus/fuzz_request-13.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_str-00.bin b/src/fuzz/corpus/fuzz_str-00.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_str-01.bin b/src/fuzz/corpus/fuzz_str-01.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_str-02.bin b/src/fuzz/corpus/fuzz_str-02.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_str-03.bin b/src/fuzz/corpus/fuzz_str-03.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_str-04.bin b/src/fuzz/corpus/fuzz_str-04.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_str-05.bin b/src/fuzz/corpus/fuzz_str-05.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_str-06.bin b/src/fuzz/corpus/fuzz_str-06.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_str-07.bin b/src/fuzz/corpus/fuzz_str-07.bin @@ -0,0 +1 @@ +@/a%41%42%zz% +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_str-08.bin b/src/fuzz/corpus/fuzz_str-08.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_str-09.bin b/src/fuzz/corpus/fuzz_str-09.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_str-10.bin b/src/fuzz/corpus/fuzz_str-10.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_str-11.bin b/src/fuzz/corpus/fuzz_str-11.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_str-12.bin b/src/fuzz/corpus/fuzz_str-12.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_str-13.bin b/src/fuzz/corpus/fuzz_str-13.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_str-14.bin b/src/fuzz/corpus/fuzz_str-14.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_str-15.bin b/src/fuzz/corpus/fuzz_str-15.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_str-16.bin b/src/fuzz/corpus/fuzz_str-16.bin @@ -0,0 +1 @@ + chunked, identity, chunked +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_str-17.bin b/src/fuzz/corpus/fuzz_str-17.bin @@ -0,0 +1 @@ + CHUNKEDchunked +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_str-18.bin b/src/fuzz/corpus/fuzz_str-18.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/K1-digest-empty-realm.bin b/src/fuzz/corpus/known-findings/K1-digest-empty-realm.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/K2-chunkext-no-space.bin b/src/fuzz/corpus/known-findings/K2-chunkext-no-space.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/K3-chunkext-stop-with-error.bin b/src/fuzz/corpus/known-findings/K3-chunkext-stop-with-error.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/K4a-wsp-first-header.bin b/src/fuzz/corpus/known-findings/K4a-wsp-first-header.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/K4b-empty-header-name.bin b/src/fuzz/corpus/known-findings/K4b-empty-header-name.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/K5-bare-cr-keep.bin b/src/fuzz/corpus/known-findings/K5-bare-cr-keep.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/K6-nonce-length-collision.bin b/src/fuzz/corpus/known-findings/K6-nonce-length-collision.bin Binary files differ. diff --git a/src/fuzz/fuzz_auth_header.c b/src/fuzz/fuzz_auth_header.c @@ -0,0 +1,437 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. + If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * @file fuzz/fuzz_auth_header.c + * @brief Direct fuzzer for the "Authorization:" header parsers. + * @author Christian Grothoff + * + * MHD_get_rq_dauth_params_() and MHD_get_rq_bauth_params_() are internal + * (they live in gen_auth.c and are not exported), and both need a + * `struct MHD_Connection`. Rather than pushing bytes through a socket, + * this harness assembles the *minimal* connection object those two + * functions actually touch -- a daemon pointer, a memory pool, a state + * and a single "Authorization" header -- and then varies only the header + * value. That makes this the fastest way to explore the parameter + * parser (roughly two orders of magnitude more executions per second + * than fuzz_request). + * + * Besides memory-safety (ASAN), the harness checks the parser output for + * internal consistency: every returned parameter must be a sub-range of + * the header value that was fed in. A parameter pointing outside of it + * would be a parser bug that ASAN alone might not catch. + * + * Input format: + * byte 0 scheme selector / pool size selector + * byte 1.. the raw "Authorization" header value + */ + +#define FUZZ_HARNESS_NAME "fuzz_auth_header" +#include "fuzz_common.h" + +/* internal.h pulls in MHD_config.h and <microhttpd.h> in the right + order; including <microhttpd.h> first would redefine _MHD_EXTERN. */ +#include "internal.h" +#include "memorypool.h" +#include "gen_auth.h" +#include "mhd_str.h" + +#ifdef DAUTH_SUPPORT +#include "digestauth.h" +#endif +#ifdef BAUTH_SUPPORT +#include "basicauth.h" +#endif + +static const size_t pool_sizes[] = { 256, 512, 1024, 4096, 32768 }; + +/** + * gen_auth.c logs through MHD_DLOG(), which dereferences the daemon of + * the connection, so a real (but idle) daemon is required. It is + * created once and reused for the whole run. + */ +static struct MHD_Daemon *shared_daemon; + +/** Statistics, printed at exit with --verbose. */ +static unsigned long stat_dauth_parsed; +static unsigned long stat_dauth_failed; +static unsigned long stat_bauth_parsed; +static unsigned long stat_bauth_failed; + + +static void +print_stats (void) +{ + if (! fuzz_verbose) + return; + fprintf (stderr, + "%s: digest headers parsed=%lu rejected=%lu; " + "basic headers parsed=%lu rejected=%lu\n", + FUZZ_HARNESS_NAME, + stat_dauth_parsed, stat_dauth_failed, + stat_bauth_parsed, stat_bauth_failed); +} + + +static enum MHD_Result +dummy_ahc (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **req_cls) +{ + (void) cls; (void) connection; (void) url; (void) method; (void) version; + (void) upload_data; (void) upload_data_size; (void) req_cls; + return MHD_NO; +} + + +static void +stop_shared_daemon (void) +{ + if (NULL != shared_daemon) + { + MHD_stop_daemon (shared_daemon); + shared_daemon = NULL; + } +} + + +static struct MHD_Daemon * +get_shared_daemon (void) +{ + if (NULL == shared_daemon) + { + shared_daemon = + MHD_start_daemon (MHD_USE_NO_LISTEN_SOCKET + | (fuzz_verbose ? MHD_USE_ERROR_LOG : 0u), + 0, NULL, NULL, &dummy_ahc, NULL, + MHD_OPTION_END); + if (NULL != shared_daemon) + { + (void) atexit (&stop_shared_daemon); + (void) atexit (&print_stats); + } + } + return shared_daemon; +} + + +#ifdef DAUTH_SUPPORT +/** + * Every parsed parameter must be a sub-range of the header value. + */ +static void +check_param (const struct MHD_RqDAuthParam *pm, + const char *base, + size_t base_len, + const char *what) +{ + char msg[256]; + + if (NULL == pm->value.str) + { + if (0 != pm->value.len) + { + (void) snprintf (msg, sizeof (msg), + "digest parameter '%s' has NULL string but " + "non-zero length", what); + fuzz_report_finding (msg); + } + return; + } + if ( (pm->value.str < base) || + (pm->value.str > base + base_len) || + (pm->value.len > base_len) || + (pm->value.str + pm->value.len > base + base_len) ) + { + (void) snprintf (msg, sizeof (msg), + "digest parameter '%s' points outside of the " + "Authorization header value", what); + fuzz_report_finding (msg); + } +} + + +#endif /* DAUTH_SUPPORT */ + + +int +LLVMFuzzerTestOneInput (const uint8_t *data, + size_t size) +{ + static const char hdr_name[] = MHD_HTTP_HEADER_AUTHORIZATION; + struct MHD_Connection c; + struct MHD_HTTP_Req_Header h; + struct MemoryPool *pool; + char *value; + size_t vlen; + unsigned int sel; + size_t pool_size; + + if (size < 2) + return 0; + if (NULL == get_shared_daemon ()) + return 0; + sel = data[0]; + vlen = size - 1; + if (vlen > 4096) + vlen = 4096; + pool_size = pool_sizes[(sel >> 3) % (sizeof (pool_sizes) + / sizeof (pool_sizes[0]))]; + + /* Exactly sized, zero-terminated copy of the header value, so that + ASAN catches any read past its end. */ + value = (char *) malloc (vlen + 1); + if (NULL == value) + return 0; + memcpy (value, data + 1, vlen); + value[vlen] = '\0'; + + pool = MHD_pool_create (pool_size); + if (NULL == pool) + { + free (value); + return 0; + } + + memset (&c, 0, sizeof (c)); + memset (&h, 0, sizeof (h)); + h.header = hdr_name; + h.header_size = MHD_STATICSTR_LEN_ (MHD_HTTP_HEADER_AUTHORIZATION); + h.value = value; + h.value_size = vlen; + h.kind = MHD_HEADER_KIND; + c.daemon = shared_daemon; + c.pool = pool; + c.rq.headers_received = &h; + c.rq.headers_received_tail = &h; + c.state = MHD_CONNECTION_HEADERS_PROCESSED; + +#ifdef DAUTH_SUPPORT + if (0 == (sel & 0x01)) + { + const struct MHD_RqDAuth *da; + + da = MHD_get_rq_dauth_params_ (&c); + if (NULL == da) + stat_dauth_failed++; + else + { + stat_dauth_parsed++; + check_param (&da->nonce, value, vlen, "nonce"); + check_param (&da->opaque, value, vlen, "opaque"); + check_param (&da->response, value, vlen, "response"); + check_param (&da->username, value, vlen, "username"); + check_param (&da->username_ext, value, vlen, "username*"); + check_param (&da->realm, value, vlen, "realm"); + check_param (&da->uri, value, vlen, "uri"); + check_param (&da->qop_raw, value, vlen, "qop"); + check_param (&da->cnonce, value, vlen, "cnonce"); + check_param (&da->nc, value, vlen, "nc"); + /* The result must be stable: a second call returns the cache. */ + if (da != MHD_get_rq_dauth_params_ (&c)) + fuzz_report_finding ("MHD_get_rq_dauth_params_() is not idempotent"); + } + } +#endif /* DAUTH_SUPPORT */ +#ifdef BAUTH_SUPPORT + if (0 != (sel & 0x01)) + { + const struct MHD_RqBAuth *ba; + + ba = MHD_get_rq_bauth_params_ (&c); + if (NULL == ba) + stat_bauth_failed++; + else + { + stat_bauth_parsed++; + if (NULL != ba->token68.str) + { + if ( (ba->token68.str < value) || + (ba->token68.str + ba->token68.len > value + vlen) ) + fuzz_report_finding ("basic auth token68 points outside of the " + "Authorization header value"); + /* Decoding must fit into the documented maximum size. */ + if (0 != ba->token68.len) + { + size_t need = MHD_base64_max_dec_size_ (ba->token68.len); + uint8_t *bin = (uint8_t *) malloc ((0 == need) ? 1 : need); + + if (NULL != bin) + { + size_t r = MHD_base64_to_bin_n (ba->token68.str, + ba->token68.len, + bin, + need); + if (r > need) + fuzz_report_finding ("MHD_base64_to_bin_n() exceeded " + "MHD_base64_max_dec_size_()"); + free (bin); + } + } + } + if (ba != MHD_get_rq_bauth_params_ (&c)) + fuzz_report_finding ("MHD_get_rq_bauth_params_() is not idempotent"); + } + } +#endif /* BAUTH_SUPPORT */ + + MHD_pool_destroy (pool); + free (value); + return 0; +} + + +/* ------------------------------------------------------------------ */ +/* Generator */ +/* ------------------------------------------------------------------ */ + +static const char *const gen_param_names[] = { + "username", "username*", "realm", "nonce", "uri", "response", "algorithm", + "qop", "nc", "cnonce", "opaque", "userhash", "charset", "domain", + "unknown", "", "USERNAME", "user name" +}; + +static const char *const gen_param_values[] = { + "\"user\"", "user", "\"\"", "\"a\\\"b\"", "\"a\\\\\"", "\"\\\"", + "UTF-8''a%20b", "utf-8''%41", "''", "'", "true", "false", "TRUE", + "auth", "auth-int", "auth,auth-int", "\"auth\"", "MD5", "SHA-256", + "SHA-512-256", "BOGUS", "\"SHA-256\"", "00000001", "ffffffff", + "0123456789abcdef0123456789abcdef", + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"" +}; + + +static size_t +fuzz_generate (struct fuzz_rng *rng, + uint8_t *buf, + size_t cap) +{ + size_t len = 0; + unsigned int nparams; + unsigned int i; + int basic; + + if (cap < 32) + return 0; + basic = fuzz_chance (rng, 4); + buf[len++] = (uint8_t) ((fuzz_byte (rng) & 0xFE) | (basic ? 1u : 0u)); + +/* NB: evaluate the argument exactly once -- it usually contains a + call into the PRNG. */ +#define ADD(s) \ + do { \ + const char *s_ = (s); \ + size_t l_ = strlen (s_); \ + if (len + l_ >= cap) \ + return len; \ + memcpy (buf + len, s_, l_); \ + len += l_; \ + } while (0) + + if (basic) + { + unsigned int n; + + ADD ("Basic "); + n = fuzz_below (rng, 48); + for (i = 0; (i < n) && (len < cap); i++) + buf[len++] = + (uint8_t) "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789+/= "[fuzz_below (rng, 66)]; + return len; + } + + ADD ("Digest "); + nparams = 1 + fuzz_below (rng, 12); + for (i = 0; i < nparams; i++) + { + if (0 != i) + ADD (fuzz_chance (rng, 8) ? "," : ", "); + ADD (gen_param_names[fuzz_below (rng, + (uint32_t) (sizeof (gen_param_names) + / sizeof (char *)))]); + if (! fuzz_chance (rng, 10)) + ADD (fuzz_chance (rng, 10) ? " = " : "="); + ADD (gen_param_values[fuzz_below (rng, + (uint32_t) (sizeof (gen_param_values) + / sizeof (char *)))]); + } +#undef ADD + return len; +} + + +/* ------------------------------------------------------------------ */ +/* Seed corpus */ +/* ------------------------------------------------------------------ */ + +struct ah_seed +{ + const char *txt; + size_t len; +}; + +#define ASEED(t) { t, sizeof (t) - 1 } + +static const struct ah_seed ah_seeds[] = { + ASEED ("\x00" "Digest username=\"user\", realm=\"TestRealm\", " + "nonce=\"0123456789abcdef\", uri=\"/a\", qop=auth, nc=00000001, " + "cnonce=\"x\", algorithm=MD5, response=\"0123456789abcdef\""), + ASEED ("\x00" "Digest algorithm=BOGUS"), + ASEED ("\x00" "Digest algorithm="), + ASEED ("\x00" "Digest username*=UTF-8''a%20b"), + ASEED ("\x00" "Digest username*=''"), + ASEED ("\x00" "Digest username=\"\\\""), + ASEED ("\x00" "Digest userhash=true, username=\"aaaa\""), + ASEED ("\x00" "Digest response=\"" + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\""), + ASEED ("\x00" "Digest"), + ASEED ("\x00" "Digest "), + ASEED ("\x00" "Digest ,,,,,"), + ASEED ("\x00" "Digest nc=\"ffffffffffffffffffffffff\""), + ASEED ("\x01" "Basic dXNlcjpwYXNz"), + ASEED ("\x01" "Basic "), + ASEED ("\x01" "Basic ===="), + ASEED ("\x01" "Basic QQ==QQ==") +}; + + +static size_t +fuzz_seed_count (void) +{ + return sizeof (ah_seeds) / sizeof (ah_seeds[0]); +} + + +static const uint8_t * +fuzz_seed_get (size_t idx, + size_t *len) +{ + *len = ah_seeds[idx].len; + return (const uint8_t *) ah_seeds[idx].txt; +} diff --git a/src/fuzz/fuzz_common.h b/src/fuzz/fuzz_common.h @@ -0,0 +1,894 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. + If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * @file fuzz/fuzz_common.h + * @brief Shared, header-only fuzzing driver for the MHD in-process fuzzers. + * @author Christian Grothoff + * + * Every harness in this directory is a single translation unit that + * includes this header. The header provides: + * - a deterministic, seeded PRNG (splitmix64 / xoshiro256**), + * - a generic byte-level mutator, + * - crash bookkeeping (the input of the currently running iteration is + * dumped to the crash directory whenever the process dies), + * - a standalone @c main() driver (generator + mutator loop, corpus + * replay, single-file replay) so that the harnesses are usable with + * a plain gcc + ASAN/UBSAN build, i.e. without clang/libFuzzer. + * + * The harness itself must provide: + * - @c LLVMFuzzerTestOneInput() (the actual fuzz target), + * - @c fuzz_generate() (a structure-aware input generator), + * - @c fuzz_seed_count() / @c fuzz_seed_get() (a built-in seed corpus). + * + * Define @c FUZZ_NO_MAIN when linking against libFuzzer or AFL++'s + * driver, which supply their own @c main(). + */ + +#ifndef MHD_FUZZ_COMMON_H +#define MHD_FUZZ_COMMON_H 1 + +#include <stdint.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <fcntl.h> +#include <signal.h> +#include <errno.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <dirent.h> + +#ifndef FUZZ_HARNESS_NAME +#define FUZZ_HARNESS_NAME "fuzz" +#endif + +/** + * Not every harness uses every helper; silence -Wunused-function. + */ +#define FUZZ_UNUSED __attribute__ ((unused)) + +/** + * Hard upper bound on the size of a single fuzz input. + */ +#ifndef FUZZ_MAX_INPUT +#define FUZZ_MAX_INPUT 32768 +#endif + +/** + * Default number of iterations of the built-in driver. Kept small so + * that "make check" stays in the "couple of seconds" range; raise with + * --iterations=N or the MHD_FUZZ_ITERATIONS environment variable. + */ +#ifndef FUZZ_DEFAULT_ITERATIONS +#define FUZZ_DEFAULT_ITERATIONS 3000 +#endif + +/** + * Default per-iteration watchdog, in seconds. + */ +#ifndef FUZZ_DEFAULT_TIMEOUT +#define FUZZ_DEFAULT_TIMEOUT 20 +#endif + + +/* ------------------------------------------------------------------ */ +/* Interface to be implemented by each harness */ +/* ------------------------------------------------------------------ */ + +/** + * The fuzz target. Signature is the libFuzzer one on purpose, so that + * the very same harness can be linked with libFuzzer or AFL++ later. + */ +int +LLVMFuzzerTestOneInput (const uint8_t *data, + size_t size); + +struct fuzz_rng; + +/** + * Structure-aware generator used by the built-in standalone driver. + * Must write at most @a cap bytes to @a buf and return the number of + * bytes written. Purely random bytes almost never form a valid HTTP + * request, so this is what actually makes the gcc-only driver useful. + */ +FUZZ_UNUSED static size_t +fuzz_generate (struct fuzz_rng *rng, + uint8_t *buf, + size_t cap); + +/** + * @return number of entries in the built-in seed corpus + */ +FUZZ_UNUSED static size_t +fuzz_seed_count (void); + +/** + * @param idx index of the seed to retrieve + * @param[out] len set to the length of the seed + * @return pointer to the seed bytes + */ +FUZZ_UNUSED static const uint8_t * +fuzz_seed_get (size_t idx, + size_t *len); + + +/* ------------------------------------------------------------------ */ +/* Deterministic PRNG */ +/* ------------------------------------------------------------------ */ + +struct fuzz_rng +{ + uint64_t s[4]; +}; + +FUZZ_UNUSED static uint64_t +fuzz_splitmix64 (uint64_t *x) +{ + uint64_t z; + + *x += UINT64_C (0x9E3779B97F4A7C15); + z = *x; + z = (z ^ (z >> 30)) * UINT64_C (0xBF58476D1CE4E5B9); + z = (z ^ (z >> 27)) * UINT64_C (0x94D049BB133111EB); + return z ^ (z >> 31); +} + + +FUZZ_UNUSED static void +fuzz_rng_seed (struct fuzz_rng *r, + uint64_t seed) +{ + uint64_t x = seed; + unsigned int i; + + for (i = 0; i < 4; i++) + r->s[i] = fuzz_splitmix64 (&x); +} + + +FUZZ_UNUSED static uint64_t +fuzz_rot64 (uint64_t x, + unsigned int k) +{ + return (x << k) | (x >> (64 - k)); +} + + +/** + * xoshiro256** -- small, fast, deterministic and identical on every + * platform, which is what we need for reproducible fuzzing runs. + */ +FUZZ_UNUSED static uint64_t +fuzz_next (struct fuzz_rng *r) +{ + const uint64_t res = fuzz_rot64 (r->s[1] * 5, 7) * 9; + const uint64_t t = r->s[1] << 17; + + r->s[2] ^= r->s[0]; + r->s[3] ^= r->s[1]; + r->s[1] ^= r->s[2]; + r->s[0] ^= r->s[3]; + r->s[2] ^= t; + r->s[3] = fuzz_rot64 (r->s[3], 45); + return res; +} + + +/** + * @return uniformly distributed value in [0, n), 0 if @a n is 0 + */ +FUZZ_UNUSED static uint32_t +fuzz_below (struct fuzz_rng *r, + uint32_t n) +{ + if (0 == n) + return 0; + return (uint32_t) (fuzz_next (r) % n); +} + + +FUZZ_UNUSED static uint8_t +fuzz_byte (struct fuzz_rng *r) +{ + return (uint8_t) (fuzz_next (r) & 0xFF); +} + + +/** + * @return true with a probability of 1/@a n + */ +FUZZ_UNUSED static int +fuzz_chance (struct fuzz_rng *r, + uint32_t n) +{ + return 0 == fuzz_below (r, n); +} + + +/* ------------------------------------------------------------------ */ +/* Global driver state */ +/* ------------------------------------------------------------------ */ + +/** + * Non-zero if the input of the current iteration comes from a trusted + * source (the built-in generator, the built-in seed corpus, --file or + * --corpus-dir) and has NOT been mutated afterwards. Harnesses use + * this to enable "ground truth" oracles, e.g. the expected decoded + * request body that a generated chunked request declares. Random + * mutations invalidate such declarations, hence the flag. + */ +FUZZ_UNUSED static int fuzz_pristine; + +/** + * Non-zero to let the harness enable MHD's own error log. + */ +FUZZ_UNUSED static int fuzz_verbose; + +/** + * Non-zero to skip the replay of the built-in seed corpus at the start + * of a run (useful when a seed is known to trigger an already-reported + * finding and one wants to look for others). + */ +FUZZ_UNUSED static int fuzz_skip_seeds; + +/* Only read by the built-in driver; under -DFUZZ_NO_MAIN (the libFuzzer + and AFL++ builds, see contrib/oss-fuzz/build.sh) they are written but + never read, which is not a defect. */ +FUZZ_UNUSED static uint64_t fuzz_cur_seed; +FUZZ_UNUSED static uint64_t fuzz_cur_iter; +static const uint8_t *fuzz_cur_input; +static size_t fuzz_cur_input_len; +static const char *fuzz_crash_dir = "crashes"; + +/* Pre-rendered, so that the death/signal handlers stay + async-signal-safe (no snprintf, no malloc). */ +static char fuzz_crash_path[512]; +static char fuzz_crash_msg[512]; +static volatile sig_atomic_t fuzz_dumped; + + +FUZZ_UNUSED static void +fuzz_write_all (int fd, + const void *buf, + size_t len) +{ + const char *p = (const char *) buf; + + while (0 != len) + { + ssize_t w = write (fd, p, len); + if (0 >= w) + break; + p += w; + len -= (size_t) w; + } +} + + +FUZZ_UNUSED static void +fuzz_msg (const char *s) +{ + fuzz_write_all (STDERR_FILENO, s, strlen (s)); +} + + +/** + * Dump the input of the currently running iteration so that the + * failure can be replayed with --file=... Async-signal-safe. + */ +FUZZ_UNUSED static void +fuzz_dump_current (void) +{ + int fd; + + if (fuzz_dumped) + return; + fuzz_dumped = 1; + if ( (NULL == fuzz_cur_input) || + ('\0' == fuzz_crash_path[0]) ) + return; + (void) mkdir (fuzz_crash_dir, 0755); + fd = open (fuzz_crash_path, + O_WRONLY | O_CREAT | O_TRUNC, + 0644); + if (0 > fd) + { + fuzz_msg ("\n*** FUZZ: failed to write crash file ***\n"); + return; + } + fuzz_write_all (fd, fuzz_cur_input, fuzz_cur_input_len); + (void) close (fd); + fuzz_msg ("\n*** FUZZ: reproducer written to "); + fuzz_msg (fuzz_crash_path); + fuzz_msg (" ***\n*** FUZZ: "); + fuzz_msg (fuzz_crash_msg); + fuzz_msg (" ***\n"); +} + + +FUZZ_UNUSED static void +fuzz_death_callback (void) +{ + fuzz_dump_current (); +} + + +FUZZ_UNUSED static void +fuzz_sig_handler (int sig) +{ + fuzz_dump_current (); + if (SIGALRM == sig) + { + fuzz_msg ("*** FUZZ: HANG detected (watchdog fired) ***\n"); + _exit (99); + } + /* restore default handler and re-raise so that the usual + ASAN/abort diagnostics are produced */ + signal (sig, SIG_DFL); + raise (sig); +} + + +/** + * Report a logical (non-memory-safety) finding: dump the reproducer + * and abort so that the failure is impossible to overlook. + */ +FUZZ_UNUSED static void +fuzz_report_finding (const char *what) +{ + size_t l = strlen (what); + + if (l >= sizeof (fuzz_crash_msg)) + l = sizeof (fuzz_crash_msg) - 1; + memcpy (fuzz_crash_msg, what, l); + fuzz_crash_msg[l] = '\0'; + fuzz_msg ("\n*** FUZZ FINDING: "); + fuzz_msg (fuzz_crash_msg); + fuzz_msg (" ***\n"); + fuzz_dump_current (); + abort (); +} + + +/* Weak declaration: resolved when built with ASAN (gcc or clang), + NULL otherwise. ASAN calls this right before it terminates the + process, which is the only reliable hook when abort_on_error=0. */ +extern void +__sanitizer_set_death_callback (void (*cb)(void)) __attribute__ ((weak)); + + +#ifndef FUZZ_NO_MAIN + +static void +fuzz_install_handlers (void) +{ + struct sigaction sa; + + if (NULL != __sanitizer_set_death_callback) + __sanitizer_set_death_callback (&fuzz_death_callback); + memset (&sa, 0, sizeof (sa)); + sa.sa_handler = &fuzz_sig_handler; + sigemptyset (&sa.sa_mask); + sa.sa_flags = 0; + (void) sigaction (SIGABRT, &sa, NULL); + (void) sigaction (SIGSEGV, &sa, NULL); + (void) sigaction (SIGBUS, &sa, NULL); + (void) sigaction (SIGILL, &sa, NULL); + (void) sigaction (SIGFPE, &sa, NULL); + (void) sigaction (SIGALRM, &sa, NULL); + (void) signal (SIGPIPE, SIG_IGN); +} + + +/** + * Remember which input we are about to feed to the target, and + * pre-render the name of the file it would be dumped to. + */ +static void +fuzz_set_current (const uint8_t *data, + size_t size, + const char *tag) +{ + fuzz_cur_input = data; + fuzz_cur_input_len = size; + fuzz_dumped = 0; + (void) snprintf (fuzz_crash_path, + sizeof (fuzz_crash_path), + "%s/crash-%s-seed%llu-iter%llu.bin", + fuzz_crash_dir, + FUZZ_HARNESS_NAME, + (unsigned long long) fuzz_cur_seed, + (unsigned long long) fuzz_cur_iter); + (void) snprintf (fuzz_crash_msg, + sizeof (fuzz_crash_msg), + "harness=%s seed=%llu iteration=%llu source=%s", + FUZZ_HARNESS_NAME, + (unsigned long long) fuzz_cur_seed, + (unsigned long long) fuzz_cur_iter, + tag); +} + + +/* ------------------------------------------------------------------ */ +/* Generic byte-level mutator */ +/* ------------------------------------------------------------------ */ + +static const uint8_t fuzz_interesting[] = { + 0x00, 0x01, 0x07, 0x09, 0x0A, 0x0D, 0x20, 0x22, 0x25, 0x26, 0x27, + 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x3A, 0x3B, 0x3D, 0x3F, 0x40, + 0x5C, 0x7B, 0x7D, 0x7F, 0x80, 0xC0, 0xFE, 0xFF +}; + +static const char *const fuzz_interesting_str[] = { + "\r\n", "\r\n\r\n", "\n", "%", "%%NONCE%%", ";", "=", "\"", "\\", + "chunked", "Transfer-Encoding: ", "Content-Length: ", "algorithm=", + "userhash=true", "Authorization: Digest ", "Authorization: Basic ", + "boundary=", "multipart/form-data", "0\r\n\r\n", "?", "&", + "aaaaaaaaaaaaaaaa" +}; + + +/** + * Apply a single random mutation to @a buf. + * + * @param rng the PRNG state + * @param[in,out] buf the buffer to mutate + * @param len current length + * @param cap capacity of @a buf + * @return new length + */ +static size_t +fuzz_mutate_once (struct fuzz_rng *rng, + uint8_t *buf, + size_t len, + size_t cap) +{ + uint32_t op; + + if (0 == len) + { + buf[0] = fuzz_byte (rng); + return 1; + } + op = fuzz_below (rng, 10); + switch (op) + { + case 0: /* bit flip */ + { + size_t p = fuzz_below (rng, (uint32_t) len); + buf[p] = (uint8_t) (buf[p] ^ (1u << fuzz_below (rng, 8))); + break; + } + case 1: /* random byte */ + buf[fuzz_below (rng, (uint32_t) len)] = fuzz_byte (rng); + break; + case 2: /* interesting byte */ + buf[fuzz_below (rng, (uint32_t) len)] = + fuzz_interesting[fuzz_below (rng, + (uint32_t) (sizeof (fuzz_interesting)))]; + break; + case 3: /* add/sub small value */ + { + size_t p = fuzz_below (rng, (uint32_t) len); + buf[p] = (uint8_t) (buf[p] + (int) fuzz_below (rng, 17) - 8); + break; + } + case 4: /* erase a run */ + { + size_t p = fuzz_below (rng, (uint32_t) len); + size_t n = 1 + fuzz_below (rng, (uint32_t) (len - p)); + memmove (buf + p, buf + p + n, len - p - n); + len -= n; + break; + } + case 5: /* insert repeated byte */ + { + size_t p = fuzz_below (rng, (uint32_t) len + 1); + size_t n = 1 + fuzz_below (rng, 32); + uint8_t v = fuzz_byte (rng); + if (len + n > cap) + n = cap - len; + if (0 == n) + break; + memmove (buf + p + n, buf + p, len - p); + memset (buf + p, v, n); + len += n; + break; + } + case 6: /* duplicate a chunk */ + { + size_t p = fuzz_below (rng, (uint32_t) len); + size_t n = 1 + fuzz_below (rng, (uint32_t) (len - p)); + size_t d = fuzz_below (rng, (uint32_t) len + 1); + if (len + n > cap) + n = cap - len; + if (0 == n) + break; + memmove (buf + d + n, buf + d, len - d); + memmove (buf + d, buf + ((p >= d) ? (p + n) : p), n); + len += n; + break; + } + case 7: /* insert an interesting token */ + { + const char *s = + fuzz_interesting_str[fuzz_below (rng, + (uint32_t) + (sizeof (fuzz_interesting_str) + / sizeof (fuzz_interesting_str[0])))]; + size_t n = strlen (s); + size_t p = fuzz_below (rng, (uint32_t) len + 1); + if (len + n > cap) + break; + memmove (buf + p + n, buf + p, len - p); + memcpy (buf + p, s, n); + len += n; + break; + } + case 8: /* swap two bytes */ + { + size_t a = fuzz_below (rng, (uint32_t) len); + size_t b = fuzz_below (rng, (uint32_t) len); + uint8_t t = buf[a]; + buf[a] = buf[b]; + buf[b] = t; + break; + } + default: /* truncate */ + len = 1 + fuzz_below (rng, (uint32_t) len); + break; + } + return len; +} + + +/* ------------------------------------------------------------------ */ +/* Standalone driver */ +/* ------------------------------------------------------------------ */ + +static int +fuzz_run_file (const char *path) +{ + FILE *f; + uint8_t *buf; + size_t n; + + f = fopen (path, "rb"); + if (NULL == f) + { + fprintf (stderr, + "%s: cannot open '%s': %s\n", + FUZZ_HARNESS_NAME, + path, + strerror (errno)); + return 1; + } + buf = (uint8_t *) malloc (FUZZ_MAX_INPUT); + if (NULL == buf) + { + (void) fclose (f); + return 1; + } + n = fread (buf, 1, FUZZ_MAX_INPUT, f); + (void) fclose (f); + fuzz_pristine = 1; + fuzz_set_current (buf, n, path); + alarm (FUZZ_DEFAULT_TIMEOUT); + (void) LLVMFuzzerTestOneInput (buf, n); + alarm (0); + free (buf); + return 0; +} + + +static int +fuzz_run_corpus_dir (const char *dir) +{ + DIR *d; + struct dirent *de; + char path[1024]; + int ret = 0; + unsigned int cnt = 0; + + d = opendir (dir); + if (NULL == d) + { + fprintf (stderr, + "%s: cannot open corpus dir '%s': %s\n", + FUZZ_HARNESS_NAME, + dir, + strerror (errno)); + return 1; + } + while (NULL != (de = readdir (d))) + { + struct stat sb; + + if ('.' == de->d_name[0]) + continue; + (void) snprintf (path, sizeof (path), "%s/%s", dir, de->d_name); + if ( (0 != stat (path, &sb)) || + (! S_ISREG (sb.st_mode)) ) + continue; + fuzz_cur_iter = cnt++; + ret |= fuzz_run_file (path); + } + (void) closedir (d); + printf ("%s: replayed %u corpus file(s) from %s\n", + FUZZ_HARNESS_NAME, cnt, dir); + return ret; +} + + +static void +fuzz_usage (const char *argv0) +{ + printf ( + "Usage: %s [OPTIONS] [FILE...]\n" + "\n" + "In-process fuzzing harness '%s' for GNU libmicrohttpd.\n" + "\n" + " --iterations=N number of generate/mutate iterations (default %d)\n" + " --seed=N PRNG seed; runs are fully reproducible (default 1)\n" + " --corpus-dir=DIR replay every regular file in DIR and exit\n" + " --file=PATH replay a single input and exit (crash reproduction)\n" + " --crash-dir=DIR where to write reproducers (default 'crashes')\n" + " --timeout=SEC per-iteration watchdog (default %d, 0 disables)\n" + " --write-corpus=DIR write the built-in seed corpus to DIR and exit\n" + " --skip-seeds do not replay the built-in seed corpus first\n" + " --verbose enable MHD's error log inside the harness\n" + " --help this text\n" + "\n" + "Environment: MHD_FUZZ_ITERATIONS, MHD_FUZZ_SEED, MHD_FUZZ_TIMEOUT,\n" + " MHD_FUZZ_CRASH_DIR, MHD_FUZZ_VERBOSE\n" + "\n" + "Bare FILE arguments are equivalent to --file=FILE (libFuzzer-style).\n", + argv0, FUZZ_HARNESS_NAME, + (int) FUZZ_DEFAULT_ITERATIONS, (int) FUZZ_DEFAULT_TIMEOUT); +} + + +static int +fuzz_write_corpus (const char *dir) +{ + size_t i; + size_t n = fuzz_seed_count (); + + if ( (0 != mkdir (dir, 0755)) && + (EEXIST != errno) ) + { + fprintf (stderr, "%s: mkdir '%s': %s\n", + FUZZ_HARNESS_NAME, dir, strerror (errno)); + return 1; + } + for (i = 0; i < n; i++) + { + char path[1024]; + size_t len; + const uint8_t *s = fuzz_seed_get (i, &len); + FILE *f; + + (void) snprintf (path, sizeof (path), "%s/%s-%02u.bin", + dir, FUZZ_HARNESS_NAME, (unsigned int) i); + f = fopen (path, "wb"); + if (NULL == f) + { + fprintf (stderr, "%s: fopen '%s': %s\n", + FUZZ_HARNESS_NAME, path, strerror (errno)); + return 1; + } + if (len != fwrite (s, 1, len, f)) + { + (void) fclose (f); + return 1; + } + (void) fclose (f); + } + printf ("%s: wrote %u seed(s) to %s\n", + FUZZ_HARNESS_NAME, (unsigned int) n, dir); + return 0; +} + + +int +main (int argc, char *const *argv) +{ + uint64_t iterations = FUZZ_DEFAULT_ITERATIONS; + uint64_t seed = 1; + unsigned int timeout = FUZZ_DEFAULT_TIMEOUT; + const char *corpus_dir = NULL; + const char *single_file = NULL; + const char *write_corpus = NULL; + struct fuzz_rng rng; + uint8_t *buf; + uint64_t i; + int j; + const char *e; + int ret = 0; + + e = getenv ("MHD_FUZZ_ITERATIONS"); + if (NULL != e) + iterations = strtoull (e, NULL, 10); + e = getenv ("MHD_FUZZ_SEED"); + if (NULL != e) + seed = strtoull (e, NULL, 10); + e = getenv ("MHD_FUZZ_TIMEOUT"); + if (NULL != e) + timeout = (unsigned int) strtoul (e, NULL, 10); + e = getenv ("MHD_FUZZ_CRASH_DIR"); + if (NULL != e) + fuzz_crash_dir = e; + e = getenv ("MHD_FUZZ_VERBOSE"); + if (NULL != e) + fuzz_verbose = (0 != atoi (e)); + e = getenv ("MHD_FUZZ_SKIP_SEEDS"); + if (NULL != e) + fuzz_skip_seeds = (0 != atoi (e)); + + for (j = 1; j < argc; j++) + { + const char *a = argv[j]; + + if (0 == strncmp (a, "--iterations=", 13)) + iterations = strtoull (a + 13, NULL, 10); + else if (0 == strncmp (a, "--seed=", 7)) + seed = strtoull (a + 7, NULL, 10); + else if (0 == strncmp (a, "--corpus-dir=", 13)) + corpus_dir = a + 13; + else if (0 == strncmp (a, "--file=", 7)) + single_file = a + 7; + else if (0 == strncmp (a, "--crash-dir=", 12)) + fuzz_crash_dir = a + 12; + else if (0 == strncmp (a, "--timeout=", 10)) + timeout = (unsigned int) strtoul (a + 10, NULL, 10); + else if (0 == strncmp (a, "--write-corpus=", 15)) + write_corpus = a + 15; + else if (0 == strcmp (a, "--skip-seeds")) + fuzz_skip_seeds = 1; + else if (0 == strcmp (a, "--verbose")) + fuzz_verbose = 1; + else if ( (0 == strcmp (a, "--help")) || + (0 == strcmp (a, "-h")) ) + { + fuzz_usage (argv[0]); + return 0; + } + else if ('-' == a[0]) + { + fprintf (stderr, "%s: unknown option '%s'\n", FUZZ_HARNESS_NAME, a); + fuzz_usage (argv[0]); + return 2; + } + else + single_file = a; + } + + fuzz_cur_seed = seed; + fuzz_install_handlers (); + + if (NULL != write_corpus) + return fuzz_write_corpus (write_corpus); + + if (NULL != single_file) + { + printf ("%s: replaying %s\n", FUZZ_HARNESS_NAME, single_file); + ret = fuzz_run_file (single_file); + printf ("%s: replay finished without a finding\n", FUZZ_HARNESS_NAME); + return ret; + } + if (NULL != corpus_dir) + return fuzz_run_corpus_dir (corpus_dir); + + buf = (uint8_t *) malloc (FUZZ_MAX_INPUT); + if (NULL == buf) + return 1; + fuzz_rng_seed (&rng, seed); + printf ("%s: seed=%llu iterations=%llu\n", + FUZZ_HARNESS_NAME, + (unsigned long long) seed, + (unsigned long long) iterations); + fflush (stdout); + + for (i = 0; i < iterations; i++) + { + size_t len; + const char *tag; + uint32_t mode; + + fuzz_cur_iter = i; + if ( (! fuzz_skip_seeds) && + (i < fuzz_seed_count ()) ) + { + size_t sl; + const uint8_t *s = fuzz_seed_get ((size_t) i, &sl); + + if (sl > FUZZ_MAX_INPUT) + sl = FUZZ_MAX_INPUT; + memcpy (buf, s, sl); + len = sl; + fuzz_pristine = 1; + tag = "builtin-seed"; + } + else + { + mode = fuzz_below (&rng, 100); + if (mode < 55) + { + fuzz_pristine = 1; + len = fuzz_generate (&rng, buf, FUZZ_MAX_INPUT); + tag = "generated"; + } + else + { + unsigned int k; + unsigned int nmut; + + if (mode < 85) + { + len = fuzz_generate (&rng, buf, FUZZ_MAX_INPUT); + tag = "generated+mutated"; + } + else + { + size_t sl; + const uint8_t *s; + + if (0 == fuzz_seed_count ()) + { + len = fuzz_generate (&rng, buf, FUZZ_MAX_INPUT); + } + else + { + s = fuzz_seed_get (fuzz_below (&rng, + (uint32_t) fuzz_seed_count ()), + &sl); + if (sl > FUZZ_MAX_INPUT) + sl = FUZZ_MAX_INPUT; + memcpy (buf, s, sl); + len = sl; + } + tag = "seed+mutated"; + } + fuzz_pristine = 0; + nmut = 1 + fuzz_below (&rng, 8); + for (k = 0; k < nmut; k++) + len = fuzz_mutate_once (&rng, buf, len, FUZZ_MAX_INPUT); + } + } + fuzz_set_current (buf, len, tag); + if (0 != timeout) + alarm (timeout); + (void) LLVMFuzzerTestOneInput (buf, len); + if (0 != timeout) + alarm (0); + } + free (buf); + printf ("%s: %llu iterations completed, no findings\n", + FUZZ_HARNESS_NAME, + (unsigned long long) iterations); + return ret; +} + + +#endif /* ! FUZZ_NO_MAIN */ + +#endif /* MHD_FUZZ_COMMON_H */ diff --git a/src/fuzz/fuzz_postprocessor.c b/src/fuzz/fuzz_postprocessor.c @@ -0,0 +1,477 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. + If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * @file fuzz/fuzz_postprocessor.c + * @brief Fuzzer for MHD_post_process() + * @author Christian Grothoff + * + * MHD_create_post_processor() only ever looks at the "Content-Type" + * header of the connection it is given, so this harness fabricates the + * minimal connection object instead of pushing a whole request through + * a socket. That keeps the harness fast and lets the fuzzer control + * three dimensions that matter for the post processor and that a real + * request would not expose directly: + * + * - the raw Content-Type (encoding and multipart boundary), + * - the post processor buffer size, + * - how the POST data is split across MHD_post_process() calls. + * + * Input format: + * byte 0 content type selector + * byte 1 post processor buffer size selector + * byte 2 chunking pattern selector + * byte 3 length of the boundary taken from the payload + * byte 4.. the POST data + */ + +#define FUZZ_HARNESS_NAME "fuzz_postprocessor" +#include "fuzz_common.h" + +/* internal.h pulls in MHD_config.h and <microhttpd.h> in the right + order; including <microhttpd.h> first would redefine _MHD_EXTERN. */ +#include "internal.h" + +static const size_t pp_buf_sizes[] = { + 256, 257, 300, 512, 1024, 2048, 4096, 65536 +}; + +static const size_t chunk_patterns[] = { + 1, 2, 3, 5, 7, 13, 32, 64, 1024, 0 /* 0 == everything at once */ +}; + +static struct MHD_Daemon *shared_daemon; + +/** Statistics, printed at exit with --verbose. */ +static unsigned long stat_pp_created; +static unsigned long stat_pp_failed; +static unsigned long stat_values; + + +static enum MHD_Result +dummy_ahc (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **req_cls) +{ + (void) cls; (void) connection; (void) url; (void) method; (void) version; + (void) upload_data; (void) upload_data_size; (void) req_cls; + return MHD_NO; +} + + +static void +stop_shared_daemon (void) +{ + if (NULL != shared_daemon) + { + MHD_stop_daemon (shared_daemon); + shared_daemon = NULL; + } +} + + +static void +print_stats (void) +{ + if (! fuzz_verbose) + return; + fprintf (stderr, + "%s: post processors created=%lu rejected=%lu, " + "values reported=%lu\n", + FUZZ_HARNESS_NAME, + stat_pp_created, stat_pp_failed, stat_values); +} + + +static struct MHD_Daemon * +get_shared_daemon (void) +{ + if (NULL == shared_daemon) + { + shared_daemon = + MHD_start_daemon (MHD_USE_NO_LISTEN_SOCKET + | (fuzz_verbose ? MHD_USE_ERROR_LOG : 0u), + 0, NULL, NULL, &dummy_ahc, NULL, + MHD_OPTION_END); + if (NULL != shared_daemon) + { + (void) atexit (&stop_shared_daemon); + (void) atexit (&print_stats); + } + } + return shared_daemon; +} + + +struct pp_ctx +{ + const char *data; + size_t data_len; + uint64_t max_off; +}; + + +static enum MHD_Result +post_iter (void *cls, + enum MHD_ValueKind kind, + const char *key, + const char *filename, + const char *content_type, + const char *transfer_encoding, + const char *data, + uint64_t off, + size_t size) +{ + struct pp_ctx *ctx = (struct pp_ctx *) cls; + volatile size_t sink = 0; + + (void) kind; + stat_values++; + /* Touch every string the post processor claims to have produced; + ASAN turns any dangling or out-of-bounds pointer into an error. */ + if (NULL != key) + sink += strlen (key); + if (NULL != filename) + sink += strlen (filename); + if (NULL != content_type) + sink += strlen (content_type); + if (NULL != transfer_encoding) + sink += strlen (transfer_encoding); + if ( (NULL != data) && (0 != size) ) + { + size_t i; + + for (i = 0; i < size; i++) + sink += (size_t) (unsigned char) data[i]; + } + else if (NULL == data) + { + if (0 != size) + fuzz_report_finding ("MHD_PostDataIterator called with NULL data but " + "non-zero size"); + } + /* A single value is delivered in order and can never be longer than + the POST data that was fed in. */ + if (off + size > ctx->data_len + 1) + fuzz_report_finding ("MHD_PostDataIterator reported more value data " + "than the POST data contained"); + if (off > ctx->max_off + ctx->data_len) + fuzz_report_finding ("MHD_PostDataIterator offset jumped implausibly"); + ctx->max_off = off; + (void) sink; + return MHD_YES; +} + + +int +LLVMFuzzerTestOneInput (const uint8_t *data, + size_t size) +{ + struct MHD_Connection c; + struct MHD_HTTP_Req_Header h; + struct MHD_PostProcessor *pp; + struct pp_ctx ctx; + char *ctype; + char *body; + size_t body_len; + size_t buf_size; + size_t chunk; + size_t off; + size_t blen; + unsigned int sel; + + if (size < 5) + return 0; + if (NULL == get_shared_daemon ()) + return 0; + + sel = data[0]; + buf_size = pp_buf_sizes[data[1] % (sizeof (pp_buf_sizes) + / sizeof (pp_buf_sizes[0]))]; + chunk = chunk_patterns[data[2] % (sizeof (chunk_patterns) + / sizeof (chunk_patterns[0]))]; + blen = data[3]; + + body_len = size - 4; + if (body_len > 8192) + body_len = 8192; + if (blen > body_len) + blen = body_len; + + /* Exactly sized copies so that ASAN catches reads past the end. */ + body = (char *) malloc (body_len + 1); + if (NULL == body) + return 0; + memcpy (body, data + 4, body_len); + body[body_len] = '\0'; + + ctype = (char *) malloc (blen + 64); + if (NULL == ctype) + { + free (body); + return 0; + } + switch (sel % 6) + { + case 0: + memcpy (ctype, MHD_HTTP_POST_ENCODING_FORM_URLENCODED, + sizeof (MHD_HTTP_POST_ENCODING_FORM_URLENCODED)); + break; + case 1: + case 2: + { + /* multipart with a boundary taken from the payload */ + size_t o = 0; + size_t i; + + memcpy (ctype, MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA, + sizeof (MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA) - 1); + o = sizeof (MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA) - 1; + memcpy (ctype + o, "; boundary=", 11); + o += 11; + for (i = 0; i < blen; i++) + { + char ch = body[i]; + + /* A NUL would truncate the header value; map it away. */ + ctype[o++] = ('\0' == ch) ? 'x' : ch; + } + ctype[o] = '\0'; + break; + } + case 3: + (void) snprintf (ctype, blen + 64, + "multipart/form-data; boundary=\"%.*s\"", + (int) ((blen > 40) ? 40 : blen), body); + break; + case 4: + memcpy (ctype, MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA, + sizeof (MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA)); + break; + default: + memcpy (ctype, "text/plain", sizeof ("text/plain")); + break; + } + + memset (&c, 0, sizeof (c)); + memset (&h, 0, sizeof (h)); + h.header = MHD_HTTP_HEADER_CONTENT_TYPE; + h.header_size = MHD_STATICSTR_LEN_ (MHD_HTTP_HEADER_CONTENT_TYPE); + h.value = ctype; + h.value_size = strlen (ctype); + h.kind = MHD_HEADER_KIND; + c.daemon = shared_daemon; + c.rq.headers_received = &h; + c.rq.headers_received_tail = &h; + c.state = MHD_CONNECTION_HEADERS_PROCESSED; + + ctx.data = body; + ctx.data_len = body_len; + ctx.max_off = 0; + + pp = MHD_create_post_processor (&c, buf_size, &post_iter, &ctx); + if (NULL == pp) + { + stat_pp_failed++; + free (ctype); + free (body); + return 0; + } + stat_pp_created++; + + off = 0; + while (off < body_len) + { + size_t n = (0 == chunk) ? (body_len - off) : chunk; + + if (n > body_len - off) + n = body_len - off; + (void) MHD_post_process (pp, body + off, n); + off += n; + } + /* Final, zero-length call: what MHD itself does at the end of the + upload. */ + (void) MHD_post_process (pp, body + body_len, 0); + (void) MHD_destroy_post_processor (pp); + + free (ctype); + free (body); + return 0; +} + + +/* ------------------------------------------------------------------ */ +/* Generator */ +/* ------------------------------------------------------------------ */ + +static const char *const gen_disp[] = { + "Content-Disposition: form-data; name=\"a\"", + "Content-Disposition: form-data; name=\"a\"; filename=\"f.txt\"", + "Content-Disposition: form-data; name=a", + "Content-Disposition: form-data", + "Content-Disposition: attachment; name=\"a\"", + "Content-Disposition: form-data; name=\"\"", + "Content-Disposition: form-data; name=\"a", + "Content-Type: text/plain", + "Content-Transfer-Encoding: binary", + "X-Other: value" +}; + +static const char *const gen_kv[] = { + "a=1", "b=%41", "c", "d=", "=e", "&", "&&", "a=%", "a=%4", "a=%zz", + "verylongkeyname=verylongvaluewithlotsofcharacters", "a+b=c+d", + "%41%42=%43%44" +}; + + +static size_t +fuzz_generate (struct fuzz_rng *rng, + uint8_t *buf, + size_t cap) +{ + size_t len = 0; + int multipart; + const char *boundary; + unsigned int nparts; + unsigned int i; + static const char *const boundaries[] = { + "--abc", "XY", "boundary", "-", "aa", "0123456789012345678901234567890", + "a\"b" + }; + +#define ADD(s) \ + do { \ + const char *s_ = (s); \ + size_t l_ = strlen (s_); \ + if (len + l_ >= cap) \ + return len; \ + memcpy (buf + len, s_, l_); \ + len += l_; \ + } while (0) + + if (cap < 64) + return 0; + multipart = ! fuzz_chance (rng, 3); + boundary = boundaries[fuzz_below (rng, + (uint32_t) (sizeof (boundaries) + / sizeof (char *)))]; + buf[len++] = (uint8_t) (multipart ? (1 + fuzz_below (rng, 2)) : 0); + buf[len++] = fuzz_byte (rng); + buf[len++] = fuzz_byte (rng); + buf[len++] = (uint8_t) strlen (boundary); + if (multipart) + { + /* The first strlen(boundary) bytes of the body double as the + boundary in the Content-Type header (see the input format). */ + ADD (boundary); + nparts = 1 + fuzz_below (rng, 4); + for (i = 0; i < nparts; i++) + { + unsigned int nhdr = fuzz_below (rng, 3); + unsigned int k; + + ADD ("\r\n--"); + ADD (boundary); + ADD ("\r\n"); + for (k = 0; k <= nhdr; k++) + { + ADD (gen_disp[fuzz_below (rng, + (uint32_t) (sizeof (gen_disp) + / sizeof (char *)))]); + ADD ("\r\n"); + } + ADD ("\r\n"); + { + unsigned int n = fuzz_below (rng, 40); + + for (k = 0; (k < n) && (len < cap); k++) + buf[len++] = (uint8_t) ('A' + fuzz_below (rng, 26)); + } + } + ADD ("\r\n--"); + ADD (boundary); + ADD (fuzz_chance (rng, 4) ? "\r\n" : "--\r\n"); + } + else + { + unsigned int n = 1 + fuzz_below (rng, 10); + + for (i = 0; i < n; i++) + { + if (0 != i) + ADD ("&"); + ADD (gen_kv[fuzz_below (rng, + (uint32_t) (sizeof (gen_kv) / sizeof (char *)))]); + } + } +#undef ADD + return len; +} + + +/* ------------------------------------------------------------------ */ +/* Seed corpus */ +/* ------------------------------------------------------------------ */ + +struct pp_seed +{ + const char *txt; + size_t len; +}; + +#define PSEED(t) { t, sizeof (t) - 1 } + +static const struct pp_seed pp_seeds[] = { + PSEED ("\x00\x00\x00\x00" "a=1&b=%41&c"), + PSEED ("\x00\x00\x04\x00" "a=1&b=%41&c"), + PSEED ("\x00\x00\x00\x00" "a=%"), + PSEED ("\x00\x00\x00\x00" "&&&&"), + PSEED ("\x01\x00\x00\x05" "--abc\r\n----abc\r\n" + "Content-Disposition: form-data; name=\"k\"\r\n\r\nvalue\r\n" + "----abc--\r\n"), + PSEED ("\x01\x00\x01\x05" "--abc\r\n----abc\r\n" + "Content-Disposition: form-data; name=\"k\"; filename=\"f\"\r\n" + "Content-Type: text/plain\r\n\r\nvalue\r\n----abc--\r\n"), + PSEED ("\x01\x07\x02\x02" "XY\r\n--XY\r\n\r\nnoheaders\r\n--XY--\r\n"), + PSEED ("\x01\x00\x00\x01" "-\r\n---\r\n\r\nx\r\n-----\r\n"), + PSEED ("\x04\x00\x00\x00" "no boundary at all"), + PSEED ("\x05\x00\x00\x00" "text/plain body"), + PSEED ("\x03\x00\x00\x03" "a\"b\r\n--a\"b\r\n\r\nv\r\n--a\"b--\r\n") +}; + + +static size_t +fuzz_seed_count (void) +{ + return sizeof (pp_seeds) / sizeof (pp_seeds[0]); +} + + +static const uint8_t * +fuzz_seed_get (size_t idx, + size_t *len) +{ + *len = pp_seeds[idx].len; + return (const uint8_t *) pp_seeds[idx].txt; +} diff --git a/src/fuzz/fuzz_request.c b/src/fuzz/fuzz_request.c @@ -0,0 +1,1555 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. + If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * @file fuzz/fuzz_request.c + * @brief End-to-end in-process fuzzer for the MHD request parser. + * @author Christian Grothoff + * + * A real `struct MHD_Daemon` is created with #MHD_USE_NO_LISTEN_SOCKET + * and driven through a `socketpair()` that is handed to MHD with + * #MHD_add_connection(). The daemon runs in external-polling mode, so + * everything happens in the fuzzer's thread: fully deterministic, no + * TCP stack, no ports, no races. + * + * Input format (see README): + * + * byte 0 configuration: connection memory limit + * byte 1 configuration: handler behaviour bitmask + * byte 2 configuration: client discipline / insanity level + * byte 3 configuration: digest-auth parameters + * byte 4.. a sequence of send-segments, each introduced by a + * little-endian 16 bit header (op << 14) | length + * op 0 send the payload + * op 1 the payload is the *expected* decoded request body + * (ground truth for the body oracle); nothing is sent + * op 2 send the payload and pump the daemon extra rounds + * op 3 close the connection, open a fresh one, send + * + * Splitting the byte stream into explicit segments matters: MHD's + * parser is incremental and several past bugs only showed up for + * particular split points. + * + * The literal ASCII token "%%NONCE%%" inside a segment is replaced, + * at send time, by the most recent `nonce="..."` value seen in a + * response from the daemon. This is what allows the fuzzer to walk + * through the Digest-Auth challenge/response handshake and reach the + * code that is only executed for a *valid* nonce. + */ + +#define FUZZ_HARNESS_NAME "fuzz_request" +#include "fuzz_common.h" + +#include <microhttpd.h> +#include <sys/socket.h> +#include <netinet/in.h> + +#define MAX_SEGMENTS 96 +#define MAX_CONNECTIONS 8 +#define RESP_BUF_SIZE 16384 +#define GEN_BUF_SIZE 8192 +#define MAX_EXPECT_BODY 4096 + +#define DIGEST_REALM "TestRealm" +#define DIGEST_USER "user" +#define DIGEST_PASS "pass" + +/* ------------------------------------------------------------------ */ +/* Per-iteration state */ +/* ------------------------------------------------------------------ */ + +struct fuzz_cfg +{ + size_t mem_limit; + int discipline; + unsigned int insanity; + unsigned int nonce_nc_size; + enum MHD_DigestAuthMultiAlgo3 algo; + enum MHD_DigestAuthMultiQOP qop; + int do_digest; + int do_basic; + int do_postproc; + int do_iterate; + int chunked_reply; + int error_reply; +}; + +static struct fuzz_cfg cfg; + +/** Most recent nonce harvested from a 401 response. */ +static char nonce_val[192]; +static size_t nonce_len; + +/** Ground-truth body of a generated (pristine) request, see README. */ +static uint8_t expect_body[MAX_EXPECT_BODY]; +static size_t expect_body_len; +static int oracle_on; + +/** Statistics, printed at exit with --verbose. */ +static unsigned long stat_daemons; +static unsigned long stat_handler_calls; +static unsigned long stat_final_calls; +static unsigned long stat_body_bytes; +static unsigned long stat_challenges; +static unsigned long stat_auth_ok; +static int stats_registered; + + +static void +print_stats (void) +{ + if (! fuzz_verbose) + return; + fprintf (stderr, + "%s: daemons=%lu handler calls=%lu (final=%lu) body bytes=%lu " + "401 challenges=%lu authenticated=%lu\n", + FUZZ_HARNESS_NAME, + stat_daemons, stat_handler_calls, stat_final_calls, + stat_body_bytes, stat_challenges, stat_auth_ok); +} + + +/** Bytes received from the daemon during the current iteration. */ +static char resp_buf[RESP_BUF_SIZE]; +static size_t resp_len; + +static const size_t mem_limit_tbl[] = { + 0 /* MHD default */, 128, 192, 256, 320, 384, 512, 768, 1024, 1400, 1500, + 2048, 4096, 32768 +}; + +static const int discipline_tbl[] = { -3, -2, -1, 0, 1, 2 }; + +/** + * Lower bound for MHD_OPTION_CLIENT_DISCIPLINE_LVL, -3 is the full + * range. On a tree without the patches listed in README section 6, + * levels below 0 reach two stale mhd_assert()s in get_req_header(); + * set MHD_FUZZ_MIN_DISCIPLINE=0 there so that the rest of the state + * space keeps being explored. + */ +static int min_discipline = -3; +static int min_discipline_read; + +/** + * Restrict the generator to a single grammar shape (see enum gen_shape). + * -1 (the default) means "pick a random shape every time". Set with + * MHD_FUZZ_SHAPE=<n>; handy for triage and for regression testing a + * specific past bug. + */ +static int forced_shape = -1; +static int forced_shape_read; + +/** + * Lower bound for MHD_OPTION_CONNECTION_MEMORY_LIMIT. 0 (the default) + * fuzzes the full range, including the 128 byte pools that are needed + * to reach the read-buffer "shift back" code. On a tree without the + * patches listed in README section 6, pools below ~500 bytes combined + * with a chunked request body reach two mhd_assert()s in connection.c; + * set MHD_FUZZ_MIN_MEM_LIMIT=512 there. + */ +static size_t min_mem_limit; +static int min_mem_limit_read; + +static const enum MHD_DigestAuthMultiAlgo3 algo_tbl[] = { + MHD_DIGEST_AUTH_MULT_ALGO3_SHA256, + MHD_DIGEST_AUTH_MULT_ALGO3_MD5, + MHD_DIGEST_AUTH_MULT_ALGO3_SHA512_256, + MHD_DIGEST_AUTH_MULT_ALGO3_SHA256 +}; + +static const unsigned int nnc_tbl[] = { 4, 1, 8, 64 }; + +/** Fixed entropy so that nonces are reproducible across runs. */ +static const char digest_rnd[32] = + "\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67\x89\xab\xcd\xef" + "\xfe\xdc\xba\x98\x76\x54\x32\x10\xfe\xdc\xba\x98\x76\x54\x32\x10"; + + +/* ------------------------------------------------------------------ */ +/* Access handler */ +/* ------------------------------------------------------------------ */ + +struct hstate +{ + size_t body_off; + struct MHD_PostProcessor *pp; + int pp_tried; +}; + + +static enum MHD_Result +kv_iter (void *cls, + enum MHD_ValueKind kind, + const char *key, + const char *value) +{ + volatile size_t sink = 0; + + (void) cls; + (void) kind; + if (NULL != key) + sink += strlen (key); + if (NULL != value) + sink += strlen (value); + (void) sink; + return MHD_YES; +} + + +static enum MHD_Result +post_iter (void *cls, + enum MHD_ValueKind kind, + const char *key, + const char *filename, + const char *content_type, + const char *transfer_encoding, + const char *data, + uint64_t off, + size_t size) +{ + volatile size_t sink = 0; + + (void) cls; + (void) kind; + (void) off; + if (NULL != key) + sink += strlen (key); + if (NULL != filename) + sink += strlen (filename); + if (NULL != content_type) + sink += strlen (content_type); + if (NULL != transfer_encoding) + sink += strlen (transfer_encoding); + if ( (NULL != data) && (0 != size) ) + sink += (size_t) (unsigned char) data[size - 1]; + (void) sink; + return MHD_YES; +} + + +/** + * Check the body MHD hands to the application against the body that + * the generator encoded into the request. MHD is allowed to reject + * the request at any point, but every byte that it *does* deliver must + * be the next expected byte. A mismatch means the framing layer + * (Content-Length or chunked transfer coding) got out of sync, which + * is exactly the class of bug that enables request smuggling. + */ +static void +oracle_check_body (struct hstate *hs, + const char *data, + size_t size) +{ + if ( (! oracle_on) || + (0 == size) ) + return; + if ( (hs->body_off + size > expect_body_len) || + (0 != memcmp (expect_body + hs->body_off, data, size)) ) + fuzz_report_finding ( + "request body desync: the bytes MHD delivered to the application " + "differ from the body encoded in the generated request " + "(chunked/Content-Length framing bug, cf. HTTP request smuggling)"); + hs->body_off += size; +} + + +static enum MHD_Result +ahc (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **req_cls) +{ + struct hstate *hs = (struct hstate *) *req_cls; + struct MHD_Response *resp; + enum MHD_Result ret; + volatile size_t sink = 0; + + (void) cls; + if (NULL == hs) + { + hs = (struct hstate *) calloc (1, sizeof (struct hstate)); + if (NULL == hs) + return MHD_NO; + *req_cls = hs; + return MHD_YES; + } + + stat_handler_calls++; + /* Touch the parsed request line the way a real application would. */ + if (NULL != url) + sink += strlen (url); + if (NULL != method) + sink += strlen (method); + if (NULL != version) + sink += strlen (version); + (void) sink; + + if (0 != *upload_data_size) + { + stat_body_bytes += (unsigned long) *upload_data_size; + oracle_check_body (hs, upload_data, *upload_data_size); + if (cfg.do_postproc) + { + if ( (NULL == hs->pp) && + (! hs->pp_tried) ) + { + hs->pp_tried = 1; + hs->pp = MHD_create_post_processor (connection, + 1024, + &post_iter, + NULL); + } + if (NULL != hs->pp) + (void) MHD_post_process (hs->pp, upload_data, *upload_data_size); + } + *upload_data_size = 0; + return MHD_YES; + } + + stat_final_calls++; + if (cfg.do_iterate) + { + (void) MHD_get_connection_values (connection, + MHD_HEADER_KIND + | MHD_GET_ARGUMENT_KIND + | MHD_COOKIE_KIND + | MHD_FOOTER_KIND, + &kv_iter, + NULL); + } + + if (oracle_on && + (hs->body_off != expect_body_len) ) + fuzz_report_finding ( + "request body truncated: MHD completed the request but delivered " + "fewer body bytes than the generated request contained"); + + if (cfg.do_basic) + { + struct MHD_BasicAuthInfo *bai; + + bai = MHD_basic_auth_get_username_password3 (connection); + if (NULL != bai) + { + if (NULL != bai->username) + sink += bai->username_len; + if (NULL != bai->password) + sink += bai->password_len; + MHD_free (bai); + } + } + + if (cfg.do_digest) + { + enum MHD_DigestAuthResult dres; + + dres = MHD_digest_auth_check3 (connection, + DIGEST_REALM, + DIGEST_USER, + DIGEST_PASS, + 0 /* daemon default nonce timeout */, + 0 /* daemon default max_nc */, + cfg.qop, + MHD_DIGEST_AUTH_MULT_ALGO3_ANY_NON_SESSION); + if (MHD_DAUTH_OK == dres) + stat_auth_ok++; + else + { + stat_challenges++; + resp = MHD_create_response_from_buffer_static (0, ""); + if (NULL == resp) + return MHD_NO; + ret = MHD_queue_auth_required_response3 (connection, + DIGEST_REALM, + "0123456789abcdef" /* opaque */, + "/", + resp, + (MHD_DAUTH_NONCE_STALE == dres) + ? MHD_YES : MHD_NO, + cfg.qop, + cfg.algo, + MHD_YES /* userhash */, + MHD_NO); + MHD_destroy_response (resp); + return ret; + } + } + + if (cfg.chunked_reply) + resp = MHD_create_response_from_buffer_copy (11, "hello world"); + else + resp = MHD_create_response_from_buffer_static (2, "ok"); + if (NULL == resp) + return MHD_NO; + ret = MHD_queue_response (connection, + cfg.error_reply + ? MHD_HTTP_FORBIDDEN : MHD_HTTP_OK, + resp); + MHD_destroy_response (resp); + return ret; +} + + +static void +completed_cb (void *cls, + struct MHD_Connection *connection, + void **req_cls, + enum MHD_RequestTerminationCode toe) +{ + struct hstate *hs = (struct hstate *) *req_cls; + + (void) cls; + (void) connection; + (void) toe; + if (NULL == hs) + return; + if (NULL != hs->pp) + (void) MHD_destroy_post_processor (hs->pp); + free (hs); + *req_cls = NULL; +} + + +static void +panic_cb (void *cls, + const char *file, + unsigned int line, + const char *reason) +{ + char msg[512]; + + (void) cls; + (void) snprintf (msg, sizeof (msg), + "MHD_PANIC() reached from network input at %s:%u: %s", + (NULL != file) ? file : "?", + line, + (NULL != reason) ? reason : "?"); + fuzz_report_finding (msg); +} + + +/* ------------------------------------------------------------------ */ +/* Driving the daemon */ +/* ------------------------------------------------------------------ */ + +/** + * Read whatever the daemon has produced so far and look for a fresh + * Digest-Auth nonce in it. + */ +static void +drain_and_harvest (int sock) +{ + for (;;) + { + ssize_t n; + char tmp[4096]; + + n = recv (sock, tmp, sizeof (tmp), MSG_DONTWAIT); + if (0 >= n) + break; + if (resp_len + (size_t) n < RESP_BUF_SIZE) + { + memcpy (resp_buf + resp_len, tmp, (size_t) n); + resp_len += (size_t) n; + resp_buf[resp_len] = '\0'; + } + } + if (0 != resp_len) + { + const char *p = resp_buf; + + while (NULL != (p = strstr (p, "nonce=\""))) + { + const char *s = p + 7; + const char *e = strchr (s, '"'); + + p = s; + if (NULL == e) + break; + if ((size_t) (e - s) < sizeof (nonce_val)) + { + memcpy (nonce_val, s, (size_t) (e - s)); + nonce_len = (size_t) (e - s); + nonce_val[nonce_len] = '\0'; + } + } + } +} + + +static void +pump (struct MHD_Daemon *d, + int sock, + unsigned int rounds) +{ + unsigned int i; + + for (i = 0; i < rounds; i++) + { + (void) MHD_run (d); + drain_and_harvest (sock); + } +} + + +/** + * Copy @a in to @a out, expanding every occurrence of the ASCII token + * "%%NONCE%%" to the harvested nonce. + * + * @return number of bytes written to @a out + */ +static size_t +expand_nonce (const uint8_t *in, + size_t in_len, + uint8_t *out, + size_t out_cap) +{ + static const char tok[] = "%%NONCE%%"; + const size_t tok_len = sizeof (tok) - 1; + size_t i = 0; + size_t o = 0; + + while (i < in_len) + { + if ( (i + tok_len <= in_len) && + (0 == memcmp (in + i, tok, tok_len)) ) + { + if (o + nonce_len > out_cap) + break; + memcpy (out + o, nonce_val, nonce_len); + o += nonce_len; + i += tok_len; + continue; + } + if (o >= out_cap) + break; + out[o++] = in[i++]; + } + return o; +} + + +static void +send_all (struct MHD_Daemon *d, + int sock, + const uint8_t *data, + size_t len) +{ + size_t off = 0; + unsigned int stall = 0; + + while ( (off < len) && + (stall < 64) ) + { + ssize_t s = send (sock, data + off, len - off, MSG_DONTWAIT); + + if (0 < s) + { + off += (size_t) s; + stall = 0; + continue; + } + stall++; + pump (d, sock, 2); + if ( (0 > s) && + (EAGAIN != errno) && + (EWOULDBLOCK != errno) && + (EINTR != errno) ) + break; + } +} + + +static int +new_connection (struct MHD_Daemon *d, + int *sock) +{ + int sv[2]; + struct sockaddr_in sa; + + if (0 != socketpair (AF_UNIX, SOCK_STREAM, 0, sv)) + return -1; + memset (&sa, 0, sizeof (sa)); + sa.sin_family = AF_INET; + sa.sin_port = htons (44444); + sa.sin_addr.s_addr = htonl (INADDR_LOOPBACK); + if (MHD_YES != MHD_add_connection (d, + (MHD_socket) sv[1], + (const struct sockaddr *) &sa, + (socklen_t) sizeof (sa))) + { + /* MHD has already closed sv[1] in that case */ + (void) close (sv[0]); + return -1; + } + *sock = sv[0]; + return 0; +} + + +static void +close_connection (struct MHD_Daemon *d, + int *sock) +{ + if (0 > *sock) + return; + (void) shutdown (*sock, SHUT_WR); + pump (d, *sock, 4); + (void) close (*sock); + *sock = -1; +} + + +/* ------------------------------------------------------------------ */ +/* The fuzz target */ +/* ------------------------------------------------------------------ */ + +int +LLVMFuzzerTestOneInput (const uint8_t *data, + size_t size) +{ + struct MHD_Daemon *d; + struct MHD_OptionItem opts[8]; + unsigned int nopt = 0; + unsigned int flags; + int sock = -1; + size_t pos; + unsigned int nseg = 0; + unsigned int nconn = 1; + static uint8_t xbuf[FUZZ_MAX_INPUT + 4096]; + + if (size < 6) + return 0; + + nonce_len = 0; + nonce_val[0] = '\0'; + resp_len = 0; + resp_buf[0] = '\0'; + + memset (&cfg, 0, sizeof (cfg)); + cfg.mem_limit = + mem_limit_tbl[data[0] % (sizeof (mem_limit_tbl) + / sizeof (mem_limit_tbl[0]))]; + cfg.do_digest = (0 != (data[1] & 0x01)); + cfg.do_basic = (0 != (data[1] & 0x02)); + cfg.do_postproc = (0 != (data[1] & 0x04)); + cfg.do_iterate = (0 != (data[1] & 0x08)); + cfg.chunked_reply = (0 != (data[1] & 0x20)); + cfg.error_reply = (0 != (data[1] & 0x40)); + if (! min_mem_limit_read) + { + const char *e = getenv ("MHD_FUZZ_MIN_MEM_LIMIT"); + + min_mem_limit_read = 1; + if (NULL != e) + min_mem_limit = (size_t) strtoul (e, NULL, 10); + } + if ( (0 != cfg.mem_limit) && + (cfg.mem_limit < min_mem_limit) ) + cfg.mem_limit = min_mem_limit; + if (! min_discipline_read) + { + const char *e = getenv ("MHD_FUZZ_MIN_DISCIPLINE"); + + min_discipline_read = 1; + if (NULL != e) + min_discipline = atoi (e); + } + cfg.discipline = + discipline_tbl[(data[2] & 0x0F) % (sizeof (discipline_tbl) + / sizeof (discipline_tbl[0]))]; + if (cfg.discipline < min_discipline) + cfg.discipline = min_discipline; + cfg.insanity = (unsigned int) MHD_DSC_SANE; + cfg.algo = algo_tbl[data[3] & 0x03]; + cfg.qop = (0 != (data[3] & 0x04)) + ? MHD_DIGEST_AUTH_MULT_QOP_AUTH + : MHD_DIGEST_AUTH_MULT_QOP_ANY_NON_INT; + cfg.nonce_nc_size = nnc_tbl[(data[3] >> 4) & 0x03]; + + oracle_on = 0; + expect_body_len = 0; + + if (0 != cfg.mem_limit) + { + opts[nopt].option = MHD_OPTION_CONNECTION_MEMORY_LIMIT; + opts[nopt].value = (intptr_t) cfg.mem_limit; + opts[nopt].ptr_value = NULL; + nopt++; + } + opts[nopt].option = MHD_OPTION_CLIENT_DISCIPLINE_LVL; + opts[nopt].value = (intptr_t) cfg.discipline; + opts[nopt].ptr_value = NULL; + nopt++; + opts[nopt].option = MHD_OPTION_SERVER_INSANITY; + opts[nopt].value = (intptr_t) cfg.insanity; + opts[nopt].ptr_value = NULL; + nopt++; + opts[nopt].option = MHD_OPTION_NONCE_NC_SIZE; + opts[nopt].value = (intptr_t) cfg.nonce_nc_size; + opts[nopt].ptr_value = NULL; + nopt++; + opts[nopt].option = MHD_OPTION_DIGEST_AUTH_RANDOM; + opts[nopt].value = (intptr_t) sizeof (digest_rnd); + opts[nopt].ptr_value = (void *) (intptr_t) digest_rnd; + nopt++; + opts[nopt].option = MHD_OPTION_END; + opts[nopt].value = 0; + opts[nopt].ptr_value = NULL; + + flags = MHD_USE_NO_LISTEN_SOCKET; + if (fuzz_verbose) + flags |= MHD_USE_ERROR_LOG; + + MHD_set_panic_func (&panic_cb, NULL); + d = MHD_start_daemon (flags, + 0, + NULL, NULL, + &ahc, NULL, + MHD_OPTION_ARRAY, opts, + /* passed through the varargs rather than through + the option array: storing a function pointer in + the array's intptr_t member is not strictly + conforming C */ + MHD_OPTION_NOTIFY_COMPLETED, &completed_cb, NULL, + MHD_OPTION_END); + if (NULL == d) + return 0; + stat_daemons++; + if (! stats_registered) + { + stats_registered = 1; + (void) atexit (&print_stats); + } + + if (0 != new_connection (d, &sock)) + { + MHD_stop_daemon (d); + return 0; + } + + pos = 4; + while ( (pos + 2 <= size) && + (nseg < MAX_SEGMENTS) ) + { + unsigned int hdr = (unsigned int) data[pos] + | ((unsigned int) data[pos + 1] << 8); + unsigned int op = hdr >> 14; + size_t slen = (size_t) (hdr & 0x3FFF); + size_t elen; + + pos += 2; + nseg++; + if (slen > size - pos) + slen = size - pos; + + if (1 == op) + { + /* Ground-truth declaration, not wire data. */ + if (fuzz_pristine && (slen <= MAX_EXPECT_BODY)) + { + memcpy (expect_body, data + pos, slen); + expect_body_len = slen; + oracle_on = 1; + } + pos += slen; + continue; + } + if ( (3 == op) && + (nconn < MAX_CONNECTIONS) ) + { + close_connection (d, &sock); + if (0 != new_connection (d, &sock)) + break; + nconn++; + } + + elen = expand_nonce (data + pos, slen, xbuf, sizeof (xbuf)); + pos += slen; + if (0 != elen) + send_all (d, sock, xbuf, elen); + pump (d, sock, (2 == op) ? 12u : 3u); + if (0 == slen) + pump (d, sock, 4); + } + + close_connection (d, &sock); + pump (d, sock, 4); + MHD_stop_daemon (d); + return 0; +} + + +/* ------------------------------------------------------------------ */ +/* Structure-aware HTTP request generator */ +/* ------------------------------------------------------------------ */ + +struct sbuf +{ + uint8_t *p; + size_t len; + size_t cap; +}; + + +static void +sb_raw (struct sbuf *b, + const void *v, + size_t n) +{ + if (b->len + n > b->cap) + n = b->cap - b->len; + memcpy (b->p + b->len, v, n); + b->len += n; +} + + +static void +sb_str (struct sbuf *b, + const char *s) +{ + sb_raw (b, s, strlen (s)); +} + + +static void +sb_u64 (struct sbuf *b, + uint64_t v, + int hex, + unsigned int min_digits) +{ + char tmp[32]; + unsigned int n = 0; + + do + { + unsigned int dig = (unsigned int) (v % (hex ? 16u : 10u)); + tmp[n++] = (char) ((dig < 10) ? ('0' + dig) : ('a' + dig - 10)); + v /= (hex ? 16u : 10u); + } + while ( (0 != v) && (n < sizeof (tmp)) ); + while ( (n < min_digits) && (n < sizeof (tmp)) ) + tmp[n++] = '0'; + while (0 != n) + { + char c = tmp[--n]; + sb_raw (b, &c, 1); + } +} + + +static const char *const gen_methods[] = { + "GET", "POST", "PUT", "HEAD", "DELETE", "OPTIONS", "TRACE", "PATCH", + "CONNECT", "BREW", "get", "M-SEARCH", "\tGET", "GET " +}; + +static const char *const gen_versions[] = { + "HTTP/1.1", "HTTP/1.0", "HTTP/1.1", "HTTP/1.1", "HTTP/0.9", "HTTP/1.2", + "HTTP/2.0", "http/1.1", "HTTP/1.", "HTTP/11" +}; + +static const char *const gen_targets[] = { + "/", "/a", "/a/b/c", "/a?x=1", "/a?x=1&y=2", "/?novalue", "/?a=1&b", + "/?a&b&c", "/%41%42", "/a%00b", "/%zz", "/a?%41=%42", "*", + "http://example.org/a", "/a?x=1&novalue", "/..%2f..%2fetc", + "/a?=", "/a?&", "/very/long/path/that/keeps/going/and/going/and/going" +}; + +static const char *const gen_hdr_names[] = { + "Host", "Accept", "User-Agent", "Connection", "Cookie", "Content-Type", + "Expect", "TE", "Trailer", "X-Custom", "Referer", "Accept-Encoding", + "content-length", "transfer-encoding", "X-A-Very-Long-Header-Name-Indeed" +}; + +static const char *const gen_hdr_values[] = { + "example.org", "*/*", "fuzz/1.0", "keep-alive", "close", + "a=1; b=2; c", "100-continue", "trailers", "X-Trail", "value", + "", " ", "\ttabbed", "a, b, c", "chunked", "identity, chunked" +}; + +/** Deliberately includes tokens MHD does not know: bug #1 lives here. */ +static const char *const gen_algos[] = { + "MD5", "SHA-256", "SHA-512-256", "MD5-sess", "SHA-256-sess", + "SHA-512-256-sess", "sha-256", "SHA256", "BOGUS", "", "\"SHA-256\"", + "MD5 ", "SHA-1", "xyzzy", "SHA-512", "\"BOGUS\"", "0" +}; + +static const char *const gen_ctypes[] = { + "application/x-www-form-urlencoded", + "multipart/form-data; boundary=--abc", + "multipart/form-data; boundary=\"XY\"", + "multipart/form-data", + "text/plain" +}; + + +/** + * Append a random query string, optionally ending in an argument + * without '=' (which is the shape needed for the read-buffer shift-back + * bug). + */ +static void +gen_query (struct fuzz_rng *rng, + struct sbuf *b, + int force_trailing_novalue) +{ + unsigned int n = fuzz_below (rng, 4); + unsigned int i; + + if ( (0 == n) && (! force_trailing_novalue) ) + return; + sb_str (b, "?"); + for (i = 0; i < n; i++) + { + if (0 != i) + sb_str (b, "&"); + sb_str (b, "k"); + sb_u64 (b, i, 0, 1); + if (! fuzz_chance (rng, 3)) + { + sb_str (b, "="); + sb_str (b, fuzz_chance (rng, 4) ? "%41%42" : "v"); + } + } + if (force_trailing_novalue) + { + if (0 != n) + sb_str (b, "&"); + sb_str (b, "novalue"); + } +} + + +static void +gen_headers (struct fuzz_rng *rng, + struct sbuf *b, + unsigned int n) +{ + unsigned int i; + + for (i = 0; i < n; i++) + { + sb_str (b, gen_hdr_names[fuzz_below (rng, + (uint32_t) (sizeof (gen_hdr_names) + / sizeof (char *)))]); + sb_str (b, fuzz_chance (rng, 8) ? " :" : ":"); + if (! fuzz_chance (rng, 6)) + sb_str (b, " "); + sb_str (b, gen_hdr_values[fuzz_below (rng, + (uint32_t) (sizeof (gen_hdr_values) + / sizeof (char *)))]); + if (fuzz_chance (rng, 12)) + sb_str (b, "\r\n\tfolded-continuation"); + if (fuzz_chance (rng, 20)) + sb_str (b, "\n"); /* bare LF */ + else if (fuzz_chance (rng, 25)) + sb_str (b, "\r"); /* bare CR */ + else + sb_str (b, "\r\n"); + } +} + + +/** + * Emit a chunked body. When @a oracle is non-zero the body is + * strictly RFC 9112 conformant and the decoded payload is recorded in + * #expect_body, so that the harness can verify what MHD hands to the + * application. Chunk extensions are emitted frequently on purpose. + */ +static void +gen_chunked_body (struct fuzz_rng *rng, + struct sbuf *b, + int oracle) +{ + unsigned int nchunks = 1 + fuzz_below (rng, 4); + unsigned int i; + + if (oracle) + expect_body_len = 0; + for (i = 0; i < nchunks; i++) + { + unsigned int clen = 1 + fuzz_below (rng, 24); + unsigned int j; + + sb_u64 (b, clen, 1, fuzz_chance (rng, 4) ? 4 : 1); + /* chunk extension -- the parsing of the terminating CRLF of this + very line was broken (bug #3) */ + if (! fuzz_chance (rng, 2)) + { + switch (fuzz_below (rng, 5)) + { + case 0: + sb_str (b, ";ext"); + break; + case 1: + sb_str (b, ";ext=val"); + break; + case 2: + sb_str (b, ";ext=\"quoted value\""); + break; + case 3: + sb_str (b, ";a=1;b=2;c"); + break; + default: + sb_str (b, ";x=\"a;b\""); + break; + } + } + sb_str (b, "\r\n"); + for (j = 0; j < clen; j++) + { + char c = (char) ('A' + ((i * 7 + j) % 26)); + + sb_raw (b, &c, 1); + if (oracle && (expect_body_len < MAX_EXPECT_BODY)) + expect_body[expect_body_len++] = (uint8_t) c; + } + sb_str (b, "\r\n"); + } + sb_str (b, "0"); + if (fuzz_chance (rng, 4)) + sb_str (b, ";final=\"x\""); + sb_str (b, "\r\n"); + if (fuzz_chance (rng, 3)) + sb_str (b, "X-Trailer: value\r\n"); + sb_str (b, "\r\n"); +} + + +static void +gen_digest_header (struct fuzz_rng *rng, + struct sbuf *b, + const char *uri, + int use_nonce_token, + int overlong_response, + int prefer_valid) +{ + unsigned int nresp; + unsigned int i; + + sb_str (b, "Authorization: Digest "); + sb_str (b, "username=\""); + if (fuzz_chance (rng, prefer_valid ? 20 : 6)) + { + /* userhash notation: a long hex string is accepted here too */ + unsigned int n = 2 * (16 + fuzz_below (rng, 48)); + for (i = 0; i < n; i++) + sb_str (b, "a"); + } + else + sb_str (b, DIGEST_USER); + sb_str (b, "\", "); + if (fuzz_chance (rng, prefer_valid ? 25 : 8)) + sb_str (b, "userhash=true, "); + sb_str (b, "realm=\""); + sb_str (b, fuzz_chance (rng, prefer_valid ? 25 : 8) + ? "OtherRealm" : DIGEST_REALM); + sb_str (b, "\", nonce=\""); + if (use_nonce_token) + sb_str (b, "%%NONCE%%"); + else + { + unsigned int n = 8 + fuzz_below (rng, 80); + for (i = 0; i < n; i++) + sb_str (b, "0"); + } + sb_str (b, "\", uri=\""); + sb_str (b, uri); + sb_str (b, "\", qop="); + sb_str (b, fuzz_chance (rng, prefer_valid ? 25 : 8) ? "auth-int" : "auth"); + sb_str (b, ", nc="); + sb_u64 (b, 1 + fuzz_below (rng, 3), 1, 8); + sb_str (b, ", cnonce=\"deadbeef\", algorithm="); + if (prefer_valid && (! fuzz_chance (rng, 5))) + sb_str (b, "SHA-256"); /* must match the algorithm of the challenge */ + else + sb_str (b, gen_algos[fuzz_below (rng, + (uint32_t) (sizeof (gen_algos) + / sizeof (char *)))]); + sb_str (b, ", opaque=\"0123456789abcdef\", response=\""); + if (overlong_response) + nresp = 66 + 2 * fuzz_below (rng, 32); /* 66 .. 128 hex digits */ + else + nresp = 2 * (1 + fuzz_below (rng, 66)); + for (i = 0; i < nresp; i++) + { + char c = "0123456789abcdef"[fuzz_below (rng, 16)]; + + sb_raw (b, &c, 1); + } + sb_str (b, "\"\r\n"); +} + + +enum gen_shape +{ + SHAPE_PLAIN = 0, + SHAPE_NOHDR_QARG, + SHAPE_CL_BODY, + SHAPE_CHUNKED, + SHAPE_DIGEST_SIMPLE, + SHAPE_DIGEST_REPLAY, + SHAPE_BASIC, + SHAPE_POST_FORM, + SHAPE_WEIRD, + SHAPE_COUNT +}; + + +/** + * Build one HTTP request into @a b. + * + * @return non-zero if the request is exactly reproducible, i.e. the + * #expect_body oracle may be used + */ +static int +gen_one_request (struct fuzz_rng *rng, + struct sbuf *b, + enum gen_shape shape, + int second_of_pair) +{ + switch (shape) + { + case SHAPE_NOHDR_QARG: + /* No header lines at all + a trailing query argument without '='; + combined with a small connection memory pool this is the shape + that reaches the read-buffer "shift back" computation. */ + sb_str (b, "GET /"); + gen_query (rng, b, 1); + sb_str (b, " "); + sb_str (b, fuzz_chance (rng, 2) ? "HTTP/1.0" : "HTTP/1.1"); + sb_str (b, "\r\n\r\n"); + return 0; + + case SHAPE_CL_BODY: + { + unsigned int blen = fuzz_below (rng, 64); + unsigned int i; + + sb_str (b, "POST /a HTTP/1.1\r\nHost: x\r\nContent-Length: "); + sb_u64 (b, blen, 0, 1); + sb_str (b, "\r\n\r\n"); + expect_body_len = 0; + for (i = 0; i < blen; i++) + { + char c = (char) ('a' + (i % 26)); + + sb_raw (b, &c, 1); + if (expect_body_len < MAX_EXPECT_BODY) + expect_body[expect_body_len++] = (uint8_t) c; + } + return 1; + } + + case SHAPE_CHUNKED: + sb_str (b, "POST /a HTTP/1.1\r\nHost: x\r\n" + "Transfer-Encoding: chunked\r\n\r\n"); + gen_chunked_body (rng, b, 1); + return 1; + + case SHAPE_DIGEST_SIMPLE: + sb_str (b, "GET /a HTTP/1.1\r\nHost: x\r\n"); + gen_digest_header (rng, b, "/a", 0, fuzz_chance (rng, 2), 0); + sb_str (b, "\r\n"); + return 0; + + case SHAPE_DIGEST_REPLAY: + if (! second_of_pair) + sb_str (b, "GET /a HTTP/1.1\r\nHost: x\r\n\r\n"); + else + { + sb_str (b, "GET /a HTTP/1.1\r\nHost: x\r\n"); + gen_digest_header (rng, b, "/a", 1, 1, 1); + sb_str (b, "\r\n"); + } + return 0; + + case SHAPE_BASIC: + sb_str (b, "GET /a HTTP/1.1\r\nHost: x\r\nAuthorization: Basic "); + { + unsigned int n = fuzz_below (rng, 40); + unsigned int i; + + for (i = 0; i < n; i++) + { + char c = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" + [fuzz_below (rng, 65)]; + + sb_raw (b, &c, 1); + } + } + sb_str (b, "\r\n\r\n"); + return 0; + + case SHAPE_POST_FORM: + { + const char *ct = gen_ctypes[fuzz_below (rng, + (uint32_t) (sizeof (gen_ctypes) + / sizeof (char *)))]; + + sb_str (b, "POST /a HTTP/1.1\r\nHost: x\r\nContent-Type: "); + sb_str (b, ct); + sb_str (b, "\r\nTransfer-Encoding: chunked\r\n\r\n"); + gen_chunked_body (rng, b, 0); + return 0; + } + + case SHAPE_WEIRD: + sb_str (b, gen_methods[fuzz_below (rng, + (uint32_t) (sizeof (gen_methods) + / sizeof (char *)))]); + sb_str (b, " "); + sb_str (b, gen_targets[fuzz_below (rng, + (uint32_t) (sizeof (gen_targets) + / sizeof (char *)))]); + sb_str (b, " "); + sb_str (b, gen_versions[fuzz_below (rng, + (uint32_t) (sizeof (gen_versions) + / sizeof (char *)))]); + sb_str (b, fuzz_chance (rng, 8) ? "\n" : "\r\n"); + gen_headers (rng, b, fuzz_below (rng, 6)); + sb_str (b, "\r\n"); + return 0; + + case SHAPE_PLAIN: + case SHAPE_COUNT: + default: + sb_str (b, gen_methods[fuzz_below (rng, 3)]); + sb_str (b, " /"); + gen_query (rng, b, fuzz_chance (rng, 3)); + sb_str (b, " HTTP/1.1\r\n"); + gen_headers (rng, b, fuzz_below (rng, 4)); + sb_str (b, "\r\n"); + return 0; + } +} + + +/** + * Serialise @a body into the segment format understood by + * LLVMFuzzerTestOneInput(), starting at @a out[*out_len]. + */ +static void +emit_segments (struct fuzz_rng *rng, + struct sbuf *out, + const uint8_t *body, + size_t body_len, + int new_conn_first) +{ + size_t off = 0; + int first = 1; + + while (off < body_len) + { + size_t chunk; + unsigned int op; + uint8_t hdr[2]; + unsigned int hv; + + switch (fuzz_below (rng, 6)) + { + case 0: + chunk = 1; + break; + case 1: + chunk = 2 + fuzz_below (rng, 6); + break; + case 2: + chunk = 8 + fuzz_below (rng, 40); + break; + default: + chunk = body_len - off; + break; + } + if (chunk > body_len - off) + chunk = body_len - off; + if (chunk > 0x3FFF) + chunk = 0x3FFF; + op = (first && new_conn_first) ? 3u : (fuzz_chance (rng, 4) ? 2u : 0u); + hv = (op << 14) | (unsigned int) chunk; + hdr[0] = (uint8_t) (hv & 0xFF); + hdr[1] = (uint8_t) (hv >> 8); + if (out->len + 2 + chunk > out->cap) + return; + sb_raw (out, hdr, 2); + sb_raw (out, body + off, chunk); + off += chunk; + first = 0; + } +} + + +static size_t +fuzz_generate (struct fuzz_rng *rng, + uint8_t *buf, + size_t cap) +{ + struct sbuf out; + uint8_t req[GEN_BUF_SIZE]; + struct sbuf rb; + enum gen_shape shape; + uint8_t cfg_bytes[4]; + int oracle; + unsigned int nreq; + unsigned int i; + + expect_body_len = 0; + + out.p = buf; + out.len = 0; + out.cap = cap; + + if (! forced_shape_read) + { + const char *e = getenv ("MHD_FUZZ_SHAPE"); + + forced_shape_read = 1; + if (NULL != e) + forced_shape = atoi (e); + } + if (0 <= forced_shape) + shape = (enum gen_shape) (forced_shape % (int) SHAPE_COUNT); + else + shape = (enum gen_shape) fuzz_below (rng, (uint32_t) SHAPE_COUNT); + + /* --- configuration bytes --- */ + cfg_bytes[0] = fuzz_byte (rng); + cfg_bytes[1] = fuzz_byte (rng); + cfg_bytes[2] = fuzz_byte (rng); + cfg_bytes[3] = fuzz_byte (rng); + switch (shape) + { + case SHAPE_NOHDR_QARG: + /* small connection memory pool: indices 1..9 of mem_limit_tbl */ + cfg_bytes[0] = (uint8_t) (1 + fuzz_below (rng, 9)); + break; + case SHAPE_DIGEST_SIMPLE: + case SHAPE_DIGEST_REPLAY: + cfg_bytes[0] = (uint8_t) (fuzz_chance (rng, 2) ? 0 : 12); + cfg_bytes[1] = (uint8_t) ((cfg_bytes[1] | 0x01u) & ~0x04u); + /* SHA-256 challenge: 32 byte digest -> 128 hex chars pass the + 'response' length check while hash1_bin[] is only 32 bytes */ + cfg_bytes[3] = (uint8_t) ((cfg_bytes[3] & 0xF0u) | 0x00u); + break; + case SHAPE_POST_FORM: + cfg_bytes[1] |= 0x04u; + break; + case SHAPE_CHUNKED: + case SHAPE_CL_BODY: + cfg_bytes[1] &= (uint8_t) ~0x04u; /* keep the body oracle clean */ + break; + default: + break; + } + cfg_bytes[1] |= 0x08u; /* always iterate the values */ + sb_raw (&out, cfg_bytes, 4); + + nreq = (SHAPE_DIGEST_REPLAY == shape) ? 2u : (1u + (fuzz_chance (rng, 6) + ? 1u : 0u)); + oracle = 0; + for (i = 0; i < nreq; i++) + { + rb.p = req; + rb.len = 0; + rb.cap = sizeof (req); + oracle = gen_one_request (rng, &rb, shape, (int) i); + if (oracle && (1 == nreq) && (expect_body_len <= MAX_EXPECT_BODY)) + { + /* Declare the ground truth for the body oracle (op 1). */ + unsigned int hv = (1u << 14) | (unsigned int) expect_body_len; + uint8_t hdr[2]; + + hdr[0] = (uint8_t) (hv & 0xFF); + hdr[1] = (uint8_t) (hv >> 8); + sb_raw (&out, hdr, 2); + sb_raw (&out, expect_body, expect_body_len); + } + emit_segments (rng, &out, req, rb.len, + (0 != i) || fuzz_chance (rng, 8)); + } + (void) oracle; + return out.len; +} + + +/* ------------------------------------------------------------------ */ +/* Built-in seed corpus */ +/* ------------------------------------------------------------------ */ + +/** + * The built-in seed corpus is described symbolically and rendered into + * the wire format at run time, so that segment lengths never have to be + * spelled out by hand. + */ +struct seed_part +{ + unsigned int op; /**< 0 send, 1 expected body, 3 new conn */ + const char *txt; /**< NUL terminated payload */ +}; + +struct seed_def +{ + const char *name; + unsigned char cfg[4]; + struct seed_part parts[4]; +}; + +#define P_END { 0, NULL } + +static const struct seed_def seeds[] = { + { "plain", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END } }, + + { "content-length-body", { 0x00, 0x08, 0x03, 0x00 }, + { { 1, "hello" }, + { 0, "POST /a HTTP/1.1\r\nHost: x\r\nContent-Length: 5\r\n\r\nhello" }, + P_END, P_END } }, + + /* chunk extensions: the terminating CRLF of the chunk-size line used + to be left in the stream (bug #3, commit c13f4c64) */ + { "chunked-with-extensions", { 0x00, 0x08, 0x03, 0x00 }, + { { 1, "ABCDE" }, + { 0, "POST /a HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\n" + "\r\n2;ext=val\r\nAB\r\n3;a=\"b;c\"\r\nCDE\r\n0\r\n\r\n" }, + P_END, P_END } }, + + { "chunked-split", { 0x00, 0x08, 0x03, 0x00 }, + { { 1, "ABCDEF" }, + { 0, "POST /a HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\n" + "\r\n3\r\nABC\r\n3;x=\"y\"" }, + { 2, "\r\nDEF\r\n0\r\n\r\n" }, + P_END } }, + + /* small connection memory pool + trailing query argument without '=' + and no header lines at all (bug #2, commit 29eaa56b) */ + { "small-pool-trailing-query-arg", { 0x06, 0x08, 0x03, 0x00 }, + { { 0, "GET /?novalue HTTP/1.0\r\n\r\n" }, P_END, P_END, P_END } }, + + { "small-pool-trailing-query-arg-2", { 0x03, 0x08, 0x03, 0x00 }, + { { 0, "GET /?a=1&b HTTP/1.0\r\n\r\n" }, P_END, P_END, P_END } }, + + /* unrecognised 'algorithm' token -> MHD_DIGEST_AUTH_ALGO3_INVALID + (bug #1, commit bd49ce93) */ + { "digest-unknown-algorithm", { 0x00, 0x09, 0x03, 0x00 }, + { { 0, "GET /a HTTP/1.1\r\nHost: x\r\nAuthorization: Digest " + "username=\"user\", realm=\"TestRealm\", nonce=\"0000\", " + "uri=\"/a\", algorithm=BOGUS, response=\"00\"\r\n\r\n" }, + P_END, P_END, P_END } }, + + /* full digest handshake: harvest the nonce from the 401 and replay it + with a 128 hex digit 'response' value (bug #4, commit 5a73c1ae) */ + { "digest-overlong-response", { 0x00, 0x09, 0x03, 0x00 }, + { { 0, "GET /a HTTP/1.1\r\nHost: x\r\n\r\n" }, + { 3, "GET /a HTTP/1.1\r\nHost: x\r\nAuthorization: Digest " + "username=\"user\", realm=\"TestRealm\", nonce=\"%%NONCE%%\", " + "uri=\"/a\", qop=auth, nc=00000001, cnonce=\"deadbeef\", " + "algorithm=SHA-256, response=\"" + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + "\"\r\n\r\n" }, + P_END, P_END } }, + + { "digest-userhash", { 0x00, 0x09, 0x03, 0x00 }, + { { 0, "GET /a HTTP/1.1\r\nHost: x\r\n\r\n" }, + { 3, "GET /a HTTP/1.1\r\nHost: x\r\nAuthorization: Digest " + "username=\"" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "\", userhash=true, realm=\"TestRealm\", nonce=\"%%NONCE%%\", " + "uri=\"/a\", qop=auth, nc=00000001, cnonce=\"deadbeef\", " + "algorithm=SHA-256, response=\"0123456789abcdef\"\r\n\r\n" }, + P_END, P_END } }, + + { "basic-auth", { 0x00, 0x0a, 0x03, 0x00 }, + { { 0, "GET /a HTTP/1.1\r\nHost: x\r\n" + "Authorization: Basic dXNlcjpwYXNz\r\n\r\n" }, + P_END, P_END, P_END } }, + + { "multipart-post", { 0x00, 0x0c, 0x03, 0x00 }, + { { 0, "POST /a HTTP/1.1\r\nHost: x\r\n" + "Content-Type: multipart/form-data; boundary=--abc\r\n" + "Transfer-Encoding: chunked\r\n\r\n" + "52\r\n----abc\r\nContent-Disposition: form-data; name=\"k\"\r\n\r\n" + "value\r\n----abc--\r\n\r\n0\r\n\r\n" }, + P_END, P_END, P_END } }, + + { "urlencoded-post", { 0x00, 0x0c, 0x03, 0x00 }, + { { 1, "a=1&b=%41&c" }, + { 0, "POST /a HTTP/1.1\r\nHost: x\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-Length: 11\r\n\r\na=1&b=%41&c" }, + P_END, P_END } }, + + { "folded-header", { 0x00, 0x08, 0x00, 0x00 }, + { { 0, "GET /a HTTP/1.1\r\nHost: x\r\nX-Fold: a\r\n\tb\r\n\r\n" }, + P_END, P_END, P_END } }, + + { "pipelined", { 0x00, 0x08, 0x03, 0x00 }, + { { 0, "GET /a HTTP/1.1\r\nHost: x\r\n\r\n" + "GET /b?q HTTP/1.1\r\nHost: x\r\n\r\n" }, + P_END, P_END, P_END } } +}; + +static uint8_t seed_render_buf[4096]; + + +static size_t +fuzz_seed_count (void) +{ + return sizeof (seeds) / sizeof (seeds[0]); +} + + +static const uint8_t * +fuzz_seed_get (size_t idx, + size_t *len) +{ + const struct seed_def *sd = &seeds[idx]; + struct sbuf b; + unsigned int i; + + b.p = seed_render_buf; + b.len = 0; + b.cap = sizeof (seed_render_buf); + sb_raw (&b, sd->cfg, 4); + for (i = 0; i < sizeof (sd->parts) / sizeof (sd->parts[0]); i++) + { + size_t n; + unsigned int hv; + uint8_t hdr[2]; + + if (NULL == sd->parts[i].txt) + break; + n = strlen (sd->parts[i].txt); + if (n > 0x3FFF) + n = 0x3FFF; + hv = (sd->parts[i].op << 14) | (unsigned int) n; + hdr[0] = (uint8_t) (hv & 0xFF); + hdr[1] = (uint8_t) (hv >> 8); + sb_raw (&b, hdr, 2); + sb_raw (&b, sd->parts[i].txt, n); + } + *len = b.len; + return seed_render_buf; +} diff --git a/src/fuzz/fuzz_str.c b/src/fuzz/fuzz_str.c @@ -0,0 +1,544 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. + If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * @file fuzz/fuzz_str.c + * @brief Direct in-process fuzzer for the string primitives of mhd_str.c + * @author Christian Grothoff + * + * Every destination buffer is obtained with malloc() at *exactly* the + * size that the documented contract of the function under test + * requires -- never a generous stack array. Under AddressSanitizer the + * redzone immediately after the allocation turns any one-byte + * over-write into a hard error, which is precisely the property a + * fixed-size stack buffer would not give us. + * + * In addition to the contract-exact calls, several functions are also + * called with a deliberately *too small* output buffer: functions that + * take a buffer size argument must detect this and must not write past + * the end. + * + * Input format: + * byte 0 target selector + * byte 1 auxiliary parameter (buffer size shrink, split point, ...) + * byte 2.. payload + */ + +#define FUZZ_HARNESS_NAME "fuzz_str" +#include "fuzz_common.h" + +#include "mhd_options.h" +#include "mhd_str.h" + +/** + * Largest digest MHD supports; this is the size of the on-stack buffers + * that digestauth.c passes to MHD_hex_to_bin(). + */ +#define MODEL_MAX_DIGEST 32 + +/** + * Model the (pre-2026) digestauth.c call site that fed a client + * controlled, up to 4 * digest_size characters long 'response' value + * into MHD_hex_to_bin() together with a digest_size byte buffer. + * + * MHD_hex_to_bin() has no output size parameter: it writes len/2 bytes, + * so a caller with a fixed size buffer *must* bound the input length. + * This target replays that call site with an exactly sized heap buffer + * so that ASAN reports the overflow. It is OFF by default (it models a + * caller, not the library) -- enable it with MHD_FUZZ_MODEL_DIGEST_SINK=1 + * to reproduce the primitive behind the digest 'response' overflow. + */ +static int model_digest_sink; +static int model_digest_sink_read; + +enum str_target +{ + TGT_HEX_TO_BIN = 0, + TGT_BIN_TO_HEX, + TGT_BIN_TO_HEX_Z, + TGT_PCT_STRICT, + TGT_PCT_LENIENT, + TGT_PCT_IN_PLACE_STRICT, + TGT_PCT_IN_PLACE_LENIENT, + TGT_UNQUOTE, + TGT_QUOTE, + TGT_BASE64, + TGT_TO_UINT64, + TGT_TOKENS, + TGT_EQUAL_CASELESS, + TGT_DIGEST_HEX_SINK, + TGT_COUNT +}; + + +/** + * Allocate exactly @a n bytes. Zero-sized allocations are turned into + * a one byte allocation so that the returned pointer stays valid, but + * the harness never tells the library about that extra byte. + */ +static void * +xalloc (size_t n) +{ + void *p = malloc ((0 == n) ? 1 : n); + + if (NULL == p) + abort (); + return p; +} + + +static char * +dup_z (const uint8_t *d, + size_t n) +{ + char *p = (char *) xalloc (n + 1); + + memcpy (p, d, n); + p[n] = '\0'; + return p; +} + + +int +LLVMFuzzerTestOneInput (const uint8_t *data, + size_t size) +{ + enum str_target tgt; + const uint8_t *p; + size_t n; + unsigned int aux; + + if (size < 3) + return 0; + tgt = (enum str_target) (data[0] % (unsigned int) TGT_COUNT); + aux = data[1]; + p = data + 2; + n = size - 2; + /* Keep the inputs small: these are unit-level primitives and short + inputs explore the interesting corner cases far more efficiently. */ + if (n > 512) + n = 512; + + switch (tgt) + { + case TGT_HEX_TO_BIN: + { + /* Contract: the output buffer must be len/2 bytes long, or + len/2 + 1 if len is odd. */ + size_t need = (n / 2) + (n % 2); + uint8_t *out = (uint8_t *) xalloc (need); + size_t r = MHD_hex_to_bin ((const char *) p, n, out); + + if (r > need) + fuzz_report_finding ("MHD_hex_to_bin() reported more bytes written " + "than its documented output size"); + free (out); + break; + } + + case TGT_BIN_TO_HEX: + { + char *out = (char *) xalloc (2 * n); + size_t r = MHD_bin_to_hex (p, n, out); + + if (r != 2 * n) + fuzz_report_finding ("MHD_bin_to_hex() did not write 2 * size chars"); + free (out); + break; + } + + case TGT_BIN_TO_HEX_Z: + { + char *out = (char *) xalloc (2 * n + 1); + size_t r = MHD_bin_to_hex_z (p, n, out); + + if (r != 2 * n) + fuzz_report_finding ("MHD_bin_to_hex_z() did not write 2 * size chars"); + if ('\0' != out[r]) + fuzz_report_finding ("MHD_bin_to_hex_z() result is not " + "zero-terminated"); + free (out); + break; + } + + case TGT_PCT_STRICT: + { + /* First with the exact maximum size, then with a buffer that is + deliberately too small. */ + char *out = (char *) xalloc (n); + size_t r = MHD_str_pct_decode_strict_n_ ((const char *) p, n, out, n); + size_t small; + + if (r > n) + fuzz_report_finding ("MHD_str_pct_decode_strict_n_() overran the " + "documented output size"); + free (out); + small = (0 == n) ? 0 : (n * (aux % 100u)) / 100u; + out = (char *) xalloc (small); + (void) MHD_str_pct_decode_strict_n_ ((const char *) p, n, out, small); + free (out); + break; + } + + case TGT_PCT_LENIENT: + { + char *out = (char *) xalloc (n); + bool broken = false; + size_t r = + MHD_str_pct_decode_lenient_n_ ((const char *) p, n, out, n, &broken); + size_t small; + + if (r > n) + fuzz_report_finding ("MHD_str_pct_decode_lenient_n_() overran the " + "documented output size"); + free (out); + small = (0 == n) ? 0 : (n * (aux % 100u)) / 100u; + out = (char *) xalloc (small); + (void) MHD_str_pct_decode_lenient_n_ ((const char *) p, n, out, small, + NULL); + free (out); + break; + } + + case TGT_PCT_IN_PLACE_STRICT: + { + char *s = dup_z (p, n); + size_t r = MHD_str_pct_decode_in_place_strict_ (s); + + if (r > n) + fuzz_report_finding ("MHD_str_pct_decode_in_place_strict_() grew " + "the string"); + free (s); + break; + } + + case TGT_PCT_IN_PLACE_LENIENT: + { + char *s = dup_z (p, n); + bool broken = false; + size_t r = MHD_str_pct_decode_in_place_lenient_ (s, &broken); + + if (r > n) + fuzz_report_finding ("MHD_str_pct_decode_in_place_lenient_() grew " + "the string"); + free (s); + break; + } + +#ifdef DAUTH_SUPPORT + case TGT_UNQUOTE: + { + /* Unquoting never grows the string. */ + char *out = (char *) xalloc (n); + size_t r = MHD_str_unquote ((const char *) p, n, out); + + if (r > n) + fuzz_report_finding ("MHD_str_unquote() wrote more characters than " + "the quoted input had"); + free (out); + break; + } +#else /* ! DAUTH_SUPPORT */ + case TGT_UNQUOTE: + break; +#endif /* ! DAUTH_SUPPORT */ + +#if defined(DAUTH_SUPPORT) || defined(BAUTH_SUPPORT) + case TGT_QUOTE: + { + /* Quoting can at most double the size. */ + char *out = (char *) xalloc (2 * n); + size_t r = MHD_str_quote ((const char *) p, n, out, 2 * n); + size_t small; + + if (r > 2 * n) + fuzz_report_finding ("MHD_str_quote() wrote more than 2 * len chars"); + free (out); + small = (0 == n) ? 0 : (n * (aux % 200u)) / 100u; + out = (char *) xalloc (small); + (void) MHD_str_quote ((const char *) p, n, out, small); + free (out); + break; + } +#else /* ! (DAUTH_SUPPORT || BAUTH_SUPPORT) */ + case TGT_QUOTE: + break; +#endif /* ! (DAUTH_SUPPORT || BAUTH_SUPPORT) */ + +#ifdef BAUTH_SUPPORT + case TGT_BASE64: + { + size_t need = MHD_base64_max_dec_size_ (n); + uint8_t *out = (uint8_t *) xalloc (need); + size_t r = MHD_base64_to_bin_n ((const char *) p, n, out, need); + size_t small; + + if (r > need) + fuzz_report_finding ("MHD_base64_to_bin_n() exceeded " + "MHD_base64_max_dec_size_()"); + free (out); + small = (0 == need) ? 0 : (need * (aux % 100u)) / 100u; + out = (uint8_t *) xalloc (small); + (void) MHD_base64_to_bin_n ((const char *) p, n, out, small); + free (out); + break; + } +#else /* ! BAUTH_SUPPORT */ + case TGT_BASE64: + break; +#endif /* ! BAUTH_SUPPORT */ + + case TGT_TO_UINT64: + { + char *s = dup_z (p, n); + uint64_t v1 = 0; + uint64_t v2 = 0; + size_t r1 = MHD_str_to_uint64_n_ (s, n, &v1); + size_t r2 = MHD_strx_to_uint64_n_ (s, n, &v2); + + if (r1 > n) + fuzz_report_finding ("MHD_str_to_uint64_n_() consumed more than " + "maxlen characters"); + if (r2 > n) + fuzz_report_finding ("MHD_strx_to_uint64_n_() consumed more than " + "maxlen characters"); + free (s); + break; + } + + case TGT_TOKENS: + { + /* The "token" arguments have documented preconditions (no NUL, + space, tab or comma); a fuzzer that violates them would only + find its own mistakes, so sanitise them here. */ + char *s = dup_z (p, n); + size_t split = (0 == n) ? 0 : (aux % n); + char *tok = dup_z (p + split, n - split); + size_t tlen; + ssize_t bs; + char *out; + size_t k; + + for (k = 0; k < n - split; k++) + { + if ( ('\0' == tok[k]) || (' ' == tok[k]) || + ('\t' == tok[k]) || (',' == tok[k]) ) + tok[k] = 'x'; + } + tlen = strlen (tok); + if (0 == tlen) + { + free (tok); + tok = dup_z ((const uint8_t *) "chunked", 7); + tlen = 7; + } + (void) MHD_str_has_token_caseless_ (s, tok, tlen); + + /* Documented worst case growth of the output is 50%. */ + bs = (ssize_t) (n + n / 2 + 1); + out = (char *) xalloc ((size_t) bs); + (void) MHD_str_remove_token_caseless_ (s, n, tok, tlen, out, &bs); + if (bs > (ssize_t) (n + n / 2 + 1)) + fuzz_report_finding ("MHD_str_remove_token_caseless_() reported a " + "result larger than the documented 50% growth"); + if (0 <= bs) + { + /* The output of the previous call is normalised, which is the + documented precondition of the in-place variant. */ + char *norm = dup_z ((const uint8_t *) out, (size_t) bs); + size_t nlen = (size_t) bs; + + (void) MHD_str_remove_tokens_caseless_ (norm, &nlen, tok, tlen); + if (nlen > (size_t) bs) + fuzz_report_finding ("MHD_str_remove_tokens_caseless_() grew the " + "string"); + free (norm); + } + free (out); + free (tok); + free (s); + break; + } + + case TGT_EQUAL_CASELESS: + { + size_t split = (0 == n) ? 0 : (aux % n); + char *a = dup_z (p, split); + char *b = dup_z (p + split, n - split); + + (void) MHD_str_equal_caseless_bin_n_ (a, b, (split < n - split) + ? split : (n - split)); + (void) MHD_str_equal_caseless_n_ (a, b, n); + free (a); + free (b); + break; + } + + case TGT_DIGEST_HEX_SINK: + { + /* See the comment on model_digest_sink above. */ + size_t len = n; + uint8_t *out; + + if (! model_digest_sink_read) + { + const char *e = getenv ("MHD_FUZZ_MODEL_DIGEST_SINK"); + + model_digest_sink_read = 1; + if (NULL != e) + model_digest_sink = (0 != atoi (e)); + } + if (! model_digest_sink) + break; + /* digestauth.c only checked 'len <= 4 * digest_size' before the + fix; replay exactly that bound. */ + if (len > 4 * MODEL_MAX_DIGEST) + len = 4 * MODEL_MAX_DIGEST; + out = (uint8_t *) xalloc (MODEL_MAX_DIGEST); + (void) MHD_hex_to_bin ((const char *) p, len, out); + free (out); + break; + } + + case TGT_COUNT: + default: + break; + } + return 0; +} + + +/* ------------------------------------------------------------------ */ +/* Generator */ +/* ------------------------------------------------------------------ */ + +static const char *const gen_str_atoms[] = { + "%41", "%", "%%", "%zz", "%0", "%00", "%ff", "\\", "\\\"", "\"", + "0123456789abcdef", "0123456789ABCDEF", "ffffffffffffffffffffffff", + "gg", "0x", "18446744073709551615", "99999999999999999999", + "a, b, c", "chunked", "identity", " , ", ",,", "token", + "QUJD", "QQ==", "Q===", "====", "AAAA", "AA=A", + "\x00\x01\x7f\x80\xff", " ", "\t", "\r\n" +}; + + +static size_t +fuzz_generate (struct fuzz_rng *rng, + uint8_t *buf, + size_t cap) +{ + size_t len = 0; + unsigned int natoms; + unsigned int i; + + if (cap < 8) + return 0; + buf[len++] = (uint8_t) fuzz_below (rng, (uint32_t) TGT_COUNT); + buf[len++] = fuzz_byte (rng); + natoms = 1 + fuzz_below (rng, 12); + for (i = 0; i < natoms; i++) + { + if (fuzz_chance (rng, 3)) + { + /* a run of hex digits of a length that is interesting for the + digest code paths (32, 64, 128 characters) */ + unsigned int k; + unsigned int run = 1 + fuzz_below (rng, 140); + + for (k = 0; (k < run) && (len < cap); k++) + buf[len++] = (uint8_t) "0123456789abcdef"[fuzz_below (rng, 16)]; + } + else if (fuzz_chance (rng, 8)) + { + if (len < cap) + buf[len++] = fuzz_byte (rng); + } + else + { + const char *a = + gen_str_atoms[fuzz_below (rng, + (uint32_t) (sizeof (gen_str_atoms) + / sizeof (char *)))]; + size_t al = strlen (a); + + if (len + al > cap) + break; + memcpy (buf + len, a, al); + len += al; + } + } + return len; +} + + +/* ------------------------------------------------------------------ */ +/* Seed corpus */ +/* ------------------------------------------------------------------ */ + +struct str_seed +{ + const char *txt; + size_t len; +}; + +#define SSEED(t) { t, sizeof (t) - 1 } + +static const struct str_seed str_seeds[] = { + /* hex -> bin, 128 characters: the length digestauth.c used to allow + into a 32 byte buffer */ + SSEED ("\x00\x00" + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"), + SSEED ("\x00\x00" "0123456789abcdef0123456789abcdef"), + SSEED ("\x00\x00" "abc"), + SSEED ("\x00\x00" "zz"), + SSEED ("\x01\x00" "\x01\x02\x03\x04"), + SSEED ("\x02\x00" "\xff\xfe"), + SSEED ("\x03\x00" "/a%41%42%zz%"), + SSEED ("\x04\x40" "/a%41%42%zz%"), + SSEED ("\x05\x00" "%41%42%%%0"), + SSEED ("\x06\x00" "%41%42%%%0"), + SSEED ("\x07\x00" "a\\\"b\\\\c"), + SSEED ("\x08\x00" "a\"b\\c"), + SSEED ("\x09\x00" "QUJDRA=="), + SSEED ("\x09\x00" "QUJDR==="), + SSEED ("\x0a\x00" "18446744073709551615"), + SSEED ("\x0a\x00" "ffffffffffffffff"), + SSEED ("\x0b\x03" "chunked, identity, chunked"), + SSEED ("\x0c\x04" "CHUNKEDchunked"), + SSEED ("\x0d\x00" + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") +}; + + +static size_t +fuzz_seed_count (void) +{ + return sizeof (str_seeds) / sizeof (str_seeds[0]); +} + + +static const uint8_t * +fuzz_seed_get (size_t idx, + size_t *len) +{ + *len = str_seeds[idx].len; + return (const uint8_t *) str_seeds[idx].txt; +} diff --git a/src/microhttpd/Makefile.am b/src/microhttpd/Makefile.am @@ -16,6 +16,11 @@ lib_LTLIBRARIES = \ noinst_DATA = MOSTLYCLEANFILES = +# Test-only header, #included by every test program but not compiled on its +# own and not part of the library; see mhd_panic_tripwire.h. +EXTRA_DIST = \ + mhd_panic_tripwire.h + AM_V_LIB = $(am__v_LIB_@AM_V@) am__v_LIB_ = $(am__v_LIB_@AM_DEFAULT_V@) am__v_LIB_0 = @echo " LIB " $@; @@ -87,6 +92,7 @@ libmicrohttpd_la_SOURCES = \ mhd_str.c mhd_str.h mhd_str_types.h\ mhd_send.h mhd_send.c \ mhd_assert.h \ + mhd_check.h \ mhd_sockets.c mhd_sockets.h \ mhd_itc.c mhd_itc.h mhd_itc_types.h \ mhd_compat.c mhd_compat.h \ @@ -240,7 +246,11 @@ check_PROGRAMS = \ test_client_put_chunked_steps_hard_close \ test_options \ test_mhd_version \ - test_set_panic + test_set_panic \ + test_rq_shift_back \ + test_chunked_ext \ + test_raw_requests \ + test_option_matrix if ENABLE_MD5 check_PROGRAMS += \ @@ -302,7 +312,8 @@ if ENABLE_DAUTH check_PROGRAMS += \ test_str_quote \ test_dauth_userdigest \ - test_dauth_userhash + test_dauth_userhash \ + test_dauth_malformed endif if ENABLE_BAUTH check_PROGRAMS += \ @@ -580,6 +591,42 @@ test_options_SOURCES = \ test_options_LDADD = \ $(builddir)/libmicrohttpd.la +# The environment-driven daemon option matrix (see mhd_opt_matrix.h and +# contrib/run-option-matrix.sh). It uses the public API only, so it is +# simply compiled into every test that honours the matrix. +MHD_OPT_MATRIX_SOURCES = \ + mhd_opt_matrix.c mhd_opt_matrix.h + +test_rq_shift_back_SOURCES = \ + test_rq_shift_back.c mhd_sockets.h mhd_assert.h \ + $(MHD_OPT_MATRIX_SOURCES) +test_rq_shift_back_LDADD = \ + $(builddir)/libmicrohttpd.la + +test_chunked_ext_SOURCES = \ + test_chunked_ext.c mhd_sockets.h mhd_assert.h \ + $(MHD_OPT_MATRIX_SOURCES) +test_chunked_ext_LDADD = \ + $(builddir)/libmicrohttpd.la + +test_raw_requests_SOURCES = \ + test_raw_requests.c mhd_sockets.h mhd_assert.h \ + $(MHD_OPT_MATRIX_SOURCES) +test_raw_requests_LDADD = \ + $(builddir)/libmicrohttpd.la + +test_option_matrix_SOURCES = \ + test_option_matrix.c mhd_sockets.h mhd_assert.h \ + $(MHD_OPT_MATRIX_SOURCES) +test_option_matrix_LDADD = \ + $(builddir)/libmicrohttpd.la + +test_dauth_malformed_SOURCES = \ + test_dauth_malformed.c mhd_sockets.h mhd_assert.h \ + $(MHD_OPT_MATRIX_SOURCES) +test_dauth_malformed_LDADD = \ + $(builddir)/libmicrohttpd.la + test_client_put_shutdown_SOURCES = \ test_client_put_stop.c test_client_put_shutdown_LDADD = \ diff --git a/src/microhttpd/connection.c b/src/microhttpd/connection.c @@ -55,6 +55,7 @@ #endif /* HAVE_SYS_PARAM_H */ #include "mhd_send.h" #include "mhd_assert.h" +#include "mhd_check.h" /** * Get whether bare LF in HTTP header and other protocol elements @@ -1736,6 +1737,19 @@ try_ready_chunked_body (struct MHD_Connection *connection, sizeof(chunk_hdr)); mhd_assert (chunk_hdr_len != 0); mhd_assert (chunk_hdr_len <= sizeof(chunk_hdr)); + /* This underflow would turn 'write_buffer_send_offset' into a huge value and + make the following memcpy() write outside the write buffer. */ + if (MHD_CHECK_FAILED_ ((chunk_hdr_len + 2) <= max_chunk_hdr_len)) + { + MHD_CHECK_LOG_ (connection->daemon, + "(chunk_hdr_len + 2) <= max_chunk_hdr_len"); +#if defined(MHD_USE_THREADS) + MHD_mutex_unlock_chk_ (&response->mutex); +#endif /* MHD_USE_THREADS */ + connection_close_error (connection, + NULL); + return MHD_NO; + } *p_finished = false; connection->write_buffer_send_offset = (max_chunk_hdr_len - (chunk_hdr_len + 2)); @@ -5693,10 +5707,23 @@ send_redirect_fixed_rq_target (struct MHD_Connection *c) do { const char chr = c->rq.hdrs.rq_line.rq_tgt[i++]; + /* The number of bytes that this iteration appends to the buffer. */ + const size_t add_size = + ((' ' == chr) || ('\t' == chr) || (0x0B == chr) || (0x0C == chr)) ? 3 : 1; mhd_assert ('\r' != chr); /* Replaced during request line parsing */ mhd_assert ('\n' != chr); /* Rejected during request line parsing */ mhd_assert (0 != chr); /* Rejected during request line parsing */ + /* Check the remaining space before write. */ + if (MHD_CHECK_FAILED_ (add_size <= fixed_uri_len - o)) + { + MHD_CHECK_LOG_ (c->daemon, + "add_size <= fixed_uri_len - o"); + free (b); + connection_close_error (c, + NULL); + return; + } switch (chr) { case ' ': @@ -6696,8 +6723,15 @@ get_req_headers (struct MHD_Connection *c, bool process_footers) { last_elmnt_end = c->rq.version + HTTP_VER_LEN; } - mhd_assert (NULL != last_elmnt_end); - mhd_assert ((last_elmnt_end + 1) < c->read_buffer); + /* Check that @a last_elmnt_end points into the request that has + just been parsed, which lives entirely + between the start of the request line and the current read buffer + position. */ + MHD_CHECK_CONN_CLOSE_RET_ (c, + (NULL != last_elmnt_end) && + (c->rq.method <= last_elmnt_end) && + (last_elmnt_end < c->read_buffer - 1), + true); shift_back_size = (size_t) (c->read_buffer - (last_elmnt_end + 1)); if (0 != c->read_buffer_offset) memmove (c->read_buffer - shift_back_size, diff --git a/src/microhttpd/digestauth.c b/src/microhttpd/digestauth.c @@ -48,6 +48,7 @@ #include "mhd_compat.h" #include "mhd_bithelpers.h" #include "mhd_assert.h" +#include "mhd_check.h" /** @@ -2802,7 +2803,12 @@ digest_auth_check_all_inner (struct MHD_Connection *connection, if (digest_ext_error (da)) return MHD_DAUTH_ERROR; #endif /* MHD_DIGEST_HAS_EXT_ERROR */ - mhd_assert (sizeof (tmp1) >= (2 * digest_size)); + /* MHD_bin_to_hex() + takes no output size and writes exactly 2 * digest_size bytes into + this fixed-size stack buffer */ + MHD_CHECK_ (connection->daemon, + (2 * digest_size) <= sizeof (tmp1), + return MHD_DAUTH_ERROR); MHD_bin_to_hex (hash1_bin, digest_size, tmp1); if (! is_param_equal_caseless (&params->username, tmp1, 2 * digest_size)) return MHD_DAUTH_WRONG_USERNAME; @@ -2968,7 +2974,9 @@ digest_auth_check_all_inner (struct MHD_Connection *connection, mhd_assert (! da->hashing); digest_reset (da); /* Update digest with H(A1) */ - mhd_assert (sizeof (tmp1) >= (digest_size * 2)); + MHD_CHECK_ (connection->daemon, + (digest_size * 2) <= sizeof (tmp1), + return MHD_DAUTH_ERROR); if (NULL == userdigest) MHD_bin_to_hex (hash1_bin, digest_size, tmp1); else diff --git a/src/microhttpd/md5_ext.c b/src/microhttpd/md5_ext.c @@ -86,6 +86,8 @@ MHD_MD5_finish_reset (struct Md5CtxExt *ctx, { if (0 == ctx->ext_error) gnutls_hash_output (ctx->handle, digest); + else + gnutls_hash_deinit (ctx->handle, NULL); } diff --git a/src/microhttpd/memorypool.c b/src/microhttpd/memorypool.c @@ -31,6 +31,7 @@ #include <string.h> #include <stdint.h> #include "mhd_assert.h" +#include "mhd_check.h" #ifdef HAVE_SYS_MMAN_H #include <sys/mman.h> #endif @@ -552,9 +553,23 @@ MHD_pool_reallocate (struct MemoryPool *pool, const size_t old_offset = mp_ptr_diff_ (old, pool->memory); const bool shrinking = (old_size > new_size); + /* 'old' and 'old_size' come from the caller and bound both the + memset() that shrinking performs a few lines below and the + memcpy (new_blc, old, old_size) at the end of this function, so this + is the precondition of two unbounded copies and must hold in release + builds too. Failing it returns NULL, aka + "the pool cannot satisfy this request" and which leaves + 'old' valid, so the connection is failed by the caller instead of + corrupting the pool. + The lower bound stays an assertion because mp_ptr_le_() only exists in + debug builds when user-poisoning is active; it is subsumed anyway, + because mp_ptr_diff_() wraps for a pointer below pool->memory and the + resulting huge 'old_offset' fails the bound below. */ mhd_assert (mp_ptr_le_ (pool->memory, old)); /* (pool->memory + pool->size >= (uint8_t*) old + old_size) */ - mhd_assert ((pool->size - _MHD_RED_ZONE_SIZE) >= (old_offset + old_size)); + MHD_CHECK_RET_ ((pool->size - _MHD_RED_ZONE_SIZE) >= + (old_offset + old_size), + NULL); /* Blocks "from the end" must not be reallocated */ /* (old_size == 0 || pool->memory + pool->pos > (uint8_t*) old) */ mhd_assert ((old_size == 0) || \ diff --git a/src/microhttpd/mhd_check.h b/src/microhttpd/mhd_check.h @@ -0,0 +1,202 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. + If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * @file microhttpd/mhd_check.h + * @brief macros for MHD_CHECK_(), always-compiled memory-safety invariants + * @author Christian Grothoff + * + * mhd_assert() is compiled out in release builds (NDEBUG), which is the right + * thing for the several hundred internal state assertions of this library but + * the wrong thing for the handful of them that stand between attacker-supplied + * data and a memmove()/memcpy()/array index. For those, an assertion that is + * absent in the shipped build is not a safety net at all. + * + * MHD_CHECK_() is the always-compiled companion for exactly that subset. It + * does NOT depend on NDEBUG, _DEBUG or --enable-asserts: the test is one + * predictable, well-predicted branch that is present in every build. + * + * RULES FOR USING IT + * ------------------ + * 1. Only invariants whose violation would mean memory-unsafe behaviour - + * pointer/offset arithmetic, buffer bounds, length subtractions that could + * underflow, indices into fixed-size arrays. Ordinary state assertions + * stay mhd_assert(). + * 2. Check the *precondition*, not the symptom. An assertion that compares + * two pointers after the bad one has already been derived is decoration: + * it happily passes on a NULL-derived pointer. Assert what has to be true + * for the following arithmetic to be defined. See commit 29eaa56b. + * 3. A failing check must FAIL THE CONNECTION - close it, or reply 4xx/5xx - + * and must not continue and must not panic. MHD_PANIC() aborts the whole + * daemon, which turns a bounded per-connection problem into a global + * denial of service; that is the opposite of what this is for. + * 4. Deliberately no mhd_assert() inside: a check must behave identically in + * debug and in release builds, otherwise the debug build stops exercising + * the recovery path that the release build relies on. + * + * WHERE THE MACROS COME FROM + * -------------------------- + * The connection-level macros below expand to the failure exits that + * connection.c already uses - connection_close_error() / + * CONNECTION_CLOSE_ERROR() and transmit_error_response_static() - rather than + * introducing a parallel mechanism. They are therefore usable only from + * connection.c and only after those helpers have been defined; this header + * intentionally does not include connection.c's internals, the macros are + * expanded at the point of use. + * + * Translation units that have a `struct MHD_Daemon *` at hand (connection.c, + * digestauth.c, postprocessor.c, ...) get a log line through the usual + * MHD_DLOG()/HAVE_MESSAGES machinery; they must include "internal.h" before + * this header. Translation units without a daemon (memorypool.c, mhd_str.c) + * use the MHD_CHECK_RET_() form, which is silent. + */ + +#ifndef MHD_CHECK_H +#define MHD_CHECK_H 1 + +#include "mhd_options.h" + +/** + * Branch hint: an invariant violation is by construction the unlikely case. + */ +#if defined(__GNUC__) || defined(__clang__) +# define MHD_CHECK_FAILED_(expr) (__builtin_expect (! (expr), 0)) +#else /* ! __GNUC__ */ +# define MHD_CHECK_FAILED_(expr) (! (expr)) +#endif /* ! __GNUC__ */ + +/** + * Report a violated invariant through the daemon's log. + * + * @param daemon the daemon to log to + * @param expr_str the stringified invariant that was violated + */ +#ifdef HAVE_MESSAGES +# define MHD_CHECK_LOG_(daemon,expr_str) \ + MHD_DLOG (daemon, \ + _ ("Internal invariant violated at %s:%u: (%s). " \ + "Failing the connection.\n"), \ + __FILE__, \ + (unsigned int) __LINE__, \ + expr_str) +#else /* ! HAVE_MESSAGES */ +# define MHD_CHECK_LOG_(daemon,expr_str) ((void) 0) +#endif /* ! HAVE_MESSAGES */ + + +/** + * The generic always-compiled invariant check. + * + * @param daemon the daemon to log to + * @param expr the invariant, must evaluate to true + * @param fail_stmt a single statement to execute when @a expr is false; it + * must transfer control out of the current function + * (return, goto or break), because execution must not + * continue past a violated memory-safety invariant. + * When the failure action needs several statements, spell + * the check out instead - "if (MHD_CHECK_FAILED_ (expr)) { + * MHD_CHECK_LOG_ (daemon, "expr"); ... }" - because a + * braced block passed as a macro argument is re-indented + * by contrib/uncrustify.cfg + */ +#define MHD_CHECK_(daemon,expr,fail_stmt) \ + do { \ + if (MHD_CHECK_FAILED_ (expr)) \ + { \ + MHD_CHECK_LOG_ (daemon, #expr); \ + fail_stmt; \ + } \ + } while (0) + + +/** + * Always-compiled invariant check for translation units that have no daemon + * pointer available for logging (memorypool.c, mhd_str.c). Silent; the + * caller is expected to turn the returned failure value into a log message + * and a failed connection. + * + * @param expr the invariant, must evaluate to true + * @param retval the value to return when @a expr is false + */ +#define MHD_CHECK_RET_(expr,retval) \ + do { \ + if (MHD_CHECK_FAILED_ (expr)) \ + return retval; \ + } while (0) + + +/** + * Always-compiled invariant check that closes the connection with an error + * and returns @a retval. For use in connection.c only, and only after + * connection_close_error() has been defined. + * + * @param c the connection to fail + * @param expr the invariant, must evaluate to true + * @param retval the value to return when @a expr is false + */ +#define MHD_CHECK_CONN_CLOSE_RET_(c,expr,retval) \ + MHD_CHECK_ ((c)->daemon, \ + expr, \ + { \ + connection_close_error ((c), \ + NULL); \ + return retval; \ + }) + + +/** + * Always-compiled invariant check that closes the connection with an error + * and returns from a void function. For use in connection.c only, and only + * after connection_close_error() has been defined. + * + * @param c the connection to fail + * @param expr the invariant, must evaluate to true + */ +#define MHD_CHECK_CONN_CLOSE_RET_VOID_(c,expr) \ + MHD_CHECK_ ((c)->daemon, \ + expr, \ + { \ + connection_close_error ((c), \ + NULL); \ + return; \ + }) + + +/** + * Always-compiled invariant check that queues a static error reply for the + * connection and returns @a retval. For use in connection.c only, and only + * after transmit_error_response_static() has been defined. + * + * @param c the connection to fail + * @param expr the invariant, must evaluate to true + * @param code the HTTP status code to reply with + * @param msg the static message to reply with + * @param retval the value to return when @a expr is false + */ +#define MHD_CHECK_CONN_REPLY_RET_(c,expr,code,msg,retval) \ + MHD_CHECK_ ((c)->daemon, \ + expr, \ + { \ + transmit_error_response_static ((c), \ + (code), \ + msg); \ + return retval; \ + }) + +#endif /* ! MHD_CHECK_H */ diff --git a/src/microhttpd/mhd_opt_matrix.c b/src/microhttpd/mhd_opt_matrix.c @@ -0,0 +1,532 @@ +/* + This file is part of GNU libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + GNU libmicrohttpd is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with GNU libmicrohttpd. + If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * @file microhttpd/mhd_opt_matrix.c + * @brief The environment-driven daemon option matrix shared by the tests + * @author Christian Grothoff + */ + +#include "platform.h" +#include <microhttpd.h> +#include <stdlib.h> +#include <string.h> +#include <stdio.h> + +#include "mhd_opt_matrix.h" + + +/** + * The built-in matrix of daemon option profiles. + * + * Every value of every dimension listed in the header appears at least once: + * + * * memory limit: 0 (default), 64, 128, 256, 512, 1024, 2048, 3072, 4096; + * * client discipline level: -3, -2, -1, 0, 1, 2, 3; + * * #MHD_OPTION_STRICT_FOR_CLIENT with a positive and with a negative value; + * * threading: external polling, one internal thread, thread-per-connection, + * thread pool; + * * polling backend: select(), poll(), epoll. + * + * Profile zero is "neutral": it is exactly the configuration the tests of + * this directory used before the matrix was introduced. + * + * The combination "thread-per-connection + epoll" is deliberately absent: MHD + * rejects it (daemon.c:8680). External polling is always paired with the + * select()-style interface, as that is the only one the tests can drive. + */ +static const struct MHD_OptMatrixProfile opt_profiles[] = { + /* name mem discp legacy threading + poll pool */ + { "default", 0, 0, 0, MHD_OPT_MATRIX_THR_INTERNAL, + MHD_OPT_MATRIX_POLL_SELECT, 0 }, + { "mem-64", 64, 0, 0, MHD_OPT_MATRIX_THR_INTERNAL, + MHD_OPT_MATRIX_POLL_SELECT, 0 }, + { "mem-128", 128, 0, 0, MHD_OPT_MATRIX_THR_INTERNAL, + MHD_OPT_MATRIX_POLL_POLL, 0 }, + { "mem-256", 256, 0, 0, MHD_OPT_MATRIX_THR_PER_CONNECTION, + MHD_OPT_MATRIX_POLL_SELECT, 0 }, + { "mem-512", 512, 0, 0, MHD_OPT_MATRIX_THR_PER_CONNECTION, + MHD_OPT_MATRIX_POLL_POLL, 0 }, + { "mem-1024", 1024, 0, 0, MHD_OPT_MATRIX_THR_INTERNAL, + MHD_OPT_MATRIX_POLL_EPOLL, 0 }, + { "mem-2048", 2048, 0, 0, MHD_OPT_MATRIX_THR_POOL, + MHD_OPT_MATRIX_POLL_SELECT, 4 }, + { "mem-3072", 3072, 0, 0, MHD_OPT_MATRIX_THR_EXTERNAL, + MHD_OPT_MATRIX_POLL_SELECT, 0 }, + { "mem-4096", 4096, 0, 0, MHD_OPT_MATRIX_THR_POOL, + MHD_OPT_MATRIX_POLL_EPOLL, 3 }, + { "strict-1", 0, 1, 0, MHD_OPT_MATRIX_THR_INTERNAL, + MHD_OPT_MATRIX_POLL_SELECT, 0 }, + { "strict-2", 1024, 2, 0, MHD_OPT_MATRIX_THR_INTERNAL, + MHD_OPT_MATRIX_POLL_POLL, 0 }, + { "strict-3", 512, 3, 0, MHD_OPT_MATRIX_THR_INTERNAL, + MHD_OPT_MATRIX_POLL_EPOLL, 0 }, + { "lax-1", 512, -1, 0, MHD_OPT_MATRIX_THR_INTERNAL, + MHD_OPT_MATRIX_POLL_SELECT, 0 }, + { "lax-2", 256, -2, 0, MHD_OPT_MATRIX_THR_PER_CONNECTION, + MHD_OPT_MATRIX_POLL_SELECT, 0 }, + { "lax-3", 2048, -3, 0, MHD_OPT_MATRIX_THR_POOL, + MHD_OPT_MATRIX_POLL_SELECT, 4 }, + { "legacy-strict", 768, 1, 1, MHD_OPT_MATRIX_THR_INTERNAL, + MHD_OPT_MATRIX_POLL_SELECT, 0 }, + { "legacy-lax", 768, -1, 1, MHD_OPT_MATRIX_THR_EXTERNAL, + MHD_OPT_MATRIX_POLL_SELECT, 0 } +}; + +/** + * The number of built-in profiles. + */ +#define NUM_PROFILES \ + ((unsigned int) (sizeof(opt_profiles) / sizeof(opt_profiles[0]))) + + +/** + * Non-zero once the environment has been read into @a env_profile. + */ +static int env_parsed; + +/** + * Non-zero if the environment selects a profile at all. + */ +static int env_selected; + +/** + * The profile derived from the environment. + */ +static struct MHD_OptMatrixProfile env_profile; + +/** + * The name of @a env_profile. + */ +static char env_profile_name[64]; + + +unsigned int +mhd_opt_matrix_num_profiles (void) +{ + return NUM_PROFILES; +} + + +const struct MHD_OptMatrixProfile * +mhd_opt_matrix_profile (unsigned int idx) +{ + return opt_profiles + (idx % NUM_PROFILES); +} + + +const struct MHD_OptMatrixProfile * +mhd_opt_matrix_lookup (const char *name_or_idx) +{ + unsigned int i; + + if ((NULL == name_or_idx) || (0 == name_or_idx[0])) + return NULL; + for (i = 0; i < NUM_PROFILES; ++i) + { + if (0 == strcmp (name_or_idx, opt_profiles[i].name)) + return opt_profiles + i; + } + if (('0' <= name_or_idx[0]) && ('9' >= name_or_idx[0])) + { + char *end; + unsigned long v; + + v = strtoul (name_or_idx, &end, 10); + if ((NULL != end) && (0 == *end) && (NUM_PROFILES > v)) + return opt_profiles + v; + } + return NULL; +} + + +/** + * Parse the threading mode name. + * + * @param s the name + * @param[out] out the parsed mode + * @return non-zero on success + */ +static int +parse_threading (const char *s, enum MHD_OptMatrixThreading *out) +{ + if (0 == strcmp (s, "external")) + *out = MHD_OPT_MATRIX_THR_EXTERNAL; + else if (0 == strcmp (s, "internal")) + *out = MHD_OPT_MATRIX_THR_INTERNAL; + else if ((0 == strcmp (s, "per-connection")) || + (0 == strcmp (s, "tpc"))) + *out = MHD_OPT_MATRIX_THR_PER_CONNECTION; + else if (0 == strcmp (s, "pool")) + *out = MHD_OPT_MATRIX_THR_POOL; + else + return 0; + return ! 0; +} + + +/** + * Parse the polling backend name. + * + * @param s the name + * @param[out] out the parsed backend + * @return non-zero on success + */ +static int +parse_poll (const char *s, enum MHD_OptMatrixPoll *out) +{ + if (0 == strcmp (s, "select")) + *out = MHD_OPT_MATRIX_POLL_SELECT; + else if (0 == strcmp (s, "poll")) + *out = MHD_OPT_MATRIX_POLL_POLL; + else if (0 == strcmp (s, "epoll")) + *out = MHD_OPT_MATRIX_POLL_EPOLL; + else + return 0; + return ! 0; +} + + +/** + * Report a broken environment variable and terminate: a typo in a driver + * script must not silently produce a run with the default configuration. + * + * @param name the name of the variable + * @param value the offending value + */ +_MHD_NORETURN static void +env_error (const char *name, const char *value) +{ + fprintf (stderr, + "Invalid value '%s' for the environment variable %s.\n", + (NULL != value) ? value : "(null)", + name); + fflush (stderr); + exit (99); +} + + +const struct MHD_OptMatrixProfile * +mhd_opt_matrix_from_env (void) +{ + const char *prof_env; + const char *mem_env; + const char *discp_env; + const char *strict_env; + const char *thr_env; + const char *poll_env; + + if (0 != env_parsed) + return env_selected ? &env_profile : NULL; + env_parsed = ! 0; + + prof_env = getenv (MHD_OPT_MATRIX_PROFILE_ENV); + mem_env = getenv (MHD_OPT_MATRIX_MEM_ENV); + discp_env = getenv (MHD_OPT_MATRIX_DISCP_ENV); + strict_env = getenv (MHD_OPT_MATRIX_STRICT_ENV); + thr_env = getenv (MHD_OPT_MATRIX_THREADING_ENV); + poll_env = getenv (MHD_OPT_MATRIX_POLL_ENV); + if ((NULL == prof_env) && (NULL == mem_env) && (NULL == discp_env) && + (NULL == strict_env) && (NULL == thr_env) && (NULL == poll_env)) + return NULL; /* Nothing selected: keep the previous behaviour */ + + if (NULL != prof_env) + { + const struct MHD_OptMatrixProfile *base; + + base = mhd_opt_matrix_lookup (prof_env); + if (NULL == base) + env_error (MHD_OPT_MATRIX_PROFILE_ENV, prof_env); + env_profile = *base; + } + else + env_profile = opt_profiles[0]; + strncpy (env_profile_name, env_profile.name, sizeof(env_profile_name) - 1); + env_profile_name[sizeof(env_profile_name) - 1] = 0; + env_profile.name = env_profile_name; + + /* The explicit per-dimension overrides below are NOT clamped: they are an + explicit request by the caller, see the header. */ + if (NULL != mem_env) + { + if (0 == strcmp (mem_env, "default")) + env_profile.mem_limit = 0; + else + { + char *end; + unsigned long v; + + v = strtoul (mem_env, &end, 10); + if ((NULL == end) || (0 != *end)) + env_error (MHD_OPT_MATRIX_MEM_ENV, mem_env); + env_profile.mem_limit = (size_t) v; + } + } + if (NULL != discp_env) + { + char *end; + long v; + + v = strtol (discp_env, &end, 10); + if ((NULL == end) || (0 != *end) || (-3 > v) || (3 < v)) + env_error (MHD_OPT_MATRIX_DISCP_ENV, discp_env); + env_profile.discipline_lvl = (int) v; + env_profile.use_legacy_strict = 0; + } + if (NULL != strict_env) + { + char *end; + long v; + + v = strtol (strict_env, &end, 10); + if ((NULL == end) || (0 != *end)) + env_error (MHD_OPT_MATRIX_STRICT_ENV, strict_env); + env_profile.discipline_lvl = (int) v; + env_profile.use_legacy_strict = ! 0; + } + if (NULL != thr_env) + { + if (! parse_threading (thr_env, &env_profile.threading)) + env_error (MHD_OPT_MATRIX_THREADING_ENV, thr_env); + if ((MHD_OPT_MATRIX_THR_POOL == env_profile.threading) && + (2 > env_profile.thread_pool_size)) + env_profile.thread_pool_size = 4; + } + if (NULL != poll_env) + { + if (! parse_poll (poll_env, &env_profile.poll_backend)) + env_error (MHD_OPT_MATRIX_POLL_ENV, poll_env); + } + env_selected = ! 0; + return &env_profile; +} + + +int +mhd_opt_matrix_effective_discipline (const struct MHD_OptMatrixProfile *prof) +{ + if (NULL == prof) + return 0; + if (! prof->use_legacy_strict) + return prof->discipline_lvl; + /* Mirror the mapping of daemon.c:7102-7106 exactly. */ + if (-1 >= prof->discipline_lvl) + return -3; + if (1 <= prof->discipline_lvl) + return 1; + return 0; +} + + +int +mhd_opt_matrix_profile_supported (const struct MHD_OptMatrixProfile *prof) +{ + if (NULL == prof) + return ! 0; + if ((MHD_OPT_MATRIX_THR_EXTERNAL != prof->threading) && + (MHD_YES != MHD_is_feature_supported (MHD_FEATURE_THREADS))) + return 0; + if (MHD_OPT_MATRIX_THR_EXTERNAL == prof->threading) + { + /* Only the select()-style external interface is driven by the tests. */ + return (MHD_OPT_MATRIX_POLL_SELECT == prof->poll_backend) ? ! 0 : 0; + } + if ((MHD_OPT_MATRIX_POLL_POLL == prof->poll_backend) && + (MHD_YES != MHD_is_feature_supported (MHD_FEATURE_POLL))) + return 0; + if (MHD_OPT_MATRIX_POLL_EPOLL == prof->poll_backend) + { + if (MHD_YES != MHD_is_feature_supported (MHD_FEATURE_EPOLL)) + return 0; + /* MHD refuses to combine 'epoll' with a thread per connection. */ + if (MHD_OPT_MATRIX_THR_PER_CONNECTION == prof->threading) + return 0; + } + return ! 0; +} + + +const char * +mhd_opt_matrix_describe (const struct MHD_OptMatrixProfile *prof, + char *buf, + size_t buf_size) +{ + static const char *const thr_names[] = { + "external polling", "internal thread", "thread-per-connection", + "thread pool" + }; + static const char *const poll_names[] = { "select()", "poll()", "epoll" }; + char mem[32]; + char pool[32]; + char discp[64]; + + if (NULL == prof) + { + snprintf (buf, buf_size, "(built-in defaults of the test)"); + return buf; + } + if (0 == prof->mem_limit) + snprintf (mem, sizeof(mem), "default"); + else + snprintf (mem, sizeof(mem), "%lu", (unsigned long) prof->mem_limit); + if (MHD_OPT_MATRIX_THR_POOL == prof->threading) + snprintf (pool, sizeof(pool), " x%u", prof->thread_pool_size); + else + pool[0] = 0; + /* Always show the level that is really in effect: the deprecated option + silently maps its value, see mhd_opt_matrix_effective_discipline(). */ + if (prof->use_legacy_strict) + snprintf (discp, sizeof(discp), "strict_for_client=%d -> discipline_lvl=%d", + prof->discipline_lvl, + mhd_opt_matrix_effective_discipline (prof)); + else + snprintf (discp, sizeof(discp), "discipline_lvl=%d", prof->discipline_lvl); + snprintf (buf, buf_size, + "'%s' (mem_limit=%s, %s, %s%s, %s)", + prof->name, + mem, + discp, + thr_names[(unsigned int) prof->threading], + pool, + poll_names[(unsigned int) prof->poll_backend]); + return buf; +} + + +unsigned int +mhd_opt_matrix_fill_options (const struct MHD_OptMatrixProfile *prof, + struct MHD_OptionItem *ops, + unsigned int max_ops) +{ + unsigned int n = 0; + + if (4 > max_ops) + return 0; + if (NULL != prof) + { + if (0 != prof->mem_limit) + { + ops[n].option = MHD_OPTION_CONNECTION_MEMORY_LIMIT; + ops[n].value = (intptr_t) prof->mem_limit; + ops[n].ptr_value = NULL; + ++n; + } + if (0 != prof->discipline_lvl) + { + ops[n].option = prof->use_legacy_strict ? + MHD_OPTION_STRICT_FOR_CLIENT : + MHD_OPTION_CLIENT_DISCIPLINE_LVL; + ops[n].value = (intptr_t) prof->discipline_lvl; + ops[n].ptr_value = NULL; + ++n; + } + if ((MHD_OPT_MATRIX_THR_POOL == prof->threading) && + (1 < prof->thread_pool_size)) + { + ops[n].option = MHD_OPTION_THREAD_POOL_SIZE; + ops[n].value = (intptr_t) prof->thread_pool_size; + ops[n].ptr_value = NULL; + ++n; + } + } + ops[n].option = MHD_OPTION_END; + ops[n].value = 0; + ops[n].ptr_value = NULL; + ++n; + return n; +} + + +unsigned int +mhd_opt_matrix_daemon_flags (const struct MHD_OptMatrixProfile *prof, + unsigned int base_flags, + int allow_external) +{ + unsigned int flags = base_flags; + + if (NULL == prof) + return flags; + switch (prof->threading) + { + case MHD_OPT_MATRIX_THR_EXTERNAL: + if (allow_external) + return flags; /* No internal thread, no polling backend flag */ + /* The caller cannot drive MHD_run(); fall back to an internal thread. */ + return flags | MHD_USE_INTERNAL_POLLING_THREAD; + case MHD_OPT_MATRIX_THR_PER_CONNECTION: + flags |= MHD_USE_INTERNAL_POLLING_THREAD | MHD_USE_THREAD_PER_CONNECTION; + break; + case MHD_OPT_MATRIX_THR_INTERNAL: + case MHD_OPT_MATRIX_THR_POOL: + default: + flags |= MHD_USE_INTERNAL_POLLING_THREAD; + break; + } + switch (prof->poll_backend) + { + case MHD_OPT_MATRIX_POLL_POLL: + flags |= MHD_USE_POLL; + break; + case MHD_OPT_MATRIX_POLL_EPOLL: + flags |= MHD_USE_EPOLL; + break; + case MHD_OPT_MATRIX_POLL_SELECT: + default: + break; + } + return flags; +} + + +int +mhd_opt_matrix_is_external (const struct MHD_OptMatrixProfile *prof) +{ + if (NULL == prof) + return 0; + return (MHD_OPT_MATRIX_THR_EXTERNAL == prof->threading) ? ! 0 : 0; +} + + +int +mhd_opt_matrix_raise_mem_limit (struct MHD_OptMatrixProfile *prof, + size_t min_limit) +{ + if ((NULL == prof) || (0 == min_limit) || (0 == prof->mem_limit)) + return 0; + if (prof->mem_limit >= min_limit) + return 0; + prof->mem_limit = min_limit; + return ! 0; +} + + +void +mhd_opt_matrix_print_notice (const char *test_name) +{ + const struct MHD_OptMatrixProfile *prof; + const char *name = (NULL != test_name) ? test_name : "test"; + char desc[256]; + + prof = mhd_opt_matrix_from_env (); + printf ("%s: option matrix profile %s\n", + name, + mhd_opt_matrix_describe (prof, desc, sizeof(desc))); + fflush (stdout); +} diff --git a/src/microhttpd/mhd_opt_matrix.h b/src/microhttpd/mhd_opt_matrix.h @@ -0,0 +1,387 @@ +/* + This file is part of GNU libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + GNU libmicrohttpd is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with GNU libmicrohttpd. + If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * @file microhttpd/mhd_opt_matrix.h + * @brief The environment-driven daemon option matrix shared by the tests + * @author Christian Grothoff + * + * This header must be included *after* "platform.h" and <microhttpd.h>. + * + * ## What this is + * + * Almost every test in this directory starts its daemon with one hard-coded + * set of options, so the whole test suite only ever exercises a single point + * of a large configuration space. This helper turns that space into a small + * table of named *profiles* and lets the profile be selected from the + * environment, so that an already built test binary can be re-run across the + * whole matrix without recompiling anything (see contrib/run-option-matrix.sh). + * + * The dimensions of the matrix are: + * + * * #MHD_OPTION_CONNECTION_MEMORY_LIMIT in + * {64, 128, 256, 512, 1024, 2048, 3072, 4096, library default}. 64 is the + * smallest value MHD accepts (anything below is rounded up to 64, see + * daemon.c). Everything up to 2048 keeps the read buffer below + * #MHD_BUF_INC_SIZE (1500) resp. small enough to unlock the read-buffer + * "shift back" path fixed by commit 29eaa56b, 3072 and 4096 do not, so both + * sides of that boundary are in the sweep; + * * #MHD_OPTION_CLIENT_DISCIPLINE_LVL in {-3 ... 3}; + * * #MHD_OPTION_STRICT_FOR_CLIENT, the older two-valued form of the same + * knob, so that its translation code is covered as well. Beware: that + * option maps *every* value of -1 or below to client discipline level -3, + * the most permissive one (daemon.c:7102-7106), so an application asking + * for "slightly lenient" in fact selects the most lenient mode; + * * the threading mode: external polling, internal polling thread, + * thread-per-connection and thread pool; + * * the polling backend: select(), poll() and epoll. Profiles asking for a + * backend that this build does not provide are reported as unsupported by + * mhd_opt_matrix_profile_supported(). + * + * #MHD_OPTION_SERVER_INSANITY is deliberately *not* a dimension: in this MHD + * version `enum MHD_DisableSanityCheck` has exactly one member, + * #MHD_DSC_SANE (zero), so the option cannot disable anything. + * + * ## Environment variables + * + * * #MHD_OPT_MATRIX_PROFILE_ENV ("MHD_TEST_PROFILE") - the profile to use, + * given either by name ("mem-512") or by index ("4"). The special value + * "list" is not handled here; contrib/run-option-matrix.sh knows the names. + * * #MHD_OPT_MATRIX_MEM_ENV ("MHD_TEST_MEM_LIMIT") - override the connection + * memory limit of the selected profile. "0" or "default" selects the + * library default. + * * #MHD_OPT_MATRIX_DISCP_ENV ("MHD_TEST_DISCIPLINE") - override the client + * discipline level (-3 ... 3) and use #MHD_OPTION_CLIENT_DISCIPLINE_LVL. + * * #MHD_OPT_MATRIX_STRICT_ENV ("MHD_TEST_STRICT_FOR_CLIENT") - override the + * same knob but use the deprecated #MHD_OPTION_STRICT_FOR_CLIENT instead. + * * #MHD_OPT_MATRIX_THREADING_ENV ("MHD_TEST_THREADING") - one of + * "external", "internal", "per-connection", "pool". + * * #MHD_OPT_MATRIX_POLL_ENV ("MHD_TEST_POLL") - one of "select", "poll", + * "epoll". + * + * If *none* of these variables is set, mhd_opt_matrix_from_env() + * returns NULL and every test keeps its previous, hard-coded behaviour. This + * is what a stock "make check" does, so nothing regresses. + */ + +#ifndef MHD_OPT_MATRIX_H +#define MHD_OPT_MATRIX_H 1 + +#include <stddef.h> +#include <microhttpd.h> + +/** + * The name of the environment variable selecting the profile. + */ +#define MHD_OPT_MATRIX_PROFILE_ENV "MHD_TEST_PROFILE" + +/** + * The name of the environment variable overriding the memory limit. + */ +#define MHD_OPT_MATRIX_MEM_ENV "MHD_TEST_MEM_LIMIT" + +/** + * The name of the environment variable overriding the discipline level. + */ +#define MHD_OPT_MATRIX_DISCP_ENV "MHD_TEST_DISCIPLINE" + +/** + * The name of the environment variable overriding the discipline level via + * the deprecated #MHD_OPTION_STRICT_FOR_CLIENT option. + */ +#define MHD_OPT_MATRIX_STRICT_ENV "MHD_TEST_STRICT_FOR_CLIENT" + +/** + * The name of the environment variable overriding the threading mode. + */ +#define MHD_OPT_MATRIX_THREADING_ENV "MHD_TEST_THREADING" + +/** + * The name of the environment variable overriding the polling backend. + */ +#define MHD_OPT_MATRIX_POLL_ENV "MHD_TEST_POLL" + +/** + * The threading mode of a profile. + */ +enum MHD_OptMatrixThreading +{ + /** + * External polling: the application drives the daemon with MHD_run(). + */ + MHD_OPT_MATRIX_THR_EXTERNAL = 0, + + /** + * One internal polling thread for all connections. + */ + MHD_OPT_MATRIX_THR_INTERNAL = 1, + + /** + * One internal thread per connection. + */ + MHD_OPT_MATRIX_THR_PER_CONNECTION = 2, + + /** + * A pool of internal polling threads. + */ + MHD_OPT_MATRIX_THR_POOL = 3 +}; + + +/** + * The polling backend of a profile. + */ +enum MHD_OptMatrixPoll +{ + /** + * Use select(). + */ + MHD_OPT_MATRIX_POLL_SELECT = 0, + + /** + * Use poll(), needs #MHD_FEATURE_POLL. + */ + MHD_OPT_MATRIX_POLL_POLL = 1, + + /** + * Use epoll, needs #MHD_FEATURE_EPOLL. + */ + MHD_OPT_MATRIX_POLL_EPOLL = 2 +}; + + +/** + * One point of the daemon option matrix. + * + * A caller may copy the structure and modify the copy, e.g. with + * mhd_opt_matrix_raise_mem_limit(); the tables returned by + * mhd_opt_matrix_profile() must not be modified. + */ +struct MHD_OptMatrixProfile +{ + /** + * Human readable name of the profile, used for logging and for + * #MHD_OPT_MATRIX_PROFILE_ENV. + */ + const char *name; + + /** + * The value for #MHD_OPTION_CONNECTION_MEMORY_LIMIT. + * Zero means "keep the MHD default". + */ + size_t mem_limit; + + /** + * The value for #MHD_OPTION_CLIENT_DISCIPLINE_LVL or, when + * @a use_legacy_strict is set, for #MHD_OPTION_STRICT_FOR_CLIENT. + * Zero means "keep the MHD default" and adds no option at all. + */ + int discipline_lvl; + + /** + * If non-zero, the deprecated #MHD_OPTION_STRICT_FOR_CLIENT option is used + * instead of #MHD_OPTION_CLIENT_DISCIPLINE_LVL, so that the legacy option + * translation code is exercised as well. + */ + int use_legacy_strict; + + /** + * The threading mode. + */ + enum MHD_OptMatrixThreading threading; + + /** + * The polling backend. + */ + enum MHD_OptMatrixPoll poll_backend; + + /** + * The number of worker threads, only used with + * #MHD_OPT_MATRIX_THR_POOL. + */ + unsigned int thread_pool_size; +}; + + +/** + * The number of built-in profiles. + */ +unsigned int +mhd_opt_matrix_num_profiles (void); + + +/** + * Get the built-in profile number @a idx. + * + * Profile zero is the "default" profile: the library defaults for every + * dimension plus one internal polling thread, i.e. the configuration the + * tests of this directory used before the matrix was introduced. + * + * @param idx the index of the profile, wraps around + * @return the profile, never NULL + */ +const struct MHD_OptMatrixProfile * +mhd_opt_matrix_profile (unsigned int idx); + + +/** + * Look up a profile by name or by decimal index. + * + * @param name_or_idx the name ("mem-512") or the index ("4") + * @return the profile, or NULL if @a name_or_idx matches nothing + */ +const struct MHD_OptMatrixProfile * +mhd_opt_matrix_lookup (const char *name_or_idx); + + +/** + * Get the profile selected by the environment. + * + * @return NULL if the environment selects nothing, in which case the caller + * must keep its own, previous configuration; otherwise a pointer to + * a static profile that stays valid until the next call + */ +const struct MHD_OptMatrixProfile * +mhd_opt_matrix_from_env (void); + + +/** + * Get the client discipline level that MHD will *really* use for @a prof. + * + * This is not always @a prof->discipline_lvl: #MHD_OPTION_STRICT_FOR_CLIENT + * only has two settings, and daemon.c:7102-7106 maps every value of -1 or + * below to level -3 (the most permissive one) and every value of 1 or above + * to level 1. A caller that decides what a request must do therefore has to + * ask for the effective level, not for the configured one - a test that + * believes it asked for "slightly lenient" in fact runs at -3. + * + * @param prof the profile to inspect, may be NULL (0 is returned then) + * @return the client discipline level in effect + */ +int +mhd_opt_matrix_effective_discipline (const struct MHD_OptMatrixProfile *prof); + + +/** + * Check whether @a prof can be used by this build at run time. + * + * @param prof the profile to check + * @return non-zero if the profile is usable, zero if the required threading + * support or polling backend is missing + */ +int +mhd_opt_matrix_profile_supported (const struct MHD_OptMatrixProfile *prof); + + +/** + * Render a one-line, human readable description of @a prof. + * + * @param prof the profile to describe + * @param[out] buf the buffer to write to + * @param buf_size the size of @a buf + * @return @a buf + */ +const char * +mhd_opt_matrix_describe (const struct MHD_OptMatrixProfile *prof, + char *buf, + size_t buf_size); + + +/** + * Fill @a ops with the options of @a prof. + * + * The array is terminated with an #MHD_OPTION_END element, so it can be + * passed to MHD_start_daemon() as + * + * MHD_start_daemon (flags, port, NULL, NULL, ahc, cls, + * MHD_OPTION_ARRAY, ops, + * ... other options ..., + * MHD_OPTION_END); + * + * which works for a varargs call just as well as for a call that already + * uses #MHD_OPTION_ARRAY. + * + * @param prof the profile to apply, may be NULL (only #MHD_OPTION_END is + * stored then) + * @param[out] ops the array to fill + * @param max_ops the number of elements of @a ops, at least 4 + * @return the number of elements used, including the terminating + * #MHD_OPTION_END; zero if @a ops is too small + */ +unsigned int +mhd_opt_matrix_fill_options (const struct MHD_OptMatrixProfile *prof, + struct MHD_OptionItem *ops, + unsigned int max_ops); + + +/** + * Combine the daemon flags required by @a prof with @a base_flags. + * + * @param prof the profile to apply, may be NULL (@a base_flags is returned) + * @param base_flags the flags the caller wants in any case, e.g. + * #MHD_USE_ERROR_LOG + * @param allow_external if zero, a profile that asks for external polling is + * served with an internal polling thread instead; use + * this in tests whose client cannot call MHD_run() + * @return the flags for MHD_start_daemon() + */ +unsigned int +mhd_opt_matrix_daemon_flags (const struct MHD_OptMatrixProfile *prof, + unsigned int base_flags, + int allow_external); + + +/** + * Check whether @a prof needs the application to drive MHD_run(). + * + * @param prof the profile to check, may be NULL + * @return non-zero if the daemon has no internal thread + */ +int +mhd_opt_matrix_is_external (const struct MHD_OptMatrixProfile *prof); + + +/** + * Raise the connection memory limit of @a prof to at least @a min_limit. + * + * Tests that cannot work with a tiny connection pool (because the requests + * they must send do not fit into it) use this to keep the other dimensions of + * the matrix while staying functional. The change must be reported in the + * test output so that the log stays truthful. + * + * @param[in,out] prof the profile to adjust, must be a caller-owned copy + * @param min_limit the smallest acceptable limit, zero does nothing + * @return non-zero if the limit was actually raised + */ +int +mhd_opt_matrix_raise_mem_limit (struct MHD_OptMatrixProfile *prof, + size_t min_limit); + + +/** + * Print the one-line notice naming the profile in use and, if a profile is + * selected, the profile. + * + * @param test_name the name of the calling test, used as the line prefix + */ +void +mhd_opt_matrix_print_notice (const char *test_name); + + +#endif /* MHD_OPT_MATRIX_H */ diff --git a/src/microhttpd/mhd_panic_tripwire.h b/src/microhttpd/mhd_panic_tripwire.h @@ -0,0 +1,477 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. + If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * @file microhttpd/mhd_panic_tripwire.h + * @brief Test-only tripwire that turns a fatal libmicrohttpd invariant + * failure into a loud, greppable, machine-classifiable test error. + * @author Christian Grothoff + * + * Rationale (TESTING.md, proposal P5): no network input must ever be able to + * reach #MHD_PANIC() or a failing mhd_assert(). When it does, the test suite + * should say so in a way that a CI job or the option-matrix driver can pick + * out mechanically, instead of leaving behind an anonymous SIGABRT that is + * indistinguishable from an ordinary test failure. + * + * USAGE + * ----- + * Add exactly one line to a test program: + * + * #include "mhd_panic_tripwire.h" + * + * Nothing else is needed: on GCC/clang the header installs itself from a + * `__attribute__((constructor))`, which runs after the libmicrohttpd shared + * object has run its own initialiser (and therefore after MHD_init() has + * installed the stock panic handler), so the tripwire wins. On toolchains + * without constructor support, call mhd_panic_tripwire_install() as the first + * statement of main() instead. + * + * WHAT IS INTERCEPTED + * ------------------- + * 1. #MHD_PANIC() - via the public MHD_set_panic_func() hook. The reference + * to MHD_set_panic_func() is *weak*, so this header can also be included + * by the unit tests that compile a couple of library objects directly and + * do not link libmicrohttpd at all; there the hook is simply skipped. + * 2. abort() - via a SIGABRT handler. This matters because in this code base + * the overwhelming majority of "fatal invariant reached from network + * input" sites are mhd_assert(), which goes to assert(3)/abort(3) and + * never passes through mhd_panic(). Set MHD_TEST_PANIC_TRIPWIRE_ABRT=0 + * to intercept #MHD_PANIC() only. + * + * WHY THE HANDLER MUST NOT RETURN, AND WHY IT DOES NOT longjmp() + * -------------------------------------------------------------- + * #MHD_PANIC(msg) expands to `mhd_panic (...); BUILTIN_NOT_REACHED;`, i.e. + * __builtin_unreachable(). A panic handler that returns therefore drops the + * caller straight into undefined behaviour - the compiler has already been + * told that the following code is dead and may have deleted it. So the + * handler has to terminate the process. + * + * longjmp() back into main() is not an option either: MHD_PANIC() is reached + * from library code that may be running on a daemon worker thread or on a + * thread-per-connection thread, and a longjmp() across threads is undefined. + * Even on the right thread it would leave the daemon's mutexes locked and its + * connection lists half-updated, so any "recording" done afterwards would run + * in a process whose state is already corrupt. We therefore report and + * terminate rather than recover. + * + * TERMINATION STATUS + * ------------------ + * The default is _exit(99). The automake parallel test harness classifies + * exit status 99 as a hard ERROR rather than an ordinary FAIL, which is + * exactly the "this is not a normal test failure, a fatal invariant was + * reached" signal wanted here. Note that this also applies to tests listed + * in XFAIL_TESTS - a test that is *expected* to trip an assertion (such as + * src/microhttpd/test_known_bugs.c) must therefore NOT include this header. + * + * ENVIRONMENT VARIABLES + * --------------------- + * @c MHD_TEST_PANIC_TRIPWIRE + * - unset, or any unrecognised value: enabled, terminate with _exit(99). + * - @c 0 / @c off / @c no / @c disable: completely disabled; the stock + * libmicrohttpd behaviour (mhd_panic_std(), plain abort()) is left in + * place. Use this when a debugger or an external tool wants the + * original behaviour. + * - @c abort: print the marker and the stack trace, then re-raise SIGABRT + * with the default disposition so that a core dump is produced. + * - @c exit:N (0 <= N <= 255): print the marker and the stack trace, then + * terminate with status N instead of 99. + * + * @c MHD_TEST_PANIC_TRIPWIRE_ABRT + * - @c 0 / @c off / @c no / @c disable: do not install the SIGABRT handler; + * only #MHD_PANIC() is intercepted. Failing mhd_assert()s then abort as + * usual. + * - unset or anything else: the SIGABRT handler is installed. + * + * OUTPUT FORMAT + * ------------- + * A single greppable marker line is written to stderr, followed (where + * available) by a stack trace and a final status line: + * + * MHD-PANIC-TRIPWIRE: kind=MHD_PANIC file=connection.c line=1234 \ + * reason=Data offset exceeds limit. + * MHD-PANIC-TRIPWIRE-FRAME: ... + * MHD-PANIC-TRIPWIRE: terminating with exit status 99 + * + * Grep for #MHD_PANIC_TRIPWIRE_MARKER ("MHD-PANIC-TRIPWIRE") to classify a + * failure. Everything is emitted with write(2) and without allocating, so + * the same code path is usable from the SIGABRT handler. + */ + +#ifndef MHD_PANIC_TRIPWIRE_H +#define MHD_PANIC_TRIPWIRE_H 1 + +#include <stdlib.h> +#include <string.h> +#include <signal.h> +#ifdef _WIN32 +#include <io.h> +#else /* ! _WIN32 */ +#include <unistd.h> +#endif /* ! _WIN32 */ + +/** + * The distinctive, greppable marker that prefixes every line printed by the + * tripwire. + */ +#define MHD_PANIC_TRIPWIRE_MARKER "MHD-PANIC-TRIPWIRE" + +/** + * The exit status used by default. The automake test harness reports this + * as a hard ERROR instead of a FAIL. + */ +#define MHD_PANIC_TRIPWIRE_EXIT_STATUS 99 + +/* Detect a usable backtrace() implementation. This must not depend on + configure, because this header is also used from test directories that do + not run any dedicated check for it. Degrade silently when absent. */ +#if ! defined(MHD_PANIC_TRIPWIRE_BACKTRACE_) +# if defined(__has_include ) +# if __has_include (<execinfo.h>) +# define MHD_PANIC_TRIPWIRE_BACKTRACE_ 1 +# endif /* __has_include(<execinfo.h>) */ +# elif defined(HAVE_EXECINFO_H) || defined(__GLIBC__) +# define MHD_PANIC_TRIPWIRE_BACKTRACE_ 1 +# endif /* HAVE_EXECINFO_H || __GLIBC__ */ +#endif /* ! MHD_PANIC_TRIPWIRE_BACKTRACE_ */ +#ifdef MHD_PANIC_TRIPWIRE_BACKTRACE_ +#include <execinfo.h> +#endif /* MHD_PANIC_TRIPWIRE_BACKTRACE_ */ + +/* Weak references let this header be included by the unit tests that do not + link libmicrohttpd (test_str*, test_md5, test_sha*, test_auth_parse, ...). + Without weak-symbol support the MHD_PANIC() hook is simply not installed + and only the abort() hook remains. */ +#if defined(__GNUC__) && defined(__ELF__) && ! defined(_WIN32) +# define MHD_PANIC_TRIPWIRE_WEAK_ __attribute__ ((weak)) +#endif /* __GNUC__ && __ELF__ && ! _WIN32 */ + +#ifdef MHD_PANIC_TRIPWIRE_WEAK_ +/* Deliberately declared here instead of pulling in <microhttpd.h>: the + signature is identical to MHD_PanicCallback, so this declaration is + compatible with the one in <microhttpd.h> when that header is also + included, and it keeps this file usable in translation units that never + see the public header. */ +extern MHD_PANIC_TRIPWIRE_WEAK_ void +MHD_set_panic_func (void (*cb)(void *cls, + const char *file, + unsigned int line, + const char *reason), + void *cls); + +#endif /* MHD_PANIC_TRIPWIRE_WEAK_ */ + +#if defined(__GNUC__) || defined(__clang__) +# define MHD_PANIC_TRIPWIRE_UNUSED_ __attribute__ ((unused)) +# define MHD_PANIC_TRIPWIRE_NORETURN_ __attribute__ ((noreturn)) +#else /* ! __GNUC__ */ +# define MHD_PANIC_TRIPWIRE_UNUSED_ +# define MHD_PANIC_TRIPWIRE_NORETURN_ +#endif /* ! __GNUC__ */ + +/** + * Selected action, resolved once by the installer so that the handlers + * themselves never have to call getenv(). + * 0: terminate with #mhd_panic_tripwire_status_; 1: re-raise SIGABRT. + */ +static volatile sig_atomic_t mhd_panic_tripwire_reraise_ = 0; + +/** + * The exit status to use when #mhd_panic_tripwire_reraise_ is zero. + */ +static volatile sig_atomic_t mhd_panic_tripwire_status_ = + MHD_PANIC_TRIPWIRE_EXIT_STATUS; + +/** + * Set once a report is in progress, so that a second thread tripping at the + * same time does not interleave its output with the first one. + */ +static volatile sig_atomic_t mhd_panic_tripwire_busy_ = 0; + + +/** + * Write a string literal to stderr. + * + * @param str a string literal + */ +#define MHD_PANIC_TRIPWIRE_WRS_(str) \ + mhd_panic_tripwire_wr_ (str, sizeof(str) - 1) + + +/** + * Write a buffer to stderr. Async-signal-safe: no locking, no allocation, + * no stdio. + * + * @param buf the bytes to write + * @param len the number of bytes to write + */ +static void +mhd_panic_tripwire_wr_ (const char *buf, + size_t len) +{ + size_t off = 0; + + while (off < len) + { +#ifdef _WIN32 + int res = _write (2, buf + off, (unsigned int) (len - off)); +#else /* ! _WIN32 */ + ssize_t res = write (STDERR_FILENO, buf + off, len - off); +#endif /* ! _WIN32 */ + if (0 >= res) + return; /* Give up rather than spin. */ + off += (size_t) res; + } +} + + +/** + * Write a NUL-terminated string to stderr, replacing every control character + * by a space so that the marker always stays on a single line. + * + * @param str the string to write, may be NULL + */ +static void +mhd_panic_tripwire_wr_clean_ (const char *str) +{ + char chunk[256]; + size_t used = 0; + + if (NULL == str) + { + MHD_PANIC_TRIPWIRE_WRS_ ("(null)"); + return; + } + while (0 != *str) + { + const unsigned char chr = (unsigned char) *str++; + + chunk[used++] = (0x20 > chr) ? ' ' : (char) chr; + if (sizeof(chunk) == used) + { + mhd_panic_tripwire_wr_ (chunk, used); + used = 0; + } + } + if (0 != used) + mhd_panic_tripwire_wr_ (chunk, used); +} + + +/** + * Write an unsigned number in decimal to stderr. + * + * @param val the value to print + */ +static void +mhd_panic_tripwire_wr_num_ (unsigned long val) +{ + char buf[24]; + size_t pos = sizeof(buf); + + do + { + buf[--pos] = (char) ('0' + (val % 10)); + val /= 10; + } while (0 != val); + mhd_panic_tripwire_wr_ (buf + pos, sizeof(buf) - pos); +} + + +/** + * Print the stack trace of the calling thread, if the platform provides one. + * Silently does nothing otherwise. + */ +static void +mhd_panic_tripwire_backtrace_ (void) +{ +#ifdef MHD_PANIC_TRIPWIRE_BACKTRACE_ + void *frames[64]; + int num; + + num = backtrace (frames, (int) (sizeof(frames) / sizeof(frames[0]))); + if (0 >= num) + return; + MHD_PANIC_TRIPWIRE_WRS_ (MHD_PANIC_TRIPWIRE_MARKER "-FRAMES: "); + mhd_panic_tripwire_wr_num_ ((unsigned long) num); + MHD_PANIC_TRIPWIRE_WRS_ (" frames follow\n"); + /* backtrace_symbols_fd() does not allocate, unlike backtrace_symbols(). */ + backtrace_symbols_fd (frames, num, 2); +#endif /* MHD_PANIC_TRIPWIRE_BACKTRACE_ */ +} + + +/** + * Print the marker line and the stack trace, then terminate the process. + * Never returns; see the file comment for why recovering is not an option. + * + * @param kind a short token naming what was intercepted + * @param file the source file of the failure, may be NULL + * @param line the source line of the failure + * @param reason the human-readable reason, may be NULL + */ +MHD_PANIC_TRIPWIRE_NORETURN_ static void +mhd_panic_tripwire_report_ (const char *kind, + const char *file, + unsigned int line, + const char *reason) +{ + if (0 == mhd_panic_tripwire_busy_) + { + mhd_panic_tripwire_busy_ = 1; + MHD_PANIC_TRIPWIRE_WRS_ ("\n" MHD_PANIC_TRIPWIRE_MARKER ": kind="); + mhd_panic_tripwire_wr_clean_ (kind); + MHD_PANIC_TRIPWIRE_WRS_ (" file="); + mhd_panic_tripwire_wr_clean_ ((NULL != file) ? file : "(unknown)"); + MHD_PANIC_TRIPWIRE_WRS_ (" line="); + mhd_panic_tripwire_wr_num_ ((unsigned long) line); + MHD_PANIC_TRIPWIRE_WRS_ (" reason="); + mhd_panic_tripwire_wr_clean_ ((NULL != reason) ? reason : "(none)"); + MHD_PANIC_TRIPWIRE_WRS_ ("\n"); + mhd_panic_tripwire_backtrace_ (); + if (0 == mhd_panic_tripwire_reraise_) + { + MHD_PANIC_TRIPWIRE_WRS_ (MHD_PANIC_TRIPWIRE_MARKER + ": terminating with exit status "); + mhd_panic_tripwire_wr_num_ ((unsigned long) mhd_panic_tripwire_status_); + MHD_PANIC_TRIPWIRE_WRS_ ("\n"); + } + else + MHD_PANIC_TRIPWIRE_WRS_ (MHD_PANIC_TRIPWIRE_MARKER + ": re-raising SIGABRT for a core dump\n"); + } + if (0 != mhd_panic_tripwire_reraise_) + { + signal (SIGABRT, SIG_DFL); + raise (SIGABRT); + } + _exit ((int) mhd_panic_tripwire_status_); +} + + +/** + * The #MHD_PanicCallback installed by the tripwire. + * + * @param cls unused + * @param file the name of the file with the problem + * @param line the line number with the problem + * @param reason the error message with details + */ +static void +mhd_panic_tripwire_panic_cb_ (void *cls, + const char *file, + unsigned int line, + const char *reason) +{ + (void) cls; /* Mute compiler warning. */ + mhd_panic_tripwire_report_ ("MHD_PANIC", + file, + line, + reason); +} + + +/** + * The SIGABRT handler installed by the tripwire. Catches a failing + * mhd_assert() (which goes through assert(3) and abort(3)) and any other + * abort(), for example from the C library's own consistency checks. + * + * @param sig the signal number, always SIGABRT + */ +static void +mhd_panic_tripwire_abrt_cb_ (int sig) +{ + (void) sig; /* Mute compiler warning. */ + mhd_panic_tripwire_report_ ("abort", + NULL, + 0, + "abort() reached; see the line above for the " + "failed assertion, if any"); +} + + +/** + * Test whether an environment variable value means "off". + * + * @param val the value to test, may be NULL + * @return non-zero if @a val explicitly disables a feature + */ +static int +mhd_panic_tripwire_is_off_ (const char *val) +{ + if (NULL == val) + return 0; + return (0 == strcmp (val, "0")) || + (0 == strcmp (val, "off")) || + (0 == strcmp (val, "no")) || + (0 == strcmp (val, "disable")); +} + + +/** + * Install the panic tripwire. Called automatically from a constructor on + * GCC-compatible toolchains; call it explicitly as the first statement of + * main() on any other toolchain. Idempotent. + */ +MHD_PANIC_TRIPWIRE_UNUSED_ static void +mhd_panic_tripwire_install (void) +{ + const char *mode = getenv ("MHD_TEST_PANIC_TRIPWIRE"); + + if (mhd_panic_tripwire_is_off_ (mode)) + return; /* Leave the stock behaviour completely untouched. */ + if ((NULL != mode) && (0 == strcmp (mode, "abort"))) + mhd_panic_tripwire_reraise_ = 1; + else if ((NULL != mode) && (0 == strncmp (mode, "exit:", 5))) + { + const long status = strtol (mode + 5, NULL, 10); + + if ((0 <= status) && (255 >= status)) + mhd_panic_tripwire_status_ = (sig_atomic_t) status; + } +#ifdef MHD_PANIC_TRIPWIRE_WEAK_ + if (NULL != MHD_set_panic_func) + MHD_set_panic_func (&mhd_panic_tripwire_panic_cb_, + NULL); +#else /* ! MHD_PANIC_TRIPWIRE_WEAK_ */ + MHD_set_panic_func (&mhd_panic_tripwire_panic_cb_, + NULL); +#endif /* ! MHD_PANIC_TRIPWIRE_WEAK_ */ + if (! mhd_panic_tripwire_is_off_ (getenv ("MHD_TEST_PANIC_TRIPWIRE_ABRT"))) + signal (SIGABRT, + &mhd_panic_tripwire_abrt_cb_); +} + + +#if defined(__GNUC__) || defined(__clang__) +/** + * Self-installer, so that adding the tripwire to a test program is a single + * #include line. Runs after the libmicrohttpd initialiser (the library is a + * dependency of the test executable and is therefore initialised first), so + * MHD_init()'s call to MHD_set_panic_func(NULL, NULL) cannot undo this. + */ +__attribute__ ((constructor)) static void +mhd_panic_tripwire_ctor_ (void) +{ + mhd_panic_tripwire_install (); +} + + +#endif /* __GNUC__ || __clang__ */ + +#endif /* ! MHD_PANIC_TRIPWIRE_H */ diff --git a/src/microhttpd/mhd_str.c b/src/microhttpd/mhd_str.c @@ -31,6 +31,7 @@ #include <string.h> #include "mhd_assert.h" +#include "mhd_check.h" #include "mhd_limits.h" #include "mhd_assert.h" @@ -2408,7 +2409,7 @@ MHD_base64_to_bin_n (const char *base64, const MHD_base64_map_type_ v4 = base64_char_to_value_ (in[i + 3]); if ((0 > v1) || (0 > v2)) return 0; /* Invalid char or padding at first two positions */ - mhd_assert (j < bin_size); + MHD_CHECK_RET_ (j < bin_size, 0); out[j++] = (uint8_t) (((uint8_t) (((uint8_t) v1) << 2)) | ((uint8_t) (((uint8_t) v2) >> 4))); if (0 > v3) diff --git a/src/microhttpd/sha256_ext.c b/src/microhttpd/sha256_ext.c @@ -85,6 +85,8 @@ MHD_SHA256_finish_reset (struct Sha256CtxExt *ctx, { if (0 == ctx->ext_error) gnutls_hash_output (ctx->handle, digest); + else + gnutls_hash_deinit (ctx->handle, NULL); } diff --git a/src/microhttpd/test_auth_parse.c b/src/microhttpd/test_auth_parse.c @@ -38,6 +38,10 @@ #include "internal.h" #include "connection.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef MHD_STATICSTR_LEN_ /** @@ -1059,11 +1063,66 @@ expect_digest_n (const char *hdr, size_t hdr_len, expect_digest_n(h,MHD_STATICSTR_LEN_(h),\ no,a,rs,un,ux,rm,ur,qr,qe,c,nc,uh,__LINE__) +/** + * Check the properties of the algorithm and QOP enumerations that the + * consumers of the parsed header rely on. + * + * The "invalid" members have the numeric value zero, which means that the + * usual "is the client value allowed by the application" test + * + * if (client_value != (client_value & allowed_mask)) + * reject (); + * + * lets the "invalid" value pass for every possible @a allowed_mask. Every + * consumer must therefore reject the "invalid" value explicitly before + * using it. Failing to do so made an unknown 'algorithm=' token in the + * request abort the whole daemon. + * + * @return zero if succeed, number of failures otherwise + */ +static unsigned int +check_digest_enum_sentinels (void) +{ + unsigned int r = 0; /**< The number of errors */ + + if (0 != (unsigned int) MHD_DIGEST_AUTH_ALGO3_INVALID) + { + r++; + fprintf (stderr, "MHD_DIGEST_AUTH_ALGO3_INVALID is not zero. Line: %u\n", + (unsigned int) __LINE__); + } + if (0 != (unsigned int) MHD_DIGEST_BASE_ALGO_INVALID) + { + r++; + fprintf (stderr, "MHD_DIGEST_BASE_ALGO_INVALID is not zero. Line: %u\n", + (unsigned int) __LINE__); + } + if (0 != (unsigned int) MHD_DIGEST_AUTH_QOP_INVALID) + { + r++; + fprintf (stderr, "MHD_DIGEST_AUTH_QOP_INVALID is not zero. Line: %u\n", + (unsigned int) __LINE__); + } + /* The bitmask test alone does not reject the "invalid" values. */ + if (0 != (((unsigned int) MHD_DIGEST_AUTH_ALGO3_INVALID) + & ~((unsigned int) MHD_DIGEST_AUTH_MULT_ALGO3_ANY_NON_SESSION))) + { + r++; + fprintf (stderr, "The bitmask check unexpectedly rejects " + "MHD_DIGEST_AUTH_ALGO3_INVALID. Line: %u\n", + (unsigned int) __LINE__); + } + return r; +} + + static unsigned int check_digest (void) { unsigned int r = 0; /**< The number of errors */ + r += check_digest_enum_sentinels (); + r += expect_digest ("Digest", NULL, MHD_DIGEST_AUTH_ALGO3_MD5, \ NULL, NULL, NULL, NULL, NULL, \ NULL, MHD_DIGEST_AUTH_QOP_NONE, NULL, NULL, 0); @@ -1254,6 +1313,59 @@ check_digest (void) MHD_DIGEST_AUTH_ALGO3_INVALID, \ NULL, NULL, NULL, NULL, NULL, \ NULL, MHD_DIGEST_AUTH_QOP_NONE, NULL, NULL, 0); + /* More spellings that the client may send and that must all be reported + as #MHD_DIGEST_AUTH_ALGO3_INVALID. Note that the numeric value of + that enumeration member is zero, so a consumer that only validates the + value with a bitmask ("(algo & allowed) == algo") accepts every one of + these; the consumer must check for the INVALID value explicitly. */ + r += expect_digest ("Digest algorithm=BOGUS", NULL, \ + MHD_DIGEST_AUTH_ALGO3_INVALID, \ + NULL, NULL, NULL, NULL, NULL, \ + NULL, MHD_DIGEST_AUTH_QOP_NONE, NULL, NULL, 0); + r += expect_digest ("Digest algorithm=\"\"", NULL, \ + MHD_DIGEST_AUTH_ALGO3_INVALID, \ + NULL, NULL, NULL, NULL, NULL, \ + NULL, MHD_DIGEST_AUTH_QOP_NONE, NULL, NULL, 0); + r += expect_digest ("Digest algorithm=\"BOGUS\"", NULL, \ + MHD_DIGEST_AUTH_ALGO3_INVALID, \ + NULL, NULL, NULL, NULL, NULL, \ + NULL, MHD_DIGEST_AUTH_QOP_NONE, NULL, NULL, 0); + r += expect_digest ("Digest algorithm=SHA-512", NULL, \ + MHD_DIGEST_AUTH_ALGO3_INVALID, \ + NULL, NULL, NULL, NULL, NULL, \ + NULL, MHD_DIGEST_AUTH_QOP_NONE, NULL, NULL, 0); + r += expect_digest ("Digest algorithm=SHA-1", NULL, \ + MHD_DIGEST_AUTH_ALGO3_INVALID, \ + NULL, NULL, NULL, NULL, NULL, \ + NULL, MHD_DIGEST_AUTH_QOP_NONE, NULL, NULL, 0); + r += expect_digest ("Digest algorithm=MD5 ", NULL, \ + MHD_DIGEST_AUTH_ALGO3_MD5, \ + NULL, NULL, NULL, NULL, NULL, \ + NULL, MHD_DIGEST_AUTH_QOP_NONE, NULL, NULL, 0); + r += expect_digest ("Digest algorithm=\"MD5 \"", NULL, \ + MHD_DIGEST_AUTH_ALGO3_INVALID, \ + NULL, NULL, NULL, NULL, NULL, \ + NULL, MHD_DIGEST_AUTH_QOP_NONE, NULL, NULL, 0); + r += expect_digest ("Digest algorithm=\" MD5\"", NULL, \ + MHD_DIGEST_AUTH_ALGO3_INVALID, \ + NULL, NULL, NULL, NULL, NULL, \ + NULL, MHD_DIGEST_AUTH_QOP_NONE, NULL, NULL, 0); + r += expect_digest ("Digest algorithm=\"MD\\5\"", NULL, \ + MHD_DIGEST_AUTH_ALGO3_MD5, \ + NULL, NULL, NULL, NULL, NULL, \ + NULL, MHD_DIGEST_AUTH_QOP_NONE, NULL, NULL, 0); + /* A duplicated parameter is accepted and the last occurrence wins. + Both orders are checked so that any change of that behaviour is + noticed: a client that can make different HTTP implementations pick + different occurrences of the same parameter is a source of trouble. */ + r += expect_digest ("Digest algorithm=MD5,algorithm=BOGUS", NULL, \ + MHD_DIGEST_AUTH_ALGO3_INVALID, \ + NULL, NULL, NULL, NULL, NULL, \ + NULL, MHD_DIGEST_AUTH_QOP_NONE, NULL, NULL, 0); + r += expect_digest ("Digest algorithm=BOGUS,algorithm=MD5", NULL, \ + MHD_DIGEST_AUTH_ALGO3_MD5, \ + NULL, NULL, NULL, NULL, NULL, \ + NULL, MHD_DIGEST_AUTH_QOP_NONE, NULL, NULL, 0); r += expect_digest ("Digest qop=auth", NULL, \ MHD_DIGEST_AUTH_ALGO3_MD5, \ diff --git a/src/microhttpd/test_chunked_ext.c b/src/microhttpd/test_chunked_ext.c @@ -0,0 +1,1032 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + libmicrohttpd is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 2, or (at your + option) any later version. + + libmicrohttpd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libmicrohttpd; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +/** + * @file test_chunked_ext.c + * @brief Regression test for the handling of the chunk-extension part of + * the chunk size line of a chunked request body. + * + * When a chunk size line carries a chunk-extension (RFC 9112, Section 7.1.1) + * the length of that line used to be computed as the position of the CR (or + * of the bare LF) instead of the position just after the line delimiter. + * As a result the line delimiter was not consumed and became part of the + * chunk data, shifting all the following body parsing by two (or one) bytes. + * That both corrupts the data given to the application and de-synchronises + * the request/response stream, which can be abused for HTTP request + * smuggling. + * + * Every sub-case sends, in a single write, a chunked request using a + * chunk-extension immediately followed by a pipelined second request on the + * same connection. The test verifies the exact bytes received by the + * request handler, that exactly one chunked request was processed and that + * the pipelined request was parsed correctly. + * + * The test honours the daemon option matrix of mhd_opt_matrix.h: when one of + * the MHD_TEST_* environment variables selects a profile, its connection + * memory limit, threading mode and polling backend are applied to every + * daemon this test starts. The *client discipline level* is deliberately + * NOT taken from the profile: it is the very thing each sub-case varies (a + * bare LF is accepted as the chunk size line delimiter only at level -3), so + * the per-sub-case value always wins. Without those environment variables + * nothing changes. As the sub-cases need a pool that can still hold the + * request and the reply, a profile asking for less than #MIN_POOL_FOR_TEST + * bytes is raised to that value and the adjustment is reported. + * + * @author Christian Grothoff + */ +#include "MHD_config.h" +#include "platform.h" +#include <microhttpd.h> +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <errno.h> + +#ifdef HAVE_STRINGS_H +#include <strings.h> +#endif /* HAVE_STRINGS_H */ + +#ifndef WINDOWS +#include <unistd.h> +#endif /* ! WINDOWS */ + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN 1 +#endif /* !WIN32_LEAN_AND_MEAN */ +#include <windows.h> +#endif /* _WIN32 */ + +#ifdef HAVE_SIGNAL_H +#include <signal.h> +#endif /* HAVE_SIGNAL_H */ + +#include "mhd_sockets.h" /* only macros used */ +#include "mhd_opt_matrix.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + +/** + * The smallest connection memory pool this test can work with: the sub-cases + * send a chunked request plus a pipelined second request and expect two + * complete replies, which does not fit into a smaller pool. + */ +#define MIN_POOL_FOR_TEST 1024 + +#ifndef MHD_STATICSTR_LEN_ +/** + * Determine length of static string / macro strings at compile time. + */ +#define MHD_STATICSTR_LEN_(macro) (sizeof(macro) / sizeof(char) - 1) +#endif /* ! MHD_STATICSTR_LEN_ */ + +#if MHD_VERSION >= 0x00097701 +#define TEST_MK_RESPONSE(len,str) \ + MHD_create_response_from_buffer_static ((len), (str)) +#else /* MHD_VERSION < 0x00097701 */ +#define TEST_MK_RESPONSE(len,str) \ + MHD_create_response_from_buffer ((len), (void *) (str), \ + MHD_RESPMEM_PERSISTENT) +#endif /* MHD_VERSION < 0x00097701 */ + +/** + * The exit code for a failed test. + */ +#define EXIT_FAILED_TEST 1 + +/** + * The exit code for a failure of the test framework itself. + */ +#define EXIT_HARD_ERROR 99 + +/** + * The maximum time (in seconds) to wait for any single socket operation. + */ +#define TEST_SOCKET_TIMEOUT_SEC 5 + +/** + * The time (in milliseconds) to wait between the two parts of a request + * which is intentionally sent in two writes. + */ +#define TEST_SPLIT_DELAY_MS 50 + +/** + * The body of the response sent by the test daemon. + */ +#define RESP_BODY "OK" + +/** + * The URL of the chunked request. + */ +#define URL_FIRST "/first" + +/** + * The URL of the pipelined request. + */ +#define URL_SECOND "/second" + +/** + * The head of the chunked request, the chunked body is appended to it. + */ +#define REQ_FIRST_HEAD \ + "POST " URL_FIRST " HTTP/1.1\r\nHost: h\r\n" \ + "Transfer-Encoding: chunked\r\n\r\n" + +/** + * The complete pipelined request, appended after the chunked body. + */ +#define REQ_SECOND \ + "GET " URL_SECOND " HTTP/1.1\r\nHost: h\r\nConnection: close\r\n\r\n" + +/** + * The number of the requests that could be tracked. + */ +#define MAX_TRACKED_REQS 8 + +/** + * The maximum size of the tracked request body. + */ +#define MAX_TRACKED_BODY 256 + +/** + * The data tracked for a single request. + */ +struct ReqTrack +{ + /** + * The URL of the request. + */ + char url[64]; + + /** + * The body of the request as given to the request handler. + */ + char body[MAX_TRACKED_BODY]; + + /** + * The number of bytes in @a body. + */ + size_t body_size; + + /** + * Non-zero if the body of the request did not fit in @a body. + */ + int body_overflow; + + /** + * Non-zero if the request has been fully received. + */ + int completed; +}; + +/** + * The tracked requests. + * Written by the daemon thread only, read by the main thread only after + * MHD_stop_daemon() (which joins the daemon thread) has returned. + */ +static struct ReqTrack tracked[MAX_TRACKED_REQS]; + +/** + * The number of the used elements in #tracked. + */ +static unsigned int num_tracked; + +/** + * Be verbose. + */ +static int verbose; + +/** + * The profile selected by the environment, NULL if the built-in daemon + * configuration is used (which is the case for a stock "make check"). + */ +static const struct MHD_OptMatrixProfile *test_prof; + +/** + * The (possibly adjusted) copy of the profile @a test_prof points to. + */ +static struct MHD_OptMatrixProfile test_prof_copy; + + +_MHD_NORETURN static void +hard_error (const char *desc, + int line_num) +{ + fprintf (stderr, + "HARD ERROR: %s at line %d. Last errno: %d (%s).\n", + desc, + line_num, + (int) errno, + strerror (errno)); + fflush (stderr); + exit (EXIT_HARD_ERROR); +} + + +#define hardErrorExit(desc) hard_error ((desc), __LINE__) + + +/** + * Pause the execution for the specified number of milliseconds. + * + * @param ms the number of the milliseconds to sleep + */ +static void +test_sleep_ms (unsigned int ms) +{ +#if defined(_WIN32) && ! defined(__CYGWIN__) + Sleep ((DWORD) ms); +#elif defined(HAVE_NANOSLEEP) + struct timespec slp; + struct timespec rmn; + int num_retries = 0; + + slp.tv_sec = (time_t) (ms / 1000); + slp.tv_nsec = (long) ((ms % 1000) * 1000000); + while (0 != nanosleep (&slp, &rmn)) + { + if (EINTR != errno) + break; + if (8 < num_retries++) + break; + slp = rmn; + } +#elif defined(HAVE_USLEEP) + usleep (((unsigned long) ms) * 1000); +#else /* ! HAVE_NANOSLEEP && ! HAVE_USLEEP */ + (void) ms; /* Mute compiler warning, the test works without the delay */ +#endif /* ! HAVE_NANOSLEEP && ! HAVE_USLEEP */ +} + + +/** + * The handler of the requests, tracks the URL and the exact body bytes. + */ +static enum MHD_Result +ahc_track (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **req_cls) +{ + struct ReqTrack *trk = (struct ReqTrack *) *req_cls; + struct MHD_Response *resp; + enum MHD_Result ret; + + (void) cls; (void) method; (void) version; + + if (NULL == trk) + { + if (MAX_TRACKED_REQS <= num_tracked) + return MHD_NO; /* Too many requests, the check in the main thread fails */ + trk = tracked + num_tracked++; + memset (trk, + 0, + sizeof (*trk)); + if (sizeof (trk->url) <= strlen (url)) + return MHD_NO; + memcpy (trk->url, + url, + strlen (url) + 1); + *req_cls = trk; + return MHD_YES; + } + if (0 != *upload_data_size) + { + if (sizeof (trk->body) < trk->body_size + *upload_data_size) + trk->body_overflow = 1; + else + { + memcpy (trk->body + trk->body_size, + upload_data, + *upload_data_size); + trk->body_size += *upload_data_size; + } + *upload_data_size = 0; + return MHD_YES; + } + trk->completed = 1; + resp = TEST_MK_RESPONSE (MHD_STATICSTR_LEN_ (RESP_BODY), + RESP_BODY); + if (NULL == resp) + return MHD_NO; + ret = MHD_queue_response (connection, + MHD_HTTP_OK, + resp); + MHD_destroy_response (resp); + return ret; +} + + +/** + * Set send and receive timeouts on the socket so that the test cannot + * block indefinitely. + * + * @param sk the socket to set the timeouts on + * @return non-zero if succeed, zero if failed + */ +static int +set_socket_timeouts (MHD_socket sk) +{ +#if defined(MHD_WINSOCK_SOCKETS) + DWORD tv = (DWORD) (TEST_SOCKET_TIMEOUT_SEC * 1000); +#else /* ! MHD_WINSOCK_SOCKETS */ + struct timeval tv; + + tv.tv_sec = (time_t) TEST_SOCKET_TIMEOUT_SEC; + tv.tv_usec = 0; +#endif /* ! MHD_WINSOCK_SOCKETS */ + if (0 != setsockopt (sk, + SOL_SOCKET, + SO_RCVTIMEO, + (const void *) &tv, + (socklen_t) sizeof (tv))) + return 0; + if (0 != setsockopt (sk, + SOL_SOCKET, + SO_SNDTIMEO, + (const void *) &tv, + (socklen_t) sizeof (tv))) + return 0; + return ! 0; +} + + +/** + * Open a TCP connection to the given port on the loopback interface. + * + * @param port the port to connect to + * @return the connected socket, MHD_INVALID_SOCKET on failure + */ +static MHD_socket +connect_to_port (uint16_t port) +{ + MHD_socket sk; + struct sockaddr_in sa; + + sk = socket (PF_INET, + SOCK_STREAM, + 0); + if (MHD_INVALID_SOCKET == sk) + return MHD_INVALID_SOCKET; + if (! set_socket_timeouts (sk)) + { + (void) MHD_socket_close_ (sk); + return MHD_INVALID_SOCKET; + } + memset (&sa, + 0, + sizeof (sa)); + sa.sin_family = AF_INET; + sa.sin_port = htons (port); + sa.sin_addr.s_addr = htonl (INADDR_LOOPBACK); + if (0 != connect (sk, + (const struct sockaddr *) &sa, + (socklen_t) sizeof (sa))) + { + (void) MHD_socket_close_ (sk); + return MHD_INVALID_SOCKET; + } + return sk; +} + + +/** + * Send all the given data over the socket. + * + * @param sk the socket to use + * @param buf the data to send + * @param size the size of @a buf + * @return non-zero if succeed, zero if failed + */ +static int +send_all (MHD_socket sk, + const char *buf, + size_t size) +{ + size_t off = 0; + + while (off < size) + { + ssize_t res; + + res = MHD_send_ (sk, + buf + off, + size - off); + if (0 > res) + { + if (MHD_SCKT_ERR_IS_EINTR_ (MHD_socket_get_error_ ())) + continue; + return 0; + } + if (0 == res) + return 0; + off += (size_t) res; + } + return ! 0; +} + + +/** + * Receive data from the socket until the remote side closes the connection, + * the buffer is full or the receive timeout expires. + * + * @param sk the socket to use + * @param[out] buf the buffer to fill + * @param buf_size the size of @a buf, the result is always zero-terminated, + * therefore at most @a buf_size minus one bytes are received + * @return the number of the received bytes + */ +static size_t +recv_all (MHD_socket sk, + char *buf, + size_t buf_size) +{ + size_t off = 0; + + while (off + 1 < buf_size) + { + ssize_t res; + + res = MHD_recv_ (sk, + buf + off, + buf_size - 1 - off); + if (0 > res) + { + if (MHD_SCKT_ERR_IS_EINTR_ (MHD_socket_get_error_ ())) + continue; + break; /* Timeout or error */ + } + if (0 == res) + break; /* The remote side closed the connection */ + off += (size_t) res; + } + buf[off] = 0; + return off; +} + + +/** + * Count the number of the non-overlapping occurrences of @a needle + * in @a haystack. + * + * @param haystack the zero-terminated string to search in + * @param needle the zero-terminated string to search for + * @return the number of the found occurrences + */ +static unsigned int +count_substr (const char *haystack, + const char *needle) +{ + const size_t needle_len = strlen (needle); + unsigned int num = 0; + const char *pos = haystack; + + if (0 == needle_len) + return 0; + while (NULL != (pos = strstr (pos, needle))) + { + ++num; + pos += needle_len; + } + return num; +} + + +/** + * Print the data to the standard error stream, replacing the non-printable + * characters with their escaped hexadecimal form. + * + * @param data the data to print + * @param size the size of @a data + */ +static void +print_escaped (const char *data, + size_t size) +{ + size_t i; + + for (i = 0; i < size; ++i) + { + const unsigned char chr = (unsigned char) data[i]; + + if ((0x20 <= chr) && (0x7e >= chr) && ('\\' != chr)) + fputc ((int) chr, + stderr); + else if ('\\' == chr) + fputs ("\\\\", + stderr); + else if ('\r' == chr) + fputs ("\\r", + stderr); + else if ('\n' == chr) + fputs ("\\n", + stderr); + else if ('\t' == chr) + fputs ("\\t", + stderr); + else + fprintf (stderr, + "\\x%02X", + (unsigned int) chr); + } +} + + +/** + * The description of a single sub-case. + */ +struct ChunkCase +{ + /** + * The human readable name of the sub-case. + */ + const char *name; + + /** + * The chunked body of the request, including the terminating chunk and + * the (empty) trailer section. + */ + const char *chunked; + + /** + * The exact bytes that the request handler must receive. + */ + const char *expected; + + /** + * The number of the bytes in @a expected. + */ + size_t expected_size; + + /** + * If non-zero, the request is sent in two writes and this is the number of + * the bytes of @a chunked included in the first write. + */ + size_t split_at; + + /** + * The value for MHD_OPTION_CLIENT_DISCIPLINE_LVL. + */ + int discipline; + + /** + * Non-zero if the sub-case requires the support of the bare LF as the + * line delimiter of the chunk size line. + */ + int need_bare_lf; +}; + +#define CHUNK_CASE(nm,ch,exp,spl,dsc,blf) \ + { (nm), (ch), (exp), MHD_STATICSTR_LEN_ (exp), (spl), (dsc), (blf) } + +/** + * The sub-cases. + * All of them must be processed identically: the handler must receive + * exactly the expected bytes, and the pipelined request must be processed + * as the second (and the last) request on the connection. + */ +static const struct ChunkCase chunkcases[] = { + CHUNK_CASE ("no chunk-extension (control)", + "5\r\nHELLO\r\n0\r\n\r\n", + "HELLO", 0, 0, 0), + CHUNK_CASE ("chunk-extension with a value", + "5;ext=val\r\nHELLO\r\n0\r\n\r\n", + "HELLO", 0, 0, 0), + CHUNK_CASE ("chunk-extension without a value", + "5;ext\r\nHELLO\r\n0\r\n\r\n", + "HELLO", 0, 0, 0), + CHUNK_CASE ("chunk-extension after a bad whitespace (space)", + "5 ;ext=val\r\nHELLO\r\n0\r\n\r\n", + "HELLO", 0, 0, 0), + CHUNK_CASE ("chunk-extension after a bad whitespace (tab)", + "5\t;ext\r\nHELLO\r\n0\r\n\r\n", + "HELLO", 0, 0, 0), + CHUNK_CASE ("chunk-extension with a quoted-string value", + "5;ext=\"quoted; value\"\r\nHELLO\r\n0\r\n\r\n", + "HELLO", 0, 0, 0), + CHUNK_CASE ("several chunk-extensions", + "5;e1=v1;e2=\"v 2\";e3\r\nHELLO\r\n0\r\n\r\n", + "HELLO", 0, 0, 0), + CHUNK_CASE ("two chunks, both with a chunk-extension", + "3;a=b\r\nabc\r\n4;c=d\r\ndefg\r\n0\r\n\r\n", + "abcdefg", 0, 0, 0), + CHUNK_CASE ("two chunks, only the first with a chunk-extension", + "3;a=b\r\nabc\r\n4\r\ndefg\r\n0\r\n\r\n", + "abcdefg", 0, 0, 0), + CHUNK_CASE ("two chunks, only the second with a chunk-extension", + "3\r\nabc\r\n4;c=d\r\ndefg\r\n0\r\n\r\n", + "abcdefg", 0, 0, 0), + CHUNK_CASE ("chunk-extension on the terminating chunk as well", + "3;a=b\r\nabc\r\n4;c=d\r\ndefg\r\n0;last=1\r\n\r\n", + "abcdefg", 0, 0, 0), + CHUNK_CASE ("chunk data with an embedded CRLF", + "7;ext=val\r\nAB\r\nCDE\r\n0\r\n\r\n", + "AB\r\nCDE", 0, 0, 0), + CHUNK_CASE ("chunk-extension line split between two writes", + "5;ext=val\r\nHELLO\r\n0\r\n\r\n", + "HELLO", 4, 0, 0), + CHUNK_CASE ("chunk-extension line delimiter split between two writes", + "5;ext=val\r\nHELLO\r\n0\r\n\r\n", + "HELLO", 10, 0, 0), + CHUNK_CASE ("chunk-extension with a bare LF line delimiter", + "5;ext=val\nHELLO\r\n0\r\n\r\n", + "HELLO", 0, -3, 1), + CHUNK_CASE ("two chunks with a bare LF line delimiter", + "3;a=b\nabc\r\n4;c=d\ndefg\r\n0\r\n\r\n", + "abcdefg", 0, -3, 1) +}; + +/** + * The sub-case used to detect whether this build accepts the bare LF as the + * line delimiter of the chunk size line. It does not use any + * chunk-extension, therefore it is not affected by the tested bug. + */ +static const struct ChunkCase bare_lf_probe = + CHUNK_CASE ("bare LF support probe", + "5\nHELLO\r\n0\r\n\r\n", + "HELLO", 0, -3, 0); + + +/** + * Run a single sub-case. + * + * @param cc the sub-case to run + * @param quiet if non-zero, do not report the failures + * @return non-zero if the sub-case succeeded, zero if it failed + */ +static int +run_subcase (const struct ChunkCase *cc, + int quiet) +{ + struct MHD_Daemon *d; + const union MHD_DaemonInfo *dinfo; + uint16_t port; + MHD_socket sk; + char req[1024]; + char resp[4096]; + size_t req_size; + size_t chunked_size; + size_t resp_size; + int failed = 0; + int sent_ok; + struct MHD_OptMatrixProfile prof; + struct MHD_OptionItem ops[8]; + unsigned int flags; + + chunked_size = strlen (cc->chunked); + req_size = MHD_STATICSTR_LEN_ (REQ_FIRST_HEAD) + chunked_size + + MHD_STATICSTR_LEN_ (REQ_SECOND); + if (sizeof (req) <= req_size) + hardErrorExit ("The request does not fit in the buffer"); + memcpy (req, + REQ_FIRST_HEAD, + MHD_STATICSTR_LEN_ (REQ_FIRST_HEAD)); + memcpy (req + MHD_STATICSTR_LEN_ (REQ_FIRST_HEAD), + cc->chunked, + chunked_size); + memcpy (req + MHD_STATICSTR_LEN_ (REQ_FIRST_HEAD) + chunked_size, + REQ_SECOND, + MHD_STATICSTR_LEN_ (REQ_SECOND) + 1); + + num_tracked = 0; + memset (tracked, + 0, + sizeof (tracked)); + + /* The profile (if any) supplies the memory limit, the threading mode and + the polling backend. The client discipline level always comes from the + sub-case, see the file comment. */ + if (NULL != test_prof) + prof = *test_prof; + else + { + memset (&prof, 0, sizeof (prof)); + prof.name = "built-in"; + prof.threading = MHD_OPT_MATRIX_THR_INTERNAL; + prof.poll_backend = MHD_OPT_MATRIX_POLL_SELECT; + } + prof.discipline_lvl = 0; + prof.use_legacy_strict = 0; + if (0 == mhd_opt_matrix_fill_options (&prof, ops, + (unsigned int) (sizeof (ops) + / sizeof (ops[0])))) + hardErrorExit ("The daemon option array is too small"); + /* The client of this test is a plain blocking socket client, so external + polling is served with an internal polling thread instead. */ + flags = mhd_opt_matrix_daemon_flags (&prof, MHD_USE_ERROR_LOG, 0); + + d = MHD_start_daemon (flags, + 0, + NULL, NULL, + &ahc_track, NULL, + MHD_OPTION_ARRAY, ops, + MHD_OPTION_CLIENT_DISCIPLINE_LVL, cc->discipline, + MHD_OPTION_CONNECTION_TIMEOUT, + (unsigned int) TEST_SOCKET_TIMEOUT_SEC, + MHD_OPTION_END); + if (NULL == d) + hardErrorExit ("Cannot start the test daemon"); + dinfo = MHD_get_daemon_info (d, + MHD_DAEMON_INFO_BIND_PORT); + if ((NULL == dinfo) || (0 == dinfo->port)) + { + MHD_stop_daemon (d); + hardErrorExit ("Cannot detect the port used by the test daemon"); + } + port = (uint16_t) dinfo->port; + + sk = connect_to_port (port); + if (MHD_INVALID_SOCKET == sk) + { + MHD_stop_daemon (d); + hardErrorExit ("Cannot connect to the test daemon"); + } + + if (0 == cc->split_at) + sent_ok = send_all (sk, + req, + req_size); + else + { + const size_t first_part = + MHD_STATICSTR_LEN_ (REQ_FIRST_HEAD) + cc->split_at; + + if (first_part >= req_size) + hardErrorExit ("Wrong 'split_at' value in the sub-case"); + sent_ok = send_all (sk, + req, + first_part); + if (sent_ok) + { + test_sleep_ms (TEST_SPLIT_DELAY_MS); + sent_ok = send_all (sk, + req + first_part, + req_size - first_part); + } + } + if (! sent_ok) + { + if (! quiet) + fprintf (stderr, + "FAILED: sub-case '%s': cannot send the request.\n", + cc->name); + (void) MHD_socket_close_ (sk); + MHD_stop_daemon (d); + return 0; + } + resp_size = recv_all (sk, + resp, + sizeof (resp)); + (void) MHD_socket_close_ (sk); + /* MHD_stop_daemon() joins the daemon thread, therefore all the data + tracked by the request handler is safely visible afterwards. */ + MHD_stop_daemon (d); + + if (2 != num_tracked) + { + if (! quiet) + fprintf (stderr, + "FAILED: sub-case '%s': the number of the processed requests " + "is %u, while exactly 2 requests are expected.\n", + cc->name, + num_tracked); + failed = 1; + } + if (1 <= num_tracked) + { + const struct ReqTrack *const trk = tracked + 0; + + if (0 != strcmp (trk->url, + URL_FIRST)) + { + if (! quiet) + fprintf (stderr, + "FAILED: sub-case '%s': the URL of the first request is " + "'%s', while '%s' is expected.\n", + cc->name, + trk->url, + URL_FIRST); + failed = 1; + } + if (0 != trk->body_overflow) + { + if (! quiet) + fprintf (stderr, + "FAILED: sub-case '%s': the body of the first request is " + "too large.\n", + cc->name); + failed = 1; + } + else if ((cc->expected_size != trk->body_size) || + (0 != memcmp (cc->expected, + trk->body, + cc->expected_size))) + { + if (! quiet) + { + fprintf (stderr, + "FAILED: sub-case '%s': the body of the first request is '", + cc->name); + print_escaped (trk->body, + trk->body_size); + fprintf (stderr, + "' (%u bytes), while '", + (unsigned) trk->body_size); + print_escaped (cc->expected, + cc->expected_size); + fprintf (stderr, + "' (%u bytes) is expected.\n", + (unsigned) cc->expected_size); + } + failed = 1; + } + if (0 == trk->completed) + { + if (! quiet) + fprintf (stderr, + "FAILED: sub-case '%s': the first request has not been " + "completed.\n", + cc->name); + failed = 1; + } + } + if (2 <= num_tracked) + { + const struct ReqTrack *const trk = tracked + 1; + + if (0 != strcmp (trk->url, + URL_SECOND)) + { + if (! quiet) + fprintf (stderr, + "FAILED: sub-case '%s': the URL of the pipelined request is " + "'%s', while '%s' is expected. The request stream has been " + "de-synchronised.\n", + cc->name, + trk->url, + URL_SECOND); + failed = 1; + } + if ((0 != trk->body_size) || (0 != trk->body_overflow)) + { + if (! quiet) + fprintf (stderr, + "FAILED: sub-case '%s': the pipelined request has a body of " + "%u bytes, while no body is expected.\n", + cc->name, + (unsigned) trk->body_size); + failed = 1; + } + if (0 == trk->completed) + { + if (! quiet) + fprintf (stderr, + "FAILED: sub-case '%s': the pipelined request has not been " + "completed.\n", + cc->name); + failed = 1; + } + } + if (2 != count_substr (resp, + "HTTP/1.1 ")) + { + if (! quiet) + fprintf (stderr, + "FAILED: sub-case '%s': the number of the replies is %u, " + "while exactly 2 replies are expected. The reply stream is:\n" + "%.*s\n", + cc->name, + count_substr (resp, "HTTP/1.1 "), + (int) resp_size, + resp); + failed = 1; + } + else if (2 != count_substr (resp, + "HTTP/1.1 200 ")) + { + if (! quiet) + fprintf (stderr, + "FAILED: sub-case '%s': not all replies have the '200' status " + "code. The reply stream is:\n%.*s\n", + cc->name, + (int) resp_size, + resp); + failed = 1; + } + + if ((0 == failed) && verbose) + printf ("PASSED: %s\n", + cc->name); + return ! failed; +} + + +int +main (int argc, char *const *argv) +{ + unsigned int num_failed = 0; + unsigned int num_skipped = 0; + int bare_lf_supported; + size_t i; + + verbose = 0; + for (i = 1; i < (size_t) argc; ++i) + { + if ((0 == strcmp (argv[i], "-v")) || + (0 == strcmp (argv[i], "--verbose"))) + verbose = 1; + } + + if (MHD_YES != MHD_is_feature_supported (MHD_FEATURE_AUTOSUPPRESS_SIGPIPE)) + { +#if defined(HAVE_SIGNAL_H) && defined(SIGPIPE) + if (SIG_ERR == signal (SIGPIPE, SIG_IGN)) + hardErrorExit ("Cannot suppress the SIGPIPE signal"); +#else /* ! HAVE_SIGNAL_H || ! SIGPIPE */ + fprintf (stderr, + "Cannot suppress the SIGPIPE signal.\n"); +#endif /* ! HAVE_SIGNAL_H || ! SIGPIPE */ + } + + mhd_opt_matrix_print_notice ("test_chunked_ext"); + test_prof = mhd_opt_matrix_from_env (); + if (NULL != test_prof) + { + char desc[256]; + + test_prof_copy = *test_prof; + test_prof = &test_prof_copy; + if (! mhd_opt_matrix_profile_supported (test_prof)) + { + printf ("test_chunked_ext: the selected profile %s is not supported by " + "this build, the test is skipped.\n", + mhd_opt_matrix_describe (test_prof, desc, sizeof (desc))); + return 77; + } + if (mhd_opt_matrix_raise_mem_limit (&test_prof_copy, MIN_POOL_FOR_TEST)) + printf ("test_chunked_ext: the connection memory limit of the profile " + "was raised to %u, the smallest pool this test can work " + "with.\n", (unsigned) MIN_POOL_FOR_TEST); + } + + /* Detect whether the bare LF is accepted as the line delimiter of the + chunk size line with the lowest client discipline level. */ + bare_lf_supported = run_subcase (&bare_lf_probe, + 1); + if ((! bare_lf_supported) && verbose) + printf ("The bare LF as the chunk size line delimiter is not supported " + "by this build, the related sub-cases are skipped.\n"); + + for (i = 0; i < sizeof (chunkcases) / sizeof (chunkcases[0]); ++i) + { + const struct ChunkCase *const cc = chunkcases + i; + + if ((0 != cc->need_bare_lf) && (! bare_lf_supported)) + { + ++num_skipped; + continue; + } + /* Report the sub-case before running it, so that the failing sub-case + can be identified even if the daemon aborts the process. */ + fprintf (stderr, + "Running sub-case: %s\n", + cc->name); + fflush (stderr); + if (! run_subcase (cc, + 0)) + ++num_failed; + } + + if (0 != num_failed) + { + fprintf (stderr, + "FAILED: %u sub-case(s) failed.\n", + num_failed); + return EXIT_FAILED_TEST; + } + if (verbose) + printf ("All sub-cases passed (%u sub-case(s) skipped).\n", + num_skipped); + return 0; +} diff --git a/src/microhttpd/test_client_put_stop.c b/src/microhttpd/test_client_put_stop.c @@ -89,6 +89,10 @@ #include "test_helpers.h" #include "mhd_assert.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_CPU_COUNT) && (MHD_CPU_COUNT + 0) < 2 #undef MHD_CPU_COUNT #endif diff --git a/src/microhttpd/test_daemon.c b/src/microhttpd/test_daemon.c @@ -32,6 +32,10 @@ #include <string.h> #include <stdio.h> +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef WINDOWS #include <unistd.h> #endif diff --git a/src/microhttpd/test_dauth_malformed.c b/src/microhttpd/test_dauth_malformed.c @@ -0,0 +1,1248 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + libmicrohttpd is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 2, or (at your + option) any later version. + + libmicrohttpd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libmicrohttpd; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +/** + * @file test_dauth_malformed.c + * @brief Testcase for malformed Digest "Authorization:" request headers + * @author Christian Grothoff + * + * The malformed headers used by this test cannot be produced with libcurl, + * therefore all the requests are sent over a raw socket. + * + * The test is a regression test for two remotely triggerable defects: + * + * - An unrecognised "algorithm=" token is parsed to + * #MHD_DIGEST_AUTH_ALGO3_INVALID, the numeric value of which is zero. The + * "is the client's algorithm allowed by the application" bit-mask test is + * passed trivially by a zero value, so an invalid algorithm used to reach + * digest_init_one_time(), which aborts the process with MHD_PANIC(). + * + * - The "response=" value was hex-decoded into a #MAX_DIGEST bytes long + * buffer on the stack, while the only length check allowed four times as + * many characters as the digest size, resulting in an out-of-bounds stack + * write of up to #MAX_DIGEST bytes. Reaching the decoder requires an + * otherwise complete and valid Digest handshake, hence the two-step + * exchange performed by this test. + * + * A number of adjacent malformed, empty, over-long and duplicated parameters + * is checked as well. Every sub-case must leave the daemons alive and must + * be answered with a regular HTTP error reply. + * + * Two daemons are used: one that issues SHA-256 challenges and one that + * issues the challenges with the default "any non-session" algorithm. This + * keeps the length of the nonces recorded by a single daemon uniform, which + * is what a real-world application does as well. + * + * The test honours the daemon option matrix of mhd_opt_matrix.h: when one of + * the MHD_TEST_* environment variables selects a profile, its connection + * memory limit, client discipline level, threading mode and polling backend + * are applied to both daemons. Without those variables nothing changes. A + * Digest handshake needs room for a 76 character nonce plus the realm, the + * username and the response, so a profile asking for less than + * #MIN_POOL_FOR_TEST bytes is raised to that value and the adjustment is + * reported. + */ + +#include "MHD_config.h" +#include "platform.h" +#include <microhttpd.h> +#include <stdlib.h> +#include <string.h> +#include <stdarg.h> +#include <stdint.h> +#include <errno.h> + +#ifdef HAVE_STRINGS_H +#include <strings.h> +#endif /* HAVE_STRINGS_H */ + +#ifndef WINDOWS +#include <unistd.h> +#include <sys/socket.h> +#endif + +#include "mhd_sockets.h" /* only macros used */ +#include "mhd_opt_matrix.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + +/** + * The smallest connection memory pool this test can work with: a complete + * Digest "Authorization:" header plus the "WWW-Authenticate:" challenge of + * the reply do not fit into a smaller pool. + */ +#define MIN_POOL_FOR_TEST 4096 + +#ifndef MHD_STATICSTR_LEN_ +/** + * Determine length of static string / macro strings at compile time. + */ +#define MHD_STATICSTR_LEN_(macro) (sizeof(macro) / sizeof(char) - 1) +#endif /* ! MHD_STATICSTR_LEN_ */ + +#if defined(DAUTH_SUPPORT) && defined(MHD_SHA256_SUPPORT) + +/** + * The realm used by the test daemons. + */ +#define TEST_REALM "TestRealm" + +/** + * The username known to the test daemons. + */ +#define TEST_USERNAME "testuser" + +/** + * The password known to the test daemons. + */ +#define TEST_PASSWORD "testpass" + +/** + * The URL used by all the requests. + */ +#define TEST_URL "/auth" + +/** + * The size of the buffer used to build a request. + */ +#define REQ_BUF_SIZE 32768 + +/** + * The size of the buffer used to receive a reply. + */ +#define RPLY_BUF_SIZE 8192 + +/** + * The length of the over-long parameter values. + */ +#define LONG_PARAM_LEN 4000 + +/** + * Socket send/receive timeout in seconds. + */ +#define SOCK_TIMEOUT_SEC 5 + +/** + * The number of the "nonce"/"nc" pairs recorded by a daemon. + */ +#define NONCE_NC_SIZE 100 + + +/** + * The port of the daemon issuing SHA-256 challenges. + */ +static uint16_t port_sha256; + +/** + * The port of the daemon issuing "any non-session" (MD5) challenges. + */ +static uint16_t port_any; + +/** + * The port used by the sub-case being executed. + */ +static uint16_t test_port; + +/** + * The number of the sub-case being executed. + */ +static unsigned int case_num; + +/** + * The number of the failed sub-cases. + */ +static unsigned int failures; + +/** + * The 'nc' value for the next request, incremented for every request so that + * a "nonce"/"nc" combination is never re-used. + */ +static unsigned int nc_counter; + +/** + * Run only the sub-cases with a name containing this string, NULL for all. + */ +static const char *case_filter; + +/** + * Set to non-zero by the daemon if a malformed request was authenticated. + */ +static volatile int wrongly_authenticated; + +/** + * The profile selected by the environment, NULL if the built-in daemon + * configuration is used (which is the case for a stock "make check"). + */ +static const struct MHD_OptMatrixProfile *test_prof; + +/** + * The (possibly adjusted) copy of the profile @a test_prof points to. + */ +static struct MHD_OptMatrixProfile test_prof_copy; + + +_MHD_NORETURN static void +hard_error (const char *msg) +{ + fprintf (stderr, + "Hard error: %s\n", + msg); + fflush (stderr); + exit (99); +} + + +/** + * Fill @a buf with @a len hexadecimal digits and zero-terminate it. + * + * @param[out] buf the buffer to fill, must be at least @a len + 1 bytes long + * @param len the number of the hexadecimal digits to generate + * @return the @a buf pointer + */ +static char * +fill_hex (char *buf, + size_t len) +{ + static const char hex_chars[] = "0123456789abcdef"; + size_t i; + + for (i = 0; i < len; ++i) + buf[i] = hex_chars[i % (MHD_STATICSTR_LEN_ (hex_chars))]; + buf[len] = 0; + return buf; +} + + +/** + * Fill @a buf with @a len copies of @a chr and zero-terminate it. + * + * @param[out] buf the buffer to fill, must be at least @a len + 1 bytes long + * @param chr the character to repeat + * @param len the number of the characters to generate + * @return the @a buf pointer + */ +static char * +fill_chr (char *buf, + char chr, + size_t len) +{ + memset (buf, chr, len); + buf[len] = 0; + return buf; +} + + +/* * * * * * * * * * * * The daemon side * * * * * * * * * * * */ + +static enum MHD_Result +ahc_check (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **req_cls) +{ + static int marker; + const enum MHD_DigestAuthMultiAlgo3 *const challenge_algo = cls; + struct MHD_Response *response; + enum MHD_DigestAuthResult check_res; + enum MHD_Result ret; + (void) url; (void) method; (void) version; (void) upload_data; + (void) upload_data_size; + + if (&marker != *req_cls) + { /* The first call for the request */ + *req_cls = &marker; + return MHD_YES; + } + + check_res = + MHD_digest_auth_check3 (connection, + TEST_REALM, + TEST_USERNAME, + TEST_PASSWORD, + 300, + 0, + MHD_DIGEST_AUTH_MULT_QOP_ANY_NON_INT, + MHD_DIGEST_AUTH_MULT_ALGO3_ANY_NON_SESSION); + if (MHD_DAUTH_OK == check_res) + { /* None of the sub-cases must be authenticated */ + static const char page[] = "Authenticated"; + + wrongly_authenticated = 1; + response = + MHD_create_response_from_buffer (MHD_STATICSTR_LEN_ (page), + (void *) page, + MHD_RESPMEM_PERSISTENT); + if (NULL == response) + return MHD_NO; + ret = MHD_queue_response (connection, + MHD_HTTP_OK, + response); + MHD_destroy_response (response); + return ret; + } + + if (1) + { + static const char page[] = "Access denied"; + + response = + MHD_create_response_from_buffer (MHD_STATICSTR_LEN_ (page), + (void *) page, + MHD_RESPMEM_PERSISTENT); + if (NULL == response) + return MHD_NO; + ret = + MHD_queue_auth_required_response3 (connection, + TEST_REALM, + "test-opaque", + NULL, + response, + (MHD_DAUTH_NONCE_STALE == check_res) ? + MHD_YES : MHD_NO, + MHD_DIGEST_AUTH_MULT_QOP_ANY_NON_INT, + *challenge_algo, + MHD_YES, + MHD_NO); + MHD_destroy_response (response); + } + return ret; +} + + +/* * * * * * * * * * * * The raw client side * * * * * * * * * * * */ + +/** + * Send @a req to the daemon at #test_port and receive the complete reply. + * + * @param req the request to send, zero-terminated + * @param[out] rply the buffer for the reply + * @param rply_size the size of the @a rply buffer + * @return the number of the received bytes, + * -1 if no reply has been received + */ +static ssize_t +exchange (const char *req, + char *rply, + size_t rply_size) +{ + MHD_socket sckt; + struct sockaddr_in sa; + struct timeval tmo; + size_t req_len; + size_t sent; + size_t received; + + sckt = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (MHD_INVALID_SOCKET == sckt) + hard_error ("socket() failed"); + + tmo.tv_sec = SOCK_TIMEOUT_SEC; + tmo.tv_usec = 0; + if ( (0 != setsockopt (sckt, SOL_SOCKET, SO_RCVTIMEO, + (const void *) &tmo, sizeof (tmo))) || + (0 != setsockopt (sckt, SOL_SOCKET, SO_SNDTIMEO, + (const void *) &tmo, sizeof (tmo))) ) + hard_error ("setsockopt() failed"); + + memset (&sa, 0, sizeof (sa)); + sa.sin_family = AF_INET; + sa.sin_port = htons (test_port); + sa.sin_addr.s_addr = htonl (INADDR_LOOPBACK); + if (0 != connect (sckt, (const struct sockaddr *) &sa, sizeof (sa))) + { + MHD_socket_close_chk_ (sckt); + return -1; + } + + req_len = strlen (req); + sent = 0; + while (sent < req_len) + { + ssize_t res; + + res = send (sckt, req + sent, req_len - sent, 0); + if (0 >= res) + break; + sent += (size_t) res; + } + if (sent != req_len) + { + MHD_socket_close_chk_ (sckt); + return -1; + } + + received = 0; + while (received + 1 < rply_size) + { + ssize_t res; + + res = recv (sckt, rply + received, rply_size - received - 1, 0); + if (0 >= res) + break; + received += (size_t) res; + } + rply[received] = 0; + MHD_socket_close_chk_ (sckt); + if (0 == received) + return -1; + return (ssize_t) received; +} + + +/** + * Extract the HTTP status code from a raw reply. + * + * @param rply the reply, zero-terminated + * @return the status code, zero if the reply is not a valid HTTP reply + */ +static unsigned int +get_status (const char *rply) +{ + unsigned int status; + + if (MHD_STATICSTR_LEN_ ("HTTP/1.1 000") > strlen (rply)) + return 0; + if (0 != memcmp (rply, "HTTP/1.", MHD_STATICSTR_LEN_ ("HTTP/1."))) + return 0; + status = 0; + if (1 != sscanf (rply + MHD_STATICSTR_LEN_ ("HTTP/1.1 "), "%3u", &status)) + return 0; + return status; +} + + +/** + * Request a challenge from the daemon at #test_port and extract the "nonce" + * value from it. + * + * @param[out] nonce the buffer for the "nonce" value + * @param nonce_size the size of the @a nonce buffer + * @return non-zero on success, zero on failure + */ +static int +get_nonce (char *nonce, + size_t nonce_size) +{ + static const char nonce_tk[] = "nonce=\""; + static const char req[] = + "GET " TEST_URL " HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Connection: close\r\n" + "\r\n"; + char rply[RPLY_BUF_SIZE]; + const char *start; + const char *end; + size_t len; + + if (0 >= exchange (req, rply, sizeof (rply))) + return 0; + if (MHD_HTTP_UNAUTHORIZED != get_status (rply)) + return 0; + start = strstr (rply, nonce_tk); + if (NULL == start) + return 0; + start += MHD_STATICSTR_LEN_ (nonce_tk); + end = strchr (start, '"'); + if (NULL == end) + return 0; + len = (size_t) (end - start); + if (len + 1 > nonce_size) + return 0; + memcpy (nonce, start, len); + nonce[len] = 0; + return 0 != len; +} + + +/** + * Obtain a "nonce" or fail the whole test. + * + * @param[out] nonce the buffer for the "nonce" value + * @param nonce_size the size of the @a nonce buffer + */ +static void +need_nonce (char *nonce, + size_t nonce_size) +{ + if (! get_nonce (nonce, nonce_size)) + hard_error ("cannot obtain a 'nonce' from the daemon"); +} + + +/** + * Run a single sub-case: send a request with the given "Authorization:" + * header value and check that the daemon replies with a regular HTTP error. + * + * @param name the name of the sub-case, used for reporting + * @param auth_fmt the printf-style format of the "Authorization:" header + * value, NULL to send no "Authorization:" header + * @param ... the arguments for the @a auth_fmt + */ +static void +run_case (const char *name, + const char *auth_fmt, + ...) +{ + static char req[REQ_BUF_SIZE]; + static char auth[REQ_BUF_SIZE - 512]; + char rply[RPLY_BUF_SIZE]; + unsigned int status; + + ++case_num; + if ( (NULL != case_filter) && + (NULL == strstr (name, case_filter)) ) + return; + + /* Report the sub-case before it is executed: if the daemon aborts the + process, the last reported sub-case is the failing one. */ + fprintf (stderr, + "[%02u] %s\n", + case_num, + name); + fflush (stderr); + + if (NULL != auth_fmt) + { + va_list ap; + + va_start (ap, auth_fmt); + vsnprintf (auth, sizeof (auth), auth_fmt, ap); + va_end (ap); + snprintf (req, sizeof (req), + "GET " TEST_URL " HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Connection: close\r\n" + "Authorization: %s\r\n" + "\r\n", + auth); + } + else + snprintf (req, sizeof (req), + "GET " TEST_URL " HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Connection: close\r\n" + "\r\n"); + + if (0 >= exchange (req, rply, sizeof (rply))) + { + fprintf (stderr, + "FAILED sub-case '%s': no reply received.\n", + name); + fflush (stderr); + ++failures; + return; + } + status = get_status (rply); + /* Any regular HTTP error reply is fine: the point of the test is that the + daemon neither crashes nor accepts the malformed credentials. */ + if ( (MHD_HTTP_UNAUTHORIZED != status) && + (MHD_HTTP_BAD_REQUEST != status) && + (MHD_HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE != status) ) + { + fprintf (stderr, + "FAILED sub-case '%s': unexpected HTTP status %u.\n", + name, + status); + fflush (stderr); + ++failures; + return; + } + if (0 != wrongly_authenticated) + { + fprintf (stderr, + "FAILED sub-case '%s': the malformed request was " + "authenticated.\n", + name); + fflush (stderr); + wrongly_authenticated = 0; + ++failures; + } +} + + +/** + * Build the common part of an otherwise valid Digest "Authorization:" header. + * + * @param[out] buf the buffer to fill + * @param buf_size the size of the @a buf + * @param nonce the "nonce" value received from the daemon + */ +static void +make_valid_params (char *buf, + size_t buf_size, + const char *nonce) +{ + snprintf (buf, buf_size, + "username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "nonce=\"%s\", uri=\"" TEST_URL "\", qop=auth, nc=%08x, " + "cnonce=\"0123456789abcdef\"", + nonce, + ++nc_counter); +} + + +/* * * * * * * * * * * * The sub-cases * * * * * * * * * * * */ + +/** + * Sub-cases for the missing #MHD_DIGEST_AUTH_ALGO3_INVALID check. + * + * An unknown "algorithm=" token is parsed to + * #MHD_DIGEST_AUTH_ALGO3_INVALID, which has the numeric value of zero and + * therefore satisfies the "allowed algorithms" bit-mask test. Without the + * explicit check for the invalid value the request reaches + * digest_init_one_time() and aborts the process with MHD_PANIC(). + * + * The algorithm is checked before anything else, so most of the sub-cases + * need no other parameter at all. + */ +static void +test_invalid_algorithm (void) +{ + char nonce[256]; + char params[512]; + char resp[128]; + + fill_hex (resp, 64); + + test_port = port_any; + + run_case ("bug-c/algo-bogus-alone", + "Digest algorithm=BOGUS"); + run_case ("bug-c/algo-md5-x", + "Digest algorithm=MD5-x"); + run_case ("bug-c/algo-sha512", + "Digest algorithm=SHA-512"); + run_case ("bug-c/algo-sha1", + "Digest algorithm=SHA1"); + run_case ("bug-c/algo-sha256-typo", + "Digest algorithm=SHA256"); + run_case ("bug-c/algo-empty-quotes", + "Digest algorithm=\"\""); + run_case ("bug-c/algo-empty-token", + "Digest algorithm="); + run_case ("bug-c/algo-empty-token-comma", + "Digest algorithm=, nonce=\"deadbeef\""); + run_case ("bug-c/algo-quoted-bogus", + "Digest algorithm=\"BOGUS\""); + run_case ("bug-c/algo-space", + "Digest algorithm = BOGUS"); + /* A backslash inside the value marks the parameter as "quoted", which + selects the second, separate branch of the algorithm token parser. */ + run_case ("bug-c/algo-escaped-bogus", + "Digest algorithm=\"\\BOGUS\""); + run_case ("bug-c/algo-escaped-md5-x", + "Digest algorithm=\"MD5\\-x\""); + /* The "-sess" variants are valid tokens, but they are rejected by the + bit-mask test as they are not allowed by the application. */ + run_case ("bug-c/algo-md5-sess", + "Digest algorithm=MD5-sess"); + run_case ("bug-c/algo-sha256-sess", + "Digest algorithm=SHA-256-sess"); + run_case ("bug-c/algo-sha512-256-sess", + "Digest algorithm=SHA-512-256-sess"); + run_case ("bug-c/algo-escaped-md5-sess", + "Digest algorithm=\"MD5\\-sess\""); + run_case ("bug-c/algo-escaped-sha512-256", + "Digest algorithm=\"SHA\\-512-256\""); + /* The last occurrence of a duplicated parameter is the effective one. */ + run_case ("bug-c/algo-duplicated", + "Digest algorithm=MD5, algorithm=BOGUS"); + run_case ("bug-c/algo-duplicated-rev", + "Digest algorithm=BOGUS, algorithm=MD5"); + + /* The same, but with all the other parameters present and valid, so that + nothing else can short-circuit the check. */ + test_port = port_sha256; + + need_nonce (nonce, sizeof (nonce)); + make_valid_params (params, sizeof (params), nonce); + run_case ("bug-c/algo-bogus-full", + "Digest %s, algorithm=BOGUS, response=\"%s\"", + params, + resp); + + need_nonce (nonce, sizeof (nonce)); + make_valid_params (params, sizeof (params), nonce); + run_case ("bug-c/algo-empty-full", + "Digest %s, algorithm=\"\", response=\"%s\"", + params, + resp); + + need_nonce (nonce, sizeof (nonce)); + make_valid_params (params, sizeof (params), nonce); + run_case ("bug-c/algo-escaped-bogus-full", + "Digest %s, algorithm=\"\\BOGUS\", response=\"%s\"", + params, + resp); +} + + +/** + * Sub-cases for the out-of-bounds stack write in the "response=" hex decoder. + * + * The decoder writes (len + 1) / 2 bytes into a #MAX_DIGEST (32) bytes long + * stack buffer, while the length sanity check accepts up to digest_size * 4 + * (128) characters. All the other parameters must be valid to reach the + * decoder, therefore a fresh "nonce" is obtained for every sub-case. + */ +static void +test_overlong_response (void) +{ + /* 128 and 127 characters produce the maximum overflow of 32 bytes, 66 and + 65 characters produce the minimum overflow of a single byte, 64 + characters fit exactly and must not overflow. */ + static const size_t resp_lengths[] = { 128, 127, 100, 66, 65, 64 }; + char nonce[256]; + char params[512]; + char resp[512]; + char name[128]; + size_t i; + + test_port = port_sha256; + + for (i = 0; i < sizeof (resp_lengths) / sizeof (resp_lengths[0]); ++i) + { + const size_t len = resp_lengths[i]; + + /* The value without the quotation marks */ + need_nonce (nonce, sizeof (nonce)); + make_valid_params (params, sizeof (params), nonce); + snprintf (name, sizeof (name), + "bug-d/sha256-response-%u-bare", + (unsigned) len); + run_case (name, + "Digest %s, algorithm=SHA-256, response=%s", + params, + fill_hex (resp, len)); + + /* The same value in the quotation marks */ + need_nonce (nonce, sizeof (nonce)); + make_valid_params (params, sizeof (params), nonce); + snprintf (name, sizeof (name), + "bug-d/sha256-response-%u-quoted", + (unsigned) len); + run_case (name, + "Digest %s, algorithm=SHA-256, response=\"%s\"", + params, + fill_hex (resp, len)); + } + + /* A backslash marks the value as "quoted", so it is unquoted into the + temporal buffer first and only then decoded. */ + need_nonce (nonce, sizeof (nonce)); + make_valid_params (params, sizeof (params), nonce); + run_case ("bug-d/sha256-response-127-escaped", + "Digest %s, algorithm=SHA-256, response=\"\\%s\"", + params, + fill_hex (resp, 127)); + + /* The SHA-512/256 algorithm uses the same digest size as SHA-256 and the + same nonce length, so it reaches the decoder as well. */ +#ifdef MHD_SHA512_256_SUPPORT + need_nonce (nonce, sizeof (nonce)); + make_valid_params (params, sizeof (params), nonce); + run_case ("bug-d/sha512-256-response-128-bare", + "Digest %s, algorithm=SHA-512-256, response=%s", + params, + fill_hex (resp, 128)); +#endif /* MHD_SHA512_256_SUPPORT */ + + /* The RFC2069 mode: no "qop", no "nc" and no "cnonce". */ + need_nonce (nonce, sizeof (nonce)); + run_case ("bug-d/sha256-response-128-rfc2069", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "nonce=\"%s\", uri=\"" TEST_URL "\", algorithm=SHA-256, " + "response=%s", + nonce, + fill_hex (resp, 128)); + + /* The value is not a valid hexadecimal string, the decoder must bail out + before writing anything. */ + need_nonce (nonce, sizeof (nonce)); + make_valid_params (params, sizeof (params), nonce); + run_case ("bug-d/sha256-response-128-non-hex", + "Digest %s, algorithm=SHA-256, response=%s", + params, + fill_chr (resp, 'z', 128)); + + /* Only the last character is not a hexadecimal digit, so the decoder + writes 63 bytes before it detects the error. */ + need_nonce (nonce, sizeof (nonce)); + make_valid_params (params, sizeof (params), nonce); + fill_hex (resp, 128); + resp[127] = 'z'; + run_case ("bug-d/sha256-response-128-tail-non-hex", + "Digest %s, algorithm=SHA-256, response=%s", + params, + resp); + + /* Duplicated parameter: the last (over-long) value is the effective one. */ + need_nonce (nonce, sizeof (nonce)); + make_valid_params (params, sizeof (params), nonce); + run_case ("bug-d/sha256-response-duplicated", + "Digest %s, algorithm=SHA-256, response=\"00112233\", response=%s", + params, + fill_hex (resp, 128)); + +#ifdef MHD_MD5_SUPPORT + /* The MD5 variant: the decoder writes 32 bytes, which overflows the buffer + only in an MD5-only build, where MAX_DIGEST is 16. */ + test_port = port_any; + + need_nonce (nonce, sizeof (nonce)); + make_valid_params (params, sizeof (params), nonce); + run_case ("bug-d/md5-response-64-bare", + "Digest %s, algorithm=MD5, response=%s", + params, + fill_hex (resp, 64)); + + need_nonce (nonce, sizeof (nonce)); + make_valid_params (params, sizeof (params), nonce); + run_case ("bug-d/md5-response-63-bare", + "Digest %s, algorithm=MD5, response=%s", + params, + fill_hex (resp, 63)); + + need_nonce (nonce, sizeof (nonce)); + make_valid_params (params, sizeof (params), nonce); + run_case ("bug-d/md5-response-34-quoted", + "Digest %s, algorithm=MD5, response=\"%s\"", + params, + fill_hex (resp, 34)); +#endif /* MHD_MD5_SUPPORT */ +} + + +/** + * Sub-cases for the adjacent malformed, empty, over-long and duplicated + * parameters. + */ +static void +test_adjacent (void) +{ + static char big[LONG_PARAM_LEN + 1]; + char nonce[256]; + char resp[128]; + + fill_hex (resp, 64); + + /* No or unusable "Authorization:" header. These never reach the + "nonce" handling, the daemon used does not matter. */ + test_port = port_any; + + run_case ("adj/no-auth-header", NULL); + run_case ("adj/scheme-only", "Digest"); + run_case ("adj/scheme-and-space", "Digest "); + run_case ("adj/only-commas", "Digest ,,,,"); + run_case ("adj/basic-scheme", "Basic dGVzdHVzZXI6dGVzdHBhc3M="); + run_case ("adj/unknown-scheme", "Bogus xyz"); + run_case ("adj/unterminated-quote", + "Digest username=\"" TEST_USERNAME); + run_case ("adj/unterminated-quote-escape", + "Digest username=\"" TEST_USERNAME "\\\""); + run_case ("adj/no-equal-sign", "Digest username"); + run_case ("adj/leading-equal-sign", "Digest =value"); + run_case ("adj/semicolon-in-value", "Digest username=a;b"); + run_case ("adj/garbage-after-value", "Digest username=\"a\" garbage"); + run_case ("adj/unknown-parameter", + "Digest bogus=\"x\", username=\"" TEST_USERNAME "\""); + + /* Over-long parameter values */ + run_case ("adj/username-too-long", + "Digest username=\"%s\", realm=\"" TEST_REALM "\"", + fill_chr (big, 'u', LONG_PARAM_LEN)); + run_case ("adj/realm-too-long", + "Digest username=\"" TEST_USERNAME "\", realm=\"%s\"", + fill_chr (big, 'r', LONG_PARAM_LEN)); + run_case ("adj/uri-too-long", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "uri=\"%s\"", + fill_chr (big, 'p', LONG_PARAM_LEN)); + run_case ("adj/cnonce-too-long", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "qop=auth, nc=00000001, cnonce=\"%s\"", + fill_chr (big, 'c', LONG_PARAM_LEN)); + run_case ("adj/nc-too-long", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "qop=auth, nc=%s, cnonce=\"abc\"", + fill_hex (big, LONG_PARAM_LEN)); + run_case ("adj/opaque-too-long", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "opaque=\"%s\"", + fill_chr (big, 'o', LONG_PARAM_LEN)); + run_case ("adj/nonce-too-long", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "nonce=\"%s\"", + fill_hex (big, LONG_PARAM_LEN)); + run_case ("adj/response-too-long", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "response=\"%s\"", + fill_hex (big, LONG_PARAM_LEN)); + run_case ("adj/username-ext-too-long", + "Digest username*=UTF-8''%s, realm=\"" TEST_REALM "\"", + fill_chr (big, 'e', LONG_PARAM_LEN)); + + /* The extended ("username*") notation */ + run_case ("adj/username-ext-truncated", + "Digest username*=U, realm=\"" TEST_REALM "\""); + run_case ("adj/username-ext-no-lang", + "Digest username*=UTF-8', realm=\"" TEST_REALM "\""); + run_case ("adj/username-ext-empty", + "Digest username*=UTF-8'', realm=\"" TEST_REALM "\""); + run_case ("adj/username-ext-broken-pct", + "Digest username*=UTF-8''%%, realm=\"" TEST_REALM "\""); + run_case ("adj/username-ext-broken-pct2", + "Digest username*=UTF-8''%%z, realm=\"" TEST_REALM "\""); + run_case ("adj/username-ext-broken-pct3", + "Digest username*=UTF-8''%%zz, realm=\"" TEST_REALM "\""); + run_case ("adj/username-ext-trailing-pct", + "Digest username*=UTF-8''abc%%, realm=\"" TEST_REALM "\""); + run_case ("adj/username-ext-bad-charset", + "Digest username*=ISO-8859-1''abc, realm=\"" TEST_REALM "\""); + run_case ("adj/username-ext-quoted", + "Digest username*=\"UTF-8''\\abc\", realm=\"" TEST_REALM "\""); + run_case ("adj/username-ext-and-plain", + "Digest username=\"" TEST_USERNAME "\", " + "username*=UTF-8''" TEST_USERNAME ", realm=\"" TEST_REALM "\""); + + /* The "userhash" handling */ + run_case ("adj/userhash-username-too-short", + "Digest userhash=true, username=\"abcd\", " + "realm=\"" TEST_REALM "\""); + run_case ("adj/userhash-username-empty", + "Digest userhash=true, username=\"\", " + "realm=\"" TEST_REALM "\""); + run_case ("adj/userhash-username-too-long", + "Digest userhash=true, username=\"%s\", " + "realm=\"" TEST_REALM "\"", + fill_hex (big, 200)); + run_case ("adj/userhash-username-ext", + "Digest userhash=true, username*=UTF-8''abc, " + "realm=\"" TEST_REALM "\""); + run_case ("adj/userhash-no-username", + "Digest userhash=true, realm=\"" TEST_REALM "\""); + + /* The "nc" handling */ + run_case ("adj/nc-zero", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "qop=auth, nc=00000000, cnonce=\"abc\""); + run_case ("adj/nc-non-hex", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "qop=auth, nc=zzzzzzzz, cnonce=\"abc\""); + run_case ("adj/nc-empty", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "qop=auth, nc=, cnonce=\"abc\""); + run_case ("adj/nc-overflow", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "qop=auth, nc=ffffffffffffffffffffffff, cnonce=\"abc\""); + run_case ("adj/qop-auth-no-nc", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "qop=auth, cnonce=\"abc\""); + run_case ("adj/qop-auth-no-cnonce", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "qop=auth, nc=00000001"); + run_case ("adj/cnonce-empty", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "qop=auth, nc=00000001, cnonce=\"\""); + + /* Malformed "nonce" values. These are rejected by the length check + before the "nonce"/"nc" map is consulted. */ + run_case ("adj/nonce-short", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "nonce=\"deadbeef\", uri=\"" TEST_URL "\", qop=auth, " + "nc=00000001, cnonce=\"abc\", response=\"00112233\""); + run_case ("adj/nonce-empty", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "nonce=\"\", uri=\"" TEST_URL "\", qop=auth, " + "nc=00000001, cnonce=\"abc\", response=\"00112233\""); + + /* Duplicated parameters */ + run_case ("adj/duplicated-username", + "Digest username=\"a\", username=\"" TEST_USERNAME "\", " + "realm=\"" TEST_REALM "\""); + run_case ("adj/duplicated-realm", + "Digest username=\"" TEST_USERNAME "\", realm=\"x\", " + "realm=\"" TEST_REALM "\""); + run_case ("adj/duplicated-userhash", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "userhash=false, userhash=true"); + + /* The sub-cases below use the SHA-256 algorithm and therefore need the + daemon that issues the SHA-256 (76 characters long) nonces. */ + test_port = port_sha256; + + run_case ("adj/nonce-not-generated-by-mhd", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "nonce=\"%s\", uri=\"" TEST_URL "\", algorithm=SHA-256, " + "qop=auth, nc=00000001, cnonce=\"abc\", response=\"%s\"", + fill_hex (big, 76), + resp); + + need_nonce (nonce, sizeof (nonce)); + run_case ("adj/uri-mismatch", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "nonce=\"%s\", uri=\"/other\", algorithm=SHA-256, qop=auth, " + "nc=%08x, cnonce=\"abc\", response=\"%s\"", + nonce, + ++nc_counter, + resp); + + need_nonce (nonce, sizeof (nonce)); + run_case ("adj/uri-empty", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "nonce=\"%s\", uri=\"\", algorithm=SHA-256, qop=auth, " + "nc=%08x, cnonce=\"abc\", response=\"%s\"", + nonce, + ++nc_counter, + resp); + + need_nonce (nonce, sizeof (nonce)); + run_case ("adj/uri-with-args", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "nonce=\"%s\", uri=\"" TEST_URL "?a=b\", algorithm=SHA-256, " + "qop=auth, nc=%08x, cnonce=\"abc\", response=\"%s\"", + nonce, + ++nc_counter, + resp); + + need_nonce (nonce, sizeof (nonce)); + run_case ("adj/duplicated-nonce", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "nonce=\"deadbeef\", nonce=\"%s\", uri=\"" TEST_URL "\", " + "algorithm=SHA-256, qop=auth, nc=%08x, cnonce=\"abc\", " + "response=\"%s\"", + nonce, + ++nc_counter, + resp); + + need_nonce (nonce, sizeof (nonce)); + run_case ("adj/qop-auth-int", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "nonce=\"%s\", uri=\"" TEST_URL "\", algorithm=SHA-256, " + "qop=auth-int, nc=%08x, cnonce=\"abc\", response=\"%s\"", + nonce, + ++nc_counter, + resp); + + need_nonce (nonce, sizeof (nonce)); + run_case ("adj/qop-bogus", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "nonce=\"%s\", uri=\"" TEST_URL "\", algorithm=SHA-256, " + "qop=BOGUS, nc=%08x, cnonce=\"abc\", response=\"%s\"", + nonce, + ++nc_counter, + resp); + + need_nonce (nonce, sizeof (nonce)); + run_case ("adj/userhash-64-non-hex", + "Digest userhash=true, username=\"%s\", realm=\"" TEST_REALM "\", " + "nonce=\"%s\", uri=\"" TEST_URL "\", algorithm=SHA-256, qop=auth, " + "nc=%08x, cnonce=\"abc\", response=\"%s\"", + fill_chr (big, 'z', 64), + nonce, + ++nc_counter, + resp); + + need_nonce (nonce, sizeof (nonce)); + run_case ("adj/userhash-128-hex", + "Digest userhash=true, username=\"%s\", realm=\"" TEST_REALM "\", " + "nonce=\"%s\", uri=\"" TEST_URL "\", algorithm=SHA-256, qop=auth, " + "nc=%08x, cnonce=\"abc\", response=\"%s\"", + fill_hex (big, 128), + nonce, + ++nc_counter, + resp); + + /* An empty "qop" value ("qop=" or "qop=\"\"") is parsed to + #MHD_DIGEST_AUTH_QOP_INVALID, whose numeric value is zero, so it used to + pass the "allowed QOP" bit-mask test in exactly the same way an unknown + "algorithm" token did before commit bd49ce93, and the empty value then + reached get_unquoted_param(), which asserts that the value is not empty. + Fixed in commit 3b898eae; these two sub-cases are its regression test. */ + need_nonce (nonce, sizeof (nonce)); + run_case ("adj/qop-empty", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "nonce=\"%s\", uri=\"" TEST_URL "\", algorithm=SHA-256, qop=, " + "nc=%08x, cnonce=\"abc\", response=\"%s\"", + nonce, + ++nc_counter, + resp); + + need_nonce (nonce, sizeof (nonce)); + run_case ("adj/qop-empty-quotes", + "Digest username=\"" TEST_USERNAME "\", realm=\"" TEST_REALM "\", " + "nonce=\"%s\", uri=\"" TEST_URL "\", algorithm=SHA-256, qop=\"\", " + "nc=%08x, cnonce=\"abc\", response=\"%s\"", + nonce, + ++nc_counter, + resp); +} + + +/** + * Start a test daemon. + * + * @param challenge_algo the pointer to the algorithm used for the challenges + * @param[out] pport the pointer to store the port the daemon listens on + * @return the daemon + */ +static struct MHD_Daemon * +start_test_daemon (const enum MHD_DigestAuthMultiAlgo3 *challenge_algo, + uint16_t *pport) +{ + struct MHD_Daemon *daemon; + const union MHD_DaemonInfo *dinfo; + struct MHD_OptMatrixProfile prof; + struct MHD_OptionItem ops[8]; + unsigned int flags; + + if (NULL != test_prof) + prof = *test_prof; + else + { + memset (&prof, 0, sizeof (prof)); + prof.name = "built-in"; + prof.threading = MHD_OPT_MATRIX_THR_INTERNAL; + prof.poll_backend = MHD_OPT_MATRIX_POLL_SELECT; + } + if (0 == mhd_opt_matrix_fill_options (&prof, ops, + (unsigned int) (sizeof (ops) + / sizeof (ops[0])))) + hard_error ("The daemon option array is too small"); + /* The client of this test is a plain blocking socket client, so external + polling is served with an internal polling thread instead. */ + flags = mhd_opt_matrix_daemon_flags (&prof, MHD_USE_ERROR_LOG, 0); + + daemon = MHD_start_daemon (flags, + 0, + NULL, NULL, + &ahc_check, (void *) challenge_algo, + MHD_OPTION_ARRAY, ops, + MHD_OPTION_CONNECTION_TIMEOUT, + (unsigned int) 10, + MHD_OPTION_NONCE_NC_SIZE, + (unsigned int) NONCE_NC_SIZE, + MHD_OPTION_END); + if (NULL == daemon) + hard_error ("MHD_start_daemon() failed"); + dinfo = MHD_get_daemon_info (daemon, MHD_DAEMON_INFO_BIND_PORT); + if ( (NULL == dinfo) || (0 == dinfo->port) ) + { + MHD_stop_daemon (daemon); + hard_error ("MHD_get_daemon_info() failed"); + } + *pport = dinfo->port; + return daemon; +} + + +int +main (int argc, char *const *argv) +{ + static const enum MHD_DigestAuthMultiAlgo3 algo_sha256 = + MHD_DIGEST_AUTH_MULT_ALGO3_SHA256; + static const enum MHD_DigestAuthMultiAlgo3 algo_any = + MHD_DIGEST_AUTH_MULT_ALGO3_ANY_NON_SESSION; + struct MHD_Daemon *daemon_sha256; + struct MHD_Daemon *daemon_any; + char nonce[256]; + + if (1 < argc) + case_filter = argv[1]; + + mhd_opt_matrix_print_notice ("test_dauth_malformed"); + test_prof = mhd_opt_matrix_from_env (); + if (NULL != test_prof) + { + char desc[256]; + + test_prof_copy = *test_prof; + test_prof = &test_prof_copy; + if (! mhd_opt_matrix_profile_supported (test_prof)) + { + printf ("test_dauth_malformed: the selected profile %s is not " + "supported by this build, the test is skipped.\n", + mhd_opt_matrix_describe (test_prof, desc, sizeof (desc))); + return 77; + } + if (mhd_opt_matrix_raise_mem_limit (&test_prof_copy, MIN_POOL_FOR_TEST)) + printf ("test_dauth_malformed: the connection memory limit of the " + "profile was raised to %u, the smallest pool this test can " + "work with.\n", (unsigned) MIN_POOL_FOR_TEST); + } + + daemon_sha256 = start_test_daemon (&algo_sha256, &port_sha256); + daemon_any = start_test_daemon (&algo_any, &port_any); + + test_invalid_algorithm (); + test_overlong_response (); + test_adjacent (); + + /* Both daemons must still be alive and functional. */ + test_port = port_sha256; + if (! get_nonce (nonce, sizeof (nonce))) + { + fprintf (stderr, + "FAILED: the SHA-256 daemon does not respond after the test.\n"); + ++failures; + } + test_port = port_any; + if (! get_nonce (nonce, sizeof (nonce))) + { + fprintf (stderr, + "FAILED: the default daemon does not respond after the test.\n"); + ++failures; + } + + MHD_stop_daemon (daemon_any); + MHD_stop_daemon (daemon_sha256); + + if (0 != failures) + { + fprintf (stderr, + "Test FAILED: %u of %u sub-cases failed.\n", + failures, + case_num); + return 1; + } + printf ("Test PASSED: %u sub-cases.\n", + case_num); + return 0; +} + + +#else /* ! DAUTH_SUPPORT || ! MHD_SHA256_SUPPORT */ + + +int +main (void) +{ + fprintf (stderr, + "Digest Auth or SHA-256 support is not enabled in this MHD build, " + "the test is skipped.\n"); + return 77; +} + + +#endif /* ! DAUTH_SUPPORT || ! MHD_SHA256_SUPPORT */ diff --git a/src/microhttpd/test_dauth_userdigest.c b/src/microhttpd/test_dauth_userdigest.c @@ -30,6 +30,10 @@ #include "microhttpd.h" #include "test_helpers.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_HTTPS_REQUIRE_GCRYPT) && \ (defined(MHD_SHA256_TLSLIB) || defined(MHD_MD5_TLSLIB)) #define NEED_GCRYP_INIT 1 diff --git a/src/microhttpd/test_dauth_userhash.c b/src/microhttpd/test_dauth_userhash.c @@ -30,6 +30,10 @@ #include "microhttpd.h" #include "test_helpers.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_HTTPS_REQUIRE_GCRYPT) && \ (defined(MHD_SHA256_TLSLIB) || defined(MHD_MD5_TLSLIB)) #define NEED_GCRYP_INIT 1 diff --git a/src/microhttpd/test_http_reasons.c b/src/microhttpd/test_http_reasons.c @@ -29,6 +29,10 @@ #include "microhttpd.h" #include "mhd_str.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + static const char *const r_unknown = "unknown"; /* Return zero when no error is detected */ diff --git a/src/microhttpd/test_md5.c b/src/microhttpd/test_md5.c @@ -29,6 +29,10 @@ #include <stdio.h> #include <stdlib.h> +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_MD5_TLSLIB) && defined(MHD_HTTPS_REQUIRE_GCRYPT) #define NEED_GCRYP_INIT 1 #include <gcrypt.h> diff --git a/src/microhttpd/test_mhd_version.c b/src/microhttpd/test_mhd_version.c @@ -37,6 +37,10 @@ #endif /* HAVE_STDLIB_H */ #include "microhttpd.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifdef PACKAGE_VERSION static const char str_macro_pkg_ver[] = PACKAGE_VERSION; diff --git a/src/microhttpd/test_option_matrix.c b/src/microhttpd/test_option_matrix.c @@ -0,0 +1,1575 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + libmicrohttpd is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 2, or (at your + option) any later version. + + libmicrohttpd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libmicrohttpd; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +/** + * @file test_option_matrix.c + * @brief A battery of raw HTTP requests replayed against every supported + * point of the daemon option matrix. + * @author Christian Grothoff + * + * ## Why this test exists + * + * Every other test in this directory pins one daemon configuration, so the + * suite as a whole only ever visits a single point of a fairly large + * configuration space: connection memory limit x client discipline level x + * threading mode x polling backend. Yet the code that a request travels + * through depends on all four: a small connection pool unlocks the read + * buffer "shift back" and buffer-grow paths, the discipline level switches + * whole families of parser leniency, and the threading/polling mode decides + * which of the four event loops re-enters the parser. + * + * This test walks the matrix defined in mhd_opt_matrix.c and replays the same + * small battery of hand-written raw requests against every point of it. The + * requests are sent over raw sockets because several of them (chunk + * extensions, pipelining, a whitespace-prefixed header line, an early close + * in the middle of a request) cannot be produced with libcurl. + * + * Every failure names the profile it came from, so a report is directly + * actionable. + * + * ## Command line + * + * * `-v` - report every profile and every scenario; + * * `-v -v` - additionally print the MHD error log; + * * `--profile=N` - restrict the run to one profile (name or index), the + * same as MHD_TEST_PROFILE=N in the environment; + * * `--list-profiles` - print one profile name per line and exit; this is + * what contrib/run-option-matrix.sh uses, so that the + * matrix is defined in exactly one place; + * * `--scenario=NAME` - restrict the run to one scenario. Useful to walk + * the whole battery of a profile that aborts the process + * in the middle of it. + * + * Exit codes: 0 = pass, 77 = skip, 99 = hard setup error, 1 = test failure. + */ +#include "MHD_config.h" +#include "platform.h" +#include <microhttpd.h> +#include <stdlib.h> +#include <string.h> +#include <stdarg.h> +#include <time.h> +#include <stdint.h> +#include <errno.h> + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN 1 +#endif /* !WIN32_LEAN_AND_MEAN */ +#include <windows.h> +#endif + +#ifndef WINDOWS +#include <unistd.h> +#include <sys/socket.h> +#endif + +#ifdef HAVE_SIGNAL_H +#include <signal.h> +#endif /* HAVE_SIGNAL_H */ + +#include <stdio.h> + +#include "mhd_sockets.h" /* only macros used */ +#include "mhd_opt_matrix.h" + +#ifndef MHD_STATICSTR_LEN_ +/** + * Determine length of static string / macro strings at compile time. + */ +#define MHD_STATICSTR_LEN_(macro) (sizeof(macro) / sizeof(char) - 1) +#endif /* ! MHD_STATICSTR_LEN_ */ + +/** + * Convenience shorthand used all over the scenario table. + */ +#define CRLF "\r\n" + +/** + * The Host header used by (almost) every scenario. + */ +#define H_LOCAL "Host: localhost" CRLF + +/** + * Size of the textual observation buffer. + */ +#define OBS_SIZE 4096 + +/** + * Size of the client receive buffer. + */ +#define RX_SIZE 8192 + +/** + * Hard upper bound (ms) for reading the complete server answer. + */ +#define READ_DEADLINE_MS 700 + +/** + * Silence (ms) accepted as "the server is done and keeps the connection + * alive". + */ +#define READ_GRACE_MS 10 + +/** + * Milliseconds to wait for the server side to finish tearing down the + * connection, so that the observations become stable. + */ +#define CLOSE_WAIT_MS 1500 + +/** + * Milliseconds the daemon is left running after a scenario that closes the + * connection without reading an answer, so that a wrongly parsed request + * still has a chance to show up. + */ +#define SETTLE_MS 60 + +/** + * The connection timeout of the test daemons, in seconds. + */ +#define DAEMON_TIMEOUT 8 + +/** + * Give up after that many reported failures. + */ +#define MAX_FAILURES 12 + + +/* ------------------------------------------------------------------ */ +/* Error exits */ +/* ------------------------------------------------------------------ */ + +/* Forward declaration, needed by the error exit helpers. */ +static void +print_context (FILE *out); + + +_MHD_NORETURN static void +external_error_exit (const char *desc, int line) +{ + fprintf (stderr, + "%s at line %d.\nLast errno value: %d (%s)\n", + (NULL != desc) ? desc : "System or external library call failed", + line, + (int) errno, + strerror (errno)); + print_context (stderr); + fflush (stderr); + exit (99); +} + + +#define hard_error(desc) external_error_exit (desc, __LINE__) + + +/** + * Pause execution for the given number of milliseconds. + * + * @param ms the number of milliseconds to sleep + */ +static void +sleep_ms (unsigned int ms) +{ +#if defined(_WIN32) + Sleep (ms); +#elif defined(HAVE_NANOSLEEP) + struct timespec slp = {(time_t) (ms / 1000), (long) ((ms % 1000) * 1000000)}; + struct timespec rmn; + int num_retries = 0; + + while (0 != nanosleep (&slp, &rmn)) + { + if (EINTR != errno) + hard_error ("nanosleep() failed"); + if (num_retries++ > 8) + break; + slp = rmn; + } +#elif defined(HAVE_USLEEP) + usleep ((useconds_t) (ms * 1000)); +#else + hard_error ("No sleep function available on this system"); +#endif +} + + +/** + * Monotonic-ish millisecond clock. + */ +static uint64_t +now_ms (void) +{ +#if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC) + struct timespec ts; + + if (0 == clock_gettime (CLOCK_MONOTONIC, &ts)) + return ((uint64_t) ts.tv_sec) * 1000 + (uint64_t) (ts.tv_nsec / 1000000); +#endif /* HAVE_CLOCK_GETTIME && CLOCK_MONOTONIC */ + return ((uint64_t) time (NULL)) * 1000; +} + + +/* ------------------------------------------------------------------ */ +/* Textual observation buffer */ +/* ------------------------------------------------------------------ */ + +/** + * A bounded, always NUL-terminated text accumulator. Both the expected and + * the actual observation are rendered into such a buffer, so that a single + * strcmp() performs the whole check and also yields readable diagnostics. + */ +struct obs_buf +{ + char d[OBS_SIZE]; + size_t len; + int overflow; +}; + + +static void +obs_reset (struct obs_buf *b) +{ + b->len = 0; + b->overflow = 0; + b->d[0] = 0; +} + + +static void +obs_addf (struct obs_buf *b, const char *fmt, ...) +{ + va_list ap; + int r; + size_t space; + + if (b->len >= sizeof(b->d) - 1) + { + b->overflow = 1; + return; + } + space = sizeof(b->d) - b->len; + va_start (ap, fmt); + r = vsnprintf (b->d + b->len, space, fmt, ap); + va_end (ap); + if (0 > r) + { + b->overflow = 1; + return; + } + if ((size_t) r >= space) + { + b->len = sizeof(b->d) - 1; + b->overflow = 1; + return; + } + b->len += (size_t) r; +} + + +/** + * Append @a data with all non-printable bytes escaped as \\xNN. + */ +static void +obs_add_escaped (struct obs_buf *b, const char *data, size_t size) +{ + size_t i; + + for (i = 0; i < size; ++i) + { + const unsigned char c = (unsigned char) data[i]; + + if ((0x20 <= c) && (0x7e >= c) && ('\\' != c)) + obs_addf (b, "%c", (char) c); + else if ('\\' == c) + obs_addf (b, "\\\\"); + else + obs_addf (b, "\\x%02x", (unsigned int) c); + } +} + + +/* ------------------------------------------------------------------ */ +/* The scenario battery */ +/* ------------------------------------------------------------------ */ + +/** + * One request scenario. + * + * The expectation @e exp is the concatenation of one line per request that + * MHD must parse, in the format + * + * "METHOD URL [BODY] {ARG=VAL,ARG=VAL}\n" + * + * with every byte outside the printable ASCII range escaped as \\xNN and a + * query argument without a value rendered as "ARG=<NULL>". + */ +struct scenario +{ + /** + * Short identifier, printed on failure. + */ + const char *name; + + /** + * One line saying which behaviour this scenario pins down. + */ + const char *desc; + + /** + * The raw request byte stream (may hold several pipelined requests). + */ + const char *raw; + + /** + * The number of complete requests MHD must parse out of @e raw. + */ + unsigned int n_req; + + /** + * The number of complete responses the client must see. + */ + unsigned int n_resp; + + /** + * The expected status code of the first response, zero for "any". + */ + int status; + + /** + * 1 if the connection must stay alive afterwards, 0 if MHD must close it. + */ + int alive; + + /** + * If non-zero, the client closes the connection right after sending and + * does not read any answer at all. + */ + int close_after_send; + + /** + * If non-zero, the only requirement is that the daemon survives: neither + * the number of parsed requests nor the answer is checked. Used by the + * probes whose declared purpose is to reach a defect. + */ + int loose; + + /** + * The lowest client discipline level at which this scenario is meaningful. + */ + int min_discp; + + /** + * The highest client discipline level at which this scenario is + * meaningful. + */ + int max_discp; + + /** + * The expected server side observation, see above. + */ + const char *exp; +}; + + +/** + * A header block of ten headers with values of moderate length. Kept small + * enough to still fit into a connection memory pool of + * 1024 bytes, so that the scenario is a real check rather than a permanent + * "degraded by the pool" case. + */ +#define MANY_HDRS \ + "A-One: 1" CRLF "B-Two: 22" CRLF "C-Three: 333" CRLF \ + "D-Four: 4444" CRLF "E-Five: 55555" CRLF "F-Six: 666666" CRLF \ + "G-Seven: 7777777" CRLF "H-Eight: 88888888" CRLF \ + "I-Nine: 999999999" CRLF "J-Ten: aaaaaaaaaa" CRLF + +/** + * Forty characters of chunk-extension filler. + */ +#define EXT_40 "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" + +/** + * Four hundred characters of chunk-extension filler: longer than the whole + * read buffer of a connection memory pool of 512 bytes, so that MHD has to + * take the "chunk-size line does not fit" path, and comfortably shorter than + * the read buffer of a 1024-byte pool, where the line still fits. + */ +#define EXT_400 \ + EXT_40 EXT_40 EXT_40 EXT_40 EXT_40 \ + EXT_40 EXT_40 EXT_40 EXT_40 EXT_40 + +/** + * The expected rendering of #MANY_HDRS as GET arguments: none, the block is + * a header block. Only listed here to keep the table readable. + */ +#define E_MANY "GET /h [] {}\n" + +static const struct scenario scenarios[] = { + { "get_plain", + "baseline: request line, one header, empty reply", + "GET / HTTP/1.1" CRLF H_LOCAL CRLF, + 1, 1, 200, 1, 0, 0, -3, 3, + "GET / [] {}\n" }, + { "get_query", + "query arguments: '=' value, bare argument, '+' decoding", + "GET /q?a=1&b&c=x+y HTTP/1.1" CRLF H_LOCAL CRLF, + 1, 1, 200, 1, 0, 0, -3, 3, + "GET /q [] {a=1,b=<NULL>,c=x y}\n" }, + { "post_cl", + "Content-Length body, delivered incrementally to the handler", + "POST /p HTTP/1.1" CRLF H_LOCAL "Content-Length: 11" CRLF CRLF + "Hello World", + 1, 1, 200, 1, 0, 0, -3, 3, + "POST /p [Hello World] {}\n" }, + { "post_chunked", + "chunked body without any chunk extension", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "5" CRLF "Hello" CRLF "6" CRLF " World" CRLF "0" CRLF CRLF, + 1, 1, 200, 1, 0, 0, -3, 3, + "POST /c [Hello World] {}\n" }, + { "post_chunk_ext", + "chunk extensions, incl. a quoted value holding a ';' (commit c13f4c64)", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "5;a=b" CRLF "Hello" CRLF "6;q=\"x;y\"" CRLF " World" CRLF "0;z" CRLF CRLF, + 1, 1, 200, 1, 0, 0, -3, 3, + "POST /c [Hello World] {}\n" }, + /* With a connection memory limit of about 512 or less this chunk-size + line does not fit into the read buffer. */ + { "post_chunk_ext_long", + "chunk extension longer than the space reserved for the chunk header", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "5;ext=\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"" CRLF + "Hello" CRLF "0" CRLF CRLF, + 1, 1, 200, 1, 0, 0, -3, 3, + "POST /c [Hello] {}\n" }, + /* Regression test for commits 68c83f22 and e04eb218: the chunk-size line + is longer than the read buffer of a small connection memory pool, so MHD + has to take handle_req_chunk_size_line_no_space(). With a pool of 1024 + bytes or more the line fits and the scenario is an ordinary + chunk-extension check. */ + { "post_chunk_ext_huge", + "chunk extension longer than the whole read buffer of a small pool", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "5;ext=\"" EXT_400 "\"" CRLF "Hello" CRLF "0" CRLF CRLF, + 1, 1, 200, 1, 0, 0, -3, 3, + "POST /c [Hello] {}\n" }, + /* The same, but with the over-long chunk-size line on the *second* chunk, + so that the upload callback has already run once when MHD runs out of + read buffer space. That is the extra precondition of the assertion + fixed in 68c83f22 (rq.some_payload_processed still describes the + previous read). */ + { "post_chunk_ext_huge_2nd", + "over-long chunk-size line after a chunk that was already delivered", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "5" CRLF "Hello" CRLF + "5;ext=\"" EXT_400 "\"" CRLF "World" CRLF "0" CRLF CRLF, + 1, 1, 200, 1, 0, 0, -3, 3, + "POST /c [HelloWorld] {}\n" }, + { "pipelined", + "two pipelined requests must yield exactly two parsed requests", + "GET /1 HTTP/1.1" CRLF H_LOCAL CRLF + "GET /2?x=1 HTTP/1.1" CRLF H_LOCAL CRLF, + 2, 2, 200, 1, 0, 0, -3, 3, + "GET /1 [] {}\nGET /2 [] {x=1}\n" }, + { "many_headers", + "ten header lines: pool allocation and read buffer growth", + "GET /h HTTP/1.1" CRLF H_LOCAL MANY_HDRS CRLF, + 1, 1, 200, 1, 0, 0, -3, 3, + E_MANY }, + { "early_close", + "the client vanishes in the middle of the request header", + "GET /e HTTP/1.1" CRLF H_LOCAL "X-Partial: abc", + 0, 0, 0, 0, 1, 0, -3, 3, + "" }, + + /* ---- probes for the relaxed client discipline levels ------------ */ + /* The three "strict" variants below are the control cases: at the + discipline levels at which MHD promises to reject the input, it must + really answer 400 and must not parse a request. The three "lax" + variants are the regression tests for the assertions fixed in commits + 0b750975 and 6fcdfd43; they only require that the daemon survives, since + what those modes do with the input is deliberately unspecified beyond + "do not abort". */ + { "wsp_first_hdr_strict", + "whitespace-prefixed first header line is rejected at level 0 and above", + "GET / HTTP/1.1" CRLF " " H_LOCAL "X-A: b" CRLF CRLF, + 0, 1, 400, 0, 0, 0, 0, 3, + "" }, + { "wsp_first_hdr_lax", + "whitespace-prefixed first header line at level -1 or below (0b750975)", + "GET / HTTP/1.1" CRLF " " H_LOCAL "X-A: b" CRLF CRLF, + 0, 0, 0, 0, 0, 1, -3, -1, + "" }, + { "empty_hdr_name_strict", + "an empty header name is rejected at level -1 and above", + "GET / HTTP/1.1" CRLF H_LOCAL ": value" CRLF CRLF, + 0, 1, 400, 0, 0, 0, -1, 3, + "" }, + { "empty_hdr_name_lax", + "an empty header name at level -2 or below (0b750975)", + "GET / HTTP/1.1" CRLF H_LOCAL ": value" CRLF CRLF, + 0, 0, 0, 0, 0, 1, -3, -2, + "" }, + { "bare_cr_strict", + "a bare CR in a header value is rejected at level 0 and above", + "GET / HTTP/1.1" CRLF H_LOCAL "X-Cr: va\rlue" CRLF CRLF, + 0, 1, 400, 0, 0, 0, 0, 3, + "" }, + { "bare_cr_as_sp", + "a bare CR becomes a space at level -1 and -2", + "GET / HTTP/1.1" CRLF H_LOCAL "X-Cr: va\rlue" CRLF CRLF, + 1, 1, 200, 1, 0, 0, -2, -1, + "GET / [] {}\n" }, + { "bare_cr_keep", + "a bare CR is kept in the header value at level -3 (6fcdfd43)", + "GET / HTTP/1.1" CRLF H_LOCAL "X-Cr: va\rlue" CRLF CRLF, + 0, 0, 0, 0, 0, 1, -3, -3, + "" } +}; + +#define NUM_SCENARIOS (sizeof(scenarios) / sizeof(scenarios[0])) + + +/* ------------------------------------------------------------------ */ +/* Global state */ +/* ------------------------------------------------------------------ */ + +static int verbose; + +/** The profile currently under test. */ +static const struct MHD_OptMatrixProfile *cur_prof; +/** Rendered description of @a cur_prof. */ +static char cur_prof_desc[256]; +/** The scenario currently under test. */ +static const struct scenario *cur_scen; +/** If not NULL, only the scenario with this name is run. */ +static const char *only_scen; + +/** The port of the running daemon. */ +static uint16_t global_port; +/** The running daemon, only set when it has to be driven with MHD_run(). */ +static struct MHD_Daemon *extern_daemon; + +/** Server side observations of the connection currently under test. */ +static struct obs_buf srv_obs; +/** Number of completed requests seen by the request handler. */ +static volatile unsigned int srv_n_req; +/** Set if the handler noticed something structurally impossible. */ +static volatile unsigned int srv_error; + +static volatile unsigned int conn_started; +static volatile unsigned int conn_closed; + +/** Number of (profile x scenario) combinations executed. */ +static unsigned long combos; +/** Number of combinations that could not fit into the memory pool. */ +static unsigned long combos_degraded; +/** Number of scenarios skipped because of the discipline level. */ +static unsigned long scen_skipped; +/** Number of profiles skipped because this build cannot provide them. */ +static unsigned long prof_skipped; +/** Number of failures found. */ +static unsigned long failures; + + +static void +print_context (FILE *out) +{ + fprintf (out, " context: profile %s scenario='%s'\n", + cur_prof_desc, + (NULL != cur_scen) ? cur_scen->name : "(none)"); + if (NULL != cur_scen) + fprintf (out, " scenario purpose: %s\n", cur_scen->desc); +} + + +#if defined(HAVE_SIGNAL_H) && defined(SIGABRT) +/** + * Report the profile and the scenario when the library aborts (e.g. from an + * internal assertion) instead of dying anonymously. + */ +static void +abort_handler (int sig) +{ + (void) sig; + fprintf (stderr, + "\nFATAL: the library aborted while running a scenario.\n"); + print_context (stderr); + fflush (stderr); + _exit (1); +} + + +#endif /* HAVE_SIGNAL_H && SIGABRT */ + + +/** + * Panic callback: a library panic is a test failure that names the profile + * and the scenario. + */ +static void +test_panic (void *cls, const char *file, unsigned int line, const char *reason) +{ + (void) cls; + fprintf (stderr, + "\nFATAL: MHD panic at %s:%u: %s\n", + (NULL != file) ? file : "(unknown)", + line, + (NULL != reason) ? reason : "(no reason given)"); + print_context (stderr); + fflush (stderr); + _exit (1); +} + + +/* ------------------------------------------------------------------ */ +/* Request handler: records the observations */ +/* ------------------------------------------------------------------ */ + +struct req_state +{ + struct obs_buf body; +}; + + +struct iter_ctx +{ + struct obs_buf *out; + unsigned int num; +}; + + +static enum MHD_Result +value_iterator (void *cls, + enum MHD_ValueKind kind, + const char *key, + size_t key_size, + const char *value, + size_t value_size) +{ + struct iter_ctx *const ctx = (struct iter_ctx *) cls; + + (void) kind; + if (0 != ctx->num) + obs_addf (ctx->out, ","); + ctx->num++; + if (NULL != key) + obs_add_escaped (ctx->out, key, key_size); + else + obs_addf (ctx->out, "<NULLKEY>"); + obs_addf (ctx->out, "="); + if (NULL == value) + obs_addf (ctx->out, "<NULL>"); + else + obs_add_escaped (ctx->out, value, value_size); + return MHD_YES; +} + + +static enum MHD_Result +ahc_record (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **req_cls) +{ + /* The reply body is intentionally empty, which keeps the response framing + on the client side trivial. */ + static const char rp_data[] = ""; + struct req_state *rs; + struct MHD_Response *response; + struct iter_ctx ctx; + enum MHD_Result ret; + + (void) cls; (void) version; + if (NULL == *req_cls) + { + rs = (struct req_state *) malloc (sizeof (struct req_state)); + if (NULL == rs) + hard_error ("malloc() failed"); + obs_reset (&rs->body); + *req_cls = rs; + return MHD_YES; + } + rs = (struct req_state *) *req_cls; + if (0 != *upload_data_size) + { + obs_add_escaped (&rs->body, upload_data, *upload_data_size); + *upload_data_size = 0; + return MHD_YES; + } + + /* The request is complete: record everything that is observable. */ + obs_addf (&srv_obs, "%s ", (NULL != method) ? method : "(null)"); + if (NULL != url) + obs_add_escaped (&srv_obs, url, strlen (url)); + else + obs_addf (&srv_obs, "(null)"); + obs_addf (&srv_obs, " [%s] {", rs->body.d); + ctx.out = &srv_obs; + ctx.num = 0; + MHD_get_connection_values_n (connection, MHD_GET_ARGUMENT_KIND, + &value_iterator, &ctx); + obs_addf (&srv_obs, "}\n"); + + free (rs); + *req_cls = NULL; + srv_n_req++; + + response = + MHD_create_response_from_buffer (MHD_STATICSTR_LEN_ (rp_data), + (void *) rp_data, + MHD_RESPMEM_PERSISTENT); + if (NULL == response) + { + srv_error++; + return MHD_NO; + } + ret = MHD_queue_response (connection, MHD_HTTP_OK, response); + MHD_destroy_response (response); + if (MHD_YES != ret) + srv_error++; + return ret; +} + + +/** + * The MHD error log, only printed with '-v -v': the matrix visits several + * deliberately hostile configurations and the library complains a lot. + */ +static void +test_log (void *cls, const char *fmt, va_list ap) +{ + (void) cls; + if (1 < verbose) + { + vfprintf (stderr, fmt, ap); + fflush (stderr); + } +} + + +static void +conn_notify (void *cls, + struct MHD_Connection *c, + void **socket_context, + enum MHD_ConnectionNotificationCode toe) +{ + (void) cls; (void) c; (void) socket_context; + if (MHD_CONNECTION_NOTIFY_STARTED == toe) + conn_started++; + else if (MHD_CONNECTION_NOTIFY_CLOSED == toe) + conn_closed++; +} + + +static void +req_completed (void *cls, + struct MHD_Connection *c, + void **req_cls, + enum MHD_RequestTerminationCode term_code) +{ + (void) cls; (void) c; (void) term_code; + if ((NULL != req_cls) && (NULL != *req_cls)) + { + free (*req_cls); + *req_cls = NULL; + } +} + + +/* ------------------------------------------------------------------ */ +/* Raw client */ +/* ------------------------------------------------------------------ */ + +struct client_result +{ + unsigned int n_resp; + int first_status; + int peer_closed; + int timed_out; + size_t rx_len; + char rx[RX_SIZE]; +}; + + +/** + * Case-insensitive comparison of @a size bytes (ASCII only). + */ +static int +mem_equal_ci (const char *a, const char *b, size_t size) +{ + size_t i; + + for (i = 0; i < size; ++i) + { + char ca = a[i]; + char cb = b[i]; + + if (('A' <= ca) && ('Z' >= ca)) + ca = (char) (ca - 'A' + 'a'); + if (('A' <= cb) && ('Z' >= cb)) + cb = (char) (cb - 'A' + 'a'); + if (ca != cb) + return 0; + } + return 1; +} + + +/** + * Count the number of complete HTTP responses in @a buf. + * + * @param buf the received bytes + * @param len the number of bytes in @a buf + * @param[out] first_status set to the status code of the first response + * @return the number of *complete* responses found + */ +static unsigned int +count_responses (const char *buf, size_t len, int *first_status) +{ + size_t pos = 0; + unsigned int num = 0; + + *first_status = 0; + while (pos < len) + { + const char *hdr_end; + const char *cl; + size_t hdr_len; + size_t body_len = 0; + int status; + size_t i; + + if (7 > len - pos) + break; + if (0 != memcmp (buf + pos, "HTTP/1.", 7)) + break; /* Not a well-formed response */ + hdr_end = NULL; + for (i = pos; i + 3 < len; ++i) + { + if (('\r' == buf[i]) && ('\n' == buf[i + 1]) && + ('\r' == buf[i + 2]) && ('\n' == buf[i + 3])) + { + hdr_end = buf + i + 4; + break; + } + } + if (NULL == hdr_end) + break; /* Incomplete header */ + hdr_len = (size_t) (hdr_end - (buf + pos)); + status = 0; + if ((pos + 12 <= len) && + ('0' <= buf[pos + 9]) && ('9' >= buf[pos + 9])) + status = (buf[pos + 9] - '0') * 100 + + (buf[pos + 10] - '0') * 10 + + (buf[pos + 11] - '0'); + if (0 == num) + *first_status = status; + cl = NULL; + for (i = pos; i + 15 <= pos + hdr_len; ++i) + { + if (mem_equal_ci (buf + i, "content-length:", 15)) + { + cl = buf + i + 15; + break; + } + } + if (NULL != cl) + { + while ((cl < buf + pos + hdr_len) && (' ' == *cl)) + cl++; + body_len = 0; + while ((cl < buf + pos + hdr_len) && ('0' <= *cl) && ('9' >= *cl)) + { + body_len = body_len * 10 + (size_t) (*cl - '0'); + cl++; + } + } + if (pos + hdr_len + body_len > len) + break; /* Body incomplete */ + pos += hdr_len + body_len; + num++; + } + return num; +} + + +/** + * Switch a socket to the non-blocking mode. + * + * @param sk the socket to modify + * @return non-zero on success + */ +static int +set_nonblocking (MHD_socket sk) +{ +#if defined(MHD_POSIX_SOCKETS) + int flags; + + flags = fcntl (sk, F_GETFL); + if (-1 == flags) + return 0; + return (-1 != fcntl (sk, F_SETFL, flags | O_NONBLOCK)) ? ! 0 : 0; +#else /* ! MHD_POSIX_SOCKETS */ + unsigned long mode = 1; + + return (0 == ioctlsocket (sk, FIONBIO, &mode)) ? ! 0 : 0; +#endif /* ! MHD_POSIX_SOCKETS */ +} + + +static MHD_socket +client_connect (void) +{ + MHD_socket sk; + struct sockaddr_in sa; + const MHD_SCKT_OPT_BOOL_ on_val = 1; + + sk = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (MHD_INVALID_SOCKET == sk) + hard_error ("Cannot create the client socket"); +#ifdef MHD_socket_nosignal_ + if (! MHD_socket_nosignal_ (sk)) + hard_error ("Cannot suppress SIGPIPE on the client socket"); +#endif /* MHD_socket_nosignal_ */ + if (0 != setsockopt (sk, IPPROTO_TCP, TCP_NODELAY, + (const void *) &on_val, sizeof (on_val))) + hard_error ("Cannot set the TCP_NODELAY option"); + + memset (&sa, 0, sizeof (sa)); + sa.sin_family = AF_INET; + sa.sin_port = htons (global_port); + sa.sin_addr.s_addr = htonl (INADDR_LOOPBACK); + if (0 != connect (sk, (struct sockaddr *) &sa, sizeof (sa))) + hard_error ("Cannot connect() to the test daemon"); + if (! set_nonblocking (sk)) + hard_error ("Cannot switch the client socket to the non-blocking mode"); + return sk; +} + + +/** + * Send @a data and read the answer, driving the daemon with MHD_run() when + * the profile asks for external polling. + * + * @param sk the client socket + * @param data the request bytes + * @param size the number of bytes in @a data + * @param[out] r the result + * @param min_resp stop once that many complete responses have been read + * @param want_close keep reading until the peer closes the connection + * @param read_answer if zero, close right after sending + */ +static void +client_exchange (MHD_socket sk, + const char *data, + size_t size, + struct client_result *r, + unsigned int min_resp, + int want_close, + int read_answer) +{ + const uint64_t start = now_ms (); + size_t sent = 0; + uint64_t quiet_since = 0; + + memset (r, 0, sizeof (*r)); + while (1) + { + fd_set rs; + fd_set ws; + fd_set es; + struct timeval tv; + MHD_socket maxfd_mhd = MHD_INVALID_SOCKET; + int maxfd; + int sel; + + if (now_ms () - start >= READ_DEADLINE_MS) + { + r->timed_out = 1; + break; + } + if ((sent >= size) && (! read_answer)) + break; + r->n_resp = count_responses (r->rx, r->rx_len, &r->first_status); + if ((sent >= size) && (! want_close) && (r->n_resp >= min_resp)) + { + /* Everything that was expected has arrived; accept a short silence as + "the server is done and keeps the connection alive". */ + if (0 == quiet_since) + quiet_since = now_ms (); + else if (now_ms () - quiet_since >= READ_GRACE_MS) + break; + } + else + quiet_since = 0; + + FD_ZERO (&rs); + FD_ZERO (&ws); + FD_ZERO (&es); +#if defined(MHD_POSIX_SOCKETS) + if (FD_SETSIZE <= (int) sk) + hard_error ("The client socket does not fit into fd_set"); +#endif /* MHD_POSIX_SOCKETS */ + if (! r->peer_closed) + FD_SET (sk, &rs); + if (sent < size) + FD_SET (sk, &ws); + maxfd = (int) sk; + if (NULL != extern_daemon) + { + if (MHD_YES != MHD_get_fdset (extern_daemon, &rs, &ws, &es, &maxfd_mhd)) + hard_error ("MHD_get_fdset() failed"); +#ifndef MHD_WINSOCK_SOCKETS + if ((int) maxfd_mhd > maxfd) + maxfd = (int) maxfd_mhd; +#endif /* ! MHD_WINSOCK_SOCKETS */ + } + tv.tv_sec = 0; + tv.tv_usec = 2000; + sel = select (maxfd + 1, &rs, &ws, &es, &tv); + if ((0 > sel) && (EINTR != errno)) + hard_error ("select() failed"); + if (NULL != extern_daemon) + MHD_run (extern_daemon); + + if (sent < size) + { + ssize_t s = MHD_send_ (sk, data + sent, size - sent); + + if (0 > s) + { + const int err = MHD_socket_get_error_ (); + + if (! (MHD_SCKT_ERR_IS_EINTR_ (err) || MHD_SCKT_ERR_IS_EAGAIN_ (err))) + { + /* MHD closed the connection early; that is an observation, not an + error of the test. */ + sent = size; + r->peer_closed = 1; + } + } + else + sent += (size_t) s; + } + if ((! r->peer_closed) && (r->rx_len < sizeof (r->rx))) + { + ssize_t got = MHD_recv_ (sk, r->rx + r->rx_len, + sizeof (r->rx) - r->rx_len); + + if (0 > got) + { + const int err = MHD_socket_get_error_ (); + + if (! (MHD_SCKT_ERR_IS_EINTR_ (err) || MHD_SCKT_ERR_IS_EAGAIN_ (err))) + r->peer_closed = 1; /* A connection reset counts as "closed" */ + } + else if (0 == got) + r->peer_closed = 1; + else + { + r->rx_len += (size_t) got; + quiet_since = 0; + } + } + if (r->peer_closed && (sent >= size)) + break; + } + r->n_resp = count_responses (r->rx, r->rx_len, &r->first_status); +} + + +/** + * Let the daemon run for @a ms milliseconds without any client activity. + * + * @param ms the number of milliseconds to spend + */ +static void +settle (unsigned int ms) +{ + const uint64_t start = now_ms (); + + do + { + if (NULL != extern_daemon) + MHD_run (extern_daemon); + sleep_ms (1); + } while (now_ms () - start < (uint64_t) ms); +} + + +/** + * Wait until the daemon has finished tearing down all connections, so that + * the server side observations are stable. + * + * @return non-zero on success, zero on timeout + */ +static int +wait_conns_closed (void) +{ + const uint64_t start = now_ms (); + + while (conn_closed != conn_started) + { + if (NULL != extern_daemon) + MHD_run (extern_daemon); + else + sleep_ms (1); + if (now_ms () - start > CLOSE_WAIT_MS) + return 0; + } + return ! 0; +} + + +/* ------------------------------------------------------------------ */ +/* One scenario against one profile */ +/* ------------------------------------------------------------------ */ + +static void +report_failure (const char *what, const char *exp, const char *got) +{ + failures++; + fprintf (stderr, "\nFAILED: %s\n", what); + print_context (stderr); + if (NULL != exp) + fprintf (stderr, " expected:\n%s", exp); + if (NULL != got) + fprintf (stderr, " observed:\n%s", got); + fflush (stderr); +} + + +/** + * Statuses with which MHD legitimately refuses a request that does not fit + * into the connection memory pool. A 400 is never one of them. + * + * @param status the status code of the first response + * @return non-zero if the status is a resource limitation + */ +static int +is_resource_status (int status) +{ + return ((413 == status) || (414 == status) || (431 == status) || + (500 == status) || (503 == status)) ? ! 0 : 0; +} + + +/** + * Run one scenario against the running daemon. + */ +static void +run_scenario (const struct scenario *s) +{ + struct client_result cr; + struct obs_buf got; + MHD_socket sk; + const size_t len = strlen (s->raw); + int degraded; + + cur_scen = s; + obs_reset (&srv_obs); + srv_n_req = 0; + srv_error = 0; + combos++; + + sk = client_connect (); + client_exchange (sk, s->raw, len, &cr, + s->n_resp, (0 == s->alive) && (! s->close_after_send), + s->close_after_send ? 0 : 1); + (void) MHD_socket_close_ (sk); + /* The server side observation is complete as soon as MHD has closed the + connection or has answered everything that was expected: the request + handler always runs before the reply is sent. Waiting for the whole + connection teardown is therefore only useful when neither happened. + MHD keeps a connection around for a while after an error reply in the + 'epoll' modes, so an unconditional wait would cost seconds per scenario + for no gain at all. */ + if (s->close_after_send) + settle (SETTLE_MS); /* Give MHD a chance to mis-parse the truncated stream */ + else if ((! s->loose) && (! cr.peer_closed) && (cr.n_resp < s->n_resp)) + { + if (! wait_conns_closed ()) + { + fprintf (stderr, "WARNING: timeout waiting for the connection teardown " + "(started=%u closed=%u).\n", conn_started, conn_closed); + print_context (stderr); + } + } + + obs_reset (&got); + obs_addf (&got, "%s", srv_obs.d); + + if (0 != srv_error) + { + report_failure ("the request handler could not queue a response", + NULL, NULL); + return; + } + if (s->loose) + { + /* The daemon survived, which is all this probe asks for. */ + if (1 < verbose) + printf (" %-22s survived (loose probe)\n", s->name); + return; + } + + /* A connection memory pool that is too small for the request is a + legitimate resource limitation: MHD then answers 413/414/431/500 without + ever invoking the handler, or it cannot even build the reply header and + closes the connection without answering a request it did parse. + Everything MHD *did* parse must still match, and MHD may never parse + more requests than the stream contains. A 400 Bad Request is never + accepted as a resource limitation. */ + degraded = 0; + if (! s->close_after_send) + { + if ((0 != cr.n_resp) && (cr.n_resp < s->n_resp) && + is_resource_status (cr.first_status)) + degraded = 1; + else if ((0 != cr.n_resp) && (0 != strcmp (s->exp, got.d)) && + is_resource_status (cr.first_status)) + degraded = 1; + else if ((0 == cr.n_resp) && (0 != s->n_resp) && cr.peer_closed) + degraded = 1; + } + if (degraded) + { + combos_degraded++; + if (0 != memcmp (s->exp, got.d, strlen (got.d))) + report_failure ("a request parsed with a small memory pool differs " + "from the expectation", s->exp, got.d); + else if (verbose) + printf (" %-22s degraded by the memory pool, relaxed checking\n", + s->name); + return; + } + + if (0 != strcmp (s->exp, got.d)) + { + report_failure ("the parsed requests do not match the expectation", + s->exp, got.d); + return; + } + if (srv_n_req != s->n_req) + { + char msg[128]; + + snprintf (msg, sizeof (msg), + "MHD parsed %u request(s), %u expected", + (unsigned int) srv_n_req, s->n_req); + report_failure (msg, NULL, NULL); + return; + } + if (s->close_after_send) + { + if (1 < verbose) + printf (" %-22s ok (no answer expected)\n", s->name); + return; + } + if (cr.n_resp != s->n_resp) + { + char msg[160]; + + snprintf (msg, sizeof (msg), + "the client saw %u response(s), %u expected%s", + cr.n_resp, s->n_resp, cr.timed_out ? " (timed out)" : ""); + report_failure (msg, NULL, cr.rx_len ? cr.rx : "(nothing)"); + return; + } + if ((0 != s->status) && (cr.first_status != s->status)) + { + char msg[128]; + + snprintf (msg, sizeof (msg), + "the first response has status %d, %d expected", + cr.first_status, s->status); + report_failure (msg, NULL, cr.rx); + return; + } + if (s->alive && cr.peer_closed) + { + report_failure ("MHD closed a connection that had to stay alive", + NULL, NULL); + return; + } + if ((! s->alive) && (! cr.peer_closed)) + { + report_failure ("MHD kept a connection that had to be closed", + NULL, NULL); + return; + } + if (1 < verbose) + printf (" %-22s ok\n", s->name); +} + + +/* ------------------------------------------------------------------ */ +/* Main test driver */ +/* ------------------------------------------------------------------ */ + +/** + * Start a daemon for @a prof. + * + * @param prof the profile to apply + * @return the daemon, or NULL if it cannot be started + */ +static struct MHD_Daemon * +start_daemon_for (const struct MHD_OptMatrixProfile *prof) +{ + struct MHD_OptionItem ops[8]; + struct MHD_Daemon *d; + unsigned int n; + unsigned int flags; + + n = mhd_opt_matrix_fill_options (prof, ops, + (unsigned int) (sizeof(ops) + / sizeof(ops[0]))); + if (0 == n) + hard_error ("The option array is too small"); + if (mhd_opt_matrix_is_external (prof)) + { + /* Only meaningful without an internal polling thread; MHD complains + about the option in every other mode. */ + ops[n - 1].option = MHD_OPTION_APP_FD_SETSIZE; + ops[n - 1].value = (intptr_t) (FD_SETSIZE); + ops[n - 1].ptr_value = NULL; + ops[n].option = MHD_OPTION_END; + ops[n].value = 0; + ops[n].ptr_value = NULL; + } + flags = mhd_opt_matrix_daemon_flags (prof, MHD_USE_ERROR_LOG, ! 0); + /* MHD_OPTION_EXTERNAL_LOGGER has to be the first option, otherwise the + complaints about the options that follow it go to the standard logger. */ + d = MHD_start_daemon (flags, + 0, NULL, NULL, + &ahc_record, NULL, + MHD_OPTION_EXTERNAL_LOGGER, &test_log, NULL, + MHD_OPTION_ARRAY, ops, + MHD_OPTION_NOTIFY_CONNECTION, &conn_notify, NULL, + MHD_OPTION_NOTIFY_COMPLETED, &req_completed, NULL, + MHD_OPTION_CONNECTION_TIMEOUT, + (unsigned int) DAEMON_TIMEOUT, + MHD_OPTION_END); + return d; +} + + +/** + * Run the whole battery against one profile. + * + * @param prof the profile to test + * @return zero on success, 99 if the daemon could not be started + */ +static int +run_profile (const struct MHD_OptMatrixProfile *prof) +{ + struct MHD_Daemon *d; + const union MHD_DaemonInfo *dinfo; + size_t i; + int discp; + + cur_prof = prof; + (void) mhd_opt_matrix_describe (prof, cur_prof_desc, sizeof(cur_prof_desc)); + if (! mhd_opt_matrix_profile_supported (prof)) + { + prof_skipped++; + if (verbose) + printf ("--- profile %s SKIPPED (not supported by this build) ---\n", + cur_prof_desc); + return 0; + } + if (verbose) + { + printf ("--- profile %s ---\n", cur_prof_desc); + fflush (stdout); + } + + conn_started = 0; + conn_closed = 0; + extern_daemon = NULL; + d = start_daemon_for (prof); + if (NULL == d) + { + fprintf (stderr, "Failed to start the daemon for profile %s.\n", + cur_prof_desc); + return 99; + } + if (mhd_opt_matrix_is_external (prof)) + extern_daemon = d; + dinfo = MHD_get_daemon_info (d, MHD_DAEMON_INFO_BIND_PORT); + if ((NULL == dinfo) || (0 == dinfo->port)) + { + MHD_stop_daemon (d); + fprintf (stderr, "Cannot get the port of the daemon for profile %s.\n", + cur_prof_desc); + return 99; + } + global_port = dinfo->port; + + /* Not prof->discipline_lvl: MHD_OPTION_STRICT_FOR_CLIENT maps its value, + so the "legacy-lax" profile really runs at level -3 although it asks for + -1. Deciding what a request must do from the configured value instead of + from the effective one is a trap: MHD_OPTION_STRICT_FOR_CLIENT maps + every value of -1 or below to level -3. */ + discp = mhd_opt_matrix_effective_discipline (prof); + for (i = 0; i < NUM_SCENARIOS; ++i) + { + const struct scenario *const s = scenarios + i; + + if ((NULL != only_scen) && (0 != strcmp (only_scen, s->name))) + continue; + if ((discp < s->min_discp) || (discp > s->max_discp)) + { + scen_skipped++; + if (1 < verbose) + printf (" %-22s skipped (needs discipline level %d..%d)\n", + s->name, s->min_discp, s->max_discp); + continue; + } + run_scenario (s); + if (MAX_FAILURES < failures) + break; + } + cur_scen = NULL; + extern_daemon = NULL; + MHD_stop_daemon (d); + return 0; +} + + +int +main (int argc, char *const *argv) +{ + const uint64_t t_start = now_ms (); + const struct MHD_OptMatrixProfile *env_prof; + const char *only = NULL; + unsigned int num_prof; + unsigned int i; + int ret; + + for (i = 1; i < (unsigned int) argc; ++i) + { + if ((0 == strcmp (argv[i], "-v")) || (0 == strcmp (argv[i], "--verbose"))) + verbose++; + else if (0 == strncmp (argv[i], "--profile=", 10)) + only = argv[i] + 10; + else if (0 == strncmp (argv[i], "--scenario=", 11)) + only_scen = argv[i] + 11; + else if (0 == strcmp (argv[i], "--list-profiles")) + { + unsigned int p; + + for (p = 0; p < mhd_opt_matrix_num_profiles (); ++p) + printf ("%s\n", mhd_opt_matrix_profile (p)->name); + return 0; + } + } + + strcpy (cur_prof_desc, "(not started)"); + +#if defined(HAVE_SIGNAL_H) && defined(SIGPIPE) + if (MHD_YES != MHD_is_feature_supported (MHD_FEATURE_AUTOSUPPRESS_SIGPIPE)) + { + if (SIG_ERR == signal (SIGPIPE, SIG_IGN)) + hard_error ("Cannot suppress the SIGPIPE signal"); + } +#endif /* HAVE_SIGNAL_H && SIGPIPE */ +#if defined(HAVE_SIGNAL_H) && defined(SIGABRT) + (void) signal (SIGABRT, &abort_handler); +#endif /* HAVE_SIGNAL_H && SIGABRT */ + + /* Deliberately NOT mhd_panic_tripwire.h: this test installs its own + panic and SIGABRT handlers, which print the failing input and the + daemon options along with the diagnostic. That context is worth + more here than the generic tripwire, and the two would collide + (whichever runs last wins). See TESTING.md, P5. */ + MHD_set_panic_func (&test_panic, NULL); + mhd_opt_matrix_print_notice ("test_option_matrix"); + + env_prof = mhd_opt_matrix_from_env (); + if (NULL != only) + { + /* The command line wins over the environment. */ + env_prof = mhd_opt_matrix_lookup (only); + if (NULL == env_prof) + { + fprintf (stderr, "Unknown profile '%s'.\n", only); + return 99; + } + } + if (NULL != env_prof) + { + if (! mhd_opt_matrix_profile_supported (env_prof)) + { + char desc[256]; + + printf ("test_option_matrix: the selected profile %s is not supported " + "by this build, the test is skipped.\n", + mhd_opt_matrix_describe (env_prof, desc, sizeof(desc))); + return 77; + } + num_prof = 1; + ret = run_profile (env_prof); + if (0 != ret) + return ret; + } + else + { + num_prof = mhd_opt_matrix_num_profiles (); + for (i = 0; i < num_prof; ++i) + { + ret = run_profile (mhd_opt_matrix_profile (i)); + if (0 != ret) + return ret; + if (MAX_FAILURES < failures) + break; + } + if (num_prof == prof_skipped) + { + printf ("test_option_matrix: no profile of the matrix is supported by " + "this build, the test is skipped.\n"); + return 77; + } + } + + printf ("\ntest_option_matrix: %u profile(s) (%lu skipped), " + "%u scenario(s), %lu combinations executed " + "(%lu pool-limited, %lu out of the discipline range), %.1f s\n", + num_prof, + prof_skipped, + (unsigned int) NUM_SCENARIOS, + combos, + combos_degraded, + scen_skipped, + (double) (now_ms () - t_start) / 1000.0); + if (0 != failures) + { + printf ("test_option_matrix: FAILED (%lu failure(s))\n", failures); + fflush (stdout); + return 1; + } + printf ("test_option_matrix: PASSED\n"); + fflush (stdout); + return 0; +} diff --git a/src/microhttpd/test_options.c b/src/microhttpd/test_options.c @@ -28,6 +28,10 @@ #include "microhttpd.h" #include "mhd_sockets.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + static enum MHD_Result ahc_echo (void *cls, struct MHD_Connection *connection, diff --git a/src/microhttpd/test_postprocessor.c b/src/microhttpd/test_postprocessor.c @@ -32,6 +32,10 @@ #include <stdio.h> #include "mhd_compat.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef WINDOWS #include <unistd.h> #endif diff --git a/src/microhttpd/test_postprocessor_amp.c b/src/microhttpd/test_postprocessor_amp.c @@ -5,6 +5,10 @@ #include <string.h> #include <stdio.h> +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + static uint64_t num_errors; static enum MHD_Result diff --git a/src/microhttpd/test_postprocessor_large.c b/src/microhttpd/test_postprocessor_large.c @@ -29,6 +29,10 @@ #include "internal.h" #include "mhd_compat.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef WINDOWS #include <unistd.h> #endif diff --git a/src/microhttpd/test_postprocessor_md.c b/src/microhttpd/test_postprocessor_md.c @@ -32,6 +32,10 @@ #include "microhttpd.h" #include "internal.h" #include "postprocessor.h" + +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" #if 0 #include "mhd_panic.c" #include "mhd_str.c" diff --git a/src/microhttpd/test_raw_requests.c b/src/microhttpd/test_raw_requests.c @@ -0,0 +1,2123 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + libmicrohttpd is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 2, or (at your + option) any later version. + + libmicrohttpd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libmicrohttpd; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +/** + * @file test_raw_requests.c + * @brief Data-driven raw HTTP request corpus, replayed at every possible + * TCP stream split point and under a sweep of connection memory + * limits. + * @author Christian Grothoff + * + * ## Why this test exists + * + * The MHD request parser is *incremental*: it is re-entered every time more + * bytes arrive from the network and has to decide, at each entry, whether it + * already holds a complete line / chunk header / chunk / body. Off-by-N + * errors, misplaced `break; /_* need more data *_/` statements and + * state-machine desynchronisations live exactly in that logic and they are + * invisible when the whole request arrives in a single `read()`. + * + * This test therefore takes every corpus entry and replays it over a fresh TCP + * connection once per possible split offset, forcing the server to re-enter + * the parser at that offset. The parse result (method, URL, version, upload + * body, headers, GET arguments, footers, number of complete requests, response + * status, connection re-use) must be *identical* for every split offset. Any + * divergence is a bug, no matter what the declared expectations say. + * + * The whole corpus is additionally replayed for a sweep of + * #MHD_OPTION_CONNECTION_MEMORY_LIMIT values. Values below #MHD_BUF_INC_SIZE + * (1500) unlock the read-buffer "shift back" and buffer-grow code paths that + * are never reached with the default pool size. + * + * ## How to add a corpus entry + * + * Add exactly one initialiser to the @a corpus[] table below. Nothing else + * has to be touched. The fields are: + * + * * `.name` - short identifier, printed on failure; + * * `.desc` - one line saying which behaviour the entry pins down; + * * `.raw` - the raw request byte stream (may contain several pipelined + * requests); `.raw_len` may stay 0, then `strlen()` is used; + * * `.n_req` - how many complete requests MHD *must* parse out of `.raw` + * (this is the request-smuggling / desync detector: a stream + * that must yield 2 requests may never yield 1 or 3); + * * `.reqs[]`- the per-request expectations. `.hdrs`, `.args` and `.foot` + * are newline separated `key=value` lists in the order in which + * MHD reports them; a NULL value (what a query argument without + * '=' produces) is written as `<NULL>`; + * * `.n_resp`- how many HTTP responses the client must see; + * * `.status`- expected status code of the *first* response, 0 for "any + * well-formed response"; + * * `.alive` - 1 if the connection must stay alive afterwards, 0 if MHD must + * close it; + * * `.deep` - non-zero to additionally run the (much slower) three-way split + * pass over this entry. + * + * The escaping used by the observation format has to be applied to the + * expectation strings as well: every byte outside the printable ASCII range + * appears as `\\xNN` (so a TAB inside a header value is written as `\\x09`). + * + * If the entry does not fit into the connection memory pool of the current + * sweep step, MHD answers 431/413/414/500 without ever invoking the request + * handler, or it cannot even build the reply header and closes the connection + * without an answer, or it serves only the first of several pipelined + * requests. All three are legitimate resource limitations; the test detects + * them and falls back to the relaxed check "every request that MHD did parse + * matches the expectation for its index, and MHD never parses more requests + * than the stream contains". A 400 Bad Request is never accepted as a + * resource limitation. + * + * ## The daemon option matrix + * + * The test honours the matrix of mhd_opt_matrix.h: when one of the MHD_TEST_* + * environment variables selects a profile, the built-in memory limit sweep is + * replaced by the single memory limit of the profile and the threading mode, + * the polling backend and the client discipline level of the profile are + * applied. Without those variables nothing changes, so a stock "make check" + * runs exactly the sweep described above. + * + * The declared expectations of a corpus entry are only valid for a range of + * client discipline levels - the levels are precisely the knob that switches + * the parser leniency the entries pin down. Every entry whose expectation + * depends on the level is therefore listed in @a discp_limits[] with the + * range in which it is meaningful, and is skipped outside of it. At the + * default level 0 nothing is ever skipped. + * + * ## Command line + * + * * `-v` - report the memory limit sweep and degraded entries; + * * `-v -v` - additionally print the MHD error log; + * * `--entry=NAME` - restrict the run to a single corpus entry (`NAME` may + * also be given as a bare argument). + * + * Exit codes: 0 = pass, 77 = skip, 99 = hard setup error, 1 = test failure. + */ +#include "MHD_config.h" +#include "platform.h" +#include <microhttpd.h> +#include <stdlib.h> +#include <string.h> +#include <stdarg.h> +#include <time.h> +#include <stdint.h> +#include <errno.h> + +#ifdef HAVE_STRINGS_H +#include <strings.h> +#endif /* HAVE_STRINGS_H */ + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN 1 +#endif /* !WIN32_LEAN_AND_MEAN */ +#include <windows.h> +#endif + +#ifndef WINDOWS +#include <unistd.h> +#include <sys/socket.h> +#endif + +#ifdef HAVE_LIMITS_H +#include <limits.h> +#endif /* HAVE_LIMITS_H */ + +#ifdef HAVE_SIGNAL_H +#include <signal.h> +#endif /* HAVE_SIGNAL_H */ + +#include <stdio.h> + +#include "mhd_sockets.h" /* only macros used */ +#include "mhd_opt_matrix.h" + +#ifndef MHD_STATICSTR_LEN_ +/** + * Determine length of static string / macro strings at compile time. + */ +#define MHD_STATICSTR_LEN_(macro) (sizeof(macro) / sizeof(char) - 1) +#endif /* ! MHD_STATICSTR_LEN_ */ + +/** + * Convenience shorthand used all over the corpus table. + */ +#define CRLF "\r\n" + +/** + * Maximum number of pipelined requests a single corpus entry may contain. + */ +#define MAX_REQS 4 + +/** + * Size of the textual observation buffers. + */ +#define OBS_SIZE 8192 + +/** + * Size of the client receive buffer. + */ +#define RX_SIZE 16384 + +/** + * Milliseconds to wait between two parts of a split request so that the + * server really gets to process the partial input. + */ +#define SPLIT_DELAY_MS 2 + +/** + * Hard upper bound (ms) for reading the complete server answer. + */ +#define READ_DEADLINE_MS 400 + +/** + * Silence (ms) after the expected number of responses that is accepted as + * "the server is done and keeps the connection alive". Only used for the + * unsplit reference run. + */ +#define READ_GRACE_MS 12 + +/** + * Milliseconds to wait for the server side to finish tearing down the + * connection (and thus for the observations to become stable). + */ +#define CLOSE_WAIT_MS 2000 + +/** + * Split offsets 1 .. SPLIT_DENSE_LIMIT are all exercised, beyond that only + * every SPLIT_STRIDE-th offset is used. + */ +#define SPLIT_DENSE_LIMIT 512 + +/** + * Stride for sampling split offsets of long requests. + */ +#define SPLIT_STRIDE 7 + +/** + * Maximum number of three-way split combinations per entry and pool size. + */ +#define DEEP_SPLIT_MAX 24 + +/** + * Give up after that many reported failures. + */ +#define MAX_FAILURES 10 + + +#if defined(HAVE___FUNC__) +#define externalErrorExit(ignore) \ + _externalErrorExit_func (NULL, __func__, __LINE__) +#define externalErrorExitDesc(errDesc) \ + _externalErrorExit_func (errDesc, __func__, __LINE__) +#elif defined(HAVE___FUNCTION__) +#define externalErrorExit(ignore) \ + _externalErrorExit_func (NULL, __FUNCTION__, __LINE__) +#define externalErrorExitDesc(errDesc) \ + _externalErrorExit_func (errDesc, __FUNCTION__, __LINE__) +#else +#define externalErrorExit(ignore) _externalErrorExit_func (NULL, NULL, __LINE__) +#define externalErrorExitDesc(errDesc) \ + _externalErrorExit_func (errDesc, NULL, __LINE__) +#endif + + +/* Forward declaration, needed by the error exit helpers. */ +static void +print_context (FILE *out); + + +_MHD_NORETURN static void +_externalErrorExit_func (const char *errDesc, const char *funcName, int lineNum) +{ + if ((NULL != errDesc) && (0 != errDesc[0])) + fprintf (stderr, "%s", errDesc); + else + fprintf (stderr, "System or external library call failed"); + if ((NULL != funcName) && (0 != funcName[0])) + fprintf (stderr, " in %s", funcName); + if (0 < lineNum) + fprintf (stderr, " at line %d", lineNum); + + fprintf (stderr, ".\nLast errno value: %d (%s)\n", (int) errno, + strerror (errno)); +#ifdef MHD_WINSOCK_SOCKETS + fprintf (stderr, "WSAGetLastError() value: %d\n", (int) WSAGetLastError ()); +#endif /* MHD_WINSOCK_SOCKETS */ + print_context (stderr); + fflush (stderr); + exit (99); +} + + +/** + * Pause execution for specified number of milliseconds. + * @param ms the number of milliseconds to sleep + */ +static void +_MHD_sleep (uint32_t ms) +{ +#if defined(_WIN32) + Sleep (ms); +#elif defined(HAVE_NANOSLEEP) + struct timespec slp = {(time_t) (ms / 1000), (long) ((ms % 1000) * 1000000)}; + struct timespec rmn; + int num_retries = 0; + while (0 != nanosleep (&slp, &rmn)) + { + if (EINTR != errno) + externalErrorExit (); + if (num_retries++ > 8) + break; + slp = rmn; + } +#elif defined(HAVE_USLEEP) + uint64_t us = ms * 1000; + do + { + uint64_t this_sleep; + if (999999 < us) + this_sleep = 999999; + else + this_sleep = us; + /* Ignore return value as it could be void */ + usleep ((useconds_t) this_sleep); + us -= this_sleep; + } while (us > 0); +#else + externalErrorExitDesc ("No sleep function available on this system"); +#endif +} + + +/** + * Monotonic-ish millisecond clock. + */ +static uint64_t +now_ms (void) +{ +#if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC) + struct timespec ts; + + if (0 == clock_gettime (CLOCK_MONOTONIC, &ts)) + return ((uint64_t) ts.tv_sec) * 1000 + (uint64_t) (ts.tv_nsec / 1000000); +#endif /* HAVE_CLOCK_GETTIME && CLOCK_MONOTONIC */ + return ((uint64_t) time (NULL)) * 1000; +} + + +/* ------------------------------------------------------------------ */ +/* Textual observation buffers */ +/* ------------------------------------------------------------------ */ + +/** + * A bounded, always NUL-terminated text accumulator. All observations + * (expected as well as actual) are rendered into such a buffer and the + * comparison is a plain strcmp(), which also gives readable diagnostics. + */ +struct obs_buf +{ + char d[OBS_SIZE]; + size_t len; + int overflow; +}; + + +static void +obs_reset (struct obs_buf *b) +{ + b->len = 0; + b->overflow = 0; + b->d[0] = 0; +} + + +static void +obs_addf (struct obs_buf *b, const char *fmt, ...) +{ + va_list ap; + int r; + size_t space; + + if (b->len >= sizeof(b->d) - 1) + { + b->overflow = 1; + return; + } + space = sizeof(b->d) - b->len; + va_start (ap, fmt); + r = vsnprintf (b->d + b->len, space, fmt, ap); + va_end (ap); + if (0 > r) + { + b->overflow = 1; + return; + } + if ((size_t) r >= space) + { + b->len = sizeof(b->d) - 1; + b->overflow = 1; + return; + } + b->len += (size_t) r; +} + + +/** + * Append @a data with all non-printable bytes escaped as \\xNN. + */ +static void +obs_add_escaped (struct obs_buf *b, const char *data, size_t size) +{ + size_t i; + + for (i = 0; i < size; ++i) + { + const unsigned char c = (unsigned char) data[i]; + + if ((0x20 <= c) && (0x7e >= c) && ('\\' != c)) + obs_addf (b, "%c", (char) c); + else if ('\\' == c) + obs_addf (b, "\\\\"); + else + obs_addf (b, "\\x%02x", (unsigned int) c); + } +} + + +/* ------------------------------------------------------------------ */ +/* The corpus */ +/* ------------------------------------------------------------------ */ + +/** + * Expectations for a single (possibly pipelined) request inside a stream. + */ +struct exp_req +{ + const char *method; + const char *url; + const char *version; + /** + * Expected upload body, NULL for "no body at all". + */ + const char *body; + /** + * Length of @a body, 0 means strlen(). + */ + size_t body_len; + /** + * Newline separated list of expected #MHD_HEADER_KIND elements. + */ + const char *hdrs; + /** + * Newline separated list of expected #MHD_GET_ARGUMENT_KIND elements. + */ + const char *args; + /** + * Newline separated list of expected #MHD_FOOTER_KIND elements. + */ + const char *foot; +}; + + +struct corpus_entry +{ + const char *name; + const char *desc; + const char *raw; + size_t raw_len; /**< 0 -> strlen (raw) */ + unsigned int n_req; /**< complete requests MHD must parse */ + unsigned int n_resp; /**< responses the client must see */ + int status; /**< first response status, 0 = any */ + int alive; /**< 1: keep-alive, 0: MHD must close */ + int deep; /**< also run three-way splits */ + struct exp_req reqs[MAX_REQS]; +}; + + +#define H_LOCAL "Host: localhost" CRLF +#define E_HOST "Host=localhost" + +static const struct corpus_entry corpus[] = { + /* ---- request line basics -------------------------------------- */ + { "get_11_host", + "baseline: request line + one header, incremental line assembly", + "GET / HTTP/1.1" CRLF H_LOCAL CRLF, 0, 1, 1, 200, 1, 1, + { { "GET", "/", "HTTP/1.1", NULL, 0, E_HOST, "", "" } } }, + { "get_10_nohost", + "HTTP/1.0 without Host: no header lines at all, must not keep alive", + "GET / HTTP/1.0" CRLF CRLF, 0, 1, 1, 200, 0, 1, + { { "GET", "/", "HTTP/1.0", NULL, 0, "", "", "" } } }, + { "head_11", + "HEAD: reply must carry no body, parser path is the same", + "HEAD / HTTP/1.1" CRLF H_LOCAL CRLF, 0, 1, 1, 200, 1, 0, + { { "HEAD", "/", "HTTP/1.1", NULL, 0, E_HOST, "", "" } } }, + + /* ---- query string handling ------------------------------------ */ + { "q_bare", + "single query argument without '=' -> NULL value", + "GET /?a HTTP/1.1" CRLF H_LOCAL CRLF, 0, 1, 1, 200, 1, 0, + { { "GET", "/", "HTTP/1.1", NULL, 0, E_HOST, "a=<NULL>", "" } } }, + { "q_two_bare", + "two arguments, both without '='", + "GET /?a&b HTTP/1.1" CRLF H_LOCAL CRLF, 0, 1, 1, 200, 1, 0, + { { "GET", "/", "HTTP/1.1", NULL, 0, E_HOST, "a=<NULL>\nb=<NULL>", "" } } }, + { "q_mixed", + "'=' in the first argument only", + "GET /?a=1&b HTTP/1.1" CRLF H_LOCAL CRLF, 0, 1, 1, 200, 1, 0, + { { "GET", "/", "HTTP/1.1", NULL, 0, E_HOST, "a=1\nb=<NULL>", "" } } }, + { "q_empty_val", + "'a=' must give an empty (not NULL) value", + "GET /?a= HTTP/1.1" CRLF H_LOCAL CRLF, 0, 1, 1, 200, 1, 0, + { { "GET", "/", "HTTP/1.1", NULL, 0, E_HOST, "a=", "" } } }, + { "q_trailing_amp", + "trailing '&' must not create a second, empty argument", + "GET /?a& HTTP/1.1" CRLF H_LOCAL CRLF, 0, 1, 1, 200, 1, 0, + { { "GET", "/", "HTTP/1.1", NULL, 0, E_HOST, "a=<NULL>", "" } } }, + { "q_no_key", + "'=v' must give an empty key with value 'v'", + "GET /?=v HTTP/1.1" CRLF H_LOCAL CRLF, 0, 1, 1, 200, 1, 0, + { { "GET", "/", "HTTP/1.1", NULL, 0, E_HOST, "=v", "" } } }, + { "q_pct", + "percent decoding of both key and value happens in place", + "GET /?a%20b=c%26d HTTP/1.1" CRLF H_LOCAL CRLF, 0, 1, 1, 200, 1, 0, + { { "GET", "/", "HTTP/1.1", NULL, 0, E_HOST, "a b=c&d", "" } } }, + { "q_frag", + "'#' is not special for a server: it stays part of the argument", + "GET /?a#frag HTTP/1.1" CRLF H_LOCAL CRLF, 0, 1, 1, 200, 1, 0, + { { "GET", "/", "HTTP/1.1", NULL, 0, E_HOST, "a#frag=<NULL>", "" } } }, + { "q_plus", + "'+' is decoded to a space in query arguments", + "GET /?a+b=c+d HTTP/1.1" CRLF H_LOCAL CRLF, 0, 1, 1, 200, 1, 0, + { { "GET", "/", "HTTP/1.1", NULL, 0, E_HOST, "a b=c d", "" } } }, + { "q_pct_url", + "percent decoding of the path part", + "GET /a%2Fb%20c HTTP/1.1" CRLF H_LOCAL CRLF, 0, 1, 1, 200, 1, 0, + { { "GET", "/a/b c", "HTTP/1.1", NULL, 0, E_HOST, "", "" } } }, + /* The next two entries have *no* header line at all, so the last parsed + element is a query argument with a NULL value. With a connection memory + limit below MHD_BUF_INC_SIZE this is the precondition of the read buffer + "shift back" underflow fixed in 29eaa56b. */ + { "q_only_10_nohdr", + "no headers at all + trailing arg without '=': read-buffer shift back", + "GET /x?a HTTP/1.0" CRLF CRLF, 0, 1, 1, 200, 0, 1, + { { "GET", "/x", "HTTP/1.0", NULL, 0, "", "a=<NULL>", "" } } }, + { "q_only_10_nohdr2", + "same, but two NULL-valued args (tail element is the second one)", + "GET /y?a&b HTTP/1.0" CRLF CRLF, 0, 1, 1, 200, 0, 1, + { { "GET", "/y", "HTTP/1.0", NULL, 0, "", "a=<NULL>\nb=<NULL>", "" } } }, + { "q_only_10_nohdr_val", + "control case: no headers but a trailing arg *with* '='", + "GET /z?a=1 HTTP/1.0" CRLF CRLF, 0, 1, 1, 200, 0, 0, + { { "GET", "/z", "HTTP/1.0", NULL, 0, "", "a=1", "" } } }, + + /* ---- Content-Length bodies ------------------------------------ */ + { "cl_exact", + "Content-Length body, delivered incrementally to the handler", + "POST /p HTTP/1.1" CRLF H_LOCAL "Content-Length: 5" CRLF CRLF "Hello", + 0, 1, 1, 200, 1, 1, + { { "POST", "/p", "HTTP/1.1", "Hello", 0, + E_HOST "\nContent-Length=5", "", "" } } }, + { "cl_zero", + "Content-Length: 0 must not wait for body bytes", + "POST /p HTTP/1.1" CRLF H_LOCAL "Content-Length: 0" CRLF CRLF, + 0, 1, 1, 200, 1, 0, + { { "POST", "/p", "HTTP/1.1", "", 0, + E_HOST "\nContent-Length=0", "", "" } } }, + { "cl_long", + "longer Content-Length body, forces several read-buffer rounds", + "POST /p HTTP/1.1" CRLF H_LOCAL "Content-Length: 40" CRLF CRLF + "0123456789abcdefghijklmnopqrstuvwxyz+-*/", + 0, 1, 1, 200, 1, 0, + { { "POST", "/p", "HTTP/1.1", + "0123456789abcdefghijklmnopqrstuvwxyz+-*/", 0, + E_HOST "\nContent-Length=40", "", "" } } }, + + /* ---- chunked bodies ------------------------------------------- */ + { "chunk_plain", + "plain chunked body without extensions", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "5" CRLF "Hello" CRLF "0" CRLF CRLF, + 0, 1, 1, 200, 1, 1, + { { "POST", "/c", "HTTP/1.1", "Hello", 0, + E_HOST "\nTransfer-Encoding=chunked", "", "" } } }, + /* The chunk-extension entries pin down that the CRLF that terminates the + chunk size line is consumed *together with* the extension. Failing to do + so (bug fixed in c13f4c64) shifts the whole chunk body by two bytes and + desynchronises the stream -> request smuggling. */ + { "chunk_ext_val", + "chunk extension 'ext=val': CRLF after the extension must be consumed", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "5;ext=val" CRLF "Hello" CRLF "0" CRLF CRLF, + 0, 1, 1, 200, 1, 1, + { { "POST", "/c", "HTTP/1.1", "Hello", 0, + E_HOST "\nTransfer-Encoding=chunked", "", "" } } }, + { "chunk_ext_noval", + "chunk extension without a value", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "5;ext" CRLF "Hello" CRLF "0" CRLF CRLF, + 0, 1, 1, 200, 1, 1, + { { "POST", "/c", "HTTP/1.1", "Hello", 0, + E_HOST "\nTransfer-Encoding=chunked", "", "" } } }, + { "chunk_ext_quoted", + "quoted chunk extension value containing ';'", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "5;ext=\"quoted;value\"" CRLF "Hello" CRLF "0" CRLF CRLF, + 0, 1, 1, 200, 1, 1, + { { "POST", "/c", "HTTP/1.1", "Hello", 0, + E_HOST "\nTransfer-Encoding=chunked", "", "" } } }, + { "chunk_ext_bws", + "'bad whitespace' between chunk size and ';' (lenient mode)", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "5 ;ext=val" CRLF "Hello" CRLF "0" CRLF CRLF, + 0, 1, 1, 200, 1, 1, + { { "POST", "/c", "HTTP/1.1", "Hello", 0, + E_HOST "\nTransfer-Encoding=chunked", "", "" } } }, + { "chunk_ext_last", + "chunk extension on the terminating chunk", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "5;a=b" CRLF "Hello" CRLF "0;z=y" CRLF CRLF, + 0, 1, 1, 200, 1, 1, + { { "POST", "/c", "HTTP/1.1", "Hello", 0, + E_HOST "\nTransfer-Encoding=chunked", "", "" } } }, + { "chunk_multi", + "several chunks are concatenated into one upload body", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "3" CRLF "abc" CRLF "2" CRLF "de" CRLF "1" CRLF "f" CRLF "0" CRLF CRLF, + 0, 1, 1, 200, 1, 1, + { { "POST", "/c", "HTTP/1.1", "abcdef", 0, + E_HOST "\nTransfer-Encoding=chunked", "", "" } } }, + { "chunk_lead_zeros", + "chunk size with leading zeros", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "0005" CRLF "Hello" CRLF "000" CRLF CRLF, + 0, 1, 1, 200, 1, 0, + { { "POST", "/c", "HTTP/1.1", "Hello", 0, + E_HOST "\nTransfer-Encoding=chunked", "", "" } } }, + { "chunk_trailers", + "trailer (footer) fields after the terminating chunk", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "5" CRLF "Hello" CRLF "0" CRLF "X-Trailer: v" CRLF "X-T2: w" CRLF CRLF, + 0, 1, 1, 200, 1, 1, + { { "POST", "/c", "HTTP/1.1", "Hello", 0, + E_HOST "\nTransfer-Encoding=chunked", "", "X-Trailer=v\nX-T2=w" } } }, + { "chunk_fold_trailer", + "obs-fold continuation inside a trailer field", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "5" CRLF "Hello" CRLF "0" CRLF "X-T: a" CRLF " b" CRLF CRLF, + 0, 1, 1, 200, 1, 1, + { { "POST", "/c", "HTTP/1.1", "Hello", 0, + E_HOST "\nTransfer-Encoding=chunked", "", "X-T=a b" } } }, + { "chunk_ext_trailers", + "chunk extension *and* trailers in the same message", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "5;ext=val" CRLF "Hello" CRLF "0" CRLF "X-Trailer: v" CRLF CRLF, + 0, 1, 1, 200, 1, 1, + { { "POST", "/c", "HTTP/1.1", "Hello", 0, + E_HOST "\nTransfer-Encoding=chunked", "", "X-Trailer=v" } } }, + { "chunk_empty", + "only the terminating chunk: empty upload body", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "0" CRLF CRLF, + 0, 1, 1, 200, 1, 0, + { { "POST", "/c", "HTTP/1.1", "", 0, + E_HOST "\nTransfer-Encoding=chunked", "", "" } } }, + + /* MHD keeps room for a chunk "header" of MHD_CHUNK_HEADER_REASONABLE_LEN + (24) bytes in the read buffer; a longer chunk extension forces the + buffer-grow path in the middle of the chunk size line. */ + { "chunk_ext_long", + "chunk extension longer than the reserved chunk header space", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "5;ext=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" CRLF "Hello" CRLF + "0" CRLF CRLF, + 0, 1, 1, 200, 1, 1, + { { "POST", "/c", "HTTP/1.1", "Hello", 0, + E_HOST "\nTransfer-Encoding=chunked", "", "" } } }, + { "chunk_multi_ext", + "a chunk extension on every single chunk", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "3;a=1" CRLF "abc" CRLF "2;b=2" CRLF "de" CRLF "1;c" CRLF "f" CRLF + "0;d=4" CRLF CRLF, + 0, 1, 1, 200, 1, 1, + { { "POST", "/c", "HTTP/1.1", "abcdef", 0, + E_HOST "\nTransfer-Encoding=chunked", "", "" } } }, + + /* ---- pipelining / desynchronisation --------------------------- */ + { "pipe2", + "two pipelined requests must yield exactly two parsed requests", + "GET /1 HTTP/1.1" CRLF H_LOCAL CRLF + "GET /2 HTTP/1.1" CRLF H_LOCAL CRLF, + 0, 2, 2, 200, 1, 1, + { { "GET", "/1", "HTTP/1.1", NULL, 0, E_HOST, "", "" }, + { "GET", "/2", "HTTP/1.1", NULL, 0, E_HOST, "", "" } } }, + { "pipe3", + "three pipelined requests, never two and never four", + "GET /1 HTTP/1.1" CRLF H_LOCAL CRLF + "GET /2?x=1 HTTP/1.1" CRLF H_LOCAL CRLF + "GET /3 HTTP/1.1" CRLF H_LOCAL CRLF, + 0, 3, 3, 200, 1, 0, + { { "GET", "/1", "HTTP/1.1", NULL, 0, E_HOST, "", "" }, + { "GET", "/2", "HTTP/1.1", NULL, 0, E_HOST, "x=1", "" }, + { "GET", "/3", "HTTP/1.1", NULL, 0, E_HOST, "", "" } } }, + { "pipe_chunk_get", + "chunked POST followed by a GET: the smuggling detector", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "5" CRLF "Hello" CRLF "0" CRLF CRLF + "GET /after HTTP/1.1" CRLF H_LOCAL CRLF, + 0, 2, 2, 200, 1, 1, + { { "POST", "/c", "HTTP/1.1", "Hello", 0, + E_HOST "\nTransfer-Encoding=chunked", "", "" }, + { "GET", "/after", "HTTP/1.1", NULL, 0, E_HOST, "", "" } } }, + { "pipe_chunkext_get", + "chunk *extension* + pipelined GET: smuggling detector for c13f4c64", + "POST /c HTTP/1.1" CRLF H_LOCAL "Transfer-Encoding: chunked" CRLF CRLF + "5;ext=val" CRLF "Hello" CRLF "0" CRLF CRLF + "GET /after HTTP/1.1" CRLF H_LOCAL CRLF, + 0, 2, 2, 200, 1, 1, + { { "POST", "/c", "HTTP/1.1", "Hello", 0, + E_HOST "\nTransfer-Encoding=chunked", "", "" }, + { "GET", "/after", "HTTP/1.1", NULL, 0, E_HOST, "", "" } } }, + { "pipe_cl_get", + "Content-Length POST followed by a GET", + "POST /p HTTP/1.1" CRLF H_LOCAL "Content-Length: 5" CRLF CRLF "Hello" + "GET /after HTTP/1.1" CRLF H_LOCAL CRLF, + 0, 2, 2, 200, 1, 0, + { { "POST", "/p", "HTTP/1.1", "Hello", 0, + E_HOST "\nContent-Length=5", "", "" }, + { "GET", "/after", "HTTP/1.1", NULL, 0, E_HOST, "", "" } } }, + + /* ---- header line edge cases ----------------------------------- */ + { "hdr_obs_fold", + "obs-fold continuation: CR, LF and the fold space each become one space", + "GET / HTTP/1.1" CRLF H_LOCAL "X-Fold: one" CRLF " two" CRLF CRLF, + 0, 1, 1, 200, 1, 1, + { { "GET", "/", "HTTP/1.1", NULL, 0, + E_HOST "\nX-Fold=one two", "", "" } } }, + /* Note: the CR and the LF of the folded line are each replaced by a space + and the leading whitespace of the continuation line is kept verbatim, so + a TAB survives as a TAB. */ + { "hdr_fold_multi", + "two consecutive obs-fold continuation lines, second one folded with TAB", + "GET / HTTP/1.1" CRLF H_LOCAL "X-F: one" CRLF " two" CRLF "\ttree" CRLF + CRLF, + 0, 1, 1, 200, 1, 1, + { { "GET", "/", "HTTP/1.1", NULL, 0, + E_HOST "\nX-F=one two \\x09tree", "", "" } } }, + { "hdr_empty_val", + "header with an empty value", + "GET / HTTP/1.1" CRLF H_LOCAL "X-Empty:" CRLF CRLF, + 0, 1, 1, 200, 1, 0, + { { "GET", "/", "HTTP/1.1", NULL, 0, E_HOST "\nX-Empty=", "", "" } } }, + { "hdr_dup", + "duplicated header names are reported twice, in order", + "GET / HTTP/1.1" CRLF H_LOCAL "X-Dup: 1" CRLF "X-Dup: 2" CRLF CRLF, + 0, 1, 1, 200, 1, 0, + { { "GET", "/", "HTTP/1.1", NULL, 0, + E_HOST "\nX-Dup=1\nX-Dup=2", "", "" } } }, + { "hdr_bare_lf", + "bare LF line terminators (lenient mode) must parse identically", + "GET / HTTP/1.1\n" H_LOCAL "X-B: v\n\n", + 0, 1, 1, 200, 1, 1, + { { "GET", "/", "HTTP/1.1", NULL, 0, E_HOST "\nX-B=v", "", "" } } }, + { "hdr_bare_cr", + "a bare CR inside a header value is rejected with 400 at the default " + "client discipline level", + "GET / HTTP/1.1" CRLF H_LOCAL "X-Cr: a\rb" CRLF CRLF, + 0, 0, 1, 400, 0, 1, + { { NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL } } }, + { "hdr_trail_ws", + "trailing whitespace in a header value must be stripped", + "GET / HTTP/1.1" CRLF H_LOCAL "X-Ws: val " CRLF CRLF, + 0, 1, 1, 200, 1, 0, + { { "GET", "/", "HTTP/1.1", NULL, 0, E_HOST "\nX-Ws=val", "", "" } } }, + { "hdr_lead_ws", + "several spaces after the colon are not part of the value", + "GET / HTTP/1.1" CRLF H_LOCAL "X-Lw: val" CRLF CRLF, + 0, 1, 1, 200, 1, 0, + { { "GET", "/", "HTTP/1.1", NULL, 0, E_HOST "\nX-Lw=val", "", "" } } }, + { "hdr_many", + "many small headers: exercises pool allocation and buffer growth", + "GET / HTTP/1.1" CRLF H_LOCAL + "A: 1" CRLF "B: 2" CRLF "C: 3" CRLF "D: 4" CRLF "E: 5" CRLF + "F: 6" CRLF "G: 7" CRLF "H: 8" CRLF CRLF, + 0, 1, 1, 200, 1, 0, + { { "GET", "/", "HTTP/1.1", NULL, 0, + E_HOST "\nA=1\nB=2\nC=3\nD=4\nE=5\nF=6\nG=7\nH=8", "", "" } } }, + /* Bare LF is accepted as a line terminator in the request header but NOT + inside the chunked framing (process_request_body() requires discipline + level < -2 for that), so this must be a clean 400 - and it must be a 400 + no matter where the stream is cut. */ + { "hdr_bare_lf_chunk", + "bare LF inside the chunked framing is rejected with 400", + "POST /c HTTP/1.1\n" H_LOCAL "Transfer-Encoding: chunked\n\n" + "5;ext=val\nHello\n0\n\n", + 0, 0, 1, 400, 0, 1, + { { NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL } } } +}; + +#define CORPUS_SIZE (sizeof(corpus) / sizeof(corpus[0])) + +/** + * Sweep of #MHD_OPTION_CONNECTION_MEMORY_LIMIT values; 0 means "use the MHD + * default". Everything below #MHD_BUF_INC_SIZE (1500) unlocks read-buffer + * code paths that are otherwise never taken. + */ +static const size_t mem_limits[] = { 256, 512, 1024, 1499, 4096, 0 }; + +#define MEM_LIMITS_SIZE (sizeof(mem_limits) / sizeof(mem_limits[0])) + +/** + * The range of client discipline levels in which the declared expectation of + * a corpus entry holds. + * + * Every entry that is not listed here is level independent. The bounds + * follow the predicates of connection.c directly: + * + * * a bare LF is a line terminator only while `MHD_ALLOW_BARE_LF_AS_CRLF_` + * (0 >= level) holds, and an obs-fold continuation only while + * `allow_folded` (0 >= level) holds, so both are meaningless above 0; + * * `allow_bws` is (2 > level), so the "bad whitespace" chunk-size line is + * meaningless above 1; + * * a bare CR is turned into a space by `bare_cr_as_sp` (-1 >= level) and + * kept by `bare_cr_keep` (-3 >= level), so the entry that expects a 400 + * only holds from level 0 upwards; + * * inside the chunked framing `bare_lf_as_crlf` is (-2 > level), so the + * entry that expects a 400 for a bare LF there only holds from -2 upwards. + */ +struct entry_discp_limit +{ + const char *name; + int min_discp; + int max_discp; +}; + +static const struct entry_discp_limit discp_limits[] = { + { "hdr_bare_lf", -3, 0 }, + { "hdr_obs_fold", -3, 0 }, + { "hdr_fold_multi", -3, 0 }, + { "chunk_fold_trailer", -3, 0 }, + { "chunk_ext_bws", -3, 1 }, + { "hdr_bare_cr", 0, 3 }, + { "hdr_bare_lf_chunk", -2, 3 } +}; + +#define DISCP_LIMITS_SIZE \ + (sizeof(discp_limits) / sizeof(discp_limits[0])) + +/** + * The profile selected by the environment, NULL if the built-in sweep is + * used (which is the case for a stock "make check"). + */ +static const struct MHD_OptMatrixProfile *test_prof; + +/** + * The (possibly adjusted) copy of the profile @a test_prof points to. + */ +static struct MHD_OptMatrixProfile test_prof_copy; + +/** + * The client discipline level really in effect, see + * mhd_opt_matrix_effective_discipline(). + */ +static int test_discp; + + +/** + * Check whether the declared expectation of @a name holds at the client + * discipline level currently in effect. + * + * @param name the name of the corpus entry + * @return non-zero if the entry may be run + */ +static int +entry_in_discp_range (const char *name) +{ + size_t i; + + for (i = 0; i < DISCP_LIMITS_SIZE; ++i) + { + if (0 == strcmp (name, discp_limits[i].name)) + return ((test_discp >= discp_limits[i].min_discp) && + (test_discp <= discp_limits[i].max_discp)) ? 1 : 0; + } + return 1; +} + + +/* ------------------------------------------------------------------ */ +/* Global state */ +/* ------------------------------------------------------------------ */ + +static int verbose; + +/** Current corpus entry under test (the test is strictly serialised). */ +static const struct corpus_entry *cur_entry; +/** Human readable description of the current split. */ +static char cur_split[128]; +/** Current connection memory limit. */ +static size_t cur_mem_limit; +/** Non-zero while the (slow) three-way split pass is enabled. */ +static int deep_enabled; + +static uint16_t global_port; + +/** Server side observations of the connection currently under test. */ +static struct obs_buf srv_obs; +/** Number of completed requests seen by the request handler. */ +static volatile unsigned int srv_n_req; +/** Set if the handler noticed something structurally impossible. */ +static volatile unsigned int srv_error; + +static volatile unsigned int conn_started; +static volatile unsigned int conn_closed; + +/** Total number of (entry x split x memory limit) combinations executed. */ +static unsigned long combos; +/** Number of combinations that could not fit into the memory pool. */ +static unsigned long combos_pool_limited; +/** Number of failures found. */ +static unsigned long failures; + + +static void +print_context (FILE *out) +{ + fprintf (out, " context: entry='%s' split=%s mem_limit=", + (NULL != cur_entry) ? cur_entry->name : "(none)", + cur_split); + if (0 == cur_mem_limit) + fprintf (out, "default\n"); + else + fprintf (out, "%u\n", (unsigned int) cur_mem_limit); + if (NULL != cur_entry) + fprintf (out, " entry purpose: %s\n", cur_entry->desc); +} + + +#if defined(HAVE_SIGNAL_H) && defined(SIGABRT) +/** + * Report the corpus entry and split offset when the library aborts (e.g. + * from an internal assertion), instead of dying anonymously. + */ +static void +abort_handler (int sig) +{ + (void) sig; + fprintf (stderr, + "\nFATAL: the library aborted while running a corpus entry.\n"); + print_context (stderr); + fflush (stderr); + _exit (1); +} + + +#endif /* HAVE_SIGNAL_H && SIGABRT */ + + +/** + * Panic callback: a library panic must be reported as a test failure that + * names the corpus entry and split offset. + */ +static void +test_panic (void *cls, const char *file, unsigned int line, const char *reason) +{ + (void) cls; + fprintf (stderr, + "\nFATAL: MHD panic at %s:%u: %s\n", + (NULL != file) ? file : "(unknown)", + line, + (NULL != reason) ? reason : "(no reason given)"); + print_context (stderr); + fflush (stderr); + _exit (1); +} + + +/* ------------------------------------------------------------------ */ +/* Request handler: records the observations */ +/* ------------------------------------------------------------------ */ + +struct req_state +{ + int marker; + unsigned int idx; + struct obs_buf body; +}; + + +struct iter_ctx +{ + struct obs_buf *out; + unsigned int num; +}; + + +static enum MHD_Result +value_iterator (void *cls, + enum MHD_ValueKind kind, + const char *key, + size_t key_size, + const char *value, + size_t value_size) +{ + struct iter_ctx *const ctx = (struct iter_ctx *) cls; + + (void) kind; + if (0 != ctx->num) + obs_addf (ctx->out, "\n"); + ctx->num++; + if (NULL != key) + obs_add_escaped (ctx->out, key, key_size); + else + obs_addf (ctx->out, "<NULLKEY>"); + obs_addf (ctx->out, "="); + if (NULL == value) + obs_addf (ctx->out, "<NULL>"); + else + obs_add_escaped (ctx->out, value, value_size); + return MHD_YES; +} + + +static void +dump_values (struct MHD_Connection *c, + enum MHD_ValueKind kind, + const char *label, + struct obs_buf *out) +{ + struct iter_ctx ctx; + + ctx.out = out; + ctx.num = 0; + obs_addf (out, " %s=[", label); + MHD_get_connection_values_n (c, kind, &value_iterator, &ctx); + obs_addf (out, "]\n"); +} + + +static enum MHD_Result +ahc_record (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **req_cls) +{ + /* The reply body is intentionally empty: that makes the reply of a HEAD + request byte-identical to the reply of a GET request and keeps the + client side response framing trivial. */ + static const char rp_data[] = ""; + struct req_state *rs; + struct MHD_Response *response; + enum MHD_Result ret; + + (void) cls; + if (NULL == *req_cls) + { + rs = (struct req_state *) malloc (sizeof (struct req_state)); + if (NULL == rs) + externalErrorExitDesc ("malloc() failed"); + rs->marker = 1; + rs->idx = srv_n_req; + obs_reset (&rs->body); + *req_cls = rs; + return MHD_YES; + } + rs = (struct req_state *) *req_cls; + if (0 != *upload_data_size) + { + obs_add_escaped (&rs->body, upload_data, *upload_data_size); + *upload_data_size = 0; + return MHD_YES; + } + + /* The request is complete: record everything we can observe. */ + obs_addf (&srv_obs, "REQ %u\n", rs->idx); + obs_addf (&srv_obs, " method=%s\n", (NULL != method) ? method : "(null)"); + obs_addf (&srv_obs, " url="); + if (NULL != url) + obs_add_escaped (&srv_obs, url, strlen (url)); + else + obs_addf (&srv_obs, "(null)"); + obs_addf (&srv_obs, "\n"); + obs_addf (&srv_obs, " version=%s\n", + (NULL != version) ? version : "(null)"); + obs_addf (&srv_obs, " body=[%s]\n", rs->body.d); + dump_values (connection, MHD_HEADER_KIND, "hdr", &srv_obs); + dump_values (connection, MHD_GET_ARGUMENT_KIND, "arg", &srv_obs); + dump_values (connection, MHD_FOOTER_KIND, "foot", &srv_obs); + + free (rs); + *req_cls = NULL; + srv_n_req++; + + response = + MHD_create_response_from_buffer (MHD_STATICSTR_LEN_ (rp_data), + (void *) rp_data, + MHD_RESPMEM_PERSISTENT); + if (NULL == response) + { + srv_error++; + return MHD_NO; + } + ret = MHD_queue_response (connection, MHD_HTTP_OK, response); + MHD_destroy_response (response); + if (MHD_YES != ret) + srv_error++; + return ret; +} + + +/** + * MHD is started with #MHD_USE_ERROR_LOG so that library complaints are + * available, but they are only printed with '-v -v' to keep the (very + * repetitive) output of the sweep readable. + */ +static void +test_log (void *cls, const char *fmt, va_list ap) +{ + (void) cls; + if (1 < verbose) + { + vfprintf (stderr, fmt, ap); + fflush (stderr); + } +} + + +static void +conn_notify (void *cls, + struct MHD_Connection *c, + void **socket_context, + enum MHD_ConnectionNotificationCode toe) +{ + (void) cls; (void) c; (void) socket_context; + if (MHD_CONNECTION_NOTIFY_STARTED == toe) + conn_started++; + else if (MHD_CONNECTION_NOTIFY_CLOSED == toe) + conn_closed++; +} + + +static void +req_completed (void *cls, + struct MHD_Connection *c, + void **req_cls, + enum MHD_RequestTerminationCode term_code) +{ + (void) cls; (void) c; (void) term_code; + if ((NULL != req_cls) && (NULL != *req_cls)) + { + free (*req_cls); + *req_cls = NULL; + } +} + + +/* ------------------------------------------------------------------ */ +/* Raw client */ +/* ------------------------------------------------------------------ */ + +struct client_result +{ + unsigned int n_resp; + int first_status; + int peer_closed; + int timed_out; + size_t rx_len; + char rx[RX_SIZE]; +}; + + +/** + * Case-insensitive comparison of @a size bytes (ASCII only). + */ +static int +mem_equal_ci (const char *a, const char *b, size_t size) +{ + size_t i; + + for (i = 0; i < size; ++i) + { + char ca = a[i]; + char cb = b[i]; + + if (('A' <= ca) && ('Z' >= ca)) + ca = (char) (ca - 'A' + 'a'); + if (('A' <= cb) && ('Z' >= cb)) + cb = (char) (cb - 'A' + 'a'); + if (ca != cb) + return 0; + } + return 1; +} + + +/** + * Count the number of complete HTTP responses in @a buf. + * + * @param buf the received bytes + * @param len the number of bytes in @a buf + * @param[out] first_status set to the status code of the first response + * @return the number of *complete* responses found + */ +static unsigned int +count_responses (const char *buf, size_t len, int *first_status) +{ + size_t pos = 0; + unsigned int num = 0; + + *first_status = 0; + while (pos < len) + { + const char *hdr_end; + const char *cl; + size_t hdr_len; + size_t body_len = 0; + int status; + size_t i; + + if (7 > len - pos) + break; + if (0 != memcmp (buf + pos, "HTTP/1.", 7)) + break; /* Not a well-formed response */ + hdr_end = NULL; + for (i = pos; i + 3 < len; ++i) + { + if (('\r' == buf[i]) && ('\n' == buf[i + 1]) && + ('\r' == buf[i + 2]) && ('\n' == buf[i + 3])) + { + hdr_end = buf + i + 4; + break; + } + } + if (NULL == hdr_end) + break; /* Incomplete header */ + hdr_len = (size_t) (hdr_end - (buf + pos)); + status = 0; + if ((pos + 12 <= len) && + ('0' <= buf[pos + 9]) && ('9' >= buf[pos + 9])) + status = (buf[pos + 9] - '0') * 100 + + (buf[pos + 10] - '0') * 10 + + (buf[pos + 11] - '0'); + if (0 == num) + *first_status = status; + /* Find "Content-Length:" inside this response header. */ + cl = NULL; + for (i = pos; i + 15 <= pos + hdr_len; ++i) + { + if (mem_equal_ci (buf + i, "content-length:", 15)) + { + cl = buf + i + 15; + break; + } + } + if (NULL != cl) + { + while ((cl < buf + pos + hdr_len) && (' ' == *cl)) + cl++; + body_len = 0; + while ((cl < buf + pos + hdr_len) && ('0' <= *cl) && ('9' >= *cl)) + { + body_len = body_len * 10 + (size_t) (*cl - '0'); + cl++; + } + } + if (pos + hdr_len + body_len > len) + break; /* Body incomplete */ + pos += hdr_len + body_len; + num++; + } + return num; +} + + +static MHD_socket +client_connect (void) +{ + MHD_socket sk; + struct sockaddr_in sa; + const MHD_SCKT_OPT_BOOL_ on_val = 1; + + sk = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (MHD_INVALID_SOCKET == sk) + externalErrorExitDesc ("Cannot create the client socket"); +#ifdef MHD_socket_nosignal_ + if (! MHD_socket_nosignal_ (sk)) + externalErrorExitDesc ("Cannot suppress SIGPIPE on the client socket"); +#endif /* MHD_socket_nosignal_ */ + if (0 != setsockopt (sk, IPPROTO_TCP, TCP_NODELAY, + (const void *) &on_val, sizeof (on_val))) + externalErrorExitDesc ("Cannot set TCP_NODELAY option"); + + memset (&sa, 0, sizeof (sa)); + sa.sin_family = AF_INET; + sa.sin_port = htons (global_port); + sa.sin_addr.s_addr = htonl (INADDR_LOOPBACK); + if (0 != connect (sk, (struct sockaddr *) &sa, sizeof (sa))) + externalErrorExitDesc ("Cannot connect() to the daemon"); + return sk; +} + + +static int +client_send_all (MHD_socket sk, const char *data, size_t size) +{ + size_t off = 0; + + while (off < size) + { + ssize_t s = MHD_send_ (sk, data + off, size - off); + + if (0 > s) + { + const int err = MHD_socket_get_error_ (); + + if (MHD_SCKT_ERR_IS_EINTR_ (err) || MHD_SCKT_ERR_IS_EAGAIN_ (err)) + continue; + return 0; /* Server closed the connection early, that is an observation */ + } + off += (size_t) s; + } + return 1; +} + + +/** + * Wait for a socket to become readable. + * + * @return 1 readable, 0 timeout, -1 error + */ +static int +wait_readable (MHD_socket sk, unsigned int timeout_ms) +{ + fd_set rs; + struct timeval tv; + int r; + + FD_ZERO (&rs); + FD_SET (sk, &rs); + tv.tv_sec = (time_t) (timeout_ms / 1000); + tv.tv_usec = (long) ((timeout_ms % 1000) * 1000); + r = select ((int) sk + 1, &rs, NULL, NULL, &tv); + if (0 > r) + { + if (EINTR == errno) + return 0; + return -1; + } + return (0 == r) ? 0 : 1; +} + + +/** + * Read the server answer. + * + * @param sk the socket to read from + * @param[out] r the result + * @param min_resp stop once that many complete responses have been read + * @param want_close keep reading until the peer closes the connection + * @param grace_ms extra silence to wait for after @a min_resp responses + */ +static void +client_read (MHD_socket sk, + struct client_result *r, + unsigned int min_resp, + int want_close, + unsigned int grace_ms) +{ + const uint64_t start = now_ms (); + + memset (r, 0, sizeof (*r)); + while (1) + { + const uint64_t el = now_ms () - start; + unsigned int tmo; + int wr; + ssize_t got; + + if (el >= READ_DEADLINE_MS) + { + r->timed_out = 1; + break; + } + r->n_resp = count_responses (r->rx, r->rx_len, &r->first_status); + if ((r->n_resp >= min_resp) && (! want_close)) + tmo = grace_ms; + else + tmo = (unsigned int) (READ_DEADLINE_MS - el); + wr = wait_readable (sk, tmo); + if (0 > wr) + { + r->peer_closed = 1; + break; + } + if (0 == wr) + { + if ((r->n_resp >= min_resp) && (! want_close)) + break; /* Done, connection stays alive */ + r->timed_out = 1; + break; + } + if (r->rx_len >= sizeof (r->rx)) + break; /* Way too much data, will be reported as a mismatch */ + got = MHD_recv_ (sk, r->rx + r->rx_len, sizeof (r->rx) - r->rx_len); + if (0 > got) + { + const int err = MHD_socket_get_error_ (); + + if (MHD_SCKT_ERR_IS_EINTR_ (err) || MHD_SCKT_ERR_IS_EAGAIN_ (err)) + continue; + r->peer_closed = 1; /* Connection reset counts as "closed" */ + break; + } + if (0 == got) + { + r->peer_closed = 1; + break; + } + r->rx_len += (size_t) got; + } + r->n_resp = count_responses (r->rx, r->rx_len, &r->first_status); +} + + +/** + * Wait until the daemon has finished tearing down all connections, so that + * the server side observations are stable. + * + * @return 1 on success, 0 on timeout + */ +static int +wait_conns_closed (void) +{ + const uint64_t start = now_ms (); + unsigned int spin = 0; + + while (conn_closed != conn_started) + { + if (1000 > spin) + spin++; + else + _MHD_sleep (1); + if (now_ms () - start > CLOSE_WAIT_MS) + return 0; + } + return 1; +} + + +/* ------------------------------------------------------------------ */ +/* One replay of one corpus entry */ +/* ------------------------------------------------------------------ */ + +/** + * Send @a e over a fresh connection, cut into @a n_cuts + 1 pieces at the + * offsets given in @a cuts, and render all observations into @a out. + */ +static void +run_stream (const struct corpus_entry *e, + const size_t *cuts, + unsigned int n_cuts, + unsigned int min_resp, + int want_close, + unsigned int grace_ms, + struct obs_buf *out) +{ + const size_t len = (0 != e->raw_len) ? e->raw_len : strlen (e->raw); + struct client_result cr; + MHD_socket sk; + size_t off = 0; + unsigned int i; + int send_ok = 1; + + obs_reset (&srv_obs); + srv_n_req = 0; + srv_error = 0; + + sk = client_connect (); + for (i = 0; i <= n_cuts; ++i) + { + const size_t end = (i < n_cuts) ? cuts[i] : len; + + if (end > off) + { + if (! client_send_all (sk, e->raw + off, end - off)) + { + send_ok = 0; + break; + } + off = end; + } + if (i < n_cuts) + _MHD_sleep (SPLIT_DELAY_MS); + } + client_read (sk, &cr, min_resp, want_close, grace_ms); + (void) MHD_socket_close_ (sk); + if (! wait_conns_closed ()) + { + fprintf (stderr, "WARNING: timeout waiting for connection teardown.\n"); + print_context (stderr); + } + + obs_reset (out); + obs_addf (out, "%s", srv_obs.d); + obs_addf (out, "nreq=%u\n", srv_n_req); + obs_addf (out, "nresp=%u status=%d closed=%d\n", + cr.n_resp, cr.first_status, cr.peer_closed); + if (cr.timed_out) + obs_addf (out, "TIMED_OUT\n"); + /* Note: whether the *client* managed to push out the rest of the request + before MHD closed the connection is inherently racy, so it must not be + part of the compared observation; 'closed=' already carries the relevant + information. It is only reported in verbose mode. */ + if ((! send_ok) && (1 < verbose)) + { + printf (" (send aborted by the server for entry '%s' split %s)\n", + e->name, cur_split); + fflush (stdout); + } + if (0 != srv_error) + obs_addf (out, "SERVER_ERROR=%u\n", srv_error); + if (srv_obs.overflow || out->overflow) + obs_addf (out, "OBS_OVERFLOW\n"); +} + + +/** + * Render the declared expectations of @a e into the very same format that + * run_stream() produces, so that a single strcmp() does the whole check. + */ +static void +build_expected (const struct corpus_entry *e, struct obs_buf *out) +{ + unsigned int i; + + obs_reset (out); + for (i = 0; i < e->n_req; ++i) + { + const struct exp_req *const r = &e->reqs[i]; + size_t bl; + + obs_addf (out, "REQ %u\n", i); + obs_addf (out, " method=%s\n", r->method); + obs_addf (out, " url="); + obs_add_escaped (out, r->url, strlen (r->url)); + obs_addf (out, "\n"); + obs_addf (out, " version=%s\n", r->version); + obs_addf (out, " body=["); + if (NULL != r->body) + { + bl = (0 != r->body_len) ? r->body_len : strlen (r->body); + obs_add_escaped (out, r->body, bl); + } + obs_addf (out, "]\n"); + obs_addf (out, " hdr=[%s]\n", r->hdrs); + obs_addf (out, " arg=[%s]\n", r->args); + obs_addf (out, " foot=[%s]\n", r->foot); + } + obs_addf (out, "nreq=%u\n", e->n_req); + obs_addf (out, "nresp=%u status=%d closed=%d\n", + e->n_resp, e->status, e->alive ? 0 : 1); +} + + +static void +hexdump (FILE *out, const char *data, size_t size) +{ + size_t i; + + for (i = 0; i < size; i += 16) + { + size_t j; + + fprintf (out, " %04x ", (unsigned int) i); + for (j = 0; j < 16; ++j) + { + if (i + j < size) + fprintf (out, "%02x ", (unsigned char) data[i + j]); + else + fprintf (out, " "); + } + fprintf (out, " |"); + for (j = 0; (j < 16) && (i + j < size); ++j) + { + const unsigned char c = (unsigned char) data[i + j]; + + fprintf (out, "%c", ((0x20 <= c) && (0x7e >= c)) ? (char) c : '.'); + } + fprintf (out, "|\n"); + } +} + + +static void +report_failure (const struct corpus_entry *e, + const char *what, + const char *expected, + const char *actual) +{ + const size_t len = (0 != e->raw_len) ? e->raw_len : strlen (e->raw); + + failures++; + fprintf (stderr, "\n==== FAILURE: %s ====\n", what); + fprintf (stderr, " entry: %s (%s)\n", e->name, e->desc); + fprintf (stderr, " split: %s\n", cur_split); + fprintf (stderr, " mem_limit: "); + if (0 == cur_mem_limit) + fprintf (stderr, "default\n"); + else + fprintf (stderr, "%u\n", (unsigned int) cur_mem_limit); + fprintf (stderr, " --- expected ---\n%s", expected); + fprintf (stderr, " --- observed ---\n%s", actual); + fprintf (stderr, " --- raw request (%u bytes) ---\n", (unsigned int) len); + hexdump (stderr, e->raw, len); + fflush (stderr); +} + + +/** + * Length of the leading "REQ ..." section of an observation, i.e. everything + * that describes the requests that MHD actually parsed. + */ +static size_t +obs_reqs_len (const struct obs_buf *o) +{ + const char *const p = strstr (o->d, "nreq="); + + return (NULL != p) ? (size_t) (p - o->d) : o->len; +} + + +/** + * Read an unsigned decimal number that follows @a key in the observation. + */ +static unsigned int +obs_get_uint (const struct obs_buf *o, const char *key) +{ + const char *const p = strstr (o->d, key); + + if (NULL == p) + return 0; + return (unsigned int) strtoul (p + strlen (key), NULL, 10); +} + + +/** + * Decide whether a deviation from the declared expectations is a legitimate + * *resource* limitation of an artificially small connection memory pool + * rather than a parser problem. + * + * Only these three signatures are accepted, and only when a non-default + * #MHD_OPTION_CONNECTION_MEMORY_LIMIT is configured: + * - 431/413/414/500 without a single completed request (the request header + * does not fit into the pool), + * - no reply at all and MHD closed the connection (it could not even build + * the reply header), + * - fewer requests than the stream contains but a successful reply (a + * pipelined stream that was only served partially). + * + * In particular a 400 Bad Request is *never* accepted as a resource + * limitation: a well-formed corpus entry that suddenly becomes malformed is + * exactly the kind of parser bug this test hunts for. + */ +static int +is_resource_degradation (const struct obs_buf *o, + const struct corpus_entry *e) +{ + const unsigned int nreq = obs_get_uint (o, "nreq="); + const unsigned int nresp = obs_get_uint (o, "nresp="); + const unsigned int status = obs_get_uint (o, "status="); + + if (0 == cur_mem_limit) + return 0; + if (NULL != strstr (o->d, "TIMED_OUT")) + return 0; + if ((0 == nreq) && + ((431 == status) || (413 == status) || + (414 == status) || (500 == status))) + return 1; + if ((0 == nresp) && (0 == status)) + return 1; + if ((nreq < e->n_req) && ((200 == status) || (0 == status))) + return 1; + return 0; +} + + +/** + * Status codes that a run under memory pressure may legitimately show. + * + * 0 means "no reply at all" (MHD could not build the reply header), 200 is + * the normal answer, the 4xx/5xx codes are the resource errors. A 400 is + * deliberately *not* in this set: a well-formed corpus entry that MHD + * suddenly considers malformed is a parser bug, not a resource problem. + */ +static int +status_acceptable_degraded (const struct obs_buf *o, + const struct corpus_entry *e) +{ + const unsigned int status = obs_get_uint (o, "status="); + + if ((unsigned int) e->status == status) + return 1; + switch (status) + { + case 0: + case 200: + case 413: + case 414: + case 431: + case 500: + return 1; + default: + break; + } + return 0; +} + + +/** + * Relaxed check used when the connection memory pool is too small for the + * corpus entry. + * + * Under memory pressure MHD may legitimately stop early: it may answer + * 431/413, it may fail to build a reply header, or it may serve only the + * first of several pipelined requests. What may *never* happen is that a + * request which MHD does parse is parsed differently, or that MHD conjures up + * more requests than the byte stream contains (that would be smuggling). + * + * So the requests that were parsed must form an exact prefix of the declared + * expectations. + * + * @return 1 if acceptable, 0 on a real mismatch + */ +static int +check_relaxed (const struct obs_buf *exp, const struct obs_buf *got) +{ + const size_t gl = obs_reqs_len (got); + const size_t el = obs_reqs_len (exp); + + if (gl > el) + return 0; /* More requests parsed than the stream contains */ + return 0 == memcmp (exp->d, got->d, gl); +} + + +/** + * Build the list of split offsets that will be exercised for a request of + * @a len bytes. + * + * @return the number of offsets stored in @a offs + */ +static unsigned int +build_split_offsets (size_t len, size_t *offs, unsigned int max_offs) +{ + unsigned int n = 0; + size_t s; + + for (s = 1; (s < len) && (n < max_offs); ++s) + { + if ((s <= SPLIT_DENSE_LIMIT) || (0 == (s % SPLIT_STRIDE))) + offs[n++] = s; + } + return n; +} + + +/* ------------------------------------------------------------------ */ +/* Main test driver */ +/* ------------------------------------------------------------------ */ + +static struct MHD_Daemon * +start_daemon_with_limit (size_t limit) +{ + struct MHD_Daemon *d; + struct MHD_OptMatrixProfile prof; + struct MHD_OptionItem ops[8]; + unsigned int flags; + + /* The profile (if any) supplies the discipline level, the threading mode + and the polling backend; the memory limit is the one of the sweep, which + the caller has already taken from the profile. */ + if (NULL != test_prof) + prof = *test_prof; + else + { + memset (&prof, 0, sizeof (prof)); + prof.name = "built-in"; + prof.threading = MHD_OPT_MATRIX_THR_INTERNAL; + prof.poll_backend = MHD_OPT_MATRIX_POLL_SELECT; + } + prof.mem_limit = limit; + if (0 == mhd_opt_matrix_fill_options (&prof, ops, + (unsigned int) (sizeof (ops) + / sizeof (ops[0])))) + externalErrorExitDesc ("The daemon option array is too small"); + /* The client of this test is a plain blocking socket client, so external + polling is served with an internal polling thread instead. */ + flags = mhd_opt_matrix_daemon_flags (&prof, MHD_USE_ERROR_LOG, 0); + d = MHD_start_daemon (flags, + 0, NULL, NULL, + &ahc_record, NULL, + MHD_OPTION_ARRAY, ops, + MHD_OPTION_EXTERNAL_LOGGER, &test_log, NULL, + MHD_OPTION_NOTIFY_CONNECTION, &conn_notify, NULL, + MHD_OPTION_NOTIFY_COMPLETED, &req_completed, NULL, + MHD_OPTION_CONNECTION_TIMEOUT, + (unsigned int) 8, + MHD_OPTION_END); + return d; +} + + +/** + * Run one corpus entry through the full split sweep at the current memory + * limit. + */ +static void +run_entry (const struct corpus_entry *e, unsigned long *split_counter) +{ + const size_t len = (0 != e->raw_len) ? e->raw_len : strlen (e->raw); + struct obs_buf expected; + struct obs_buf ref; + struct obs_buf got; + size_t *offs; + unsigned int n_offs; + unsigned int i; + unsigned int stride; + int degraded; + int ref_bad = 0; + const struct obs_buf *cmp; + unsigned int ref_resp; + unsigned int min_resp; + int want_close; + + cur_entry = e; + build_expected (e, &expected); + + /* 1) Unsplit reference run. */ + snprintf (cur_split, sizeof (cur_split), "none (unsplit)"); + run_stream (e, NULL, 0, e->n_resp, ! e->alive, READ_GRACE_MS, &ref); + combos++; + (*split_counter)++; + + degraded = (0 != strcmp (expected.d, ref.d)) && + is_resource_degradation (&ref, e); + if ((0 != strcmp (expected.d, ref.d)) && (! degraded)) + { + /* Either the default connection memory pool is in use, or the deviation + does not look like a resource limitation: in both cases the corpus + entry must match its declared expectations exactly. */ + report_failure (e, "unsplit run does not match the expectations", + expected.d, ref.d); + /* The reference is useless as a comparison base now; keep checking the + split runs against the declared expectations so that the report names + the concrete split offsets that are affected. */ + ref_bad = 1; + } + else if (degraded) + { + combos_pool_limited++; + if (! check_relaxed (&expected, &ref)) + { + report_failure (e, + "unsplit run with a small memory pool parsed a request " + "differently than expected", + expected.d, ref.d); + return; + } + if (verbose) + { + printf (" %-22s degraded by the memory pool, relaxed checking\n", + e->name); + fflush (stdout); + } + } + + /* Derive the read strategy for the split runs from the reference run, so + that the split runs never have to wait for a timeout. */ + ref_resp = 0; + if (1) + { + const char *p = strstr (ref.d, "nresp="); + + if (NULL != p) + ref_resp = (unsigned int) strtoul (p + 6, NULL, 10); + } + if (! degraded) + { + const char *p = strstr (ref.d, "closed="); + + min_resp = ref_resp; + want_close = (NULL != p) ? ('0' != p[7]) : 0; + stride = 1; + } + else + { + /* The client side numbers are not reproducible under memory pressure; + stop as soon as the first reply is complete (or as soon as MHD closes + the connection if it does not reply at all) and only compare the + server side observations. */ + min_resp = (0 != ref_resp) ? 1 : 0; + want_close = (0 != ref_resp) ? 0 : 1; + stride = 3; /* Sample: these runs are slower and less informative */ + } + + cmp = ref_bad ? &expected : &ref; + + /* 2) Two-way splits. */ + offs = (size_t *) malloc (sizeof (size_t) * (len + 1)); + if (NULL == offs) + externalErrorExitDesc ("malloc() failed"); + n_offs = build_split_offsets (len, offs, (unsigned int) len + 1); + for (i = 0; i < n_offs; i += stride) + { + snprintf (cur_split, sizeof (cur_split), "%u", (unsigned int) offs[i]); + run_stream (e, offs + i, 1, min_resp, want_close, 0, &got); + combos++; + (*split_counter)++; + if (degraded) + { + combos_pool_limited++; + if ((! check_relaxed (&expected, &got)) || + (! status_acceptable_degraded (&got, e)) || + (NULL != strstr (got.d, "TIMED_OUT"))) + report_failure (e, + "split point with a small memory pool parsed a " + "request differently than expected", + expected.d, got.d); + } + else if (0 != strcmp (cmp->d, got.d)) + report_failure (e, + ref_bad ? + "split point does not match the expectations" : + "split point yields a different parse than the " + "unsplit run", + cmp->d, got.d); + if (MAX_FAILURES < failures) + { + free (offs); + return; + } + } + + /* 3) Three-way splits for the marked entries. */ + if (e->deep && deep_enabled && (! degraded) && (2 < len)) + { + unsigned int done = 0; + unsigned int a; + const unsigned int step = + (unsigned int) (((len * len) / (2 * DEEP_SPLIT_MAX)) + 1); + unsigned int ctr = 0; + + for (a = 1; (a + 1 < (unsigned int) len) && (done < DEEP_SPLIT_MAX); ++a) + { + unsigned int b; + + for (b = a + 1; (b < (unsigned int) len) && (done < DEEP_SPLIT_MAX); ++b) + { + size_t two[2]; + + if (0 != (ctr++ % step)) + continue; + two[0] = a; + two[1] = b; + snprintf (cur_split, sizeof (cur_split), "%u+%u", a, b); + run_stream (e, two, 2, min_resp, want_close, 0, &got); + combos++; + (*split_counter)++; + done++; + if (0 != strcmp (cmp->d, got.d)) + report_failure (e, + ref_bad ? + "three-way split does not match the expectations" : + "three-way split yields a different parse than " + "the unsplit run", + cmp->d, got.d); + if (MAX_FAILURES < failures) + { + free (offs); + return; + } + } + } + } + free (offs); +} + + +int +main (int argc, char *const *argv) +{ + const uint64_t t_start = now_ms (); + unsigned int mi; + int i; + const char *only = NULL; + unsigned long total_splits = 0; + size_t sweep[MEM_LIMITS_SIZE]; + unsigned int sweep_size; + unsigned long entries_skipped = 0; + + for (i = 1; i < argc; ++i) + { + if ((0 == strcmp (argv[i], "-v")) || (0 == strcmp (argv[i], "--verbose"))) + verbose++; + else if (0 == strncmp (argv[i], "--entry=", 8)) + only = argv[i] + 8; + else if ('-' != argv[i][0]) + only = argv[i]; + } + + strcpy (cur_split, "(not started)"); + +#if defined(HAVE_SIGNAL_H) && defined(SIGPIPE) + if (MHD_YES != MHD_is_feature_supported (MHD_FEATURE_AUTOSUPPRESS_SIGPIPE)) + { + if (SIG_ERR == signal (SIGPIPE, SIG_IGN)) + externalErrorExitDesc ("Error suppressing SIGPIPE signal"); + } +#endif /* HAVE_SIGNAL_H && SIGPIPE */ +#if defined(HAVE_SIGNAL_H) && defined(SIGABRT) + (void) signal (SIGABRT, &abort_handler); +#endif /* HAVE_SIGNAL_H && SIGABRT */ + + /* Deliberately NOT mhd_panic_tripwire.h: this test installs its own + panic and SIGABRT handlers, which print the failing input and the + daemon options along with the diagnostic. That context is worth + more here than the generic tripwire, and the two would collide + (whichever runs last wins). See TESTING.md, P5. */ + MHD_set_panic_func (&test_panic, NULL); + mhd_opt_matrix_print_notice ("test_raw_requests"); + + test_prof = mhd_opt_matrix_from_env (); + if (NULL != test_prof) + { + char desc[256]; + + test_prof_copy = *test_prof; + test_prof = &test_prof_copy; + if (! mhd_opt_matrix_profile_supported (test_prof)) + { + printf ("test_raw_requests: the selected profile %s is not supported " + "by this build, the test is skipped.\n", + mhd_opt_matrix_describe (test_prof, desc, sizeof (desc))); + return 77; + } + /* One profile pins one memory limit; the built-in sweep is replaced. */ + sweep[0] = test_prof_copy.mem_limit; + sweep_size = 1; + } + else + { + for (mi = 0; mi < MEM_LIMITS_SIZE; ++mi) + sweep[mi] = mem_limits[mi]; + sweep_size = (unsigned int) MEM_LIMITS_SIZE; + } + test_discp = mhd_opt_matrix_effective_discipline (test_prof); + + for (mi = 0; mi < sweep_size; ++mi) + { + struct MHD_Daemon *d; + const union MHD_DaemonInfo *dinfo; + size_t ci; + + cur_mem_limit = sweep[mi]; + /* The three-way split pass is expensive; run it for the smallest pool + size that is not permanently degraded and for the default pool. */ + deep_enabled = ((1 == sweep_size) || (1 == mi) || (sweep_size - 1 == mi)); + conn_started = 0; + conn_closed = 0; + d = start_daemon_with_limit (cur_mem_limit); + if (NULL == d) + { + fprintf (stderr, "Failed to start the daemon with memory limit %u.\n", + (unsigned int) cur_mem_limit); + return 99; + } + dinfo = MHD_get_daemon_info (d, MHD_DAEMON_INFO_BIND_PORT); + if ((NULL == dinfo) || (0 == dinfo->port)) + { + MHD_stop_daemon (d); + fprintf (stderr, "Cannot get the daemon port.\n"); + return 99; + } + global_port = dinfo->port; + + if (verbose) + { + printf ("--- connection memory limit: "); + if (0 == cur_mem_limit) + printf ("default ---\n"); + else + printf ("%u ---\n", (unsigned int) cur_mem_limit); + fflush (stdout); + } + + for (ci = 0; ci < CORPUS_SIZE; ++ci) + { + unsigned long before = total_splits; + + if ((NULL != only) && (0 != strcmp (only, corpus[ci].name))) + continue; + if (! entry_in_discp_range (corpus[ci].name)) + { + entries_skipped++; + if (verbose) + printf (" %-22s skipped: its expectation is only valid for a " + "different client discipline level\n", corpus[ci].name); + continue; + } + run_entry (&corpus[ci], &total_splits); + if (1 < verbose) + { + printf (" %-22s %lu runs\n", corpus[ci].name, + total_splits - before); + fflush (stdout); + } + if (MAX_FAILURES < failures) + break; + } + MHD_stop_daemon (d); + cur_entry = NULL; + if (MAX_FAILURES < failures) + break; + } + + printf ("\ntest_raw_requests: %u corpus entries, %u memory limits, " + "%lu combinations executed (%lu pool-limited, %lu entry runs " + "skipped as out of the discipline range), %.1f s\n", + (unsigned int) ((NULL == only) ? CORPUS_SIZE : 1), + sweep_size, + combos, + combos_pool_limited, + entries_skipped, + (double) (now_ms () - t_start) / 1000.0); + if (0 != failures) + { + printf ("test_raw_requests: FAILED (%lu failure(s))\n", failures); + fflush (stdout); + return 1; + } + printf ("test_raw_requests: PASSED\n"); + fflush (stdout); + return 0; +} diff --git a/src/microhttpd/test_response_entries.c b/src/microhttpd/test_response_entries.c @@ -15,6 +15,10 @@ #include <string.h> #include <microhttpd.h> +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + static int expect_str (const char *actual, const char *expected) @@ -699,7 +703,8 @@ main (int argc, "Keep-aLive, token-1")) { fprintf (stderr, - "Cannot add \"Connection\" header with \"Keep-aLive, token-1\".\n"); + "Cannot add \"Connection\" header with \"Keep-aLive, token-1\".\n") + ; MHD_destroy_response (r); return 4; } @@ -712,7 +717,8 @@ main (int argc, "Keep-aLive, token-2")) { fprintf (stderr, - "Cannot add \"Connection\" header with \"Keep-aLive, token-2\".\n"); + "Cannot add \"Connection\" header with \"Keep-aLive, token-2\".\n") + ; MHD_destroy_response (r); return 4; } diff --git a/src/microhttpd/test_rq_shift_back.c b/src/microhttpd/test_rq_shift_back.c @@ -0,0 +1,651 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + libmicrohttpd is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 2, or (at your + option) any later version. + + libmicrohttpd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libmicrohttpd; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +/** + * @file test_rq_shift_back.c + * @brief Regression test for the read buffer "shift back" underflow that + * happened when the last element of the request headers list was + * a query argument without the '=' character. + * + * The "shift back" optimisation at the end of get_req_headers() re-uses the + * tail of the parsed request header area for the read buffer. It computes + * the end of the last element of the @a headers_received list as + * "value + value_size". A query argument given without '=' is stored with + * a NULL @a value, so the computed end was NULL, the resulting shift size + * was huge and the read buffer became a wild pointer. + * + * The optimisation is performed only when the space in the connection + * memory pool is small (MHD_BUF_INC_SIZE > read_buffer_size), therefore the + * daemons are started with a small MHD_OPTION_CONNECTION_MEMORY_LIMIT. + * The tail of the headers list is a query argument only when the request has + * no header lines at all, as the query arguments are added while the request + * line is parsed and the header fields are appended afterwards. + * + * The test honours the daemon option matrix of mhd_opt_matrix.h: when one of + * the MHD_TEST_* environment variables selects a profile, the built-in sweep + * of connection memory limits is replaced by the memory limit of the profile + * and the threading/polling mode of the profile is used as well. Without + * those variables nothing changes. As the sub-cases need a pool that can + * still hold the request, a profile asking for less than + * #MIN_POOL_FOR_TEST bytes is raised to that value and the adjustment is + * reported. + * + * @author Christian Grothoff + */ +#include "MHD_config.h" +#include "platform.h" +#include <microhttpd.h> +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <errno.h> + +#ifdef HAVE_STRINGS_H +#include <strings.h> +#endif /* HAVE_STRINGS_H */ + +#ifndef WINDOWS +#include <unistd.h> +#endif /* ! WINDOWS */ + +#ifdef HAVE_SIGNAL_H +#include <signal.h> +#endif /* HAVE_SIGNAL_H */ + +#include "mhd_sockets.h" /* only macros used */ +#include "mhd_opt_matrix.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + +#ifndef MHD_STATICSTR_LEN_ +/** + * Determine length of static string / macro strings at compile time. + */ +#define MHD_STATICSTR_LEN_(macro) (sizeof(macro) / sizeof(char) - 1) +#endif /* ! MHD_STATICSTR_LEN_ */ + +/** + * The smallest connection memory pool this test can work with: below that + * MHD cannot even build the reply header and the sub-cases would fail for a + * reason that has nothing to do with the "shift back" code. + */ +#define MIN_POOL_FOR_TEST 512 + +#if MHD_VERSION >= 0x00097701 +#define TEST_MK_RESPONSE(len,str) \ + MHD_create_response_from_buffer_static ((len), (str)) +#else /* MHD_VERSION < 0x00097701 */ +#define TEST_MK_RESPONSE(len,str) \ + MHD_create_response_from_buffer ((len), (void *) (str), \ + MHD_RESPMEM_PERSISTENT) +#endif /* MHD_VERSION < 0x00097701 */ + +/** + * The exit code for a failed test. + */ +#define EXIT_FAILED_TEST 1 + +/** + * The exit code for a failure of the test framework itself. + */ +#define EXIT_HARD_ERROR 99 + +/** + * The maximum time (in seconds) to wait for any single socket operation. + */ +#define TEST_SOCKET_TIMEOUT_SEC 5 + +/** + * The body of the response sent by the test daemon. + */ +#define RESP_BODY "OK" + +/** + * The expected start of the response. + */ +#define EXPECTED_STATUS "HTTP/1.1 200" + +/** + * Be verbose. + */ +static int verbose; + + +_MHD_NORETURN static void +hard_error (const char *desc, + int line_num) +{ + fprintf (stderr, + "HARD ERROR: %s at line %d. Last errno: %d (%s).\n", + desc, + line_num, + (int) errno, + strerror (errno)); + fflush (stderr); + exit (EXIT_HARD_ERROR); +} + + +#define hardErrorExit(desc) hard_error ((desc), __LINE__) + + +/** + * The handler of the requests, always replies with a tiny static response. + */ +static enum MHD_Result +ahc_reply (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **req_cls) +{ + static int marker; + struct MHD_Response *resp; + enum MHD_Result ret; + + (void) cls; (void) url; (void) method; (void) version; + (void) upload_data; + + if (NULL == *req_cls) + { + *req_cls = &marker; + return MHD_YES; + } + if (0 != *upload_data_size) + { + *upload_data_size = 0; + return MHD_YES; + } + resp = TEST_MK_RESPONSE (MHD_STATICSTR_LEN_ (RESP_BODY), + RESP_BODY); + if (NULL == resp) + return MHD_NO; + ret = MHD_queue_response (connection, + MHD_HTTP_OK, + resp); + MHD_destroy_response (resp); + return ret; +} + + +/** + * Set send and receive timeouts on the socket so that the test cannot + * block indefinitely. + * + * @param sk the socket to set the timeouts on + * @return non-zero if succeed, zero if failed + */ +static int +set_socket_timeouts (MHD_socket sk) +{ +#if defined(MHD_WINSOCK_SOCKETS) + DWORD tv = (DWORD) (TEST_SOCKET_TIMEOUT_SEC * 1000); +#else /* ! MHD_WINSOCK_SOCKETS */ + struct timeval tv; + + tv.tv_sec = (time_t) TEST_SOCKET_TIMEOUT_SEC; + tv.tv_usec = 0; +#endif /* ! MHD_WINSOCK_SOCKETS */ + if (0 != setsockopt (sk, + SOL_SOCKET, + SO_RCVTIMEO, + (const void *) &tv, + (socklen_t) sizeof (tv))) + return 0; + if (0 != setsockopt (sk, + SOL_SOCKET, + SO_SNDTIMEO, + (const void *) &tv, + (socklen_t) sizeof (tv))) + return 0; + return ! 0; +} + + +/** + * Open a TCP connection to the given port on the loopback interface. + * + * @param port the port to connect to + * @return the connected socket, MHD_INVALID_SOCKET on failure + */ +static MHD_socket +connect_to_port (uint16_t port) +{ + MHD_socket sk; + struct sockaddr_in sa; + + sk = socket (PF_INET, + SOCK_STREAM, + 0); + if (MHD_INVALID_SOCKET == sk) + return MHD_INVALID_SOCKET; + if (! set_socket_timeouts (sk)) + { + (void) MHD_socket_close_ (sk); + return MHD_INVALID_SOCKET; + } + memset (&sa, + 0, + sizeof (sa)); + sa.sin_family = AF_INET; + sa.sin_port = htons (port); + sa.sin_addr.s_addr = htonl (INADDR_LOOPBACK); + if (0 != connect (sk, + (const struct sockaddr *) &sa, + (socklen_t) sizeof (sa))) + { + (void) MHD_socket_close_ (sk); + return MHD_INVALID_SOCKET; + } + return sk; +} + + +/** + * Send all the given data over the socket. + * + * @param sk the socket to use + * @param buf the data to send + * @param size the size of @a buf + * @return non-zero if succeed, zero if failed + */ +static int +send_all (MHD_socket sk, + const char *buf, + size_t size) +{ + size_t off = 0; + + while (off < size) + { + ssize_t res; + + res = MHD_send_ (sk, + buf + off, + size - off); + if (0 > res) + { + if (MHD_SCKT_ERR_IS_EINTR_ (MHD_socket_get_error_ ())) + continue; + return 0; + } + if (0 == res) + return 0; + off += (size_t) res; + } + return ! 0; +} + + +/** + * Receive data from the socket until @a min_size bytes are received, the + * remote side closes the connection or the receive timeout expires. + * + * @param sk the socket to use + * @param[out] buf the buffer to fill + * @param buf_size the size of @a buf, the result is always zero-terminated, + * therefore at most @a buf_size minus one bytes are received + * @param min_size stop receiving as soon as this number of bytes is collected + * @return the number of the received bytes + */ +static size_t +recv_reply (MHD_socket sk, + char *buf, + size_t buf_size, + size_t min_size) +{ + size_t off = 0; + + while ((off < min_size) && (off + 1 < buf_size)) + { + ssize_t res; + + res = MHD_recv_ (sk, + buf + off, + buf_size - 1 - off); + if (0 > res) + { + if (MHD_SCKT_ERR_IS_EINTR_ (MHD_socket_get_error_ ())) + continue; + break; /* Timeout or error */ + } + if (0 == res) + break; /* The remote side closed the connection */ + off += (size_t) res; + } + buf[off] = 0; + return off; +} + + +/** + * The description of a single sub-case. + */ +struct SubCase +{ + /** + * The human readable name of the sub-case. + */ + const char *name; + + /** + * The raw request (and any trailing data) to be sent in a single write. + */ + const char *request; +}; + + +/** + * The sub-cases. + * The sub-cases with a trailing query argument without '=' and without any + * header line are the ones triggering the bug. The requests with some + * trailing data force a non-zero read buffer offset, which turns the + * underflow into a memmove() with a wild destination pointer (detected + * even when the asserts are disabled), therefore these variants are run + * first. The control sub-cases are run before them, so that a failure of + * the test harness itself can be distinguished from the bug. + */ +static const struct SubCase subcases[] = { + /* Controls, must always succeed. */ + { "no query arguments at all (control)", + "GET /a HTTP/1.0\r\n\r\n" }, + { "no query arguments at all, with trailing data (control)", + "GET /a HTTP/1.0\r\n\r\nZZZZZZZZ" }, + { "empty argument value (control)", + "GET /a?x= HTTP/1.0\r\n\r\n" }, + { "empty argument value, with trailing data (control)", + "GET /a?x= HTTP/1.0\r\n\r\nZZZZZZZZ" }, + { "non-empty argument value (control)", + "GET /a?x=1 HTTP/1.0\r\n\r\n" }, + { "non-empty argument value, with trailing data (control)", + "GET /a?x=1 HTTP/1.0\r\n\r\nZZZZZZZZ" }, + { "argument without '=' plus a header line (control)", + "GET /a?x HTTP/1.1\r\nHost: h\r\nConnection: close\r\n\r\n" }, + { "arguments without '=' plus a header line (control)", + "GET /a?x&y HTTP/1.1\r\nHost: h\r\nConnection: close\r\n\r\n" }, + { "argument with trailing '&' plus a header line (control)", + "GET /a?x& HTTP/1.1\r\nHost: h\r\nConnection: close\r\n\r\n" }, + /* The sub-cases triggering the bug. */ + { "single argument without '=', with trailing data", + "GET /a?x HTTP/1.0\r\n\r\nZZZZZZZZ" }, + { "single argument without '='", + "GET /a?x HTTP/1.0\r\n\r\n" }, + { "two arguments, both without '=', with trailing data", + "GET /a?x&y HTTP/1.0\r\n\r\nZZZZZZZZ" }, + { "two arguments, both without '='", + "GET /a?x&y HTTP/1.0\r\n\r\n" }, + { "argument with value followed by argument without '=', trailing data", + "GET /a?x=1&y HTTP/1.0\r\n\r\nZZZZZZZZ" }, + { "argument with value followed by argument without '='", + "GET /a?x=1&y HTTP/1.0\r\n\r\n" }, + { "single argument with trailing '&', with trailing data", + "GET /a?x& HTTP/1.0\r\n\r\nZZZZZZZZ" }, + { "single argument with trailing '&'", + "GET /a?x& HTTP/1.0\r\n\r\n" }, + { "percent-encoded argument name without '=', with trailing data", + "GET /a?%78%79 HTTP/1.0\r\n\r\nZZZZZZZZ" }, + { "percent-encoded argument name without '='", + "GET /a?%78%79 HTTP/1.0\r\n\r\n" }, + { "plus-encoded argument name without '=', with trailing data", + "GET /a?x+y HTTP/1.0\r\n\r\nZZZZZZZZ" }, + { "plus-encoded argument name without '='", + "GET /a?x+y HTTP/1.0\r\n\r\n" } +}; + +/** + * The values for MHD_OPTION_CONNECTION_MEMORY_LIMIT to be tested. + * All of them must be small enough to keep the read buffer size below + * MHD_BUF_INC_SIZE (1500), otherwise the "shift back" code is not used + * at all. + */ +static const size_t mem_limits[] = { 512, 1024 }; + +/** + * The profile selected by the environment, NULL if the built-in sweep is + * used (which is the case for a stock "make check"). + */ +static const struct MHD_OptMatrixProfile *test_prof; + +/** + * The (possibly adjusted) copy of the profile @a test_prof points to. + */ +static struct MHD_OptMatrixProfile test_prof_copy; + + +/** + * Run a single sub-case against the given port. + * + * @param port the port of the test daemon + * @param sc the sub-case to run + * @return non-zero if the sub-case succeeded, zero if it failed + */ +static int +run_subcase (uint16_t port, + const struct SubCase *sc) +{ + MHD_socket sk; + char buf[1024]; + size_t got; + int ret = ! 0; + + sk = connect_to_port (port); + if (MHD_INVALID_SOCKET == sk) + hardErrorExit ("Cannot connect to the test daemon"); + + if (! send_all (sk, + sc->request, + strlen (sc->request))) + { + fprintf (stderr, + "FAILED: cannot send the request of the sub-case '%s'.\n", + sc->name); + (void) MHD_socket_close_ (sk); + return 0; + } + got = recv_reply (sk, + buf, + sizeof (buf), + MHD_STATICSTR_LEN_ (EXPECTED_STATUS)); + if (MHD_STATICSTR_LEN_ (EXPECTED_STATUS) > got) + { + fprintf (stderr, + "FAILED: sub-case '%s': no reply received " + "(got %u bytes, expected at least %u bytes). " + "The daemon may have crashed.\n", + sc->name, + (unsigned) got, + (unsigned) MHD_STATICSTR_LEN_ (EXPECTED_STATUS)); + ret = 0; + } + else if (0 != memcmp (buf, + EXPECTED_STATUS, + MHD_STATICSTR_LEN_ (EXPECTED_STATUS))) + { + fprintf (stderr, + "FAILED: sub-case '%s': unexpected reply status. " + "Expected '%s', got '%.*s'.\n", + sc->name, + EXPECTED_STATUS, + (int) MHD_STATICSTR_LEN_ (EXPECTED_STATUS), + buf); + ret = 0; + } + else if (verbose) + printf ("PASSED: %s\n", + sc->name); + + (void) MHD_socket_close_ (sk); + return ret; +} + + +/** + * Run all sub-cases with the given connection memory limit. + * + * @param mem_limit the value for MHD_OPTION_CONNECTION_MEMORY_LIMIT + * @return the number of the failed sub-cases + */ +static unsigned int +run_with_mem_limit (size_t mem_limit) +{ + struct MHD_Daemon *d; + const union MHD_DaemonInfo *dinfo; + uint16_t port; + unsigned int num_failed = 0; + size_t i; + struct MHD_OptMatrixProfile prof; + struct MHD_OptionItem ops[8]; + unsigned int flags; + + /* The profile (if any) supplies the discipline level, the threading mode + and the polling backend; the memory limit is the one of the sweep, which + the caller has already taken from the profile. */ + if (NULL != test_prof) + { + prof = *test_prof; + prof.mem_limit = mem_limit; + } + else + { + memset (&prof, 0, sizeof (prof)); + prof.name = "built-in"; + prof.mem_limit = mem_limit; + prof.threading = MHD_OPT_MATRIX_THR_INTERNAL; + prof.poll_backend = MHD_OPT_MATRIX_POLL_SELECT; + } + if (0 == mhd_opt_matrix_fill_options (&prof, ops, + (unsigned int) (sizeof (ops) + / sizeof (ops[0])))) + hardErrorExit ("The daemon option array is too small"); + /* The client of this test is a plain blocking socket client, so external + polling is served with an internal polling thread instead. */ + flags = mhd_opt_matrix_daemon_flags (&prof, MHD_USE_ERROR_LOG, 0); + + d = MHD_start_daemon (flags, + 0, + NULL, NULL, + &ahc_reply, NULL, + MHD_OPTION_ARRAY, ops, + MHD_OPTION_CONNECTION_TIMEOUT, + (unsigned int) TEST_SOCKET_TIMEOUT_SEC, + MHD_OPTION_END); + if (NULL == d) + hardErrorExit ("Cannot start the test daemon"); + + dinfo = MHD_get_daemon_info (d, + MHD_DAEMON_INFO_BIND_PORT); + if ((NULL == dinfo) || (0 == dinfo->port)) + { + MHD_stop_daemon (d); + hardErrorExit ("Cannot detect the port used by the test daemon"); + } + port = (uint16_t) dinfo->port; + + for (i = 0; i < sizeof (subcases) / sizeof (subcases[0]); ++i) + { + /* Report the sub-case before running it: if the daemon aborts (which is + the expected failure mode of the un-fixed code) the whole test process + is terminated and the last reported sub-case is the failing one. */ + fprintf (stderr, + "Running sub-case with memory limit %u: %s\n", + (unsigned) mem_limit, + subcases[i].name); + fflush (stderr); + if (! run_subcase (port, + subcases + i)) + ++num_failed; + } + MHD_stop_daemon (d); + return num_failed; +} + + +int +main (int argc, char *const *argv) +{ + unsigned int num_failed = 0; + size_t i; + + verbose = 0; + for (i = 1; i < (size_t) argc; ++i) + { + if ((0 == strcmp (argv[i], "-v")) || + (0 == strcmp (argv[i], "--verbose"))) + verbose = 1; + } + + if (MHD_YES != MHD_is_feature_supported (MHD_FEATURE_AUTOSUPPRESS_SIGPIPE)) + { +#if defined(HAVE_SIGNAL_H) && defined(SIGPIPE) + if (SIG_ERR == signal (SIGPIPE, SIG_IGN)) + hardErrorExit ("Cannot suppress the SIGPIPE signal"); +#else /* ! HAVE_SIGNAL_H || ! SIGPIPE */ + fprintf (stderr, + "Cannot suppress the SIGPIPE signal.\n"); +#endif /* ! HAVE_SIGNAL_H || ! SIGPIPE */ + } + + mhd_opt_matrix_print_notice ("test_rq_shift_back"); + test_prof = mhd_opt_matrix_from_env (); + if (NULL != test_prof) + { + /* A profile pins one configuration: its memory limit replaces the + built-in sweep so that the driver script can walk the matrix. */ + char desc[256]; + + test_prof_copy = *test_prof; + test_prof = &test_prof_copy; + if (! mhd_opt_matrix_profile_supported (test_prof)) + { + printf ("test_rq_shift_back: the selected profile %s is not supported " + "by this build, the test is skipped.\n", + mhd_opt_matrix_describe (test_prof, desc, sizeof (desc))); + return 77; + } + if (mhd_opt_matrix_raise_mem_limit (&test_prof_copy, MIN_POOL_FOR_TEST)) + printf ("test_rq_shift_back: the connection memory limit of the " + "profile was raised to %u, the smallest pool this test can " + "work with.\n", (unsigned) MIN_POOL_FOR_TEST); + if (0 == test_prof_copy.mem_limit) + printf ("test_rq_shift_back: NOTE: the profile uses the default " + "connection memory pool, so the \"shift back\" code path is " + "not reached at all by this run.\n"); + num_failed = run_with_mem_limit (test_prof_copy.mem_limit); + } + else + { + for (i = 0; i < sizeof (mem_limits) / sizeof (mem_limits[0]); ++i) + num_failed += run_with_mem_limit (mem_limits[i]); + } + + if (0 != num_failed) + { + fprintf (stderr, + "FAILED: %u sub-case(s) failed.\n", + num_failed); + return EXIT_FAILED_TEST; + } + if (verbose) + printf ("All sub-cases passed.\n"); + return 0; +} diff --git a/src/microhttpd/test_sha1.c b/src/microhttpd/test_sha1.c @@ -30,6 +30,10 @@ #include <stdlib.h> #include <string.h> +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + static int verbose = 0; /* verbose level (0-1)*/ diff --git a/src/microhttpd/test_sha256.c b/src/microhttpd/test_sha256.c @@ -29,6 +29,10 @@ #include <stdio.h> #include <stdlib.h> +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_SHA256_TLSLIB) && defined(MHD_HTTPS_REQUIRE_GCRYPT) #define NEED_GCRYP_INIT 1 #include <gcrypt.h> diff --git a/src/microhttpd/test_sha512_256.c b/src/microhttpd/test_sha512_256.c @@ -29,6 +29,10 @@ #include <stdio.h> #include <stdlib.h> +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + static int verbose = 0; /* verbose level (0-1)*/ diff --git a/src/microhttpd/test_shutdown_select.c b/src/microhttpd/test_shutdown_select.c @@ -80,6 +80,10 @@ #include "mhd_threads.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef SOMAXCONN #define SOMAXCONN 511 #endif /* ! SOMAXCONN */ diff --git a/src/microhttpd/test_start_stop.c b/src/microhttpd/test_start_stop.c @@ -27,6 +27,10 @@ #include "platform.h" #include <microhttpd.h> +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_CPU_COUNT) && (MHD_CPU_COUNT + 0) < 2 #undef MHD_CPU_COUNT #endif diff --git a/src/microhttpd/test_str.c b/src/microhttpd/test_str.c @@ -42,6 +42,10 @@ #include "mhd_str.h" #include "test_helpers.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + static int verbose = 0; /* verbose level (0-3)*/ diff --git a/src/microhttpd/test_str_base64.c b/src/microhttpd/test_str_base64.c @@ -29,6 +29,10 @@ #include "mhd_str.h" #include "mhd_assert.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef MHD_STATICSTR_LEN_ /** * Determine length of static string / macro strings at compile time. diff --git a/src/microhttpd/test_str_bin_hex.c b/src/microhttpd/test_str_bin_hex.c @@ -26,9 +26,14 @@ #include "mhd_options.h" #include <string.h> #include <stdio.h> +#include <stdlib.h> #include "mhd_str.h" #include "mhd_assert.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef MHD_STATICSTR_LEN_ /** * Determine length of static string / macro strings at compile time. @@ -436,6 +441,191 @@ check_broken_str (void) } +/** + * The maximum length of the hexadecimal input used by the exact-sized + * output buffer checks. + */ +#define TEST_EXACT_MAX_HEX_LEN 320 + +/** + * The number of the guard bytes placed right after the output buffer. + */ +#define TEST_GUARD_SIZE 8 + +/** + * The value used to fill the guard bytes. + */ +#define TEST_GUARD_CHR 0x5A + +/** + * Check that MHD_hex_to_bin() writes no more than the documented number of + * the output bytes, which is ((@a hex_len + 1) / 2). + * + * Every caller of MHD_hex_to_bin() must size the output buffer by that + * formula. A caller that sizes the buffer by the expected size of the + * decoded value instead (like the digest authentication 'response' check + * used to do) can be overflown by simply sending a longer hexadecimal + * string, therefore this contract is checked here with buffers that have + * no slack at all: + * - an exactly-sized heap allocation, so that a run-time memory checker + * (ASAN, valgrind) traps any out-of-bounds write immediately, and + * - an allocation with a known guard pattern right after the output area, + * so that the overflow is detected even without any such tool. + * + * @param hex the input string with hexadecimal digits + * @param hex_len the length of @a hex + * @param must_succeed non-zero if the decoding is expected to succeed + * @param line_num the line number to report in case of failure + * @return zero if succeed, number of failures otherwise + */ +static unsigned int +expect_exact_dest_n (const char *const hex, + const size_t hex_len, + const int must_succeed, + const unsigned int line_num) +{ + const size_t max_out_size = (hex_len + 1) / 2; + const size_t expected_size = must_succeed ? max_out_size : 0; + uint8_t *buf; + size_t res_size; + size_t i; + unsigned int ret; + + ret = 0; + + /* (1) The exactly-sized output buffer. */ + buf = (uint8_t *) malloc (0 != max_out_size ? max_out_size : 1); + if (NULL == buf) + { + fprintf (stderr, "malloc() failed. Line: %u\n", line_num); + return 1; + } + res_size = MHD_hex_to_bin (hex, hex_len, buf); + free (buf); + if (res_size != expected_size) + { + ret++; + fprintf (stderr, + "'MHD_hex_to_bin ()' FAILED: " + "Wrong returned value with the exactly-sized buffer:\n"); + fprintf (stderr, + "\tRESULT : MHD_hex_to_bin (\"%s\", %u, ...) -> %u\n", + n_prnt (hex, hex_len), (unsigned) hex_len, + (unsigned) res_size); + fprintf (stderr, + "\tEXPECTED: MHD_hex_to_bin (\"%s\", %u, ...) -> %u\n", + n_prnt (hex, hex_len), (unsigned) hex_len, + (unsigned) expected_size); + } + + /* (2) The output buffer followed by the guard bytes. */ + buf = (uint8_t *) malloc (max_out_size + TEST_GUARD_SIZE); + if (NULL == buf) + { + fprintf (stderr, "malloc() failed. Line: %u\n", line_num); + return ret + 1; + } + memset (buf, TEST_GUARD_CHR, max_out_size + TEST_GUARD_SIZE); + res_size = MHD_hex_to_bin (hex, hex_len, buf); + if (res_size > max_out_size) + { + ret++; + fprintf (stderr, + "'MHD_hex_to_bin ()' FAILED: " + "Returned value is larger than the documented maximum " + "output size: %u > %u\n", + (unsigned) res_size, (unsigned) max_out_size); + } + for (i = 0; i < TEST_GUARD_SIZE; ++i) + { + if (TEST_GUARD_CHR != buf[max_out_size + i]) + { + ret++; + fprintf (stderr, + "'MHD_hex_to_bin ()' FAILED: " + "The guard byte number %u (of %u) after the output buffer " + "has been overwritten:\n", + (unsigned) i, (unsigned) TEST_GUARD_SIZE); + fprintf (stderr, + "\tINPUT : MHD_hex_to_bin (\"%s\", %u, ...), " + "the maximum output size is %u\n", + n_prnt (hex, hex_len), (unsigned) hex_len, + (unsigned) max_out_size); + break; + } + } + free (buf); + + if (0 != ret) + { + fprintf (stderr, + "The check is at line: %u\n\n", line_num); + } + return ret; +} + + +#define expect_exact_dest(h,ms) \ + expect_exact_dest_n (h, MHD_STATICSTR_LEN_ (h), ms, __LINE__) + + +/** + * Check the output size contract of MHD_hex_to_bin() for every input length + * up to #TEST_EXACT_MAX_HEX_LEN, for valid and for broken input. + * + * @return zero if succeed, number of failures otherwise + */ +static unsigned int +check_exact_dest_buffer (void) +{ + static const char hex_digits[] = "0123456789abcdefABCDEF"; + static char hex[TEST_EXACT_MAX_HEX_LEN + 1]; + unsigned int r = 0; /**< The number of errors */ + size_t len; + + for (len = 0; len < sizeof(hex) - 1; ++len) + hex[len] = hex_digits[len % (sizeof(hex_digits) - 1)]; + hex[sizeof(hex) - 1] = 0; + + /* Valid input of every length, including the odd lengths for which + an extra leading zero is assumed and therefore one more byte is + written than (hex_len / 2). */ + for (len = 0; len <= TEST_EXACT_MAX_HEX_LEN; ++len) + r += expect_exact_dest_n (hex, len, 1, __LINE__); + + /* Broken input: a non-hexadecimal character at every possible position. + The decoding fails, but the bytes decoded before the broken character + have already been written and must still fit into the output buffer. */ + for (len = 1; len <= 64; ++len) + { + size_t pos; + + for (pos = 0; pos < len; ++pos) + { + const char saved = hex[pos]; + + hex[pos] = 'z'; + r += expect_exact_dest_n (hex, len, 0, __LINE__); + hex[pos] = saved; + } + } + + /* The lengths that matter for the HTTP Digest authentication code: + twice the digest size is the largest input any caller may accept. */ + r += expect_exact_dest ("00112233445566778899aabbccddeeff", 1); /* MD5 */ + r += expect_exact_dest ("00112233445566778899aabbccddeeff" + "00112233445566778899aabbccddeeff", 1); /* SHA-256 */ + /* Twice as long as the largest supported digest: a caller that sizes the + output buffer by the digest size only would be overflown here. */ + r += expect_exact_dest ("00112233445566778899aabbccddeeff" + "00112233445566778899aabbccddeeff" + "00112233445566778899aabbccddeeff" + "00112233445566778899aabbccddeeff", 1); + + return r; +} + + int main (int argc, char *argv[]) { @@ -443,6 +633,7 @@ main (int argc, char *argv[]) (void) argc; (void) argv; /* Unused. Silent compiler warning. */ errcount += check_decode_bin (); errcount += check_broken_str (); + errcount += check_exact_dest_buffer (); if (0 == errcount) printf ("All tests have been passed without errors.\n"); return errcount == 0 ? 0 : 1; diff --git a/src/microhttpd/test_str_pct.c b/src/microhttpd/test_str_pct.c @@ -29,6 +29,10 @@ #include "mhd_str.h" #include "mhd_assert.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef MHD_STATICSTR_LEN_ /** * Determine length of static string / macro strings at compile time. diff --git a/src/microhttpd/test_str_quote.c b/src/microhttpd/test_str_quote.c @@ -29,6 +29,10 @@ #include "mhd_str.h" #include "mhd_assert.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef MHD_STATICSTR_LEN_ /** * Determine length of static string / macro strings at compile time. @@ -115,7 +119,8 @@ expect_result_unquote_n (const char *const quoted, const size_t quoted_len, unquoted_len)) { fprintf (stderr, - "'MHD_str_equal_caseless_quoted_bin_n ()' FAILED: Wrong result:\n"); + "'MHD_str_equal_caseless_quoted_bin_n ()' FAILED: Wrong result:\n") + ; /* This does NOT print part of the string after binary zero */ fprintf (stderr, "\tRESULT : MHD_str_equal_caseless_quoted_bin_n('%.*s', %u, " @@ -299,7 +304,8 @@ expect_match_caseless_n (const char *const quoted, const size_t quoted_len, unquoted_len)) { fprintf (stderr, - "'MHD_str_equal_caseless_quoted_bin_n ()' FAILED: Wrong result:\n"); + "'MHD_str_equal_caseless_quoted_bin_n ()' FAILED: Wrong result:\n") + ; /* This does NOT print part of the string after binary zero */ fprintf (stderr, "\tRESULT : MHD_str_equal_caseless_quoted_bin_n('%.*s', %u, " @@ -452,7 +458,8 @@ expect_result_unmatch_n (const char *const quoted, const size_t quoted_len, unquoted_len)) { fprintf (stderr, - "'MHD_str_equal_caseless_quoted_bin_n ()' FAILED: Wrong result:\n"); + "'MHD_str_equal_caseless_quoted_bin_n ()' FAILED: Wrong result:\n") + ; /* This does NOT print part of the string after binary zero */ fprintf (stderr, "\tRESULT : MHD_str_equal_caseless_quoted_bin_n('%.*s', %u, " @@ -698,7 +705,8 @@ expect_result_caseless_unmatch_n (const char *const quoted, unquoted_len)) { fprintf (stderr, - "'MHD_str_equal_caseless_quoted_bin_n ()' FAILED: Wrong result:\n"); + "'MHD_str_equal_caseless_quoted_bin_n ()' FAILED: Wrong result:\n") + ; /* This does NOT print part of the string after binary zero */ fprintf (stderr, "\tRESULT : MHD_str_equal_caseless_quoted_bin_n('%.*s', %u, " diff --git a/src/microhttpd/test_str_token.c b/src/microhttpd/test_str_token.c @@ -27,6 +27,10 @@ #include <stdio.h> #include "mhd_str.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + static int expect_found_n (const char *str, const char *token, size_t token_len) diff --git a/src/microhttpd/test_str_token_remove.c b/src/microhttpd/test_str_token_remove.c @@ -29,6 +29,10 @@ #include "mhd_str.h" #include "mhd_assert.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + static int expect_result_n (const char *str, size_t str_len, diff --git a/src/microhttpd/test_str_tokens_remove.c b/src/microhttpd/test_str_tokens_remove.c @@ -29,6 +29,10 @@ #include "mhd_str.h" #include "mhd_assert.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + static int expect_result_n (const char *str, size_t str_len, diff --git a/src/microhttpd/test_upgrade.c b/src/microhttpd/test_upgrade.c @@ -51,6 +51,10 @@ #include "test_helpers.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifdef HTTPS_SUPPORT #include <gnutls/gnutls.h> #include "../testcurl/https/tls_test_keys.h" diff --git a/src/testcurl/https/test_empty_response.c b/src/testcurl/https/test_empty_response.c @@ -36,6 +36,10 @@ #include "tls_test_common.h" #include "tls_test_keys.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + static int oneone; static enum MHD_Result diff --git a/src/testcurl/https/test_https_get.c b/src/testcurl/https/test_https_get.c @@ -35,6 +35,10 @@ #include "tls_test_common.h" #include "tls_test_keys.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + static uint16_t global_port; diff --git a/src/testcurl/https/test_https_get_iovec.c b/src/testcurl/https/test_https_get_iovec.c @@ -43,6 +43,10 @@ #include "tls_test_common.h" #include "tls_test_keys.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + static uint16_t global_port; diff --git a/src/testcurl/https/test_https_get_parallel.c b/src/testcurl/https/test_https_get_parallel.c @@ -40,6 +40,10 @@ #include "tls_test_common.h" #include "tls_test_keys.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_CPU_COUNT) && (MHD_CPU_COUNT + 0) < 4 #undef MHD_CPU_COUNT #endif diff --git a/src/testcurl/https/test_https_get_parallel_threads.c b/src/testcurl/https/test_https_get_parallel_threads.c @@ -42,6 +42,10 @@ #include "tls_test_common.h" #include "tls_test_keys.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_CPU_COUNT) && (MHD_CPU_COUNT + 0) < 4 #undef MHD_CPU_COUNT #endif diff --git a/src/testcurl/https/test_https_get_select.c b/src/testcurl/https/test_https_get_select.c @@ -37,6 +37,10 @@ #include "tls_test_common.h" #include "tls_test_keys.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + static int oneone; static enum MHD_Result diff --git a/src/testcurl/https/test_https_multi_daemon.c b/src/testcurl/https/test_https_multi_daemon.c @@ -37,6 +37,10 @@ #include "tls_test_common.h" #include "tls_test_keys.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + /* * assert initiating two separate daemons and having one shut down * doesn't affect the other diff --git a/src/testcurl/https/test_https_session_info.c b/src/testcurl/https/test_https_session_info.c @@ -35,6 +35,10 @@ #include "tls_test_common.h" #include "tls_test_keys.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + static int test_append_prio; diff --git a/src/testcurl/https/test_https_sni.c b/src/testcurl/https/test_https_sni.c @@ -36,6 +36,10 @@ #include "tls_test_common.h" #include <gnutls/gnutls.h> +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + /* This test only works with GnuTLS >= 3.0 */ #if GNUTLS_VERSION_MAJOR >= 3 diff --git a/src/testcurl/https/test_https_time_out.c b/src/testcurl/https/test_https_time_out.c @@ -54,6 +54,10 @@ #endif #include "tls_test_keys.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + static const unsigned int timeout_val = 2; static volatile unsigned int num_connects = 0; diff --git a/src/testcurl/https/test_tls_authentication.c b/src/testcurl/https/test_tls_authentication.c @@ -37,6 +37,10 @@ #include "tls_test_common.h" #include "tls_test_keys.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + /* perform a HTTP GET request via SSL/TLS */ static unsigned int diff --git a/src/testcurl/https/test_tls_options.c b/src/testcurl/https/test_tls_options.c @@ -35,6 +35,10 @@ #include "tls_test_common.h" #include "tls_test_keys.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + /* * HTTP access handler call back * used to query negotiated security parameters diff --git a/src/testcurl/perf_get.c b/src/testcurl/perf_get.c @@ -48,6 +48,10 @@ #include <errno.h> #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef WINDOWS #include <unistd.h> #include <sys/socket.h> diff --git a/src/testcurl/perf_get_concurrent.c b/src/testcurl/perf_get_concurrent.c @@ -45,6 +45,10 @@ #include <errno.h> #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_CPU_COUNT) && (MHD_CPU_COUNT + 0) < 2 #undef MHD_CPU_COUNT #endif diff --git a/src/testcurl/test_add_conn.c b/src/testcurl/test_add_conn.c @@ -37,6 +37,10 @@ #include "mhd_has_param.h" #include "mhd_sockets.h" /* only macros used */ +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN diff --git a/src/testcurl/test_basicauth.c b/src/testcurl/test_basicauth.c @@ -45,6 +45,10 @@ #include "mhd_has_param.h" #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef MHD_STATICSTR_LEN_ /** * Determine length of static string / macro strings at compile time. diff --git a/src/testcurl/test_callback.c b/src/testcurl/test_callback.c @@ -31,6 +31,10 @@ #include <microhttpd.h> #include <errno.h> +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + struct callback_closure { unsigned int called; diff --git a/src/testcurl/test_concurrent_stop.c b/src/testcurl/test_concurrent_stop.c @@ -34,6 +34,10 @@ #include <time.h> #include <pthread.h> +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_CPU_COUNT) && (MHD_CPU_COUNT + 0) < 2 #undef MHD_CPU_COUNT #endif diff --git a/src/testcurl/test_delete.c b/src/testcurl/test_delete.c @@ -36,6 +36,10 @@ #include <errno.h> #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef WINDOWS #include <unistd.h> #endif diff --git a/src/testcurl/test_digestauth.c b/src/testcurl/test_digestauth.c @@ -35,6 +35,10 @@ #include <time.h> #include <errno.h> +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_HTTPS_REQUIRE_GCRYPT) && \ (defined(MHD_SHA256_TLSLIB) || defined(MHD_MD5_TLSLIB)) #define NEED_GCRYP_INIT 1 diff --git a/src/testcurl/test_digestauth2.c b/src/testcurl/test_digestauth2.c @@ -50,6 +50,10 @@ #include "mhd_has_param.h" #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef MHD_STATICSTR_LEN_ /** * Determine length of static string / macro strings at compile time. diff --git a/src/testcurl/test_digestauth_concurrent.c b/src/testcurl/test_digestauth_concurrent.c @@ -51,6 +51,10 @@ #include "mhd_has_param.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef CURL_VERSION_BITS #define CURL_VERSION_BITS(x,y,z) ((x) << 16 | (y) << 8 | (z)) #endif /* ! CURL_VERSION_BITS */ diff --git a/src/testcurl/test_digestauth_emu_ext.c b/src/testcurl/test_digestauth_emu_ext.c @@ -48,6 +48,10 @@ #include "mhd_has_param.h" #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef MHD_STATICSTR_LEN_ /** * Determine length of static string / macro strings at compile time. diff --git a/src/testcurl/test_digestauth_sha256.c b/src/testcurl/test_digestauth_sha256.c @@ -36,6 +36,10 @@ #include <time.h> #include <errno.h> +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_HTTPS_REQUIRE_GCRYPT) && \ (defined(MHD_SHA256_TLSLIB) || defined(MHD_MD5_TLSLIB)) #define NEED_GCRYP_INIT 1 diff --git a/src/testcurl/test_digestauth_with_arguments.c b/src/testcurl/test_digestauth_with_arguments.c @@ -34,6 +34,10 @@ #include <time.h> #include <errno.h> +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_HTTPS_REQUIRE_GCRYPT) && \ (defined(MHD_SHA256_TLSLIB) || defined(MHD_MD5_TLSLIB)) #define NEED_GCRYP_INIT 1 diff --git a/src/testcurl/test_get.c b/src/testcurl/test_get.c @@ -36,6 +36,10 @@ #include "mhd_has_param.h" #include "mhd_sockets.h" /* only macros used */ +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #define EXPECTED_URI_PATH "/hello_world?a=%26&b=c" diff --git a/src/testcurl/test_get_chunked.c b/src/testcurl/test_get_chunked.c @@ -41,6 +41,10 @@ #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_CPU_COUNT) && (MHD_CPU_COUNT + 0) < 2 #undef MHD_CPU_COUNT #endif diff --git a/src/testcurl/test_get_close_keep_alive.c b/src/testcurl/test_get_close_keep_alive.c @@ -39,6 +39,10 @@ #include "mhd_has_param.h" #include "mhd_sockets.h" /* only macros used */ +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifdef HAVE_STRINGS_H #include <strings.h> #endif /* HAVE_STRINGS_H */ diff --git a/src/testcurl/test_get_empty.c b/src/testcurl/test_get_empty.c @@ -36,6 +36,10 @@ #include "mhd_has_param.h" #include "mhd_sockets.h" /* only macros used */ +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #define EXPECTED_URI_PATH "/hello_world?a=%26&b=c" diff --git a/src/testcurl/test_get_iovec.c b/src/testcurl/test_get_iovec.c @@ -48,6 +48,10 @@ #include "mhd_sockets.h" #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef WINDOWS #include <sys/socket.h> #include <unistd.h> diff --git a/src/testcurl/test_get_response_cleanup.c b/src/testcurl/test_get_response_cleanup.c @@ -55,6 +55,10 @@ #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_CPU_COUNT) && (MHD_CPU_COUNT + 0) < 2 #undef MHD_CPU_COUNT #endif diff --git a/src/testcurl/test_get_sendfile.c b/src/testcurl/test_get_sendfile.c @@ -38,6 +38,10 @@ #include "mhd_sockets.h" #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef WINDOWS #include <sys/socket.h> #include <unistd.h> diff --git a/src/testcurl/test_get_wait.c b/src/testcurl/test_get_wait.c @@ -36,6 +36,10 @@ #include <pthread.h> #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_CPU_COUNT) && (MHD_CPU_COUNT + 0) < 2 #undef MHD_CPU_COUNT #endif diff --git a/src/testcurl/test_head.c b/src/testcurl/test_head.c @@ -44,6 +44,10 @@ #include "mhd_has_param.h" #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef MHD_STATICSTR_LEN_ /** * Determine length of static string / macro strings at compile time. diff --git a/src/testcurl/test_iplimit.c b/src/testcurl/test_iplimit.c @@ -35,6 +35,10 @@ #include <time.h> #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef WINDOWS #include <unistd.h> #endif diff --git a/src/testcurl/test_large_put.c b/src/testcurl/test_large_put.c @@ -42,6 +42,10 @@ #include "mhd_has_in_name.h" #include "mhd_has_param.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_CPU_COUNT) && (MHD_CPU_COUNT + 0) < 2 #undef MHD_CPU_COUNT #endif diff --git a/src/testcurl/test_long_header.c b/src/testcurl/test_long_header.c @@ -36,6 +36,10 @@ #include <time.h> #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef WINDOWS #include <unistd.h> #endif diff --git a/src/testcurl/test_parse_cookies.c b/src/testcurl/test_parse_cookies.c @@ -43,6 +43,10 @@ #include "mhd_has_param.h" #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef MHD_STATICSTR_LEN_ /** * Determine length of static string / macro strings at compile time. diff --git a/src/testcurl/test_patch.c b/src/testcurl/test_patch.c @@ -36,6 +36,10 @@ #include <errno.h> #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef WINDOWS #include <unistd.h> #endif diff --git a/src/testcurl/test_post.c b/src/testcurl/test_post.c @@ -48,6 +48,10 @@ #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_CPU_COUNT) && (MHD_CPU_COUNT + 0) < 2 #undef MHD_CPU_COUNT #endif diff --git a/src/testcurl/test_post_loop.c b/src/testcurl/test_post_loop.c @@ -36,6 +36,10 @@ #include <errno.h> #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef WINDOWS #include <unistd.h> #endif diff --git a/src/testcurl/test_postform.c b/src/testcurl/test_postform.c @@ -58,6 +58,10 @@ #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #if defined(MHD_CPU_COUNT) && (MHD_CPU_COUNT + 0) < 2 #undef MHD_CPU_COUNT #endif diff --git a/src/testcurl/test_process_arguments.c b/src/testcurl/test_process_arguments.c @@ -36,6 +36,10 @@ #include <errno.h> #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef WINDOWS #include <unistd.h> #endif diff --git a/src/testcurl/test_process_headers.c b/src/testcurl/test_process_headers.c @@ -36,6 +36,10 @@ #include <errno.h> #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef WINDOWS #include <unistd.h> #endif diff --git a/src/testcurl/test_put.c b/src/testcurl/test_put.c @@ -36,6 +36,10 @@ #include <errno.h> #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef WINDOWS #include <unistd.h> #endif diff --git a/src/testcurl/test_put_broken_len.c b/src/testcurl/test_put_broken_len.c @@ -44,6 +44,10 @@ #include "mhd_has_param.h" #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef MHD_STATICSTR_LEN_ /** * Determine length of static string / macro strings at compile time. diff --git a/src/testcurl/test_put_chunked.c b/src/testcurl/test_put_chunked.c @@ -36,6 +36,10 @@ #include <time.h> #include <errno.h> +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef WINDOWS #include <unistd.h> #endif diff --git a/src/testcurl/test_put_header_fold.c b/src/testcurl/test_put_header_fold.c @@ -43,6 +43,10 @@ #include "mhd_has_param.h" #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + /* The next macros are borrowed from memorypool.c Keep them in sync! */ diff --git a/src/testcurl/test_quiesce.c b/src/testcurl/test_quiesce.c @@ -39,6 +39,10 @@ #include "mhd_has_in_name.h" #include "mhd_has_param.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef WINDOWS #include <unistd.h> diff --git a/src/testcurl/test_quiesce_stream.c b/src/testcurl/test_quiesce_stream.c @@ -32,6 +32,10 @@ #include <errno.h> #include <pthread.h> #include <microhttpd.h> + +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" #ifdef HAVE_UNISTD_H #include <unistd.h> #elif defined(_WIN32) diff --git a/src/testcurl/test_termination.c b/src/testcurl/test_termination.c @@ -37,6 +37,10 @@ #include <unistd.h> #include <curl/curl.h> +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifndef __MINGW32__ #include <sys/select.h> #include <sys/socket.h> diff --git a/src/testcurl/test_timeout.c b/src/testcurl/test_timeout.c @@ -40,6 +40,10 @@ #endif /* HAVE_TIME_H */ #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + /** * Pause execution for specified number of milliseconds. diff --git a/src/testcurl/test_toolarge.c b/src/testcurl/test_toolarge.c @@ -44,6 +44,10 @@ #include "mhd_has_param.h" #include "mhd_sockets.h" /* only macros used */ +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifdef HAVE_STRINGS_H #include <strings.h> #endif /* HAVE_STRINGS_H */ diff --git a/src/testcurl/test_tricky.c b/src/testcurl/test_tricky.c @@ -36,6 +36,10 @@ #include "mhd_has_param.h" #include "mhd_sockets.h" /* only macros used */ +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifdef HAVE_STRINGS_H #include <strings.h> #endif /* HAVE_STRINGS_H */ diff --git a/src/testcurl/test_urlparse.c b/src/testcurl/test_urlparse.c @@ -35,6 +35,10 @@ #include <time.h> #include "mhd_has_in_name.h" +/* Turn any MHD_PANIC() or failing mhd_assert() reached from this + test into a marked, classifiable test error (TESTING.md, P5). */ +#include "mhd_panic_tripwire.h" + #ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN 1 diff --git a/src/testzzuf/Makefile.am b/src/testzzuf/Makefile.am @@ -5,13 +5,37 @@ SUBDIRS = . # ZZUF_SEED can be redefined to use other initial seeds # for extended testing. E.g., # make ZZUF_SEED=1234 check +# When a test fails, the runner scripts print the seed(s) that produced +# the failure so that the failure can be replayed with this variable. ZZUF_SEED = 0 +# ZZUF_SEED_START / ZZUF_SEED_STOP sweep a whole range of seeds instead of +# using a single one; they are mapped onto zzuf's "--seed=start:stop". +# Every seed in the range starts one new child, so a large range takes a +# correspondingly long time. E.g., +# make ZZUF_SEED_START=0 ZZUF_SEED_STOP=500 check +# ZZUF_SEED_START takes precedence over ZZUF_SEED. +ZZUF_SEED_START = +ZZUF_SEED_STOP = + + # Additional flags for zzuf -ZZUF_FLAGS = +ZZUF_FLAGS = # Additional flags for socat (if socat is used) -SOCAT_FLAGS = +SOCAT_FLAGS = + +# Sanitizer options that are appended to the options set by configure. +# The fuzzing tests must abort at the very first error, otherwise the +# input that produced the error is lost. +# Leak detection is off: under fuzzing MHD legitimately abandons +# half-processed requests, and the test itself is killed by zzuf. +# To run the fuzzing tests under the sanitizers, configure MHD with +# ./configure --enable-sanitizers=address,undefined +# (this automatically makes the socat mode mandatory, as zzuf and the +# sanitizers cannot both intercept the standard library calls). +ZZUF_ASAN_OPTIONS = abort_on_error=1:detect_leaks=0 +ZZUF_UBSAN_OPTIONS = print_stacktrace=1:halt_on_error=1 if FORCE_USE_ZZUF_SOCAT TEST_RUNNER_SCRIPT = zzuf_socat_test_runner.sh @@ -32,9 +56,15 @@ AM_LDFLAGS = $(LDFLAGS_ac) AM_TESTS_ENVIRONMENT = $(TESTS_ENVIRONMENT_ac) \ ZZUF="$(ZZUF)" ; export ZZUF ; \ ZZUF_SEED="$(ZZUF_SEED)" ; export ZZUF_SEED ; \ + ZZUF_SEED_START="$(ZZUF_SEED_START)" ; export ZZUF_SEED_START ; \ + ZZUF_SEED_STOP="$(ZZUF_SEED_STOP)" ; export ZZUF_SEED_STOP ; \ ZZUF_FLAGS="$(ZZUF_FLAGS)" ; export ZZUF_FLAGS ; \ SOCAT="$(SOCAT)" ; export SOCAT ; \ - SOCAT_FLAGS="$(SOCAT_FLAGS)" ; export SOCAT_FLAGS ; + SOCAT_FLAGS="$(SOCAT_FLAGS)" ; export SOCAT_FLAGS ; \ + ASAN_OPTIONS="$${ASAN_OPTIONS:+$${ASAN_OPTIONS}:}$(ZZUF_ASAN_OPTIONS)" ; \ + export ASAN_OPTIONS ; \ + UBSAN_OPTIONS="$${UBSAN_OPTIONS:+$${UBSAN_OPTIONS}:}$(ZZUF_UBSAN_OPTIONS)" ; \ + export UBSAN_OPTIONS ; if USE_COVERAGE AM_CFLAGS += -fprofile-arcs -ftest-coverage @@ -74,7 +104,33 @@ check_PROGRAMS = \ test_put10 \ test_put_large10 \ test_get_long_uri10 \ - test_get_long_header10 + test_get_long_header10 \ + test_get_hostile \ + test_get_chunked_hostile \ + test_post_hostile \ + test_put_chunked_hostile \ + test_put_large_hostile \ + test_get_args \ + test_get_args_close \ + test_get_args10 \ + test_get_chunked_ext \ + test_get_chunked_ext_close \ + test_put_chunked_ext \ + test_put_chunked_ext_close \ + test_pipelined \ + test_pipelined_close + +if ENABLE_DAUTH +check_PROGRAMS += \ + test_digest_auth \ + test_digest_auth_close +endif + +if ENABLE_BAUTH +check_PROGRAMS += \ + test_basic_auth \ + test_basic_auth_close +endif .NOTPARALLEL: @@ -97,7 +153,9 @@ warn_vheavy_use: echo "NOTICE" -tests_common_sources = mhd_debug_funcs.h mhd_debug_funcs.c +tests_common_sources = \ + mhd_debug_funcs.h mhd_debug_funcs.c \ + mhd_zzuf_common.h mhd_zzuf_common.c test_get_SOURCES = \ test_get.c $(tests_common_sources) @@ -151,3 +209,64 @@ test_put_large10_SOURCES = $(test_put_large_SOURCES) test_get_long_uri10_SOURCES = $(test_get_long_uri_SOURCES) test_get_long_header10_SOURCES = $(test_get_long_header_SOURCES) + +# The "_hostile" variants run exactly the same client/server exchange as +# the programs above, but every started daemon uses another entry of the +# "hostile" daemon option matrix (see mhd_zzuf_common.c): tiny connection +# memory pools, small connection limits, short timeouts and the whole +# range of MHD_OPTION_CLIENT_DISCIPLINE_LVL / +# MHD_OPTION_STRICT_FOR_CLIENT values. + +test_get_hostile_SOURCES = $(test_get_SOURCES) + +test_get_chunked_hostile_SOURCES = $(test_get_chunked_SOURCES) + +test_post_hostile_SOURCES = $(test_post_SOURCES) + +test_put_chunked_hostile_SOURCES = $(test_put_chunked_SOURCES) + +test_put_large_hostile_SOURCES = $(test_put_large_SOURCES) + +# GET requests with hostile query strings (arguments without '=', empty +# names and values, trailing '&', percent-encoded names, a very long tail +# argument), combined with connection memory pools below MHD_BUF_INC_SIZE. + +test_get_args_SOURCES = \ + test_get_args.c $(tests_common_sources) + +test_get_args_close_SOURCES = $(test_get_args_SOURCES) + +test_get_args10_SOURCES = $(test_get_args_SOURCES) + +# Chunked transfers whose chunk-size lines carry chunk extensions, plus +# trailers/footers. libcurl cannot produce chunk extensions, so these +# tests use the raw socket client of mhd_zzuf_common.c. + +test_get_chunked_ext_SOURCES = \ + test_get_chunked_ext.c $(tests_common_sources) + +test_get_chunked_ext_close_SOURCES = $(test_get_chunked_ext_SOURCES) + +test_put_chunked_ext_SOURCES = $(test_get_chunked_ext_SOURCES) + +test_put_chunked_ext_close_SOURCES = $(test_get_chunked_ext_SOURCES) + +# Several complete requests written with a single send() call. + +test_pipelined_SOURCES = \ + test_pipelined.c $(tests_common_sources) + +test_pipelined_close_SOURCES = $(test_pipelined_SOURCES) + +# Authentication. Both are built only if the respective support has not +# been disabled at configure time (--disable-dauth / --disable-bauth). + +test_digest_auth_SOURCES = \ + test_digest_auth.c $(tests_common_sources) + +test_digest_auth_close_SOURCES = $(test_digest_auth_SOURCES) + +test_basic_auth_SOURCES = \ + test_basic_auth.c $(tests_common_sources) + +test_basic_auth_close_SOURCES = $(test_basic_auth_SOURCES) diff --git a/src/testzzuf/README b/src/testzzuf/README @@ -1,13 +1,309 @@ -Testcases in this directory require zzuf and socat. - -zzuf is used to randomly mess with the TCP connection between the CURL -clients and the MHD server. The goal is to expose problems in MHD's -error handling (by introducing random syntax errors). socat is -used to listen on port 11081 and forward the randomzied stream to -port 11080 where MHD is waiting. - -As a result, the testcases in this directory do NOT check that -whatever CURL returns is what was expected -- random modifications to -the TCP stream can have random effects ;-). Testcases "fail" if the -code crashes or hangs indefinitely. +GNU libmicrohttpd -- fuzzing testsuite (src/testzzuf) +===================================================== + +The testcases in this directory require zzuf, and most of them also +require socat. + +zzuf randomly mangles the TCP byte stream that reaches MHD. The goal is +to expose problems in MHD's error handling by introducing random syntax +errors into otherwise valid HTTP traffic. + +There are two modes of operation: + + * direct mode (zzuf_test_runner.sh) + zzuf runs the test binary itself and intercepts the data that MHD + reads from its accepted sockets. Only possible when MHD uses + accept() (not accept4()) and when no sanitizer is active, because + zzuf and the sanitizers both intercept the standard library. + + * socat mode (zzuf_socat_test_runner.sh) + zzuf runs a socat relay that listens on 127.0.0.121:10121 and + forwards the mangled stream to MHD on 127.0.0.1:4010. The test + binary (and therefore MHD) runs outside of zzuf, so this mode works + with sanitizers and with accept4(). configure selects this mode + automatically when it is required (FORCE_USE_ZZUF_SOCAT). + +Because the byte stream is corrupted at random, the testcases do NOT +check that whatever the client receives is what was expected. A testcase +"fails" when MHD crashes, aborts, hangs, reports an internal +inconsistency through its callbacks, or when a sanitizer fires. + + +What is being tested +-------------------- + +Every test starts a series of MHD daemons -- one per polling mode that +the current build supports (internal select(), internal select() with +thread-per-connection, internal poll(), internal poll() with +thread-per-connection, internal epoll, external polling, external polling +without thread safety) -- and runs a client against each of them. + +Clients come in two flavours: + + * libcurl clients, for everything libcurl can express; + * a small raw socket client (mhd_zzuf_common.c), for everything libcurl + cannot express: chunk extensions, request pipelining, header-less + requests and hand-written "Authorization:" headers. + +Both flavours use 127.0.0.101 as their source address, while MHD listens +on 127.0.0.1 (and socat on 127.0.0.121). zzuf is told to fuzz only the +traffic of the server side address, so the data the *clients* read is +left alone. + + +The daemon option matrix +------------------------ + +Historically all fuzzing tests here used essentially one set of daemon +options. Several MHD code paths are only reachable with other options, +most importantly the read-buffer "shift back" in get_req_headers(), which +is only executed when the connection memory pool is smaller than +MHD_BUF_INC_SIZE (1500 bytes). + +mhd_zzuf_common.c therefore defines a matrix of option profiles: + + name mem_limit conn_limit timeout discipline + ---------------- --------- ---------- ------- ------------------ + plain - - 2 0 + pool-128 128 - 2 0 + pool-256-strict 256 - 2 1 + pool-512-lax 512 - 2 -1 + pool-1024-conn 1024 4 2 2 + pool-1400-hard 1400 2 1 3 + pool-768-loose 768 - 1 -3 + legacy-strict 192 - 2 1 (STRICT_FOR_CLIENT) + legacy-lax 320 - 2 -1 (STRICT_FOR_CLIENT) + +"discipline" is MHD_OPTION_CLIENT_DISCIPLINE_LVL, except for the two +"legacy" profiles which use the older MHD_OPTION_STRICT_FOR_CLIENT so +that the option translation code is covered as well. + +Every daemon that a test starts uses the next profile of the matrix, so a +single run of one test program sweeps the whole matrix. + +MHD_OPTION_SERVER_INSANITY is deliberately *not* used: in this version of +MHD the only value of "enum MHD_DisableSanityCheck" is MHD_DSC_SANE (0), +so the option cannot actually disable any check. + + +The test programs +----------------- + +All programs decide what to do from their own name (argv[0]). A "10" +in the name selects HTTP/1.0, a "_close" suffix makes MHD close the +connection after every reply, and a "_hostile" marker enables the option +matrix described above. + + test_get, test_get_chunked, test_post, test_post_form, test_put, + test_put_chunked, test_put_large, test_get_long_uri, + test_get_long_header (+ "_close" and "10" variants) + The original tests: well-formed libcurl traffic, single set of + daemon options. + + test_get_hostile, test_get_chunked_hostile, test_post_hostile, + test_put_chunked_hostile, test_put_large_hostile + The very same client/server exchanges, but sweeping the daemon + option matrix. + + test_get_args (+ "_close", "10") + GET requests with hostile query strings: arguments without '=', + empty names, empty values, trailing '&', percent-encoded and + invalid percent-encoded names, duplicated names and a very long + tail argument. All daemons of this test use a connection memory + pool smaller than MHD_BUF_INC_SIZE. + In addition to the libcurl requests, the raw client sends + *header-less* HTTP/1.0 requests: libcurl always sends at least a + "Host:" header, so with libcurl alone the last parsed element of a + request header is always a header and never a query argument. + + test_get_chunked_ext, test_put_chunked_ext (+ "_close") + Chunked request bodies whose chunk-size lines carry chunk + extensions: "name=value", flags without a value, quoted-string + values containing ';', several extensions on one line, extensions + on the terminating "0" chunk, extensions after bad whitespace, an + extension line longer than the whole memory pool, and trailers + (footers) after the last chunk. The body is always split across + two send() calls so that MHD sees partial chunk-size lines. + Every third iteration appends a pipelined follow-up request, so a + chunk-extension line that is not consumed correctly shows up as a + request boundary de-synchronisation. + test_get_chunked_ext additionally makes MHD answer with a chunked + reply that carries a footer. + These tests also run a small deterministic *self-check*, see below. + + test_pipelined (+ "_close") + Two to five complete requests written with a single send() call, + mixing GET/HEAD/POST/PUT, bodies with Content-Length, chunked + bodies, and a leading empty line. Every other iteration the batch + is split in the middle of a request. + + test_digest_auth (+ "_close") [only if ENABLE_DAUTH] + libcurl performs a complete Digest handshake, so zzuf's bit flips + land inside real "WWW-Authenticate:" and "Authorization:" headers. + The raw client additionally sends hand-crafted "Authorization: + Digest" headers with an unknown "algorithm" token, an empty + "algorithm", a "-sess" algorithm, an over-long "response" (quoted + and unquoted), an over-long hex userhash, an over-long username, + a missing "response", garbage in "nc", an unterminated quoted + string and a bare "Digest" scheme without parameters. + The variants that have to survive MHD's nonce check are built from + a nonce that the raw client first obtains from MHD's own 401 reply. + + test_basic_auth (+ "_close") [only if ENABLE_BAUTH] + libcurl performs a Basic handshake; the raw client adds truncated + Base64, characters outside the Base64 alphabet, payloads without + the ':' separator, payloads decoding to binary zeros, padding in + the middle, an empty value, a 4 KiB payload, whitespace inside the + payload and an unknown authentication scheme. + +Automake conditionals used by this directory: + FORCE_USE_ZZUF_SOCAT, USE_COVERAGE, VHEAVY_TESTS (already used before) + ENABLE_DAUTH, ENABLE_BAUTH (new here) +All of them are defined in configure.ac. The directory as a whole is +only built when RUN_ZZUF_TESTS is true (see src/Makefile.am), which in +turn requires --enable-heavy-tests and a usable libcurl. + + +The deterministic self-check +---------------------------- + +Bit flipping can show that a parser crashes, but it cannot show that a +parser produces the *wrong result*. test_get_chunked_ext and +test_put_chunked_ext therefore also verify, once per run, that a chunked +body with chunk extensions is decoded byte-exactly and answered with +"200". + +That verification obviously must not be fuzzed. In socat mode zzuf only +fuzzes the socat relay, so MHD's own port is a clean channel: the +self-check connects to MHD directly, bypassing socat. In direct mode +every byte that MHD accepts is fuzzed and no clean channel exists, so the +self-check is silently skipped there. Since socat mode is mandatory +whenever MHD is built with sanitizers, the sanitizer workflow below +always includes the self-check. + + +Running under the sanitizers +---------------------------- + + ./configure --enable-heavy-tests --enable-asserts \ + --enable-sanitizers=address,undefined + make + make -C src/testzzuf check + +Enabling sanitizers automatically forces the socat mode (see +configure.ac), because zzuf and the sanitizers cannot both intercept the +standard library calls. + +The Makefile appends + + ASAN_OPTIONS=abort_on_error=1:detect_leaks=0 + UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 + +to whatever configure has already put into ASAN_OPTIONS / UBSAN_OPTIONS. +Aborting on the first error is essential: otherwise the input that +produced the error is lost. Leak detection is off because MHD +legitimately abandons half-processed requests when the input is broken, +and because the test process may be killed by zzuf. + +The values can be overridden per run: + + make ZZUF_ASAN_OPTIONS=abort_on_error=1:detect_leaks=1 check + + +Seeds, replaying and sweeping +----------------------------- + +A single seed (the default is 0): + + make ZZUF_SEED=1234 check + +A range of seeds; zzuf starts one child per seed and stops at the first +crash: + + make ZZUF_SEED_START=0 ZZUF_SEED_STOP=500 check + +ZZUF_SEED_START takes precedence over ZZUF_SEED. A large range takes a +correspondingly long time, so for CI it is usually better to sweep a +moderate range per job and to give different jobs different ranges. + +Both runner scripts print a prominent, easy to grep block when a test +fails: + + ############################################################### + ## FUZZING TEST FAILED + ## test : ./test_get_args + ## exit code : 134 + ## seed(s) used: 0 + ## FAILING SEED(S): 17 + ## Replay a single failing seed with: + ## make ZZUF_SEED=17 TESTS=test_get_args check + ############################################################### + +In direct mode the failing seeds are extracted from zzuf's own per-child +messages ("zzuf[s=17,r=...]: signal 6 (SIGABRT)"). In socat mode the +test program runs outside of zzuf, so the runner reports the exit code +(or the signal) of the *server side* together with the seed the run was +started with. + +Other useful knobs: + + make ZZUF_FLAGS='--ratio=0.01:0.1' check extra zzuf flags + make SOCAT_FLAGS='-v' check extra socat flags + make TESTS='test_get_args test_pipelined' check run a subset + ./configure --enable-heavy-tests=full 200 instead of 10 client + iterations per daemon + + +What zzuf-style fuzzing fundamentally cannot find +------------------------------------------------- + +This is important, because three of the four remotely triggerable bugs +that were fixed on 2026-07-27 had been sitting in code that this +directory nominally "covered". + +zzuf flips bits in an otherwise valid, libcurl-generated byte stream. +That gives a *uniformly random* perturbation of a *structurally valid* +input. Consequently: + + * It cannot invent structure that the generator never produces. + libcurl never emits chunk extensions, never pipelines, never sends a + request without a "Host:" header, and never sends an "Authorization:" + header unless the test asks it to. No amount of bit flipping turns a + libcurl request into a request with a chunk extension: the fuzzer + would have to insert bytes, not change them, and it would have to + keep the surrounding framing intact. + + * It essentially never produces a *semantically hostile but + syntactically valid* token. "algorithm=BOGUS-ALGO" or a + "response=" with 128 hex digits are perfectly well-formed HTTP; they + are simply values a well-behaved client never sends. Random bit + flips overwhelmingly produce inputs that MHD rejects in the first few + hundred bytes of parsing, so the deep parsers are never reached with + values that are valid enough to get there. Reaching the over-long + "response" check even requires a *server generated* nonce, which a + random mutation can never produce. + + * It only explores the option space that the tests configure. The + read-buffer shift-back arithmetic simply does not run unless the + connection memory pool is below 1500 bytes. + + * It only detects crashes and hangs. A parser that silently accepts a + malformed chunk-extension line, or that mis-frames a request, looks + exactly like a healthy parser to this suite unless the resulting + memory access happens to be invalid. + +The additions described above buy back a good deal of this: the raw +client provides the structures libcurl cannot express, the option matrix +provides the configurations, and the deterministic self-check provides an +oracle for "wrong result" instead of only "crash". But they do it by +hand-writing interesting inputs, which does not generalise. + +The complementary approach is a structure-aware harness, which lives in +src/fuzz/. There the HTTP request is *generated* from the fuzzer's +random bytes (so the fuzzer can produce any header, any chunk extension, +any authentication parameter, in any combination) and the fuzzer is +coverage-guided, so it can learn its way into the deep parsers instead of +stumbling into them. Use src/testzzuf for "does MHD survive a hostile +network", and src/fuzz for "does MHD parse hostile HTTP correctly". + diff --git a/src/testzzuf/mhd_zzuf_common.c b/src/testzzuf/mhd_zzuf_common.c @@ -0,0 +1,972 @@ +/* + This file is part of GNU libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + GNU libmicrohttpd is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with GNU libmicrohttpd. + If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * @file testzzuf/mhd_zzuf_common.c + * @brief Shared support code for the fuzzing tests in this directory + * @author Christian Grothoff + */ + +#include "platform.h" +#include <curl/curl.h> +#include <microhttpd.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <errno.h> + +#ifdef HAVE_NETINET_IN_H +#include <netinet/in.h> +#endif /* HAVE_NETINET_IN_H */ +#ifdef HAVE_ARPA_INET_H +#include <arpa/inet.h> +#endif /* HAVE_ARPA_INET_H */ +#ifdef HAVE_NETINET_TCP_H +#include <netinet/tcp.h> +#endif /* HAVE_NETINET_TCP_H */ + +#include "mhd_zzuf_common.h" +#include "mhd_debug_funcs.h" +#include "test_helpers.h" + + +int zzuf_run_with_socat; +int zzuf_dry_run; +int zzuf_oneone; +int zzuf_use_close; + +const struct zzuf_opt_profile *zzuf_active_profile; + +/** + * The name of the running program, as given in argv[0]. + */ +static const char *zzuf_prog_name = ""; + + +/* *** Platform abstraction for the raw socket client *** */ + +#if defined(MHD_POSIX_SOCKETS) +#define ZZUF_CLOSE_SOCKET(s) close (s) +#define ZZUF_SOCK_ERR (errno) +#define ZZUF_ERR_IS_AGAIN(e) \ + ((EAGAIN == (e)) || (EWOULDBLOCK == (e)) || (EINTR == (e))) +#define ZZUF_ERR_IS_INPROGRESS(e) \ + ((EINPROGRESS == (e)) || (EALREADY == (e)) || ZZUF_ERR_IS_AGAIN (e)) +#else /* ! MHD_POSIX_SOCKETS */ +#define ZZUF_CLOSE_SOCKET(s) closesocket (s) +#define ZZUF_SOCK_ERR ((int) WSAGetLastError ()) +#define ZZUF_ERR_IS_AGAIN(e) (WSAEWOULDBLOCK == (e)) +#define ZZUF_ERR_IS_INPROGRESS(e) \ + ((WSAEWOULDBLOCK == (e)) || (WSAEINPROGRESS == (e)) || \ + (WSAEALREADY == (e))) +#endif /* ! MHD_POSIX_SOCKETS */ + +#ifdef MSG_NOSIGNAL +#define ZZUF_SEND_FLAGS MSG_NOSIGNAL +#else /* ! MSG_NOSIGNAL */ +#define ZZUF_SEND_FLAGS 0 +#endif /* ! MSG_NOSIGNAL */ + + +void +zzuf_parse_common_args (int argc, char *const *argv) +{ + const char *name; + + name = (NULL != argv[0]) ? argv[0] : ""; + zzuf_prog_name = name; + zzuf_oneone = ! has_in_name (name, "10"); + zzuf_use_close = has_in_name (name, "_close"); + zzuf_run_with_socat = has_param (argc, argv, "--with-socat"); + zzuf_dry_run = has_param (argc, argv, "--dry-run") || + has_param (argc, argv, "-n"); +#ifdef SIGPIPE + /* The raw client writes to sockets that MHD may close at any moment + because of the fuzzed input. Do not die from SIGPIPE. */ + (void) signal (SIGPIPE, SIG_IGN); +#endif /* SIGPIPE */ +} + + +int +zzuf_name_has (const char *marker) +{ + return has_in_name (zzuf_prog_name, marker); +} + + +unsigned int +zzuf_loop_count (void) +{ + return zzuf_dry_run ? 0 : (unsigned int) ZZUF_LOOP_COUNT; +} + + +/* *** The option matrix *** */ + +/** + * The matrix of daemon options exercised by the fuzzing tests. + * + * Entry zero must stay "neutral": it reproduces the configuration that was + * used before the option matrix was introduced. + * + * The memory limits below 1500 (MHD_BUF_INC_SIZE) are the interesting ones: + * only with such a small connection pool MHD tries to re-use the tail of the + * request header buffer ("shift back"), and only then the buffer arithmetic + * around the last parsed header/argument element is reachable at all. + * + * #MHD_OPTION_SERVER_INSANITY is deliberately *not* used: in this MHD version + * the only value of `enum MHD_DisableSanityCheck` is #MHD_DSC_SANE (zero), so + * the option cannot disable anything and setting it would be a no-op. + * + */ +static const struct zzuf_opt_profile opt_profiles[] = { + /* name mem conn timeout discipline legacy */ + { "plain", 0, 0, ZZUF_MHD_TIMEOUT, 0, 0 }, + { "pool-128", 128, 0, ZZUF_MHD_TIMEOUT, 0, 0 }, + { "pool-256-strict", 256, 0, ZZUF_MHD_TIMEOUT, 1, 0 }, + { "pool-512-lax", 512, 0, ZZUF_MHD_TIMEOUT, -1, 0 }, + { "pool-1024-conn", 1024, 4, ZZUF_MHD_TIMEOUT, 2, 0 }, + { "pool-1400-hard", 1400, 2, 1, 3, 0 }, + { "pool-768-loose", 768, 0, 1, -3, 0 }, + { "legacy-strict", 192, 0, ZZUF_MHD_TIMEOUT, 1, 1 }, + { "legacy-lax", 320, 0, ZZUF_MHD_TIMEOUT, -1, 1 } +}; + + +unsigned int +zzuf_num_opt_profiles (void) +{ + return (unsigned int) (sizeof(opt_profiles) / sizeof(opt_profiles[0])); +} + + +const struct zzuf_opt_profile * +zzuf_opt_profile (unsigned int idx) +{ + return opt_profiles + (idx % zzuf_num_opt_profiles ()); +} + + +/* *** Daemon handling *** */ + +uint16_t +zzuf_pick_port (uint16_t offset) +{ + if (zzuf_run_with_socat) + return (uint16_t) ZZUF_BASE_PORT; /* socat forwards to this fixed port */ + if (MHD_NO != MHD_is_feature_supported (MHD_FEATURE_AUTODETECT_BIND_PORT)) + return 0; /* Use system automatic assignment */ + /* Use a predefined port; may break parallel testing of another MHD build */ + return (uint16_t) (ZZUF_BASE_PORT + offset + (zzuf_oneone ? 100 : 0)); +} + + +struct MHD_Daemon * +zzuf_start_daemon (unsigned int daemon_flags, + uint16_t *pport, + const struct zzuf_opt_profile *prof, + size_t mem_limit_override, + MHD_AccessHandlerCallback ahc, + void *ahc_cls, + MHD_RequestCompletedCallback rcc, + void *rcc_cls, + const struct MHD_OptionItem *extra_opts) +{ + struct MHD_Daemon *d; + struct MHD_OptionItem ops[16]; + size_t num_opt; + size_t mem_limit; + + num_opt = 0; + mem_limit = (0 != mem_limit_override) ? mem_limit_override : prof->mem_limit; + if (0 != mem_limit) + { + ops[num_opt].option = MHD_OPTION_CONNECTION_MEMORY_LIMIT; + ops[num_opt].value = (intptr_t) mem_limit; + ops[num_opt].ptr_value = NULL; + ++num_opt; + } + if (0 != prof->conn_limit) + { + ops[num_opt].option = MHD_OPTION_CONNECTION_LIMIT; + ops[num_opt].value = (intptr_t) prof->conn_limit; + ops[num_opt].ptr_value = NULL; + ++num_opt; + } + if (0 != prof->discipline_lvl) + { + ops[num_opt].option = prof->use_legacy_strict ? + MHD_OPTION_STRICT_FOR_CLIENT : + MHD_OPTION_CLIENT_DISCIPLINE_LVL; + ops[num_opt].value = (intptr_t) prof->discipline_lvl; + ops[num_opt].ptr_value = NULL; + ++num_opt; + } + if (0 == (MHD_USE_INTERNAL_POLLING_THREAD & daemon_flags)) + { + ops[num_opt].option = MHD_OPTION_APP_FD_SETSIZE; + ops[num_opt].value = (intptr_t) (FD_SETSIZE); + ops[num_opt].ptr_value = NULL; + ++num_opt; + } + if (NULL != rcc) + { + ops[num_opt].option = MHD_OPTION_NOTIFY_COMPLETED; + ops[num_opt].value = (intptr_t) rcc; + ops[num_opt].ptr_value = rcc_cls; + ++num_opt; + } + if (NULL != extra_opts) + { + size_t i; + + for (i = 0; MHD_OPTION_END != extra_opts[i].option; ++i) + { + if (num_opt + 2 > sizeof(ops) / sizeof(ops[0])) + { + fprintf (stderr, "Too many daemon options at line %d.\n", + (int) __LINE__); + fflush (stderr); + abort (); /* Broken test code */ + } + ops[num_opt++] = extra_opts[i]; + } + } + if (num_opt + 1 > sizeof(ops) / sizeof(ops[0])) + { + fprintf (stderr, "Too many daemon options at line %d.\n", (int) __LINE__); + fflush (stderr); + abort (); /* Broken test code */ + } + ops[num_opt].option = MHD_OPTION_END; + ops[num_opt].value = 0; + ops[num_opt].ptr_value = NULL; + ++num_opt; + + d = MHD_start_daemon (daemon_flags /* | MHD_USE_ERROR_LOG */, + *pport, NULL, NULL, + ahc, ahc_cls, + MHD_OPTION_CONNECTION_TIMEOUT, + (unsigned int) prof->timeout, + MHD_OPTION_ARRAY, ops, + MHD_OPTION_END); + if (NULL == d) + { + fprintf (stderr, "MHD_start_daemon() failed " + "at line %d.\n", (int) __LINE__); + return NULL; + } + + /* Do not use accept4() as only accept() is intercepted by zzuf */ + if (! zzuf_run_with_socat) + MHD_avoid_accept4_ (d); + + if (0 == *pport) + { + const union MHD_DaemonInfo *dinfo; + + dinfo = MHD_get_daemon_info (d, MHD_DAEMON_INFO_BIND_PORT); + if ((NULL == dinfo) || (0 == dinfo->port)) + { + fprintf (stderr, "MHD_get_daemon_info() failed " + "at line %d.\n", (int) __LINE__); + MHD_stop_daemon (d); + return NULL; + } + *pport = dinfo->port; + } + return d; +} + + +/** + * Print a description of the daemon that is about to be tested. + * + * @param daemon_flags the daemon flags + * @param prof the option profile in use + */ +static void +print_test_starting (unsigned int daemon_flags, + const struct zzuf_opt_profile *prof) +{ + const char *mode; + + fflush (stderr); + if (0 != (MHD_USE_INTERNAL_POLLING_THREAD & daemon_flags)) + { + if (0 != (MHD_USE_THREAD_PER_CONNECTION & daemon_flags)) + mode = (0 != (MHD_USE_POLL & daemon_flags)) ? + "internal poll(), thread-per-connection" : + "internal select(), thread-per-connection"; + else if (0 != (MHD_USE_POLL & daemon_flags)) + mode = "internal poll()"; + else if (0 != (MHD_USE_EPOLL & daemon_flags)) + mode = "internal 'epoll'"; + else + mode = "internal select()"; + } + else + { + if (0 != (MHD_USE_NO_THREAD_SAFETY & daemon_flags)) + mode = "external polling, no thread safety"; + else + mode = "external polling"; + } + printf ("\nStarting test with %s; options profile '%s' " + "(mem_limit=%lu, conn_limit=%u, timeout=%u, %s=%d).\n", + mode, prof->name, + (unsigned long) prof->mem_limit, + prof->conn_limit, + prof->timeout, + prof->use_legacy_strict ? "strict_for_client" : "discipline_lvl", + prof->discipline_lvl); + fflush (stdout); +} + + +/** + * Run a single check: start a daemon, run the client, stop the daemon. + * + * @param[in,out] p the parameters + * @param daemon_flags the flags for the daemon + * @param[in,out] pprofile_num the number of the next profile to use + * @return the result of the client callback, or 1 if the daemon could not + * be started + */ +static unsigned int +run_one_mode (struct zzuf_run_params *p, + unsigned int daemon_flags, + unsigned int *pprofile_num) +{ + struct MHD_Daemon *d; + const struct zzuf_opt_profile *prof; + unsigned int ret; + + if (p->sweep_profiles) + prof = zzuf_opt_profile ((*pprofile_num)++); + else + prof = zzuf_opt_profile (0); + zzuf_active_profile = prof; + print_test_starting (daemon_flags, prof); + d = zzuf_start_daemon (daemon_flags, &p->port, prof, p->mem_limit_override, + p->ahc, p->ahc_cls, p->rcc, p->rcc_cls, + p->extra_opts); + if (NULL == d) + return 1; + ret = p->client ((0 == (MHD_USE_INTERNAL_POLLING_THREAD & daemon_flags)) ? + d : NULL, + p->port, + p->client_cls); + fprintf (stderr, "\n"); + MHD_stop_daemon (d); + fflush (stderr); + return ret; +} + + +unsigned int +zzuf_check_runnable (void) +{ + if (zzuf_run_with_socat) + return 0; + if (MHD_are_sanitizers_enabled_ ()) + { + fprintf (stderr, "Direct run with zzuf does not work with sanitizers. " + "At line %d.\n", (int) __LINE__); + return 77; + } + if (! MHD_is_avoid_accept4_possible_ ()) + { + fprintf (stderr, + "Non-debug build of MHD on this platform use accept4() function. " + "Direct run with zzuf is not possible. " + "At line %d.\n", (int) __LINE__); + return 77; + } + return 0; +} + + +unsigned int +zzuf_run_polling_modes (struct zzuf_run_params *p) +{ + unsigned int profile_num = p->profile_start; + unsigned int testRes; + unsigned int ret = 0; + + if (! zzuf_dry_run && + (MHD_YES == MHD_is_feature_supported (MHD_FEATURE_THREADS))) + { + testRes = run_one_mode (p, MHD_USE_SELECT_INTERNALLY, &profile_num); + if ((77 == testRes) || (99 == testRes)) + return testRes; + ret += testRes; + testRes = run_one_mode (p, MHD_USE_SELECT_INTERNALLY + | MHD_USE_THREAD_PER_CONNECTION, &profile_num); + if ((77 == testRes) || (99 == testRes)) + return testRes; + ret += testRes; + + if (MHD_YES == MHD_is_feature_supported (MHD_FEATURE_POLL)) + { + testRes = run_one_mode (p, MHD_USE_POLL_INTERNALLY, &profile_num); + if ((77 == testRes) || (99 == testRes)) + return testRes; + ret += testRes; + testRes = run_one_mode (p, MHD_USE_POLL_INTERNALLY + | MHD_USE_THREAD_PER_CONNECTION, &profile_num); + if ((77 == testRes) || (99 == testRes)) + return testRes; + ret += testRes; + } + + if (MHD_YES == MHD_is_feature_supported (MHD_FEATURE_EPOLL)) + { + testRes = run_one_mode (p, MHD_USE_EPOLL_INTERNALLY, &profile_num); + if ((77 == testRes) || (99 == testRes)) + return testRes; + ret += testRes; + } + + testRes = run_one_mode (p, MHD_NO_FLAG, &profile_num); + if ((77 == testRes) || (99 == testRes)) + return testRes; + ret += testRes; + } + testRes = run_one_mode (p, MHD_USE_NO_THREAD_SAFETY, &profile_num); + if ((77 == testRes) || (99 == testRes)) + return testRes; + ret += testRes; + + return ret; +} + + +/* *** The raw socket client *** */ + +/** + * Switch the socket to the non-blocking mode. + * + * @param fd the socket to modify + * @return non-zero on success, zero on failure + */ +static int +set_nonblocking (MHD_socket fd) +{ +#if defined(MHD_POSIX_SOCKETS) + int flags; + + flags = fcntl (fd, F_GETFL); + if (-1 == flags) + return 0; + if (0 != (flags & O_NONBLOCK)) + return ! 0; + return (-1 != fcntl (fd, F_SETFL, flags | O_NONBLOCK)) ? ! 0 : 0; +#else /* ! MHD_POSIX_SOCKETS */ + unsigned long mode = 1; + + return (0 == ioctlsocket (fd, FIONBIO, &mode)) ? ! 0 : 0; +#endif /* ! MHD_POSIX_SOCKETS */ +} + + +/** + * Create the client socket and start connecting to the daemon. + * + * @param port the port MHD is listening on (ignored if socat is used) + * @return the socket, or #MHD_INVALID_SOCKET on failure + */ +static MHD_socket +raw_connect (uint16_t port, int bypass_relay) +{ + MHD_socket fd; + struct sockaddr_in sa; + const char *dst_ip; + uint16_t dst_port; + int on = 1; + + if (zzuf_run_with_socat && ! bypass_relay) + { + dst_ip = ZZUF_SOCAT_IP; + dst_port = (uint16_t) ZZUF_SOCAT_PORT; + } + else + { + dst_ip = ZZUF_MHD_LISTEN_IP; + dst_port = port; + } + + fd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (MHD_INVALID_SOCKET == fd) + return MHD_INVALID_SOCKET; + (void) setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, + (const void *) &on, (socklen_t) sizeof(on)); + /* Use the same source address as libcurl does in the other tests, so that + zzuf treats the traffic of this client exactly like libcurl's traffic. */ + memset (&sa, 0, sizeof(sa)); + sa.sin_family = AF_INET; + sa.sin_port = 0; + sa.sin_addr.s_addr = inet_addr (ZZUF_CLIENT_BIND_IP); + /* Best effort only: some platforms do not have the whole 127.0.0.0/8 + range available. */ + (void) bind (fd, (const struct sockaddr *) &sa, (socklen_t) sizeof(sa)); + + if (! set_nonblocking (fd)) + { + (void) ZZUF_CLOSE_SOCKET (fd); + return MHD_INVALID_SOCKET; + } + + memset (&sa, 0, sizeof(sa)); + sa.sin_family = AF_INET; + sa.sin_port = htons (dst_port); + sa.sin_addr.s_addr = inet_addr (dst_ip); + if (0 != connect (fd, (const struct sockaddr *) &sa, (socklen_t) sizeof(sa))) + { + const int err = ZZUF_SOCK_ERR; + if (! ZZUF_ERR_IS_INPROGRESS (err)) + { + (void) ZZUF_CLOSE_SOCKET (fd); + return MHD_INVALID_SOCKET; + } + } + return fd; +} + + +int +zzuf_raw_exchange2 (struct MHD_Daemon *d_extern, + uint16_t port, + int bypass_relay, + const struct zzuf_raw_part *parts, + size_t num_parts, + unsigned int timeout_sec, + char *resp_buf, + size_t resp_buf_size, + size_t *resp_len) +{ + MHD_socket fd; + size_t cur_part = 0; + size_t cur_off = 0; + size_t resp_pos = 0; + int send_done = 0; + int peer_closed = 0; + time_t start; + char rbuf[1024]; + + if (NULL != resp_len) + *resp_len = 0; + fd = raw_connect (port, bypass_relay); + if (MHD_INVALID_SOCKET == fd) + return ZZUF_RAW_SETUP_FAILED; + + if (0 == num_parts) + send_done = 1; + + start = time (NULL); + while (1) + { + fd_set rs; + fd_set ws; + fd_set es; + struct timeval tv; + int maxfd; + MHD_socket maxfd_mhd = MHD_INVALID_SOCKET; + + if (send_done && peer_closed) + break; + if (((time_t) timeout_sec) < (time (NULL) - start)) + break; /* The peer did not close the connection in time */ + + FD_ZERO (&rs); + FD_ZERO (&ws); + FD_ZERO (&es); +#if defined(MHD_POSIX_SOCKETS) + if (FD_SETSIZE <= (int) fd) + { + (void) ZZUF_CLOSE_SOCKET (fd); + return ZZUF_RAW_SETUP_FAILED; + } +#endif /* MHD_POSIX_SOCKETS */ + if (! peer_closed) + FD_SET (fd, &rs); + if (! send_done) + FD_SET (fd, &ws); + maxfd = (int) fd; + if (NULL != d_extern) + { + if (MHD_YES != MHD_get_fdset (d_extern, &rs, &ws, &es, &maxfd_mhd)) + { + fprintf (stderr, "MHD_get_fdset() failed " + "at line %d.\n", (int) __LINE__); + (void) ZZUF_CLOSE_SOCKET (fd); + return ZZUF_RAW_SETUP_FAILED; + } +#ifndef MHD_WINSOCK_SOCKETS + if ((int) maxfd_mhd > maxfd) + maxfd = (int) maxfd_mhd; +#endif /* ! MHD_WINSOCK_SOCKETS */ + } + tv.tv_sec = 0; + tv.tv_usec = 25 * 1000; + if (-1 == select (maxfd + 1, &rs, &ws, &es, &tv)) + { +#if defined(MHD_POSIX_SOCKETS) + if (EINTR != errno) + fprintf (stderr, "Unexpected select() error " + "at line %d.\n", (int) __LINE__); +#endif /* MHD_POSIX_SOCKETS */ + } + if (NULL != d_extern) + MHD_run (d_extern); + + if (! send_done) + { + ssize_t res; + + res = (ssize_t) send (fd, + (const void *) (parts[cur_part].data + cur_off), + parts[cur_part].size - cur_off, + ZZUF_SEND_FLAGS); + if (0 > res) + { + const int err = ZZUF_SOCK_ERR; + if (! ZZUF_ERR_IS_AGAIN (err)) + { + /* MHD (or socat) closed the connection: a perfectly normal + outcome when the traffic is being fuzzed. */ + send_done = 1; + peer_closed = 1; + } + } + else + { + cur_off += (size_t) res; + if (cur_off >= parts[cur_part].size) + { + cur_off = 0; + if (++cur_part >= num_parts) + send_done = 1; + } + } + } + + if (! peer_closed) + { + ssize_t res; + + res = (ssize_t) recv (fd, rbuf, sizeof(rbuf), 0); + if (0 == res) + peer_closed = 1; + else if (0 > res) + { + const int err = ZZUF_SOCK_ERR; + if (! ZZUF_ERR_IS_AGAIN (err)) + peer_closed = 1; + } + else if ((NULL != resp_buf) && (resp_pos < resp_buf_size)) + { + size_t to_copy = (size_t) res; + if (to_copy > resp_buf_size - resp_pos) + to_copy = resp_buf_size - resp_pos; + memcpy (resp_buf + resp_pos, rbuf, to_copy); + resp_pos += to_copy; + if (NULL != resp_len) + *resp_len = resp_pos; + } + } + } + (void) ZZUF_CLOSE_SOCKET (fd); + + if (NULL != d_extern) + { + /* Give MHD a chance to notice the closed connection and to clean up. */ + unsigned int i; + for (i = 0; i < 10; ++i) + MHD_run (d_extern); + } + return ZZUF_RAW_OK; +} + + +int +zzuf_raw_exchange (struct MHD_Daemon *d_extern, + uint16_t port, + const struct zzuf_raw_part *parts, + size_t num_parts, + unsigned int timeout_sec) +{ + return zzuf_raw_exchange2 (d_extern, port, 0, parts, num_parts, timeout_sec, + NULL, 0, NULL); +} + + +int +zzuf_raw_request (struct MHD_Daemon *d_extern, + uint16_t port, + const char *request) +{ + struct zzuf_raw_part part; + + part.data = request; + part.size = strlen (request); + return zzuf_raw_exchange (d_extern, port, &part, 1, + (unsigned int) ZZUF_CLIENT_TIMEOUT); +} + + +/* *** libcurl helpers *** */ + +#ifndef CURL_VERSION_BITS +#define CURL_VERSION_BITS(x,y,z) ((x) << 16 | (y) << 8 | (z)) +#endif /* ! CURL_VERSION_BITS */ +#ifndef CURL_AT_LEAST_VERSION +#define CURL_AT_LEAST_VERSION(x,y,z) \ + (LIBCURL_VERSION_NUM >= CURL_VERSION_BITS (x, y, z)) +#endif /* ! CURL_AT_LEAST_VERSION */ + +#define ZZUF_CURL_HOST "http:/" "/" ZZUF_MHD_LISTEN_IP +#define ZZUF_CURL_HOST_SOCAT "http:/" "/" ZZUF_SOCAT_IP + + +/** + * The libcurl write callback: discards the data. + * + * @param ptr the received data + * @param size the size of the item + * @param nmemb the number of items + * @param ctx the sink + * @return the number of processed bytes + */ +static size_t +curl_sink_cb (void *ptr, size_t size, size_t nmemb, void *ctx) +{ + struct zzuf_curl_sink *sink = (struct zzuf_curl_sink *) ctx; + + (void) ptr; /* Unused. Mute compiler warning. */ + if (sink->dn_pos + size * nmemb > sizeof(sink->buf)) + return 0; /* Overflow, abort the transfer */ + memcpy (sink->buf + sink->dn_pos, ptr, size * nmemb); + sink->dn_pos += size * nmemb; + return size * nmemb; +} + + +CURL * +zzuf_curl_setup (uint16_t port, + const char *uri_tail, + struct zzuf_curl_sink *sink) +{ + CURL *c; + CURLcode e; + char *uri; + size_t host_len; + size_t tail_len; + const char *host; + + host = zzuf_run_with_socat ? ZZUF_CURL_HOST_SOCAT : ZZUF_CURL_HOST; + if (zzuf_run_with_socat) + port = (uint16_t) ZZUF_SOCAT_PORT; + host_len = strlen (host); + tail_len = strlen (uri_tail); + uri = malloc (host_len + tail_len + 1); + if (NULL == uri) + { + fprintf (stderr, "malloc() failed at line %d.\n", (int) __LINE__); + return NULL; + } + memcpy (uri, host, host_len); + memcpy (uri + host_len, uri_tail, tail_len + 1); + + sink->dn_pos = 0; + c = curl_easy_init (); + if (NULL == c) + { + fprintf (stderr, "curl_easy_init() failed at line %d.\n", (int) __LINE__); + free (uri); + return NULL; + } + if ((CURLE_OK == (e = curl_easy_setopt (c, CURLOPT_URL, uri))) && + (CURLE_OK == (e = curl_easy_setopt (c, CURLOPT_NOSIGNAL, 1L))) && + (CURLE_OK == (e = curl_easy_setopt (c, CURLOPT_WRITEFUNCTION, + &curl_sink_cb))) && + (CURLE_OK == (e = curl_easy_setopt (c, CURLOPT_WRITEDATA, sink))) && + (CURLE_OK == (e = curl_easy_setopt (c, CURLOPT_CONNECTTIMEOUT, + ((long) ZZUF_CLIENT_TIMEOUT)))) && + (CURLE_OK == (e = curl_easy_setopt (c, CURLOPT_TIMEOUT, + ((long) ZZUF_CLIENT_TIMEOUT)))) && + (CURLE_OK == (e = curl_easy_setopt (c, CURLOPT_FAILONERROR, 0L))) && +#if CURL_AT_LEAST_VERSION (7, 45, 0) + (CURLE_OK == (e = curl_easy_setopt (c, CURLOPT_DEFAULT_PROTOCOL, + "http"))) && +#endif /* CURL_AT_LEAST_VERSION (7, 45, 0) */ +#if CURL_AT_LEAST_VERSION (7, 85, 0) + (CURLE_OK == (e = curl_easy_setopt (c, CURLOPT_PROTOCOLS_STR, + "http"))) && +#elif CURL_AT_LEAST_VERSION (7, 19, 4) + (CURLE_OK == (e = curl_easy_setopt (c, CURLOPT_PROTOCOLS, + CURLPROTO_HTTP))) && +#endif /* CURL_AT_LEAST_VERSION (7, 19, 4) */ + (CURLE_OK == (e = curl_easy_setopt (c, CURLOPT_HTTP_VERSION, + zzuf_oneone ? + CURL_HTTP_VERSION_1_1 : + CURL_HTTP_VERSION_1_0))) && +#if CURL_AT_LEAST_VERSION (7, 24, 0) + (CURLE_OK == (e = curl_easy_setopt (c, CURLOPT_INTERFACE, + "host!" ZZUF_CLIENT_BIND_IP))) && +#else /* ! CURL_AT_LEAST_VERSION (7, 24, 0) */ + (CURLE_OK == (e = curl_easy_setopt (c, CURLOPT_INTERFACE, + ZZUF_CLIENT_BIND_IP))) && +#endif /* ! CURL_AT_LEAST_VERSION (7, 24, 0) */ + (CURLE_OK == (e = curl_easy_setopt (c, CURLOPT_PORT, ((long) port))))) + { + free (uri); + return c; /* Success exit point */ + } + fprintf (stderr, "curl_easy_setopt() failed at line %d, error: %s\n", + (int) __LINE__, curl_easy_strerror (e)); + curl_easy_cleanup (c); + free (uri); + return NULL; /* Failure exit point */ +} + + +int +zzuf_curl_driver_init (struct zzuf_curl_driver *drv, + struct MHD_Daemon *d_extern) +{ + drv->d_extern = d_extern; + drv->multi = NULL; + if (NULL == d_extern) + return ! 0; + drv->multi = curl_multi_init (); + if (NULL == drv->multi) + { + fprintf (stderr, "curl_multi_init() failed at line %d.\n", (int) __LINE__); + return 0; + } + return ! 0; +} + + +void +zzuf_curl_driver_deinit (struct zzuf_curl_driver *drv) +{ + if (NULL != drv->multi) + curl_multi_cleanup (drv->multi); + drv->multi = NULL; + drv->d_extern = NULL; +} + + +void +zzuf_curl_driver_perform (struct zzuf_curl_driver *drv, + CURL *c) +{ + CURLMcode mret; + time_t start; + + if (NULL == drv->d_extern) + { + /* The daemon has an internal polling thread, just run the transfer. */ + (void) curl_easy_perform (c); + return; + } + mret = curl_multi_add_handle (drv->multi, c); + if (CURLM_OK != mret) + { + fprintf (stderr, "curl_multi_add_handle() failed at line %d, error: %s\n", + (int) __LINE__, curl_multi_strerror (mret)); + return; + } + start = time (NULL); + do + { + fd_set rs; + fd_set ws; + fd_set es; + int maxfd_curl; + MHD_socket maxfd_mhd; + int maxfd; + int running; + struct timeval tv; + + maxfd_curl = 0; + maxfd_mhd = MHD_INVALID_SOCKET; + FD_ZERO (&rs); + FD_ZERO (&ws); + FD_ZERO (&es); + curl_multi_perform (drv->multi, &running); + if (0 == running) + { + int msgs_left; + do + { + (void) curl_multi_info_read (drv->multi, &msgs_left); + } while (0 != msgs_left); + break; /* The transfer has been finished */ + } + mret = curl_multi_fdset (drv->multi, &rs, &ws, &es, &maxfd_curl); + if (CURLM_OK != mret) + { + fprintf (stderr, "curl_multi_fdset() failed at line %d, error: %s\n", + (int) __LINE__, curl_multi_strerror (mret)); + break; + } + if (MHD_YES != MHD_get_fdset (drv->d_extern, &rs, &ws, &es, &maxfd_mhd)) + { + fprintf (stderr, "MHD_get_fdset() failed at line %d.\n", (int) __LINE__); + break; + } +#ifndef MHD_WINSOCK_SOCKETS + if ((int) maxfd_mhd > maxfd_curl) + maxfd = (int) maxfd_mhd; + else +#endif /* ! MHD_WINSOCK_SOCKETS */ + maxfd = maxfd_curl; + tv.tv_sec = 0; + tv.tv_usec = 25 * 1000; + if (0 == MHD_get_timeout64s (drv->d_extern)) + tv.tv_usec = 0; + else + { + long curl_to = -1; + curl_multi_timeout (drv->multi, &curl_to); + if (0 == curl_to) + tv.tv_usec = 0; + } + if (-1 == select (maxfd + 1, &rs, &ws, &es, &tv)) + { +#if defined(MHD_POSIX_SOCKETS) + if (EINTR != errno) + fprintf (stderr, "Unexpected select() error at line %d.\n", + (int) __LINE__); +#else /* ! MHD_POSIX_SOCKETS */ + if ((WSAEINVAL != WSAGetLastError ()) || + (0 != rs.fd_count) || (0 != ws.fd_count) || (0 != es.fd_count)) + fprintf (stderr, "Unexpected select() error at line %d.\n", + (int) __LINE__); + Sleep ((unsigned long) tv.tv_usec / 1000); +#endif /* ! MHD_POSIX_SOCKETS */ + } + MHD_run (drv->d_extern); + } while (time (NULL) - start <= (time_t) ZZUF_CLIENT_TIMEOUT); + curl_multi_remove_handle (drv->multi, c); +} diff --git a/src/testzzuf/mhd_zzuf_common.h b/src/testzzuf/mhd_zzuf_common.h @@ -0,0 +1,572 @@ +/* + This file is part of GNU libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + GNU libmicrohttpd is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with GNU libmicrohttpd. + If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * @file testzzuf/mhd_zzuf_common.h + * @brief Shared support code for the fuzzing tests in this directory + * @author Christian Grothoff + * + * This header must be included *after* "platform.h" and <microhttpd.h>. + * + * The helpers here provide: + * - the common command line handling ("--dry-run", "--with-socat"), + * - the matrix of "hostile" daemon option profiles (small memory pools, + * small connection limits, short timeouts, strictness levels), + * - a generic driver that runs a client callback against every polling + * mode supported by the current MHD build, + * - a tiny raw socket HTTP client, needed for all requests that libcurl + * is unable to generate (chunk extensions, pipelining, header-less + * requests, hand-crafted "Authorization:" headers). + */ + +#ifndef MHD_ZZUF_COMMON_H +#define MHD_ZZUF_COMMON_H 1 + +#include <stddef.h> +#include <stdint.h> +#include <curl/curl.h> +#include <microhttpd.h> + +#ifndef MHD_STATICSTR_LEN_ +/** + * Determine length of static string / macro strings at compile time. + */ +#define MHD_STATICSTR_LEN_(macro) (sizeof(macro) / sizeof(char) - 1) +#endif /* ! MHD_STATICSTR_LEN_ */ + +/** + * The IP address MHD is listening on. + * Must match 'mhd_listen_ip' in zzuf_test_runner.sh, as this is the only + * address for which zzuf is instructed to fuzz the network traffic. + */ +#define ZZUF_MHD_LISTEN_IP "127.0.0.1" + +/** + * The IP address used by the test clients as the source address. + * Must be different from #ZZUF_MHD_LISTEN_IP so that the traffic + * generated by the clients themselves is not fuzzed. + */ +#define ZZUF_CLIENT_BIND_IP "127.0.0.101" + +/** + * The IP address socat is listening on. + * Must match 'socat_listen_ip' in zzuf_socat_test_runner.sh. + */ +#define ZZUF_SOCAT_IP "127.0.0.121" + +/** + * The port socat is listening on. + * Must match 'socat_listen_port' in zzuf_socat_test_runner.sh. + */ +#define ZZUF_SOCAT_PORT 10121 + +/** + * The port MHD is listening on when the port cannot be auto-detected. + * Must match 'mhd_listen_port' in zzuf_socat_test_runner.sh. + */ +#define ZZUF_BASE_PORT 4010 + +/** + * The default MHD connection timeout (in seconds) used by the tests. + */ +#define ZZUF_MHD_TIMEOUT 2 + +/** + * The client-side timeout (in seconds). + */ +#define ZZUF_CLIENT_TIMEOUT 5 + +/** + * A larger loop count runs more random tests, but takes longer. + * Can be redefined by CPPFLAGS=-DZZUF_LOOP_COUNT=123 + */ +#ifndef ZZUF_LOOP_COUNT +#ifndef _MHD_VHEAVY_TESTS +#define ZZUF_LOOP_COUNT 10 +#else /* _MHD_VHEAVY_TESTS */ +#define ZZUF_LOOP_COUNT 200 +#endif /* _MHD_VHEAVY_TESTS */ +#endif /* ! ZZUF_LOOP_COUNT */ + + +/* *** Global test parameters, initialised by zzuf_parse_common_args() *** */ + +/** + * Non-zero if the traffic is relayed (and fuzzed) by socat. + */ +extern int zzuf_run_with_socat; + +/** + * Non-zero if this is a "dry run", i.e. no real client requests are made. + */ +extern int zzuf_dry_run; + +/** + * Non-zero if HTTP/1.1 should be used by the clients, zero for HTTP/1.0. + */ +extern int zzuf_oneone; + +/** + * Non-zero if MHD should close the connection after each reply. + */ +extern int zzuf_use_close; + +/** + * Parse the common command line parameters and derive the test flavour + * from the program name. Also makes SIGPIPE non-fatal (if applicable). + * + * @param argc as given to main() + * @param argv as given to main() + */ +void +zzuf_parse_common_args (int argc, char *const *argv); + + +/** + * Check whether the name of the running program contains @a marker. + * zzuf_parse_common_args() must have been called before. + * + * @param marker the marker to look for + * @return non-zero if the marker is present, zero otherwise + */ +int +zzuf_name_has (const char *marker); + + +/** + * The number of client iterations to perform, zero for "dry runs". + */ +unsigned int +zzuf_loop_count (void); + + +/* *** The matrix of "hostile" daemon option profiles *** */ + +/** + * One set of daemon options used to widen the tested option matrix. + */ +struct zzuf_opt_profile +{ + /** + * Human readable name of the profile, used for logging. + */ + const char *name; + + /** + * The value for #MHD_OPTION_CONNECTION_MEMORY_LIMIT. + * Zero means "keep the MHD default". + * Values below MHD_BUF_INC_SIZE (1500) enable the read buffer + * "shift back" code path in MHD. + */ + size_t mem_limit; + + /** + * The value for #MHD_OPTION_CONNECTION_LIMIT. + * Zero means "keep the MHD default". + */ + unsigned int conn_limit; + + /** + * The value for #MHD_OPTION_CONNECTION_TIMEOUT (seconds). + * Never zero, as the tests rely on MHD closing idle connections. + */ + unsigned int timeout; + + /** + * The value for #MHD_OPTION_CLIENT_DISCIPLINE_LVL (or, when + * @a use_legacy_strict is set, for #MHD_OPTION_STRICT_FOR_CLIENT). + */ + int discipline_lvl; + + /** + * If non-zero, the (older) #MHD_OPTION_STRICT_FOR_CLIENT option is used + * instead of #MHD_OPTION_CLIENT_DISCIPLINE_LVL, so that the legacy + * option translation code is exercised as well. + */ + int use_legacy_strict; +}; + + +/** + * The number of available option profiles. + */ +unsigned int +zzuf_num_opt_profiles (void); + + +/** + * Get the option profile number @a idx (wrapping around). + * + * Profile zero is always the "plain" profile, i.e. the set of options + * that was used by the fuzzing tests before the option matrix was added. + */ +const struct zzuf_opt_profile * +zzuf_opt_profile (unsigned int idx); + + +/** + * The profile that is in use by the currently running daemon. + * Never NULL after the first daemon has been started. + */ +extern const struct zzuf_opt_profile *zzuf_active_profile; + + +/* *** Daemon handling *** */ + +/** + * Select the port to be used by the daemon. + * + * @param offset the per-test offset added to #ZZUF_BASE_PORT if the port + * cannot be auto-detected + * @return the port to be given to MHD_start_daemon(), may be zero + */ +uint16_t +zzuf_pick_port (uint16_t offset); + + +/** + * Start a daemon for the test. + * + * @param daemon_flags the flags for MHD_start_daemon() + * @param[in,out] pport the port to use, updated with the real port + * @param prof the option profile to apply, never NULL + * @param mem_limit_override if non-zero, this value is used for + * #MHD_OPTION_CONNECTION_MEMORY_LIMIT instead of + * the value from @a prof + * @param ahc the access handler callback + * @param ahc_cls the closure for @a ahc + * @param rcc the request completed callback, may be NULL + * @param rcc_cls the closure for @a rcc + * @param extra_opts additional options, terminated by #MHD_OPTION_END; + * may be NULL + * @return the daemon, or NULL on error + */ +struct MHD_Daemon * +zzuf_start_daemon (unsigned int daemon_flags, + uint16_t *pport, + const struct zzuf_opt_profile *prof, + size_t mem_limit_override, + MHD_AccessHandlerCallback ahc, + void *ahc_cls, + MHD_RequestCompletedCallback rcc, + void *rcc_cls, + const struct MHD_OptionItem *extra_opts); + + +/** + * The client callback invoked by zzuf_run_polling_modes() once per daemon. + * + * @param d_extern the daemon that must be driven by MHD_run() while the + * client is waiting; NULL if the daemon uses an internal + * polling thread + * @param port the port the daemon is listening on + * @param cls the closure + * @return zero on success, 77 to skip the test, 99 for an external + * (non-MHD) error, any other non-zero value for a test failure + */ +typedef unsigned int +(*zzuf_client_func)(struct MHD_Daemon *d_extern, + uint16_t port, + void *cls); + + +/** + * Parameters for zzuf_run_polling_modes(). + */ +struct zzuf_run_params +{ + /** + * The port to use, updated in place. + */ + uint16_t port; + + /** + * The access handler callback. + */ + MHD_AccessHandlerCallback ahc; + + /** + * The closure for @e ahc. + */ + void *ahc_cls; + + /** + * The request completed callback, may be NULL. + */ + MHD_RequestCompletedCallback rcc; + + /** + * The closure for @e rcc. + */ + void *rcc_cls; + + /** + * If non-zero, this value is always used for the connection memory + * limit, overriding the value from the option profile. + */ + size_t mem_limit_override; + + /** + * If non-zero, a different option profile is used for every started + * daemon, sweeping the whole option matrix during a single test run. + */ + int sweep_profiles; + + /** + * The number of the first option profile to use when @e sweep_profiles + * is set. Use 1 to skip the "plain" profile, i.e. to make sure that + * every daemon of the test uses a small connection memory pool. + */ + unsigned int profile_start; + + /** + * The client callback. + */ + zzuf_client_func client; + + /** + * The closure for @e client. + */ + void *client_cls; + + /** + * Additional daemon options, terminated by #MHD_OPTION_END. + * May be NULL. + */ + const struct MHD_OptionItem *extra_opts; +}; + + +/** + * Run the client callback against a daemon in every polling mode + * supported by the current MHD build. + * + * @param[in,out] p the parameters + * @return zero if all checks succeeded, 77 to skip, 99 for an external + * error, any other non-zero value on failure + */ +unsigned int +zzuf_run_polling_modes (struct zzuf_run_params *p); + + +/** + * Check whether the test can be run at all with the current build and + * the current invocation mode. + * + * @return zero if the test may run, 77 if the test must be skipped + */ +unsigned int +zzuf_check_runnable (void); + + +/* *** The raw socket client *** */ + +/** + * One piece of a raw request. Every piece is written with a separate + * send() call, so the pieces typically end up in separate TCP segments. + * This is used deliberately to feed MHD with partial header, chunk-size + * and chunk-extension lines. + */ +struct zzuf_raw_part +{ + /** + * The data to send, not zero-terminated. + */ + const char *data; + + /** + * The number of bytes to send. + */ + size_t size; +}; + +/** + * The raw exchange has been performed (whatever the result was). + */ +#define ZZUF_RAW_OK 0 + +/** + * The raw client could not be set up. This is an external error and + * must not be reported as a test failure. + */ +#define ZZUF_RAW_SETUP_FAILED 1 + + +/** + * Connect to the daemon (via socat, if used), send @a parts and read the + * reply until the peer closes the connection or the timeout expires. + * + * The reply is discarded: under fuzzing, any reply (including no reply + * at all) is a valid outcome. Only crashes and hangs are failures. + * + * @param d_extern if not NULL, MHD_run() is called while waiting + * @param port the port MHD is listening on (ignored if socat is used) + * @param parts the pieces of the request + * @param num_parts the number of @a parts + * @param timeout_sec the maximum number of seconds to spend + * @return #ZZUF_RAW_OK or #ZZUF_RAW_SETUP_FAILED + */ +int +zzuf_raw_exchange (struct MHD_Daemon *d_extern, + uint16_t port, + const struct zzuf_raw_part *parts, + size_t num_parts, + unsigned int timeout_sec); + + +/** + * The same as zzuf_raw_exchange(), but with two extra abilities. + * + * With @a bypass_relay set, the client connects to MHD directly instead of + * going through socat. As zzuf only fuzzes the socat process, such an + * exchange is *not* fuzzed at all. This makes it possible to run a small + * deterministic self-check of the parser inside the very same test binary. + * Note that this only works in the socat mode: when zzuf runs the test + * binary itself, all traffic accepted by MHD is fuzzed and there is no + * clean channel to MHD. + * + * @param d_extern if not NULL, MHD_run() is called while waiting + * @param port the port MHD is listening on + * @param bypass_relay if non-zero, connect to MHD directly + * @param parts the pieces of the request + * @param num_parts the number of @a parts + * @param timeout_sec the maximum number of seconds to spend + * @param[out] resp_buf the buffer for the reply, may be NULL + * @param resp_buf_size the size of @a resp_buf + * @param[out] resp_len set to the number of bytes stored in @a resp_buf, + * may be NULL + * @return #ZZUF_RAW_OK or #ZZUF_RAW_SETUP_FAILED + */ +int +zzuf_raw_exchange2 (struct MHD_Daemon *d_extern, + uint16_t port, + int bypass_relay, + const struct zzuf_raw_part *parts, + size_t num_parts, + unsigned int timeout_sec, + char *resp_buf, + size_t resp_buf_size, + size_t *resp_len); + + +/** + * Convenience wrapper around zzuf_raw_exchange() for a request that is + * sent as a single zero-terminated string. + * + * @param d_extern if not NULL, MHD_run() is called while waiting + * @param port the port MHD is listening on (ignored if socat is used) + * @param request the zero-terminated request + * @return #ZZUF_RAW_OK or #ZZUF_RAW_SETUP_FAILED + */ +int +zzuf_raw_request (struct MHD_Daemon *d_extern, + uint16_t port, + const char *request); + + +/* *** libcurl helpers *** */ + +/** + * The sink for the data downloaded by libcurl. + */ +struct zzuf_curl_sink +{ + /** + * The number of bytes received so far. + */ + size_t dn_pos; + + /** + * The scratch buffer. + */ + char buf[2048]; +}; + + +/** + * Create an "easy" handle with the settings that are common to all + * fuzzing tests in this directory. + * + * @param port the port MHD is listening on (ignored if socat is used) + * @param uri_tail the path (and query string) of the request-target, + * must start with a slash + * @param sink the download sink, must stay valid while the handle is used + * @return the handle, or NULL on failure + */ +CURL * +zzuf_curl_setup (uint16_t port, + const char *uri_tail, + struct zzuf_curl_sink *sink); + + +/** + * The state needed to run "easy" handles against a daemon that may or + * may not have an internal polling thread. + */ +struct zzuf_curl_driver +{ + /** + * The daemon to drive with MHD_run(), NULL for daemons with an internal + * polling thread. + */ + struct MHD_Daemon *d_extern; + + /** + * The "multi" handle, only used when @e d_extern is not NULL. + */ + CURLM *multi; +}; + + +/** + * Initialise the driver. + * + * @param[out] drv the driver to initialise + * @param d_extern the daemon to drive, or NULL + * @return non-zero on success, zero on failure + */ +int +zzuf_curl_driver_init (struct zzuf_curl_driver *drv, + struct MHD_Daemon *d_extern); + + +/** + * Release the resources of the driver. + * + * @param drv the driver to clean up + */ +void +zzuf_curl_driver_deinit (struct zzuf_curl_driver *drv); + + +/** + * Run a single transfer to completion (or to the timeout). + * + * The result of the transfer is deliberately ignored: with the traffic + * being fuzzed, libcurl may fail in arbitrary ways. + * + * @param drv the driver + * @param c the "easy" handle to run + */ +void +zzuf_curl_driver_perform (struct zzuf_curl_driver *drv, + CURL *c); + + +#endif /* MHD_ZZUF_COMMON_H */ diff --git a/src/testzzuf/test_basic_auth.c b/src/testzzuf/test_basic_auth.c @@ -0,0 +1,436 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + libmicrohttpd is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 2, or (at your + option) any later version. + + libmicrohttpd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libmicrohttpd; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +/** + * @file testzzuf/test_basic_auth.c + * @brief Fuzzing testcase for Basic Authentication + * @author Christian Grothoff + * + * Before this test existed, no fuzzing test of this directory ever sent an + * "Authorization:" header, so neither the Base64 decoder nor the username / + * password splitting of MHD were reachable by the fuzzer at all. + * + * libcurl performs a normal Basic Authentication handshake (so that zzuf's + * bit flips land inside a real "Authorization: Basic" header) and the raw + * socket client adds hand-crafted headers with Base64 payloads that are + * truncated, over-long, contain characters outside the Base64 alphabet, + * decode to data with binary zeros or lack the ':' separator entirely. + */ + +#include "platform.h" +#include <curl/curl.h> +#include <microhttpd.h> +#include <stdlib.h> +#include <string.h> + +#include "mhd_zzuf_common.h" + +#define TEST_MAGIC_MARKER 0xFEE1C0DE + +#define DENIED_PAGE "Access denied." +#define GRANTED_PAGE "Access granted." + +/** + * The port offset used if the port cannot be auto-detected. + */ +#define TEST_PORT_OFFSET 165 + +#define TEST_REALM "TestRealm" +#define TEST_USERNAME "testuser" +#define TEST_PASSWORD "testpass" + +#define TEST_URI "/bauth" + +#define HOST_HDR "Host: " ZZUF_MHD_LISTEN_IP "\r\n" + +/** + * The size of the buffer used to build the raw requests. + */ +#define RAW_BUF_SIZE 8192 + + +/** + * The closure of the access handler callback. + */ +struct ahc_param +{ + /** + * Must have #TEST_MAGIC_MARKER value. + */ + unsigned int magic; + + /** + * Non-zero if any error has been encountered. + */ + unsigned int err_flag; + + /** + * The checksum over the decoded credentials, only used so that the + * compiler cannot optimise the memory accesses away. + */ + unsigned int cred_sum; +}; + + +static enum MHD_Result +ahc_basic (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **req_cls) +{ + static int marker; + struct ahc_param *param = (struct ahc_param *) cls; + struct MHD_BasicAuthInfo *creds; + struct MHD_Response *response; + enum MHD_Result ret; + int authenticated = 0; + + (void) url; (void) method; (void) version; (void) upload_data; + + if ((NULL == param) || (TEST_MAGIC_MARKER != param->magic)) + { + fprintf (stderr, "The 'cls' parameter is invalid " + "at line %d.\n", (int) __LINE__); + fflush (stderr); + abort (); + } + if (NULL == *req_cls) + { + *req_cls = &marker; + return MHD_YES; + } + if ((NULL != upload_data_size) && (0 != *upload_data_size)) + { + *upload_data_size = 0; /* Discard the body */ + return MHD_YES; + } + + /* This is the call that decodes the (fuzzed or hand-crafted) client's + "Authorization: Basic" header. */ + creds = MHD_basic_auth_get_username_password3 (connection); + if (NULL != creds) + { + size_t i; + + if (NULL == creds->username) + { + fprintf (stderr, "The 'username' member is NULL " + "at line %d.\n", (int) __LINE__); + param->err_flag = 1; + } + else + { + /* Touch every byte, including the zero-termination that MHD + guarantees. */ + for (i = 0; i <= creds->username_len; ++i) + param->cred_sum += (unsigned int) (unsigned char) creds->username[i]; + } + if (NULL == creds->password) + { + if (0 != creds->password_len) + { + fprintf (stderr, "The 'password' is NULL while 'password_len' is " + "not zero at line %d.\n", (int) __LINE__); + param->err_flag = 1; + } + } + else + { + for (i = 0; i <= creds->password_len; ++i) + param->cred_sum += (unsigned int) (unsigned char) creds->password[i]; + } + if ((NULL != creds->username) && + (MHD_STATICSTR_LEN_ (TEST_USERNAME) == creds->username_len) && + (0 == memcmp (creds->username, TEST_USERNAME, + MHD_STATICSTR_LEN_ (TEST_USERNAME))) && + (NULL != creds->password) && + (MHD_STATICSTR_LEN_ (TEST_PASSWORD) == creds->password_len) && + (0 == memcmp (creds->password, TEST_PASSWORD, + MHD_STATICSTR_LEN_ (TEST_PASSWORD)))) + authenticated = 1; + MHD_free (creds); + } + + if (authenticated) + { + response = + MHD_create_response_from_buffer_static (MHD_STATICSTR_LEN_ ( + GRANTED_PAGE), + GRANTED_PAGE); + if (NULL == response) + return MHD_NO; /* External error */ + if (zzuf_use_close || ! zzuf_oneone) + (void) MHD_add_response_header (response, + MHD_HTTP_HEADER_CONNECTION, "close"); + ret = MHD_queue_response (connection, MHD_HTTP_OK, response); + MHD_destroy_response (response); + return ret; + } + + response = + MHD_create_response_from_buffer_static (MHD_STATICSTR_LEN_ (DENIED_PAGE), + DENIED_PAGE); + if (NULL == response) + return MHD_NO; /* External error */ + if (zzuf_use_close || ! zzuf_oneone) + (void) MHD_add_response_header (response, + MHD_HTTP_HEADER_CONNECTION, "close"); + ret = MHD_queue_basic_auth_required_response3 (connection, TEST_REALM, + MHD_NO, response); + MHD_destroy_response (response); + return ret; +} + + +/** + * Build one hostile "Authorization: Basic" request. + * + * @param variant the number of the variant to build + * @param[out] buf the buffer for the request + * @param buf_size the size of @a buf + * @return the length of the request, or zero on failure + */ +static size_t +build_hostile_request (unsigned int variant, + char *buf, + size_t buf_size) +{ + static const char b64_alphabet[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + char big[4096]; + const char *value; + size_t i; + int len; + + switch (variant % 10) + { + case 0: + /* A truncated Base64 payload (not a multiple of four). */ + value = "dGVzdHVzZXI"; + break; + case 1: + /* Characters outside the Base64 alphabet. */ + value = "!!!!\?\?\?\?===="; /* '?' escaped to avoid a trigraph */ + break; + case 2: + /* Valid Base64 that decodes to a string without ':' separator. */ + value = "dGVzdHVzZXI="; + break; + case 3: + /* Valid Base64 that decodes to "\0:\0" (binary zeros). */ + value = "ADoA"; + break; + case 4: + /* Valid Base64 that decodes to ":" only (empty username). */ + value = "Og=="; + break; + case 5: + /* Padding in the middle of the payload. */ + value = "dGVz=dXNlcjpwYXNz"; + break; + case 6: + /* An empty value. */ + value = ""; + break; + case 7: + /* A very long, but valid, Base64 payload. */ + for (i = 0; i + 1 < sizeof(big); ++i) + big[i] = b64_alphabet[i % 64]; + big[sizeof(big) - 1] = 0; + value = big; + break; + case 8: + /* Whitespace inside the payload. */ + value = "dGVz dXNl cjpw YXNz"; + break; + case 9: + default: + /* An unknown scheme, and a second "Authorization" header. */ + len = snprintf (buf, buf_size, + "GET " TEST_URI " HTTP/1.1\r\n" HOST_HDR + "Authorization: Bearer dGVzdHVzZXI6dGVzdHBhc3M=\r\n" + "Authorization: Basic\r\n" + "Connection: close\r\n\r\n"); + if ((0 > len) || (buf_size <= (size_t) len)) + return 0; + return (size_t) len; + } + len = snprintf (buf, buf_size, + "GET " TEST_URI " HTTP/1.1\r\n" HOST_HDR + "Authorization: Basic %s\r\n" + "Connection: close\r\n\r\n", + value); + if ((0 > len) || (buf_size <= (size_t) len)) + return 0; + return (size_t) len; +} + + +static unsigned int +client_run (struct MHD_Daemon *d_extern, + uint16_t port, + void *cls) +{ + struct ahc_param *param = (struct ahc_param *) cls; + struct zzuf_curl_driver drv; + struct zzuf_curl_sink sink; + char *req; + unsigned int i; + unsigned int loops; + + loops = zzuf_loop_count (); + if (! zzuf_curl_driver_init (&drv, d_extern)) + return 99; /* Not an MHD error */ + req = malloc (RAW_BUF_SIZE); + if (NULL == req) + { + zzuf_curl_driver_deinit (&drv); + return 99; /* Not an MHD error */ + } + + for (i = 0; i < loops; ++i) + { + CURL *c; + struct zzuf_raw_part part; + size_t req_len; + + fprintf (stderr, "."); + /* 1. A well-formed Basic Authentication handshake by libcurl, going + through the fuzzing layer. */ + c = zzuf_curl_setup (port, TEST_URI, &sink); + if (NULL == c) + { + free (req); + zzuf_curl_driver_deinit (&drv); + return 99; /* Not an MHD error */ + } + if ((CURLE_OK != curl_easy_setopt (c, CURLOPT_HTTPAUTH, + (long) CURLAUTH_BASIC)) || + (CURLE_OK != curl_easy_setopt (c, CURLOPT_USERPWD, + TEST_USERNAME ":" TEST_PASSWORD))) + { + curl_easy_cleanup (c); + free (req); + zzuf_curl_driver_deinit (&drv); + return 99; /* Not an MHD error */ + } + zzuf_curl_driver_perform (&drv, c); + curl_easy_cleanup (c); + + /* 2. The hand-crafted hostile headers. */ + req_len = build_hostile_request (i, req, RAW_BUF_SIZE); + if (0 != req_len) + { + part.data = req; + part.size = req_len; + /* Once through the fuzzing layer ... */ + if (ZZUF_RAW_SETUP_FAILED == + zzuf_raw_exchange (d_extern, port, &part, 1, + (unsigned int) ZZUF_CLIENT_TIMEOUT)) + { + free (req); + zzuf_curl_driver_deinit (&drv); + return 99; /* Not an MHD error */ + } + /* ... and once verbatim. */ + if (ZZUF_RAW_SETUP_FAILED == + zzuf_raw_exchange2 (d_extern, port, 1, &part, 1, + (unsigned int) ZZUF_CLIENT_TIMEOUT, + NULL, 0, NULL)) + { + free (req); + zzuf_curl_driver_deinit (&drv); + return 99; /* Not an MHD error */ + } + } + fflush (stderr); + } + free (req); + zzuf_curl_driver_deinit (&drv); + + if (0 != param->err_flag) + { + fprintf (stderr, "One or more errors have been detected by the access " + "handler callback function. At line %d.\n", (int) __LINE__); + return 1; + } + return 0; +} + + +int +main (int argc, char *const *argv) +{ + struct ahc_param param; + struct zzuf_run_params rp; + unsigned int res; + int use_magic_exit_codes; + + zzuf_parse_common_args (argc, argv); + use_magic_exit_codes = zzuf_run_with_socat || zzuf_dry_run; + + if (MHD_NO == MHD_is_feature_supported (MHD_FEATURE_BASIC_AUTH)) + { + fprintf (stderr, "Basic Authentication is not supported by this " + "MHD build.\n"); + return use_magic_exit_codes ? 77 : 0; + } + res = zzuf_check_runnable (); + if (0 != res) + return use_magic_exit_codes ? (int) res : 0; + + if (CURLE_OK != curl_global_init (CURL_GLOBAL_WIN32)) + { + fprintf (stderr, "curl_global_init() failed at line %d.\n", + (int) __LINE__); + return use_magic_exit_codes ? 99 : 0; + } + + param.magic = (unsigned int) TEST_MAGIC_MARKER; + param.err_flag = 0; + param.cred_sum = 0; + + memset (&rp, 0, sizeof(rp)); + rp.port = zzuf_pick_port (TEST_PORT_OFFSET); + rp.ahc = &ahc_basic; + rp.ahc_cls = &param; + rp.rcc = NULL; + rp.rcc_cls = NULL; + /* The hostile headers are large, use a memory limit that can hold them. */ + rp.mem_limit_override = 8192; + rp.sweep_profiles = 1; + rp.profile_start = 0; + rp.client = &client_run; + rp.client_cls = &param; + rp.extra_opts = NULL; + + res = zzuf_run_polling_modes (&rp); + curl_global_cleanup (); + + if (99 == res) + return use_magic_exit_codes ? 99 : 0; + if (77 == res) + return use_magic_exit_codes ? 77 : 0; + return (0 == res) ? 0 : 1; /* 0 == pass */ +} diff --git a/src/testzzuf/test_digest_auth.c b/src/testzzuf/test_digest_auth.c @@ -0,0 +1,642 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + libmicrohttpd is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 2, or (at your + option) any later version. + + libmicrohttpd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libmicrohttpd; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +/** + * @file testzzuf/test_digest_auth.c + * @brief Fuzzing testcase for Digest Authentication + * @author Christian Grothoff + * + * The Digest Authentication parser of MHD used to be completely outside of + * the reach of the fuzzing tests of this directory: no test ever produced + * an "Authorization: Digest" header. + * + * This test closes that gap from two sides: + * - libcurl performs a complete, well-formed digest handshake (two round + * trips), so zzuf's bit flips land inside real "WWW-Authenticate:" and + * "Authorization:" headers; + * - the raw socket client sends hand-crafted "Authorization: Digest" + * headers that are *syntactically* well-formed but semantically hostile + * (unknown "algorithm" token, over-long "response", over-long userhash, + * over-long username, missing parameters, ...). Random bit flipping + * essentially never produces such an input; these headers have to be + * written on purpose. + * + * The hostile headers that need a server generated nonce (everything that + * is checked *after* the nonce validation) are built from the nonce that + * MHD hands out in its own 401 reply, which the raw client parses. + */ + +#include "platform.h" +#include <curl/curl.h> +#include <microhttpd.h> +#include <stdlib.h> +#include <string.h> + +#include "mhd_zzuf_common.h" + +#define TEST_MAGIC_MARKER 0xFEE1C0DE + +#define DENIED_PAGE "Access denied." +#define GRANTED_PAGE "Access granted." + +/** + * The port offset used if the port cannot be auto-detected. + */ +#define TEST_PORT_OFFSET 155 + +#define TEST_REALM "TestRealm" +#define TEST_OPAQUE "0123456789abcdef" +#define TEST_USERNAME "testuser" +#define TEST_PASSWORD "testpass" + +/** + * The path that makes MHD issue an MD5 challenge (used by libcurl). + */ +#define URI_MD5 "/dauth/md5" + +/** + * The path that makes MHD issue a SHA-256 challenge. A SHA-256 nonce is + * needed to reach the code that decodes the client's "response" parameter + * with a SHA-256 sized digest. + */ +#define URI_SHA256 "/dauth/sha256" + +#define HOST_HDR "Host: " ZZUF_MHD_LISTEN_IP "\r\n" + +/** + * The size of the buffer used to build the raw requests. + */ +#define RAW_BUF_SIZE 2048 + +/** + * The random data for the nonce generation. + */ +static const char digest_rnd_data[32] = +{ 'z','z','u','f','_','d','i','g','e','s','t','_','a','u','t','h', + '_','r','a','n','d','o','m','_','s','e','e','d','_','0','0','1' }; + + +/** + * The closure of the access handler callback. + */ +struct ahc_param +{ + /** + * Must have #TEST_MAGIC_MARKER value. + */ + unsigned int magic; + + /** + * Non-zero if any error has been encountered. + */ + unsigned int err_flag; + + /** + * The number of successful authentications. + */ + unsigned int num_ok; +}; + + +static enum MHD_Result +ahc_digest (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **req_cls) +{ + static int marker; + struct ahc_param *param = (struct ahc_param *) cls; + struct MHD_Response *response; + enum MHD_Result ret; + enum MHD_DigestAuthResult check_res; + enum MHD_DigestAuthMultiAlgo3 challenge_algo; + + (void) method; (void) version; (void) upload_data; + + if ((NULL == param) || (TEST_MAGIC_MARKER != param->magic)) + { + fprintf (stderr, "The 'cls' parameter is invalid " + "at line %d.\n", (int) __LINE__); + fflush (stderr); + abort (); + } + if (NULL == *req_cls) + { + *req_cls = &marker; + return MHD_YES; + } + if ((NULL != upload_data_size) && (0 != *upload_data_size)) + { + *upload_data_size = 0; /* Discard the body */ + return MHD_YES; + } + + /* This is the call that parses the (fuzzed or hand-crafted) client's + "Authorization: Digest" header. */ + check_res = MHD_digest_auth_check3 (connection, + TEST_REALM, + TEST_USERNAME, + TEST_PASSWORD, + 0 /* daemon default timeout */, + 0 /* daemon default max nc */, + MHD_DIGEST_AUTH_MULT_QOP_ANY_NON_INT, + MHD_DIGEST_AUTH_MULT_ALGO3_ANY_NON_SESSION); + if (MHD_DAUTH_OK == check_res) + { + param->num_ok++; + response = + MHD_create_response_from_buffer_static (MHD_STATICSTR_LEN_ ( + GRANTED_PAGE), + GRANTED_PAGE); + if (NULL == response) + return MHD_NO; /* External error */ + if (zzuf_use_close || ! zzuf_oneone) + (void) MHD_add_response_header (response, + MHD_HTTP_HEADER_CONNECTION, "close"); + ret = MHD_queue_response (connection, MHD_HTTP_OK, response); + MHD_destroy_response (response); + return ret; + } + + /* Not authenticated (or the header was broken): send a challenge. */ + if ((NULL != url) && (0 == strcmp (url, URI_SHA256))) + challenge_algo = MHD_DIGEST_AUTH_MULT_ALGO3_SHA256; + else + challenge_algo = MHD_DIGEST_AUTH_MULT_ALGO3_MD5; + response = + MHD_create_response_from_buffer_static (MHD_STATICSTR_LEN_ (DENIED_PAGE), + DENIED_PAGE); + if (NULL == response) + return MHD_NO; /* External error */ + if (zzuf_use_close || ! zzuf_oneone) + (void) MHD_add_response_header (response, + MHD_HTTP_HEADER_CONNECTION, "close"); + ret = MHD_queue_auth_required_response3 (connection, + TEST_REALM, + TEST_OPAQUE, + NULL, + response, + (MHD_DAUTH_NONCE_STALE == + check_res) ? MHD_YES : MHD_NO, + MHD_DIGEST_AUTH_MULT_QOP_AUTH, + challenge_algo, + MHD_YES /* userhash support */, + MHD_NO); + MHD_destroy_response (response); + return ret; +} + + +/** + * Extract the value of the "nonce" parameter from a reply. + * + * @param reply the reply, not necessarily zero-terminated + * @param reply_len the number of valid bytes in @a reply + * @param[out] nonce the buffer for the nonce + * @param nonce_size the size of @a nonce + * @return non-zero if a nonce has been extracted, zero otherwise + */ +static int +extract_nonce (const char *reply, + size_t reply_len, + char *nonce, + size_t nonce_size) +{ + static const char marker[] = "nonce=\""; + size_t i; + + if (reply_len < MHD_STATICSTR_LEN_ (marker) + 1) + return 0; + for (i = 0; i <= reply_len - MHD_STATICSTR_LEN_ (marker) - 1; ++i) + { + size_t k; + + if (0 != memcmp (reply + i, marker, MHD_STATICSTR_LEN_ (marker))) + continue; + i += MHD_STATICSTR_LEN_ (marker); + for (k = 0; (i + k < reply_len) && (k + 1 < nonce_size); ++k) + { + if ('"' == reply[i + k]) + { + nonce[k] = 0; + return (0 != k) ? ! 0 : 0; + } + if (('\r' == reply[i + k]) || ('\n' == reply[i + k])) + return 0; + nonce[k] = reply[i + k]; + } + return 0; + } + return 0; +} + + +/** + * Fill @a buf with @a len hexadecimal digits and zero-terminate it. + * + * @param[out] buf the buffer to fill + * @param len the number of digits to write + */ +static void +fill_hex (char *buf, size_t len) +{ + static const char hex[] = "0123456789abcdef"; + size_t i; + + for (i = 0; i < len; ++i) + buf[i] = hex[i % 16]; + buf[len] = 0; +} + + +/** + * Build one hostile "Authorization: Digest" request. + * + * @param variant the number of the variant to build + * @param nonce the server generated nonce, may be an empty string + * @param[out] buf the buffer for the request + * @param buf_size the size of @a buf + * @return the length of the request, or zero on failure + */ +static size_t +build_hostile_request (unsigned int variant, + const char *nonce, + char *buf, + size_t buf_size) +{ + char big1[512]; + char big2[512]; + int len; + + switch (variant % 10) + { + case 0: + /* An "algorithm" token that MHD does not know. Before the fix of + 2026-07-27 this made MHD_digest_auth_check3() run into + MHD_PANIC("Wrong 'malgo3' value, API violation") and abort(). + No valid nonce is needed: the algorithm is checked first. */ + len = snprintf (buf, buf_size, + "GET " URI_MD5 " HTTP/1.1\r\n" HOST_HDR + "Authorization: Digest username=\"" TEST_USERNAME "\", " + "realm=\"" TEST_REALM "\", nonce=\"%s\", uri=\"" URI_MD5 + "\", algorithm=BOGUS-ALGO, qop=auth, nc=00000001, " + "cnonce=\"0a0b0c0d\", response=\"" + "0123456789abcdef0123456789abcdef\"\r\n" + "Connection: close\r\n\r\n", + nonce); + break; + case 1: + /* An empty (quoted) "algorithm" value. */ + len = snprintf (buf, buf_size, + "GET " URI_MD5 " HTTP/1.1\r\n" HOST_HDR + "Authorization: Digest username=\"" TEST_USERNAME "\", " + "realm=\"" TEST_REALM "\", nonce=\"%s\", uri=\"" URI_MD5 + "\", algorithm=\"\", qop=auth, nc=00000001, " + "cnonce=\"0a0b0c0d\", response=\"" + "0123456789abcdef0123456789abcdef\"\r\n" + "Connection: close\r\n\r\n", + nonce); + break; + case 2: + /* A "-sess" algorithm, which MHD does not support. */ + len = snprintf (buf, buf_size, + "GET " URI_MD5 " HTTP/1.1\r\n" HOST_HDR + "Authorization: Digest username=\"" TEST_USERNAME "\", " + "realm=\"" TEST_REALM "\", nonce=\"%s\", uri=\"" URI_MD5 + "\", algorithm=MD5-sess, qop=auth, nc=00000001, " + "cnonce=\"0a0b0c0d\", response=\"" + "0123456789abcdef0123456789abcdef\"\r\n" + "Connection: close\r\n\r\n", + nonce); + break; + case 3: + /* An over-long "response" together with a SHA-256 nonce. Before the + fix of 2026-07-27 the hex decoder wrote 64 bytes into a 32 byte + stack buffer. This needs a *valid* server nonce, otherwise the + nonce check rejects the request before the "response" is decoded. */ + fill_hex (big1, 128); + len = snprintf (buf, buf_size, + "GET " URI_SHA256 " HTTP/1.1\r\n" HOST_HDR + "Authorization: Digest username=\"" TEST_USERNAME "\", " + "realm=\"" TEST_REALM "\", nonce=\"%s\", uri=\"" + URI_SHA256 "\", algorithm=SHA-256, qop=auth, " + "nc=00000001, cnonce=\"0a0b0c0d\", response=\"%s\"\r\n" + "Connection: close\r\n\r\n", + nonce, big1); + break; + case 4: + /* The same, unquoted. */ + fill_hex (big1, 128); + len = snprintf (buf, buf_size, + "GET " URI_SHA256 " HTTP/1.1\r\n" HOST_HDR + "Authorization: Digest username=\"" TEST_USERNAME "\", " + "realm=\"" TEST_REALM "\", nonce=%s, uri=\"" + URI_SHA256 "\", algorithm=SHA-256, qop=auth, " + "nc=00000001, cnonce=\"0a0b0c0d\", response=%s\r\n" + "Connection: close\r\n\r\n", + nonce, big1); + break; + case 5: + /* An over-long hex encoded userhash. */ + fill_hex (big1, 256); + fill_hex (big2, 64); + len = snprintf (buf, buf_size, + "GET " URI_SHA256 " HTTP/1.1\r\n" HOST_HDR + "Authorization: Digest username=\"%s\", userhash=true, " + "realm=\"" TEST_REALM "\", nonce=\"%s\", uri=\"" + URI_SHA256 "\", algorithm=SHA-256, qop=auth, " + "nc=00000001, cnonce=\"0a0b0c0d\", response=\"%s\"\r\n" + "Connection: close\r\n\r\n", + big1, nonce, big2); + break; + case 6: + /* A very long username. */ + fill_hex (big1, 500); + len = snprintf (buf, buf_size, + "GET " URI_MD5 " HTTP/1.1\r\n" HOST_HDR + "Authorization: Digest username=\"%s\", " + "realm=\"" TEST_REALM "\", nonce=\"%s\", uri=\"" URI_MD5 + "\", algorithm=MD5, qop=auth, nc=00000001, " + "cnonce=\"0a0b0c0d\", response=\"" + "0123456789abcdef0123456789abcdef\"\r\n" + "Connection: close\r\n\r\n", + big1, nonce); + break; + case 7: + /* No "response" parameter at all, plus the extended notation for the + username. */ + len = snprintf (buf, buf_size, + "GET " URI_MD5 " HTTP/1.1\r\n" HOST_HDR + "Authorization: Digest username*=UTF-8''%%41%%42, " + "realm=\"" TEST_REALM "\", nonce=\"%s\", uri=\"" URI_MD5 + "\", algorithm=MD5, qop=auth, nc=00000001, " + "cnonce=\"0a0b0c0d\"\r\n" + "Connection: close\r\n\r\n", + nonce); + break; + case 8: + /* Garbage in "nc" and an unterminated quoted string. */ + len = snprintf (buf, buf_size, + "GET " URI_MD5 " HTTP/1.1\r\n" HOST_HDR + "Authorization: Digest username=\"" TEST_USERNAME "\", " + "realm=\"" TEST_REALM "\", nonce=\"%s\", uri=\"" URI_MD5 + "\", algorithm=MD5, qop=auth, nc=zzzzzzzz, " + "cnonce=\"0a0b0c0d, response=\"" + "0123456789abcdef0123456789abcdef\"\r\n" + "Connection: close\r\n\r\n", + nonce); + break; + case 9: + default: + /* A "Digest" scheme without any parameter, and a second, empty + "Authorization" header. */ + len = snprintf (buf, buf_size, + "GET " URI_MD5 " HTTP/1.1\r\n" HOST_HDR + "Authorization: Digest\r\n" + "Authorization: \r\n" + "Connection: close\r\n\r\n"); + break; + } + if ((0 > len) || (buf_size <= (size_t) len)) + return 0; + return (size_t) len; +} + + +/** + * Ask MHD for a challenge and extract the nonce from the reply. + * + * @param d_extern the daemon to drive, or NULL + * @param port the port MHD is listening on + * @param uri the URI to request + * @param[out] nonce the buffer for the nonce + * @param nonce_size the size of @a nonce + */ +static void +fetch_nonce (struct MHD_Daemon *d_extern, + uint16_t port, + const char *uri, + char *nonce, + size_t nonce_size) +{ + struct zzuf_raw_part part; + char req[256]; + char reply[2048]; + size_t reply_len = 0; + int len; + + nonce[0] = 0; + len = snprintf (req, sizeof(req), + "GET %s HTTP/1.1\r\n" HOST_HDR "Connection: close\r\n\r\n", + uri); + if ((0 > len) || (sizeof(req) <= (size_t) len)) + return; + part.data = req; + part.size = (size_t) len; + /* Bypass socat (when it is used): a nonce that has been mangled by zzuf + is useless for building a request that must survive the nonce check. */ + if (ZZUF_RAW_OK != + zzuf_raw_exchange2 (d_extern, port, 1, &part, 1, + (unsigned int) ZZUF_CLIENT_TIMEOUT, + reply, sizeof(reply), &reply_len)) + return; + (void) extract_nonce (reply, reply_len, nonce, nonce_size); +} + + +static unsigned int +client_run (struct MHD_Daemon *d_extern, + uint16_t port, + void *cls) +{ + struct ahc_param *param = (struct ahc_param *) cls; + struct zzuf_curl_driver drv; + struct zzuf_curl_sink sink; + char nonce_md5[256]; + char nonce_sha256[256]; + char *req; + unsigned int i; + unsigned int loops; + + loops = zzuf_loop_count (); + if (! zzuf_curl_driver_init (&drv, d_extern)) + return 99; /* Not an MHD error */ + req = malloc (RAW_BUF_SIZE); + if (NULL == req) + { + zzuf_curl_driver_deinit (&drv); + return 99; /* Not an MHD error */ + } + nonce_md5[0] = 0; + nonce_sha256[0] = 0; + + for (i = 0; i < loops; ++i) + { + CURL *c; + struct zzuf_raw_part part; + size_t req_len; + const char *nonce; + + fprintf (stderr, "."); + /* 1. A complete, well-formed digest handshake performed by libcurl. + The traffic goes through the fuzzing layer. */ + c = zzuf_curl_setup (port, URI_MD5, &sink); + if (NULL == c) + { + free (req); + zzuf_curl_driver_deinit (&drv); + return 99; /* Not an MHD error */ + } + if ((CURLE_OK != curl_easy_setopt (c, CURLOPT_HTTPAUTH, + (long) CURLAUTH_DIGEST)) || + (CURLE_OK != curl_easy_setopt (c, CURLOPT_USERPWD, + TEST_USERNAME ":" TEST_PASSWORD))) + { + curl_easy_cleanup (c); + free (req); + zzuf_curl_driver_deinit (&drv); + return 99; /* Not an MHD error */ + } + zzuf_curl_driver_perform (&drv, c); + curl_easy_cleanup (c); + + /* 2. Refresh the nonces from time to time: MHD only accepts a given + nonce/nc combination once. */ + if (0 == i % 2) + { + fetch_nonce (d_extern, port, URI_MD5, nonce_md5, sizeof(nonce_md5)); + fetch_nonce (d_extern, port, URI_SHA256, + nonce_sha256, sizeof(nonce_sha256)); + } + + /* 3. The hand-crafted hostile headers. */ + nonce = ((3 == i % 10) || (4 == i % 10) || (5 == i % 10)) ? + nonce_sha256 : nonce_md5; + req_len = build_hostile_request (i, nonce, req, RAW_BUF_SIZE); + if (0 != req_len) + { + part.data = req; + part.size = req_len; + /* Once through the fuzzing layer ... */ + if (ZZUF_RAW_SETUP_FAILED == + zzuf_raw_exchange (d_extern, port, &part, 1, + (unsigned int) ZZUF_CLIENT_TIMEOUT)) + { + free (req); + zzuf_curl_driver_deinit (&drv); + return 99; /* Not an MHD error */ + } + /* ... and once verbatim, so that the hostile header is guaranteed + to reach the parser unmodified at least once. */ + if (ZZUF_RAW_SETUP_FAILED == + zzuf_raw_exchange2 (d_extern, port, 1, &part, 1, + (unsigned int) ZZUF_CLIENT_TIMEOUT, + NULL, 0, NULL)) + { + free (req); + zzuf_curl_driver_deinit (&drv); + return 99; /* Not an MHD error */ + } + } + fflush (stderr); + } + free (req); + zzuf_curl_driver_deinit (&drv); + + if (0 != param->err_flag) + { + fprintf (stderr, "One or more errors have been detected by the access " + "handler callback function. At line %d.\n", (int) __LINE__); + return 1; + } + return 0; +} + + +int +main (int argc, char *const *argv) +{ + struct ahc_param param; + struct zzuf_run_params rp; + struct MHD_OptionItem extra_opts[3]; + unsigned int res; + int use_magic_exit_codes; + + zzuf_parse_common_args (argc, argv); + use_magic_exit_codes = zzuf_run_with_socat || zzuf_dry_run; + + if (MHD_NO == MHD_is_feature_supported (MHD_FEATURE_DIGEST_AUTH)) + { + fprintf (stderr, "Digest Authentication is not supported by this " + "MHD build.\n"); + return use_magic_exit_codes ? 77 : 0; + } + res = zzuf_check_runnable (); + if (0 != res) + return use_magic_exit_codes ? (int) res : 0; + + if (CURLE_OK != curl_global_init (CURL_GLOBAL_WIN32)) + { + fprintf (stderr, "curl_global_init() failed at line %d.\n", + (int) __LINE__); + return use_magic_exit_codes ? 99 : 0; + } + + param.magic = (unsigned int) TEST_MAGIC_MARKER; + param.err_flag = 0; + param.num_ok = 0; + + extra_opts[0].option = MHD_OPTION_DIGEST_AUTH_RANDOM; + extra_opts[0].value = (intptr_t) sizeof(digest_rnd_data); + extra_opts[0].ptr_value = (void *) (intptr_t) digest_rnd_data; + extra_opts[1].option = MHD_OPTION_NONCE_NC_SIZE; + extra_opts[1].value = (intptr_t) 300; + extra_opts[1].ptr_value = NULL; + extra_opts[2].option = MHD_OPTION_END; + extra_opts[2].value = 0; + extra_opts[2].ptr_value = NULL; + + memset (&rp, 0, sizeof(rp)); + rp.port = zzuf_pick_port (TEST_PORT_OFFSET); + rp.ahc = &ahc_digest; + rp.ahc_cls = &param; + rp.rcc = NULL; + rp.rcc_cls = NULL; + /* The digest headers are large, use a memory limit that can hold them. */ + rp.mem_limit_override = 4096; + rp.sweep_profiles = 1; + rp.profile_start = 0; + rp.client = &client_run; + rp.client_cls = &param; + rp.extra_opts = extra_opts; + + res = zzuf_run_polling_modes (&rp); + curl_global_cleanup (); + + if (99 == res) + return use_magic_exit_codes ? 99 : 0; + if (77 == res) + return use_magic_exit_codes ? 77 : 0; + return (0 == res) ? 0 : 1; /* 0 == pass */ +} diff --git a/src/testzzuf/test_get.c b/src/testzzuf/test_get.c @@ -39,6 +39,7 @@ #endif #include "mhd_debug_funcs.h" +#include "mhd_zzuf_common.h" #include "test_helpers.h" #ifndef MHD_STATICSTR_LEN_ @@ -98,6 +99,24 @@ static int use_long_header; static int use_long_uri; static int use_close; static int run_with_socat; +/** + * Non-zero if this program sweeps the matrix of "hostile" daemon options + * (small connection memory pools, small connection limits, short timeouts + * and the various client discipline levels) instead of using the single + * "plain" set of daemon options. + * Derived from the "_hostile" marker in the program name. + */ +static int use_hostile_opts; + +/** + * The daemon option profile that is currently in use. + */ +static const struct zzuf_opt_profile *cur_profile; + +/** + * The connection timeout that the current daemon has been started with. + */ +static unsigned int expected_conn_timeout = MHD_TIMEOUT; #define TEST_BASE_URI "http:/" "/127.0.0.1/test_uri" #define TEST_BASE_URI_SOCAT "http:/" "/127.0.0.121/test_uri" @@ -575,7 +594,7 @@ ahc_check (void *cls, "at line %d.\n", (int) __LINE__); param->err_flag = 1; } - else if (MHD_TIMEOUT != conn_info->connection_timeout) + else if (expected_conn_timeout != conn_info->connection_timeout) { fprintf (stderr, "The 'MHD_get_connection_info' has returned " "unexpected timeout value " @@ -1177,9 +1196,13 @@ start_daemon_for_test (unsigned int daemon_flags, uint16_t *pport, struct MHD_OptionItem ops[] = { { MHD_OPTION_END, 0, NULL }, { MHD_OPTION_END, 0, NULL }, + { MHD_OPTION_END, 0, NULL }, + { MHD_OPTION_END, 0, NULL }, + { MHD_OPTION_END, 0, NULL }, { MHD_OPTION_END, 0, NULL } }; size_t num_opt; + size_t mem_limit; num_opt = 0; @@ -1187,16 +1210,33 @@ start_daemon_for_test (unsigned int daemon_flags, uint16_t *pport, callback_param->err_flag = 0; callback_param->num_replies = 0; + /* The tests with intentionally huge URIs, headers or bodies need a memory + limit that matches the data they send; for all other tests the limit + comes from the option profile (which may be "keep the default"). */ if (use_put_large) + mem_limit = (size_t) (PUT_LARGE_SIZE / 4); + else if (use_long_header || use_long_uri) + mem_limit = (size_t) (TEST_STRING_VLONG_LEN / 2); + else + mem_limit = cur_profile->mem_limit; + if (0 != mem_limit) { ops[num_opt].option = MHD_OPTION_CONNECTION_MEMORY_LIMIT; - ops[num_opt].value = (intptr_t) (PUT_LARGE_SIZE / 4); + ops[num_opt].value = (intptr_t) mem_limit; ++num_opt; } - else if (use_long_header || use_long_uri) + if (0 != cur_profile->conn_limit) { - ops[num_opt].option = MHD_OPTION_CONNECTION_MEMORY_LIMIT; - ops[num_opt].value = (intptr_t) (TEST_STRING_VLONG_LEN / 2); + ops[num_opt].option = MHD_OPTION_CONNECTION_LIMIT; + ops[num_opt].value = (intptr_t) cur_profile->conn_limit; + ++num_opt; + } + if (0 != cur_profile->discipline_lvl) + { + ops[num_opt].option = cur_profile->use_legacy_strict ? + MHD_OPTION_STRICT_FOR_CLIENT : + MHD_OPTION_CLIENT_DISCIPLINE_LVL; + ops[num_opt].value = (intptr_t) cur_profile->discipline_lvl; ++num_opt; } if (0 == (MHD_USE_INTERNAL_POLLING_THREAD & daemon_flags)) @@ -1205,11 +1245,12 @@ start_daemon_for_test (unsigned int daemon_flags, uint16_t *pport, ops[num_opt].value = (intptr_t) (FD_SETSIZE); ++num_opt; } + expected_conn_timeout = cur_profile->timeout; d = MHD_start_daemon (daemon_flags /* | MHD_USE_ERROR_LOG */, *pport, NULL, NULL, &ahc_check, callback_param, MHD_OPTION_CONNECTION_TIMEOUT, - (unsigned int) MHD_TIMEOUT, + expected_conn_timeout, MHD_OPTION_NOTIFY_COMPLETED, &req_completed_cleanup, callback_param, MHD_OPTION_ARRAY, ops, @@ -1245,9 +1286,20 @@ start_daemon_for_test (unsigned int daemon_flags, uint16_t *pport, } +/** + * Select the daemon option profile for the next daemon and print + * a description of the test that is about to start. + * + * @param daemon_flags the flags of the daemon that is about to be started + */ static void print_test_starting (unsigned int daemon_flags) { + static unsigned int profile_num; + + /* Every started daemon uses the next profile of the matrix, so that a + single run of a "_hostile" program sweeps the whole option matrix. */ + cur_profile = zzuf_opt_profile (use_hostile_opts ? profile_num++ : 0); fflush (stderr); if (0 != (MHD_USE_INTERNAL_POLLING_THREAD & daemon_flags)) { @@ -1280,6 +1332,16 @@ print_test_starting (unsigned int daemon_flags) printf ("\nStarting test with%s thread safety with external polling.\n", ((0 != (MHD_USE_NO_THREAD_SAFETY & daemon_flags)) ? "out" : "")); } + if (use_hostile_opts) + printf ("Daemon options profile '%s' " + "(mem_limit=%lu, conn_limit=%u, timeout=%u, %s=%d).\n", + cur_profile->name, + (unsigned long) cur_profile->mem_limit, + cur_profile->conn_limit, + cur_profile->timeout, + cur_profile->use_legacy_strict ? + "strict_for_client" : "discipline_lvl", + cur_profile->discipline_lvl); fflush (stdout); } @@ -1591,6 +1653,8 @@ run_all_checks (void) port += 20; else if (use_put_chunked) port += 25; + if (use_hostile_opts) + port += 40; } } else @@ -1655,6 +1719,8 @@ main (int argc, char *const *argv) use_long_header = has_in_name (argv[0], "_long_header"); use_long_uri = has_in_name (argv[0], "_long_uri"); use_close = has_in_name (argv[0], "_close"); + use_hostile_opts = has_in_name (argv[0], "_hostile"); + cur_profile = zzuf_opt_profile (0); run_with_socat = has_param (argc, argv, "--with-socat"); dry_run = has_param (argc, argv, "--dry-run") || diff --git a/src/testzzuf/test_get_args.c b/src/testzzuf/test_get_args.c @@ -0,0 +1,439 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + libmicrohttpd is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 2, or (at your + option) any later version. + + libmicrohttpd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libmicrohttpd; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +/** + * @file testzzuf/test_get_args.c + * @brief Fuzzing testcase for GET requests with hostile query strings + * @author Christian Grothoff + * + * The point of this test is the combination of + * - query strings that contain arguments *without* the '=' character, + * empty names, empty values, trailing '&', percent-encoded names and + * a very long tail argument, and + * - a very small connection memory pool (below MHD_BUF_INC_SIZE == 1500). + * + * Only with a pool that small does MHD try to re-use the tail of the + * request header buffer ("shift back", see get_req_headers() in + * connection.c), and the position of the end of the *last parsed element* + * is used for the pointer arithmetic there. For an argument without '=' + * that element has no value at all, so the "end of the value" is not a + * valid pointer. + * + * libcurl always sends at least a "Host:" header, so with libcurl alone + * the last parsed element is always a header and never a query argument. + * Therefore this test additionally uses the raw socket client to send + * header-less HTTP/1.0 requests, for which the last parsed element really + * is the trailing query argument. + */ + +#include "platform.h" +#include <curl/curl.h> +#include <microhttpd.h> +#include <stdlib.h> +#include <string.h> + +#include "mhd_zzuf_common.h" + +#define TEST_MAGIC_MARKER 0xFEE1C0DE + +#define EMPTY_PAGE "Empty page." + +/** + * The port offset used if the port cannot be auto-detected. + */ +#define TEST_PORT_OFFSET 140 + +/** + * The length of the "very long" trailing argument. + * Deliberately larger than the largest memory pool of the option matrix, + * so that MHD has to reject some of the requests as "too large" as well. + */ +#define LONG_ARG_LEN 700 + + +/** + * The closure of the access handler callback. + */ +struct ahc_param +{ + /** + * Must have #TEST_MAGIC_MARKER value. + */ + unsigned int magic; + + /** + * Non-zero if any error has been encountered. + */ + unsigned int err_flag; + + /** + * The checksum over all seen arguments, only used to make sure that + * the compiler cannot optimise the memory accesses away. + */ + unsigned int arg_sum; +}; + + +/** + * The query strings used by the libcurl-driven part of the test. + * Every one of them ends with an element that has no '=' character. + */ +static const char *const query_strings[] = { + "/test_uri?a=1&b&c=&d", + "/test_uri?x&y=&z=%20%41&trail", + "/test_uri?%61%62=%63&noeq", + "/test_uri?one=1&&two=2&", + "/test_uri?=novalue&=&x", + "/test_uri?a=1;b=2&c", + "/test_uri?%zz=%2&broken%", + "/test_uri?dup=1&dup=2&dup" +}; + +/** + * The raw (header-less) requests. Filled in by init_raw_requests(). + */ +static char *raw_requests[6]; + + +/** + * Build the raw requests used by this test. + * + * @return non-zero on success, zero on failure + */ +static int +init_raw_requests (void) +{ + static const char *const tails[] = { + /* Last element of the request has no '=': the interesting case. */ + "/test_uri?a=1&b HTTP/1.0\r\n\r\n", + "/test_uri?trailing_argument_without_equals_sign HTTP/1.0\r\n\r\n", + "/test_uri?a=1&b=2& HTTP/1.0\r\n\r\n", + "/test_uri?= HTTP/1.0\r\n\r\n" + }; + size_t i; + char *buf; + + for (i = 0; i < sizeof(tails) / sizeof(tails[0]); ++i) + { + const size_t len = strlen ("GET ") + strlen (tails[i]); + buf = malloc (len + 1); + if (NULL == buf) + return 0; + memcpy (buf, "GET ", strlen ("GET ")); + memcpy (buf + strlen ("GET "), tails[i], strlen (tails[i]) + 1); + raw_requests[i] = buf; + } + /* A header-less request with a very long trailing argument without '=' */ + buf = malloc (LONG_ARG_LEN + 64); + if (NULL == buf) + return 0; + memcpy (buf, "GET /t?k=v&", strlen ("GET /t?k=v&")); + memset (buf + strlen ("GET /t?k=v&"), 'L', LONG_ARG_LEN); + memcpy (buf + strlen ("GET /t?k=v&") + LONG_ARG_LEN, + " HTTP/1.0\r\n\r\n", + strlen (" HTTP/1.0\r\n\r\n") + 1); + raw_requests[4] = buf; + /* The same, but with a single (minimal) header line, so that the last + parsed element is a header and the "normal" branch is taken with the + very same (small) memory pool. */ + buf = malloc (LONG_ARG_LEN + 96); + if (NULL == buf) + return 0; + memcpy (buf, "GET /t?k=v&", strlen ("GET /t?k=v&")); + memset (buf + strlen ("GET /t?k=v&"), 'L', LONG_ARG_LEN); + memcpy (buf + strlen ("GET /t?k=v&") + LONG_ARG_LEN, + " HTTP/1.1\r\nHost: a\r\n\r\n", + strlen (" HTTP/1.1\r\nHost: a\r\n\r\n") + 1); + raw_requests[5] = buf; + return ! 0; +} + + +/** + * Free the raw requests. + */ +static void +free_raw_requests (void) +{ + size_t i; + + for (i = 0; i < sizeof(raw_requests) / sizeof(raw_requests[0]); ++i) + { + if (NULL != raw_requests[i]) + free (raw_requests[i]); + raw_requests[i] = NULL; + } +} + + +/** + * Touch every byte of every key and value so that the sanitizers (or + * the operating system) notice pointers into unmapped memory. + * + * @param cls the closure + * @param kind the kind of the value + * @param key the key, never NULL + * @param key_size the size of @a key + * @param value the value, may be NULL for arguments without '=' + * @param value_size the size of @a value + * @return #MHD_YES to continue the iteration + */ +static enum MHD_Result +arg_iterator (void *cls, + enum MHD_ValueKind kind, + const char *key, + size_t key_size, + const char *value, + size_t value_size) +{ + struct ahc_param *param = (struct ahc_param *) cls; + size_t i; + + (void) kind; /* Unused. Mute compiler warning. */ + if (TEST_MAGIC_MARKER != param->magic) + { + fprintf (stderr, "The 'param->magic' has wrong value " + "at line %d.\n", (int) __LINE__); + fflush (stderr); + abort (); + } + if ((NULL == key) && (0 != key_size)) + { + fprintf (stderr, "The 'key' is NULL while 'key_size' is not zero " + "at line %d.\n", (int) __LINE__); + param->err_flag = 1; + return MHD_YES; + } + if ((NULL == value) && (0 != value_size)) + { + fprintf (stderr, "The 'value' is NULL while 'value_size' is not zero " + "at line %d.\n", (int) __LINE__); + param->err_flag = 1; + return MHD_YES; + } + for (i = 0; i < key_size; ++i) + param->arg_sum += (unsigned int) (unsigned char) key[i]; + for (i = 0; i < value_size; ++i) + param->arg_sum += (unsigned int) (unsigned char) value[i]; + /* Also touch the zero-termination that MHD guarantees. */ + if (NULL != key) + param->arg_sum += (unsigned int) (unsigned char) key[key_size]; + if (NULL != value) + param->arg_sum += (unsigned int) (unsigned char) value[value_size]; + return MHD_YES; +} + + +static enum MHD_Result +ahc_args (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **req_cls) +{ + static int marker; + struct ahc_param *param = (struct ahc_param *) cls; + struct MHD_Response *response; + enum MHD_Result ret; + const char *val; + + (void) url; (void) method; (void) version; (void) upload_data; + + if ((NULL == param) || (TEST_MAGIC_MARKER != param->magic)) + { + fprintf (stderr, "The 'cls' parameter is invalid " + "at line %d.\n", (int) __LINE__); + fflush (stderr); + abort (); + } + if (NULL == *req_cls) + { + *req_cls = &marker; + return MHD_YES; + } + if ((NULL != upload_data_size) && (0 != *upload_data_size)) + { + *upload_data_size = 0; /* Discard any body */ + return MHD_YES; + } + + /* Walk over all arguments; this is what dereferences the (possibly + broken) pointers stored while the request line was parsed. */ + (void) MHD_get_connection_values_n (connection, + MHD_GET_ARGUMENT_KIND, + &arg_iterator, + param); + /* Also walk over the headers, so that a corrupted header list is + detected as well. */ + (void) MHD_get_connection_values_n (connection, + MHD_HEADER_KIND, + &arg_iterator, + param); + /* Lookups of both present and absent keys, including a key that is + known to be present without a value. */ + val = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "b"); + if (NULL != val) + param->arg_sum += (unsigned int) strlen (val); + val = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "a"); + if (NULL != val) + param->arg_sum += (unsigned int) strlen (val); + val = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, + "no_such_argument"); + if (NULL != val) + param->arg_sum += (unsigned int) strlen (val); + + response = + MHD_create_response_from_buffer_static (MHD_STATICSTR_LEN_ (EMPTY_PAGE), + EMPTY_PAGE); + if (NULL == response) + { + fprintf (stderr, "MHD_create_response_from_buffer_static() failed " + "at line %d.\n", (int) __LINE__); + return MHD_NO; /* External error, do not raise the error flag */ + } + ret = MHD_YES; + if (zzuf_use_close || ! zzuf_oneone) + ret = MHD_add_response_header (response, + MHD_HTTP_HEADER_CONNECTION, + "close"); + if (MHD_YES == ret) + ret = MHD_queue_response (connection, MHD_HTTP_OK, response); + MHD_destroy_response (response); + return ret; +} + + +static unsigned int +client_run (struct MHD_Daemon *d_extern, + uint16_t port, + void *cls) +{ + struct ahc_param *param = (struct ahc_param *) cls; + struct zzuf_curl_driver drv; + struct zzuf_curl_sink sink; + unsigned int i; + unsigned int loops; + + loops = zzuf_loop_count (); + if (! zzuf_curl_driver_init (&drv, d_extern)) + return 99; /* Not an MHD error */ + + for (i = 0; i < loops; ++i) + { + CURL *c; + const unsigned int num_q = + (unsigned int) (sizeof(query_strings) / sizeof(query_strings[0])); + const unsigned int num_r = + (unsigned int) (sizeof(raw_requests) / sizeof(raw_requests[0])); + + fprintf (stderr, "."); + c = zzuf_curl_setup (port, query_strings[i % num_q], &sink); + if (NULL == c) + { + zzuf_curl_driver_deinit (&drv); + return 99; /* Not an MHD error */ + } + zzuf_curl_driver_perform (&drv, c); + curl_easy_cleanup (c); + + /* The raw, header-less requests: only these can make a query + argument the last parsed element of the request header. */ + if (ZZUF_RAW_SETUP_FAILED == + zzuf_raw_request (d_extern, port, raw_requests[i % num_r])) + { + fprintf (stderr, "The raw client could not be set up " + "at line %d.\n", (int) __LINE__); + zzuf_curl_driver_deinit (&drv); + return 99; /* Not an MHD error */ + } + fflush (stderr); + } + zzuf_curl_driver_deinit (&drv); + + if (0 != param->err_flag) + { + fprintf (stderr, "One or more errors have been detected by the access " + "handler callback function. At line %d.\n", (int) __LINE__); + return 1; + } + return 0; +} + + +int +main (int argc, char *const *argv) +{ + struct ahc_param param; + struct zzuf_run_params rp; + unsigned int res; + int use_magic_exit_codes; + + zzuf_parse_common_args (argc, argv); + use_magic_exit_codes = zzuf_run_with_socat || zzuf_dry_run; + + res = zzuf_check_runnable (); + if (0 != res) + return use_magic_exit_codes ? (int) res : 0; + + if (CURLE_OK != curl_global_init (CURL_GLOBAL_WIN32)) + { + fprintf (stderr, "curl_global_init() failed at line %d.\n", + (int) __LINE__); + return use_magic_exit_codes ? 99 : 0; + } + memset (raw_requests, 0, sizeof(raw_requests)); + if (! init_raw_requests ()) + { + fprintf (stderr, "malloc() failed at line %d.\n", (int) __LINE__); + free_raw_requests (); + curl_global_cleanup (); + return use_magic_exit_codes ? 99 : 0; + } + + param.magic = (unsigned int) TEST_MAGIC_MARKER; + param.err_flag = 0; + param.arg_sum = 0; + + memset (&rp, 0, sizeof(rp)); + rp.port = zzuf_pick_port (TEST_PORT_OFFSET); + rp.ahc = &ahc_args; + rp.ahc_cls = &param; + rp.rcc = NULL; + rp.rcc_cls = NULL; + rp.mem_limit_override = 0; /* Use the small pools of the option matrix */ + rp.sweep_profiles = 1; + rp.profile_start = 1; /* Skip the "plain" (large pool) profile */ + rp.client = &client_run; + rp.client_cls = &param; + + res = zzuf_run_polling_modes (&rp); + + free_raw_requests (); + curl_global_cleanup (); + + if (99 == res) + return use_magic_exit_codes ? 99 : 0; + if (77 == res) + return use_magic_exit_codes ? 77 : 0; + return (0 == res) ? 0 : 1; /* 0 == pass */ +} diff --git a/src/testzzuf/test_get_chunked_ext.c b/src/testzzuf/test_get_chunked_ext.c @@ -0,0 +1,742 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + libmicrohttpd is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 2, or (at your + option) any later version. + + libmicrohttpd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libmicrohttpd; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +/** + * @file testzzuf/test_get_chunked_ext.c + * @brief Fuzzing testcase for chunked transfers with chunk extensions + * @author Christian Grothoff + * + * This single source builds two programs: + * - test_get_chunked_ext: the request is a GET with a chunked body and + * MHD answers with a chunked reply that carries a footer, so that both + * the request-side chunk parser and the reply-side chunk writer are + * exercised; + * - test_put_chunked_ext: the request is a PUT with a chunked body and + * MHD answers from a static buffer. + * + * libcurl cannot generate chunk extensions, so the requests are written + * by the raw socket client of mhd_zzuf_common.c. The client still talks + * to the very same port (and therefore through the very same zzuf/socat + * corruption layer) as the libcurl based tests of this directory. + * + * In addition to the fuzzed traffic, the test runs a small deterministic + * self-check of the chunk-extension parser. The self-check talks to MHD + * *directly*, bypassing socat, and is therefore not fuzzed at all; it can + * only be run in the socat mode (which is the mode that is mandatory when + * MHD is built with sanitizers). The self-check verifies that a chunked + * body with chunk extensions is decoded byte-exactly; a parser that fails + * to consume the CRLF that terminates a chunk-extension line cannot pass + * it. + */ + +#include "platform.h" +#include <curl/curl.h> +#include <microhttpd.h> +#include <stdlib.h> +#include <string.h> + +#include "mhd_zzuf_common.h" + +#define TEST_MAGIC_MARKER 0xFEE1C0DE + +#define EMPTY_PAGE "Empty page." + +/** + * The port offset used if the port cannot be auto-detected. + */ +#define TEST_PORT_OFFSET 145 + +/** + * The port offset of the (unfuzzed) self-check daemon. + */ +#define TEST_SELFCHECK_PORT_OFFSET 160 + +/** + * The maximum size of a request body accepted by this test. + */ +#define MAX_BODY_SIZE 256 + + +/** + * Non-zero for the "put" flavour of this test. + */ +static int use_put; + +/** + * Non-zero if MHD should answer with a chunked reply. + */ +static int use_chunked_reply; + + +/** + * One chunked request body with (possibly hostile) chunk extensions. + */ +struct chunk_case +{ + /** + * The name of the case; also used as the last path segment of the URI. + */ + const char *name; + + /** + * The chunked body, exactly as it goes on the wire. + */ + const char *wire; + + /** + * The body that MHD is expected to decode, or NULL if the request is + * not required to be accepted at all (deliberately malformed input). + */ + const char *expected; +}; + + +static const struct chunk_case chunk_cases[] = { + { "simple", + "5;name=value\r\nHELLO\r\n0\r\n\r\n", + "HELLO" }, + { "flag", + "5;flag\r\nHELLO\r\n0\r\n\r\n", + "HELLO" }, + { "quoted", + "5;n=\"a;b=c\"\r\nHELLO\r\n0\r\n\r\n", + "HELLO" }, + { "multi", + "3;a=b;c=d;e\r\nabc\r\n2;x\r\nde\r\n0\r\n\r\n", + "abcde" }, + { "lastext", + "5;a=b\r\nHELLO\r\n0;final=1\r\n\r\n", + "HELLO" }, + { "trailers", + "5;a=b\r\nHELLO\r\n0\r\nX-Trailer: yes\r\nX-Other: 1\r\n\r\n", + "HELLO" }, + { "extonly", + "5;a\r\nHELLO\r\n0;b\r\n\r\n", + "HELLO" }, + /* The following cases are deliberately malformed: MHD is free to reject + them, it just must not misbehave. */ + { "wsbefore", + "5 ;n=v\r\nHELLO\r\n0\r\n\r\n", + NULL }, + { "wsaround", + "5; n = v \r\nHELLO\r\n0\r\n\r\n", + NULL }, + { "barelf", + "5;a=b\nHELLO\r\n0\r\n\r\n", + NULL }, + { "extcr", + "5;a=b\rHELLO\r\n0\r\n\r\n", + NULL }, + { "noext", + "5;\r\nHELLO\r\n0\r\n\r\n", + NULL } +}; + +#define NUM_CHUNK_CASES \ + (sizeof(chunk_cases) / sizeof(chunk_cases[0])) + +/** + * The number of leading entries of #chunk_cases that are well-formed and + * therefore usable for the deterministic self-check. + */ +#define NUM_VALID_CHUNK_CASES 7 + +/** + * A chunk-size line with a very long extension, built at run time. + */ +static char *long_ext_body; + + +/** + * The closure of the access handler callback. + */ +struct ahc_param +{ + /** + * Must have #TEST_MAGIC_MARKER value. + */ + unsigned int magic; + + /** + * Non-zero if any error has been encountered. + */ + unsigned int err_flag; + + /** + * Non-zero while the deterministic self-check is running. + */ + int self_check; + + /** + * The body that is expected during the self-check, may be NULL. + */ + const char *expect_body; + + /** + * Set to non-zero by the handler when the expected body has been seen. + */ + int body_matched; +}; + + +/** + * The per-request state. + */ +struct req_state +{ + /** + * The number of bytes stored in @e body. + */ + size_t body_len; + + /** + * Set to non-zero if more data has been received than fits into @e body. + */ + int overflow; + + /** + * The received body. + */ + char body[MAX_BODY_SIZE]; +}; + + +/** + * The closure of the content reader callback. + */ +struct content_cb_param +{ + /** + * The response the callback belongs to, needed to add the footer. + */ + struct MHD_Response *response; +}; + + +/** + * MHD content reader callback for the chunked reply. + * + * @param cls the closure + * @param pos the position in the reply body + * @param[out] buf the buffer to fill + * @param max the size of @a buf + * @return the number of bytes written, or the end-of-stream marker + */ +static ssize_t +content_cb (void *cls, uint64_t pos, char *buf, size_t max) +{ + struct content_cb_param *ccp = (struct content_cb_param *) cls; + + if (pos >= 512) + { + if (MHD_YES != MHD_add_response_footer (ccp->response, + "Footer", "working")) + { + fprintf (stderr, "MHD_add_response_footer() failed " + "at line %d.\n", (int) __LINE__); + fflush (stderr); + abort (); + } + return MHD_CONTENT_READER_END_OF_STREAM; + } + if (max > 128) + max = 128; + memset (buf, 'A' + (char) (unsigned int) (pos / 128), max); + return (ssize_t) max; +} + + +/** + * Free the closure of the content reader callback. + * + * @param ptr the closure to free + */ +static void +content_cb_free (void *ptr) +{ + free (ptr); +} + + +/** + * Iterator over the request footers (trailers). + * + * @param cls the closure + * @param kind the kind of the value + * @param key the key + * @param value the value + * @return #MHD_YES to continue the iteration + */ +static enum MHD_Result +footer_iterator (void *cls, + enum MHD_ValueKind kind, + const char *key, + const char *value) +{ + unsigned int *sum = (unsigned int *) cls; + + (void) kind; /* Unused. Mute compiler warning. */ + if (NULL != key) + *sum += (unsigned int) strlen (key); + if (NULL != value) + *sum += (unsigned int) strlen (value); + return MHD_YES; +} + + +static enum MHD_Result +ahc_chunked_ext (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **req_cls) +{ + struct ahc_param *param = (struct ahc_param *) cls; + struct req_state *rs; + struct MHD_Response *response; + enum MHD_Result ret; + unsigned int footer_sum = 0; + + (void) url; (void) method; (void) version; + + if ((NULL == param) || (TEST_MAGIC_MARKER != param->magic)) + { + fprintf (stderr, "The 'cls' parameter is invalid " + "at line %d.\n", (int) __LINE__); + fflush (stderr); + abort (); + } + if (NULL == *req_cls) + { + rs = malloc (sizeof(struct req_state)); + if (NULL == rs) + return MHD_NO; /* External error */ + rs->body_len = 0; + rs->overflow = 0; + *req_cls = rs; + return MHD_YES; + } + rs = (struct req_state *) *req_cls; + + if ((NULL != upload_data_size) && (0 != *upload_data_size)) + { + size_t to_copy = *upload_data_size; + + if (to_copy > sizeof(rs->body) - rs->body_len) + { + to_copy = sizeof(rs->body) - rs->body_len; + rs->overflow = 1; + } + if ((0 != to_copy) && (NULL != upload_data)) + { + memcpy (rs->body + rs->body_len, upload_data, to_copy); + rs->body_len += to_copy; + } + *upload_data_size = 0; /* All data have been processed */ + return MHD_YES; + } + + /* The request is complete. */ + (void) MHD_get_connection_values (connection, MHD_FOOTER_KIND, + &footer_iterator, &footer_sum); + if (param->self_check && (NULL != param->expect_body)) + { + const size_t elen = strlen (param->expect_body); + + if ((! rs->overflow) && + (elen == rs->body_len) && + (0 == memcmp (rs->body, param->expect_body, elen))) + param->body_matched = 1; + else + { + fprintf (stderr, + "Self-check: the decoded request body does not match the " + "expected body. Expected %u bytes ('%s'), got %u bytes. " + "At line %d.\n", + (unsigned int) elen, param->expect_body, + (unsigned int) rs->body_len, (int) __LINE__); + param->err_flag = 1; + } + } + free (rs); + *req_cls = NULL; + + if (use_chunked_reply) + { + struct content_cb_param *ccp; + + ccp = malloc (sizeof(struct content_cb_param)); + if (NULL == ccp) + return MHD_NO; /* External error */ + ccp->response = NULL; + response = MHD_create_response_from_callback (MHD_SIZE_UNKNOWN, 1024, + &content_cb, ccp, + &content_cb_free); + if (NULL == response) + free (ccp); + else + ccp->response = response; + } + else + response = + MHD_create_response_from_buffer_static (MHD_STATICSTR_LEN_ (EMPTY_PAGE), + EMPTY_PAGE); + if (NULL == response) + { + fprintf (stderr, "Failed to create the response " + "at line %d.\n", (int) __LINE__); + return MHD_NO; /* External error, do not raise the error flag */ + } + ret = MHD_YES; + if (zzuf_use_close || ! zzuf_oneone) + ret = MHD_add_response_header (response, + MHD_HTTP_HEADER_CONNECTION, + "close"); + if (MHD_YES == ret) + ret = MHD_queue_response (connection, MHD_HTTP_OK, response); + MHD_destroy_response (response); + return ret; +} + + +static void +req_completed (void *cls, + struct MHD_Connection *connection, + void **req_cls, + enum MHD_RequestTerminationCode toe) +{ + (void) cls; (void) connection; (void) toe; + + if (NULL == *req_cls) + return; + free (*req_cls); + *req_cls = NULL; +} + + +/** + * Build the request head for a given case. + * + * @param name the case name (used in the URI) + * @param close_conn if non-zero, "Connection: close" is added, so that MHD + * closes the connection right after the reply instead of + * waiting for the connection timeout + * @param[out] buf the buffer to fill + * @param buf_size the size of @a buf + */ +static void +build_head (const char *name, int close_conn, char *buf, size_t buf_size) +{ + int len; + + len = snprintf (buf, buf_size, + "%s /chunked_ext/%s HTTP/1.1\r\n" + "Host: " ZZUF_MHD_LISTEN_IP "\r\n" + "Transfer-Encoding: chunked\r\n" + "%s" + "\r\n", + use_put ? "PUT" : "GET", + name, + close_conn ? "Connection: close\r\n" : ""); + if ((0 > len) || (buf_size <= (size_t) len)) + { + fprintf (stderr, "snprintf() failed at line %d.\n", (int) __LINE__); + fflush (stderr); + abort (); + } +} + + +static unsigned int +client_run (struct MHD_Daemon *d_extern, + uint16_t port, + void *cls) +{ + struct ahc_param *param = (struct ahc_param *) cls; + unsigned int i; + unsigned int loops; + + loops = zzuf_loop_count (); + for (i = 0; i < loops; ++i) + { + const struct chunk_case *cc = chunk_cases + (i % NUM_CHUNK_CASES); + struct zzuf_raw_part parts[4]; + char head[256]; + size_t num_parts; + const char *wire; + size_t wire_len; + size_t split; + + fprintf (stderr, "."); + build_head (cc->name, 0, head, sizeof(head)); + if ((NULL != long_ext_body) && (0 == i % 5)) + wire = long_ext_body; /* Occasionally use the over-long extension */ + else + wire = cc->wire; + wire_len = strlen (wire); + /* Split the body so that the chunk-size (and chunk-extension) lines + are torn apart between two TCP segments. */ + split = wire_len / 2; + parts[0].data = head; + parts[0].size = strlen (head); + parts[1].data = wire; + parts[1].size = split; + parts[2].data = wire + split; + parts[2].size = wire_len - split; + /* Always append a pipelined follow-up request: if the chunk-extension + line is not consumed correctly, the request boundary is lost and this + second request is interpreted as chunk data (or the other way round). + Its "Connection: close" also makes MHD close the connection right + after the reply instead of waiting for the connection timeout. */ + parts[3].data = "GET /chunked_ext/after HTTP/1.1\r\n" + "Host: " ZZUF_MHD_LISTEN_IP "\r\n" + "Connection: close\r\n\r\n"; + parts[3].size = strlen (parts[3].data); + num_parts = 4; + if (ZZUF_RAW_SETUP_FAILED == + zzuf_raw_exchange (d_extern, port, parts, num_parts, + (unsigned int) ZZUF_CLIENT_TIMEOUT)) + { + fprintf (stderr, "The raw client could not be set up " + "at line %d.\n", (int) __LINE__); + return 99; /* Not an MHD error */ + } + fflush (stderr); + } + + if (0 != param->err_flag) + { + fprintf (stderr, "One or more errors have been detected by the access " + "handler callback function. At line %d.\n", (int) __LINE__); + return 1; + } + return 0; +} + + +/** + * Run the deterministic (unfuzzed) self-check of the chunk-extension + * parser. Only possible in the socat mode, where MHD's own port is not + * touched by zzuf. + * + * @return zero on success, non-zero on failure + */ +static unsigned int +run_self_check (void) +{ + struct MHD_Daemon *d; + struct MHD_Daemon *d_extern; + struct ahc_param param; + uint16_t port; + unsigned int flags; + unsigned int i; + unsigned int ret = 0; + + param.magic = (unsigned int) TEST_MAGIC_MARKER; + param.err_flag = 0; + param.self_check = 1; + param.expect_body = NULL; + param.body_matched = 0; + + if (MHD_NO != MHD_is_feature_supported (MHD_FEATURE_AUTODETECT_BIND_PORT)) + port = 0; + else + port = (uint16_t) (ZZUF_BASE_PORT + TEST_SELFCHECK_PORT_OFFSET); + if (MHD_YES == MHD_is_feature_supported (MHD_FEATURE_THREADS)) + flags = MHD_USE_SELECT_INTERNALLY; + else + flags = MHD_USE_NO_THREAD_SAFETY; + d = zzuf_start_daemon (flags, &port, zzuf_opt_profile (0), 0, + &ahc_chunked_ext, &param, + &req_completed, &param, NULL); + if (NULL == d) + return 99; /* Not an MHD error */ + d_extern = (0 == (MHD_USE_INTERNAL_POLLING_THREAD & flags)) ? d : NULL; + + printf ("Running the deterministic (unfuzzed) chunk-extension " + "self-check...\n"); + fflush (stdout); + for (i = 0; i < NUM_VALID_CHUNK_CASES; ++i) + { + const struct chunk_case *cc = chunk_cases + i; + struct zzuf_raw_part parts[3]; + char head[256]; + char resp[512]; + size_t resp_len = 0; + size_t wire_len; + size_t split; + + build_head (cc->name, 1, head, sizeof(head)); + wire_len = strlen (cc->wire); + split = wire_len / 2; + parts[0].data = head; + parts[0].size = strlen (head); + parts[1].data = cc->wire; + parts[1].size = split; + parts[2].data = cc->wire + split; + parts[2].size = wire_len - split; + + param.expect_body = cc->expected; + param.body_matched = 0; + if (ZZUF_RAW_SETUP_FAILED == + zzuf_raw_exchange2 (d_extern, port, 1, parts, 3, + (unsigned int) ZZUF_CLIENT_TIMEOUT, + resp, sizeof(resp), &resp_len)) + { + fprintf (stderr, "The raw client could not be set up " + "at line %d.\n", (int) __LINE__); + MHD_stop_daemon (d); + return 99; /* Not an MHD error */ + } + if (! param.body_matched) + { + fprintf (stderr, + "Self-check FAILED for the chunk-extension case '%s': " + "MHD did not decode the body as expected. " + "At line %d.\n", cc->name, (int) __LINE__); + ret = 1; + } + else if ((12 > resp_len) || (0 != memcmp (resp, "HTTP/1.1 200", 12))) + { + fprintf (stderr, + "Self-check FAILED for the chunk-extension case '%s': " + "MHD did not reply with '200'. " + "At line %d.\n", cc->name, (int) __LINE__); + ret = 1; + } + } + MHD_stop_daemon (d); + if (0 != param.err_flag) + ret = 1; + if (0 == ret) + printf ("The chunk-extension self-check succeeded.\n"); + fflush (stdout); + return ret; +} + + +/** + * Build the chunked body with a very long chunk extension. + * + * With a connection memory pool of 256 or 512 bytes the chunk-size line + * does not fit into the read buffer, so MHD emits the 413 reply from + * handle_req_chunk_size_line_no_space(). Before commit e04eb218 it then + * re-entered the error path and discarded that reply, so this is also the + * regression test for it. + * + * @return non-zero on success, zero on failure + */ +static int +init_long_ext_body (void) +{ + static const char head[] = "5;longext="; + static const char tail[] = "\r\nHELLO\r\n0\r\n\r\n"; + const size_t ext_len = 600; + char *buf; + + buf = malloc (sizeof(head) + ext_len + sizeof(tail)); + if (NULL == buf) + return 0; + memcpy (buf, head, MHD_STATICSTR_LEN_ (head)); + memset (buf + MHD_STATICSTR_LEN_ (head), 'E', ext_len); + memcpy (buf + MHD_STATICSTR_LEN_ (head) + ext_len, tail, sizeof(tail)); + long_ext_body = buf; + return ! 0; +} + + +int +main (int argc, char *const *argv) +{ + struct ahc_param param; + struct zzuf_run_params rp; + unsigned int res; + int use_magic_exit_codes; + + zzuf_parse_common_args (argc, argv); + use_put = zzuf_name_has ("_put"); + use_chunked_reply = zzuf_name_has ("_get"); + if (1 != ((use_put ? 1 : 0) + (use_chunked_reply ? 1 : 0))) + { + fprintf (stderr, "Wrong test name '%s': no or multiple indications " + "for the test type.\n", (NULL != argv[0]) ? argv[0] : "(NULL)"); + return 99; + } + use_magic_exit_codes = zzuf_run_with_socat || zzuf_dry_run; + + res = zzuf_check_runnable (); + if (0 != res) + return use_magic_exit_codes ? (int) res : 0; + + long_ext_body = NULL; + if (! init_long_ext_body ()) + { + fprintf (stderr, "malloc() failed at line %d.\n", (int) __LINE__); + return use_magic_exit_codes ? 99 : 0; + } + + /* The self-check needs an unfuzzed channel to MHD, which only exists + when zzuf fuzzes socat instead of this process. */ + if (zzuf_run_with_socat && ! zzuf_dry_run) + { + res = run_self_check (); + if (99 == res) + { + free (long_ext_body); + return 99; + } + if (0 != res) + { + free (long_ext_body); + return 1; + } + } + + param.magic = (unsigned int) TEST_MAGIC_MARKER; + param.err_flag = 0; + param.self_check = 0; + param.expect_body = NULL; + param.body_matched = 0; + + memset (&rp, 0, sizeof(rp)); + rp.port = zzuf_pick_port (TEST_PORT_OFFSET); + rp.ahc = &ahc_chunked_ext; + rp.ahc_cls = &param; + rp.rcc = &req_completed; + rp.rcc_cls = &param; + rp.mem_limit_override = 0; + rp.sweep_profiles = 1; + rp.profile_start = 0; + rp.client = &client_run; + rp.client_cls = &param; + + res = zzuf_run_polling_modes (&rp); + free (long_ext_body); + long_ext_body = NULL; + + if (99 == res) + return use_magic_exit_codes ? 99 : 0; + if (77 == res) + return use_magic_exit_codes ? 77 : 0; + return (0 == res) ? 0 : 1; /* 0 == pass */ +} diff --git a/src/testzzuf/test_pipelined.c b/src/testzzuf/test_pipelined.c @@ -0,0 +1,301 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff + + libmicrohttpd is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 2, or (at your + option) any later version. + + libmicrohttpd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libmicrohttpd; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +/** + * @file testzzuf/test_pipelined.c + * @brief Fuzzing testcase for pipelined requests + * @author Christian Grothoff + * + * Several complete requests are written with a single send() call, so that + * MHD has to find the request boundaries inside one read buffer. Corrupting + * such a stream (or combining it with a connection memory pool that is too + * small to hold all pipelined requests at once) is the most direct way to + * produce a request boundary de-synchronisation. + * + * libcurl does not pipeline any more (HTTP/1.1 pipelining support was + * removed in libcurl 7.62), therefore the raw socket client is used. The + * traffic still goes to the same port and through the same zzuf/socat + * corruption layer as the libcurl based tests of this directory. + */ + +#include "platform.h" +#include <curl/curl.h> +#include <microhttpd.h> +#include <stdlib.h> +#include <string.h> + +#include "mhd_zzuf_common.h" + +#define TEST_MAGIC_MARKER 0xFEE1C0DE + +#define EMPTY_PAGE "Empty page." + +/** + * The port offset used if the port cannot be auto-detected. + */ +#define TEST_PORT_OFFSET 150 + +/** + * The maximum size of a single pipelined batch. + */ +#define MAX_BATCH_SIZE 4096 + +#define HOST_HDR "Host: " ZZUF_MHD_LISTEN_IP "\r\n" + + +/** + * The closure of the access handler callback. + */ +struct ahc_param +{ + /** + * Must have #TEST_MAGIC_MARKER value. + */ + unsigned int magic; + + /** + * Non-zero if any error has been encountered. + */ + unsigned int err_flag; + + /** + * The number of requests seen so far. + */ + unsigned int num_requests; +}; + + +/** + * The request templates that are concatenated into one pipelined batch. + * The last request of every batch is added by build_batch(). + */ +static const char *const req_templates[] = { + "GET /pipe/a HTTP/1.1\r\n" HOST_HDR "\r\n", + "GET /pipe/b?x=1&y HTTP/1.1\r\n" HOST_HDR "X-Pad: 0123456789\r\n\r\n", + "HEAD /pipe/c HTTP/1.1\r\n" HOST_HDR "\r\n", + "POST /pipe/d HTTP/1.1\r\n" HOST_HDR + "Content-Length: 5\r\n\r\nHELLO", + "PUT /pipe/e HTTP/1.1\r\n" HOST_HDR + "Transfer-Encoding: chunked\r\n\r\n5\r\nHELLO\r\n0\r\n\r\n", + /* An empty line before the request line: allowed to be skipped by MHD */ + "\r\nGET /pipe/f HTTP/1.1\r\n" HOST_HDR "\r\n", + /* A request with a body that is *not* announced: the following bytes + must not be swallowed as a body */ + "GET /pipe/g HTTP/1.1\r\n" HOST_HDR "\r\n" +}; + +#define NUM_REQ_TEMPLATES \ + (sizeof(req_templates) / sizeof(req_templates[0])) + + +static enum MHD_Result +ahc_pipelined (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **req_cls) +{ + static int marker; + struct ahc_param *param = (struct ahc_param *) cls; + struct MHD_Response *response; + enum MHD_Result ret; + + (void) url; (void) method; (void) version; (void) upload_data; + + if ((NULL == param) || (TEST_MAGIC_MARKER != param->magic)) + { + fprintf (stderr, "The 'cls' parameter is invalid " + "at line %d.\n", (int) __LINE__); + fflush (stderr); + abort (); + } + if ((NULL == url) || (NULL == method) || (NULL == version)) + { + fprintf (stderr, "One of the mandatory string parameters is NULL " + "at line %d.\n", (int) __LINE__); + param->err_flag = 1; + return MHD_NO; + } + if (NULL == *req_cls) + { + *req_cls = &marker; + return MHD_YES; + } + if ((NULL != upload_data_size) && (0 != *upload_data_size)) + { + *upload_data_size = 0; /* Discard the body */ + return MHD_YES; + } + param->num_requests++; + + response = + MHD_create_response_from_buffer_static (MHD_STATICSTR_LEN_ (EMPTY_PAGE), + EMPTY_PAGE); + if (NULL == response) + { + fprintf (stderr, "MHD_create_response_from_buffer_static() failed " + "at line %d.\n", (int) __LINE__); + return MHD_NO; /* External error, do not raise the error flag */ + } + ret = MHD_YES; + if (zzuf_use_close) + ret = MHD_add_response_header (response, + MHD_HTTP_HEADER_CONNECTION, + "close"); + if (MHD_YES == ret) + ret = MHD_queue_response (connection, MHD_HTTP_OK, response); + MHD_destroy_response (response); + return ret; +} + + +/** + * Build one pipelined batch of requests. + * + * @param iteration the number of the current iteration + * @param[out] buf the buffer to fill + * @param buf_size the size of @a buf + * @return the number of bytes written to @a buf + */ +static size_t +build_batch (unsigned int iteration, char *buf, size_t buf_size) +{ + static const char last_req[] = + "GET /pipe/last HTTP/1.1\r\n" HOST_HDR "Connection: close\r\n\r\n"; + size_t pos = 0; + unsigned int i; + unsigned int num; + + num = 2 + (iteration % 4); /* 2 to 5 requests per batch */ + for (i = 0; i < num; ++i) + { + const char *req = + req_templates[(iteration + i) % NUM_REQ_TEMPLATES]; + const size_t len = strlen (req); + + if (pos + len + sizeof(last_req) > buf_size) + break; + memcpy (buf + pos, req, len); + pos += len; + } + if (pos + MHD_STATICSTR_LEN_ (last_req) <= buf_size) + { + memcpy (buf + pos, last_req, MHD_STATICSTR_LEN_ (last_req)); + pos += MHD_STATICSTR_LEN_ (last_req); + } + return pos; +} + + +static unsigned int +client_run (struct MHD_Daemon *d_extern, + uint16_t port, + void *cls) +{ + struct ahc_param *param = (struct ahc_param *) cls; + unsigned int i; + unsigned int loops; + char batch[MAX_BATCH_SIZE]; + + loops = zzuf_loop_count (); + for (i = 0; i < loops; ++i) + { + struct zzuf_raw_part parts[2]; + size_t batch_len; + size_t num_parts; + + fprintf (stderr, "."); + batch_len = build_batch (i, batch, sizeof(batch)); + parts[0].data = batch; + parts[0].size = batch_len; + num_parts = 1; + if (0 != i % 2) + { + /* Every other iteration: split the batch in the middle of a request, + so that MHD has to keep a partial request in the read buffer while + the previous ones are still being answered. */ + parts[0].size = batch_len / 2; + parts[1].data = batch + parts[0].size; + parts[1].size = batch_len - parts[0].size; + num_parts = 2; + } + if (ZZUF_RAW_SETUP_FAILED == + zzuf_raw_exchange (d_extern, port, parts, num_parts, + (unsigned int) ZZUF_CLIENT_TIMEOUT)) + { + fprintf (stderr, "The raw client could not be set up " + "at line %d.\n", (int) __LINE__); + return 99; /* Not an MHD error */ + } + fflush (stderr); + } + + if (0 != param->err_flag) + { + fprintf (stderr, "One or more errors have been detected by the access " + "handler callback function. At line %d.\n", (int) __LINE__); + return 1; + } + return 0; +} + + +int +main (int argc, char *const *argv) +{ + struct ahc_param param; + struct zzuf_run_params rp; + unsigned int res; + int use_magic_exit_codes; + + zzuf_parse_common_args (argc, argv); + use_magic_exit_codes = zzuf_run_with_socat || zzuf_dry_run; + + res = zzuf_check_runnable (); + if (0 != res) + return use_magic_exit_codes ? (int) res : 0; + + param.magic = (unsigned int) TEST_MAGIC_MARKER; + param.err_flag = 0; + param.num_requests = 0; + + memset (&rp, 0, sizeof(rp)); + rp.port = zzuf_pick_port (TEST_PORT_OFFSET); + rp.ahc = &ahc_pipelined; + rp.ahc_cls = &param; + rp.rcc = NULL; + rp.rcc_cls = NULL; + rp.mem_limit_override = 0; + rp.sweep_profiles = 1; + rp.profile_start = 0; + rp.client = &client_run; + rp.client_cls = &param; + + res = zzuf_run_polling_modes (&rp); + + if (99 == res) + return use_magic_exit_codes ? 99 : 0; + if (77 == res) + return use_magic_exit_codes ? 77 : 0; + return (0 == res) ? 0 : 1; /* 0 == pass */ +} diff --git a/src/testzzuf/zzuf_socat_test_runner.sh b/src/testzzuf/zzuf_socat_test_runner.sh @@ -1,4 +1,20 @@ #!/bin/sh +# +# Run one fuzzing test with zzuf fuzzing a socat relay that sits between +# the test clients and MHD. This mode is mandatory when MHD is built with +# sanitizers (zzuf and the sanitizers cannot both intercept the standard +# library calls) or when MHD uses accept4(). +# +# Recognised environment variables: +# ZZUF the zzuf command (set by configure) +# ZZUF_SEED the single seed to use (default "0") +# ZZUF_SEED_START the first seed of a seed range (overrides ZZUF_SEED) +# ZZUF_SEED_STOP the last seed of a seed range +# ZZUF_FLAGS additional flags for zzuf +# SOCAT the socat command (set by configure) +# SOCAT_FLAGS additional flags for socat +# +# This script is POSIX shell only, do not use any bashism. if set -m ; then : ; else echo "The shell $SHELL does not support background jobs, the test cannot run." 1>&2 @@ -30,11 +46,58 @@ if command -v "${SOCAT}" > /dev/null 2>&1 ; then : ; else exit 77 fi +# Build the "--seed" parameter for zzuf. +zzuf_seed_param='' +if test -n "${ZZUF_SEED_START}" ; then + if test -n "${ZZUF_SEED_STOP}" ; then + zzuf_seed_param="--seed=${ZZUF_SEED_START}:${ZZUF_SEED_STOP}" + seed_descr="range ${ZZUF_SEED_START}:${ZZUF_SEED_STOP}" + else + zzuf_seed_param="--seed=${ZZUF_SEED_START}" + seed_descr="${ZZUF_SEED_START}" + fi +elif test -n "${ZZUF_SEED}" ; then + zzuf_seed_param="--seed=${ZZUF_SEED}" + seed_descr="${ZZUF_SEED}" +else + seed_descr="(zzuf default)" +fi + +test_descr="$*" +test_name=`echo "$1" | sed 's|^.*/||'` + +# Print a loud, easy to grep report about a failing run. +# In this mode zzuf fuzzes socat, while the test program (and MHD) runs +# outside of zzuf, so the exit code below is the exit code (or the signal) +# of the *server side* and the seed is the one this run was started with. +# $1: the exit code of the test program +report_failure () +{ + echo '' 1>&2 + echo '###############################################################' 1>&2 + echo '## FUZZING TEST FAILED (server side)' 1>&2 + echo "## test : ${test_descr}" 1>&2 + echo "## exit code : $1" 1>&2 + if test "$1" -gt 128 2> /dev/null ; then + echo "## (killed by signal `expr $1 - 128`)" 1>&2 + fi + echo "## FAILING SEED: ${seed_descr}" 1>&2 + echo '## Replay with:' 1>&2 + if test -n "${ZZUF_SEED_START}" ; then + echo "## make ZZUF_SEED_START=${ZZUF_SEED_START} \ +ZZUF_SEED_STOP=${ZZUF_SEED_STOP} TESTS=${test_name} check" 1>&2 + else + echo "## make ZZUF_SEED=${ZZUF_SEED} TESTS=${test_name} check" 1>&2 + fi + echo '###############################################################' 1>&2 + echo '' 1>&2 +} + socat_test_params="-ls -lu \ -T0.1 -4 \ TCP-LISTEN:${socat_listen_port},bind=${socat_listen_ip},reuseaddr,linger=2,linger2=1,accept-timeout=0.1 \ TCP:127.0.0.1:${mhd_listen_port},reuseaddr" - + echo "## Trying to run socat to test ports availability..." if "${SOCAT}" ${socat_test_params} ; then echo "Success." @@ -43,13 +106,14 @@ else exit 77 fi -# fuzz the input only for IP ${socat_listen_ip}. libcurl uses another IP -# in this test therefore libcurl input is not fuzzed. +# fuzz the input only for IP ${socat_listen_ip}. libcurl and the raw client +# use another IP in these tests, therefore the input of the clients is not +# fuzzed. zzuf_all_params="--ratio=0.001:0.4 --autoinc --verbose --signal \ --max-usertime=${max_runtime_sec} --check-exit --network --allow=${socat_listen_ip} --exclude=." -if test -n "${ZZUF_SEED}" ; then - zzuf_all_params="${zzuf_all_params} --seed=${ZZUF_SEED}" +if test -n "${zzuf_seed_param}" ; then + zzuf_all_params="${zzuf_all_params} ${zzuf_seed_param}" fi if test -n "${ZZUF_FLAGS}" ; then @@ -63,7 +127,7 @@ socat_options="-ls -lu \ -T3 -4" socat_addr1="TCP-LISTEN:${socat_listen_port},bind=${socat_listen_ip},reuseaddr,nodelay,linger=2,linger2=1,accept-timeout=${max_runtime_sec},fork" socat_addr2="TCP:127.0.0.1:${mhd_listen_port},reuseaddr,connect-timeout=3,nodelay,linger=2,linger2=1" - + if test -n "${SOCAT_FLAGS}" ; then socat_options="${socat_options} ${SOCAT_FLAGS}" fi @@ -73,7 +137,7 @@ fi # Uncomment the next line to see all traffic in logs #socat_options="${socat_options} -v" - + stop_zzuf_socat () { trap - EXIT @@ -88,7 +152,7 @@ stop_zzuf_socat () fi } -echo "## Starting zzuf with socat to reflect fuzzed traffic..." +echo "## Starting zzuf with socat to reflect fuzzed traffic (seed: ${seed_descr})..." trap 'stop_zzuf_socat' EXIT zzuf_pid="starting" "${ZZUF}" ${zzuf_all_params} "${SOCAT}" ${socat_options} ${socat_addr1} ${socat_addr2} & @@ -101,18 +165,26 @@ else exit 99 fi -echo "## Starting real test of $@ with traffic fuzzed by zzuf with socat..." +echo "## Starting real test of $* with traffic fuzzed by zzuf with socat..." "$@" --with-socat test_result=$? trap - EXIT -echo "$@ has exited with the return code $test_result" +echo "$* has exited with the return code $test_result" if kill -s 0 -- $$ 2> /dev/null ; then - if kill -s 0 -- ${zzuf_pid} -${zzuf_pid} ; then : ; else + if kill -s 0 -- ${zzuf_pid} -${zzuf_pid} 2> /dev/null ; then : ; else echo "No running zzuf with socat is detected after the test." 1>&2 echo "Looks like zzuf ended prematurely, at least part of the testing has not been performed." 1>&2 - test_result=99 + if test "x${test_result}" = "x0" ; then + test_result=99 + else + echo "The test itself has already failed with the code ${test_result}," 1>&2 + echo "this code is kept as the result of this run." 1>&2 + fi fi fi -kill -TERM ${zzuf_pid} -${zzuf_pid} +kill -TERM ${zzuf_pid} -${zzuf_pid} 2> /dev/null zzuf_pid='' +if test "x${test_result}" != "x0" && test "x${test_result}" != "x77" ; then + report_failure "${test_result}" +fi exit $test_result diff --git a/src/testzzuf/zzuf_test_runner.sh b/src/testzzuf/zzuf_test_runner.sh @@ -1,4 +1,18 @@ #!/bin/sh +# +# Run one fuzzing test with zzuf intercepting the network input of the +# test process itself. +# +# Recognised environment variables: +# ZZUF the zzuf command (set by configure) +# ZZUF_SEED the single seed to use (default "0") +# ZZUF_SEED_START the first seed of a seed range (overrides ZZUF_SEED) +# ZZUF_SEED_STOP the last seed of a seed range +# ZZUF_FLAGS additional flags for zzuf +# SOCAT the socat command (set by configure) +# SOCAT_FLAGS additional flags for socat +# +# This script is POSIX shell only, do not use any bashism. mhd_listen_ip='127.0.0.1' max_runtime_sec='1800' @@ -13,6 +27,75 @@ if command -v "${ZZUF}" > /dev/null 2>&1 ; then : ; else exit 77 fi +# Build the "--seed" parameter for zzuf. +# A seed range makes zzuf start one child per seed, which is what CI uses +# to sweep a large number of seeds. +zzuf_seed_param='' +if test -n "${ZZUF_SEED_START}" ; then + if test -n "${ZZUF_SEED_STOP}" ; then + zzuf_seed_param="--seed=${ZZUF_SEED_START}:${ZZUF_SEED_STOP}" + seed_descr="range ${ZZUF_SEED_START}:${ZZUF_SEED_STOP}" + else + zzuf_seed_param="--seed=${ZZUF_SEED_START}" + seed_descr="${ZZUF_SEED_START}" + fi +elif test -n "${ZZUF_SEED}" ; then + zzuf_seed_param="--seed=${ZZUF_SEED}" + seed_descr="${ZZUF_SEED}" +else + seed_descr="(zzuf default)" +fi + +test_log='' + +cleanup_log () +{ + if test -n "${test_log}" ; then + rm -f "${test_log}" + test_log='' + fi +} + +# Print a loud, easy to grep report about a failing run, including the +# seed(s) that produced the failure. +# $1: the exit code of zzuf +report_failure () +{ + failed_seeds='' + if test -n "${test_log}" && test -r "${test_log}" ; then + # zzuf prints one line per child, e.g. + # zzuf[s=17,r=0.012345]: signal 6 (SIGABRT) + failed_seeds=`grep -E '^zzuf\[s=[0-9]+' "${test_log}" 2> /dev/null | \ + grep -E 'signal|exit|crash|abort|too (slow|much|many)' 2> /dev/null | \ + sed -e 's/^zzuf\[s=//' -e 's/[^0-9].*$//' | \ + sort -n -u | tr '\n' ' '` + fi + echo '' 1>&2 + echo '###############################################################' 1>&2 + echo '## FUZZING TEST FAILED' 1>&2 + echo "## test : ${test_descr}" 1>&2 + echo "## exit code : $1" 1>&2 + echo "## seed(s) used: ${seed_descr}" 1>&2 + if test -n "${failed_seeds}" ; then + echo "## FAILING SEED(S): ${failed_seeds}" 1>&2 + echo '## Replay a single failing seed with:' 1>&2 + for one_seed in ${failed_seeds} ; do + echo "## make ZZUF_SEED=${one_seed} TESTS=${test_name} check" 1>&2 + done + else + echo '## No failing seed could be extracted from the zzuf output.' 1>&2 + echo '## Replay the whole run with:' 1>&2 + if test -n "${ZZUF_SEED_START}" ; then + echo "## make ZZUF_SEED_START=${ZZUF_SEED_START} \ +ZZUF_SEED_STOP=${ZZUF_SEED_STOP} TESTS=${test_name} check" 1>&2 + else + echo "## make ZZUF_SEED=${ZZUF_SEED} TESTS=${test_name} check" 1>&2 + fi + fi + echo '###############################################################' 1>&2 + echo '' 1>&2 +} + run_with_socat () { echo "Trying to run the test with socat..." @@ -43,13 +126,18 @@ run_with_socat () echo "Cannot determine script location, will try current directory." 1>&2 script_dir="./" fi + cleanup_log $SHELL "${script_dir}zzuf_socat_test_runner.sh" "$@" exit $? } +test_descr="$*" +test_name=`echo "$1" | sed 's|^.*/||'` +trap 'cleanup_log' EXIT + # zzuf cannot pass-through the return value of checked program # so try the direct dry-run first to get possible 77 or 99 codes -echo "## Dry-run of the $@..." +echo "## Dry-run of the $* ..." if "$@" --dry-run ; then echo "# Dry-run succeeded." else @@ -58,18 +146,19 @@ else if test $res_code -ne 99; then run_with_socat "$@" fi - echo "$@ will not be run with zzuf." 1>&2 + echo "$* will not be run with zzuf." 1>&2 exit $res_code fi -# fuzz the input only for IP ${mhd_listen_ip}. libcurl uses another IP -# in this test therefore libcurl input is not fuzzed. +# fuzz the input only for IP ${mhd_listen_ip}. libcurl and the raw client +# use another IP in these tests, therefore the input of the clients is not +# fuzzed. zzuf_all_params="--ratio=0.001:0.4 --autoinc --verbose --signal \ --max-usertime=${max_runtime_sec} --check-exit --network \ --allow=${mhd_listen_ip} --exclude=." -if test -n "${ZZUF_SEED}" ; then - zzuf_all_params="${zzuf_all_params} --seed=${ZZUF_SEED}" +if test -n "${zzuf_seed_param}" ; then + zzuf_all_params="${zzuf_all_params} ${zzuf_seed_param}" fi if test -n "${ZZUF_FLAGS}" ; then @@ -79,16 +168,37 @@ fi # Uncomment the next line to see more data in logs #zzuf_all_params="${zzuf_all_params} -dd" -echo "## Dry-run of the $@ with zzuf..." +echo "## Dry-run of the $* with zzuf..." if "$ZZUF" ${zzuf_all_params} "$@" --dry-run ; then echo "# Dry-run with zzuf succeeded." else res_code=$? - echo "$@ cannot be run with zzuf directly." 1>&2 + echo "$* cannot be run with zzuf directly." 1>&2 run_with_socat "$@" exit $res_code fi -echo "## Real test of $@ with zzuf..." -"$ZZUF" ${zzuf_all_params} "$@" -exit $? +echo "## Real test of $* with zzuf (seed: ${seed_descr})..." +if command -v tee > /dev/null 2>&1 ; then + test_log="${TMPDIR:-/tmp}/mhd_zzuf_$$.log" + rc_file="${test_log}.rc" + { "$ZZUF" ${zzuf_all_params} "$@" ; echo $? > "${rc_file}" ; } 2>&1 | \ + tee "${test_log}" + if test -r "${rc_file}" ; then + res_code=`cat "${rc_file}"` + else + res_code=1 + fi + rm -f "${rc_file}" +else + # No 'tee': run without capturing, the failing seed has to be taken + # from the test log by hand. + "$ZZUF" ${zzuf_all_params} "$@" + res_code=$? +fi + +if test "x${res_code}" != "x0" ; then + report_failure "${res_code}" +fi +cleanup_log +exit ${res_code}