[BUG] Remove a curl easy handle before releasing what it points at - #4405
[BUG] Remove a curl easy handle before releasing what it points at#4405thc1006 wants to merge 1 commit into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4405 +/- ##
==========================================
- Coverage 82.37% 82.36% -0.00%
==========================================
Files 502 502
Lines 19883 19890 +7
==========================================
+ Hits 16377 16381 +4
- Misses 3506 3509 +3
🚀 New features to boost your workflow:
|
dd5df05 to
44b53fe
Compare
|
Where the five uncovered patch lines are, measured with the same One is the guard this change is about. One is a null easy handle in Three are the I would rather say that plainly than add cases that pin defensive branches into place with test-only seams. If you want any of the three covered, say which and I will send them. |
Teardown touched the easy handle in the wrong order and on the wrong thread. Cleanup cleared CURLOPT_PRIVATE and called curl_easy_reset on it, from whichever thread ran the cancel, while the transfer could still be active. doRemoveSessions then freed the header list before curl_multi_remove_handle and cleaned the handle up whatever that call returned. libcurl is explicit on all three: changing options while a transfer is in progress may have undefined behaviour, the header list has to outlive the handle that points at it, and a handle has to leave the multi handle before it can be cleaned up. Cleanup now hands the resource over untouched. doRemoveSessions removes it, checks the CURLMcode, and only then frees the list and the handle. A failed removal leaves both alone: leaking one easy handle is better than freeing one the multi stack may still own. A handle that was never added reports CURLM_OK, so no separate bookkeeping is needed to tell the two apart. Clearing CURLOPT_PRIVATE was load bearing rather than tidy up. The IO loop reads it back to decide whether a CURLMSG_DONE belongs to a session that has already gone, so PerformCurlMessage checks is_cleaned_ instead. That keeps the behaviour without writing to a handle from a thread that does not own it. Fixes open-telemetry#4391. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
44b53fe to
e625eef
Compare
Part of #4391, not a full fix, so no closing keyword. The issue also names the retry path at
doRetrySessions(), wherecurl_multi_remove_handleandcurl_multi_add_handleare still unchecked. That belongs with the retry queue work rather than here, so #4391 should stay open until it is done.Changes
Teardown touches the easy handle in the wrong order, and part of it on the wrong thread. Three things, and libcurl is explicit about each one.
HttpOperation::Cleanup()clearsCURLOPT_PRIVATEand callscurl_easy_reset()on the handle. It runs on whichever thread cancelled, and the transfer can still be live insidecurl_multi_perform()on the IO thread.curl_easy_setopt: "Changing options with curl_easy_setopt() while a transfer is still in progress may cause undefined and undesired behavior."HttpClient::doRemoveSessions()frees the header list before it removes the handle.CURLOPT_HTTPHEADER: "libcurl does not copy the entire list so you must keep it around until you no longer use this handle for a transfer before you call curl_slist_free_all on the list."And it calls
curl_easy_cleanup()whatevercurl_multi_remove_handle()returned, because the return is dropped.curl_easy_cleanup: "To close an easy handle that has been used with the multi interface, make sure to first call curl_multi_remove_handle to remove it from the multi handle before it is closed."So:
Cleanup()hands the resource over untouched. The next thing that happens to the handle iscurl_easy_cleanup(), so resetting it first bought nothing even when it was safe.doRemoveSessions()removes the handle first, checks theCURLMcode, and only then frees the list and the handle. A failed removal leaves both alone and logs. Leaking one easy handle is the better half of that trade: freeing one the multi stack may still own corrupts whatever holds it.CURLM_OKfromcurl_multi_remove_handle()(lib/multi.c:if(!data->multi) return CURLM_OK;), so nothing has to track which handles were scheduled and which were not.Clearing
CURLOPT_PRIVATEwas load bearing rather than tidying up. The IO loop reads it back to decide whether a bufferedCURLMSG_DONEstill belongs to anyone, soPerformCurlMessage()now checksis_cleaned_instead. Same de-duplication, no write to a handle from a thread that does not own it.On tests
I have not added one, and I would rather say why than tick the box on something thin.
Two of the three parts are ordering inside teardown with nothing observable from outside. Whether
curl_multi_remove_handle()runs before or aftercurl_slist_free_all(), and whetherCleanup()touched the handle on its way out, are not reachable through the public API.The third, the
is_cleaned_guard, is observable in principle, and I measured that the current suite does not discriminate it. Neutralising the guard and rebuilding leaves all 23 cases green, twice over. The case that does exercise the property it preserves isACancelAfterTheResponseReportsOneOutcomeon #4392, which this PR sits behind.What I ran instead is the whole suite under AddressSanitizer with
detect_leaks=1, because "leave both alone when the removal fails" is the part of this change that could plausibly leak.If you want a dedicated case for the guard, say so and I will build one around a buffered
CURLMSG_DONE. I did not want to ship a test I had not first watched fail.Checks
curl_http_test,OTELCPP_MAINTAINER_MODE=ONdetect_leaks=1__asan_symbols in the binaryclang-format18.1.8Related
This is the last of the four open against these two files. #4392 and #4395 both touch
SendAsync()andCleanup(), and #4394 touchesresetMultiHandle(). Nothing here overlaps #4394. It does overlap the other two inCleanup(), so I will rebase this one after they land rather than the other way round, and I have kept it to the smallest change that closes #4391 so that rebase stays mechanical.For significant contributions please make sure you have completed the following items:
CHANGELOG.mdupdated for non-trivial changes