fuzz_postprocessor.c (13175B)
1 /* 2 This file is part of libmicrohttpd 3 Copyright (C) 2026 Christian Grothoff 4 5 This library is free software; you can redistribute it and/or 6 modify it under the terms of the GNU Lesser General Public 7 License as published by the Free Software Foundation; either 8 version 2.1 of the License, or (at your option) any later version. 9 10 This library is distributed in the hope that it will be useful, 11 but WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 Lesser General Public License for more details. 14 15 You should have received a copy of the GNU Lesser General Public 16 License along with this library. 17 If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 /** 21 * @file fuzz/fuzz_postprocessor.c 22 * @brief Fuzzer for MHD_post_process() 23 * @author Christian Grothoff 24 * 25 * MHD_create_post_processor() only ever looks at the "Content-Type" 26 * header of the connection it is given, so this harness fabricates the 27 * minimal connection object instead of pushing a whole request through 28 * a socket. That keeps the harness fast and lets the fuzzer control 29 * three dimensions that matter for the post processor and that a real 30 * request would not expose directly: 31 * 32 * - the raw Content-Type (encoding and multipart boundary), 33 * - the post processor buffer size, 34 * - how the POST data is split across MHD_post_process() calls. 35 * 36 * Input format: 37 * byte 0 content type selector 38 * byte 1 post processor buffer size selector 39 * byte 2 chunking pattern selector 40 * byte 3 length of the boundary taken from the payload 41 * byte 4.. the POST data 42 */ 43 44 #define FUZZ_HARNESS_NAME "fuzz_postprocessor" 45 #include "fuzz_common.h" 46 47 /* internal.h pulls in MHD_config.h and <microhttpd.h> in the right 48 order; including <microhttpd.h> first would redefine _MHD_EXTERN. */ 49 #include "internal.h" 50 51 static const size_t pp_buf_sizes[] = { 52 256, 257, 300, 512, 1024, 2048, 4096, 65536 53 }; 54 55 static const size_t chunk_patterns[] = { 56 1, 2, 3, 5, 7, 13, 32, 64, 1024, 0 /* 0 == everything at once */ 57 }; 58 59 static struct MHD_Daemon *shared_daemon; 60 61 /** Statistics, printed at exit with --verbose. */ 62 static unsigned long stat_pp_created; 63 static unsigned long stat_pp_failed; 64 static unsigned long stat_values; 65 66 67 static enum MHD_Result 68 dummy_ahc (void *cls, 69 struct MHD_Connection *connection, 70 const char *url, 71 const char *method, 72 const char *version, 73 const char *upload_data, 74 size_t *upload_data_size, 75 void **req_cls) 76 { 77 (void) cls; (void) connection; (void) url; (void) method; (void) version; 78 (void) upload_data; (void) upload_data_size; (void) req_cls; 79 return MHD_NO; 80 } 81 82 83 static void 84 stop_shared_daemon (void) 85 { 86 if (NULL != shared_daemon) 87 { 88 MHD_stop_daemon (shared_daemon); 89 shared_daemon = NULL; 90 } 91 } 92 93 94 static void 95 print_stats (void) 96 { 97 if (! fuzz_verbose) 98 return; 99 fprintf (stderr, 100 "%s: post processors created=%lu rejected=%lu, " 101 "values reported=%lu\n", 102 FUZZ_HARNESS_NAME, 103 stat_pp_created, stat_pp_failed, stat_values); 104 } 105 106 107 static struct MHD_Daemon * 108 get_shared_daemon (void) 109 { 110 if (NULL == shared_daemon) 111 { 112 shared_daemon = 113 MHD_start_daemon (MHD_USE_NO_LISTEN_SOCKET 114 | (fuzz_verbose ? MHD_USE_ERROR_LOG : 0u), 115 0, NULL, NULL, &dummy_ahc, NULL, 116 MHD_OPTION_END); 117 if (NULL != shared_daemon) 118 { 119 (void) atexit (&stop_shared_daemon); 120 (void) atexit (&print_stats); 121 } 122 } 123 return shared_daemon; 124 } 125 126 127 struct pp_ctx 128 { 129 const char *data; 130 size_t data_len; 131 uint64_t max_off; 132 }; 133 134 135 static enum MHD_Result 136 post_iter (void *cls, 137 enum MHD_ValueKind kind, 138 const char *key, 139 const char *filename, 140 const char *content_type, 141 const char *transfer_encoding, 142 const char *data, 143 uint64_t off, 144 size_t size) 145 { 146 struct pp_ctx *ctx = (struct pp_ctx *) cls; 147 volatile size_t sink = 0; 148 149 (void) kind; 150 stat_values++; 151 /* Touch every string the post processor claims to have produced; 152 ASAN turns any dangling or out-of-bounds pointer into an error. */ 153 if (NULL != key) 154 sink += strlen (key); 155 if (NULL != filename) 156 sink += strlen (filename); 157 if (NULL != content_type) 158 sink += strlen (content_type); 159 if (NULL != transfer_encoding) 160 sink += strlen (transfer_encoding); 161 if ( (NULL != data) && (0 != size) ) 162 { 163 size_t i; 164 165 for (i = 0; i < size; i++) 166 sink += (size_t) (unsigned char) data[i]; 167 } 168 else if (NULL == data) 169 { 170 if (0 != size) 171 fuzz_report_finding ("MHD_PostDataIterator called with NULL data but " 172 "non-zero size"); 173 } 174 /* A single value is delivered in order and can never be longer than 175 the POST data that was fed in. */ 176 if (off + size > ctx->data_len + 1) 177 fuzz_report_finding ("MHD_PostDataIterator reported more value data " 178 "than the POST data contained"); 179 if (off > ctx->max_off + ctx->data_len) 180 fuzz_report_finding ("MHD_PostDataIterator offset jumped implausibly"); 181 ctx->max_off = off; 182 (void) sink; 183 return MHD_YES; 184 } 185 186 187 int 188 LLVMFuzzerTestOneInput (const uint8_t *data, 189 size_t size) 190 { 191 struct MHD_Connection c; 192 struct MHD_HTTP_Req_Header h; 193 struct MHD_PostProcessor *pp; 194 struct pp_ctx ctx; 195 char *ctype; 196 char *body; 197 size_t body_len; 198 size_t buf_size; 199 size_t chunk; 200 size_t off; 201 size_t blen; 202 unsigned int sel; 203 204 if (size < 5) 205 return 0; 206 if (NULL == get_shared_daemon ()) 207 return 0; 208 209 sel = data[0]; 210 buf_size = pp_buf_sizes[data[1] % (sizeof (pp_buf_sizes) 211 / sizeof (pp_buf_sizes[0]))]; 212 chunk = chunk_patterns[data[2] % (sizeof (chunk_patterns) 213 / sizeof (chunk_patterns[0]))]; 214 blen = data[3]; 215 216 body_len = size - 4; 217 if (body_len > 8192) 218 body_len = 8192; 219 if (blen > body_len) 220 blen = body_len; 221 222 /* Exactly sized copies so that ASAN catches reads past the end. */ 223 body = (char *) malloc (body_len + 1); 224 if (NULL == body) 225 return 0; 226 memcpy (body, data + 4, body_len); 227 body[body_len] = '\0'; 228 229 ctype = (char *) malloc (blen + 64); 230 if (NULL == ctype) 231 { 232 free (body); 233 return 0; 234 } 235 switch (sel % 6) 236 { 237 case 0: 238 memcpy (ctype, MHD_HTTP_POST_ENCODING_FORM_URLENCODED, 239 sizeof (MHD_HTTP_POST_ENCODING_FORM_URLENCODED)); 240 break; 241 case 1: 242 case 2: 243 { 244 /* multipart with a boundary taken from the payload */ 245 size_t o = 0; 246 size_t i; 247 248 memcpy (ctype, MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA, 249 sizeof (MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA) - 1); 250 o = sizeof (MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA) - 1; 251 memcpy (ctype + o, "; boundary=", 11); 252 o += 11; 253 for (i = 0; i < blen; i++) 254 { 255 char ch = body[i]; 256 257 /* A NUL would truncate the header value; map it away. */ 258 ctype[o++] = ('\0' == ch) ? 'x' : ch; 259 } 260 ctype[o] = '\0'; 261 break; 262 } 263 case 3: 264 (void) snprintf (ctype, blen + 64, 265 "multipart/form-data; boundary=\"%.*s\"", 266 (int) ((blen > 40) ? 40 : blen), body); 267 break; 268 case 4: 269 memcpy (ctype, MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA, 270 sizeof (MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA)); 271 break; 272 default: 273 memcpy (ctype, "text/plain", sizeof ("text/plain")); 274 break; 275 } 276 277 memset (&c, 0, sizeof (c)); 278 memset (&h, 0, sizeof (h)); 279 h.header = MHD_HTTP_HEADER_CONTENT_TYPE; 280 h.header_size = MHD_STATICSTR_LEN_ (MHD_HTTP_HEADER_CONTENT_TYPE); 281 h.value = ctype; 282 h.value_size = strlen (ctype); 283 h.kind = MHD_HEADER_KIND; 284 c.daemon = shared_daemon; 285 c.rq.headers_received = &h; 286 c.rq.headers_received_tail = &h; 287 c.state = MHD_CONNECTION_HEADERS_PROCESSED; 288 289 ctx.data = body; 290 ctx.data_len = body_len; 291 ctx.max_off = 0; 292 293 pp = MHD_create_post_processor (&c, buf_size, &post_iter, &ctx); 294 if (NULL == pp) 295 { 296 stat_pp_failed++; 297 free (ctype); 298 free (body); 299 return 0; 300 } 301 stat_pp_created++; 302 303 off = 0; 304 while (off < body_len) 305 { 306 size_t n = (0 == chunk) ? (body_len - off) : chunk; 307 308 if (n > body_len - off) 309 n = body_len - off; 310 (void) MHD_post_process (pp, body + off, n); 311 off += n; 312 } 313 /* Final, zero-length call: what MHD itself does at the end of the 314 upload. */ 315 (void) MHD_post_process (pp, body + body_len, 0); 316 (void) MHD_destroy_post_processor (pp); 317 318 free (ctype); 319 free (body); 320 return 0; 321 } 322 323 324 /* ------------------------------------------------------------------ */ 325 /* Generator */ 326 /* ------------------------------------------------------------------ */ 327 328 static const char *const gen_disp[] = { 329 "Content-Disposition: form-data; name=\"a\"", 330 "Content-Disposition: form-data; name=\"a\"; filename=\"f.txt\"", 331 "Content-Disposition: form-data; name=a", 332 "Content-Disposition: form-data", 333 "Content-Disposition: attachment; name=\"a\"", 334 "Content-Disposition: form-data; name=\"\"", 335 "Content-Disposition: form-data; name=\"a", 336 "Content-Type: text/plain", 337 "Content-Transfer-Encoding: binary", 338 "X-Other: value" 339 }; 340 341 static const char *const gen_kv[] = { 342 "a=1", "b=%41", "c", "d=", "=e", "&", "&&", "a=%", "a=%4", "a=%zz", 343 "verylongkeyname=verylongvaluewithlotsofcharacters", "a+b=c+d", 344 "%41%42=%43%44" 345 }; 346 347 348 static size_t 349 fuzz_generate (struct fuzz_rng *rng, 350 uint8_t *buf, 351 size_t cap) 352 { 353 size_t len = 0; 354 int multipart; 355 const char *boundary; 356 unsigned int nparts; 357 unsigned int i; 358 static const char *const boundaries[] = { 359 "--abc", "XY", "boundary", "-", "aa", "0123456789012345678901234567890", 360 "a\"b" 361 }; 362 363 #define ADD(s) \ 364 do { \ 365 const char *s_ = (s); \ 366 size_t l_ = strlen (s_); \ 367 if (len + l_ >= cap) \ 368 return len; \ 369 memcpy (buf + len, s_, l_); \ 370 len += l_; \ 371 } while (0) 372 373 if (cap < 64) 374 return 0; 375 multipart = ! fuzz_chance (rng, 3); 376 boundary = boundaries[fuzz_below (rng, 377 (uint32_t) (sizeof (boundaries) 378 / sizeof (char *)))]; 379 buf[len++] = (uint8_t) (multipart ? (1 + fuzz_below (rng, 2)) : 0); 380 buf[len++] = fuzz_byte (rng); 381 buf[len++] = fuzz_byte (rng); 382 buf[len++] = (uint8_t) strlen (boundary); 383 if (multipart) 384 { 385 /* The first strlen(boundary) bytes of the body double as the 386 boundary in the Content-Type header (see the input format). */ 387 ADD (boundary); 388 nparts = 1 + fuzz_below (rng, 4); 389 for (i = 0; i < nparts; i++) 390 { 391 unsigned int nhdr = fuzz_below (rng, 3); 392 unsigned int k; 393 394 ADD ("\r\n--"); 395 ADD (boundary); 396 ADD ("\r\n"); 397 for (k = 0; k <= nhdr; k++) 398 { 399 ADD (gen_disp[fuzz_below (rng, 400 (uint32_t) (sizeof (gen_disp) 401 / sizeof (char *)))]); 402 ADD ("\r\n"); 403 } 404 ADD ("\r\n"); 405 { 406 unsigned int n = fuzz_below (rng, 40); 407 408 for (k = 0; (k < n) && (len < cap); k++) 409 buf[len++] = (uint8_t) ('A' + fuzz_below (rng, 26)); 410 } 411 } 412 ADD ("\r\n--"); 413 ADD (boundary); 414 ADD (fuzz_chance (rng, 4) ? "\r\n" : "--\r\n"); 415 } 416 else 417 { 418 unsigned int n = 1 + fuzz_below (rng, 10); 419 420 for (i = 0; i < n; i++) 421 { 422 if (0 != i) 423 ADD ("&"); 424 ADD (gen_kv[fuzz_below (rng, 425 (uint32_t) (sizeof (gen_kv) / sizeof (char *)))]); 426 } 427 } 428 #undef ADD 429 return len; 430 } 431 432 433 /* ------------------------------------------------------------------ */ 434 /* Seed corpus */ 435 /* ------------------------------------------------------------------ */ 436 437 struct pp_seed 438 { 439 const char *txt; 440 size_t len; 441 }; 442 443 #define PSEED(t) { t, sizeof (t) - 1 } 444 445 static const struct pp_seed pp_seeds[] = { 446 PSEED ("\x00\x00\x00\x00" "a=1&b=%41&c"), 447 PSEED ("\x00\x00\x04\x00" "a=1&b=%41&c"), 448 PSEED ("\x00\x00\x00\x00" "a=%"), 449 PSEED ("\x00\x00\x00\x00" "&&&&"), 450 PSEED ("\x01\x00\x00\x05" "--abc\r\n----abc\r\n" 451 "Content-Disposition: form-data; name=\"k\"\r\n\r\nvalue\r\n" 452 "----abc--\r\n"), 453 PSEED ("\x01\x00\x01\x05" "--abc\r\n----abc\r\n" 454 "Content-Disposition: form-data; name=\"k\"; filename=\"f\"\r\n" 455 "Content-Type: text/plain\r\n\r\nvalue\r\n----abc--\r\n"), 456 PSEED ("\x01\x07\x02\x02" "XY\r\n--XY\r\n\r\nnoheaders\r\n--XY--\r\n"), 457 PSEED ("\x01\x00\x00\x01" "-\r\n---\r\n\r\nx\r\n-----\r\n"), 458 PSEED ("\x04\x00\x00\x00" "no boundary at all"), 459 PSEED ("\x05\x00\x00\x00" "text/plain body"), 460 PSEED ("\x03\x00\x00\x03" "a\"b\r\n--a\"b\r\n\r\nv\r\n--a\"b--\r\n") 461 }; 462 463 464 static size_t 465 fuzz_seed_count (void) 466 { 467 return sizeof (pp_seeds) / sizeof (pp_seeds[0]); 468 } 469 470 471 static const uint8_t * 472 fuzz_seed_get (size_t idx, 473 size_t *len) 474 { 475 *len = pp_seeds[idx].len; 476 return (const uint8_t *) pp_seeds[idx].txt; 477 }