fuzz_str.c (15904B)
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_str.c 22 * @brief Direct in-process fuzzer for the string primitives of mhd_str.c 23 * @author Christian Grothoff 24 * 25 * Every destination buffer is obtained with malloc() at *exactly* the 26 * size that the documented contract of the function under test 27 * requires -- never a generous stack array. Under AddressSanitizer the 28 * redzone immediately after the allocation turns any one-byte 29 * over-write into a hard error, which is precisely the property a 30 * fixed-size stack buffer would not give us. 31 * 32 * In addition to the contract-exact calls, several functions are also 33 * called with a deliberately *too small* output buffer: functions that 34 * take a buffer size argument must detect this and must not write past 35 * the end. 36 * 37 * Input format: 38 * byte 0 target selector 39 * byte 1 auxiliary parameter (buffer size shrink, split point, ...) 40 * byte 2.. payload 41 */ 42 43 #define FUZZ_HARNESS_NAME "fuzz_str" 44 #include "fuzz_common.h" 45 46 #include "mhd_options.h" 47 #include "mhd_str.h" 48 49 /** 50 * Largest digest MHD supports; this is the size of the on-stack buffers 51 * that digestauth.c passes to MHD_hex_to_bin(). 52 */ 53 #define MODEL_MAX_DIGEST 32 54 55 /** 56 * Model the (pre-2026) digestauth.c call site that fed a client 57 * controlled, up to 4 * digest_size characters long 'response' value 58 * into MHD_hex_to_bin() together with a digest_size byte buffer. 59 * 60 * MHD_hex_to_bin() has no output size parameter: it writes len/2 bytes, 61 * so a caller with a fixed size buffer *must* bound the input length. 62 * This target replays that call site with an exactly sized heap buffer 63 * so that ASAN reports the overflow. It is OFF by default (it models a 64 * caller, not the library) -- enable it with MHD_FUZZ_MODEL_DIGEST_SINK=1 65 * to reproduce the primitive behind the digest 'response' overflow. 66 */ 67 static int model_digest_sink; 68 static int model_digest_sink_read; 69 70 enum str_target 71 { 72 TGT_HEX_TO_BIN = 0, 73 TGT_BIN_TO_HEX, 74 TGT_BIN_TO_HEX_Z, 75 TGT_PCT_STRICT, 76 TGT_PCT_LENIENT, 77 TGT_PCT_IN_PLACE_STRICT, 78 TGT_PCT_IN_PLACE_LENIENT, 79 TGT_UNQUOTE, 80 TGT_QUOTE, 81 TGT_BASE64, 82 TGT_TO_UINT64, 83 TGT_TOKENS, 84 TGT_EQUAL_CASELESS, 85 TGT_DIGEST_HEX_SINK, 86 TGT_COUNT 87 }; 88 89 90 /** 91 * Allocate exactly @a n bytes. Zero-sized allocations are turned into 92 * a one byte allocation so that the returned pointer stays valid, but 93 * the harness never tells the library about that extra byte. 94 */ 95 static void * 96 xalloc (size_t n) 97 { 98 void *p = malloc ((0 == n) ? 1 : n); 99 100 if (NULL == p) 101 abort (); 102 return p; 103 } 104 105 106 static char * 107 dup_z (const uint8_t *d, 108 size_t n) 109 { 110 char *p = (char *) xalloc (n + 1); 111 112 memcpy (p, d, n); 113 p[n] = '\0'; 114 return p; 115 } 116 117 118 int 119 LLVMFuzzerTestOneInput (const uint8_t *data, 120 size_t size) 121 { 122 enum str_target tgt; 123 const uint8_t *p; 124 size_t n; 125 unsigned int aux; 126 127 if (size < 3) 128 return 0; 129 tgt = (enum str_target) (data[0] % (unsigned int) TGT_COUNT); 130 aux = data[1]; 131 p = data + 2; 132 n = size - 2; 133 /* Keep the inputs small: these are unit-level primitives and short 134 inputs explore the interesting corner cases far more efficiently. */ 135 if (n > 512) 136 n = 512; 137 138 switch (tgt) 139 { 140 case TGT_HEX_TO_BIN: 141 { 142 /* Contract: the output buffer must be len/2 bytes long, or 143 len/2 + 1 if len is odd. */ 144 size_t need = (n / 2) + (n % 2); 145 uint8_t *out = (uint8_t *) xalloc (need); 146 size_t r = MHD_hex_to_bin ((const char *) p, n, out); 147 148 if (r > need) 149 fuzz_report_finding ("MHD_hex_to_bin() reported more bytes written " 150 "than its documented output size"); 151 free (out); 152 break; 153 } 154 155 case TGT_BIN_TO_HEX: 156 { 157 char *out = (char *) xalloc (2 * n); 158 size_t r = MHD_bin_to_hex (p, n, out); 159 160 if (r != 2 * n) 161 fuzz_report_finding ("MHD_bin_to_hex() did not write 2 * size chars"); 162 free (out); 163 break; 164 } 165 166 case TGT_BIN_TO_HEX_Z: 167 { 168 char *out = (char *) xalloc (2 * n + 1); 169 size_t r = MHD_bin_to_hex_z (p, n, out); 170 171 if (r != 2 * n) 172 fuzz_report_finding ("MHD_bin_to_hex_z() did not write 2 * size chars"); 173 if ('\0' != out[r]) 174 fuzz_report_finding ("MHD_bin_to_hex_z() result is not " 175 "zero-terminated"); 176 free (out); 177 break; 178 } 179 180 case TGT_PCT_STRICT: 181 { 182 /* First with the exact maximum size, then with a buffer that is 183 deliberately too small. */ 184 char *out = (char *) xalloc (n); 185 size_t r = MHD_str_pct_decode_strict_n_ ((const char *) p, n, out, n); 186 size_t small; 187 188 if (r > n) 189 fuzz_report_finding ("MHD_str_pct_decode_strict_n_() overran the " 190 "documented output size"); 191 free (out); 192 small = (0 == n) ? 0 : (n * (aux % 100u)) / 100u; 193 out = (char *) xalloc (small); 194 (void) MHD_str_pct_decode_strict_n_ ((const char *) p, n, out, small); 195 free (out); 196 break; 197 } 198 199 case TGT_PCT_LENIENT: 200 { 201 char *out = (char *) xalloc (n); 202 bool broken = false; 203 size_t r = 204 MHD_str_pct_decode_lenient_n_ ((const char *) p, n, out, n, &broken); 205 size_t small; 206 207 if (r > n) 208 fuzz_report_finding ("MHD_str_pct_decode_lenient_n_() overran the " 209 "documented output size"); 210 free (out); 211 small = (0 == n) ? 0 : (n * (aux % 100u)) / 100u; 212 out = (char *) xalloc (small); 213 (void) MHD_str_pct_decode_lenient_n_ ((const char *) p, n, out, small, 214 NULL); 215 free (out); 216 break; 217 } 218 219 case TGT_PCT_IN_PLACE_STRICT: 220 { 221 char *s = dup_z (p, n); 222 size_t r = MHD_str_pct_decode_in_place_strict_ (s); 223 224 if (r > n) 225 fuzz_report_finding ("MHD_str_pct_decode_in_place_strict_() grew " 226 "the string"); 227 free (s); 228 break; 229 } 230 231 case TGT_PCT_IN_PLACE_LENIENT: 232 { 233 char *s = dup_z (p, n); 234 bool broken = false; 235 size_t r = MHD_str_pct_decode_in_place_lenient_ (s, &broken); 236 237 if (r > n) 238 fuzz_report_finding ("MHD_str_pct_decode_in_place_lenient_() grew " 239 "the string"); 240 free (s); 241 break; 242 } 243 244 #ifdef DAUTH_SUPPORT 245 case TGT_UNQUOTE: 246 { 247 /* Unquoting never grows the string. */ 248 char *out = (char *) xalloc (n); 249 size_t r = MHD_str_unquote ((const char *) p, n, out); 250 251 if (r > n) 252 fuzz_report_finding ("MHD_str_unquote() wrote more characters than " 253 "the quoted input had"); 254 free (out); 255 break; 256 } 257 #else /* ! DAUTH_SUPPORT */ 258 case TGT_UNQUOTE: 259 break; 260 #endif /* ! DAUTH_SUPPORT */ 261 262 #if defined(DAUTH_SUPPORT) || defined(BAUTH_SUPPORT) 263 case TGT_QUOTE: 264 { 265 /* Quoting can at most double the size. */ 266 char *out = (char *) xalloc (2 * n); 267 size_t r = MHD_str_quote ((const char *) p, n, out, 2 * n); 268 size_t small; 269 270 if (r > 2 * n) 271 fuzz_report_finding ("MHD_str_quote() wrote more than 2 * len chars"); 272 free (out); 273 small = (0 == n) ? 0 : (n * (aux % 200u)) / 100u; 274 out = (char *) xalloc (small); 275 (void) MHD_str_quote ((const char *) p, n, out, small); 276 free (out); 277 break; 278 } 279 #else /* ! (DAUTH_SUPPORT || BAUTH_SUPPORT) */ 280 case TGT_QUOTE: 281 break; 282 #endif /* ! (DAUTH_SUPPORT || BAUTH_SUPPORT) */ 283 284 #ifdef BAUTH_SUPPORT 285 case TGT_BASE64: 286 { 287 size_t need = MHD_base64_max_dec_size_ (n); 288 uint8_t *out = (uint8_t *) xalloc (need); 289 size_t r = MHD_base64_to_bin_n ((const char *) p, n, out, need); 290 size_t small; 291 292 if (r > need) 293 fuzz_report_finding ("MHD_base64_to_bin_n() exceeded " 294 "MHD_base64_max_dec_size_()"); 295 free (out); 296 small = (0 == need) ? 0 : (need * (aux % 100u)) / 100u; 297 out = (uint8_t *) xalloc (small); 298 (void) MHD_base64_to_bin_n ((const char *) p, n, out, small); 299 free (out); 300 break; 301 } 302 #else /* ! BAUTH_SUPPORT */ 303 case TGT_BASE64: 304 break; 305 #endif /* ! BAUTH_SUPPORT */ 306 307 case TGT_TO_UINT64: 308 { 309 char *s = dup_z (p, n); 310 uint64_t v1 = 0; 311 uint64_t v2 = 0; 312 size_t r1 = MHD_str_to_uint64_n_ (s, n, &v1); 313 size_t r2 = MHD_strx_to_uint64_n_ (s, n, &v2); 314 315 if (r1 > n) 316 fuzz_report_finding ("MHD_str_to_uint64_n_() consumed more than " 317 "maxlen characters"); 318 if (r2 > n) 319 fuzz_report_finding ("MHD_strx_to_uint64_n_() consumed more than " 320 "maxlen characters"); 321 free (s); 322 break; 323 } 324 325 case TGT_TOKENS: 326 { 327 /* The "token" arguments have documented preconditions (no NUL, 328 space, tab or comma); a fuzzer that violates them would only 329 find its own mistakes, so sanitise them here. */ 330 char *s = dup_z (p, n); 331 size_t split = (0 == n) ? 0 : (aux % n); 332 char *tok = dup_z (p + split, n - split); 333 size_t tlen; 334 ssize_t bs; 335 char *out; 336 size_t k; 337 338 for (k = 0; k < n - split; k++) 339 { 340 if ( ('\0' == tok[k]) || (' ' == tok[k]) || 341 ('\t' == tok[k]) || (',' == tok[k]) ) 342 tok[k] = 'x'; 343 } 344 tlen = strlen (tok); 345 if (0 == tlen) 346 { 347 free (tok); 348 tok = dup_z ((const uint8_t *) "chunked", 7); 349 tlen = 7; 350 } 351 (void) MHD_str_has_token_caseless_ (s, tok, tlen); 352 353 /* Documented worst case growth of the output is 50%. */ 354 bs = (ssize_t) (n + n / 2 + 1); 355 out = (char *) xalloc ((size_t) bs); 356 (void) MHD_str_remove_token_caseless_ (s, n, tok, tlen, out, &bs); 357 if (bs > (ssize_t) (n + n / 2 + 1)) 358 fuzz_report_finding ("MHD_str_remove_token_caseless_() reported a " 359 "result larger than the documented 50% growth"); 360 if (0 <= bs) 361 { 362 /* The output of the previous call is normalised, which is the 363 documented precondition of the in-place variant. */ 364 char *norm = dup_z ((const uint8_t *) out, (size_t) bs); 365 size_t nlen = (size_t) bs; 366 367 (void) MHD_str_remove_tokens_caseless_ (norm, &nlen, tok, tlen); 368 if (nlen > (size_t) bs) 369 fuzz_report_finding ("MHD_str_remove_tokens_caseless_() grew the " 370 "string"); 371 free (norm); 372 } 373 free (out); 374 free (tok); 375 free (s); 376 break; 377 } 378 379 case TGT_EQUAL_CASELESS: 380 { 381 size_t split = (0 == n) ? 0 : (aux % n); 382 char *a = dup_z (p, split); 383 char *b = dup_z (p + split, n - split); 384 385 (void) MHD_str_equal_caseless_bin_n_ (a, b, (split < n - split) 386 ? split : (n - split)); 387 (void) MHD_str_equal_caseless_n_ (a, b, n); 388 free (a); 389 free (b); 390 break; 391 } 392 393 case TGT_DIGEST_HEX_SINK: 394 { 395 /* See the comment on model_digest_sink above. */ 396 size_t len = n; 397 uint8_t *out; 398 399 if (! model_digest_sink_read) 400 { 401 const char *e = getenv ("MHD_FUZZ_MODEL_DIGEST_SINK"); 402 403 model_digest_sink_read = 1; 404 if (NULL != e) 405 model_digest_sink = (0 != atoi (e)); 406 } 407 if (! model_digest_sink) 408 break; 409 /* digestauth.c only checked 'len <= 4 * digest_size' before the 410 fix; replay exactly that bound. */ 411 if (len > 4 * MODEL_MAX_DIGEST) 412 len = 4 * MODEL_MAX_DIGEST; 413 out = (uint8_t *) xalloc (MODEL_MAX_DIGEST); 414 (void) MHD_hex_to_bin ((const char *) p, len, out); 415 free (out); 416 break; 417 } 418 419 case TGT_COUNT: 420 default: 421 break; 422 } 423 return 0; 424 } 425 426 427 /* ------------------------------------------------------------------ */ 428 /* Generator */ 429 /* ------------------------------------------------------------------ */ 430 431 static const char *const gen_str_atoms[] = { 432 "%41", "%", "%%", "%zz", "%0", "%00", "%ff", "\\", "\\\"", "\"", 433 "0123456789abcdef", "0123456789ABCDEF", "ffffffffffffffffffffffff", 434 "gg", "0x", "18446744073709551615", "99999999999999999999", 435 "a, b, c", "chunked", "identity", " , ", ",,", "token", 436 "QUJD", "QQ==", "Q===", "====", "AAAA", "AA=A", 437 "\x00\x01\x7f\x80\xff", " ", "\t", "\r\n" 438 }; 439 440 441 static size_t 442 fuzz_generate (struct fuzz_rng *rng, 443 uint8_t *buf, 444 size_t cap) 445 { 446 size_t len = 0; 447 unsigned int natoms; 448 unsigned int i; 449 450 if (cap < 8) 451 return 0; 452 buf[len++] = (uint8_t) fuzz_below (rng, (uint32_t) TGT_COUNT); 453 buf[len++] = fuzz_byte (rng); 454 natoms = 1 + fuzz_below (rng, 12); 455 for (i = 0; i < natoms; i++) 456 { 457 if (fuzz_chance (rng, 3)) 458 { 459 /* a run of hex digits of a length that is interesting for the 460 digest code paths (32, 64, 128 characters) */ 461 unsigned int k; 462 unsigned int run = 1 + fuzz_below (rng, 140); 463 464 for (k = 0; (k < run) && (len < cap); k++) 465 buf[len++] = (uint8_t) "0123456789abcdef"[fuzz_below (rng, 16)]; 466 } 467 else if (fuzz_chance (rng, 8)) 468 { 469 if (len < cap) 470 buf[len++] = fuzz_byte (rng); 471 } 472 else 473 { 474 const char *a = 475 gen_str_atoms[fuzz_below (rng, 476 (uint32_t) (sizeof (gen_str_atoms) 477 / sizeof (char *)))]; 478 size_t al = strlen (a); 479 480 if (len + al > cap) 481 break; 482 memcpy (buf + len, a, al); 483 len += al; 484 } 485 } 486 return len; 487 } 488 489 490 /* ------------------------------------------------------------------ */ 491 /* Seed corpus */ 492 /* ------------------------------------------------------------------ */ 493 494 struct str_seed 495 { 496 const char *txt; 497 size_t len; 498 }; 499 500 #define SSEED(t) { t, sizeof (t) - 1 } 501 502 static const struct str_seed str_seeds[] = { 503 /* hex -> bin, 128 characters: the length digestauth.c used to allow 504 into a 32 byte buffer */ 505 SSEED ("\x00\x00" 506 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" 507 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"), 508 SSEED ("\x00\x00" "0123456789abcdef0123456789abcdef"), 509 SSEED ("\x00\x00" "abc"), 510 SSEED ("\x00\x00" "zz"), 511 SSEED ("\x01\x00" "\x01\x02\x03\x04"), 512 SSEED ("\x02\x00" "\xff\xfe"), 513 SSEED ("\x03\x00" "/a%41%42%zz%"), 514 SSEED ("\x04\x40" "/a%41%42%zz%"), 515 SSEED ("\x05\x00" "%41%42%%%0"), 516 SSEED ("\x06\x00" "%41%42%%%0"), 517 SSEED ("\x07\x00" "a\\\"b\\\\c"), 518 SSEED ("\x08\x00" "a\"b\\c"), 519 SSEED ("\x09\x00" "QUJDRA=="), 520 SSEED ("\x09\x00" "QUJDR==="), 521 SSEED ("\x0a\x00" "18446744073709551615"), 522 SSEED ("\x0a\x00" "ffffffffffffffff"), 523 SSEED ("\x0b\x03" "chunked, identity, chunked"), 524 SSEED ("\x0c\x04" "CHUNKEDchunked"), 525 SSEED ("\x0d\x00" 526 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" 527 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") 528 }; 529 530 531 static size_t 532 fuzz_seed_count (void) 533 { 534 return sizeof (str_seeds) / sizeof (str_seeds[0]); 535 } 536 537 538 static const uint8_t * 539 fuzz_seed_get (size_t idx, 540 size_t *len) 541 { 542 *len = str_seeds[idx].len; 543 return (const uint8_t *) str_seeds[idx].txt; 544 }