✓ProveIt
Examples / Report #1929597 Saved analysis

CVE-2023-28320: siglongjmp race condition

curl · 2023-04-0211.5s analysis · $0.017 in model calls

What the report claims

A race condition in the USE_ALARM_TIMEOUT codepath of lib/hostip.c can cause a segmentation fault during concurrent DNS resolution, potentially leading to denial of service.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2023-28320 siglongjmp race condition

Shown for comparison after analysis. The pipeline did not see this label.

ProveIt's finding

Code supports the claim

The code supports the reported flaw. A maintainer should review its impact and decide what to do next.

Why this finding?
  • Global curl_jmpenv used without locking in USE_ALARM_TIMEOUT codepath (lib/hostip.c:915)
  • Comment at line 912 acknowledges known problems with this technique
  • Multiple threads overwrite each other's sigsetjmp context, causing siglongjmp to jump to wrong stack frame
  • Attacker in privileged network position can induce DNS timeouts to trigger the race
  • Results in segmentation fault (denial of service)
  • GLOBAL_INIT_IS_THREADSAFE and curl_simple_lock_lock exist but are not used to protect this codepath
How this analysis ran 5 claims · 11 citations verified

Checked against curl master on 2023-04-02 (a13ef31d0fbb). Test evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.

1 · Grounding
grounded
no model · 11 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 3177 tokens · $0.0003
3 · Evidence
7 code snippets
real curl source at a13ef31d0fbb · 5 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
11930 tokens · 9.5s · $0.0172
5 · Receipts
11 of 11 citations verified
every citation must point at code the judge was shown
Review a draft reply to the reporter

Edit and verify before sending.

Thank you for the detailed report. The race condition in the USE_ALARM_TIMEOUT codepath is confirmed: lib/hostip.c:915 uses global curl_jmpenv without any locking, and the comment at line 912 acknowledges known problems. Multiple threads doing DNS resolution with timeouts will overwrite each other's jump buffer, causing siglongjmp to jump to an invalid context and crash. This is a valid denial-of-service issue reachable by an attacker who can block DNS responses. The fix should add locking around the USE_ALARM_TIMEOUT section when GLOBAL_INIT_IS_THREADSAFE is available, as you proposed.

Inspect all claims and source code 5 claims · 7 code excerpts

Report claims

C1flaw supported

siglongjmp race condition when multiple threads perform DNS resolving in USE_ALARM_TIMEOUT codepath

Read the reasoning

The USE_ALARM_TIMEOUT codepath uses a global sigjmp_buf (curl_jmpenv) at line 915 with no locking. Multiple threads calling Curl_resolv_timeout concurrently will overwrite each other's jump buffer. When a SIGALRM fires, siglongjmp uses whichever thread's context was last stored, causing a crash.

C2location supported

lib/hostip.c

Read the reasoning

The flaw is in lib/hostip.c in the Curl_resolv_timeout function where USE_ALARM_TIMEOUT codepath executes.

C3trigger supported

DNS timeout occurs while two threads are resolving

Read the reasoning

When two threads resolve names simultaneously with timeouts, the second thread's sigsetjmp overwrites the first thread's curl_jmpenv. If the first thread's alarm then fires, siglongjmp jumps to the second thread's context.

C4impact supported

segmentation fault

Read the reasoning

The gdb backtrace in the report shows segmentation fault in Curl_failf after siglongjmp, consistent with jumping to an invalid stack frame from another thread.

C5reproduction supported

compile with USE_ALARM_TIMEOUT defined, add CURLOPT_TIMEOUT, point DNS to blackhole server, run multithread test

Read the reasoning

The reproduction steps are feasible: define USE_ALARM_TIMEOUT, set CURLOPT_TIMEOUT, use a blackhole DNS server to trigger timeouts, run multithreaded test.

What the report says about other projects

not addressed It is important that libcurl can find and use thread safe versions of these and other system calls, as otherwise it cannot function fully thread safe.

not addressed CURL_VERSION_THREADSAFE checks availability of atomic init, not MT safety of DNS resolution.

Read the original report (excerpt)

Full report on HackerOne ↗

