README (48764B)
1 GNU libmicrohttpd -- in-process fuzzing harnesses 2 ================================================= 3 4 This directory contains eight in-process fuzzing harnesses for MHD. All 5 of them are *dual mode*: 6 7 * they export the libFuzzer entry point 8 9 int LLVMFuzzerTestOneInput (const uint8_t *data, size_t size); 10 11 so the very same source can be linked with clang/libFuzzer, AFL++ or 12 OSS-Fuzz, and 13 14 * they ship a **built-in standalone driver** (`fuzz_common.h`) with a 15 deterministic, seeded generator + mutator loop, so they are useful 16 with nothing but gcc and `-fsanitize=address,undefined`. 17 18 The standalone driver is compiled unless `FUZZ_NO_MAIN` is defined. 19 20 21 ------------------------------------------------------------------- 22 1. The harnesses 23 ------------------------------------------------------------------- 24 25 fuzz_request.c the flagship. Feeds arbitrary bytes into a real 26 `struct MHD_Daemon` through a `socketpair()`, 27 using MHD_USE_NO_LISTEN_SOCKET + 28 MHD_add_connection() and external polling 29 (MHD_run()). Everything runs in one thread, so 30 the harness is deterministic and fast (~20k 31 requests/s under ASAN+UBSAN). 32 33 fuzz_str.c direct fuzzing of the string primitives in 34 src/microhttpd/mhd_str.c. Every output buffer is 35 malloc()ed at *exactly* the documented size so 36 that ASAN's redzone catches a one-byte overrun. 37 38 fuzz_auth_header.c direct fuzzing of the "Authorization:" header 39 parsers, MHD_get_rq_dauth_params_() and 40 MHD_get_rq_bauth_params_() (gen_auth.c), through 41 a minimal fabricated `struct MHD_Connection`. 42 ~250k execs/s. 43 44 fuzz_postprocessor.c fuzzing of MHD_post_process() with random 45 Content-Type (urlencoded / multipart with random 46 boundaries), random post-processor buffer sizes 47 and random chunking of the POST data. 48 49 fuzz_options.c the daemon *configuration* surface. fuzz_request 50 always starts the daemon in one shape; this one 51 lets the input pick the MHD_FLAG bits and the 52 MHD_OPTION array, so the flag validation in 53 MHD_start_daemon(), parse_options_va(), the 54 internal polling thread, the thread pool, 55 epoll/poll/select, the listen socket, the per-IP 56 and per-daemon connection limits and quiesce all 57 become reachable. 58 59 fuzz_eventloop.c the external event loop and the scheduling of the 60 connection life cycle. The input is not a 61 request but a *schedule*: a program of one-byte 62 opcodes interpreted against a live daemon, so the 63 application can poll at adversarial times, ignore 64 the timeout it was given, call 65 MHD_run_from_select() with descriptor sets that 66 do not match what MHD asked for, and 67 suspend/resume across those calls. Byte 3 bit 4 68 also lets it stop the daemon with a connection 69 queued by MHD_add_connection() but never 70 started, which is the only way into 71 new_connection_close_() -- see section 5.5. 72 73 fuzz_memorypool.c direct fuzzing of src/microhttpd/memorypool.c, 74 the per-connection bump allocator. A single 75 mis-computed offset there is a cross-request 76 information leak that ASAN cannot see on its own, 77 because the whole pool is one malloc()ed object; 78 the harness therefore carries its own oracles and 79 knows the red zone size that 80 MHD_ASAN_POISON_ACTIVE adds between two blocks. 81 82 fuzz_tls.c MHD's own TLS plumbing -- the MHD_USE_TLS option 83 surface (HTTPS_MEM_KEY/CERT/TRUST/DHPARAMS, 84 PRIORITIES, CRED_TYPE, KEY_PASSWORD, ALPN, SNI, 85 GNUTLS_PSK_CRED_HANDLER) rather than GnuTLS, 86 which has its own OSS-Fuzz project. Byte 9 bit 7 87 switches on the TLS-PSK scenario, the only route 88 to psk_gnutls_adapter() -- see section 5.5. It 89 needs a TLS backend and is therefore the one 90 harness contrib/oss-fuzz/build.sh does not build 91 (that build is --disable-https). 92 93 Shared code lives in `fuzz_common.h` (header-only, so every harness 94 stays a single translation unit). 95 96 97 ------------------------------------------------------------------- 98 2. Design of fuzz_request 99 ------------------------------------------------------------------- 100 101 2.1 Why a socketpair 102 -------------------- 103 104 MHD_add_connection() accepts any already-connected socket, so a 105 `socketpair(AF_UNIX, SOCK_STREAM)` is enough: no listen socket, no port, 106 no TCP stack, no second thread. The daemon is started with 107 MHD_USE_NO_LISTEN_SOCKET and *without* MHD_USE_INTERNAL_POLLING_THREAD, 108 and the harness pumps it with MHD_run() between sends. A fake 109 127.0.0.1 `struct sockaddr_in` is passed so that per-IP accounting and 110 MHD_get_connection_info() see something sane. 111 112 2.2 Input format 113 ---------------- 114 115 byte 0 connection memory limit selector 116 (index into {default,128,192,256,320,384,512,768,1024, 117 1400,1500,2048,4096,32768}) 118 byte 1 handler behaviour bitmask 119 0x01 call MHD_digest_auth_check3() / 120 MHD_queue_auth_required_response3() 121 0x02 call MHD_basic_auth_get_username_password3() 122 0x04 run the request body through MHD_post_process() 123 0x08 iterate MHD_get_connection_values() over headers, 124 GET arguments, cookies and footers 125 0x10 unused (see 2.2.1) 126 0x20 reply with a larger, copied response body 127 (only meaningful for response kind 0) 128 0x40 reply 403 instead of 200 129 0x80 unused 130 byte 2 low nibble: MHD_OPTION_CLIENT_DISCIPLINE_LVL selector 131 (index into {-3,-2,-1,0,1,2}); 132 high nibble: reserved for MHD_OPTION_SERVER_INSANITY 133 (MHD 1.0.7 only defines MHD_DSC_SANE, so the value is 0) 134 byte 3 digest configuration: bits 0-1 select the algorithm of the 135 401 challenge {SHA-256, MD5, SHA-512-256, SHA-256}, 136 bit 2 selects the QOP, bits 4-5 select MHD_OPTION_NONCE_NC_SIZE 137 byte 4-9 the API-selection block, see 2.2.1 138 byte 10. a sequence of *send segments*. Each segment starts with a 139 little-endian 16 bit header: 140 141 (op << 14) | length length <= 0x3FFF 142 143 op 0 send `length` bytes on the current connection 144 op 1 the payload is the *expected decoded request body* 145 (ground truth for the body oracle, see 2.4); it is 146 not sent 147 op 2 send, then pump the daemon for extra rounds 148 op 3 close the current connection, open a fresh one on 149 the same daemon, then send 150 151 An explicit length encoding (rather than a magic delimiter) is used so 152 that the fuzzer can move a split point without having to invent an 153 escaping scheme. Splitting matters: MHD's parser is incremental and 154 several bugs only appear for particular split points. 155 156 An input shorter than ten bytes is rejected: the configuration block is 157 mandatory, and an input that short has no segment stream either. 158 159 2.2.1 The API-selection block 160 ----------------------------- 161 162 Bytes 4-9 pick which parts of the public API the iteration touches. 163 They are always present. Bytes 0-3 alone decide how MHD *parses* the 164 request; these six decide which of the response constructors, which 165 authentication entry point, which event loop, and which introspection 166 calls run against the parsed result. 167 168 The all-zero setting is the plainest one -- a static two byte buffer 169 response, MHD_run() as the event loop, no suspend, no upgrade, no extra 170 introspection -- so zeroing bytes 4-9 of any input reduces it to the 171 request-parsing-only behaviour that the harness had before these bytes 172 existed. 173 174 These bytes were gated behind bit 0x10 of byte 1 while they were being 175 brought up, so that the corpus predating them kept its byte-exact 176 meaning. The gate is gone; bit 0x10 is left unused rather than 177 reassigned, so that a corpus file written while it was a gate cannot 178 silently change meaning. The reproducers in known-findings/ that predate 179 the change (K1-K6, seven files) were migrated by inserting six zero bytes 180 at offset 4 -- the identity transformation, since the all-zero block is 181 the old behaviour -- and all seven still drive MHD along their recorded 182 paths, verified by comparing the --verbose daemon, handler, body and 183 challenge counts before and after. 184 185 byte 4 response construction 186 bits 0-3 which constructor to use: 187 0 buffer_static 1 buffer_copy 2 empty 188 3 buffer/PERSISTENT 4 buffer/MUST_FREE 5 buffer/MUST_COPY 189 6 buffer_with_free_callback 7 data (copy) 190 8 data (free) 9 callback, known length 191 10 callback, MHD_SIZE_UNKNOWN (chunked reply) 192 11 fd 12 fd_at_offset 13 fd64 193 14 pipe 15 iovec 194 bits 4-5 number of response headers to add (0-3) 195 bit 6 also add a response footer (a chunked trailer) 196 bit 7 exercise MHD_get_response_header(), 197 MHD_get_response_headers(), 198 MHD_del_response_header() and 199 MHD_set_response_options() 200 byte 5 authentication entry point 201 bits 0-3 0 check3 (as before) 1 check 202 2 check2 3 check_digest 203 4 check_digest2 5 check_digest3 204 6 get_username 7 get_username3 205 8 get_request_info3 9 as 0 206 bits 4-5 challenge variant: 0 queue_auth_required_response3, 207 1 queue_auth_fail_response, 208 2 queue_auth_fail_response2, 209 3 the basic-auth pair 210 bit 6 use the v1 MHD_basic_auth_get_username_password() 211 bit 7 also call the connection-less digest helpers 212 byte 6 event loop and introspection 213 bits 0-1 0 MHD_run(), 1 MHD_get_fdset()+run_from_select(), 214 2 and 3 the *2 variants 215 bit 2 query all four MHD_get_timeout*() forms 216 bit 3 MHD_lookup_connection_value(), 217 MHD_lookup_connection_value_n(), 218 MHD_get_connection_URI_path_n() 219 bit 4 MHD_set_connection_value() 220 bit 5 MHD_get_connection_info() / MHD_get_daemon_info() 221 bit 6 MHD_set_connection_option() 222 bit 7 MHD_quiesce_daemon() before stopping 223 byte 7 suspend and upgrade 224 bits 0-1 0 none, 1 suspend+resume in the handler, 225 2 suspend and resume from the pump loop, 226 3 as 1 227 bit 2 allow HTTP "Upgrade" (only acted on when the 228 request really asks for one, see below) 229 bits 3-4 which MHD_upgrade_action() to issue first 230 byte 8 seed picking the response header names and values, and the 231 MHD_RF_* flags for MHD_set_response_options() 232 byte 9 seed for the content-reader callback: body length, block 233 size, and whether it fails part way through 234 235 Two constraints on the harness are worth stating, because both are 236 application-contract requirements rather than things worth fuzzing, and 237 violating either makes MHD abort on input that is perfectly legal: 238 239 * the connection is only suspended when the handler is about to return 240 MHD_YES. Returning MHD_NO asks MHD to terminate the connection, and 241 terminating one that the same callback just suspended trips 242 mhd_assert (! connection->suspended) in MHD_connection_close_(); 243 244 * a 101 response is only queued for a request that is actually an 245 upgrade request (HTTP/1.1, an "Upgrade" header, and a "Connection" 246 header naming the upgrade token and not "close"). All three come 247 off the wire, so the path stays attacker-driven. 248 249 This is exactly the check a real application can perform, and no 250 more. It deliberately does not try to predict whether MHD will 251 accept the upgrade: a request that passes it used to abort MHD, which 252 is finding K7 in section 6, and MHD_queue_response() now answers 253 MHD_NO for that case instead. Do not tighten the condition to keep 254 the suite green -- an application cannot do better than this, so 255 neither should the harness. 256 257 The message-framing headers (Content-Length, Transfer-Encoding) are 258 deliberately absent from the response-header table: MHD generates them 259 itself from the response object, so an application that also sets them 260 by hand is lying to the library about its own body. 261 MHD_RF_INSANITY_HEADER_CONTENT_LENGTH is the sanctioned way to explore 262 that corner and is reachable through bit 7 of byte 4. 263 264 2.3 The %%NONCE%% placeholder 265 ----------------------------- 266 267 Interesting parts of digestauth.c are only reached *after* the client 268 presents a nonce that MHD itself generated. A stateless fuzzer can 269 never guess one. Therefore the harness rewrites the literal ASCII token 270 271 %%NONCE%% 272 273 inside a segment, at send time, into the most recent `nonce="..."` value 274 seen in a response from the daemon. A generated (or hand-written) input 275 can thus be: 276 277 request 1: GET /a -> handler calls MHD_digest_auth_check3(), 278 gets MHD_DAUTH_WRONG_HEADER and replies 279 401 + WWW-Authenticate: Digest ... nonce="X" 280 op 3: new connection 281 request 2: GET /a with Authorization: Digest ... nonce="%%NONCE%%" 282 283 which walks all the way into the 'response' comparison. Without this 284 the over-long `response=` stack overflow (see 5.4) is unreachable. 285 286 2.4 Oracles 287 ----------- 288 289 Memory errors are caught by ASAN/UBSAN and aborts by the signal 290 handlers. In addition fuzz_request installs two behavioural oracles: 291 292 a) MHD_set_panic_func() -- any MHD_PANIC() reached from network input 293 is a finding (a remote abort), not a legitimate "API violation". 294 295 b) A request-body oracle. Framing bugs (chunked transfer coding, 296 Content-Length) do not corrupt memory, they corrupt *data*, which 297 is exactly what HTTP request smuggling exploits. The input can 298 therefore declare the expected decoded body in an `op 1` segment. 299 Every byte that MHD hands to the application must be the next 300 expected byte, and when MHD completes the request the delivered 301 body must be complete. MHD is free to reject the request at any 302 point -- only what it *does* deliver is checked. 303 304 The declaration is honoured only for un-mutated inputs (the driver 305 exposes this as `fuzz_pristine`), because a random mutation would 306 of course invalidate the ground truth. 307 308 2.5 The generator 309 ----------------- 310 311 Purely random bytes essentially never form a valid HTTP request, so the 312 standalone driver uses a small HTTP grammar (`fuzz_generate()`), and 313 then optionally applies byte-level mutations on top. Shapes: 314 315 0 SHAPE_PLAIN random method/target/version + headers 316 1 SHAPE_NOHDR_QARG *no header lines at all* plus a trailing 317 query argument without '=' (this is the 318 exact shape needed for the read-buffer 319 shift-back bug; the generator also forces a 320 small connection memory pool for it) 321 2 SHAPE_CL_BODY Content-Length body + body oracle 322 3 SHAPE_CHUNKED chunked body with chunk extensions 323 (";ext", ";ext=val", ";ext=\"quoted\"", 324 ";a=1;b=2;c") + trailers + body oracle 325 4 SHAPE_DIGEST_SIMPLE Authorization: Digest with a randomised 326 parameter set, including unknown 327 `algorithm=` tokens and `response=` values 328 of every length up to 128 hex digits 329 5 SHAPE_DIGEST_REPLAY the two-request nonce handshake of 2.3 330 6 SHAPE_BASIC Authorization: Basic with random base64 331 7 SHAPE_POST_FORM urlencoded / multipart POST bodies 332 8 SHAPE_WEIRD folded headers, bare CR, bare LF, 333 whitespace before the colon, percent 334 encoding, absolute-form targets, ... 335 336 `MHD_FUZZ_SHAPE=<n>` restricts the generator to a single shape, which is 337 very handy for triage and for regression-testing a specific past bug. 338 339 340 ------------------------------------------------------------------- 341 3. Running the harnesses 342 ------------------------------------------------------------------- 343 344 Build (gcc only, no clang required): 345 346 SRC=/path/to/libmicrohttpd # configured build tree 347 gcc -g -O1 -Wall -Wextra \ 348 -fsanitize=address,undefined -fno-sanitize-recover=all \ 349 -I$SRC -I$SRC/src/include -I$SRC/src/microhttpd -I$SRC/src/fuzz \ 350 -o fuzz_request $SRC/src/fuzz/fuzz_request.c \ 351 $SRC/src/microhttpd/.libs/libmicrohttpd.a -lpthread 352 353 (the same command line for fuzz_str, fuzz_auth_header and 354 fuzz_postprocessor; the static archive is required because fuzz_str and 355 fuzz_auth_header use functions that are hidden in the shared object). 356 357 Options of the built-in driver (identical for all harnesses): 358 359 --iterations=N number of generate/mutate iterations [3000] 360 --seed=N PRNG seed; (harness, seed) fully determines a run 361 --corpus-dir=DIR replay every regular file in DIR and exit 362 --file=PATH replay a single input and exit (crash repro) 363 --crash-dir=DIR where reproducers are written [crashes] 364 --timeout=SEC per-iteration watchdog, 0 disables [20] 365 --write-corpus=DIR dump the built-in seed corpus to DIR 366 --skip-seeds do not replay the built-in corpus first 367 --verbose enable MHD's error log + print statistics 368 --help 369 370 Environment variables (all optional): 371 372 MHD_FUZZ_ITERATIONS, MHD_FUZZ_SEED, MHD_FUZZ_TIMEOUT, 373 MHD_FUZZ_CRASH_DIR, MHD_FUZZ_VERBOSE, MHD_FUZZ_SKIP_SEEDS 374 375 MHD_FUZZ_SHAPE=<n> (fuzz_request) restrict the generator 376 to one grammar shape 377 MHD_FUZZ_MIN_DISCIPLINE=<n> (fuzz_request) lower bound for 378 MHD_OPTION_CLIENT_DISCIPLINE_LVL, 379 default -3 (the full range) 380 MHD_FUZZ_MIN_MEM_LIMIT=<n> (fuzz_request) lower bound for 381 MHD_OPTION_CONNECTION_MEMORY_LIMIT, 382 default 0 (the full range) 383 MHD_FUZZ_MODEL_DIGEST_SINK=1 (fuzz_str) enable the modelled 384 digest 'response' call site, see 5.4 385 386 Typical use: 387 388 # quick smoke test (a couple of seconds) 389 ./fuzz_request 390 391 # a real session 392 ./fuzz_request --iterations=5000000 --seed=$RANDOM 393 394 # regression: replay the whole checked-in corpus 395 ./fuzz_request --corpus-dir=corpus 396 ./fuzz_str --corpus-dir=corpus # ignores foreign files gracefully 397 398 # reproduce a crash 399 ./fuzz_request --file=crashes/crash-fuzz_request-seed3-iter55.bin 400 401 402 ------------------------------------------------------------------- 403 4. Reproducing a failure 404 ------------------------------------------------------------------- 405 406 Whenever the process dies -- ASAN error, UBSAN error, `mhd_assert()`, 407 MHD_PANIC(), a body-oracle finding, or the watchdog -- the input of the 408 running iteration is written to 409 410 $MHD_FUZZ_CRASH_DIR/crash-<harness>-seed<S>-iter<N>.bin 411 412 and a line is printed telling you the harness, the seed and the 413 iteration. The dump is produced from 414 415 * `__sanitizer_set_death_callback()` (weakly linked; present whenever 416 the binary is built with ASAN), and 417 * SIGABRT/SIGSEGV/SIGBUS/SIGILL/SIGFPE/SIGALRM handlers, 418 419 using only async-signal-safe calls. Replay with `--file=...`; the run 420 is fully deterministic, so `--seed=S --iterations=N+1` reproduces the 421 whole session as well. 422 423 424 ------------------------------------------------------------------- 425 5. What these harnesses find (regression coverage) 426 ------------------------------------------------------------------- 427 428 The four vulnerabilities fixed in MHD 1.0.7+1 are all rediscovered from 429 scratch. Each has a dedicated seed in `corpus/`, and the generator 430 finds each of them on its own within a few thousand iterations. 431 432 5.1 digestauth.c: unknown `algorithm=` token -> MHD_PANIC() 433 An `algorithm=` token MHD does not know parses to 434 MHD_DIGEST_AUTH_ALGO3_INVALID, which is 0, so the allow-mask test 435 `c_algo == (c_algo & malgo3)` passes for *any* mask; the code then 436 calls digest_init_one_time() with an invalid algorithm and panics. 437 Found by: SHAPE_DIGEST_SIMPLE / SHAPE_DIGEST_REPLAY, the panic hook, 438 corpus seed `digest-unknown-algorithm`. 439 440 5.2 connection.c get_req_headers(): read-buffer shift-back underflow 441 Needs, all at once: a small MHD_OPTION_CONNECTION_MEMORY_LIMIT 442 (MHD_BUF_INC_SIZE (1500) > read_buffer_size), *no header lines*, and 443 a trailing query argument without '=' (whose `value` is NULL). 444 Found by: SHAPE_NOHDR_QARG, corpus seeds 445 `small-pool-trailing-query-arg[-2]`. 446 447 5.3 connection.c process_request_body(): chunk-extension CRLF 448 `chunk_size_line_len = i` instead of `i + 2` leaves the CRLF of the 449 chunk-size line in the stream, so the following chunk data is 450 shifted -- a body desync, i.e. a request-smuggling primitive. This 451 corrupts no memory, so it is caught by the body oracle (2.4). 452 Found by: SHAPE_CHUNKED, corpus seeds `chunked-with-extensions`, 453 `chunked-split`. 454 455 5.4 digestauth.c: over-long `response=` -> stack buffer overflow 456 `response` was accepted up to `digest_size * 4` characters (128 for 457 SHA-256) and then decoded with MHD_hex_to_bin() into 458 `uint8_t hash1_bin[MAX_DIGEST]` (32 bytes) -- up to 64 bytes 459 written, 32 bytes of stack smashed. Reaching it requires a *valid* 460 nonce, hence the %%NONCE%% mechanism of 2.3. 461 Found by: SHAPE_DIGEST_REPLAY, corpus seed 462 `digest-overlong-response`. 463 464 fuzz_str additionally reproduces the underlying primitive: 465 MHD_hex_to_bin() has no output-size parameter and writes len/2 466 bytes, so any caller with a fixed-size buffer must bound the input 467 length itself. `MHD_FUZZ_MODEL_DIGEST_SINK=1` enables a target that 468 replays exactly the pre-fix call site (32 byte heap buffer, input 469 length bounded only by 4 * 32) and ASAN reports the overflow 470 immediately. The target models a *caller*, not the library, so it 471 is off by default. 472 473 5.5 Code that only a deliberate scenario reaches 474 A coverage-guided engine gets to a branch by mutating towards it, so 475 it never gets to one that needs several unrelated configuration 476 bytes to be right at the same time: the intermediate inputs score no 477 better than the ones around them, and the fuzzer has no gradient to 478 follow. An 8 hour, 12 core, 1.6 billion execution campaign, on top 479 of the whole test suite, left exactly three library functions at zero 480 coverage for that reason. All three now have a scenario: 481 482 psk_gnutls_adapter() (daemon.c) -- MHD's only piece of TLS code that 483 takes a buffer straight from an application callback. Needs 484 GNUTLS_CRD_PSK, a PSK-capable priority string *on both ends* 485 ("NORMAL" has none), MHD_OPTION_GNUTLS_PSK_CRED_HANDLER, and a 486 client offering a PSK identity. fuzz_tls byte 9 bit 7 switches all 487 four on together; bits 3-5 then pick which arm of the adapter to 488 exercise (missing callback, failing callback, 4 KiB key, key length 489 taken from the identity the client sent, size above UINT_MAX, and 490 the two sizes below the MHD_PSK_MIN_SIZE the adapter enforces -- 491 zero and one byte short, which together with the exactly-minimum 492 PSK_OK bracket that check from both sides). Seeds `psk-*`. 493 494 new_connection_close_() (daemon.c) -- frees a connection that was 495 accepted but never started. On a thread-safe daemon (the default, 496 including a pure external event loop) MHD_add_connection() only 497 queues the socket; the connection object is built by the next run. 498 The function is reachable only if the daemon is stopped before that 499 run happens, and every harness used to call MHD_run() once more 500 during teardown. fuzz_eventloop byte 3 bit 4 queues one connection 501 after the teardown and stops without running. Seeds 502 `stop-with-queued-connection`, `stop-with-queued-after-quiesce`. 503 It is not a race: no second thread is involved. 504 505 MHD_check_response_header_token_ci() (response.c) -- this one was 506 not a fuzzing problem. The function was added in 2017 and never 507 called, so no input could reach it. The single place that wants it, 508 the "Connection: upgrade" check in MHD_queue_response(), open-coded 509 the scan against response->first_header instead; that now calls the 510 helper, and the existing `ext-upgrade` seed and K7 cover it. 511 512 513 ------------------------------------------------------------------- 514 6. Findings against MHD 1.0.7 - all fixed, kept as regressions 515 ------------------------------------------------------------------- 516 517 Running these harnesses against v1.0.7 built with `--enable-asserts` 518 reported the following *additional* issues on top of the four 519 vulnerabilities of section 5. All of them were `mhd_assert()`s reachable 520 from network input, i.e. a remote abort in builds that keep assertions 521 enabled, and all of them are fixed on master. They are documented here 522 because the reproducers are kept as a regression corpus: a failure of one 523 of them means the corresponding fix has been undone. Byte-exact reproducers are in `corpus/known-findings/`; replay 524 one with 525 526 ./fuzz_request --file=corpus/known-findings/K1-digest-empty-realm.bin 527 528 K1 digestauth.c:2467 is_param_equal(): 529 mhd_assert (0 != param->value.len) -> fixed in 300a2ab0 530 Trigger (default daemon configuration!), one request: 531 GET /a HTTP/1.1 532 Host: x 533 Authorization: Digest username="user", realm="", nonce="0000", 534 uri="/a", response="00" 535 digest_auth_check_all_inner() rejects a *missing* realm/username but 536 not an *empty* one, so a zero-length parameter reaches 537 is_param_equal(), whose documented precondition is a non-empty 538 value. Requires only that the application calls 539 MHD_digest_auth_check3(). Real defect (missing validation). 540 Repro: corpus/known-findings/K1-digest-empty-realm.bin 541 542 K2 connection.c:3582 handle_recv_no_space(): 543 mhd_assert ((MHD_PROC_RECV_BODY_CHUNKED != stage) || 544 ! c->rq.some_payload_processed) -> fixed in 68c83f22 545 Trigger: small MHD_OPTION_CONNECTION_MEMORY_LIMIT (<= ~400 bytes) 546 plus a chunked body whose *second* chunk-size line carries a chunk 547 extension that does not fit into the remaining read buffer. The 548 flag reflects the last application callback only and survives later 549 reads, so the assertion is over-strong; the code below it already 550 handles the situation. Stale assertion. 551 Repro: corpus/known-findings/K2-chunkext-no-space.bin 552 553 K3 connection.c:2881 transmit_error_response_len(): 554 mhd_assert (! connection->stop_with_error) -> fixed in e04eb218 555 Trigger: small connection memory pool plus an over-long, unterminated 556 chunk extension: 557 POST /a HTTP/1.1 / Transfer-Encoding: chunked 558 d;ext="qqqqqqqq... (longer than the read buffer) 559 handle_req_chunk_size_line_no_space() is missing a `return` after it 560 has already queued the "chunk extension too big" response. Real 561 defect: in a release build the second call forces the connection to 562 MHD_CONNECTION_CLOSED and the 413 response is never sent. 563 Repro: corpus/known-findings/K3-chunkext-stop-with-error.bin 564 565 K4 connection.c:6099 get_req_header(): 566 mhd_assert ((0 == c->rq.hdrs.hdr.value_start) || 567 (0 != c->rq.hdrs.hdr.name_len)) -> fixed in 0b750975 568 Trigger a) MHD_OPTION_CLIENT_DISCIPLINE_LVL <= -1, first header line 569 starting with whitespace: 570 GET /a HTTP/1.1\r\n Host: x\r\n\r\n 571 Trigger b) MHD_OPTION_CLIENT_DISCIPLINE_LVL <= -2, empty header 572 name: 573 GET /a HTTP/1.1\r\n: value\r\nHost: x\r\n\r\n 574 Both shapes are explicitly allowed by those discipline levels. 575 Stale assertion. 576 Repro: corpus/known-findings/K4a-wsp-first-header.bin, 577 corpus/known-findings/K4b-empty-header-name.bin 578 579 K5 connection.c:6394 get_req_header(): 580 mhd_assert ('\r' != chr) -> fixed in 6fcdfd43 581 Trigger: MHD_OPTION_CLIENT_DISCIPLINE_LVL = -3, which sets 582 `bare_cr_keep = true`; the branch that keeps a bare CR falls through 583 into the "not a whitespace, not the end of the line" arm whose 584 assertion predates that mode. 585 GET /a HTTP/1.1\r\nHost: x\r\nX: y\r\r\n\r\n 586 Stale assertion. 587 Repro: corpus/known-findings/K5-bare-cr-keep.bin 588 589 K6 digestauth.c:860 check_nonce_nc(): 590 mhd_assert (0 == nn->nonce[noncelen]) 591 The nonce-nc slot array is indexed by a hash of the nonce, but the 592 slot content is compared assuming the *stored* nonce has the same 593 length as the presented one. A client can therefore make MHD read 594 the terminator of a nonce at the wrong offset by presenting a nonce 595 whose length belongs to a different digest algorithm. 596 Note that this one is *timing dependent*: the nonce carries a 597 millisecond timestamp and whether it counts as stale depends on the 598 wall clock, so the same input reproduces only in a fraction of the 599 replays (about 1 in 30 for the corpus file below). Replay it in a 600 loop. 601 Trigger (MHD_OPTION_NONCE_NC_SIZE = 1 makes every nonce land in slot 602 0, which turns the collision into a certainty; larger arrays only 603 need more attempts): 604 request 1: GET /a -> 401 with a SHA-256 nonce 605 (76 chars) stored in the slot 606 request 2: Authorization: Digest username="user", 607 realm="TestRealm", nonce="<44 zeros>", uri="/a", 608 response="e" 609 (no algorithm parameter -> MD5 -> nonce length 44, 610 all-zero timestamp -> not stale) 611 The generator needs ~150k iterations to hit it on its own. 612 Repro: corpus/known-findings/K6-nonce-length-collision.bin 613 614 K7 connection.c:2596 build_header_response(): 615 mhd_assert ((NULL == r->upgrade_handler) || 616 (MHD_CONN_MUST_UPGRADE == c->keepalive)) 617 Fixed by commit acef58a0. Was driven entirely from the wire, with 618 default daemon options. 619 620 While parsing the request headers, connection.c sets 621 c->keepalive = MHD_CONN_MUST_CLOSE 622 in three places for requests whose message framing it distrusts: 623 624 4988 two "Content-Length" headers with different values 625 (client discipline -3 only) 626 5012 "Transfer-Encoding" on an HTTP/1.0 request 627 (client discipline <= 0) 628 5051 "Content-Length" *and* "Transfer-Encoding: chunked" in the 629 same request -- no discipline gate at all, so this one 630 fires with the default configuration 631 632 In every case the request is then handed to the access handler as a 633 normal request. If the handler answers 101 with a response from 634 MHD_create_response_for_upgrade(), MHD_queue_response() accepts it: 635 it checks MHD_ALLOW_UPGRADE, the status code, the "Connection" 636 header and the HTTP version, but not whether the connection can 637 still be kept open. keepalive_possible() then returns 638 MHD_CONN_MUST_CLOSE from its very first test, which is placed 639 *before* the upgrade branch, and the assertion above fires while the 640 reply header is being built. 641 642 The application cannot defend itself. The harness checks the 643 request the way an application would -- HTTP/1.1, an "Upgrade" 644 header, and a "Connection" header naming the upgrade token and not 645 "close" -- and the reproducer satisfies all three. MHD's decision 646 is not exposed through any public accessor. 647 648 It needs --enable-asserts and an application that answers upgrade 649 requests, so it is a robustness defect rather than a memory-safety 650 one; but it is reachable from a single well-formed request against a 651 default-configured server, which is a good deal worse than the 652 "application misuse" it first looked like. 653 654 The fix rejects the response in MHD_queue_response() 655 (connection.c:8338) next to the other upgrade preconditions, so the 656 application gets MHD_NO -- which it already has to handle -- instead 657 of an abort. 658 Repro: corpus/known-findings/K7-upgrade-after-must-close.bin 659 GET / HTTP/1.1 660 Host: x 661 Connection: Upgrade 662 Upgrade: fuzz-protocol 663 Content-Length: 0 664 Transfer-Encoding: chunked 665 (then "0\r\n\r\n") 666 667 K8 digestauth.c MHD_digest_auth_check_digest2(): 668 MHD_PANIC ("Wrong 'malgo3' value, only one base hashing 669 algorithm ... must be specified, API violation") 670 Fixed by commit 07c051dd. An API-contract defect rather than a 671 parsing bug: unlike K7 the offending argument came from the 672 application, not from the network. 673 674 MHD_DIGEST_ALG_AUTO is a documented member of 675 enum MHD_DigestAuthAlgorithm, and the `algo` parameter of 676 MHD_digest_auth_check_digest2() is documented only as "digest 677 algorithms allowed for verification". But the function maps AUTO to 678 MHD_DIGEST_AUTH_MULT_ALGO3_ANY_NON_SESSION and forwards it to 679 MHD_digest_auth_check_digest3(), which requires *exactly one* base 680 hashing algorithm because the caller supplies a pre-computed digest 681 of one specific length -- and aborts through MHD_PANIC() when more 682 than one is named. 683 684 So MHD_digest_auth_check_digest2 (..., MHD_DIGEST_ALG_AUTO) 685 deterministically kills the process. The same is not true of 686 MHD_digest_auth_check2(), which takes a password rather than a 687 digest and whose AUTO handling is fine. 688 689 Unlike K7 this is entirely under the application's control -- no 690 network input is involved -- so it is a usability trap rather than a 691 robustness bug: AUTO simply cannot be used with the 692 pre-computed-digest entry points, and no mapping can rescue it, 693 because the digest length does not identify the algorithm (SHA-256 694 and SHA-512/256 digests are both 32 bytes). 695 696 The fix documents the restriction in microhttpd.h and returns MHD_NO 697 instead of aborting. AUTO is left unusable with the 698 pre-computed-digest entry points, deliberately: there is nothing to 699 map it to. 700 Repro: none committed. The argument is the application's, so the 701 harness never passes AUTO there (see run_digest_check() in 702 fuzz_request.c); to see it, change the MHD_DIGEST_ALG_* argument of 703 the variant-4 call to MHD_DIGEST_ALG_AUTO and replay 704 corpus/fuzz_request-37.bin. 705 706 K9 postprocessor.c:1119 post_process_multipart(): 707 LeakSanitizer: direct leak, strdup() from MHD_post_process() 708 Found by fuzz_postprocessor, not fuzz_request -- the first finding 709 that did not come from the daemon harness. Remotely triggerable, 710 default configuration, no assertions needed. 711 712 On entering PP_PerformCheckMultipart the code did 713 714 pp->nested_boundary = strstr (pp->content_type, "boundary="); 715 ... 716 pp->nested_boundary = strdup (&pp->nested_boundary[9]); 717 718 The first assignment stores an *interior pointer into 719 pp->content_type* over whatever pp->nested_boundary held. If the 720 post processor already owned a boundary -- which it does for every 721 nested "multipart/mixed" part after the first, unless the state 722 machine happened to pass through PP_PerformCleanup in between -- that 723 allocation is lost. MHD_destroy_post_processor() frees only the last 724 one. 725 726 So a body with N nested multipart/mixed parts, each carrying its own 727 "boundary=", leaks N-1 blocks, and the client picks how long each one 728 is. That makes it a memory-exhaustion vector against any application 729 that calls MHD_post_process() on multipart input, not the one byte 730 the reproducer happens to leak (its boundary is the empty string). 731 732 Fixed by copying into a local first and releasing any previously 733 owned boundary before taking ownership of the new one; the error 734 path no longer overwrites the old pointer either. 735 Repro: corpus/known-findings/K9-fuzz_postprocessor-nested-boundary-leak.bin 736 737 K10 memorypool.c MHD_pool_deallocate(): end block of an exactly-full 738 pool was never returned; the front/end dispatch tested pool->pos, 739 where the two ranges meet. Fixed. 740 741 K11 memorypool.c MHD_pool_reallocate(): returned non-NULL for a 742 wrapping @a new_size, handing the caller a block it believed was 743 nearly SIZE_MAX bytes long. Fixed. 744 745 K12 daemon.c MHD_start_daemon_va(): leaked the GnuTLS DH parameters 746 built from MHD_OPTION_HTTPS_MEM_DHPARAMS on every startup-failure 747 exit. The application cannot free them. Fixed. 748 749 K13 daemon.c MHD_epoll() / MHD_quiesce_daemon(): both threads remove 750 the listening socket from the epoll set, and the loser aborts on 751 ENOENT. Open; two alternative fixes are proposed as 752 ../../patches/K13.diff (tolerate ENOENT everywhere) and 753 ../../patches/K13b.diff (take a lock, keep the strict check). 754 755 K14 daemon.c:1358 call_handlers(): 756 mhd_assert (! force_close || MHD_CONNECTION_CLOSED == con->state) 757 Open, fix proposed in ../../patches/K14.diff. 758 759 MHD_connection_handle_read() closes the connection when its 760 @a socket_error argument is set -- but only once it gets far enough 761 to try. It returns early, leaving the state untouched, when the 762 connection is suspended, when a TLS handshake is in progress, and 763 when the read buffer has no free space at all. The caller asserts 764 the post-condition unconditionally. 765 766 Instrumenting the assertion site with the reproducer gives 767 768 state=12 (HEADERS_PROCESSED) suspended=0 769 rb_size=2021 rb_off=2021 evinfo=READ 770 771 i.e. the read buffer is exactly full. A client that fills the 772 connection memory pool without the application consuming the body 773 gets there; no unusual API use is involved. Observed through 774 MHD_run_from_select2(), i.e. from the external event loop. 775 776 The reproducer does NOT fire on a plain in-tree gcc build: ASan's 777 pool redzones change the buffer arithmetic enough to matter, so 778 confirm it against the OSS-Fuzz build configuration. 779 Repro: corpus/known-findings/K14-fuzz_eventloop-force-close-not-closed.bin 780 781 K15 daemon.c:9201 close_all_connections(): 782 mhd_assert (MHD_D_IS_USING_THREADS_ (daemon)) 783 Fixed; found while giving new_connection_close_() its first coverage 784 (section 5.5). 785 786 Three places describe the same invariant and one of them disagreed. 787 internal_add_connection() queues a connection when 788 `external_add && MHD_D_IS_THREAD_SAFE_(daemon)`, and 789 new_connections_list_process_() asserts MHD_D_IS_THREAD_SAFE_ -- 790 that is, whenever the daemon was not started with 791 MHD_USE_NO_THREAD_SAFETY, which includes every external event loop. 792 The shutdown drain asserted the much stronger 793 MHD_D_IS_USING_THREADS_ instead, i.e. that the daemon has an 794 internal polling thread. 795 796 So an application that drives an external event loop, calls 797 MHD_add_connection() and then MHD_stop_daemon() before the next 798 MHD_run() aborts on an --enable-asserts build. No threads and no 799 race are involved, and a build with NDEBUG handles the same sequence 800 correctly, which is why nothing had noticed: the list is exactly what 801 holds the connection at that point and new_connection_close_() frees 802 it properly. 803 804 The assert was the outlier and now matches the other two sites. The 805 opposite reading -- that an external event loop should not queue at 806 all and internal_add_connection() should test 807 MHD_D_IS_USING_THREADS_ -- would change MHD_add_connection()'s 808 threading contract, since queueing is what makes that call safe from 809 another thread on a daemon with no thread of its own. 810 Repro: corpus/known-findings/K15-fuzz_eventloop-stop-with-queued-connection.bin 811 812 Status 813 ------ 814 815 K1-K12 and K15 are fixed on master: 816 817 K1 300a2ab0 K4a 0b750975 K7 acef58a0 K10 (memorypool) 818 K2 68c83f22 K4b 0b750975 K8 07c051dd K11 (memorypool) 819 K3 e04eb218 K5 6fcdfd43 K9 (postproc) K12 (daemon/TLS) 820 K6 f438804c K15 (daemon) 821 822 K13 and K14 are open, with proposed fixes in ../../patches/. Until K14 823 lands, `make check-corpus` fails on an `--enable-asserts` build, because 824 its reproducer is committed and still reproduces -- which is the 825 documented behaviour for an open finding, but worth knowing before a CI 826 run. 827 828 The reproducers in `corpus/known-findings/` otherwise all replay clean 829 on a build configured with `--enable-asserts`, and 830 `make check-corpus` asserts exactly that -- it replays that directory 831 along with the generated corpus. K8 has no reproducer, because its 832 trigger is an argument the application chooses rather than anything that 833 comes off the wire. 834 835 A reproducer belongs to the harness that found it, and says so in its 836 name: `K<n>-<harness>-<what>.bin`. The ones without a harness in the 837 name predate the convention and are all fuzz_request inputs. 838 `contrib/oss-fuzz/make_seed_corpus.sh` routes each one into the right 839 target's seed corpus on that basis -- a fuzz_postprocessor reproducer in 840 fuzz_request's corpus would just be an uninteresting input. 841 842 When the next finding is opened, its reproducer goes into 843 `corpus/known-findings/` and will make `make check-corpus` fail until the 844 fix lands. That is the intended behaviour: it is the same arrangement as 845 the `XFAIL_TESTS` entries in `src/microhttpd/Makefile.am`, except that 846 here the expectation is not encoded, so the commit that adds a live 847 reproducer should say so. `contrib/oss-fuzz/make_seed_corpus.sh` has to 848 skip such a reproducer as well, or every ClusterFuzz run starts by 849 rediscovering it; see the comment there. 850 851 The `make check` defaults in Makefile.am 852 853 MHD_FUZZ_ITERATIONS = 50000 854 MHD_FUZZ_MIN_DISCIPLINE = -3 (full range) 855 MHD_FUZZ_MIN_MEM_LIMIT = 0 (full range) 856 857 exercise the whole matrix and take about three seconds in total under 858 ASAN+UBSAN. A real session: 859 860 MHD_FUZZ_MIN_DISCIPLINE=-3 MHD_FUZZ_MIN_MEM_LIMIT=0 \ 861 ./fuzz_request --iterations=1000000 --seed=1 862 863 ------------------------------------------------------------------- 864 7. Building with clang/libFuzzer or AFL++ 865 ------------------------------------------------------------------- 866 867 Both need `-DFUZZ_NO_MAIN` so that the driver's `main()` is left out. 868 869 7.1 libFuzzer 870 ------------- 871 872 # build the library itself with the same instrumentation 873 ./configure --enable-static --disable-shared --enable-asserts \ 874 CC=clang \ 875 CFLAGS="-g -O1 -fsanitize=fuzzer-no-link,address,undefined \ 876 -fno-sanitize-recover=all -fprofile-instr-generate \ 877 -fcoverage-mapping" 878 make -C src/microhttpd 879 880 clang -g -O1 -DFUZZ_NO_MAIN \ 881 -fsanitize=fuzzer,address,undefined -fno-sanitize-recover=all \ 882 -I. -Isrc/include -Isrc/microhttpd -Isrc/fuzz \ 883 -o fuzz_request src/fuzz/fuzz_request.c \ 884 src/microhttpd/.libs/libmicrohttpd.a -lpthread 885 886 mkdir -p CORPUS && ./fuzz_request --help >/dev/null 2>&1 || true 887 ./fuzz_request CORPUS src/fuzz/corpus \ 888 -max_len=8192 -rss_limit_mb=4096 -timeout=20 889 890 # minimise a crash found by libFuzzer 891 ./fuzz_request -minimize_crash=1 -runs=100000 crash-<hash> 892 893 7.2 AFL++ 894 --------- 895 896 export CC=afl-clang-lto AFL_USE_ASAN=1 AFL_USE_UBSAN=1 897 ./configure --enable-static --disable-shared --enable-asserts 898 make -C src/microhttpd 899 900 afl-clang-lto -g -O1 -DFUZZ_NO_MAIN \ 901 -I. -Isrc/include -Isrc/microhttpd -Isrc/fuzz \ 902 -o fuzz_request src/fuzz/fuzz_request.c \ 903 $(afl-config --libdir 2>/dev/null || echo /usr/local/lib/afl)/afl-compiler-rt.o \ 904 /usr/local/lib/afl/libAFLDriver.a \ 905 src/microhttpd/.libs/libmicrohttpd.a -lpthread 906 907 afl-fuzz -i src/fuzz/corpus -o findings -- ./fuzz_request @@ 908 909 (`libAFLDriver.a` provides a `main()` that reads the file named on the 910 command line and calls LLVMFuzzerTestOneInput(); that is why 911 `-DFUZZ_NO_MAIN` is required. With `AFL_LLVM_PERSISTENT` / 912 `__AFL_LOOP` the same binary can be used in persistent mode.) 913 914 7.3 OSS-Fuzz 915 ------------ 916 917 Ready to go: see `../../contrib/oss-fuzz/` and its README. That 918 directory holds the OSS-Fuzz `build.sh`, `project.yaml` and `Dockerfile`, 919 per-harness dictionaries (`dicts/fuzz_*.dict`), per-harness `.options` 920 files (`max_len`, `dict`) and `make_seed_corpus.sh`, which packages 921 `corpus/` — including the `corpus/known-findings/` reproducers of the 922 findings that are already *fixed*, so that they become permanent 923 regressions — into the `<fuzzer>_seed_corpus.zip` files OSS-Fuzz 924 expects. 925 926 `build.sh` configures out of tree with 927 928 --enable-static --disable-shared --with-pic --enable-fuzzing 929 --enable-asserts --disable-https --disable-curl --disable-doc 930 --disable-examples --disable-tools --enable-build-type=neutral 931 932 honouring OSS-Fuzz's `$CFLAGS` (no `--enable-sanitizers`: OSS-Fuzz 933 supplies the instrumentation), and then compiles each harness exactly as 934 in 7.1 but against `$LIB_FUZZING_ENGINE`. HTTPS is off on purpose — the 935 harnesses never speak TLS, and leaving GnuTLS out is what makes the 936 MemorySanitizer build possible. 937 938 Two things to know before reading a report from there: 939 940 * the request-body oracle of 2.4 is **inactive** under libFuzzer. It 941 is gated on `fuzz_pristine`, which nothing sets when `FUZZ_NO_MAIN` 942 is defined — correctly so, since libFuzzer's mutations invalidate the 943 declared ground truth. Pure framing/desync defects (5.3) therefore 944 remain the job of the built-in driver, i.e. of `make check`; 945 * `primary_contact` in `project.yaml` is a placeholder. OSS-Fuzz needs 946 an address the maintainer controls; it has to be filled in before the 947 project can be submitted. 948 949 The token list in `fuzz_common.h` (`fuzz_interesting_str`) is the 950 generator's equivalent of those dictionaries; the two are intentionally 951 similar but are not generated from each other. 952 953 OSS-Fuzz is deliberately not part of `contrib/ci/jobs/`; the bounded 954 in-tree equivalents for CI are `make -C src/fuzz check` and 955 `make -C src/fuzz check-corpus`. 956 957 958 ------------------------------------------------------------------- 959 8. Adding a harness 960 ------------------------------------------------------------------- 961 962 Create `fuzz_<name>.c` with 963 964 #define FUZZ_HARNESS_NAME "fuzz_<name>" 965 #include "fuzz_common.h" 966 967 and implement 968 969 int LLVMFuzzerTestOneInput (const uint8_t *, size_t); 970 static size_t fuzz_generate (struct fuzz_rng *, uint8_t *, size_t); 971 static size_t fuzz_seed_count (void); 972 static const uint8_t *fuzz_seed_get (size_t, size_t *); 973 974 then add it to `check_PROGRAMS` in Makefile.am. Report non-crashing 975 findings with `fuzz_report_finding("...")`, which dumps the reproducer 976 and aborts. 977 978 Two rules learnt the hard way: 979 980 * do not violate documented *preconditions* of the function under 981 test (e.g. MHD_str_remove_token_caseless_() asserts that the token 982 contains no space, tab, comma or NUL) -- otherwise the harness only 983 finds its own bugs; 984 985 * never evaluate a macro argument twice when it contains a PRNG call.