Describe the bug
A use-after-free is reachable through purely safe code by holding a reference returned from effective_url() (or any other functions which call getopt_str/getopt_bytes) across a perform() call. After the second perform(), the reference reads freed memory.
AddressSanitizer confirms this is a heap-use-after-free.
The command is RUSTFLAGS="-Z sanitizer=address" cargo run --example poc_effective_url_uaf
ASAN output:
=================================================================
==3747821==ERROR: AddressSanitizer: heap-use-after-free on address 0x7288da7ff420 at pc 0x60c1ca0adef4 bp 0x7ffe16311100 sp 0x7ffe163108b8
READ of size 1 at 0x7288da7ff420 thread T0
#0 0x60c1ca0adef3 in memcpy /rustc/llvm/src/llvm-project/compiler-rt/lib/asan/../sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115:5
#1 0x60c1ca1b6e2d in core::ptr::copy_nonoverlapping::h97acfe6ad30ebd17 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/core/src/ptr/mod.rs:547:14
#2 0x60c1ca1b6e2d in std::io::buffered::bufwriter::BufWriter$LT$W$GT$::write_to_buffer_unchecked::h3f57a224160a7017 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/io/buffered/bufwriter.rs:446:13
#3 0x60c1ca1b6e2d in _$LT$std..io..buffered..bufwriter..BufWriter$LT$W$GT$$u20$as$u20$std..io..Write$GT$::write_all::h876ab6fdfc5ce5b8 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/io/buffered/bufwriter.rs:549:22
#4 0x60c1ca1b6e2d in _$LT$std..io..buffered..linewritershim..LineWriterShim$LT$W$GT$$u20$as$u20$std..io..Write$GT$::write_all::h521b1ff5ebebbf5d /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/io/buffered/linewritershim.rs
#5 0x60c1ca1b6e2d in _$LT$std..io..buffered..linewriter..LineWriter$LT$W$GT$$u20$as$u20$std..io..Write$GT$::write_all::h592b06df2e7a6065 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/io/buffered/linewriter.rs:209:46
#6 0x60c1ca1b6e2d in _$LT$std..io..stdio..StdoutLock$u20$as$u20$std..io..Write$GT$::write_all::haab1b9685135a4f4 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/io/stdio.rs:860:33
#7 0x60c1ca1b78d5 in _$LT$std..io..default_write_fmt..Adapter$LT$T$GT$$u20$as$u20$core..fmt..Write$GT$::write_str::h0140c20ed9c59ded /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/io/mod.rs:628:30
#8 0x60c1ca1d7013 in core::fmt::Formatter::write_str::h286a7b22baec06d8 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/core/src/fmt/mod.rs:1886:18
#9 0x60c1ca1d7013 in _$LT$str$u20$as$u20$core..fmt..Debug$GT$::fmt::hc5209c6df06c8d33 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/core/src/fmt/mod.rs:2735:23
#10 0x60c1ca0e7fb3 in _$LT$$RF$T$u20$as$u20$core..fmt..Debug$GT$::fmt::h4875a9f64db318db /home/tony/.rustup/toolchains/nightly-2025-08-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/fmt/mod.rs:2658:62
#11 0x60c1ca1d6d08 in core::fmt::builders::DebugTuple::field::_$u7b$$u7b$closure$u7d$$u7d$::h47968e5ad189eb8c /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/core/src/fmt/builders.rs:330:35
#12 0x60c1ca1d6d08 in core::fmt::builders::DebugTuple::field_with::_$u7b$$u7b$closure$u7d$$u7d$::hf8e723f4efacaade /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/core/src/fmt/builders.rs:355:17
#13 0x60c1ca1d6d08 in core::result::Result$LT$T$C$E$GT$::and_then::h83e14fc5f0ea04d1 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/core/src/result.rs:1483:22
#14 0x60c1ca1d6d08 in core::fmt::builders::DebugTuple::field_with::h18a6cfe995292b14 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/core/src/fmt/builders.rs:342:35
#15 0x60c1ca1d6d08 in core::fmt::builders::DebugTuple::field::h0e9715aca75ffc74 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/core/src/fmt/builders.rs:330:14
#16 0x60c1ca1d6d08 in core::fmt::Formatter::debug_tuple_field1_finish::h1eb3034ec57245bc /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/core/src/fmt/mod.rs:2404:17
#17 0x60c1ca0ddcb2 in _$LT$core..option..Option$LT$T$GT$$u20$as$u20$core..fmt..Debug$GT$::fmt::h0b40fa495959b028 /home/tony/.rustup/toolchains/nightly-2025-08-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/option.rs:588:20
#18 0x60c1ca1d60a2 in core::fmt::rt::Argument::fmt::h8a0118be6af5db40 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/core/src/fmt/rt.rs:173:76
#19 0x60c1ca1d60a2 in core::fmt::write::hca39e7f1c4949f2f /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/core/src/fmt/mod.rs:1468:25
#20 0x60c1ca1b6abd in std::io::default_write_fmt::h0eff647ecb0f018e /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/io/mod.rs:639:11
#21 0x60c1ca1b6abd in std::io::Write::write_fmt::hc9b62fc2891d9a4d /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/io/mod.rs:1954:13
#22 0x60c1ca1b6abd in _$LT$$RF$std..io..stdio..Stdout$u20$as$u20$std..io..Write$GT$::write_fmt::h6db882bd861efaa1 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/io/stdio.rs:834:21
#23 0x60c1ca1b7421 in _$LT$std..io..stdio..Stdout$u20$as$u20$std..io..Write$GT$::write_fmt::h03b79f1369db63ce /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/io/stdio.rs:808:18
#24 0x60c1ca1b7421 in std::io::stdio::print_to::ha854b7705c7aa2ae /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/io/stdio.rs:1164:32
#25 0x60c1ca1b7421 in std::io::stdio::_print::h931a80cf29107f83 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/io/stdio.rs:1275:5
#26 0x60c1ca0dd9e5 in poc_effective_url_uaf::main::h5649952c245e6a45 /home/tony/proj/tests/curl-rust/examples/poc_effective_url_uaf.rs:30:5
#27 0x60c1ca0ddd7a in core::ops::function::FnOnce::call_once::h5c1ddd6a872b35a4 /home/tony/.rustup/toolchains/nightly-2025-08-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:253:5
#28 0x60c1ca0dd17d in std::sys::backtrace::__rust_begin_short_backtrace::h3eec8b7dd0e6f950 /home/tony/.rustup/toolchains/nightly-2025-08-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/backtrace.rs:158:18
#29 0x60c1ca0dd100 in std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::h171089efc686c4df /home/tony/.rustup/toolchains/nightly-2025-08-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/rt.rs:206:18
#30 0x60c1ca1b521f in core::ops::function::impls::_$LT$impl$u20$core..ops..function..FnOnce$LT$A$GT$$u20$for$u20$$RF$F$GT$::call_once::h99b89a21395e737e /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/core/src/ops/function.rs:290:21
#31 0x60c1ca1b521f in std::panicking::catch_unwind::do_call::h51fde413d96f01d7 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/panicking.rs:589:40
#32 0x60c1ca1b521f in std::panicking::catch_unwind::h8f41973163f34070 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/panicking.rs:552:19
#33 0x60c1ca1b521f in std::panic::catch_unwind::h442dcdbd746e6f4f /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/panic.rs:359:14
#34 0x60c1ca1b521f in std::rt::lang_start_internal::_$u7b$$u7b$closure$u7d$$u7d$::h5a036fb08f859f95 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/rt.rs:175:24
#35 0x60c1ca1b521f in std::panicking::catch_unwind::do_call::h8300227963603c66 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/panicking.rs:589:40
#36 0x60c1ca1b521f in std::panicking::catch_unwind::h7118510fdbd8d238 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/panicking.rs:552:19
#37 0x60c1ca1b521f in std::panic::catch_unwind::hfb2e6b22c219a577 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/panic.rs:359:14
#38 0x60c1ca1b521f in std::rt::lang_start_internal::h9fe5bed3f7fb1990 /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/std/src/rt.rs:171:5
#39 0x60c1ca0dd06f in std::rt::lang_start::hacb593e7b684c3a8 /home/tony/.rustup/toolchains/nightly-2025-08-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/rt.rs:205:5
#40 0x60c1ca0ddb1d in main (/home/tony/proj/tests/curl-rust/target/debug/examples/poc_effective_url_uaf+0x142b1d) (BuildId: 281813aa260f5ea20966c46d6f5a77cba7ea9dc9)
#41 0x7658db62a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#42 0x7658db62a28a in __libc_start_main csu/../csu/libc-start.c:360:3
#43 0x60c1ca0237f4 in _start (/home/tony/proj/tests/curl-rust/target/debug/examples/poc_effective_url_uaf+0x887f4) (BuildId: 281813aa260f5ea20966c46d6f5a77cba7ea9dc9)
0x7288da7ff420 is located 0 bytes inside of 32-byte region [0x7288da7ff420,0x7288da7ff440)
freed by thread T0 here:
#0 0x60c1ca0aebf6 in free /rustc/llvm/src/llvm-project/compiler-rt/lib/asan/asan_malloc_linux.cpp:51:3
#1 0x60c1ca12ad25 in curl_free /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/escape.c:192:3
#2 0x60c1ca128fbd in Curl_bufref_free /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/bufref.c:61:5
#3 0x60c1ca12900b in Curl_bufref_set /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/bufref.c:78:3
#4 0x60c1ca16c794 in Curl_pretransfer /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/transfer.c:513:3
#5 0x60c1ca14ca47 in multi_runsingle /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/multi.c:2520:16
#6 0x60c1ca14d2a8 in multi_perform /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/multi.c:2803:17
#7 0x60c1ca14d4e1 in curl_multi_perform /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/multi.c:2864:10
#8 0x60c1ca10504f in easy_transfer /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/easy.c:694:17
#9 0x60c1ca1052de in easy_perform /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/easy.c:802:42
#10 0x60c1ca105335 in curl_easy_perform /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/easy.c:820:10
#11 0x60c1ca0ecff8 in curl::easy::handler::Easy2$LT$H$GT$::perform::ha0e3707963033325 /home/tony/proj/tests/curl-rust/src/easy/handler.rs:3200:37
#12 0x60c1ca0f2e4b in curl::easy::handle::Easy::do_perform::h7de409d500ce7573 /home/tony/proj/tests/curl-rust/src/easy/handle.rs:1406:20
#13 0x60c1ca0f2cec in curl::easy::handle::Easy::perform::h547399a98049c20a /home/tony/proj/tests/curl-rust/src/easy/handle.rs:1386:14
#14 0x60c1ca0dd96b in poc_effective_url_uaf::main::h5649952c245e6a45 /home/tony/proj/tests/curl-rust/examples/poc_effective_url_uaf.rs:28:5
#15 0x60c1ca0ddd7a in core::ops::function::FnOnce::call_once::h5c1ddd6a872b35a4 /home/tony/.rustup/toolchains/nightly-2025-08-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:253:5
#16 0x60c1ca0dd06f in std::rt::lang_start::hacb593e7b684c3a8 /home/tony/.rustup/toolchains/nightly-2025-08-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/rt.rs:205:5
#17 0x60c1ca0ddb1d in main (/home/tony/proj/tests/curl-rust/target/debug/examples/poc_effective_url_uaf+0x142b1d) (BuildId: 281813aa260f5ea20966c46d6f5a77cba7ea9dc9)
#18 0x7658db62a28a in __libc_start_main csu/../csu/libc-start.c:360:3
#19 0x60c1ca0237f4 in _start (/home/tony/proj/tests/curl-rust/target/debug/examples/poc_effective_url_uaf+0x887f4) (BuildId: 281813aa260f5ea20966c46d6f5a77cba7ea9dc9)
previously allocated by thread T0 here:
#0 0x60c1ca0af28c in realloc /rustc/llvm/src/llvm-project/compiler-rt/lib/asan/asan_malloc_linux.cpp:81:3
#1 0x60c1ca113dfb in dyn_nappend /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/curlx/dynbuf.c:105:15
#2 0x60c1ca113f91 in curlx_dyn_addn /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/curlx/dynbuf.c:167:10
#3 0x60c1ca147e42 in alloc_addbyter /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/mprintf.c:1146:21
#4 0x60c1ca147470 in out_string /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/mprintf.c:910:5
#5 0x60c1ca147a99 in formatf /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/mprintf.c:1060:10
#6 0x60c1ca147f53 in curl_mvaprintf /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/mprintf.c:1177:9
#7 0x60c1ca14806e in curl_maprintf /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/mprintf.c:1192:7
#8 0x60c1ca17971b in urlget_url /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/urlapi.c:1508:11
#9 0x60c1ca179a97 in curl_url_get /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/urlapi.c:1616:12
#10 0x60c1ca17148b in parseurlandfillconn /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/url.c:1574:10
#11 0x60c1ca174a4d in url_create_needle /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/url.c:3050:12
#12 0x60c1ca174f71 in url_find_or_create_conn /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/url.c:3229:12
#13 0x60c1ca175629 in Curl_connect /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/url.c:3430:12
#14 0x60c1ca14c2e9 in state_connect /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/multi.c:2312:21
#15 0x60c1ca14cb0e in multi_runsingle /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/multi.c:2545:17
#16 0x60c1ca14d2a8 in multi_perform /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/multi.c:2803:17
#17 0x60c1ca14d4e1 in curl_multi_perform /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/multi.c:2864:10
#18 0x60c1ca10504f in easy_transfer /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/easy.c:694:17
#19 0x60c1ca1052de in easy_perform /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/easy.c:802:42
#20 0x60c1ca105335 in curl_easy_perform /home/tony/proj/tests/curl-rust/curl-sys/curl/lib/easy.c:820:10
#21 0x60c1ca0ecff8 in curl::easy::handler::Easy2$LT$H$GT$::perform::ha0e3707963033325 /home/tony/proj/tests/curl-rust/src/easy/handler.rs:3200:37
#22 0x60c1ca0f2e4b in curl::easy::handle::Easy::do_perform::h7de409d500ce7573 /home/tony/proj/tests/curl-rust/src/easy/handle.rs:1406:20
#23 0x60c1ca0f2cec in curl::easy::handle::Easy::perform::h547399a98049c20a /home/tony/proj/tests/curl-rust/src/easy/handle.rs:1386:14
#24 0x60c1ca0dd7a8 in poc_effective_url_uaf::main::h5649952c245e6a45 /home/tony/proj/tests/curl-rust/examples/poc_effective_url_uaf.rs:20:5
#25 0x60c1ca0ddd7a in core::ops::function::FnOnce::call_once::h5c1ddd6a872b35a4 /home/tony/.rustup/toolchains/nightly-2025-08-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:253:5
#26 0x60c1ca0dd06f in std::rt::lang_start::hacb593e7b684c3a8 /home/tony/.rustup/toolchains/nightly-2025-08-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/rt.rs:205:5
#27 0x60c1ca0ddb1d in main (/home/tony/proj/tests/curl-rust/target/debug/examples/poc_effective_url_uaf+0x142b1d) (BuildId: 281813aa260f5ea20966c46d6f5a77cba7ea9dc9)
#28 0x7658db62a28a in __libc_start_main csu/../csu/libc-start.c:360:3
#29 0x60c1ca0237f4 in _start (/home/tony/proj/tests/curl-rust/target/debug/examples/poc_effective_url_uaf+0x887f4) (BuildId: 281813aa260f5ea20966c46d6f5a77cba7ea9dc9)
SUMMARY: AddressSanitizer: heap-use-after-free /rustc/ec7c02612527d185c379900b613311bc1dcbf7dc/library/core/src/ptr/mod.rs:547:14 in core::ptr::copy_nonoverlapping::h97acfe6ad30ebd17
Shadow bytes around the buggy address:
0x7288da7ff180: 00 00 00 00 fa fa 00 00 00 00 fa fa fd fd fd fd
0x7288da7ff200: fa fa fd fd fd fd fa fa 00 00 05 fa fa fa 00 00
0x7288da7ff280: 02 fa fa fa fd fd fd fa fa fa 00 00 06 fa fa fa
0x7288da7ff300: fd fd fd fa fa fa 00 00 03 fa fa fa fd fd fd fa
0x7288da7ff380: fa fa 00 00 03 fa fa fa fd fd fd fd fa fa fd fd
=>0x7288da7ff400: fd fd fa fa[fd]fd fd fd fa fa fd fd fd fd fa fa
0x7288da7ff480: fd fd fd fd fa fa fd fd fd fa fa fa fd fd fd fa
0x7288da7ff500: fa fa 00 00 00 fa fa fa fd fd fd fd fa fa fd fd
0x7288da7ff580: fd fd fa fa 00 00 00 00 fa fa 00 00 00 00 fa fa
0x7288da7ff600: 00 00 00 00 fa fa 00 00 00 00 fa fa 00 00 00 00
0x7288da7ff680: fa fa fd fd fd fd fa fa fd fd fd fa fa fa fd fd
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==3747821==ABORTING
Root cause
The ASAN trace has three sections (from bottom to top):
- Section 1 ("previously allocated") — the first
perform() (poc_effective_url_uaf.rs:20:5) allocates the URL buffer via Curl_connect -> parseurlandfillconn -> curl_maprintf -> realloc.
- Section 2 ("freed") — the second
perform() (poc_effective_url_uaf.rs:28:5) frees that buffer via Curl_pretransfer -> Curl_bufref_set -> Curl_bufref_free -> free().
- Section 3 ("READ of size 1") — user code reads the dangling
&str (via println!), triggering the ASAN report.
Between Section 1 and Section 2, user code calls easy.effective_url() which returns &str pointing directly into the buffer allocated in Section 1. After Section 2 frees it, the &str becomes dangling.
Why the borrow checker fails to prevent this:
pub fn perform(&self) -> Result<(), Error> // shared borrow
pub fn effective_url(&self) -> Result<Option<&str>, Error> // shared borrow, returns &str tied to &self
Both take &self. Rust allows multiple shared borrows to coexist:
let url_ref = easy.effective_url()?; // &str pointing into libcurl's heap buffer
easy.perform()?; // frees that buffer in Curl_pretransfer
println!("{}", url_ref); // reads freed memory
The type system assumes &str tied to &self remains valid while &self is live. But the buffer is owned by libcurl's C heap, and perform() unconditionally frees it.
To reproduce
use curl::easy::Easy;
// #[inline(never)] ensures ASAN call stack shows distinct function names
// for the two perform() calls, making the trace unambiguous.
#[inline(never)]
fn first_perform(easy: &Easy) {
let _ = easy.perform();
}
#[inline(never)]
fn second_perform(easy: &Easy) {
let _ = easy.perform();
}
fn main() {
let mut easy = Easy::new();
easy.url("http://127.0.0.1:1").unwrap();
// First perform — allocates effective_url buffer inside libcurl
first_perform(&easy);
// Get reference to libcurl's internal buffer — &str lifetime tied to &easy
let url_ref = easy.effective_url().unwrap();
println!("Before second perform: {:?}", url_ref);
// Second perform — libcurl frees the buffer in Curl_pretransfer (unconditionally).
// perform() takes &self, so Rust allows this alongside the live url_ref borrow.
second_perform(&easy);
println!("After second perform: {:?}", url_ref); // uaf here
}
Run with:
RUSTFLAGS="-Z sanitizer=address"cargo run --example poc_effective_url_uaf
Soundness bug
This is a soundness bug:
- The caller writes no
unsafe code.
- The Rust type system should prevent use-after-free but actually failed.
- This violates Rust's safety guarantee that safe code must not be able to observe undefined behavior.
Test environment
- Version: curl-rust master (commit 0cfd9e3)
- OS: Ubuntu 24.04, x86_64
- Rustc: rustc 1.91.0-nightly (ec7c02612 2025-08-05)
Suggested fix
For example, change getopt_str/getopt_bytes to return owned data, and change effective_url to return Result<Option<String>, Error> instead of Result<Option<&str>, Error>. Other callers of getopt_str/getopt_bytes would also need to be updated accordingly.
fn getopt_bytes(&self, opt: curl_sys::CURLINFO) -> Result<Option<Vec<u8>>, Error> { // return type: &[u8] -> Vec<u8>
unsafe {
let p = self.getopt_ptr(opt)?;
if p.is_null() {
Ok(None)
} else {
Ok(Some(CStr::from_ptr(p).to_bytes().to_vec())) // -> return owned Vec<u8> instead of borrowed &[u8]
}
}
}
fn getopt_str(&self, opt: curl_sys::CURLINFO) -> Result<Option<String>, Error> { // return type: &str -> String
match self.getopt_bytes(opt) {
Ok(None) => Ok(None),
Err(e) => Err(e),
Ok(Some(bytes)) => match String::from_utf8(bytes) { // str::from_utf8(bytes) -> String::from_utf8(bytes)
Ok(s) => Ok(Some(s)),
Err(_) => Err(Error::new(curl_sys::CURLE_CONV_FAILED)),
},
}
}
pub fn effective_url(&self)-> Result<Option<String>, Error> { // return type: &str -> String
self.getopt_str(curl_sys::CURLINFO_EFFECTIVE_URL)
}
Describe the bug
A use-after-free is reachable through purely safe code by holding a reference returned from
effective_url()(or any other functions which callgetopt_str/getopt_bytes) across aperform()call. After the secondperform(), the reference reads freed memory.AddressSanitizer confirms this is a heap-use-after-free.
The command is
RUSTFLAGS="-Z sanitizer=address" cargo run --example poc_effective_url_uafASAN output:
Root cause
The ASAN trace has three sections (from bottom to top):
perform()(poc_effective_url_uaf.rs:20:5) allocates the URL buffer viaCurl_connect->parseurlandfillconn->curl_maprintf->realloc.perform()(poc_effective_url_uaf.rs:28:5) frees that buffer viaCurl_pretransfer->Curl_bufref_set->Curl_bufref_free->free().&str(viaprintln!), triggering the ASAN report.Between Section 1 and Section 2, user code calls
easy.effective_url()which returns&strpointing directly into the buffer allocated in Section 1. After Section 2 frees it, the&strbecomes dangling.Why the borrow checker fails to prevent this:
Both take
&self. Rust allows multiple shared borrows to coexist:The type system assumes
&strtied to&selfremains valid while&selfis live. But the buffer is owned by libcurl's C heap, andperform()unconditionally frees it.To reproduce
Run with:
RUSTFLAGS="-Z sanitizer=address"cargo run --example poc_effective_url_uafSoundness bug
This is a soundness bug:
unsafecode.Test environment
Suggested fix
For example, change
getopt_str/getopt_bytesto return owned data, and changeeffective_urlto returnResult<Option<String>, Error>instead ofResult<Option<&str>, Error>. Other callers ofgetopt_str/getopt_byteswould also need to be updated accordingly.