test_get_chunked_ext.c (20486B)
1 /* 2 This file is part of libmicrohttpd 3 Copyright (C) 2026 Christian Grothoff 4 5 libmicrohttpd is free software; you can redistribute it and/or modify 6 it under the terms of the GNU General Public License as published 7 by the Free Software Foundation; either version 2, or (at your 8 option) any later version. 9 10 libmicrohttpd is distributed in the hope that it will be useful, but 11 WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 General Public License for more details. 14 15 You should have received a copy of the GNU General Public License 16 along with libmicrohttpd; see the file COPYING. If not, write to the 17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 18 Boston, MA 02110-1301, USA. 19 */ 20 21 /** 22 * @file testzzuf/test_get_chunked_ext.c 23 * @brief Fuzzing testcase for chunked transfers with chunk extensions 24 * @author Christian Grothoff 25 * 26 * This single source builds two programs: 27 * - test_get_chunked_ext: the request is a GET with a chunked body and 28 * MHD answers with a chunked reply that carries a footer, so that both 29 * the request-side chunk parser and the reply-side chunk writer are 30 * exercised; 31 * - test_put_chunked_ext: the request is a PUT with a chunked body and 32 * MHD answers from a static buffer. 33 * 34 * libcurl cannot generate chunk extensions, so the requests are written 35 * by the raw socket client of mhd_zzuf_common.c. The client still talks 36 * to the very same port (and therefore through the very same zzuf/socat 37 * corruption layer) as the libcurl based tests of this directory. 38 * 39 * In addition to the fuzzed traffic, the test runs a small deterministic 40 * self-check of the chunk-extension parser. The self-check talks to MHD 41 * *directly*, bypassing socat, and is therefore not fuzzed at all; it can 42 * only be run in the socat mode (which is the mode that is mandatory when 43 * MHD is built with sanitizers). The self-check verifies that a chunked 44 * body with chunk extensions is decoded byte-exactly; a parser that fails 45 * to consume the CRLF that terminates a chunk-extension line cannot pass 46 * it. 47 */ 48 49 #include "platform.h" 50 #include <curl/curl.h> 51 #include <microhttpd.h> 52 #include <stdlib.h> 53 #include <string.h> 54 55 #include "mhd_zzuf_common.h" 56 57 #define TEST_MAGIC_MARKER 0xFEE1C0DE 58 59 #define EMPTY_PAGE "Empty page." 60 61 /** 62 * The port offset used if the port cannot be auto-detected. 63 */ 64 #define TEST_PORT_OFFSET 145 65 66 /** 67 * The port offset of the (unfuzzed) self-check daemon. 68 */ 69 #define TEST_SELFCHECK_PORT_OFFSET 160 70 71 /** 72 * The maximum size of a request body accepted by this test. 73 */ 74 #define MAX_BODY_SIZE 256 75 76 77 /** 78 * Non-zero for the "put" flavour of this test. 79 */ 80 static int use_put; 81 82 /** 83 * Non-zero if MHD should answer with a chunked reply. 84 */ 85 static int use_chunked_reply; 86 87 88 /** 89 * One chunked request body with (possibly hostile) chunk extensions. 90 */ 91 struct chunk_case 92 { 93 /** 94 * The name of the case; also used as the last path segment of the URI. 95 */ 96 const char *name; 97 98 /** 99 * The chunked body, exactly as it goes on the wire. 100 */ 101 const char *wire; 102 103 /** 104 * The body that MHD is expected to decode, or NULL if the request is 105 * not required to be accepted at all (deliberately malformed input). 106 */ 107 const char *expected; 108 }; 109 110 111 static const struct chunk_case chunk_cases[] = { 112 { "simple", 113 "5;name=value\r\nHELLO\r\n0\r\n\r\n", 114 "HELLO" }, 115 { "flag", 116 "5;flag\r\nHELLO\r\n0\r\n\r\n", 117 "HELLO" }, 118 { "quoted", 119 "5;n=\"a;b=c\"\r\nHELLO\r\n0\r\n\r\n", 120 "HELLO" }, 121 { "multi", 122 "3;a=b;c=d;e\r\nabc\r\n2;x\r\nde\r\n0\r\n\r\n", 123 "abcde" }, 124 { "lastext", 125 "5;a=b\r\nHELLO\r\n0;final=1\r\n\r\n", 126 "HELLO" }, 127 { "trailers", 128 "5;a=b\r\nHELLO\r\n0\r\nX-Trailer: yes\r\nX-Other: 1\r\n\r\n", 129 "HELLO" }, 130 { "extonly", 131 "5;a\r\nHELLO\r\n0;b\r\n\r\n", 132 "HELLO" }, 133 /* The following cases are deliberately malformed: MHD is free to reject 134 them, it just must not misbehave. */ 135 { "wsbefore", 136 "5 ;n=v\r\nHELLO\r\n0\r\n\r\n", 137 NULL }, 138 { "wsaround", 139 "5; n = v \r\nHELLO\r\n0\r\n\r\n", 140 NULL }, 141 { "barelf", 142 "5;a=b\nHELLO\r\n0\r\n\r\n", 143 NULL }, 144 { "extcr", 145 "5;a=b\rHELLO\r\n0\r\n\r\n", 146 NULL }, 147 { "noext", 148 "5;\r\nHELLO\r\n0\r\n\r\n", 149 NULL } 150 }; 151 152 #define NUM_CHUNK_CASES \ 153 (sizeof(chunk_cases) / sizeof(chunk_cases[0])) 154 155 /** 156 * The number of leading entries of #chunk_cases that are well-formed and 157 * therefore usable for the deterministic self-check. 158 */ 159 #define NUM_VALID_CHUNK_CASES 7 160 161 /** 162 * A chunk-size line with a very long extension, built at run time. 163 */ 164 static char *long_ext_body; 165 166 167 /** 168 * The closure of the access handler callback. 169 */ 170 struct ahc_param 171 { 172 /** 173 * Must have #TEST_MAGIC_MARKER value. 174 */ 175 unsigned int magic; 176 177 /** 178 * Non-zero if any error has been encountered. 179 */ 180 unsigned int err_flag; 181 182 /** 183 * Non-zero while the deterministic self-check is running. 184 */ 185 int self_check; 186 187 /** 188 * The body that is expected during the self-check, may be NULL. 189 */ 190 const char *expect_body; 191 192 /** 193 * Set to non-zero by the handler when the expected body has been seen. 194 */ 195 int body_matched; 196 }; 197 198 199 /** 200 * The per-request state. 201 */ 202 struct req_state 203 { 204 /** 205 * The number of bytes stored in @e body. 206 */ 207 size_t body_len; 208 209 /** 210 * Set to non-zero if more data has been received than fits into @e body. 211 */ 212 int overflow; 213 214 /** 215 * The received body. 216 */ 217 char body[MAX_BODY_SIZE]; 218 }; 219 220 221 /** 222 * The closure of the content reader callback. 223 */ 224 struct content_cb_param 225 { 226 /** 227 * The response the callback belongs to, needed to add the footer. 228 */ 229 struct MHD_Response *response; 230 }; 231 232 233 /** 234 * MHD content reader callback for the chunked reply. 235 * 236 * @param cls the closure 237 * @param pos the position in the reply body 238 * @param[out] buf the buffer to fill 239 * @param max the size of @a buf 240 * @return the number of bytes written, or the end-of-stream marker 241 */ 242 static ssize_t 243 content_cb (void *cls, uint64_t pos, char *buf, size_t max) 244 { 245 struct content_cb_param *ccp = (struct content_cb_param *) cls; 246 247 if (pos >= 512) 248 { 249 if (MHD_YES != MHD_add_response_footer (ccp->response, 250 "Footer", "working")) 251 { 252 fprintf (stderr, "MHD_add_response_footer() failed " 253 "at line %d.\n", (int) __LINE__); 254 fflush (stderr); 255 abort (); 256 } 257 return MHD_CONTENT_READER_END_OF_STREAM; 258 } 259 if (max > 128) 260 max = 128; 261 memset (buf, 'A' + (char) (unsigned int) (pos / 128), max); 262 return (ssize_t) max; 263 } 264 265 266 /** 267 * Free the closure of the content reader callback. 268 * 269 * @param ptr the closure to free 270 */ 271 static void 272 content_cb_free (void *ptr) 273 { 274 free (ptr); 275 } 276 277 278 /** 279 * Iterator over the request footers (trailers). 280 * 281 * @param cls the closure 282 * @param kind the kind of the value 283 * @param key the key 284 * @param value the value 285 * @return #MHD_YES to continue the iteration 286 */ 287 static enum MHD_Result 288 footer_iterator (void *cls, 289 enum MHD_ValueKind kind, 290 const char *key, 291 const char *value) 292 { 293 unsigned int *sum = (unsigned int *) cls; 294 295 (void) kind; /* Unused. Mute compiler warning. */ 296 if (NULL != key) 297 *sum += (unsigned int) strlen (key); 298 if (NULL != value) 299 *sum += (unsigned int) strlen (value); 300 return MHD_YES; 301 } 302 303 304 static enum MHD_Result 305 ahc_chunked_ext (void *cls, 306 struct MHD_Connection *connection, 307 const char *url, 308 const char *method, 309 const char *version, 310 const char *upload_data, 311 size_t *upload_data_size, 312 void **req_cls) 313 { 314 struct ahc_param *param = (struct ahc_param *) cls; 315 struct req_state *rs; 316 struct MHD_Response *response; 317 enum MHD_Result ret; 318 unsigned int footer_sum = 0; 319 320 (void) url; (void) method; (void) version; 321 322 if ((NULL == param) || (TEST_MAGIC_MARKER != param->magic)) 323 { 324 fprintf (stderr, "The 'cls' parameter is invalid " 325 "at line %d.\n", (int) __LINE__); 326 fflush (stderr); 327 abort (); 328 } 329 if (NULL == *req_cls) 330 { 331 rs = malloc (sizeof(struct req_state)); 332 if (NULL == rs) 333 return MHD_NO; /* External error */ 334 rs->body_len = 0; 335 rs->overflow = 0; 336 *req_cls = rs; 337 return MHD_YES; 338 } 339 rs = (struct req_state *) *req_cls; 340 341 if ((NULL != upload_data_size) && (0 != *upload_data_size)) 342 { 343 size_t to_copy = *upload_data_size; 344 345 if (to_copy > sizeof(rs->body) - rs->body_len) 346 { 347 to_copy = sizeof(rs->body) - rs->body_len; 348 rs->overflow = 1; 349 } 350 if ((0 != to_copy) && (NULL != upload_data)) 351 { 352 memcpy (rs->body + rs->body_len, upload_data, to_copy); 353 rs->body_len += to_copy; 354 } 355 *upload_data_size = 0; /* All data have been processed */ 356 return MHD_YES; 357 } 358 359 /* The request is complete. */ 360 (void) MHD_get_connection_values (connection, MHD_FOOTER_KIND, 361 &footer_iterator, &footer_sum); 362 if (param->self_check && (NULL != param->expect_body)) 363 { 364 const size_t elen = strlen (param->expect_body); 365 366 if ((! rs->overflow) && 367 (elen == rs->body_len) && 368 (0 == memcmp (rs->body, param->expect_body, elen))) 369 param->body_matched = 1; 370 else 371 { 372 fprintf (stderr, 373 "Self-check: the decoded request body does not match the " 374 "expected body. Expected %u bytes ('%s'), got %u bytes. " 375 "At line %d.\n", 376 (unsigned int) elen, param->expect_body, 377 (unsigned int) rs->body_len, (int) __LINE__); 378 param->err_flag = 1; 379 } 380 } 381 free (rs); 382 *req_cls = NULL; 383 384 if (use_chunked_reply) 385 { 386 struct content_cb_param *ccp; 387 388 ccp = malloc (sizeof(struct content_cb_param)); 389 if (NULL == ccp) 390 return MHD_NO; /* External error */ 391 ccp->response = NULL; 392 response = MHD_create_response_from_callback (MHD_SIZE_UNKNOWN, 1024, 393 &content_cb, ccp, 394 &content_cb_free); 395 if (NULL == response) 396 free (ccp); 397 else 398 ccp->response = response; 399 } 400 else 401 response = 402 MHD_create_response_from_buffer_static (MHD_STATICSTR_LEN_ (EMPTY_PAGE), 403 EMPTY_PAGE); 404 if (NULL == response) 405 { 406 fprintf (stderr, "Failed to create the response " 407 "at line %d.\n", (int) __LINE__); 408 return MHD_NO; /* External error, do not raise the error flag */ 409 } 410 ret = MHD_YES; 411 if (zzuf_use_close || ! zzuf_oneone) 412 ret = MHD_add_response_header (response, 413 MHD_HTTP_HEADER_CONNECTION, 414 "close"); 415 if (MHD_YES == ret) 416 ret = MHD_queue_response (connection, MHD_HTTP_OK, response); 417 MHD_destroy_response (response); 418 return ret; 419 } 420 421 422 static void 423 req_completed (void *cls, 424 struct MHD_Connection *connection, 425 void **req_cls, 426 enum MHD_RequestTerminationCode toe) 427 { 428 (void) cls; (void) connection; (void) toe; 429 430 if (NULL == *req_cls) 431 return; 432 free (*req_cls); 433 *req_cls = NULL; 434 } 435 436 437 /** 438 * Build the request head for a given case. 439 * 440 * @param name the case name (used in the URI) 441 * @param close_conn if non-zero, "Connection: close" is added, so that MHD 442 * closes the connection right after the reply instead of 443 * waiting for the connection timeout 444 * @param[out] buf the buffer to fill 445 * @param buf_size the size of @a buf 446 */ 447 static void 448 build_head (const char *name, int close_conn, char *buf, size_t buf_size) 449 { 450 int len; 451 452 len = snprintf (buf, buf_size, 453 "%s /chunked_ext/%s HTTP/1.1\r\n" 454 "Host: " ZZUF_MHD_LISTEN_IP "\r\n" 455 "Transfer-Encoding: chunked\r\n" 456 "%s" 457 "\r\n", 458 use_put ? "PUT" : "GET", 459 name, 460 close_conn ? "Connection: close\r\n" : ""); 461 if ((0 > len) || (buf_size <= (size_t) len)) 462 { 463 fprintf (stderr, "snprintf() failed at line %d.\n", (int) __LINE__); 464 fflush (stderr); 465 abort (); 466 } 467 } 468 469 470 static unsigned int 471 client_run (struct MHD_Daemon *d_extern, 472 uint16_t port, 473 void *cls) 474 { 475 struct ahc_param *param = (struct ahc_param *) cls; 476 unsigned int i; 477 unsigned int loops; 478 479 loops = zzuf_loop_count (); 480 for (i = 0; i < loops; ++i) 481 { 482 const struct chunk_case *cc = chunk_cases + (i % NUM_CHUNK_CASES); 483 struct zzuf_raw_part parts[4]; 484 char head[256]; 485 size_t num_parts; 486 const char *wire; 487 size_t wire_len; 488 size_t split; 489 490 fprintf (stderr, "."); 491 build_head (cc->name, 0, head, sizeof(head)); 492 if ((NULL != long_ext_body) && (0 == i % 5)) 493 wire = long_ext_body; /* Occasionally use the over-long extension */ 494 else 495 wire = cc->wire; 496 wire_len = strlen (wire); 497 /* Split the body so that the chunk-size (and chunk-extension) lines 498 are torn apart between two TCP segments. */ 499 split = wire_len / 2; 500 parts[0].data = head; 501 parts[0].size = strlen (head); 502 parts[1].data = wire; 503 parts[1].size = split; 504 parts[2].data = wire + split; 505 parts[2].size = wire_len - split; 506 /* Always append a pipelined follow-up request: if the chunk-extension 507 line is not consumed correctly, the request boundary is lost and this 508 second request is interpreted as chunk data (or the other way round). 509 Its "Connection: close" also makes MHD close the connection right 510 after the reply instead of waiting for the connection timeout. */ 511 parts[3].data = "GET /chunked_ext/after HTTP/1.1\r\n" 512 "Host: " ZZUF_MHD_LISTEN_IP "\r\n" 513 "Connection: close\r\n\r\n"; 514 parts[3].size = strlen (parts[3].data); 515 num_parts = 4; 516 if (ZZUF_RAW_SETUP_FAILED == 517 zzuf_raw_exchange (d_extern, port, parts, num_parts, 518 (unsigned int) ZZUF_CLIENT_TIMEOUT)) 519 { 520 fprintf (stderr, "The raw client could not be set up " 521 "at line %d.\n", (int) __LINE__); 522 return 99; /* Not an MHD error */ 523 } 524 fflush (stderr); 525 } 526 527 if (0 != param->err_flag) 528 { 529 fprintf (stderr, "One or more errors have been detected by the access " 530 "handler callback function. At line %d.\n", (int) __LINE__); 531 return 1; 532 } 533 return 0; 534 } 535 536 537 /** 538 * Run the deterministic (unfuzzed) self-check of the chunk-extension 539 * parser. Only possible in the socat mode, where MHD's own port is not 540 * touched by zzuf. 541 * 542 * @return zero on success, non-zero on failure 543 */ 544 static unsigned int 545 run_self_check (void) 546 { 547 struct MHD_Daemon *d; 548 struct MHD_Daemon *d_extern; 549 struct ahc_param param; 550 uint16_t port; 551 unsigned int flags; 552 unsigned int i; 553 unsigned int ret = 0; 554 555 param.magic = (unsigned int) TEST_MAGIC_MARKER; 556 param.err_flag = 0; 557 param.self_check = 1; 558 param.expect_body = NULL; 559 param.body_matched = 0; 560 561 if (MHD_NO != MHD_is_feature_supported (MHD_FEATURE_AUTODETECT_BIND_PORT)) 562 port = 0; 563 else 564 port = (uint16_t) (ZZUF_BASE_PORT + TEST_SELFCHECK_PORT_OFFSET); 565 if (MHD_YES == MHD_is_feature_supported (MHD_FEATURE_THREADS)) 566 flags = MHD_USE_SELECT_INTERNALLY; 567 else 568 flags = MHD_USE_NO_THREAD_SAFETY; 569 d = zzuf_start_daemon (flags, &port, zzuf_opt_profile (0), 0, 570 &ahc_chunked_ext, ¶m, 571 &req_completed, ¶m, NULL); 572 if (NULL == d) 573 return 99; /* Not an MHD error */ 574 d_extern = (0 == (MHD_USE_INTERNAL_POLLING_THREAD & flags)) ? d : NULL; 575 576 printf ("Running the deterministic (unfuzzed) chunk-extension " 577 "self-check...\n"); 578 fflush (stdout); 579 for (i = 0; i < NUM_VALID_CHUNK_CASES; ++i) 580 { 581 const struct chunk_case *cc = chunk_cases + i; 582 struct zzuf_raw_part parts[3]; 583 char head[256]; 584 char resp[512]; 585 size_t resp_len = 0; 586 size_t wire_len; 587 size_t split; 588 589 build_head (cc->name, 1, head, sizeof(head)); 590 wire_len = strlen (cc->wire); 591 split = wire_len / 2; 592 parts[0].data = head; 593 parts[0].size = strlen (head); 594 parts[1].data = cc->wire; 595 parts[1].size = split; 596 parts[2].data = cc->wire + split; 597 parts[2].size = wire_len - split; 598 599 param.expect_body = cc->expected; 600 param.body_matched = 0; 601 if (ZZUF_RAW_SETUP_FAILED == 602 zzuf_raw_exchange2 (d_extern, port, 1, parts, 3, 603 (unsigned int) ZZUF_CLIENT_TIMEOUT, 604 resp, sizeof(resp), &resp_len)) 605 { 606 fprintf (stderr, "The raw client could not be set up " 607 "at line %d.\n", (int) __LINE__); 608 MHD_stop_daemon (d); 609 return 99; /* Not an MHD error */ 610 } 611 if (! param.body_matched) 612 { 613 fprintf (stderr, 614 "Self-check FAILED for the chunk-extension case '%s': " 615 "MHD did not decode the body as expected. " 616 "At line %d.\n", cc->name, (int) __LINE__); 617 ret = 1; 618 } 619 else if ((12 > resp_len) || (0 != memcmp (resp, "HTTP/1.1 200", 12))) 620 { 621 fprintf (stderr, 622 "Self-check FAILED for the chunk-extension case '%s': " 623 "MHD did not reply with '200'. " 624 "At line %d.\n", cc->name, (int) __LINE__); 625 ret = 1; 626 } 627 } 628 MHD_stop_daemon (d); 629 if (0 != param.err_flag) 630 ret = 1; 631 if (0 == ret) 632 printf ("The chunk-extension self-check succeeded.\n"); 633 fflush (stdout); 634 return ret; 635 } 636 637 638 /** 639 * Build the chunked body with a very long chunk extension. 640 * 641 * With a connection memory pool of 256 or 512 bytes the chunk-size line 642 * does not fit into the read buffer, so MHD emits the 413 reply from 643 * handle_req_chunk_size_line_no_space(). Before commit e04eb218 it then 644 * re-entered the error path and discarded that reply, so this is also the 645 * regression test for it. 646 * 647 * @return non-zero on success, zero on failure 648 */ 649 static int 650 init_long_ext_body (void) 651 { 652 static const char head[] = "5;longext="; 653 static const char tail[] = "\r\nHELLO\r\n0\r\n\r\n"; 654 const size_t ext_len = 600; 655 char *buf; 656 657 buf = malloc (sizeof(head) + ext_len + sizeof(tail)); 658 if (NULL == buf) 659 return 0; 660 memcpy (buf, head, MHD_STATICSTR_LEN_ (head)); 661 memset (buf + MHD_STATICSTR_LEN_ (head), 'E', ext_len); 662 memcpy (buf + MHD_STATICSTR_LEN_ (head) + ext_len, tail, sizeof(tail)); 663 long_ext_body = buf; 664 return ! 0; 665 } 666 667 668 int 669 main (int argc, char *const *argv) 670 { 671 struct ahc_param param; 672 struct zzuf_run_params rp; 673 unsigned int res; 674 int use_magic_exit_codes; 675 676 zzuf_parse_common_args (argc, argv); 677 use_put = zzuf_name_has ("_put"); 678 use_chunked_reply = zzuf_name_has ("_get"); 679 if (1 != ((use_put ? 1 : 0) + (use_chunked_reply ? 1 : 0))) 680 { 681 fprintf (stderr, "Wrong test name '%s': no or multiple indications " 682 "for the test type.\n", (NULL != argv[0]) ? argv[0] : "(NULL)"); 683 return 99; 684 } 685 use_magic_exit_codes = zzuf_run_with_socat || zzuf_dry_run; 686 687 res = zzuf_check_runnable (); 688 if (0 != res) 689 return use_magic_exit_codes ? (int) res : 0; 690 691 long_ext_body = NULL; 692 if (! init_long_ext_body ()) 693 { 694 fprintf (stderr, "malloc() failed at line %d.\n", (int) __LINE__); 695 return use_magic_exit_codes ? 99 : 0; 696 } 697 698 /* The self-check needs an unfuzzed channel to MHD, which only exists 699 when zzuf fuzzes socat instead of this process. */ 700 if (zzuf_run_with_socat && ! zzuf_dry_run) 701 { 702 res = run_self_check (); 703 if (99 == res) 704 { 705 free (long_ext_body); 706 return 99; 707 } 708 if (0 != res) 709 { 710 free (long_ext_body); 711 return 1; 712 } 713 } 714 715 param.magic = (unsigned int) TEST_MAGIC_MARKER; 716 param.err_flag = 0; 717 param.self_check = 0; 718 param.expect_body = NULL; 719 param.body_matched = 0; 720 721 memset (&rp, 0, sizeof(rp)); 722 rp.port = zzuf_pick_port (TEST_PORT_OFFSET); 723 rp.ahc = &ahc_chunked_ext; 724 rp.ahc_cls = ¶m; 725 rp.rcc = &req_completed; 726 rp.rcc_cls = ¶m; 727 rp.mem_limit_override = 0; 728 rp.sweep_profiles = 1; 729 rp.profile_start = 0; 730 rp.client = &client_run; 731 rp.client_cls = ¶m; 732 733 res = zzuf_run_polling_modes (&rp); 734 free (long_ext_body); 735 long_ext_body = NULL; 736 737 if (99 == res) 738 return use_magic_exit_codes ? 99 : 0; 739 if (77 == res) 740 return use_magic_exit_codes ? 77 : 0; 741 return (0 == res) ? 0 : 1; /* 0 == pass */ 742 }