## Summary: If the system has no POSIX or Windows threading support, `USE_ALARM_TIMEOUT` codepath will be used in `lib/hostip.c`. If two threads will perform DNS resolving, a wrong register context can be used on the signal handler`siglongjmp` call if DNS timeout occurs. Typically this results in segmentation fault, but depending on platform specifics other impacts might be possible (but unlikely). The documentation warns against this very issue in https://curl.se/libcurl/c/threadsafe.html `It is important that libcurl can find and use thread safe versions of these and other system calls, as otherwise it cannot function fully thread safe.` The issue is that there is no way for the application using libcurl to know if the library is MT safe for DNS resolution or not. `CURL_VERSION_THREADSAFE` is mentioned, but this checks availability of atomic init, not MT safety of DNS resolution. A remote attacker in a privileged network position is able to selectively block the DNS responses and may thus induce the affected target application to crash. ## Steps To Reproduce: 1. For quick testing on POSIX systems add `#define USE_ALARM_TIMEOUT` to `lib/hostip.c`, for example: ``` diff --git a/lib/hostip.c b/lib/hostip.c index 2381290fd..0148f2861 100644 --- a/lib/hostip.c +++ b/lib/hostip.c @@ -75,6 +75,7 @@ /* alarm-based timeouts can only be used with all the dependencies satisfied */ #define USE_ALARM_TIMEOUT #endif +#define USE_ALARM_TIMEOUT #define MAX_HOSTCACHE_LEN (255 + 7) /* max FQDN + colon + port number + zero */ ``` 2. Compile libcurl 3. Compile version of https://curl.se/libcurl/c/multithread.html but add `curl_easy_setopt(curl, CURLOPT_TIMEOUT, 2);` to `pull_one_url` function. 4. Change DNS config to point to blackhole DNS server at `3.219.212.117` (blackhole.webpagetest.org) 5. Execute the compiled `multithread` and the application will segfault. ``` $ LD_LIBRARY_PATH=./lib/.libs:$LD_LIBRARY_PATH gdb ./multithread GNU gdb (Debian 13.1-2) 13.1 Copyright (C) 2023 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration detai …

Source code

