test_digest_auth.c (21665B)
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_digest_auth.c 23 * @brief Fuzzing testcase for Digest Authentication 24 * @author Christian Grothoff 25 * 26 * The Digest Authentication parser of MHD used to be completely outside of 27 * the reach of the fuzzing tests of this directory: no test ever produced 28 * an "Authorization: Digest" header. 29 * 30 * This test closes that gap from two sides: 31 * - libcurl performs a complete, well-formed digest handshake (two round 32 * trips), so zzuf's bit flips land inside real "WWW-Authenticate:" and 33 * "Authorization:" headers; 34 * - the raw socket client sends hand-crafted "Authorization: Digest" 35 * headers that are *syntactically* well-formed but semantically hostile 36 * (unknown "algorithm" token, over-long "response", over-long userhash, 37 * over-long username, missing parameters, ...). Random bit flipping 38 * essentially never produces such an input; these headers have to be 39 * written on purpose. 40 * 41 * The hostile headers that need a server generated nonce (everything that 42 * is checked *after* the nonce validation) are built from the nonce that 43 * MHD hands out in its own 401 reply, which the raw client parses. 44 */ 45 46 #include "platform.h" 47 #include <curl/curl.h> 48 #include <microhttpd.h> 49 #include <stdlib.h> 50 #include <string.h> 51 52 #include "mhd_zzuf_common.h" 53 54 #define TEST_MAGIC_MARKER 0xFEE1C0DE 55 56 #define DENIED_PAGE "Access denied." 57 #define GRANTED_PAGE "Access granted." 58 59 /** 60 * The port offset used if the port cannot be auto-detected. 61 */ 62 #define TEST_PORT_OFFSET 155 63 64 #define TEST_REALM "TestRealm" 65 #define TEST_OPAQUE "0123456789abcdef" 66 #define TEST_USERNAME "testuser" 67 #define TEST_PASSWORD "testpass" 68 69 /** 70 * The path that makes MHD issue an MD5 challenge (used by libcurl). 71 */ 72 #define URI_MD5 "/dauth/md5" 73 74 /** 75 * The path that makes MHD issue a SHA-256 challenge. A SHA-256 nonce is 76 * needed to reach the code that decodes the client's "response" parameter 77 * with a SHA-256 sized digest. 78 */ 79 #define URI_SHA256 "/dauth/sha256" 80 81 #define HOST_HDR "Host: " ZZUF_MHD_LISTEN_IP "\r\n" 82 83 /** 84 * The size of the buffer used to build the raw requests. 85 */ 86 #define RAW_BUF_SIZE 2048 87 88 /** 89 * The random data for the nonce generation. 90 */ 91 static const char digest_rnd_data[32] = 92 { 'z','z','u','f','_','d','i','g','e','s','t','_','a','u','t','h', 93 '_','r','a','n','d','o','m','_','s','e','e','d','_','0','0','1' }; 94 95 96 /** 97 * The closure of the access handler callback. 98 */ 99 struct ahc_param 100 { 101 /** 102 * Must have #TEST_MAGIC_MARKER value. 103 */ 104 unsigned int magic; 105 106 /** 107 * Non-zero if any error has been encountered. 108 */ 109 unsigned int err_flag; 110 111 /** 112 * The number of successful authentications. 113 */ 114 unsigned int num_ok; 115 }; 116 117 118 static enum MHD_Result 119 ahc_digest (void *cls, 120 struct MHD_Connection *connection, 121 const char *url, 122 const char *method, 123 const char *version, 124 const char *upload_data, 125 size_t *upload_data_size, 126 void **req_cls) 127 { 128 static int marker; 129 struct ahc_param *param = (struct ahc_param *) cls; 130 struct MHD_Response *response; 131 enum MHD_Result ret; 132 enum MHD_DigestAuthResult check_res; 133 enum MHD_DigestAuthMultiAlgo3 challenge_algo; 134 135 (void) method; (void) version; (void) upload_data; 136 137 if ((NULL == param) || (TEST_MAGIC_MARKER != param->magic)) 138 { 139 fprintf (stderr, "The 'cls' parameter is invalid " 140 "at line %d.\n", (int) __LINE__); 141 fflush (stderr); 142 abort (); 143 } 144 if (NULL == *req_cls) 145 { 146 *req_cls = ▮ 147 return MHD_YES; 148 } 149 if ((NULL != upload_data_size) && (0 != *upload_data_size)) 150 { 151 *upload_data_size = 0; /* Discard the body */ 152 return MHD_YES; 153 } 154 155 /* This is the call that parses the (fuzzed or hand-crafted) client's 156 "Authorization: Digest" header. */ 157 check_res = MHD_digest_auth_check3 (connection, 158 TEST_REALM, 159 TEST_USERNAME, 160 TEST_PASSWORD, 161 0 /* daemon default timeout */, 162 0 /* daemon default max nc */, 163 MHD_DIGEST_AUTH_MULT_QOP_ANY_NON_INT, 164 MHD_DIGEST_AUTH_MULT_ALGO3_ANY_NON_SESSION); 165 if (MHD_DAUTH_OK == check_res) 166 { 167 param->num_ok++; 168 response = 169 MHD_create_response_from_buffer_static (MHD_STATICSTR_LEN_ ( 170 GRANTED_PAGE), 171 GRANTED_PAGE); 172 if (NULL == response) 173 return MHD_NO; /* External error */ 174 if (zzuf_use_close || ! zzuf_oneone) 175 (void) MHD_add_response_header (response, 176 MHD_HTTP_HEADER_CONNECTION, "close"); 177 ret = MHD_queue_response (connection, MHD_HTTP_OK, response); 178 MHD_destroy_response (response); 179 return ret; 180 } 181 182 /* Not authenticated (or the header was broken): send a challenge. */ 183 if ((NULL != url) && (0 == strcmp (url, URI_SHA256))) 184 challenge_algo = MHD_DIGEST_AUTH_MULT_ALGO3_SHA256; 185 else 186 challenge_algo = MHD_DIGEST_AUTH_MULT_ALGO3_MD5; 187 response = 188 MHD_create_response_from_buffer_static (MHD_STATICSTR_LEN_ (DENIED_PAGE), 189 DENIED_PAGE); 190 if (NULL == response) 191 return MHD_NO; /* External error */ 192 if (zzuf_use_close || ! zzuf_oneone) 193 (void) MHD_add_response_header (response, 194 MHD_HTTP_HEADER_CONNECTION, "close"); 195 ret = MHD_queue_auth_required_response3 (connection, 196 TEST_REALM, 197 TEST_OPAQUE, 198 NULL, 199 response, 200 (MHD_DAUTH_NONCE_STALE == 201 check_res) ? MHD_YES : MHD_NO, 202 MHD_DIGEST_AUTH_MULT_QOP_AUTH, 203 challenge_algo, 204 MHD_YES /* userhash support */, 205 MHD_NO); 206 MHD_destroy_response (response); 207 return ret; 208 } 209 210 211 /** 212 * Extract the value of the "nonce" parameter from a reply. 213 * 214 * @param reply the reply, not necessarily zero-terminated 215 * @param reply_len the number of valid bytes in @a reply 216 * @param[out] nonce the buffer for the nonce 217 * @param nonce_size the size of @a nonce 218 * @return non-zero if a nonce has been extracted, zero otherwise 219 */ 220 static int 221 extract_nonce (const char *reply, 222 size_t reply_len, 223 char *nonce, 224 size_t nonce_size) 225 { 226 static const char marker[] = "nonce=\""; 227 size_t i; 228 229 if (reply_len < MHD_STATICSTR_LEN_ (marker) + 1) 230 return 0; 231 for (i = 0; i <= reply_len - MHD_STATICSTR_LEN_ (marker) - 1; ++i) 232 { 233 size_t k; 234 235 if (0 != memcmp (reply + i, marker, MHD_STATICSTR_LEN_ (marker))) 236 continue; 237 i += MHD_STATICSTR_LEN_ (marker); 238 for (k = 0; (i + k < reply_len) && (k + 1 < nonce_size); ++k) 239 { 240 if ('"' == reply[i + k]) 241 { 242 nonce[k] = 0; 243 return (0 != k) ? ! 0 : 0; 244 } 245 if (('\r' == reply[i + k]) || ('\n' == reply[i + k])) 246 return 0; 247 nonce[k] = reply[i + k]; 248 } 249 return 0; 250 } 251 return 0; 252 } 253 254 255 /** 256 * Fill @a buf with @a len hexadecimal digits and zero-terminate it. 257 * 258 * @param[out] buf the buffer to fill 259 * @param len the number of digits to write 260 */ 261 static void 262 fill_hex (char *buf, size_t len) 263 { 264 static const char hex[] = "0123456789abcdef"; 265 size_t i; 266 267 for (i = 0; i < len; ++i) 268 buf[i] = hex[i % 16]; 269 buf[len] = 0; 270 } 271 272 273 /** 274 * Build one hostile "Authorization: Digest" request. 275 * 276 * @param variant the number of the variant to build 277 * @param nonce the server generated nonce, may be an empty string 278 * @param[out] buf the buffer for the request 279 * @param buf_size the size of @a buf 280 * @return the length of the request, or zero on failure 281 */ 282 static size_t 283 build_hostile_request (unsigned int variant, 284 const char *nonce, 285 char *buf, 286 size_t buf_size) 287 { 288 char big1[512]; 289 char big2[512]; 290 int len; 291 292 switch (variant % 10) 293 { 294 case 0: 295 /* An "algorithm" token that MHD does not know. Before the fix of 296 2026-07-27 this made MHD_digest_auth_check3() run into 297 MHD_PANIC("Wrong 'malgo3' value, API violation") and abort(). 298 No valid nonce is needed: the algorithm is checked first. */ 299 len = snprintf (buf, buf_size, 300 "GET " URI_MD5 " HTTP/1.1\r\n" HOST_HDR 301 "Authorization: Digest username=\"" TEST_USERNAME "\", " 302 "realm=\"" TEST_REALM "\", nonce=\"%s\", uri=\"" URI_MD5 303 "\", algorithm=BOGUS-ALGO, qop=auth, nc=00000001, " 304 "cnonce=\"0a0b0c0d\", response=\"" 305 "0123456789abcdef0123456789abcdef\"\r\n" 306 "Connection: close\r\n\r\n", 307 nonce); 308 break; 309 case 1: 310 /* An empty (quoted) "algorithm" value. */ 311 len = snprintf (buf, buf_size, 312 "GET " URI_MD5 " HTTP/1.1\r\n" HOST_HDR 313 "Authorization: Digest username=\"" TEST_USERNAME "\", " 314 "realm=\"" TEST_REALM "\", nonce=\"%s\", uri=\"" URI_MD5 315 "\", algorithm=\"\", qop=auth, nc=00000001, " 316 "cnonce=\"0a0b0c0d\", response=\"" 317 "0123456789abcdef0123456789abcdef\"\r\n" 318 "Connection: close\r\n\r\n", 319 nonce); 320 break; 321 case 2: 322 /* A "-sess" algorithm, which MHD does not support. */ 323 len = snprintf (buf, buf_size, 324 "GET " URI_MD5 " HTTP/1.1\r\n" HOST_HDR 325 "Authorization: Digest username=\"" TEST_USERNAME "\", " 326 "realm=\"" TEST_REALM "\", nonce=\"%s\", uri=\"" URI_MD5 327 "\", algorithm=MD5-sess, qop=auth, nc=00000001, " 328 "cnonce=\"0a0b0c0d\", response=\"" 329 "0123456789abcdef0123456789abcdef\"\r\n" 330 "Connection: close\r\n\r\n", 331 nonce); 332 break; 333 case 3: 334 /* An over-long "response" together with a SHA-256 nonce. Before the 335 fix of 2026-07-27 the hex decoder wrote 64 bytes into a 32 byte 336 stack buffer. This needs a *valid* server nonce, otherwise the 337 nonce check rejects the request before the "response" is decoded. */ 338 fill_hex (big1, 128); 339 len = snprintf (buf, buf_size, 340 "GET " URI_SHA256 " HTTP/1.1\r\n" HOST_HDR 341 "Authorization: Digest username=\"" TEST_USERNAME "\", " 342 "realm=\"" TEST_REALM "\", nonce=\"%s\", uri=\"" 343 URI_SHA256 "\", algorithm=SHA-256, qop=auth, " 344 "nc=00000001, cnonce=\"0a0b0c0d\", response=\"%s\"\r\n" 345 "Connection: close\r\n\r\n", 346 nonce, big1); 347 break; 348 case 4: 349 /* The same, unquoted. */ 350 fill_hex (big1, 128); 351 len = snprintf (buf, buf_size, 352 "GET " URI_SHA256 " HTTP/1.1\r\n" HOST_HDR 353 "Authorization: Digest username=\"" TEST_USERNAME "\", " 354 "realm=\"" TEST_REALM "\", nonce=%s, uri=\"" 355 URI_SHA256 "\", algorithm=SHA-256, qop=auth, " 356 "nc=00000001, cnonce=\"0a0b0c0d\", response=%s\r\n" 357 "Connection: close\r\n\r\n", 358 nonce, big1); 359 break; 360 case 5: 361 /* An over-long hex encoded userhash. */ 362 fill_hex (big1, 256); 363 fill_hex (big2, 64); 364 len = snprintf (buf, buf_size, 365 "GET " URI_SHA256 " HTTP/1.1\r\n" HOST_HDR 366 "Authorization: Digest username=\"%s\", userhash=true, " 367 "realm=\"" TEST_REALM "\", nonce=\"%s\", uri=\"" 368 URI_SHA256 "\", algorithm=SHA-256, qop=auth, " 369 "nc=00000001, cnonce=\"0a0b0c0d\", response=\"%s\"\r\n" 370 "Connection: close\r\n\r\n", 371 big1, nonce, big2); 372 break; 373 case 6: 374 /* A very long username. */ 375 fill_hex (big1, 500); 376 len = snprintf (buf, buf_size, 377 "GET " URI_MD5 " HTTP/1.1\r\n" HOST_HDR 378 "Authorization: Digest username=\"%s\", " 379 "realm=\"" TEST_REALM "\", nonce=\"%s\", uri=\"" URI_MD5 380 "\", algorithm=MD5, qop=auth, nc=00000001, " 381 "cnonce=\"0a0b0c0d\", response=\"" 382 "0123456789abcdef0123456789abcdef\"\r\n" 383 "Connection: close\r\n\r\n", 384 big1, nonce); 385 break; 386 case 7: 387 /* No "response" parameter at all, plus the extended notation for the 388 username. */ 389 len = snprintf (buf, buf_size, 390 "GET " URI_MD5 " HTTP/1.1\r\n" HOST_HDR 391 "Authorization: Digest username*=UTF-8''%%41%%42, " 392 "realm=\"" TEST_REALM "\", nonce=\"%s\", uri=\"" URI_MD5 393 "\", algorithm=MD5, qop=auth, nc=00000001, " 394 "cnonce=\"0a0b0c0d\"\r\n" 395 "Connection: close\r\n\r\n", 396 nonce); 397 break; 398 case 8: 399 /* Garbage in "nc" and an unterminated quoted string. */ 400 len = snprintf (buf, buf_size, 401 "GET " URI_MD5 " HTTP/1.1\r\n" HOST_HDR 402 "Authorization: Digest username=\"" TEST_USERNAME "\", " 403 "realm=\"" TEST_REALM "\", nonce=\"%s\", uri=\"" URI_MD5 404 "\", algorithm=MD5, qop=auth, nc=zzzzzzzz, " 405 "cnonce=\"0a0b0c0d, response=\"" 406 "0123456789abcdef0123456789abcdef\"\r\n" 407 "Connection: close\r\n\r\n", 408 nonce); 409 break; 410 case 9: 411 default: 412 /* A "Digest" scheme without any parameter, and a second, empty 413 "Authorization" header. */ 414 len = snprintf (buf, buf_size, 415 "GET " URI_MD5 " HTTP/1.1\r\n" HOST_HDR 416 "Authorization: Digest\r\n" 417 "Authorization: \r\n" 418 "Connection: close\r\n\r\n"); 419 break; 420 } 421 if ((0 > len) || (buf_size <= (size_t) len)) 422 return 0; 423 return (size_t) len; 424 } 425 426 427 /** 428 * Ask MHD for a challenge and extract the nonce from the reply. 429 * 430 * @param d_extern the daemon to drive, or NULL 431 * @param port the port MHD is listening on 432 * @param uri the URI to request 433 * @param[out] nonce the buffer for the nonce 434 * @param nonce_size the size of @a nonce 435 */ 436 static void 437 fetch_nonce (struct MHD_Daemon *d_extern, 438 uint16_t port, 439 const char *uri, 440 char *nonce, 441 size_t nonce_size) 442 { 443 struct zzuf_raw_part part; 444 char req[256]; 445 char reply[2048]; 446 size_t reply_len = 0; 447 int len; 448 449 nonce[0] = 0; 450 len = snprintf (req, sizeof(req), 451 "GET %s HTTP/1.1\r\n" HOST_HDR "Connection: close\r\n\r\n", 452 uri); 453 if ((0 > len) || (sizeof(req) <= (size_t) len)) 454 return; 455 part.data = req; 456 part.size = (size_t) len; 457 /* Bypass socat (when it is used): a nonce that has been mangled by zzuf 458 is useless for building a request that must survive the nonce check. */ 459 if (ZZUF_RAW_OK != 460 zzuf_raw_exchange2 (d_extern, port, 1, &part, 1, 461 (unsigned int) ZZUF_CLIENT_TIMEOUT, 462 reply, sizeof(reply), &reply_len)) 463 return; 464 (void) extract_nonce (reply, reply_len, nonce, nonce_size); 465 } 466 467 468 static unsigned int 469 client_run (struct MHD_Daemon *d_extern, 470 uint16_t port, 471 void *cls) 472 { 473 struct ahc_param *param = (struct ahc_param *) cls; 474 struct zzuf_curl_driver drv; 475 struct zzuf_curl_sink sink; 476 char nonce_md5[256]; 477 char nonce_sha256[256]; 478 char *req; 479 unsigned int i; 480 unsigned int loops; 481 482 loops = zzuf_loop_count (); 483 if (! zzuf_curl_driver_init (&drv, d_extern)) 484 return 99; /* Not an MHD error */ 485 req = malloc (RAW_BUF_SIZE); 486 if (NULL == req) 487 { 488 zzuf_curl_driver_deinit (&drv); 489 return 99; /* Not an MHD error */ 490 } 491 nonce_md5[0] = 0; 492 nonce_sha256[0] = 0; 493 494 for (i = 0; i < loops; ++i) 495 { 496 CURL *c; 497 struct zzuf_raw_part part; 498 size_t req_len; 499 const char *nonce; 500 501 fprintf (stderr, "."); 502 /* 1. A complete, well-formed digest handshake performed by libcurl. 503 The traffic goes through the fuzzing layer. */ 504 c = zzuf_curl_setup (port, URI_MD5, &sink); 505 if (NULL == c) 506 { 507 free (req); 508 zzuf_curl_driver_deinit (&drv); 509 return 99; /* Not an MHD error */ 510 } 511 if ((CURLE_OK != curl_easy_setopt (c, CURLOPT_HTTPAUTH, 512 (long) CURLAUTH_DIGEST)) || 513 (CURLE_OK != curl_easy_setopt (c, CURLOPT_USERPWD, 514 TEST_USERNAME ":" TEST_PASSWORD))) 515 { 516 curl_easy_cleanup (c); 517 free (req); 518 zzuf_curl_driver_deinit (&drv); 519 return 99; /* Not an MHD error */ 520 } 521 zzuf_curl_driver_perform (&drv, c); 522 curl_easy_cleanup (c); 523 524 /* 2. Refresh the nonces from time to time: MHD only accepts a given 525 nonce/nc combination once. */ 526 if (0 == i % 2) 527 { 528 fetch_nonce (d_extern, port, URI_MD5, nonce_md5, sizeof(nonce_md5)); 529 fetch_nonce (d_extern, port, URI_SHA256, 530 nonce_sha256, sizeof(nonce_sha256)); 531 } 532 533 /* 3. The hand-crafted hostile headers. */ 534 nonce = ((3 == i % 10) || (4 == i % 10) || (5 == i % 10)) ? 535 nonce_sha256 : nonce_md5; 536 req_len = build_hostile_request (i, nonce, req, RAW_BUF_SIZE); 537 if (0 != req_len) 538 { 539 part.data = req; 540 part.size = req_len; 541 /* Once through the fuzzing layer ... */ 542 if (ZZUF_RAW_SETUP_FAILED == 543 zzuf_raw_exchange (d_extern, port, &part, 1, 544 (unsigned int) ZZUF_CLIENT_TIMEOUT)) 545 { 546 free (req); 547 zzuf_curl_driver_deinit (&drv); 548 return 99; /* Not an MHD error */ 549 } 550 /* ... and once verbatim, so that the hostile header is guaranteed 551 to reach the parser unmodified at least once. */ 552 if (ZZUF_RAW_SETUP_FAILED == 553 zzuf_raw_exchange2 (d_extern, port, 1, &part, 1, 554 (unsigned int) ZZUF_CLIENT_TIMEOUT, 555 NULL, 0, NULL)) 556 { 557 free (req); 558 zzuf_curl_driver_deinit (&drv); 559 return 99; /* Not an MHD error */ 560 } 561 } 562 fflush (stderr); 563 } 564 free (req); 565 zzuf_curl_driver_deinit (&drv); 566 567 if (0 != param->err_flag) 568 { 569 fprintf (stderr, "One or more errors have been detected by the access " 570 "handler callback function. At line %d.\n", (int) __LINE__); 571 return 1; 572 } 573 return 0; 574 } 575 576 577 int 578 main (int argc, char *const *argv) 579 { 580 struct ahc_param param; 581 struct zzuf_run_params rp; 582 struct MHD_OptionItem extra_opts[3]; 583 unsigned int res; 584 int use_magic_exit_codes; 585 586 zzuf_parse_common_args (argc, argv); 587 use_magic_exit_codes = zzuf_run_with_socat || zzuf_dry_run; 588 589 if (MHD_NO == MHD_is_feature_supported (MHD_FEATURE_DIGEST_AUTH)) 590 { 591 fprintf (stderr, "Digest Authentication is not supported by this " 592 "MHD build.\n"); 593 return use_magic_exit_codes ? 77 : 0; 594 } 595 res = zzuf_check_runnable (); 596 if (0 != res) 597 return use_magic_exit_codes ? (int) res : 0; 598 599 if (CURLE_OK != curl_global_init (CURL_GLOBAL_WIN32)) 600 { 601 fprintf (stderr, "curl_global_init() failed at line %d.\n", 602 (int) __LINE__); 603 return use_magic_exit_codes ? 99 : 0; 604 } 605 606 param.magic = (unsigned int) TEST_MAGIC_MARKER; 607 param.err_flag = 0; 608 param.num_ok = 0; 609 610 extra_opts[0].option = MHD_OPTION_DIGEST_AUTH_RANDOM; 611 extra_opts[0].value = (intptr_t) sizeof(digest_rnd_data); 612 extra_opts[0].ptr_value = (void *) (intptr_t) digest_rnd_data; 613 extra_opts[1].option = MHD_OPTION_NONCE_NC_SIZE; 614 extra_opts[1].value = (intptr_t) 300; 615 extra_opts[1].ptr_value = NULL; 616 extra_opts[2].option = MHD_OPTION_END; 617 extra_opts[2].value = 0; 618 extra_opts[2].ptr_value = NULL; 619 620 memset (&rp, 0, sizeof(rp)); 621 rp.port = zzuf_pick_port (TEST_PORT_OFFSET); 622 rp.ahc = &ahc_digest; 623 rp.ahc_cls = ¶m; 624 rp.rcc = NULL; 625 rp.rcc_cls = NULL; 626 /* The digest headers are large, use a memory limit that can hold them. */ 627 rp.mem_limit_override = 4096; 628 rp.sweep_profiles = 1; 629 rp.profile_start = 0; 630 rp.client = &client_run; 631 rp.client_cls = ¶m; 632 rp.extra_opts = extra_opts; 633 634 res = zzuf_run_polling_modes (&rp); 635 curl_global_cleanup (); 636 637 if (99 == res) 638 return use_magic_exit_codes ? 99 : 0; 639 if (77 == res) 640 return use_magic_exit_codes ? 77 : 0; 641 return (0 == res) ? 0 : 1; /* 0 == pass */ 642 }