CVE-2023-28320: siglongjmp race condition
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
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.
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
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.
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.
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.
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.
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)
Source code
| 84 | void 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 | } |
| 863 | enum 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 |
| 964 | clean_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 | } |
| 27 | #define GLOBAL_INIT_IS_THREADSAFE |
| C1: GLOBAL_INIT_IS_THREADSAFE is defined, but no locking is used around curl_jmpenv in current code |
| 76 | #define USE_ALARM_TIMEOUT |
| C1: USE_ALARM_TIMEOUT is defined, enabling the alarm-based timeout codepath | |
| C5: USE_ALARM_TIMEOUT defined in source |
| 3169 | CURLcode 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 | } |
Show 1 lines (not cited by the judge)
| 3133 | #define CURL_VERSION_THREADSAFE (1<<30) /* libcurl API is thread-safe */ |
Show 1 lines (not cited by the judge)
| 47 | #define curl_simple_lock_lock(m) AcquireSRWLockExclusive(m) |