lib/curl_log.clines 84–103 · definition of Curl_failf
84void Curl_failf(struct Curl_easy *data, const char *fmt, ...)
C4: Curl_failf definition - the gdb backtrace shows crash in this function
85{
86 DEBUGASSERT(!strchr(fmt, '\n'));
87 if(data->set.verbose || data->set.errorbuffer) {
88 va_list ap;
89 int len;
90 char error[CURL_ERROR_SIZE + 2];
91 va_start(ap, fmt);
92 len = mvsnprintf(error, CURL_ERROR_SIZE, fmt, ap);
93
94 if(data->set.errorbuffer && !data->state.errorbuf) {
95 strcpy(data->set.errorbuffer, error);
96 data->state.errorbuf = TRUE; /* wrote error string */
97 }
98 error[len++] = '\n';
99 error[len] = '\0';
100 Curl_debug(data, CURLINFO_TEXT, error, len);
101 va_end(ap);
102 }
103}
lib/hostip.clines 863–1009 · definition of Curl_resolv_timeout
863enum resolve_t Curl_resolv_timeout(struct Curl_easy *data,
C2: Curl_resolv_timeout function definition containing the USE_ALARM_TIMEOUT codepath
864 const char *hostname,
865 int port,
866 struct Curl_dns_entry **entry,
867 timediff_t timeoutms)
868{
869#ifdef USE_ALARM_TIMEOUT
870#ifdef HAVE_SIGACTION
871 struct sigaction keep_sigact; /* store the old struct here */
872 volatile bool keep_copysig = FALSE; /* whether old sigact has been saved */
873 struct sigaction sigact;
874#else
875#ifdef HAVE_SIGNAL
876 void (*keep_sigact)(int); /* store the old handler here */
877#endif /* HAVE_SIGNAL */
878#endif /* HAVE_SIGACTION */
879 volatile long timeout;
880 volatile unsigned int prev_alarm = 0;
881#endif /* USE_ALARM_TIMEOUT */
882 enum resolve_t rc;
883
884 *entry = NULL;
885
886 if(timeoutms < 0)
887 /* got an already expired timeout */
888 return CURLRESOLV_TIMEDOUT;
889
890#ifdef USE_ALARM_TIMEOUT
891 if(data->set.no_signal)
892 /* Ignore the timeout when signals are disabled */
893 timeout = 0;
894 else
895 timeout = (timeoutms > LONG_MAX) ? LONG_MAX : (long)timeoutms;
896
897 if(!timeout)
898 /* USE_ALARM_TIMEOUT defined, but no timeout actually requested */
899 return Curl_resolv(data, hostname, port, TRUE, entry);
900
901 if(timeout < 1000) {
902 /* The alarm() function only provides integer second resolution, so if
903 we want to wait less than one second we must bail out already now. */
904 failf(data,
905 "remaining timeout of %ld too small to resolve via SIGALRM method",
906 timeout);
907 return CURLRESOLV_TIMEDOUT;
908 }
909 /* This allows us to time-out from the name resolver, as the timeout
910 will generate a signal and we will siglongjmp() from that here.
911 This technique has problems (see alarmfunc).
912 This should be the last thing we do before calling Curl_resolv(),
C1: Comment acknowledges 'This technique has problems (see alarmfunc)'
913 as otherwise we'd have to worry about variables that get modified
914 before we invoke Curl_resolv() (and thus use "volatile"). */
915 if(sigsetjmp(curl_jmpenv, 1)) {
C1: sigsetjmp(curl_jmpenv, 1) uses global curl_jmpenv without any thread synchronization
C3: sigsetjmp establishes the jump buffer that siglongjmp will use on timeout
916 /* this is coming from a siglongjmp() after an alarm signal */
917 failf(data, "name lookup timed out");
C4: failf called after siglongjmp returns, which leads to Curl_failf
918 rc = CURLRESOLV_ERROR;
919 goto clean_up;
920 }
921 else {
922 /*************************************************************
923 * Set signal handler to catch SIGALRM
924 * Store the old value to be able to set it back later!
925 *************************************************************/
926#ifdef HAVE_SIGACTION
927 sigaction(SIGALRM, NULL, &sigact);
928 keep_sigact = sigact;
929 keep_copysig = TRUE; /* yes, we have a copy */
930 sigact.sa_handler = alarmfunc;
931#ifdef SA_RESTART
932 /* HPUX doesn't have SA_RESTART but defaults to that behavior! */
933 sigact.sa_flags &= ~SA_RESTART;
934#endif
935 /* now set the new struct */
936 sigaction(SIGALRM, &sigact, NULL);
937#else /* HAVE_SIGACTION */
938 /* no sigaction(), revert to the much lamer signal() */
939#ifdef HAVE_SIGNAL
940 keep_sigact = signal(SIGALRM, alarmfunc);
941#endif
942#endif /* HAVE_SIGACTION */
943
944 /* alarm() makes a signal get sent when the timeout fires off, and that
945 will abort system calls */
946 prev_alarm = alarm(curlx_sltoui(timeout/1000L));
C3: alarm() sets a timer that will send SIGALRM when timeout expires
947 }
948
949#else
950#ifndef CURLRES_ASYNCH
951 if(timeoutms)
952 infof(data, "timeout on name lookup is not supported");
953#else
954 (void)timeoutms; /* timeoutms not used with an async resolver */
955#endif
956#endif /* USE_ALARM_TIMEOUT */
957
958 /* Perform the actual name resolution. This might be interrupted by an
959 * alarm if it takes too long.
960 */
961 rc = Curl_resolv(data, hostname, port, TRUE, entry);
962
963#ifdef USE_ALARM_TIMEOUT
964clean_up:
965
966 if(!prev_alarm)
967 /* deactivate a possibly active alarm before uninstalling the handler */
968 alarm(0);
969
970#ifdef HAVE_SIGACTION
971 if(keep_copysig) {
972 /* we got a struct as it looked before, now put that one back nice
973 and clean */
974 sigaction(SIGALRM, &keep_sigact, NULL); /* put it back */
975 }
976#else
977#ifdef HAVE_SIGNAL
978 /* restore the previous SIGALRM handler */
979 signal(SIGALRM, keep_sigact);
980#endif
981#endif /* HAVE_SIGACTION */
982
983 /* switch back the alarm() to either zero or to what it was before minus
984 the time we spent until now! */
985 if(prev_alarm) {
986 /* there was an alarm() set before us, now put it back */
987 timediff_t elapsed_secs = Curl_timediff(Curl_now(),
988 data->conn->created) / 1000;
989
990 /* the alarm period is counted in even number of seconds */
991 unsigned long alarm_set = (unsigned long)(prev_alarm - elapsed_secs);
992
993 if(!alarm_set ||
994 ((alarm_set >= 0x80000000) && (prev_alarm < 0x80000000)) ) {
995 /* if the alarm time-left reached zero or turned "negative" (counted
996 with unsigned values), we should fire off a SIGALRM here, but we
997 won't, and zero would be to switch it off so we never set it to
998 less than 1! */
999 alarm(1);
1000 rc = CURLRESOLV_TIMEDOUT;
1001 failf(data, "Previous alarm fired off");
1002 }
1003 else
1004 alarm((unsigned int)alarm_set);
1005 }
1006#endif /* USE_ALARM_TIMEOUT */
1007
1008 return rc;
1009}
lib/easy_lock.hlines 27–27 · definition of GLOBAL_INIT_IS_THREADSAFE
27#define GLOBAL_INIT_IS_THREADSAFE
C1: GLOBAL_INIT_IS_THREADSAFE is defined, but no locking is used around curl_jmpenv in current code
lib/hostip.clines 76–76 · definition of USE_ALARM_TIMEOUT
76#define USE_ALARM_TIMEOUT
C1: USE_ALARM_TIMEOUT is defined, enabling the alarm-based timeout codepath
C5: USE_ALARM_TIMEOUT defined in source
lib/setopt.clines 3169–3183 · definition of curl_easy_setopt
3169CURLcode curl_easy_setopt(struct Curl_easy *data, CURLoption tag, ...)
C5: curl_easy_setopt supports CURLOPT_TIMEOUT
3170{
3171 va_list arg;
3172 CURLcode result;
3173
3174 if(!data)
3175 return CURLE_BAD_FUNCTION_ARGUMENT;
3176
3177 va_start(arg, tag);
3178
3179 result = Curl_vsetopt(data, tag, arg);
3180
3181 va_end(arg);
3182 return result;
3183}
include/curl/curl.hlines 3133–3133 · definition of CURL_VERSION_THREADSAFE
Show 1 lines (not cited by the judge)
3133#define CURL_VERSION_THREADSAFE (1<<30) /* libcurl API is thread-safe */
lib/easy_lock.hlines 47–47 · definition of curl_simple_lock_lock
Show 1 lines (not cited by the judge)
47#define curl_simple_lock_lock(m) AcquireSRWLockExclusive(m)