Reports
MEDIA[CORRUPCIÓN DE MEMORIA]#3735193

CVE-2026-8925: doble liberación de memoria en curl al fallar la negociación SASL con GSASL

Si la prueba de un mecanismo SCRAM fallaba, curl liberaba el contexto de libgsasl sin anular el puntero y lo volvía a liberar al cerrar la conexión. Un servidor IMAP o SMTP podía provocarlo solo con anunciar SCRAM.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

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_MECHANISM y el fallo se da en cada conexión.
  • Falta de memoria (GSASL_MALLOC_ERROR): gsasl_client_start hace internamente un calloc(1, 0xf0); posible en contenedores con la memoria limitada.
  • libgsasl compilada con --disable-client: devuelve GSASL_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.

  1. Compilar curl con GSASL y AddressSanitizer. El investigador usó CMake con -DCURL_USE_GSASL=ON -DCURL_USE_OPENSSL=ON -DBUILD_SHARED_LIBS=OFF y las opciones -fsanitize=address -g3 -O0, con libgsasl 2.2.1 (también vale una compilación --with-gsasl --enable-debug con ASan).
  1. Preparar un servidor IMAP o SMTP (el investigador usó un IMAP simulado) que anuncie AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1 en sus capacidades.
  1. 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;
}
  1. 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;
}
  1. 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 en Curl_auth_gsasl_cleanup (gsasl.c:113), llamada desde gsasl_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 a NULL y que el destructor final no lo vuelve a liberar.
  • Las estructuras reutilizadas entre intentos sucesivos (aquí, una gsasldata para 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_PRELOAD que 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.