HexBrain

curl · Report #3356088

Race condition on global `gss_context` during SOCKS5 GSS-API negotiation in libcurl

Informativestatus
mediumseverity
17upvotes
26 Sept 2025disclosed

How it was found

Summary:

Concurrent SOCKS5 GSS-API authentications share a file-scope global gss_context without synchronization, causing data races and undefined behavior.

  • Global context defined at:
static gss_ctx_id_t gss_context = GSS_C_NO_CONTEXT;
  • Passed by address into the GSS init routine per negotiation:
gss_major_status = Curl_gss_init_sec_context(data,
                                             &gss_minor_status,
                                             &gss_context,
                                             server,
                                             &Curl_krb5_mech_oid,
                                             NULL,
                                             gss_token,
                                             &gss_send_token,
                                             TRUE,
                                             &gss_ret_flags);

Using Helgrind with libcurl’s DEBUGBUILD GSS stub (CURL_STUB_GSS_CREDS=KRB5) to avoid libkrb5-internal noise, I observed a reproducible data race that directly references the global symbol gss_context:

  • Helgrind evidence (from docker_helgrind.log):
==XXXX== Possible data race during read of size 8 at ...
==XXXX==    at 0x1CEFC0: stub_gss_init_sec_context (curl_gssapi.c:126)
==XXXX==    by 0x1CEE43: Curl_gss_init_sec_context (curl_gssapi.c:329)
==XXXX==    by 0x1BCB3B: Curl_SOCKS5_gssapi_negotiate (socks_gssapi.c:184)
...
==XXXX== This conflicts with a previous write of size 8 by thread #Y
==XXXX==    at 0x1CF4A4: stub_gss_init_sec_context (curl_gssapi.c:264)
==XXXX==    by 0x1CEE43: Curl_gss_init_sec_context (curl_gssapi.c:329)
==XXXX==    by 0x1BCB3B: Curl_SOCKS5_gssapi_negotiate (socks_gssapi.c:184)
==XXXX==  Address ... is 0 bytes inside data symbol "gss_context"

[AI assistance was used to orchestrate the build/run environment and tooling and to help with report writing. All findings, code locations, and outputs cited below were directly observed and verified from the built code and runtime logs.]

Affected version

  • Built from current master (configure summary reported: curl version: 8.17.0-DEV) in an Ubuntu 24.04 (arm64) container
  • GSS-API provider: MIT Kerberos (system libgssapi_krb5), and also tested with curl’s DEBUGBUILD GSS stub via CURL_STUB_GSS_CREDS=KRB5

Steps To Reproduce:

  1. Start an Ubuntu container and install prerequisites:
docker run --rm -it -v "$PWD":/src -w /src ubuntu:24.04 bash -lc '
  set -euo pipefail
  export DEBIAN_FRONTEND=noninteractive
  apt-get update -qq
  apt-get install -y -qq clang make autoconf automake libtool pkg-config libkrb5-dev python3 valgrind > /dev/null
  update-ca-certificates > /dev/null 2>&1 || true
'
  1. Configure and build libcurl (Debug, GSS-API, minimal deps):
docker run --rm -it -v "$PWD":/src -w /src ubuntu:24.04 bash -lc '
  set -euo pipefail
  if [ -f Makefile ]; then make distclean || true; fi
  if [ -x ./buildconf ]; then ./buildconf; else autoreconf -fi; fi
  export CC=clang CFLAGS="-O0 -g -fno-omit-frame-pointer" LDFLAGS=""
  ./configure --enable-debug --with-gssapi --disable-shared \
    --without-ssl --without-libidn2 --without-libpsl --without-libssh2 \
    --without-brotli --without-zstd --without-nghttp2 --without-nghttp3 --without-ngtcp2
  make -j"$(nproc)"
'
  1. Launch a minimal SOCKS5 proxy that selects GSS-API (method 1):
docker run --rm -it -v "$PWD":/src -w /src ubuntu:24.04 bash -lc '
  set -euo pipefail
  cat > proxy.py <<PY
import socket, threading, struct

def handle(c):
    try:
        data=c.recv(262)
        if len(data)<3: return
        c.sendall(b"\x05\x01")
        hdr=c.recv(4)
        if len(hdr)<4: return
        ln=struct.unpack("!H", hdr[2:4])[0]
        _=c.recv(ln)
        c.sendall(b"\x01\x01\x00\x01D")
        hdr=c.recv(4)
        if len(hdr)<4: return
        ln=struct.unpack("!H", hdr[2:4])[0]
        _=c.recv(ln)
        c.sendall(b"\x01\x02\x00\x01\x00")
    finally:
        try: c.close()
        except: pass

