mirror of
https://git.gnunet.org/libmicrohttpd.git
synced 2026-09-25 04:09:31 +03:00
expand fuzzer coverage
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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 @@
|
||||
Basic dXNlcjpwYXNz
|
||||
Basic
|
||||
@@ -1 +1 @@
|
||||
Basic
|
||||
Basic ====
|
||||
@@ -1 +1 @@
|
||||
Basic ====
|
||||
Basic QQ==QQ==
|
||||
@@ -1 +1 @@
|
||||
Basic QQ==QQ==
|
||||
user:TestRealm:pass
|
||||
@@ -0,0 +1 @@
|
||||
"user:TestRealm:pass
|
||||
@@ -0,0 +1 @@
|
||||
Buser:TestRealm:pass
|
||||
@@ -0,0 +1 @@
|
||||
¢user:TestRealm:pass
|
||||
@@ -0,0 +1 @@
|
||||
::
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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.
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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user