Description
I did this
Run a program using libcurl such as the following example:
#include <curl/curl.h>
int main() {
curl_global_init(0); // for posterity
printf("curl version: %s\n", curl_version());
CURLM *multi = curl_multi_init();
CURL *easy = curl_easy_init();
// Crash only happens when using HTTPS.
curl_easy_setopt(easy, CURLOPT_URL, "https://example.org");
// Any old HTTP tunneling proxy will do here.
curl_easy_setopt(easy, CURLOPT_PROXY, "http://127.0.0.1:9000");
// We're going to drive the transfer using multi interface here, because we
// want to stop during the middle.
curl_multi_add_handle(multi, easy);
// Run the multi handle once, just enough to start establishing an HTTPS
// connection.
int running_handles;
curl_multi_perform(multi, &running_handles);
// Close the easy handle *before* the multi handle. Doing it the other way
// around avoids the issue.
curl_easy_cleanup(easy);
curl_multi_cleanup(multi); // double-free happens here
}
The program crashes with the following output:
curl version: libcurl/7.80.0-DEV OpenSSL/1.1.1f zlib/1.2.11 nghttp2/1.40.0
free(): double free detected in tcache 2
fish: './a.out' terminated by signal SIGABRT (Abort)
When running the example program through a debugger the program appears to crash at this line:
Line 793 in 9db25d2
Relevant stack trace from my debugger:
conn_free
Curl_disconnect
Curl_conncache_close_all_connections
curl_multi_cleanup
I'm not totally sure if this is specifically related to proxies, as I can only reproduce when nghttp2 is enabled and using HTTPS and using a proxy, but @iiibui who originally brought the issue to my attention could reproduce with other scenarios as well.
I believe in all scenarios, rearranging the cleanup calls to close the multi handle first and then the easy handle avoids the issue. The documentation for curl_multi_cleanup
definitely recommends doing it in the order that I used, but it is unclear how important the order is. Is it a suggestion? Is it undefined behavior to cleanup in the reverse order? The docs don't say. I'm also not calling curl_multi_remove_handle
at all, but it seems that it is called automatically by curl_easy_cleanup
:
Lines 378 to 382 in 9db25d2
Note this also seems similar to #7236, which was supposedly fixed.
I expected the following
The program should exit without error.
- If calling
curl_easy_cleanup
on a handle without callingcurl_multi_remove_handle
first is undefined behavior, then I feel like the docs should say this more explicitly. The source code seems to expect people to not do this. - If it is OK to call
curl_easy_cleanup
without callingcurl_multi_remove_handle
, then this is bug introduced in 51c0ebc. Everything works just fine without callingcurl_multi_remove_handle
on versions prior to that commit that I tested.- If it is OK to do this, then I feel the docs should also be more clear as to whether it matters if
curl_easy_cleanup
is called beforecurl_multi_cleanup
or not. The phrase "should be" seems a bit non-committal. What happens if I do it out-of-order?
- If it is OK to do this, then I feel the docs should also be more clear as to whether it matters if
I help maintain the Rust bindings for libcurl, so ultimately I just want to know what the rules are for these calls so we can enforce them in the bindings.
curl/libcurl version
I can reproduce this on the latest master
commit (9db25d2 as of writing) but using git bisect
I identified 51c0ebc as the offending commit where this issue started.
I am configuring curl as follows:
./configure --with-openssl --with-nghttp2
operating system
Windows 10 under WSL2 (Ubuntu-flavored), however the original reporter of the issue reproduced on CentOS 7.3.1611.