Resumen
El equipo AISLE Research (cuenta giant_anteater) encontró una doble liberación de memoria (double free, CWE-415) en la autenticación SASL de libcurl cuando se compila con soporte de GSASL. Se le asignó el CVE-2026-8925, con severidad media. El reporte se envió el 14 de mayo de 2026 y se divulgó el 24 de junio de 2026, ya resuelto.
El fallo está repartido en dos funciones de lib/vauth/gsasl.c. La primera, Curl_auth_gsasl_is_supported() (líneas 47-50), comprueba si un mecanismo SASL se puede usar. Si gsasl_client_start() falla, libera el contexto con gsasl_done(), pero deja el puntero gsasl->ctx apuntando a la memoria ya liberada:
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 */
}
La segunda, Curl_auth_gsasl_cleanup() (líneas 108-115), se ejecuta siempre al cerrar la conexión (a través de gsasl_conn_dtor, en lib/vauth/vauth.c:201-209) y llama a gsasl_done() sin comprobar si el puntero sigue siendo válido:
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;
}
Lo que hace que el fallo se dispare de forma fiable es cómo se eligen los mecanismos. En lib/curl_sasl.c (líneas 354-383), sasl_choose_gsasl() reserva una única estructura struct gsasldata por conexión y la reutiliza para probar SCRAM-SHA-256 y, si no funciona, SCRAM-SHA-1:
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)) {
...
}
Si falla SCRAM-SHA-256, gsasl->ctx contiene un puntero liberado cuando se prueba SCRAM-SHA-1. Esa segunda llamada vuelve a inicializar el contexto, pero si también falla, se repite la liberación sin anular. El puntero que quede al terminar la selección es el que gsasl_conn_dtor vuelve a liberar al cerrar.
Quién lo controla
Lo decide el servidor: basta con que anuncie SCRAM-SHA-256 o SCRAM-SHA-1 en su respuesta de capacidades (EHLO en SMTP, CAPABILITY en IMAP) para que libcurl llame a Curl_auth_gsasl_is_supported(). No hace falta completar el intercambio; anunciarlo y cortar la conexión es suficiente.
Para que se produzca la doble liberación, además, gsasl_client_start() tiene que fallar. Según el reporte, eso ocurre en situaciones reales:
- libgsasl anterior a 1.4.0 (de antes de 2011): no conoce ningún mecanismo SCRAM, devuelve
GSASL_UNKNOWN_MECHANISMy el fallo se da en cada conexión. - Falta de memoria (
GSASL_MALLOC_ERROR):gsasl_client_starthace internamente uncalloc(1, 0xf0); posible en contenedores con la memoria limitada. - libgsasl compilada con
--disable-client: devuelveGSASL_NO_CLIENT_CODE. - Versión distinta en ejecución: curl compilado contra libgsasl 2.x que carga una 1.3.x.
Pasos de reproducción
Como las condiciones naturales son poco habituales, la prueba de concepto fuerza el fallo de gsasl_client_start() con una biblioteca cargada mediante LD_PRELOAD. Solo cambia el código de retorno; gsasl_init y gsasl_done siguen siendo los reales, así que AddressSanitizer detecta la doble liberación sobre memoria auténtica de libgsasl.
- Compilar curl con GSASL y AddressSanitizer. El investigador usó CMake con
-DCURL_USE_GSASL=ON -DCURL_USE_OPENSSL=ON -DBUILD_SHARED_LIBS=OFFy las opciones-fsanitize=address -g3 -O0, con libgsasl 2.2.1 (también vale una compilación--with-gsasl --enable-debugcon ASan).
- Preparar un servidor IMAP o SMTP (el investigador usó un IMAP simulado) que anuncie
AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1en sus capacidades.
- Crear la biblioteca que fuerza el fallo:
/* gsasl_shim.c */
#include <gsasl.h>
int gsasl_client_start(Gsasl *ctx, const char *mech, Gsasl_session **out)
{
(void)ctx; (void)mech; (void)out;
return GSASL_UNKNOWN_MECHANISM;
}
- Compilarla y lanzar curl contra el servidor:
gcc -shared -fPIC -o gsasl_shim.so gsasl_shim.c $(pkg-config --cflags gsasl)
LD_PRELOAD=./gsasl_shim.so ASAN_OPTIONS=detect_leaks=0 \
./src/curl -v imaps://user:[email protected]/
El reporte incluye también un programa mínimo con libcurl que hace lo mismo por SMTP:
#include <curl/curl.h>
int main(void)
{
CURL *easy;
curl_global_init(CURL_GLOBAL_DEFAULT);
easy = curl_easy_init();
curl_easy_setopt(easy, CURLOPT_URL, "smtps://mail.example.com/");
curl_easy_setopt(easy, CURLOPT_USERNAME, "[email protected]");
curl_easy_setopt(easy, CURLOPT_PASSWORD, "hunter2");
curl_easy_perform(easy); /* sasl_choose_gsasl() -> failed probe */
curl_easy_cleanup(easy); /* gsasl_conn_dtor() -> double-free */
curl_global_cleanup();
return 0;
}
- AddressSanitizer detiene el programa al cerrar la conexión. La primera liberación aparece en
Curl_auth_gsasl_is_supported(gsasl.c:49) y la segunda enCurl_auth_gsasl_cleanup(gsasl.c:113), llamada desdegsasl_conn_dtor:
=================================================================
==2529510==ERROR: AddressSanitizer: attempting double-free on 0x6f2bd1fe5880 in thread T0:
#0 0x716bd3f212ab in free asan_malloc_linux.cpp:51
#1 0x716bd34efb04 in gsasl_done (libgsasl.so.18+0x4b04)
#2 0x6371f1ea7b8f in Curl_auth_gsasl_cleanup gsasl.c:113 ← second free
#3 0x6371f1d905f5 in gsasl_conn_dtor vauth.c:207
#4 0x6371f1d6ddaf in Curl_conn_free url.c:520
...
#10 0x6371f1d1c1bf in multi_done_locked multi.c:662
freed by thread T0 here:
#0 0x716bd3f212ab in free asan_malloc_linux.cpp:51
#1 0x716bd34efb04 in gsasl_done (libgsasl.so.18+0x4b04)
#2 0x6371f1ea772f in Curl_auth_gsasl_is_supported gsasl.c:49 ← first free
#3 0x6371f1eeae85 in sasl_choose_gsasl curl_sasl.c:367
#4 0x6371f1eec6eb in Curl_sasl_start curl_sasl.c:536
#5 0x6371f1efd033 in imap_perform_authentication imap.c:713
#6 0x6371f1eff078 in imap_state_capability_resp imap.c:1092
previously allocated by thread T0 here:
#0 0x716bd3f2154b in realloc asan_malloc_linux.cpp:81
#1 0x716bd34efb04 in gsasl_register (libgsasl.so.18+0x4bb4)
#2 0x716bd34efa8c in gsasl_init (libgsasl.so.18+0x4a8c)
#3 0x6371f1ea767c in Curl_auth_gsasl_is_supported gsasl.c:41
SUMMARY: AddressSanitizer: double-free (libgsasl.so.18+0x4b04) in gsasl_done
=================================================================
(second and third violations follow — one per internal gsasl_init allocation,
both double-freed at gsasl.c:113 via gsasl_conn_dtor at vauth.c:207)
Después aparecen dos violaciones más del mismo tipo, una por cada reserva interna que hace gsasl_init, ambas liberadas por segunda vez en gsasl.c:113.
Impacto
Es una doble liberación de memoria en el cliente, que un servidor de correo malicioso o comprometido puede activar solo con lo que anuncia en sus capacidades, siempre que gsasl_client_start() falle en la máquina de la víctima (libgsasl antigua o compilada sin cliente, versión cambiada en ejecución o falta de memoria). El reporte no detalla más consecuencias que la propia doble liberación; la severidad asignada es media.
Remediación
El investigador propuso dos cambios. El necesario, que corrige la causa, es anular el puntero justo después de liberarlo en Curl_auth_gsasl_is_supported():
res = gsasl_client_start(gsasl->ctx, mech, &gsasl->client);
if(res != GSASL_OK) {
gsasl_done(gsasl->ctx);
+ gsasl->ctx = NULL;
return FALSE;
}
Como defensa adicional, comprobar los punteros en Curl_auth_gsasl_cleanup(), igual que ya hacen las funciones de limpieza de NTLM y Kerberos, para que ninguna ruta futura que llegue con la estructura a medio liberar repita el problema:
void Curl_auth_gsasl_cleanup(struct gsasldata *gsasl)
{
- gsasl_finish(gsasl->client);
- gsasl->client = NULL;
-
- gsasl_done(gsasl->ctx);
- gsasl->ctx = NULL;
+ if(gsasl->client) {
+ gsasl_finish(gsasl->client);
+ gsasl->client = NULL;
+ }
+ if(gsasl->ctx) {
+ gsasl_done(gsasl->ctx);
+ gsasl->ctx = NULL;
+ }
}
El investigador señala que es el mismo patrón que CVE-2018-16840 y CVE-2023-27537: liberar sin anular el puntero y que otra ruta de limpieza lo libere de nuevo.
Qué aprender de este caso
- Las rutas de error que liberan un recurso compartido son el sitio donde buscar: si una función de "prueba" (como
Curl_auth_gsasl_is_supported()) libera un campo de una estructura que sobrevive a la llamada, comprueba que lo deja aNULLy que el destructor final no lo vuelve a liberar. - Las estructuras reutilizadas entre intentos sucesivos (aquí, una
gsasldatapara SCRAM-SHA-256 y luego SCRAM-SHA-1) multiplican el problema: cada intento fallido deja un puntero colgante que el siguiente o el destructor heredan. - En C, liberar y anular deben ir siempre juntos, y las funciones de limpieza deberían tolerar campos ya liberados o nunca inicializados, como ya hacían en curl las de NTLM y Kerberos.
- Para probar rutas de error difíciles de provocar, una biblioteca con
LD_PRELOADque fuerza el código de retorno de una sola función, combinada con AddressSanitizer, saca a la luz fallos que en producción dependen de versiones antiguas o de falta de memoria.