expand fuzzer coverage

This commit is contained in:
Christian Grothoff
2026-07-28 16:56:42 +02:00
parent 1b7b319d04
commit d0270f583a
70 changed files with 2055 additions and 110 deletions
+9 -3
View File
@@ -23,7 +23,13 @@ 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.
# Replay the whole committed seed corpus through every harness. This is
# the part that proves a fixed crash stays fixed, and it is cheap.
#
# corpus/known-findings/ is deliberately NOT included: check-corpus does
# not recurse, and the reproducers of the findings that are still open
# (src/fuzz/README section 6, patches/) are expected to fail on a build
# with --enable-asserts. Replay those by hand when working on a fix:
# for f in src/fuzz/corpus/known-findings/*.bin ; do
# ./src/fuzz/fuzz_request --file="$f" ; done
make -C src/fuzz check-corpus
+20 -7
View File
@@ -69,7 +69,12 @@ the corpus packager live here and not in the OSS-Fuzz repository.
fuzz_request a real struct MHD_Daemon driven over a
socketpair; consumes daemon options *and*
stream segmentation from the input
stream segmentation from the input. Bytes
4-9 (see src/fuzz/README section 2.2.1)
additionally select the response
constructor, the authentication entry point,
the event-loop API, and whether the
connection is suspended or upgraded
fuzz_str the mhd_str.c primitives with exactly-sized
output buffers
fuzz_auth_header MHD_get_rq_dauth_params_() /
@@ -93,8 +98,8 @@ range (-3 and 0), which is what a fuzzing service should explore.
`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,
fuzz_request 8192 10 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
@@ -137,7 +142,7 @@ actionable.
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 remote aborts; findings K1-K7 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
@@ -328,13 +333,21 @@ 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
`src/fuzz/corpus/known-findings/K*.bin` are byte-exact reproducers for
the 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.
Reproducers of findings that are still *open* are skipped. A finding is
open exactly when `patches/$ID.diff` exists (see `patches/README`), and
its reproducer crashes the target by construction, so shipping it would
make every ClusterFuzz run open by rediscovering a bug that is already
written down. Committing the fix means deleting the diff, and that
alone promotes the reproducer to a shipped regression seed. At the time
of writing this skips K7.
Regenerating the corpus from the harnesses' built-in seeds:
make -C src/fuzz refresh-corpus # ./fuzz_<name> --write-corpus=corpus
+1 -1
View File
@@ -188,7 +188,7 @@ fi
# --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.
# findings K1-K7 (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
+13
View File
@@ -141,3 +141,16 @@ kv="a=b"
kv_amp="&"
kv_plus="+"
kv_noeq="a"
# HTTP "Upgrade" -- the extended configuration (byte 7 bit 0x04) only
# answers 101 for a request that really asks to upgrade, so without
# these tokens MHD_create_response_for_upgrade(), the upgrade handler
# and MHD_upgrade_action() are statistically unreachable.
upgrade_hdr="Upgrade: "
upgrade_conn="Connection: Upgrade\x0d\x0a"
upgrade_conn_lc="connection: upgrade\x0d\x0a"
upgrade_websocket="Upgrade: websocket\x0d\x0a"
upgrade_h2c="Upgrade: h2c\x0d\x0a"
upgrade_proto="fuzz-protocol"
upgrade_both="Connection: keep-alive, Upgrade\x0d\x0a"
conn_close="Connection: close\x0d\x0a"
+22 -4
View File
@@ -16,12 +16,14 @@
# 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
# known-findings/K*.bin byte-exact reproducers for the findings in
# 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.
# permanent regression tests. Reproducers of
# findings that are still open are skipped; see
# the loop below.
# README documentation, not an input; excluded.
#
# The corpus itself is regenerated from the harnesses' built-in seeds with
@@ -35,6 +37,7 @@ OUTDIR="${2:-${OUT:-$(pwd)/out}}"
CORPUS="${SRCDIR}/src/fuzz/corpus"
FINDINGS="${CORPUS}/known-findings"
PATCHES="${SRCDIR}/patches"
FUZZERS="fuzz_request fuzz_str fuzz_auth_header fuzz_postprocessor"
@@ -59,10 +62,25 @@ for fuzzer in ${FUZZERS}; do
n=$((n + 1))
done
# The K1-K6 reproducers are fuzz_request inputs.
# The K* reproducers are fuzz_request inputs.
#
# Only the ones whose defect is already fixed are shipped. A finding
# that is still open has an unapplied fix in patches/$ID.diff (see
# patches/README), and its reproducer, by construction, crashes the
# target: shipping it would make every ClusterFuzz run start by
# rediscovering a bug that is already written down, and bury the
# findings that are actually new. Deleting the diff -- which is what
# committing the fix should do -- promotes the reproducer to a
# permanent regression seed here, with no further edit.
if [ "${fuzzer}" = "fuzz_request" ] && [ -d "${FINDINGS}" ]; then
for f in "${FINDINGS}"/*.bin; do
[ -f "${f}" ] || continue
id="$(basename "${f}" | sed -n 's/^\(K[0-9]*\).*/\1/p')"
if [ -n "${id}" ] && [ -f "${PATCHES}/${id}.diff" ]; then
echo " skipping ${fuzzer} seed $(basename "${f}"): ${id} is open" \
"(${PATCHES}/${id}.diff)"
continue
fi
cp "${f}" "${dir}/known-finding-$(basename "${f}")"
n=$((n + 1))
done
+3 -1
View File
@@ -190,7 +190,9 @@ make -C src/fuzz check-corpus
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.
`corpus/known-findings/` as regressions. K7 is open, and its
reproducer lives there too — `check-corpus` does not recurse into that
directory, which is what keeps `make check` green while it is.
---
+208 -5
View File
@@ -78,8 +78,11 @@ MHD_get_connection_info() see something sane.
0x04 run the request body through MHD_post_process()
0x08 iterate MHD_get_connection_values() over headers,
GET arguments, cookies and footers
0x10 unused (see 2.2.1)
0x20 reply with a larger, copied response body
(only meaningful for response kind 0)
0x40 reply 403 instead of 200
0x80 unused
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
@@ -87,7 +90,8 @@ MHD_get_connection_info() see something sane.
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
byte 4-9 the API-selection block, see 2.2.1
byte 10. a sequence of *send segments*. Each segment starts with a
little-endian 16 bit header:
(op << 14) | length length <= 0x3FFF
@@ -105,6 +109,112 @@ 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.
An input shorter than ten bytes is rejected: the configuration block is
mandatory, and an input that short has no segment stream either.
2.2.1 The API-selection block
-----------------------------
Bytes 4-9 pick which parts of the public API the iteration touches.
They are always present. Bytes 0-3 alone decide how MHD *parses* the
request; these six decide which of the response constructors, which
authentication entry point, which event loop, and which introspection
calls run against the parsed result.
The all-zero setting is the plainest one -- a static two byte buffer
response, MHD_run() as the event loop, no suspend, no upgrade, no extra
introspection -- so zeroing bytes 4-9 of any input reduces it to the
request-parsing-only behaviour that the harness had before these bytes
existed.
These bytes were gated behind bit 0x10 of byte 1 while they were being
brought up, so that the corpus predating them kept its byte-exact
meaning. The gate is gone; bit 0x10 is left unused rather than
reassigned, so that a corpus file written while it was a gate cannot
silently change meaning. The reproducers in known-findings/ that predate
the change (K1-K6, seven files) were migrated by inserting six zero bytes
at offset 4 -- the identity transformation, since the all-zero block is
the old behaviour -- and all seven still drive MHD along their recorded
paths, verified by comparing the --verbose daemon, handler, body and
challenge counts before and after.
byte 4 response construction
bits 0-3 which constructor to use:
0 buffer_static 1 buffer_copy 2 empty
3 buffer/PERSISTENT 4 buffer/MUST_FREE 5 buffer/MUST_COPY
6 buffer_with_free_callback 7 data (copy)
8 data (free) 9 callback, known length
10 callback, MHD_SIZE_UNKNOWN (chunked reply)
11 fd 12 fd_at_offset 13 fd64
14 pipe 15 iovec
bits 4-5 number of response headers to add (0-3)
bit 6 also add a response footer (a chunked trailer)
bit 7 exercise MHD_get_response_header(),
MHD_get_response_headers(),
MHD_del_response_header() and
MHD_set_response_options()
byte 5 authentication entry point
bits 0-3 0 check3 (as before) 1 check
2 check2 3 check_digest
4 check_digest2 5 check_digest3
6 get_username 7 get_username3
8 get_request_info3 9 as 0
bits 4-5 challenge variant: 0 queue_auth_required_response3,
1 queue_auth_fail_response,
2 queue_auth_fail_response2,
3 the basic-auth pair
bit 6 use the v1 MHD_basic_auth_get_username_password()
bit 7 also call the connection-less digest helpers
byte 6 event loop and introspection
bits 0-1 0 MHD_run(), 1 MHD_get_fdset()+run_from_select(),
2 and 3 the *2 variants
bit 2 query all four MHD_get_timeout*() forms
bit 3 MHD_lookup_connection_value(),
MHD_lookup_connection_value_n(),
MHD_get_connection_URI_path_n()
bit 4 MHD_set_connection_value()
bit 5 MHD_get_connection_info() / MHD_get_daemon_info()
bit 6 MHD_set_connection_option()
bit 7 MHD_quiesce_daemon() before stopping
byte 7 suspend and upgrade
bits 0-1 0 none, 1 suspend+resume in the handler,
2 suspend and resume from the pump loop,
3 as 1
bit 2 allow HTTP "Upgrade" (only acted on when the
request really asks for one, see below)
bits 3-4 which MHD_upgrade_action() to issue first
byte 8 seed picking the response header names and values, and the
MHD_RF_* flags for MHD_set_response_options()
byte 9 seed for the content-reader callback: body length, block
size, and whether it fails part way through
Two constraints on the harness are worth stating, because both are
application-contract requirements rather than things worth fuzzing, and
violating either makes MHD abort on input that is perfectly legal:
* the connection is only suspended when the handler is about to return
MHD_YES. Returning MHD_NO asks MHD to terminate the connection, and
terminating one that the same callback just suspended trips
mhd_assert (! connection->suspended) in MHD_connection_close_();
* a 101 response is only queued for a request that is actually an
upgrade request (HTTP/1.1, an "Upgrade" header, and a "Connection"
header naming the upgrade token and not "close"). All three come
off the wire, so the path stays attacker-driven.
Unlike the suspend rule, this one is not sufficient, and is not
supposed to be: it is exactly the check a real application can
perform, and a request that passes it can still abort MHD. That is
finding K7 in section 6. Do not tighten it further to keep the
suite green -- the point is that an application cannot do better.
The message-framing headers (Content-Length, Transfer-Encoding) are
deliberately absent from the response-header table: MHD generates them
itself from the response object, so an application that also sets them
by hand is lying to the library about its own body.
MHD_RF_INSANITY_HEADER_CONTENT_LENGTH is the sanctioned way to explore
that corner and is reachable through bit 7 of byte 4.
2.3 The %%NONCE%% placeholder
-----------------------------
@@ -416,14 +526,106 @@ K6 digestauth.c:860 check_nonce_nc():
The generator needs ~150k iterations to hit it on its own.
Repro: corpus/known-findings/K6-nonce-length-collision.bin
K7 connection.c:2596 build_header_response():
mhd_assert ((NULL == r->upgrade_handler) ||
(MHD_CONN_MUST_UPGRADE == c->keepalive))
Open. Driven entirely from the wire, with default daemon options.
While parsing the request headers, connection.c sets
c->keepalive = MHD_CONN_MUST_CLOSE
in three places for requests whose message framing it distrusts:
4988 two "Content-Length" headers with different values
(client discipline -3 only)
5012 "Transfer-Encoding" on an HTTP/1.0 request
(client discipline <= 0)
5051 "Content-Length" *and* "Transfer-Encoding: chunked" in the
same request -- no discipline gate at all, so this one
fires with the default configuration
In every case the request is then handed to the access handler as a
normal request. If the handler answers 101 with a response from
MHD_create_response_for_upgrade(), MHD_queue_response() accepts it:
it checks MHD_ALLOW_UPGRADE, the status code, the "Connection"
header and the HTTP version, but not whether the connection can
still be kept open. keepalive_possible() then returns
MHD_CONN_MUST_CLOSE from its very first test, which is placed
*before* the upgrade branch, and the assertion above fires while the
reply header is being built.
The application cannot defend itself. The harness checks the
request the way an application would -- HTTP/1.1, an "Upgrade"
header, and a "Connection" header naming the upgrade token and not
"close" -- and the reproducer satisfies all three. MHD's decision
is not exposed through any public accessor.
It needs --enable-asserts and an application that answers upgrade
requests, so it is a robustness defect rather than a memory-safety
one; but it is reachable from a single well-formed request against a
default-configured server, which is a good deal worse than the
"application misuse" it first looked like.
Fix: patches/K7.diff, which rejects the response in
MHD_queue_response() next to the other upgrade preconditions, so the
application gets MHD_NO instead of an abort.
Repro: corpus/known-findings/K7-upgrade-after-must-close.bin
GET / HTTP/1.1
Host: x
Connection: Upgrade
Upgrade: fuzz-protocol
Content-Length: 0
Transfer-Encoding: chunked
(then "0\r\n\r\n")
K8 digestauth.c MHD_digest_auth_check_digest2():
MHD_PANIC ("Wrong 'malgo3' value, only one base hashing
algorithm ... must be specified, API violation")
Open. An API-contract defect rather than a parsing bug: unlike K7
the offending argument comes from the application, not the network.
MHD_DIGEST_ALG_AUTO is a documented member of
enum MHD_DigestAuthAlgorithm, and the `algo` parameter of
MHD_digest_auth_check_digest2() is documented only as "digest
algorithms allowed for verification". But the function maps AUTO to
MHD_DIGEST_AUTH_MULT_ALGO3_ANY_NON_SESSION and forwards it to
MHD_digest_auth_check_digest3(), which requires *exactly one* base
hashing algorithm because the caller supplies a pre-computed digest
of one specific length -- and aborts through MHD_PANIC() when more
than one is named.
So MHD_digest_auth_check_digest2 (..., MHD_DIGEST_ALG_AUTO)
deterministically kills the process. The same is not true of
MHD_digest_auth_check2(), which takes a password rather than a
digest and whose AUTO handling is fine.
Unlike K7 this is entirely under the application's control -- no
network input is involved -- so it is a usability trap rather than a
robustness bug: AUTO simply cannot be used with the
pre-computed-digest entry points, and no mapping can rescue it,
because the digest length does not identify the algorithm (SHA-256
and SHA-512/256 digests are both 32 bytes).
Fix: patches/K8.diff, which documents the restriction in
microhttpd.h and returns MHD_NO instead of aborting.
Repro: none committed. The argument is the application's, so the
harness never passes AUTO there (see run_digest_check() in
fuzz_request.c); to see it, change the MHD_DIGEST_ALG_* argument of
the variant-4 call to MHD_DIGEST_ALG_AUTO and replay
corpus/fuzz_request-36.bin.
Status
------
K1-K5 are fixed on master by commits 300a2ab0, 68c83f22, e04eb218,
0b750975 and 6fcdfd43 respectively; K6 by f438804c. All seven
0b750975 and 6fcdfd43 respectively; K6 by f438804c. Their seven
reproducers in `corpus/known-findings/` therefore replay clean, and
`make check-corpus` asserts exactly that.
K7 and K8 are open, and their fixes are unapplied diffs in
`../../patches/`. K7 has a reproducer, which aborts on an
`--enable-asserts` build and passes with patches/K7.diff applied; K8 has
none, because its trigger is an argument the application chooses.
The `make check` defaults in Makefile.am
MHD_FUZZ_ITERATIONS = 50000
@@ -494,9 +696,10 @@ 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.
`corpus/` — including the `corpus/known-findings/` reproducers of the
findings that are already *fixed*, so that they become permanent
regressions — into the `<fuzzer>_seed_corpus.zip` files OSS-Fuzz
expects.
`build.sh` configures out of tree with
+90 -29
View File
@@ -20,35 +20,85 @@ 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.
The set is kept minimal: every seed here adds coverage that no other
seed provides. After changing a seed table, re-check that with
<target> -merge=1 <empty dir> <this directory>
from an OSS-Fuzz style build (contrib/oss-fuzz/build.sh); a seed the
merge does not copy across is redundant and should be deleted from the
table rather than left in the corpus. Note that fuzz_request is
mildly non-deterministic -- MHD timestamps its nonces and its
connection timeouts -- so a single merge decision worth ~0.1% of the
regions is noise; only drop a seed that several independent merges
agree on.
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
00 content-length-body body oracle, Content-Length
01 chunked-with-extensions body oracle, chunk extensions
-> chunk-extension CRLF bug
03 chunked-split body oracle, chunk boundary split
02 chunked-split body oracle, chunk boundary split
across two send() calls
04 small-pool-trailing-query-arg 128..512 byte connection pool, no
03 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
04 small-pool-trailing-query-arg-2 same with "?a=1&b"
05 digest-unknown-algorithm algorithm=BOGUS
-> MHD_DIGEST_AUTH_ALGO3_INVALID
MHD_PANIC()
07 digest-overlong-response 401 challenge, then a replay on a
06 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
07 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
08 basic-auth Authorization: Basic
09 multipart-post chunked multipart/form-data
10 urlencoded-post application/x-www-form-urlencoded
11 folded-header obs-fold continuation line
12 pipelined two requests in one segment
Seeds 00-12 leave the API-selection block (bytes 4-9, see section 2.2.1
of ../README) zero and only drive the request parser. Seeds 13-39 each
switch on one area of it; without them those entry points are reachable
only by guessing six configuration bytes.
13 ext-callback-chunked MHD_SIZE_UNKNOWN content reader,
response headers and a trailer
14 ext-callback-error known-length reader that fails
part way through
15 ext-fd-response MHD_create_response_from_fd()
16 ext-fd-at-offset-response ..._from_fd_at_offset()
17 ext-pipe-response ..._from_pipe()
18 ext-iovec-response ..._from_iovec()
19 ext-empty-response-hdrapi ..._empty() plus get/del header
and MHD_set_response_options()
20 ext-digest-check-digest3 pre-computed userdigest path
21 ext-digest-v1-wrappers MHD_digest_auth_check()
22 ext-digest-request-info MHD_digest_auth_get_request_info3()
23 ext-external-event-loop MHD_get_fdset2()/run_from_select2()
plus the timeout accessors
24 ext-external-event-loop-v1 the v1 fdset/run_from_select pair
25 ext-connection-api every introspection accessor
26 ext-suspend-resume suspend, resume from the pump loop
27 ext-suspend-two-connections two connections parked at once
28 ext-upgrade 101 + MHD_upgrade_action()
29 ext-buffer-persistent buffer/MHD_RESPMEM_PERSISTENT
30 ext-buffer-free-callback ..._with_free_callback()
31 ext-from-data MHD_create_response_from_data()
32 ext-fd64-response ..._from_fd64()
33 ext-basic-auth-v1 MHD_basic_auth_get_username_password()
34 ext-basic-auth-challenge MHD_queue_basic_auth_fail_response()
35 ext-basic-auth-challenge-utf8 ..._response3() with UTF-8 charset
36 ext-digest-check-digest-v1 MHD_digest_auth_check_digest()
37 ext-digest-check-digest2 ..._digest2() (never with
MHD_DIGEST_ALG_AUTO, see K8)
38 ext-digest-get-username-v1 MHD_digest_auth_get_username()
39 ext-digest-get-username3 ..._username3()
fuzz_str seeds
--------------
@@ -64,7 +114,10 @@ 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.
parameter list, over-long values) plus Basic `token68` variants, and
-- with bit 0x02 of byte 0 -- the connection-less digest helpers at
several algorithms and several deliberately undersized output
buffers.
fuzz_postprocessor seeds
------------------------
@@ -78,34 +131,42 @@ fuzz_postprocessor seeds
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
Reproducers for the issues that this suite found in MHD itself; see
section 6 of ../README, which records the status of each. K1-K6 are
fixed on master and therefore replay clean; K7 is open and *aborts* on a
build configured with --enable-asserts.
They are kept out of the main corpus directory for exactly that reason:
`check-corpus` does not recurse, so it stays green while an open finding
still has a live reproducer here. Replay one with
./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
K7-upgrade-after-must-close.bin connection.c build_header_response()
mhd_assert (upgrade -> MUST_UPGRADE)
*** still open, see ../../../patches ***
Once a patch has landed, move the corresponding file into the main
corpus directory so that `check-corpus` keeps it as a regression seed.
These files are *not* regenerated by `--write-corpus`; they are edited by
hand. When the input format changes they have to be migrated, and the
migration has to be checked -- replay each one with `--verbose` before
and after and confirm the daemon, handler, body and challenge counts are
unchanged. The last such change made the API-selection block
unconditional, which is six zero bytes inserted at offset 4 for all
seven.
K8 has no reproducer here: its trigger is an argument the application
chooses, not network input, so the harness never performs it. See
section 6 of ../README for how to reach it by hand.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
Basic dXNlcjpwYXNz
Basic
+1 -1
View File
@@ -1 +1 @@
Basic
Basic ====
+1 -1
View File
@@ -1 +1 @@
Basic ====
Basic QQ==QQ==
+1 -1
View File
@@ -1 +1 @@
Basic QQ==QQ==
user:TestRealm:pass
+1
View File
@@ -0,0 +1 @@
"user:TestRealm:pass
+1
View File
@@ -0,0 +1 @@
Buser:TestRealm:pass
+1
View File
@@ -0,0 +1 @@
¢user:TestRealm:pass
+1
View File
@@ -0,0 +1 @@
::
+1
View File
@@ -0,0 +1 @@

+1
View File
@@ -0,0 +1 @@
Digest username="user", realm="TestRealm"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+127 -2
View File
@@ -61,6 +61,28 @@
static const size_t pool_sizes[] = { 256, 512, 1024, 4096, 32768 };
/**
* Exactly-sized, NUL terminated copy of @a len bytes of @a src.
*
* "Exactly sized" is the point: the allocation is @a len + 1 bytes and
* not one byte more, so ASAN's redzone sits immediately behind the
* terminator and any read past it is reported.
*/
static char *
fuzz_dup_n (const char *src,
size_t len)
{
char *r = (char *) malloc (len + 1);
if (NULL == r)
return NULL;
memcpy (r, src, len);
r[len] = '\0';
return r;
}
/**
* gen_auth.c logs through MHD_DLOG(), which dereferences the daemon of
* the connection, so a real (but idle) daemon is required. It is
@@ -296,6 +318,88 @@ LLVMFuzzerTestOneInput (const uint8_t *data,
}
#endif /* BAUTH_SUPPORT */
#ifdef DAUTH_SUPPORT
/* The connection-less digest helpers. They belong here rather than
in fuzz_request because they are pure functions of their string
arguments: this harness reaches roughly two orders of magnitude
more executions per second, and -- more importantly -- it can hand
them a username and realm taken straight from the fuzzer instead of
the fixed constants fuzz_request has to use.
Every output buffer is a heap allocation of *exactly* the size
declared to MHD, and that size is swept down to zero, so a helper
that writes its full digest into a buffer that is too small is
caught immediately by ASAN's redzone rather than silently
corrupting an adjacent object. That is precisely the shape of the
overflow fixed in commit 5a73c1ae. */
if (0 != (sel & 0x02))
{
/* Exactly one base hashing algorithm per entry. In particular
MHD_DIGEST_AUTH_ALGO3_INVALID must not appear: every one of these
helpers routes through digest_get_hash_size(), which asserts that
precisely one of MD5 / SHA-256 / SHA-512-256 is named. Passing
INVALID is an API violation on the caller's side, not something
worth fuzzing. */
static const enum MHD_DigestAuthAlgo3 algo3s[] = {
MHD_DIGEST_AUTH_ALGO3_MD5,
MHD_DIGEST_AUTH_ALGO3_SHA256,
MHD_DIGEST_AUTH_ALGO3_SHA512_256,
MHD_DIGEST_AUTH_ALGO3_MD5_SESSION,
MHD_DIGEST_AUTH_ALGO3_SHA256_SESSION,
MHD_DIGEST_AUTH_ALGO3_SHA512_256_SESSION
};
enum MHD_DigestAuthAlgo3 a =
algo3s[(sel >> 5) % (sizeof (algo3s) / sizeof (algo3s[0]))];
size_t hs = MHD_digest_get_hash_size (a);
/* Split the fuzzer-supplied header value into username / realm /
password. Each gets its OWN exactly-sized allocation rather than
being carved out of `value` in place: that way each string is
followed by its own ASAN redzone, so a helper reading one byte
past the end of the username is reported precisely instead of
quietly running into the realm that would follow it in a shared
buffer. */
size_t o1 = vlen / 3;
size_t o2 = (2 * vlen) / 3;
char *user = fuzz_dup_n (value, o1);
char *realm = fuzz_dup_n (value + o1, o2 - o1);
char *pass = fuzz_dup_n (value + o2, vlen - o2);
if ( (NULL != user) && (NULL != realm) && (NULL != pass) )
{
if ( (0 != hs) &&
(hs <= 64) )
{
size_t claim = hs - (size_t) (data[0] % (unsigned int) (hs + 1u));
void *bin = malloc (claim);
if (NULL != bin)
{
(void) MHD_digest_auth_calc_userdigest (a, user, realm, pass,
bin, claim);
(void) MHD_digest_auth_calc_userhash (a, user, realm, bin, claim);
free (bin);
}
}
{
size_t need = (0 != hs) ? (2 * hs + 1) : 1;
size_t claim = need - (size_t) (data[0] % (unsigned int) (need + 1u));
char *hex = (char *) malloc (claim);
if (NULL != hex)
{
(void) MHD_digest_auth_calc_userhash_hex (a, user, realm,
hex, claim);
free (hex);
}
}
}
free (user);
free (realm);
free (pass);
}
#endif /* DAUTH_SUPPORT */
MHD_pool_destroy (pool);
free (value);
return 0;
@@ -411,13 +515,34 @@ static const struct ah_seed ah_seeds[] = {
"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==")
ASEED ("\x01" "Basic QQ==QQ=="),
/* Bit 0x02 of byte 0 additionally runs the connection-less digest
helpers (MHD_digest_auth_calc_userdigest/_userhash/_userhash_hex)
with the header value split into username / realm / password. Bits
5-7 pick the algorithm and byte 0 also sets the deliberately
undersized output-buffer length, so the seeds below cover several
algorithms at several buffer sizes. Without a seed here the branch
is only reachable by a lucky bit flip in byte 0.
The three *_SESSION algorithms are not seeded separately: they hash
with the same primitive as their non-session counterpart, so they
reach no code the entries below do not, and byte 0 is the byte a
mutator flips first anyway. */
ASEED ("\x02" "user:TestRealm:pass"),
ASEED ("\x22" "user:TestRealm:pass"),
ASEED ("\x42" "user:TestRealm:pass"),
ASEED ("\xa2" "user:TestRealm:pass"),
/* Degenerate splits: empty username, empty realm, empty password. */
ASEED ("\x02" "::"),
ASEED ("\x02" ""),
/* Both the parser and the helpers in one execution. */
ASEED ("\x02" "Digest username=\"user\", realm=\"TestRealm\"")
};
+1552 -54
View File
@@ -34,7 +34,19 @@
* 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
* byte 4 response construction: which MHD_create_response_*()
* to use, how many response headers to add, footers,
* header-manipulation API
* byte 5 which authentication entry point to call: the v1/v2
* compatibility wrappers, the username/request-info
* queries, or the differing fail-response variants
* byte 6 event-loop mode (MHD_run() vs MHD_get_fdset*() +
* MHD_run_from_select*()) and the connection-introspection
* calls
* byte 7 suspend/resume and HTTP "Upgrade"
* byte 8 seed for the generated response header names/values
* byte 9 seed for the content-reader callback behaviour
* byte 10. 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
@@ -42,6 +54,13 @@
* op 2 send the payload and pump the daemon extra rounds
* op 3 close the connection, open a fresh one, send
*
* All ten configuration bytes are always present; an input shorter
* than that is rejected outright. The all-zero configuration is the
* plainest one -- a static two byte buffer response, MHD_run() as the
* event loop, no suspend, no upgrade, no extra introspection -- so
* zeroing bytes 4-9 of any input reduces it to what the harness did
* before those bytes existed.
*
* Splitting the byte stream into explicit segments matters: MHD's
* parser is incremental and several past bugs only showed up for
* particular split points.
@@ -53,12 +72,44 @@
* code that is only executed for a *valid* nonce.
*/
/* Must precede every #include: memfd_create() and MFD_CLOEXEC are only
declared by <sys/mman.h> under _GNU_SOURCE, and the OSS-Fuzz build
compiles this file with nothing but -DFUZZ_NO_MAIN (the library gets
_GNU_SOURCE from configure, the hand-compiled harnesses do not).
Without this the fd-backed response kinds silently fall back to a
buffer response and MHD_create_response_from_fd*() is never reached
-- a failure mode with no error message at all. */
#ifndef _GNU_SOURCE
#define _GNU_SOURCE 1
#endif
#define FUZZ_HARNESS_NAME "fuzz_request"
#include "fuzz_common.h"
#include <microhttpd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/select.h>
/* MHD_create_response_from_data() and MHD_create_response_from_buffer()
with an explicit MHD_ResponseMemoryMode are deprecated but are still
shipped API and are therefore still worth fuzzing; the deprecation
warning would otherwise drown the build output. */
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
/* memfd_create() gives a file descriptor backed by anonymous memory,
which is what makes the MHD_create_response_from_fd*() variants
affordable at fuzzing rates: no filesystem is touched. Where it is
unavailable the fd-backed response kinds fall back to a buffer
response (see make_response()). */
#if defined(__linux__)
#include <sys/mman.h>
#if defined(MFD_CLOEXEC)
#define FUZZ_HAVE_MEMFD 1
#endif
#endif
#define MAX_SEGMENTS 96
#define MAX_CONNECTIONS 8
@@ -66,6 +117,13 @@
#define GEN_BUF_SIZE 8192
#define MAX_EXPECT_BODY 4096
/** Number of entries in the response-construction table. */
#define RESP_KIND_COUNT 16
/** Number of authentication entry points selectable by byte 5. */
#define DAUTH_VARIANT_COUNT 10
/** Largest digest MHD_digest_auth_calc_userdigest() can produce. */
#define MAX_DIGEST_BIN 64
#define DIGEST_REALM "TestRealm"
#define DIGEST_USER "user"
#define DIGEST_PASS "pass"
@@ -88,6 +146,28 @@ struct fuzz_cfg
int do_iterate;
int chunked_reply;
int error_reply;
/* ---- configuration bytes 4-9; all zero is the plainest setting ---- */
unsigned int resp_kind; /**< index into the response constructors */
unsigned int resp_nhdr; /**< how many response headers to add */
int resp_footer; /**< also add a response footer/trailer */
int resp_hdr_api; /**< exercise get/del/set_response_options */
unsigned int dauth_variant; /**< which digest entry point to call */
unsigned int fail_variant; /**< which *_fail_response to queue */
int basic_v1; /**< use the v1 basic-auth getter */
int calc_helpers; /**< call the userdigest/userhash helpers */
unsigned int loop_mode; /**< 0 MHD_run, 1..3 external event loop */
int use_timeouts; /**< query MHD_get_timeout*() while pumping */
int conn_lookup; /**< MHD_lookup_connection_value*() */
int conn_setvalue; /**< MHD_set_connection_value() */
int conn_info; /**< MHD_get_connection_info/daemon_info */
int conn_option; /**< MHD_set_connection_option() */
int quiesce; /**< MHD_quiesce_daemon() before stopping */
unsigned int suspend_mode; /**< 0 none, 1 immediate, 2 deferred */
int allow_upgrade; /**< MHD_ALLOW_UPGRADE + upgrade response */
unsigned int upgrade_action; /**< which MHD_upgrade_action() to issue */
uint8_t hdr_seed; /**< picks response header name/value */
uint8_t crc_seed; /**< content-reader callback behaviour */
};
static struct fuzz_cfg cfg;
@@ -181,6 +261,509 @@ static const char digest_rnd[32] =
"\xfe\xdc\xba\x98\x76\x54\x32\x10\xfe\xdc\xba\x98\x76\x54\x32\x10";
/* ------------------------------------------------------------------ */
/* Configuration byte 4: response construction */
/* ------------------------------------------------------------------ */
/* Defined with the access handler further down; decorate_response()
hands it to MHD_get_response_headers(). */
static enum MHD_Result
kv_iter (void *cls,
enum MHD_ValueKind kind,
const char *key,
const char *value);
/** Daemon of the current iteration; needed by MHD_get_daemon_info(). */
static struct MHD_Daemon *cur_daemon;
/**
* Connections suspended by suspend mode 2, which the pump loop still has
* to resume. An entry is cleared by completed_cb() so that a connection
* MHD has finished with is never resumed afterwards.
*
* This has to be a set, not a single slot: op 3 opens a fresh connection
* without waiting for the previous one to finish, so several connections
* can be parked at the same time. A single slot silently loses all but
* the last, and the forgotten one is still suspended when the iteration
* calls MHD_stop_daemon(), which answers with
* MHD_PANIC ("MHD_stop_daemon() called while we have suspended
* connections") -- a harness bug that looks exactly like an MHD bug.
*/
static struct MHD_Connection *pending_resume[MAX_CONNECTIONS];
/**
* Set once the iteration has stopped feeding the daemon and only wants
* to drain it. suspend_maybe() then does nothing, which is what makes
* the flush loop in the teardown provably terminate: without it,
* resuming a connection lets the handler run and park it again, for as
* many rounds as there are pipelined requests still buffered.
*/
static int tearing_down;
/**
* Record @a c as suspended. Silently ignores the overflow case: the
* array has one slot per connection the harness can open, so it cannot
* overflow, and a dropped entry would only cost the resume below.
*/
static void
pending_resume_add (struct MHD_Connection *c)
{
unsigned int i;
for (i = 0; i < MAX_CONNECTIONS; i++)
{
if (NULL == pending_resume[i])
{
pending_resume[i] = c;
return;
}
}
}
/** Forget @a c without resuming it (it is gone). */
static void
pending_resume_drop (const struct MHD_Connection *c)
{
unsigned int i;
for (i = 0; i < MAX_CONNECTIONS; i++)
if (pending_resume[i] == c)
pending_resume[i] = NULL;
}
/**
* Resume everything still parked.
*
* @return non-zero if at least one connection was resumed, so that the
* caller knows the daemon needs another round to act on it
*/
static int
pending_resume_flush (void)
{
unsigned int i;
int any = 0;
for (i = 0; i < MAX_CONNECTIONS; i++)
{
struct MHD_Connection *c = pending_resume[i];
if (NULL == c)
continue;
/* Clear first: MHD_resume_connection() can run the handler, which
may suspend the very same connection again. */
pending_resume[i] = NULL;
MHD_resume_connection (c);
any = 1;
}
return any;
}
/** Body used by the buffer / fd / pipe / iovec response kinds. */
static const char resp_body[] = "hello world, this is the response body";
#define RESP_BODY_LEN (sizeof (resp_body) - 1)
/**
* Response header names and values, indexed by cfg.hdr_seed. The list
* mixes ordinary headers with ones MHD has to reject or handle
* specially: an empty name, a name carrying a control character, and a
* value containing CRLF -- the classic response-splitting vector, which
* MHD_add_response_header() is supposed to refuse. Exercising that
* validator is the point of this table.
*
* The message-framing headers are deliberately NOT here. MHD generates
* Content-Length and Transfer-Encoding itself from the response object,
* so an application that also sets them by hand is lying to the library
* about its own body: a manual "Transfer-Encoding: chunked" on, say, an
* iovec response sends MHD down the chunked path for a response that
* has neither a content reader nor a flat buffer, and it aborts in
* try_ready_chunked_body(). That is the harness misusing the API
* rather than a defect worth reporting, and leaving it in would bury
* every real finding under the same false report. MHD_RF_INSANITY_-
* HEADER_CONTENT_LENGTH, set through MHD_set_response_options() in
* decorate_response(), is the sanctioned way to explore that corner.
*/
static const char *const hdr_names[] = {
"X-Fuzz", "Content-Type", "Cache-Control", "Accept-Ranges",
/* "\x01" is split from the rest so that the hex escape stops after
one digit instead of swallowing the following "Bad". */
"Connection", "Set-Cookie", "X-\x01" "Bad", "", "Trailer", "Date",
"X-Fuzz", "Server", "Location", "ETag", "Vary", "Age"
};
static const char *const hdr_values[] = {
"1", "text/plain", "no-cache", "bytes", "keep-alive", "a=b",
"x\r\nInjected: yes", "", "X-Fuzz", "Thu, 01 Jan 1970 00:00:00 GMT",
"\x7f", "mhd", "/", "\"tag\"", "*", "0"
};
#define HDR_COUNT (sizeof (hdr_names) / sizeof (hdr_names[0]))
/**
* State of one MHD_create_response_from_callback() response. Allocated
* per response and released through the free callback that MHD invokes
* from MHD_destroy_response().
*/
struct crc_state
{
uint64_t total; /**< bytes to produce before end-of-stream */
int calls; /**< how often the reader has been called */
int err_at; /**< fail after this many calls, -1 to never fail */
unsigned int pattern;
};
static ssize_t
crc_cb (void *cls,
uint64_t pos,
char *buf,
size_t max)
{
struct crc_state *st = (struct crc_state *) cls;
size_t n;
size_t i;
st->calls++;
/* Deliberately reachable: an application content reader is allowed to
fail half way through a response body, and how MHD terminates the
connection in that case (especially a chunked one that can no
longer be framed correctly) is worth exercising. */
if ( (0 <= st->err_at) &&
(st->calls > st->err_at) )
return MHD_CONTENT_READER_END_WITH_ERROR;
if (pos >= st->total)
return MHD_CONTENT_READER_END_OF_STREAM;
n = (size_t) (st->total - pos);
if (n > max)
n = max;
if (0 == n)
return MHD_CONTENT_READER_END_OF_STREAM;
for (i = 0; i < n; i++)
buf[i] = (char) ('a' + (int) ((pos + i + st->pattern) % 26));
return (ssize_t) n;
}
static void
crc_free (void *cls)
{
free (cls);
}
/**
* Handler for an upgraded ("101 Switching Protocols") connection.
*
* @a extra_in holds whatever the client had already pipelined behind
* the request, which is attacker-controlled and therefore read here.
*
* The connection MUST be closed through MHD_upgrade_action(): the
* socket belongs to the application from this point on and
* MHD_stop_daemon() cannot complete while an upgraded connection is
* still outstanding.
*/
static void
upgrade_cb (void *cls,
struct MHD_Connection *connection,
void *req_cls,
const char *extra_in,
size_t extra_in_size,
MHD_socket sock,
struct MHD_UpgradeResponseHandle *urh)
{
volatile size_t sink = 0;
(void) cls;
(void) connection;
(void) req_cls;
(void) sock;
if ( (NULL != extra_in) &&
(0 != extra_in_size) )
sink += (size_t) (unsigned char) extra_in[extra_in_size - 1];
(void) sink;
if (0 != (cfg.upgrade_action & 0x01u))
(void) MHD_upgrade_action (urh, MHD_UPGRADE_ACTION_CORK_ON);
if (0 != (cfg.upgrade_action & 0x02u))
(void) MHD_upgrade_action (urh, MHD_UPGRADE_ACTION_CORK_OFF);
(void) MHD_upgrade_action (urh, MHD_UPGRADE_ACTION_CLOSE);
}
/**
* A file descriptor holding RESP_BODY_LEN bytes, backed by anonymous
* memory so that no filesystem is involved.
*
* @return -1 if unavailable, in which case the caller falls back to a
* buffer response
*/
static int
make_memfd (void)
{
#ifdef FUZZ_HAVE_MEMFD
int fd;
fd = memfd_create ("mhd-fuzz", MFD_CLOEXEC);
if (0 > fd)
return -1;
if (RESP_BODY_LEN != (size_t) write (fd, resp_body, RESP_BODY_LEN))
{
(void) close (fd);
return -1;
}
return fd;
#else
return -1;
#endif
}
/**
* The read end of a pipe holding RESP_BODY_LEN bytes. The body is far
* below the pipe capacity, so the write cannot block.
*/
static int
make_pipe_fd (void)
{
int pf[2];
if (0 != pipe (pf))
return -1;
if (RESP_BODY_LEN != (size_t) write (pf[1], resp_body, RESP_BODY_LEN))
{
(void) close (pf[0]);
(void) close (pf[1]);
return -1;
}
(void) close (pf[1]);
return pf[0];
}
/**
* Add response headers, footers and option flags as selected by byte 4
* and the header seed in byte 8.
*
* Footers are only meaningful for a chunked response -- MHD emits them
* as trailers after the last chunk -- which is why they are worth
* fuzzing together with the callback-based response kinds.
*/
static void
decorate_response (struct MHD_Response *r)
{
unsigned int i;
for (i = 0; i < cfg.resp_nhdr; i++)
{
unsigned int k = (cfg.hdr_seed + i) % HDR_COUNT;
(void) MHD_add_response_header (r, hdr_names[k], hdr_values[k]);
}
if (cfg.resp_footer)
{
unsigned int k = (unsigned int) (cfg.hdr_seed + 1u) % HDR_COUNT;
(void) MHD_add_response_footer (r, hdr_names[k], hdr_values[k]);
}
if (cfg.resp_hdr_api)
{
unsigned int k = cfg.hdr_seed % HDR_COUNT;
volatile size_t sink = 0;
const char *v;
v = MHD_get_response_header (r, hdr_names[k]);
if (NULL != v)
sink += strlen (v);
(void) sink;
(void) MHD_get_response_headers (r, &kv_iter, NULL);
(void) MHD_del_response_header (r, hdr_names[k], hdr_values[k]);
(void) MHD_set_response_options (r,
(enum MHD_ResponseFlags)
(((cfg.hdr_seed & 0x01) ?
MHD_RF_HTTP_VERSION_1_0_RESPONSE : 0)
| ((cfg.hdr_seed & 0x02) ?
MHD_RF_SEND_KEEP_ALIVE_HEADER : 0)
| ((cfg.hdr_seed & 0x04) ?
MHD_RF_INSANITY_HEADER_CONTENT_LENGTH
: 0)
| ((cfg.hdr_seed & 0x08) ?
MHD_RF_HEAD_ONLY_RESPONSE : 0)),
MHD_RO_END);
}
}
/**
* Build the response body for this iteration.
*
* Every constructor that fails is responsible for nothing: MHD releases
* neither the buffer nor the file descriptor when it returns NULL (see
* MHD_create_response_from_buffer_with_free_callback_cls() and
* MHD_create_response_from_pipe() in response.c), so ownership stays
* here and the resource has to be released explicitly. Getting this
* backwards would produce double frees that look exactly like MHD bugs.
*/
static struct MHD_Response *
make_response (void)
{
struct MHD_Response *r = NULL;
struct crc_state *st;
void *heap;
int fd;
switch (cfg.resp_kind)
{
case 0:
/* The two responses the harness used before byte 4 existed. Bit
0x20 of byte 1 (cfg.chunked_reply) selects between them and has
no other effect, so keeping the pair here is what keeps that bit
meaningful. */
if (cfg.chunked_reply)
r = MHD_create_response_from_buffer_copy (11, "hello world");
else
r = MHD_create_response_from_buffer_static (2, "ok");
break;
case 1:
r = MHD_create_response_from_buffer_copy (RESP_BODY_LEN, resp_body);
break;
case 2:
r = MHD_create_response_empty (MHD_RF_NONE);
break;
case 3:
r = MHD_create_response_from_buffer (RESP_BODY_LEN,
(void *) (intptr_t) resp_body,
MHD_RESPMEM_PERSISTENT);
break;
case 4:
heap = malloc (RESP_BODY_LEN);
if (NULL == heap)
return NULL;
memcpy (heap, resp_body, RESP_BODY_LEN);
r = MHD_create_response_from_buffer (RESP_BODY_LEN,
heap,
MHD_RESPMEM_MUST_FREE);
if (NULL == r)
free (heap);
break;
case 5:
r = MHD_create_response_from_buffer (RESP_BODY_LEN,
(void *) (intptr_t) resp_body,
MHD_RESPMEM_MUST_COPY);
break;
case 6:
heap = malloc (RESP_BODY_LEN);
if (NULL == heap)
return NULL;
memcpy (heap, resp_body, RESP_BODY_LEN);
r = MHD_create_response_from_buffer_with_free_callback (RESP_BODY_LEN,
heap,
&free);
if (NULL == r)
free (heap);
break;
case 7:
r = MHD_create_response_from_data (RESP_BODY_LEN,
(void *) (intptr_t) resp_body,
0 /* must_free */,
1 /* must_copy */);
break;
case 8:
heap = malloc (RESP_BODY_LEN);
if (NULL == heap)
return NULL;
memcpy (heap, resp_body, RESP_BODY_LEN);
r = MHD_create_response_from_data (RESP_BODY_LEN,
heap,
1 /* must_free */,
0 /* must_copy */);
if (NULL == r)
free (heap);
break;
case 9:
case 10:
st = (struct crc_state *) calloc (1, sizeof (struct crc_state));
if (NULL == st)
return NULL;
st->total = (uint64_t) (1u + (cfg.crc_seed & 0x7Fu));
st->pattern = cfg.crc_seed;
/* Injecting a reader error is only interesting some of the time;
otherwise the body would rarely complete. */
st->err_at = (0 != (cfg.crc_seed & 0x80u))
? (int) (1u + (cfg.crc_seed & 0x03u))
: -1;
r = MHD_create_response_from_callback (
(9 == cfg.resp_kind) ? st->total : MHD_SIZE_UNKNOWN,
(size_t) (16u + (cfg.crc_seed & 0x3Fu)),
&crc_cb,
st,
&crc_free);
if (NULL == r)
free (st);
break;
case 11:
case 12:
case 13:
fd = make_memfd ();
if (0 > fd)
{
r = MHD_create_response_from_buffer_static (2, "ok");
break;
}
if (11 == cfg.resp_kind)
r = MHD_create_response_from_fd (RESP_BODY_LEN, fd);
else if (12 == cfg.resp_kind)
/* Parenthesised so that the deprecated *function* is called and
not the same-named macro that redirects to the 64 bit variant;
reaching the v1 entry point is the whole point here. */
r = (MHD_create_response_from_fd_at_offset) (
RESP_BODY_LEN / 2,
fd,
(off_t) (RESP_BODY_LEN / 4));
else
r = MHD_create_response_from_fd64 ((uint64_t) RESP_BODY_LEN, fd);
if (NULL == r)
(void) close (fd);
break;
case 14:
fd = make_pipe_fd ();
if (0 > fd)
{
r = MHD_create_response_from_buffer_static (2, "ok");
break;
}
r = MHD_create_response_from_pipe (fd);
if (NULL == r)
(void) close (fd);
break;
case 15:
default:
{
static struct MHD_IoVec iov[3];
/* Static data, so no free callback is needed; MHD copies the
array itself (see the documentation of the @a iov parameter). */
iov[0].iov_base = resp_body;
iov[0].iov_len = RESP_BODY_LEN / 2;
iov[1].iov_base = resp_body + (RESP_BODY_LEN / 2);
iov[1].iov_len = RESP_BODY_LEN - (RESP_BODY_LEN / 2);
iov[2].iov_base = resp_body;
iov[2].iov_len = (size_t) (cfg.crc_seed % (RESP_BODY_LEN + 1));
r = MHD_create_response_from_iovec (iov,
(0 != iov[2].iov_len) ? 3u : 2u,
NULL,
NULL);
}
break;
}
if (NULL != r)
decorate_response (r);
return r;
}
/* ------------------------------------------------------------------ */
/* Access handler */
/* ------------------------------------------------------------------ */
@@ -212,6 +795,33 @@ kv_iter (void *cls,
}
/**
* Length-aware counterpart of kv_iter(), for
* MHD_get_connection_values_n(). Unlike the NUL-terminated iterator it
* is handed explicit sizes, so it reads exactly what MHD says is there
* rather than trusting a terminator.
*/
static enum MHD_Result
kv_iter_n (void *cls,
enum MHD_ValueKind kind,
const char *key,
size_t key_size,
const char *value,
size_t value_size)
{
volatile size_t sink = 0;
(void) cls;
(void) kind;
if ( (NULL != key) && (0 != key_size) )
sink += (size_t) (unsigned char) key[key_size - 1];
if ( (NULL != value) && (0 != value_size) )
sink += (size_t) (unsigned char) value[value_size - 1];
(void) sink;
return MHD_YES;
}
static enum MHD_Result
post_iter (void *cls,
enum MHD_ValueKind kind,
@@ -269,6 +879,483 @@ oracle_check_body (struct hstate *hs,
}
/* ------------------------------------------------------------------ */
/* Configuration byte 5: authentication entry points */
/* ------------------------------------------------------------------ */
static const enum MHD_DigestAuthAlgorithm algo1_tbl[] = {
MHD_DIGEST_ALG_AUTO,
MHD_DIGEST_ALG_MD5,
MHD_DIGEST_ALG_SHA256,
MHD_DIGEST_ALG_AUTO
};
static const enum MHD_DigestAuthAlgo3 algo3_tbl[] = {
MHD_DIGEST_AUTH_ALGO3_MD5,
MHD_DIGEST_AUTH_ALGO3_SHA256,
MHD_DIGEST_AUTH_ALGO3_SHA512_256,
MHD_DIGEST_AUTH_ALGO3_MD5_SESSION,
MHD_DIGEST_AUTH_ALGO3_SHA256_SESSION,
MHD_DIGEST_AUTH_ALGO3_SHA512_256_SESSION,
MHD_DIGEST_AUTH_ALGO3_MD5,
MHD_DIGEST_AUTH_ALGO3_SHA256
};
/** Outcome of run_digest_check(). */
#define DAUTH_CHALLENGE 0
#define DAUTH_PASSED 1
#define DAUTH_STALE 2
/**
* Exercise the digest helpers that take no connection:
* MHD_digest_get_hash_size(), MHD_digest_auth_calc_userdigest(),
* MHD_digest_auth_calc_userhash() and
* MHD_digest_auth_calc_userhash_hex().
*
* The output buffers are heap allocations of *exactly* the size that is
* passed to MHD, and that size is deliberately varied down to zero. A
* correct implementation must reject every buffer that is too small; if
* it instead writes the full digest, ASAN's redzone catches it
* immediately. This is the shape of the bug fixed in commit 5a73c1ae,
* where an over-long userhash overflowed a fixed-size decode buffer.
*/
static void
call_calc_helpers (void)
{
enum MHD_DigestAuthAlgo3 a = algo3_tbl[cfg.hdr_seed
% (sizeof (algo3_tbl)
/ sizeof (algo3_tbl[0]))];
size_t hs;
size_t claim;
void *bin;
char *hex;
hs = MHD_digest_get_hash_size (a);
if ( (0 == hs) ||
(hs > MAX_DIGEST_BIN) )
return;
claim = hs - (size_t) (cfg.crc_seed % (unsigned int) (hs + 1u));
bin = malloc (claim);
if (NULL != bin)
{
(void) MHD_digest_auth_calc_userdigest (a,
DIGEST_USER,
DIGEST_REALM,
DIGEST_PASS,
bin,
claim);
(void) MHD_digest_auth_calc_userhash (a,
DIGEST_USER,
DIGEST_REALM,
bin,
claim);
free (bin);
}
/* The hex form needs 2*hs+1 bytes; ask for less about half the time. */
claim = (2u * hs + 1u)
- (size_t) (cfg.hdr_seed % (unsigned int) (2u * hs + 2u));
hex = (char *) malloc (claim);
if (NULL != hex)
{
(void) MHD_digest_auth_calc_userhash_hex (a,
DIGEST_USER,
DIGEST_REALM,
hex,
claim);
free (hex);
}
}
/**
* Run the digest-authentication entry point selected by byte 5.
*
* Variant 0 is MHD_digest_auth_check3(), which is the plainest one and
* what the all-zero configuration selects. Variants 1-5 are the v1/v2 compatibility
* wrappers and the pre-computed-digest forms, which do their own buffer
* sizing and are therefore worth driving directly. Variants 6-8 are
* the query functions, which parse the client's Authorization header
* but return no verdict, so they run *in addition* to check3().
*
* @return one of DAUTH_CHALLENGE, DAUTH_PASSED, DAUTH_STALE
*/
static int
run_digest_check (struct MHD_Connection *connection)
{
unsigned int variant = cfg.dauth_variant;
enum MHD_DigestAuthResult dres;
uint8_t ud[MAX_DIGEST_BIN];
size_t hs;
int r1;
volatile size_t sink = 0;
switch (variant)
{
case 1:
r1 = MHD_digest_auth_check (connection,
DIGEST_REALM,
DIGEST_USER,
DIGEST_PASS,
0);
if (MHD_YES == r1)
return DAUTH_PASSED;
return (MHD_INVALID_NONCE == r1) ? DAUTH_STALE : DAUTH_CHALLENGE;
case 2:
r1 = MHD_digest_auth_check2 (connection,
DIGEST_REALM,
DIGEST_USER,
DIGEST_PASS,
0,
algo1_tbl[cfg.hdr_seed & 0x03]);
if (MHD_YES == r1)
return DAUTH_PASSED;
return (MHD_INVALID_NONCE == r1) ? DAUTH_STALE : DAUTH_CHALLENGE;
case 3:
/* MHD_digest_auth_check_digest() is MD5-only by definition. */
if (MHD_YES != MHD_digest_auth_calc_userdigest (
MHD_DIGEST_AUTH_ALGO3_MD5,
DIGEST_USER, DIGEST_REALM, DIGEST_PASS,
ud, MHD_MD5_DIGEST_SIZE))
break;
r1 = MHD_digest_auth_check_digest (connection,
DIGEST_REALM,
DIGEST_USER,
ud,
0);
if (MHD_YES == r1)
return DAUTH_PASSED;
return (MHD_INVALID_NONCE == r1) ? DAUTH_STALE : DAUTH_CHALLENGE;
case 4:
case 5:
{
/* The pre-computed-digest entry points are stricter than the
password-based ones: the digest and the algorithm have to
agree, and naming more than one base hashing algorithm is an
MHD_PANIC("API violation") rather than an error return. So the
algorithm is chosen first here and the digest, its length and
the algorithm argument are all derived from that one choice.
MHD_DIGEST_ALG_AUTO is deliberately not used for variant 4:
MHD_digest_auth_check_digest2() maps it to
MULT_ALGO3_ANY_NON_SESSION, which names three base algorithms
and therefore trips that very panic. See README section 6. */
static const enum MHD_DigestAuthAlgo3 digest_algos[] = {
MHD_DIGEST_AUTH_ALGO3_MD5,
MHD_DIGEST_AUTH_ALGO3_SHA256,
MHD_DIGEST_AUTH_ALGO3_SHA512_256,
MHD_DIGEST_AUTH_ALGO3_MD5_SESSION,
MHD_DIGEST_AUTH_ALGO3_SHA256_SESSION,
MHD_DIGEST_AUTH_ALGO3_SHA512_256_SESSION
};
enum MHD_DigestAuthAlgo3 a3;
if (4 == variant)
/* check_digest2() only knows MD5 and SHA-256. */
a3 = (0 != (cfg.hdr_seed & 0x01))
? MHD_DIGEST_AUTH_ALGO3_SHA256
: MHD_DIGEST_AUTH_ALGO3_MD5;
else
a3 = digest_algos[cfg.hdr_seed
% (sizeof (digest_algos)
/ sizeof (digest_algos[0]))];
hs = MHD_digest_get_hash_size (a3);
if ( (0 == hs) ||
(hs > sizeof (ud)) ||
(MHD_YES != MHD_digest_auth_calc_userdigest (a3,
DIGEST_USER,
DIGEST_REALM,
DIGEST_PASS,
ud,
hs)) )
break;
if (4 == variant)
{
r1 = MHD_digest_auth_check_digest2 (connection,
DIGEST_REALM,
DIGEST_USER,
ud,
hs,
0,
(MHD_DIGEST_AUTH_ALGO3_SHA256 == a3)
? MHD_DIGEST_ALG_SHA256
: MHD_DIGEST_ALG_MD5);
if (MHD_YES == r1)
return DAUTH_PASSED;
return (MHD_INVALID_NONCE == r1) ? DAUTH_STALE : DAUTH_CHALLENGE;
}
dres = MHD_digest_auth_check_digest3 (connection,
DIGEST_REALM,
DIGEST_USER,
ud,
hs,
0,
0,
cfg.qop,
/* Exactly one base algorithm.
The MULT_ALGO3_* constants are
numerically identical to their
ALGO3_* counterparts. */
(enum MHD_DigestAuthMultiAlgo3) a3);
if (MHD_DAUTH_OK == dres)
return DAUTH_PASSED;
return (MHD_DAUTH_NONCE_STALE == dres) ? DAUTH_STALE : DAUTH_CHALLENGE;
}
case 6:
{
char *u = MHD_digest_auth_get_username (connection);
if (NULL != u)
{
sink += strlen (u);
MHD_free (u);
}
}
break;
case 7:
{
struct MHD_DigestAuthUsernameInfo *ui;
ui = MHD_digest_auth_get_username3 (connection);
if (NULL != ui)
{
if (NULL != ui->username)
sink += ui->username_len;
if (NULL != ui->userhash_hex)
sink += ui->userhash_hex_len;
MHD_free (ui);
}
}
break;
case 8:
{
struct MHD_DigestAuthInfo *ai;
ai = MHD_digest_auth_get_request_info3 (connection);
if (NULL != ai)
{
if (NULL != ai->username)
sink += ai->username_len;
if (NULL != ai->realm)
sink += ai->realm_len;
if (NULL != ai->opaque)
sink += ai->opaque_len;
if (NULL != ai->userhash_hex)
sink += ai->userhash_hex_len;
sink += ai->cnonce_len;
sink += (size_t) ai->nc;
MHD_free (ai);
}
}
break;
default:
break;
}
(void) sink;
/* Variant 0, the query variants and every early break end up here. */
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)
return DAUTH_PASSED;
return (MHD_DAUTH_NONCE_STALE == dres) ? DAUTH_STALE : DAUTH_CHALLENGE;
}
/**
* Send the "authentication required" reply through the variant of the
* API selected by byte 5. The v1/v2 forms take a plain response and an
* algorithm rather than the qop/algo pair of the v3 form.
*/
static enum MHD_Result
queue_auth_challenge (struct MHD_Connection *connection,
int stale)
{
struct MHD_Response *resp;
enum MHD_Result ret;
resp = MHD_create_response_from_buffer_static (0, "");
if (NULL == resp)
return MHD_NO;
switch (cfg.fail_variant)
{
case 1:
ret = MHD_queue_auth_fail_response (connection,
DIGEST_REALM,
"0123456789abcdef",
resp,
stale ? MHD_YES : MHD_NO);
break;
case 2:
ret = MHD_queue_auth_fail_response2 (connection,
DIGEST_REALM,
"0123456789abcdef",
resp,
stale ? MHD_YES : MHD_NO,
algo1_tbl[cfg.hdr_seed & 0x03]);
break;
case 3:
if (0 != (cfg.hdr_seed & 0x01))
ret = MHD_queue_basic_auth_required_response3 (connection,
DIGEST_REALM,
(0 != (cfg.hdr_seed
& 0x02))
? MHD_YES : MHD_NO,
resp);
else
ret = MHD_queue_basic_auth_fail_response (connection,
DIGEST_REALM,
resp);
break;
case 0:
default:
ret = MHD_queue_auth_required_response3 (connection,
DIGEST_REALM,
"0123456789abcdef" /* opaque */,
"/",
resp,
stale ? MHD_YES : MHD_NO,
cfg.qop,
cfg.algo,
MHD_YES /* userhash */,
MHD_NO);
break;
}
MHD_destroy_response (resp);
return ret;
}
/* ------------------------------------------------------------------ */
/* Configuration byte 6: connection introspection */
/* ------------------------------------------------------------------ */
/**
* Call the read-only connection accessors on attacker-supplied header
* data. MHD_lookup_connection_value*() run the case-insensitive header
* matcher over whatever the client sent, which is exactly the sort of
* input the fuzzer is good at producing.
*/
static void
exercise_connection_api (struct MHD_Connection *connection)
{
volatile size_t sink = 0;
if (cfg.conn_lookup)
{
static const char *const keys[] = {
"Host", "Content-Length", "Transfer-Encoding", "Authorization",
"Cookie", "", "X-Fuzz", "connection"
};
const char *k = keys[cfg.hdr_seed % (sizeof (keys) / sizeof (keys[0]))];
const char *v;
const char *vp = NULL;
size_t vlen = 0;
const char *uri = NULL;
size_t ulen = 0;
v = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, k);
if (NULL != v)
sink += strlen (v);
if (MHD_YES == MHD_lookup_connection_value_n (connection,
MHD_HEADER_KIND,
k,
strlen (k),
&vp,
&vlen))
sink += vlen;
if (MHD_YES == MHD_get_connection_URI_path_n (connection, &uri, &ulen))
sink += ulen;
}
if (cfg.conn_setvalue)
(void) MHD_set_connection_value (connection,
MHD_HEADER_KIND,
"X-Fuzz-Added",
"1");
if (cfg.conn_info)
{
static const enum MHD_ConnectionInfoType cinfo[] = {
MHD_CONNECTION_INFO_CLIENT_ADDRESS,
MHD_CONNECTION_INFO_DAEMON,
MHD_CONNECTION_INFO_CONNECTION_FD,
MHD_CONNECTION_INFO_SOCKET_CONTEXT,
MHD_CONNECTION_INFO_CONNECTION_SUSPENDED,
MHD_CONNECTION_INFO_CONNECTION_TIMEOUT,
MHD_CONNECTION_INFO_REQUEST_HEADER_SIZE,
MHD_CONNECTION_INFO_HTTP_STATUS
};
static const enum MHD_DaemonInfoType dinfo[] = {
MHD_DAEMON_INFO_CURRENT_CONNECTIONS,
MHD_DAEMON_INFO_FLAGS,
MHD_DAEMON_INFO_LISTEN_FD,
MHD_DAEMON_INFO_BIND_PORT
};
unsigned int i;
for (i = 0; i < sizeof (cinfo) / sizeof (cinfo[0]); i++)
if (NULL != MHD_get_connection_info (connection, cinfo[i]))
sink++;
for (i = 0; i < sizeof (dinfo) / sizeof (dinfo[0]); i++)
if ( (NULL != cur_daemon) &&
(NULL != MHD_get_daemon_info (cur_daemon, dinfo[i])) )
sink++;
}
if (cfg.conn_option)
(void) MHD_set_connection_option (connection,
MHD_CONNECTION_OPTION_TIMEOUT,
(unsigned int) (cfg.crc_seed & 0x0F));
if (cfg.conn_info)
{
/* Constant-output accessors. They cannot fail on attacker input,
but they are cheap and leaving them uncovered makes the coverage
report harder to read.
MHD_fini() is deliberately NOT called anywhere: it tears down
process-global library state, so a single call would break every
later iteration in the same process. */
const char *v = MHD_get_version ();
if (NULL != v)
sink += strlen (v);
sink += (size_t) MHD_get_version_bin ();
}
(void) sink;
}
/**
* Suspend the connection if byte 7 asks for it.
*
* Mode 1 suspends and resumes straight away, which exercises both state
* transitions without leaving the connection parked. Mode 2 leaves it
* suspended and records it in @e pending_resume so that the pump loop
* resumes it on its next round; completed_cb() drops the record, so a
* connection MHD has finished with is never resumed after the fact, and
* the iteration flushes the whole set before stopping the daemon. A
* connection left suspended makes MHD_stop_daemon() MHD_PANIC().
*/
static void
suspend_maybe (struct MHD_Connection *connection)
{
if ( (0 == cfg.suspend_mode) ||
tearing_down)
return;
MHD_suspend_connection (connection);
if (1 == cfg.suspend_mode)
{
MHD_resume_connection (connection);
return;
}
pending_resume_add (connection);
}
static enum MHD_Result
ahc (void *cls,
struct MHD_Connection *connection,
@@ -283,7 +1370,10 @@ ahc (void *cls,
struct MHD_Response *resp;
enum MHD_Result ret;
volatile size_t sink = 0;
const char *conn_hdr = NULL;
char conn_lc[128];
conn_lc[0] = '\0';
(void) cls;
if (NULL == hs)
{
@@ -329,13 +1419,17 @@ ahc (void *cls,
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);
const enum MHD_ValueKind kinds = MHD_HEADER_KIND
| MHD_GET_ARGUMENT_KIND
| MHD_COOKIE_KIND
| MHD_FOOTER_KIND;
/* Alternate between the NUL-terminated iterator and the
length-aware one, which are separate code paths in MHD. */
if (0 != (cfg.hdr_seed & 0x10))
(void) MHD_get_connection_values_n (connection, kinds, &kv_iter_n, NULL);
else
(void) MHD_get_connection_values (connection, kinds, &kv_iter, NULL);
}
if (oracle_on &&
@@ -344,61 +1438,129 @@ ahc (void *cls,
"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;
exercise_connection_api (connection);
bai = MHD_basic_auth_get_username_password3 (connection);
if (NULL != bai)
/* Lower-cased copy of the request's "Connection" header, used by the
upgrade check further down. MHD's own token matcher is internal,
so a plain case-folded substring test is used here; it only has to
be good enough to keep the harness from queueing an upgrade
response on a connection that cannot be upgraded. */
if (cfg.allow_upgrade)
{
conn_hdr = MHD_lookup_connection_value (connection,
MHD_HEADER_KIND,
MHD_HTTP_HEADER_CONNECTION);
if (NULL != conn_hdr)
{
if (NULL != bai->username)
sink += bai->username_len;
if (NULL != bai->password)
sink += bai->password_len;
MHD_free (bai);
size_t i;
for (i = 0; (i + 1 < sizeof (conn_lc)) && ('\0' != conn_hdr[i]); i++)
conn_lc[i] = (char) (( ('A' <= conn_hdr[i]) && ('Z' >= conn_hdr[i]))
? (conn_hdr[i] - 'A' + 'a') : conn_hdr[i]);
conn_lc[i] = '\0';
}
}
if (cfg.do_basic)
{
if (cfg.basic_v1)
{
/* The v1 getter returns two separate malloc()ed strings and is
the one that historically had to be freed by hand. */
char *pass = NULL;
char *user;
user = MHD_basic_auth_get_username_password (connection, &pass);
if (NULL != user)
sink += strlen (user);
if (NULL != pass)
sink += strlen (pass);
if (NULL != user)
MHD_free (user);
if (NULL != pass)
MHD_free (pass);
}
else
{
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.calc_helpers)
call_calc_helpers ();
if (cfg.do_digest)
{
enum MHD_DigestAuthResult dres;
int dres = run_digest_check (connection);
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)
if (DAUTH_PASSED == 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);
return queue_auth_challenge (connection, DAUTH_STALE == dres);
}
}
/* Only answer 101 for a request that really is an upgrade request:
HTTP/1.1, an "Upgrade" header, and a "Connection" header naming the
upgrade token (RFC 9110 section 7.8). This is the check a real
application performs, and all three inputs come off the wire, so
the path stays fully attacker-driven.
It is deliberately NOT enough to keep MHD's
mhd_assert (NULL == r->upgrade_handler ||
MHD_CONN_MUST_UPGRADE == c->keepalive)
in build_header_response() satisfied, and it is not meant to be:
MHD may have decided during header parsing that the connection must
close, and does not expose that decision. That is finding K7 (see
README section 6); corpus/known-findings/K7-upgrade-after-must-close.bin
is the reproducer, and patches/K7.diff turns the abort into the
MHD_NO that the code below already handles. Do not "fix" this by
weakening the condition here -- the check above is what an
application can actually do, and the harness has to behave like
one. */
if (cfg.allow_upgrade &&
(NULL != version) &&
(0 == strcmp (version, MHD_HTTP_VERSION_1_1)) &&
(NULL != MHD_lookup_connection_value (connection,
MHD_HEADER_KIND,
MHD_HTTP_HEADER_UPGRADE)) &&
(NULL != conn_hdr) &&
(NULL != strstr (conn_lc, "upgrade")) &&
(NULL == strstr (conn_lc, "close")))
{
resp = MHD_create_response_for_upgrade (&upgrade_cb, NULL);
if (NULL != resp)
{
/* Mandatory: MHD_create_response_for_upgrade() documents that the
upgrade headers are the application's job, and
MHD_response_execute_upgrade_() asserts that "Upgrade" is
present before it hands the socket over. Omitting it is an
application-contract violation, not something worth fuzzing. */
(void) MHD_add_response_header (resp,
MHD_HTTP_HEADER_UPGRADE,
"fuzz-protocol");
decorate_response (resp);
ret = MHD_queue_response (connection,
MHD_HTTP_SWITCHING_PROTOCOLS,
resp);
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");
resp = make_response ();
if (NULL == resp)
return MHD_NO;
ret = MHD_queue_response (connection,
@@ -406,6 +1568,17 @@ ahc (void *cls,
? MHD_HTTP_FORBIDDEN : MHD_HTTP_OK,
resp);
MHD_destroy_response (resp);
/* Only after a response was actually accepted. Returning MHD_NO
tells MHD to terminate the connection, and terminating a connection
that this callback has just suspended trips
mhd_assert (! connection->suspended) in MHD_connection_close_().
That is the harness contradicting itself, not an MHD defect: the
two requests are mutually exclusive by construction. (MHD_NO is
reachable here for real -- MHD_queue_response() rejects, for
instance, a response carrying MHD_RF_HEAD_ONLY_RESPONSE on a GET,
which decorate_response() can set.) */
if (MHD_YES == ret)
suspend_maybe (connection);
return ret;
}
@@ -419,8 +1592,10 @@ completed_cb (void *cls,
struct hstate *hs = (struct hstate *) *req_cls;
(void) cls;
(void) connection;
(void) toe;
/* MHD is done with this connection, so the deferred resume of
suspend mode 2 must not fire for it any more. */
pending_resume_drop (connection);
if (NULL == hs)
return;
if (NULL != hs->pp)
@@ -497,6 +1672,84 @@ drain_and_harvest (int sock)
}
/**
* Advance the daemon by one cycle, through the event-loop API selected
* by byte 6 of the input.
*
* Mode 0 is MHD_run(), which is what the harness used before the
* extension and which does the descriptor bookkeeping internally.
* Modes 1-3 drive the daemon the way an application with its own event
* loop does: MHD_get_fdset*() to collect the descriptors, select(), then
* MHD_run_from_select*(). That is the most common production
* integration and none of it was reachable before.
*
* The select() timeout is always zero. The harness is single threaded
* and whatever the daemon is waiting for has already been written into
* the socketpair, so blocking would only burn wall clock; select() is
* called purely to populate the ready sets.
*/
static void
run_once (struct MHD_Daemon *d)
{
fd_set rs;
fd_set ws;
fd_set es;
MHD_socket max_fd = MHD_INVALID_SOCKET;
struct timeval tv;
if (0 == cfg.loop_mode)
{
(void) MHD_run (d);
return;
}
FD_ZERO (&rs);
FD_ZERO (&ws);
FD_ZERO (&es);
if (1 == cfg.loop_mode)
{
/* Parenthesised so that the real v1 function is called:
microhttpd.h also defines MHD_get_fdset as a macro that forwards
to MHD_get_fdset2 with FD_SETSIZE, so the unparenthesised name
would never reach the v1 entry point at all. Same trick for
MHD_run_from_select below. */
if (MHD_YES != (MHD_get_fdset) (d, &rs, &ws, &es, &max_fd))
{
(void) MHD_run (d);
return;
}
}
else
{
if (MHD_YES != MHD_get_fdset2 (d, &rs, &ws, &es, &max_fd,
(unsigned int) FD_SETSIZE))
{
(void) MHD_run (d);
return;
}
}
if (cfg.use_timeouts)
{
MHD_UNSIGNED_LONG_LONG tl = 0;
uint64_t t64 = 0;
volatile int64_t sink;
(void) MHD_get_timeout (d, &tl);
(void) MHD_get_timeout64 (d, &t64);
sink = MHD_get_timeout64s (d);
sink += (int64_t) MHD_get_timeout_i (d);
(void) sink;
}
tv.tv_sec = 0;
tv.tv_usec = 0;
if (MHD_INVALID_SOCKET != max_fd)
(void) select ((int) max_fd + 1, &rs, &ws, &es, &tv);
if (1 == cfg.loop_mode)
(void) (MHD_run_from_select) (d, &rs, &ws, &es);
else
(void) MHD_run_from_select2 (d, &rs, &ws, &es, (unsigned int) FD_SETSIZE);
}
static void
pump (struct MHD_Daemon *d,
int sock,
@@ -506,7 +1759,12 @@ pump (struct MHD_Daemon *d,
for (i = 0; i < rounds; i++)
{
(void) MHD_run (d);
/* Deferred resume of suspend mode 2. Each slot is cleared before
its connection is resumed, which keeps this correct even if the
resume makes MHD complete (and forget) the connection, or run the
handler again so that it suspends the same one anew. */
(void) pending_resume_flush ();
run_once (d);
drain_and_harvest (sock);
}
}
@@ -646,7 +1904,9 @@ LLVMFuzzerTestOneInput (const uint8_t *data,
in fuzz_common.h for why the process dies without it. */
fuzz_ignore_sigpipe ();
if (size < 6)
/* The ten configuration bytes are mandatory. Anything shorter has
no segment stream either, so there is nothing to fuzz. */
if (size < 10)
return 0;
nonce_len = 0;
@@ -662,6 +1922,10 @@ LLVMFuzzerTestOneInput (const uint8_t *data,
cfg.do_basic = (0 != (data[1] & 0x02));
cfg.do_postproc = (0 != (data[1] & 0x04));
cfg.do_iterate = (0 != (data[1] & 0x08));
/* Bits 0x10 and 0x80 of byte 1 are unused. 0x10 used to gate the
configuration bytes 4-9 back when they were optional; it is left
free rather than reassigned so that the meaning of the corpus
files written while it was a gate does not silently change. */
cfg.chunked_reply = (0 != (data[1] & 0x20));
cfg.error_reply = (0 != (data[1] & 0x40));
if (! min_mem_limit_read)
@@ -695,8 +1959,43 @@ LLVMFuzzerTestOneInput (const uint8_t *data,
: MHD_DIGEST_AUTH_MULT_QOP_ANY_NON_INT;
cfg.nonce_nc_size = nnc_tbl[(data[3] >> 4) & 0x03];
cfg.resp_kind = (unsigned int) (data[4] & 0x0F);
cfg.resp_nhdr = (unsigned int) ((data[4] >> 4) & 0x03);
cfg.resp_footer = (0 != (data[4] & 0x40));
cfg.resp_hdr_api = (0 != (data[4] & 0x80));
cfg.dauth_variant = (unsigned int) (data[5] & 0x0F) % DAUTH_VARIANT_COUNT;
cfg.fail_variant = (unsigned int) ((data[5] >> 4) & 0x03);
cfg.basic_v1 = (0 != (data[5] & 0x40));
cfg.calc_helpers = (0 != (data[5] & 0x80));
cfg.loop_mode = (unsigned int) (data[6] & 0x03);
cfg.use_timeouts = (0 != (data[6] & 0x04));
cfg.conn_lookup = (0 != (data[6] & 0x08));
cfg.conn_setvalue = (0 != (data[6] & 0x10));
cfg.conn_info = (0 != (data[6] & 0x20));
cfg.conn_option = (0 != (data[6] & 0x40));
cfg.quiesce = (0 != (data[6] & 0x80));
/* Only modes 0-2 exist; 3 folds onto the immediate-resume mode
rather than being a fourth, unimplemented behaviour. */
cfg.suspend_mode = (unsigned int) (data[7] & 0x03);
if (3 == cfg.suspend_mode)
cfg.suspend_mode = 1;
cfg.allow_upgrade =
(0 != (data[7] & 0x04)) &&
(MHD_YES == MHD_is_feature_supported (MHD_FEATURE_UPGRADE));
cfg.upgrade_action = (unsigned int) ((data[7] >> 3) & 0x03);
cfg.hdr_seed = data[8];
cfg.crc_seed = data[9];
oracle_on = 0;
expect_body_len = 0;
/* Must never carry over: the connections these pointed at belonged to
the previous iteration's daemon and are long gone. */
memset (pending_resume, 0, sizeof (pending_resume));
tearing_down = 0;
if (0 != cfg.mem_limit)
{
@@ -728,6 +2027,10 @@ LLVMFuzzerTestOneInput (const uint8_t *data,
flags = MHD_USE_NO_LISTEN_SOCKET;
if (fuzz_verbose)
flags |= MHD_USE_ERROR_LOG;
if (0 != cfg.suspend_mode)
flags |= MHD_ALLOW_SUSPEND_RESUME;
if (cfg.allow_upgrade)
flags |= MHD_ALLOW_UPGRADE;
MHD_set_panic_func (&panic_cb, NULL);
d = MHD_start_daemon (flags,
@@ -743,6 +2046,7 @@ LLVMFuzzerTestOneInput (const uint8_t *data,
MHD_OPTION_END);
if (NULL == d)
return 0;
cur_daemon = d;
stat_daemons++;
if (! stats_registered)
{
@@ -753,10 +2057,11 @@ LLVMFuzzerTestOneInput (const uint8_t *data,
if (0 != new_connection (d, &sock))
{
MHD_stop_daemon (d);
cur_daemon = NULL;
return 0;
}
pos = 4;
pos = 10u;
while ( (pos + 2 <= size) &&
(nseg < MAX_SEGMENTS) )
{
@@ -803,7 +2108,28 @@ LLVMFuzzerTestOneInput (const uint8_t *data,
close_connection (d, &sock);
pump (d, sock, 4);
/* A connection left suspended makes MHD_stop_daemon() MHD_PANIC()
("called while we have suspended connections"), and
MHD_resume_connection() alone is not enough: it only raises a flag,
and the connection is taken off the daemon's suspended list by
MHD_run(). So flush and run until nothing is parked any more.
tearing_down stops the handler from parking anything new, which is
what bounds this loop; the cap is only a backstop. */
tearing_down = 1;
{
unsigned int i;
for (i = 0; i < MAX_CONNECTIONS + 2u; i++)
{
if (! pending_resume_flush ())
break;
(void) MHD_run (d);
}
}
if (cfg.quiesce)
(void) MHD_quiesce_daemon (d);
MHD_stop_daemon (d);
cur_daemon = NULL;
return 0;
}
@@ -1421,14 +2747,18 @@ struct seed_def
const char *name;
unsigned char cfg[4];
struct seed_part parts[4];
/* Configuration bytes 4-9. Declared after @e parts so that a seed
which only cares about the wire data can leave the member out of
its brace initialiser and get the all-zero (plainest) setting. */
unsigned char ext[6];
};
#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 } },
/* There is deliberately no bare "GET / HTTP/1.1" seed: every seed
below opens with a request at least as simple, so a plain one adds
no coverage of its own (libFuzzer -merge=1 drops it). */
{ "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" },
@@ -1516,7 +2846,174 @@ static const struct seed_def seeds[] = {
{ "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 } }
P_END, P_END, P_END } },
/* ------------- seeds driving configuration bytes 4-9 -------------
The seeds above leave those bytes zero and only exercise the
parser. Without at least one seed per area below, the rest of the
API stays practically unreachable: libFuzzer would have to guess
six configuration bytes before anything new runs. Each seed turns
on exactly one area so that a minimiser keeps them distinguishable.
ext[] is { response, auth, event-loop/introspection, suspend and
upgrade, header seed, content-reader seed }. */
/* Chunked reply produced by a content-reader callback, with response
headers and a trailer -- the reply-side counterpart of the chunk
parsing that commit c13f4c64 fixed on the request side. */
{ "ext-callback-chunked", { 0x00, 0x08, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x0A | 0x20 | 0x40, 0x00, 0x00, 0x00, 0x01, 0x20 } },
/* Known-length callback body, with a reader error injected part way
through (crc_seed bit 0x80). */
{ "ext-callback-error", { 0x00, 0x08, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x09, 0x00, 0x00, 0x00, 0x02, 0x82 } },
{ "ext-fd-response", { 0x00, 0x08, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x0B, 0x00, 0x00, 0x00, 0x00, 0x10 } },
{ "ext-fd-at-offset-response", { 0x00, 0x08, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x0C, 0x00, 0x00, 0x00, 0x00, 0x10 } },
{ "ext-pipe-response", { 0x00, 0x08, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x0E, 0x00, 0x00, 0x00, 0x00, 0x10 } },
{ "ext-iovec-response", { 0x00, 0x08, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x0F, 0x00, 0x00, 0x00, 0x00, 0x07 } },
/* Empty response plus the header manipulation API (get/del/options). */
{ "ext-empty-response-hdrapi", { 0x00, 0x08, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x02 | 0x30 | 0x40 | 0x80, 0x00, 0x00, 0x00, 0x05, 0x00 } },
/* Digest through the pre-computed-userdigest entry point. */
{ "ext-digest-check-digest3", { 0x00, 0x09, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" },
{ 0, "GET / HTTP/1.1\r\nHost: x\r\nAuthorization: Digest "
"username=\"user\", realm=\"TestRealm\", nonce=\"%%NONCE%%\", "
"uri=\"/\", response=\"00000000000000000000000000000000\"\r\n\r\n" },
P_END, P_END },
{ 0x00, 0x05 | 0x80, 0x00, 0x00, 0x01, 0x00 } },
/* The v1 wrappers and the request-info query. */
{ "ext-digest-v1-wrappers", { 0x00, 0x09, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x00, 0x01 | 0x10, 0x00, 0x00, 0x00, 0x00 } },
{ "ext-digest-request-info", { 0x00, 0x09, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x00, 0x08 | 0x20, 0x00, 0x00, 0x03, 0x00 } },
/* Application-driven event loop: MHD_get_fdset2() + select() +
MHD_run_from_select2(), with the timeout accessors. */
{ "ext-external-event-loop", { 0x00, 0x08, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x01, 0x00, 0x02 | 0x04, 0x00, 0x00, 0x00 } },
/* The v1 fdset/run_from_select pair. */
{ "ext-external-event-loop-v1", { 0x00, 0x08, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x01, 0x00, 0x01 | 0x04, 0x00, 0x00, 0x00 } },
/* Every connection-introspection accessor at once. */
{ "ext-connection-api", { 0x00, 0x08, 0x03, 0x00 },
{ { 0, "GET /path?a=1 HTTP/1.1\r\nHost: x\r\nCookie: a=b\r\n\r\n" },
P_END, P_END, P_END },
{ 0x01, 0x00, 0x08 | 0x10 | 0x20 | 0x40 | 0x80, 0x00, 0x10, 0x00 } },
/* Suspend the connection and resume it from the pump loop. */
{ "ext-suspend-resume", { 0x00, 0x08, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" },
{ 2, "" }, P_END, P_END },
{ 0x01, 0x00, 0x00, 0x02, 0x00, 0x00 } },
/* Two connections parked at the same time (op 3 opens the second
without waiting for the first). The bookkeeping behind suspend
mode 2 has to be a set for this: while it was a single slot the
first connection was forgotten and stayed suspended, and
MHD_stop_daemon() answered with
MHD_PANIC ("called while we have suspended connections"). */
{ "ext-suspend-two-connections", { 0x00, 0x08, 0x03, 0x00 },
{ { 0, "GET /a HTTP/1.1\r\nHost: x\r\n\r\n" },
{ 3, "GET /b HTTP/1.1\r\nHost: x\r\n\r\n" },
{ 2, "" }, P_END },
{ 0x01, 0x00, 0x00, 0x02, 0x00, 0x00 } },
/* HTTP "Upgrade": the client asks for it and the handler answers 101
with an upgrade response, after which the socket is handed over. */
{ "ext-upgrade", { 0x00, 0x08, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\nConnection: Upgrade\r\n"
"Upgrade: fuzz-protocol\r\n\r\n" },
{ 2, "" }, P_END, P_END },
{ 0x01, 0x00, 0x00, 0x04 | 0x08, 0x00, 0x00 } },
/* The remaining response constructors. One seed each, because the
constructor is picked by the low nibble of ext[0] and a seed that
does not name it leaves that entry point unreachable until the
fuzzer happens to mutate the nibble. */
{ "ext-buffer-persistent", { 0x00, 0x08, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 } },
{ "ext-buffer-free-callback", { 0x00, 0x08, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x06, 0x00, 0x00, 0x00, 0x00, 0x00 } },
{ "ext-from-data", { 0x00, 0x08, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x07, 0x00, 0x00, 0x00, 0x00, 0x00 } },
{ "ext-fd64-response", { 0x00, 0x08, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00 } },
/* The v1 basic-auth getter, which returns two separate malloc()ed
strings rather than one struct. */
{ "ext-basic-auth-v1", { 0x00, 0x0a, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n"
"Authorization: Basic dXNlcjpwYXNz\r\n\r\n" },
P_END, P_END, P_END },
{ 0x01, 0x40, 0x00, 0x00, 0x00, 0x00 } },
/* Answer the challenge with the basic-auth responses instead of the
digest ones (fail_variant 3). */
{ "ext-basic-auth-challenge", { 0x00, 0x09, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x01, 0x30, 0x00, 0x00, 0x00, 0x00 } },
{ "ext-basic-auth-challenge-utf8", { 0x00, 0x09, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x01, 0x30, 0x00, 0x00, 0x03, 0x00 } },
/* The MD5-only and algorithm-parametrised pre-computed digest
wrappers. */
{ "ext-digest-check-digest-v1", { 0x00, 0x09, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x01, 0x03, 0x00, 0x00, 0x00, 0x00 } },
{ "ext-digest-check-digest2", { 0x00, 0x09, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" }, P_END, P_END, P_END },
{ 0x01, 0x04, 0x00, 0x00, 0x01, 0x00 } },
/* The username queries. */
{ "ext-digest-get-username-v1", { 0x00, 0x09, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n"
"Authorization: Digest username=\"user\", realm=\"TestRealm\"\r\n\r\n" }
,
P_END, P_END, P_END },
{ 0x01, 0x06, 0x00, 0x00, 0x00, 0x00 } },
{ "ext-digest-get-username3", { 0x00, 0x09, 0x03, 0x00 },
{ { 0, "GET / HTTP/1.1\r\nHost: x\r\n"
"Authorization: Digest userhash=true, username=\"aabb\", "
"realm=\"TestRealm\"\r\n\r\n" },
P_END, P_END, P_END },
{ 0x01, 0x07, 0x00, 0x00, 0x00, 0x00 } }
};
static uint8_t seed_render_buf[4096];
@@ -1541,6 +3038,7 @@ fuzz_seed_get (size_t idx,
b.len = 0;
b.cap = sizeof (seed_render_buf);
sb_raw (&b, sd->cfg, 4);
sb_raw (&b, sd->ext, sizeof (sd->ext));
for (i = 0; i < sizeof (sd->parts) / sizeof (sd->parts[0]); i++)
{
size_t n;