s=socket.socket(); s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
s.bind(("0.0.0.0",1081)); s.listen()
print("listening 0.0.0.0 1081", flush=True)
while True:
    c,_=s.accept()
    threading.Thread(target=handle,args=(c,),daemon=True).start()
PY
  python3 proxy.py &
  sleep 1
'
  1. Build the multithreaded PoC using libcurl:
docker run --rm -it -v "$PWD":/src -w /src ubuntu:24.04 bash -lc '
  set -euo pipefail
  cat > test_gss_race.c <<C
#include <pthread.h>
#include <curl/curl.h>
static void *worker(void *u){
  CURL *e=curl_easy_init();
  curl_easy_setopt(e,CURLOPT_URL,(char*)u);
  curl_easy_setopt(e,CURLOPT_PROXY,"socks5://127.0.0.1:1081");
  curl_easy_setopt(e,CURLOPT_SOCKS5_GSSAPI_SERVICE,"rcmd");
  curl_easy_setopt(e,CURLOPT_SOCKS5_AUTH,(long)CURLAUTH_GSSAPI);
  for(int i=0;i<50;++i) curl_easy_perform(e);
  curl_easy_cleanup(e);
  return 0;
}
int main(void){
  curl_global_init(CURL_GLOBAL_DEFAULT);
  enum { N=16 }; pthread_t th[N];
  for(int i=0;i<N;++i) pthread_create(&th[i],0,worker,"http://example.com");
  for(int i=0;i<N;++i) pthread_join(th[i],0);
  curl_global_cleanup();
  return 0;
}
C
  clang -O0 -g -Iinclude test_gss_race.c -Llib/.libs -lcurl -lgssapi_krb5 -lpthread -o test_gss_race
'
  1. Run under Helgrind with curl’s DEBUGBUILD GSS stub to surface races in curl (and reduce libkrb5 noise):
docker run --rm -it -v "$PWD":/src -w /src ubuntu:24.04 bash -lc '
  set -euo pipefail
  export CURL_STUB_GSS_CREDS=KRB5
  valgrind --tool=helgrind --quiet --fair-sched=try ./test_gss_race | tee docker_helgrind.log
'
  1. Verify the race report mentions the global symbol gss_context and the call site in socks_gssapi.c:
  • Look for lines similar to:
Possible data race ...
... Curl_SOCKS5_gssapi_negotiate (socks_gssapi.c:184)
... Address ... is 0 bytes inside data symbol "gss_context"

Supporting Material/References:

  • Source code location of the shared global:
static gss_ctx_id_t gss_context = GSS_C_NO_CONTEXT;
  • Call site passing the shared global by address into the GSS routine:
gss_major_status = Curl_gss_init_sec_context(data,
                                             &gss_minor_status,
                                             &gss_context,
                                             server,
                                             &Curl_krb5_mech_oid,
                                             NULL,
                                             gss_token,
                                             &gss_send_token,
                                             TRUE,
                                             &gss_ret_flags);
  • Helgrind log excerpt (from my run) demonstrating the race on gss_context:
==XXXX== Possible data race during read of size 8 ...
==XXXX==    at 0x1CEFC0: stub_gss_init_sec_context (curl_gssapi.c:126)
==XXXX==    by 0x1CEE43: Curl_gss_init_sec_context (curl_gssapi.c:329)
==XXXX==    by 0x1BCB3B: Curl_SOCKS5_gssapi_negotiate (socks_gssapi.c:184)
...
==XXXX== This conflicts with a previous write ...
==XXXX==    at 0x1CF4A4: stub_gss_init_sec_context (curl_gssapi.c:264)
==XXXX==    by 0x1CEE43: Curl_gss_init_sec_context (curl_gssapi.c:329)
==XXXX==    by 0x1BCB3B: Curl_SOCKS5_gssapi_negotiate (socks_gssapi.c:184)
==XXXX==  Address ... is 0 bytes inside data symbol "gss_context"

Impact

Summary:

The shared global gss_context is accessed and modified concurrently across connections without synchronization. This presents a realistic risk of:

  • Process crash / denial of service due to undefined behavior and races in GSS context handling
  • Authentication failures or misbehavior during SOCKS5 proxy negotiation under load

I did not observe or claim memory disclosure or RCE, the verified impact is concurrency-related instability (DoS/UB) when multiple handles/authentications run concurrently.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original