Reports
BAJA[CORRUPCIÓN DE MEMORIA]#3751697

curl: uso tras liberar en el árbol de dependencias HTTP/2 al llamar a curl_easy_reset() (CVE-2026-10536)

curl_easy_reset() vaciaba la configuración de un handle sin sacarlo antes del árbol de prioridades HTTP/2, y otro handle seguía apuntando a él después de liberarlo. CVE-2026-10536, severidad baja.

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

En libcurl, un handle easy puede declarar que su flujo HTTP/2 depende de otro con la opción CURLOPT_STREAM_DEPENDS. Esa relación se guarda como un árbol enlazado dentro de data->set.priority: el hijo apunta a su padre (parent) y el padre guarda al hijo en su lista children.

El fallo estaba en curl_easy_reset() (lib/easy.c). Para devolver el handle a su estado inicial, la función hacía esto:

Curl_freeset(data);
memset(&data->set, 0, sizeof(struct UserDefined));
Curl_init_userdefined(data);

El memset borra los punteros parent y children del propio handle, pero nadie avisa al padre de que ese hijo ya no forma parte del árbol. La función que sí deshace correctamente esos enlaces, data_priority_cleanup() en lib/url.c, solo se llamaba desde Curl_close(), no desde curl_easy_reset().

Resultado: tras un reset y un curl_easy_cleanup() del hijo, el padre conserva en su lista children un nodo que apunta a una estructura struct Curl_easy ya liberada. Cuando después se limpia el padre, libcurl recorre esa lista y lee y escribe en memoria liberada (use-after-free).

Según el reporte, el defecto existe desde el commit 71b7e01610 (30 de diciembre de 2022, "lib: connect/h2/h3 refactor"), que convirtió set.priority en un árbol enlazado e introdujo la función de limpieza sin añadirla a curl_easy_reset(). Afecta a cualquier compilación con USE_NGHTTP2 y se alcanza usando únicamente la API pública documentada.

Datos del caso: HackerOne lo clasificó como "Buffer Over-read" con severidad baja; se asignó el CVE-2026-10536. Se reportó el 20 de mayo de 2026 y se divulgó el 24 de junio de 2026.

Pasos de reproducción

  1. Compilar curl con AddressSanitizer:
autoreconf -fi
mkdir build && cd build
CFLAGS='-O1 -g -fsanitize=address' LDFLAGS='-fsanitize=address' CC=clang \
  ../configure --with-openssl --enable-debug --enable-maintainer-mode --disable-shared
make -j
  1. Guardar este programa como poc.c. Crea dos handles, hace que B dependa de A, resetea B y luego libera los dos:
#include <curl/curl.h>
int main(void) {
    curl_global_init(CURL_GLOBAL_DEFAULT);
    CURL *A = curl_easy_init();
    CURL *B = curl_easy_init();
    curl_easy_setopt(B, CURLOPT_STREAM_DEPENDS, A);
    curl_easy_reset(B);
    curl_easy_cleanup(B);
    curl_easy_cleanup(A);
    curl_global_cleanup();
    return 0;
}
  1. Compilarlo contra la biblioteca estática y ejecutarlo:
clang -fsanitize=address -g -I../include poc.c \
  ./lib/.libs/libcurl.a -lssl -lcrypto -lz -lnghttp2 -lidn2 -lzstd \
  -lbrotlidec -lldap -llber -o poc
./poc
  1. ASan detiene el programa al limpiar A:
=================================================================
==654355==ERROR: AddressSanitizer: heap-use-after-free on address 0x7ac36ebe2148 at pc 0x614bec3b354f bp 0x7fff212fbea0 sp 0x7fff212fbe98
READ of size 8 at 0x7ac36ebe2148 thread T0
    #0 priority_remove_child   lib/url.c:3022:3
    #1 data_priority_cleanup   lib/url.c:3094:5
    #2 Curl_close              lib/url.c:278:3
    #3 curl_easy_cleanup       lib/easy.c:844:5
    #4 main                    poc.c

