✓ProveIt
Examples / Report #3735193 Saved analysis

CVE-2026-8925: SASL double-free

curl · 2026-05-1415.6s analysis · $0.025 in model calls

What the report claims

Double free in GSASL authentication path when server advertises SCRAM mechanisms and libgsasl fails to start.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2026-8925 SASL double-free

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?
  • Curl_auth_gsasl_is_supported frees gsasl->ctx on gsasl_client_start failure but does not null the pointer (lib/vauth/gsasl.c:49-50).
  • The same gsasldata struct is reused across mechanism probes in sasl_choose_gsasl (lib/curl_sasl.c:360-368), so a failed first probe leaves a dangling pointer.
  • Curl_auth_gsasl_cleanup unconditionally calls gsasl_done(gsasl->ctx) without a NULL guard (lib/vauth/gsasl.c:113), causing a double-free at connection teardown via gsasl_conn_dtor (lib/vauth/vauth.c:207).
  • The trigger is server-controlled: any server advertising SCRAM-SHA-256 or SCRAM-SHA-1 causes the probe; the server need not complete the handshake.
  • Double-free is a heap corruption vulnerability that can lead to code execution or denial of service, and is not listed in the project's 'not security issues' categories.
How this analysis ran 5 claims · 11 citations verified

Checked against curl master on 2026-05-14 (81f950dd90b5). 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
partially grounded
no model · 25 references checked out, 1 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 3945 tokens · $0.0004
3 · Evidence
5 code snippets
real curl source at 81f950dd90b5 · 6 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
18311 tokens · 12.6s · $0.0251
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 and reproducer. The double-free is confirmed: Curl_auth_gsasl_is_supported frees gsasl->ctx on failure without nulling it (gsasl.c:49-50), and Curl_auth_gsasl_cleanup frees it again unconditionally at teardown (gsasl.c:113). The same gsasldata is reused across mechanism probes (curl_sasl.c:360-368), so a server advertising SCRAM-SHA-256 then SCRAM-SHA-1 reliably triggers the bug. This is a valid security issue — a malicious server can cause heap corruption. We will apply the fix (nulling ctx after the first free and adding NULL guards in cleanup).

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

Report claims

C1flaw supported

Curl_auth_gsasl_is_supported() frees gsasl->ctx on a failed gsasl_client_start() but never nulls the pointer

Read the reasoning

The code at lines 47-51 shows gsasl_done(gsasl->ctx) freeing the context on failure, but gsasl->ctx is not set to NULL before return.

C2location supported

lib/vauth/gsasl.c:47-50

Read the reasoning

The cited location lib/vauth/gsasl.c:47-50 matches the provided source lines 47-51 (minor line shift).

C3trigger supported

Server advertises AUTH SCRAM-SHA-256 or AUTH SCRAM-SHA-1 causing gsasl_client_start() to fail

Read the reasoning

sasl_choose_gsasl calls Curl_auth_gsasl_is_supported for each advertised mechanism; a server advertising these mechanisms triggers the probe.

C4impact supported

Double free of gsasl->ctx at connection teardown via gsasl_conn_dtor()

Read the reasoning

Cleanup frees ctx unconditionally; if a prior probe freed it without nulling, a double-free occurs at teardown.

C5reproduction supported

Use LD_PRELOAD shim to force gsasl_client_start() to return GSASL_UNKNOWN_MECHANISM and point curl at a server advertising SCRAM mechanisms

Read the reasoning

The LD_PRELOAD shim forcing GSASL_UNKNOWN_MECHANISM reproduces the failure path; ASan output in the report confirms the double-free on the actual allocations.

Deterministic checks that failed

  • symbol LD_PRELOAD exists in other curl releases but not at 81f950dd90b5
Read the original report (excerpt)

Full report on HackerOne ↗

