HexBrain

curl · Report #3751701

Use-after-free in `curl_easy_duphandle()` with HTTP/2 stream-dependency tree

Informativestatus
5upvotes
1 Jun 2026disclosed

How it was found

Hi all,

We've found an issue in lib/easy.c where curl_easy_duphandle() shallow-copies set.priority, so the original and the duplicate end up holding two independent pointer-typed variables that both reference the same heap-allocated Curl_data_prio_node chain. Freeing the chain from one side leaves the other side's pointer dangling.

dupset() at lib/easy.c:881 does the shallow copy:

dst->set = src->set;

set.priority.children is a hand-rolled singly-linked list head (struct Curl_data_prio_node *) which after the copy is aliased between both handles. The subsequent memset(dst->set.str, ...) clears only the string array, not the priority substruct.

The shallow dst->set = src->set itself is older, but it became unsafe when commit 71b7e01610 (2022-12-30, "lib: connect/h2/h3 refactor") converted set.priority into a struct holding heap pointers and added the Curl_data_prio_node chain. From that commit onward, dupset has been bit-copying live heap pointers across handles. Reachable through documented public APIs only, on any USE_NGHTTP2 build.

Reproducer

Build curl with ASan:

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

Save as poc.c:

#include <curl/curl.h>
int main(void) {
    curl_global_init(CURL_GLOBAL_DEFAULT);
    CURL *B = curl_easy_init();
    CURL *C = curl_easy_init();
    curl_easy_setopt(C, CURLOPT_STREAM_DEPENDS, B);  /* B->children = pnode_C */
    CURL *Bdup = curl_easy_duphandle(B);             /* Bdup->children == pnode_C */
    curl_easy_cleanup(C);                            /* priority_remove_child(B,C) frees pnode_C */
    curl_easy_cleanup(B);                            /* B->children now NULL — safe */
    curl_easy_cleanup(Bdup);                         /* walks Bdup->children -> UAF */
    curl_global_cleanup();
    return 0;
}

Compile and run:

clang -fsanitize=address -g -I../include poc.c \
  ./lib/.libs/libcurl.a -lssl -lcrypto -lz -lnghttp2 -lidn2 -lzstd \
  -lbrotlidec -lldap -llber -o poc
./poc

Observed output

=================================================================
==655808==ERROR: AddressSanitizer: heap-use-after-free on address 0x6f79f46004e0 at pc 0x60e44fae15dd bp 0x7ffcd2900490 sp 0x7ffcd2900488
READ of size 8 at 0x6f79f46004e0 thread T0
    #0 data_priority_cleanup       lib/url.c:3093:58
    #1 Curl_close                  lib/url.c:278:3
    #2 curl_easy_cleanup           lib/easy.c:844:5
    #3 main                        poc.c  (curl_easy_cleanup(Bdup))

0x6f79f46004e0 is located 16 bytes inside of 32-byte region [0x6f79f46004d0,0x6f79f46004f0)
freed by thread T0 here:
    #0 free
    #1 priority_remove_child       lib/url.c:3031:5
    #2 data_priority_cleanup       lib/url.c:3100:5
    #3 Curl_close                  lib/url.c:278:3
    #4 curl_easy_cleanup           lib/easy.c:844:5
    #5 main                        poc.c  (curl_easy_cleanup(C))

previously allocated by thread T0 here:
    #0 calloc
    #1 curl_dbg_calloc             lib/memdebug.c:254:9
    #2 Curl_data_priority_add_child lib/url.c:3050:13
    #3 curl_easy_setopt            lib/setopt.c:2954:12
    #4 main                        poc.c  (CURLOPT_STREAM_DEPENDS)

SUMMARY: AddressSanitizer: heap-use-after-free lib/url.c:3093:58 in data_priority_cleanup

The 32-byte region is the Curl_data_prio_node allocated by Curl_data_priority_add_child. C's cleanup walks B's children list, removes and free()s the node, and updates B->set.priority.children. Bdup->set.priority.children is a separate pointer-typed variable that still references the freed node; its cleanup dereferences it.

Suggested fix

After the shallow set copy in dupset(), reset the priority substruct on the clone so it starts with no stream-dependency relationship. If the user wants the clone to inherit the dependency, they can re-set CURLOPT_STREAM_DEPENDS on it explicitly.

--- a/lib/easy.c
+++ b/lib/easy.c
@@ -879,6 +879,9 @@ static CURLcode dupset(struct Curl_easy *dst, struct Curl_easy *src)
   /* Copy src->set into dst->set first, then deal with the strings
      afterwards */
   dst->set = src->set;
+  /* The shallow copy aliased the HTTP/2 priority tree pointers; clear them
+     so the clone owns no peer references. */
+  memset(&dst->set.priority, 0, sizeof(dst->set.priority));
 #if !defined(CURL_DISABLE_MIME) || !defined(CURL_DISABLE_FORM_API)
   dst->set.mimepostp = NULL;
 #endif

Cheers, AISLE Research Team

Impact

Summary:

use after free

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