0x7ac36ebe2148 is located 2120 bytes inside of 5096-byte region [0x7ac36ebe1900,0x7ac36ebe2ce8)
freed by thread T0 here:
    #0 free
    #1 Curl_close              lib/url.c:311:3
    #2 curl_easy_cleanup       lib/easy.c:844:5
    #3 main                    poc.c  (curl_easy_cleanup(B))

previously allocated by thread T0 here:
    #0 calloc
    #1 curl_dbg_calloc         lib/memdebug.c:254:9
    #2 Curl_open               lib/url.c:463:10
    #3 curl_easy_init          lib/easy.c:350:12
    #4 main                    poc.c

SUMMARY: AddressSanitizer: heap-use-after-free lib/url.c:3022:3 in priority_remove_child

La región de 5096 bytes liberada es la estructura struct Curl_easy completa de B. Al limpiar A, libcurl recorre A->set.priority.children, encuentra el nodo obsoleto cuyo ->data apunta a esa región liberada, lee a través de él en lib/url.c:3022 y escribe en lib/url.c:3034-3035.

Impacto

Una aplicación que use libcurl con HTTP/2, establezca dependencias entre flujos con CURLOPT_STREAM_DEPENDS y reutilice handles con curl_easy_reset() acaba leyendo y escribiendo en memoria del heap ya liberada al limpiar el handle padre. Eso puede provocar cierres inesperados o corrupción de memoria. El reporte no describe un escenario de explotación más allá de esto, y el problema depende de cómo use la biblioteca la propia aplicación, lo que encaja con la severidad baja asignada.

Remediación

El reporte propone hacer pública dentro de la biblioteca la función ya existente (renombrándola a Curl_data_priority_cleanup) y llamarla desde curl_easy_reset() antes de borrar la configuración, igual que ya hacía Curl_close():

--- a/lib/url.c
+++ b/lib/url.c
@@ -120,9 +120,9 @@
 #ifdef USE_NGHTTP2
-static void data_priority_cleanup(struct Curl_easy *data);
+void Curl_data_priority_cleanup(struct Curl_easy *data);
 #else
-#define data_priority_cleanup(x)
+#define Curl_data_priority_cleanup(x)
 #endif
@@ -275,7 +275,7 @@
   /* destroy data->state.priority for HTTP/2 dep tree */
-  data_priority_cleanup(data);
+  Curl_data_priority_cleanup(data);
@@ -3087,7 +3087,7 @@
-static void data_priority_cleanup(struct Curl_easy *data)
+void Curl_data_priority_cleanup(struct Curl_easy *data)

--- a/lib/easy.c
+++ b/lib/easy.c
@@ -1093,6 +1093,7 @@ void curl_easy_reset(CURL *curl)
   /* clear all meta data */
   Curl_meta_reset(data);
+  Curl_data_priority_cleanup(data);
   /* zero out UserDefined data: */
   Curl_freeset(data);
   memset(&data->set, 0, sizeof(struct UserDefined));

El reporte figura como resuelto y con CVE asignado; el texto no detalla si el parche aplicado fue exactamente este.

Qué aprender de este caso

  • Cuando una estructura forma parte de una relación bidireccional (un padre que guarda una lista de hijos), un memset sobre esa estructura solo borra la mitad del enlace. Busca funciones de "reset" o "reinit" que pongan a cero estructuras con punteros a otros objetos sin desengancharlos antes.
  • Si existe una función de limpieza (aquí data_priority_cleanup()), compara todos los caminos que destruyen o reinician el objeto: close, reset, dup… Un camino que la olvide es un candidato directo a use-after-free.
  • Los refactors que cambian un campo simple por una estructura enlazada (como el commit que convirtió set.priority en árbol) obligan a revisar cada sitio que copiaba o borraba ese campo a mano.
  • Un PoC de pocas líneas con ASan basta para demostrar este tipo de fallos en bibliotecas: encadenar llamadas públicas en un orden poco habitual (configurar, resetear, liberar el hijo, liberar el padre) suele sacar a la luz errores de ciclo de vida.