Hi all, We found a double-free in the GSASL authentication path — `Curl_auth_gsasl_is_supported()` frees `gsasl->ctx` on a failed `gsasl_client_start()` but never nulls the pointer, and then `Curl_auth_gsasl_cleanup()` frees it again unconditionally at connection teardown. The bug lives in two spots. `lib/vauth/gsasl.c:47-50`: ```c res = gsasl_client_start(gsasl->ctx, mech, &gsasl->client); if(res != GSASL_OK) { gsasl_done(gsasl->ctx); /* frees gsasl->ctx ... */ return FALSE; /* ... but gsasl->ctx still holds the freed pointer */ } ``` And the cleanup function at `lib/vauth/gsasl.c:108-115`: ```c void Curl_auth_gsasl_cleanup(struct gsasldata *gsasl) { gsasl_finish(gsasl->client); gsasl->client = NULL; gsasl_done(gsasl->ctx); /* no NULL guard — second free if probe failed */ gsasl->ctx = NULL; } ``` `Curl_auth_gsasl_cleanup()` is called unconditionally at connection close via `gsasl_conn_dtor` at `lib/vauth/vauth.c:201-209`. What makes this land reliably is that `sasl_choose_gsasl()` in `lib/curl_sasl.c:354-383` allocates a single `struct gsasldata` for the connection and passes the same pointer to each mechanism probe: ```c gsasl = Curl_auth_gsasl_get(sctx->conn); /* one struct, reused across all probes */ if((sctx->enabledmechs & SASL_MECH_SCRAM_SHA_256) && Curl_auth_gsasl_is_supported(data, SASL_MECH_STRING_SCRAM_SHA_256, gsasl)) { ... } else if((sctx->enabledmechs & SASL_MECH_SCRAM_SHA_1) && Curl_auth_gsasl_is_supported(data, SASL_MECH_STRING_SCRAM_SHA_1, gsasl)) { ... } ``` So if `SCRAM-SHA-256` fails, `gsasl->ctx` holds a freed pointer when `SCRAM-SHA-1` is tried. That second call reinitialises `gsasl->ctx` with a fresh allocation — but if it also fails, another free-without-null occurs. Whatever stale pointer `gsasl->ctx` holds at the end of mechanism selection is what `gsasl_conn_dtor` passes to `gsasl_done()` at teardown. The trigger is server-controlled: any server that advertises `AUTH SCRAM-SHA-256` (or `SCRAM-SHA-1`) in its capability response causes libcurl to call `Curl_auth_gsasl_is_supported()`. The server does not need to proceed with the handshake; sending the capability advertisement and dropping is sufficient. `gsasl_client_start()` can fail — and thus trigger this bug without any shim — under several real conditions: - **libgsasl < 1.4.0** (pre-2011): neither SCRAM-SHA-1 nor SCRA …

Source code

