curl · Report #3751701
Use-after-free in `curl_easy_duphandle()` with HTTP/2 stream-dependency tree
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