lib/vauth/gsasl.clines 22–75 · lines 47-50 cited by the report
22 *
23 * RFC5802 SCRAM-SHA-1 authentication
24 *
25 ***************************************************************************/
26#include "curl_setup.h"
27
28#ifdef USE_GSASL
29
30#include "vauth/vauth.h"
31#include "curl_trc.h"
32
33#include <gsasl.h>
34
35bool Curl_auth_gsasl_is_supported(struct Curl_easy *data,
36 const char *mech,
37 struct gsasldata *gsasl)
38{
39 int res;
40
41 res = gsasl_init(&gsasl->ctx);
C5: gsasl_init allocates ctx
42 if(res != GSASL_OK) {
43 failf(data, "gsasl init: %s", gsasl_strerror(res));
44 return FALSE;
45 }
46
47 res = gsasl_client_start(gsasl->ctx, mech, &gsasl->client);
C2: gsasl_client_start call at line 47
48 if(res != GSASL_OK) {
49 gsasl_done(gsasl->ctx);
C1: gsasl_done(gsasl->ctx) called but gsasl->ctx not nulled
C2: gsasl_done call at line 49
C5: first free on failure
50 return FALSE;
C1: return FALSE without clearing ctx pointer
51 }
52
53 return TRUE;
54}
55
56CURLcode Curl_auth_gsasl_start(struct Curl_easy *data,
57 struct Curl_creds *creds,
58 struct gsasldata *gsasl)
59{
60#if GSASL_VERSION_NUMBER >= 0x010b00
61 int res;
62 res =
63#endif
64 gsasl_property_set(gsasl->client, GSASL_AUTHID, creds->user);
65#if GSASL_VERSION_NUMBER >= 0x010b00
66 if(res != GSASL_OK) {
67 failf(data, "setting AUTHID failed: %s", gsasl_strerror(res));
68 return CURLE_OUT_OF_MEMORY;
69 }
70#endif
71
72#if GSASL_VERSION_NUMBER >= 0x010b00
73 res =
74#endif
75 gsasl_property_set(gsasl->client, GSASL_PASSWORD, creds->passwd);
lib/vauth/gsasl.clines 83–116 · lines 108-115 cited by the report
83 (void)data;
84
85 return CURLE_OK;
86}
87
88CURLcode Curl_auth_gsasl_token(struct Curl_easy *data,
89 const struct bufref *chlg,
90 struct gsasldata *gsasl,
91 struct bufref *out)
92{
93 int res;
94 char *response;
95 size_t outlen;
96
97 res = gsasl_step(gsasl->client, Curl_bufref_ptr(chlg), Curl_bufref_len(chlg),
98 &response, &outlen);
99 if(res != GSASL_OK && res != GSASL_NEEDS_MORE) {
100 failf(data, "GSASL step: %s", gsasl_strerror(res));
101 return CURLE_BAD_CONTENT_ENCODING;
102 }
103
104 Curl_bufref_set(out, response, outlen, gsasl_free);
105 return CURLE_OK;
106}
107
108void Curl_auth_gsasl_cleanup(struct gsasldata *gsasl)
109{
110 gsasl_finish(gsasl->client);
111 gsasl->client = NULL;
112
113 gsasl_done(gsasl->ctx);
C4: gsasl_done(gsasl->ctx) called unconditionally without NULL guard
C5: second free at cleanup
114 gsasl->ctx = NULL;
115}
116#endif
lib/vauth/vauth.clines 176–234 · lines 201-209 cited by the report
176#ifdef USE_KERBEROS5
177static void krb5_conn_dtor(void *key, size_t klen, void *entry)
178{
179 struct kerberos5data *krb5 = entry;
180 (void)key;
181 (void)klen;
182 DEBUGASSERT(krb5);
183 Curl_auth_cleanup_gssapi(krb5);
184 curlx_free(krb5);
185}
186
187struct kerberos5data *Curl_auth_krb5_get(struct connectdata *conn)
188{
189 struct kerberos5data *krb5 = Curl_conn_meta_get(conn, CURL_META_KRB5_CONN);
190 if(!krb5) {
191 krb5 = curlx_calloc(1, sizeof(*krb5));
192 if(!krb5 ||
193 Curl_conn_meta_set(conn, CURL_META_KRB5_CONN, krb5, krb5_conn_dtor))
194 return NULL;
195 }
196 return krb5;
197}
198#endif /* USE_KERBEROS5 */
199
200#ifdef USE_GSASL
201static void gsasl_conn_dtor(void *key, size_t klen, void *entry)
202{
203 struct gsasldata *gsasl = entry;
204 (void)key;
205 (void)klen;
206 DEBUGASSERT(gsasl);
207 Curl_auth_gsasl_cleanup(gsasl);
C4: gsasl_conn_dtor calls Curl_auth_gsasl_cleanup at connection teardown
208 curlx_free(gsasl);
209}
210
211struct gsasldata *Curl_auth_gsasl_get(struct connectdata *conn)
212{
213 struct gsasldata *gsasl = Curl_conn_meta_get(conn, CURL_META_GSASL_CONN);
214 if(!gsasl) {
215 gsasl = curlx_calloc(1, sizeof(*gsasl));
216 if(!gsasl ||
217 Curl_conn_meta_set(conn, CURL_META_GSASL_CONN, gsasl, gsasl_conn_dtor))
218 return NULL;
219 }
220 return gsasl;
221}
222#endif /* USE_GSASL */
223
224#ifdef USE_SPNEGO
225static void nego_conn_dtor(void *key, size_t klen, void *entry)
226{
227 struct negotiatedata *nego = entry;
228 (void)key;
229 (void)klen;
230 DEBUGASSERT(nego);
231 Curl_auth_cleanup_spnego(nego);
232 curlx_free(nego);
233}
234
lib/curl_sasl.clines 329–408 · lines 354-383 cited by the report
329 sctx->sasl->authused = SASL_MECH_GSSAPI;
330
331 if(sctx->sasl->force_ir || data->set.sasl_ir) {
332 struct kerberos5data *krb5 = Curl_auth_krb5_get(sctx->conn);
333 sctx->result = !krb5 ? CURLE_OUT_OF_MEMORY :
334 Curl_auth_create_gssapi_user_message(data, sctx->conn->creds,
335 service,
336 sctx->conn->origin->hostname,
337 (bool)sctx->sasl->mutual_auth,
338 NULL, krb5, &sctx->resp);
339 }
340 return TRUE;
341 }
342 return FALSE;
343}
344#endif /* USE_KERBEROS5 */
345
346#ifdef USE_GSASL
347static bool sasl_choose_gsasl(struct Curl_easy *data, struct sasl_ctx *sctx)
348{
349 struct gsasldata *gsasl;
350 struct bufref nullmsg;
351
352 if((sctx->enabledmechs &
353 (SASL_MECH_SCRAM_SHA_256 | SASL_MECH_SCRAM_SHA_1))) {
354 gsasl = Curl_auth_gsasl_get(sctx->conn);
355 if(!gsasl) {
356 sctx->result = CURLE_OUT_OF_MEMORY;
357 return TRUE; /* attempted, but failed */
358 }
359
360 if((sctx->enabledmechs & SASL_MECH_SCRAM_SHA_256) &&
C3: SCRAM-SHA-256 check calls Curl_auth_gsasl_is_supported
361 Curl_auth_gsasl_is_supported(data, SASL_MECH_STRING_SCRAM_SHA_256,
362 gsasl)) {
363 sctx->mech = SASL_MECH_STRING_SCRAM_SHA_256;
364 sctx->sasl->authused = SASL_MECH_SCRAM_SHA_256;
365 }
366 else if((sctx->enabledmechs & SASL_MECH_SCRAM_SHA_1) &&
C3: SCRAM-SHA-1 check calls Curl_auth_gsasl_is_supported
367 Curl_auth_gsasl_is_supported(data, SASL_MECH_STRING_SCRAM_SHA_1,
368 gsasl)) {
369 sctx->mech = SASL_MECH_STRING_SCRAM_SHA_1;
370 sctx->sasl->authused = SASL_MECH_SCRAM_SHA_1;
371 }
372 else
373 return FALSE;
374
375 Curl_bufref_init(&nullmsg);
376 sctx->state1 = SASL_GSASL;
377 sctx->state2 = SASL_GSASL;
378 sctx->result = Curl_auth_gsasl_start(data, sctx->conn->creds, gsasl);
379 if(!sctx->result && (sctx->sasl->force_ir || data->set.sasl_ir))
380 sctx->result = Curl_auth_gsasl_token(data, &nullmsg, gsasl, &sctx->resp);
381 return TRUE;
382 }
383 return FALSE;
384}
385
386#endif /* USE_GSASL */
387
388#ifndef CURL_DISABLE_DIGEST_AUTH
389static bool sasl_choose_digest(struct Curl_easy *data, struct sasl_ctx *sctx)
390{
391 (void)data;
392 if((sctx->enabledmechs & SASL_MECH_DIGEST_MD5) &&
393 Curl_auth_is_digest_supported()) {
394 sctx->mech = SASL_MECH_STRING_DIGEST_MD5;
395 sctx->state1 = SASL_DIGESTMD5;
396 sctx->sasl->authused = SASL_MECH_DIGEST_MD5;
397 return TRUE;
398 }
399 else if(sctx->enabledmechs & SASL_MECH_CRAM_MD5) {
400 sctx->mech = SASL_MECH_STRING_CRAM_MD5;
401 sctx->state1 = SASL_CRAMMD5;
402 sctx->sasl->authused = SASL_MECH_CRAM_MD5;
403 return TRUE;
404 }
405 return FALSE;
406}
407#endif /* !CURL_DISABLE_DIGEST_AUTH */
408
lib/curl_sasl.c@curl-8_20_0lines 346–384 · definition of sasl_choose_gsasl as in release curl-8_20_0; differs from master; commits that changed it since: 8f71d0fde5 creds: hold credentials
Show 39 lines (not cited by the judge)
346static bool sasl_choose_gsasl(struct Curl_easy *data, struct sasl_ctx *sctx)
347{
348 struct gsasldata *gsasl;
349 struct bufref nullmsg;
350
351 if((sctx->enabledmechs &
352 (SASL_MECH_SCRAM_SHA_256 | SASL_MECH_SCRAM_SHA_1))) {
353 gsasl = Curl_auth_gsasl_get(sctx->conn);
354 if(!gsasl) {
355 sctx->result = CURLE_OUT_OF_MEMORY;
356 return TRUE; /* attempted, but failed */
357 }
358
359 if((sctx->enabledmechs & SASL_MECH_SCRAM_SHA_256) &&
360 Curl_auth_gsasl_is_supported(data, SASL_MECH_STRING_SCRAM_SHA_256,
361 gsasl)) {
362 sctx->mech = SASL_MECH_STRING_SCRAM_SHA_256;
363 sctx->sasl->authused = SASL_MECH_SCRAM_SHA_256;
364 }
365 else if((sctx->enabledmechs & SASL_MECH_SCRAM_SHA_1) &&
366 Curl_auth_gsasl_is_supported(data, SASL_MECH_STRING_SCRAM_SHA_1,
367 gsasl)) {
368 sctx->mech = SASL_MECH_STRING_SCRAM_SHA_1;
369 sctx->sasl->authused = SASL_MECH_SCRAM_SHA_1;
370 }
371 else
372 return FALSE;
373
374 Curl_bufref_init(&nullmsg);
375 sctx->state1 = SASL_GSASL;
376 sctx->state2 = SASL_GSASL;
377 sctx->result = Curl_auth_gsasl_start(data, sctx->conn->user,
378 sctx->conn->passwd, gsasl);
379 if(!sctx->result && (sctx->sasl->force_ir || data->set.sasl_ir))
380 sctx->result = Curl_auth_gsasl_token(data, &nullmsg, gsasl, &sctx->resp);
381 return TRUE;
382 }
383 return FALSE;
384}