Skip to content

Commit e625eef

Browse files
committed
[BUG] Remove a curl easy handle before releasing what it points at
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 #4391. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
1 parent a31138b commit e625eef

3 files changed

Lines changed: 44 additions & 12 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ Increment the:
4444
deprecated C headers (`stdint.h`, `stddef.h`, `stdlib.h`, `string.h`,
4545
`stdio.h`, `ctype.h`, `limits.h`, `assert.h`) with their C++ equivalents
4646
([#4349](https://github.com/open-telemetry/opentelemetry-cpp/pull/4349))
47+
* [BUG] Remove a curl easy handle from the multi handle before freeing that
48+
handle and the header list it points at
49+
[#4391](https://github.com/open-telemetry/opentelemetry-cpp/issues/4391)
4750

4851
* [CONFIGURATION] Add SDK component builder interfaces to the registry
4952
[#4358](https://github.com/open-telemetry/opentelemetry-cpp/issues/4358)

‎ext/src/http/client/curl/http_client_curl.cc‎

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <functional>
1212
#include <list>
1313
#include <mutex>
14+
#include <ostream>
1415
#include <string>
1516
#include <thread>
1617
#include <unordered_map>
@@ -23,6 +24,7 @@
2324
#include "opentelemetry/ext/http/common/url_parser.h"
2425
#include "opentelemetry/nostd/shared_ptr.h"
2526
#include "opentelemetry/nostd/string_view.h"
27+
#include "opentelemetry/sdk/common/global_log_handler.h"
2628
#include "opentelemetry/sdk/common/thread_instrumentation.h"
2729
#include "opentelemetry/version.h"
2830

@@ -33,8 +35,6 @@
3335
# include <array>
3436

3537
# include "opentelemetry/nostd/type_traits.h"
36-
#else
37-
# include "opentelemetry/sdk/common/global_log_handler.h"
3838
#endif
3939

4040
OPENTELEMETRY_BEGIN_NAMESPACE
@@ -811,13 +811,36 @@ bool HttpClient::doRemoveSessions()
811811

812812
for (auto &removing_handle : pending_to_remove_session_handles)
813813
{
814-
if (nullptr != removing_handle.second.headers_chunk)
814+
auto &resource = removing_handle.second;
815+
if (nullptr == resource.easy_handle)
816+
{
817+
continue;
818+
}
819+
820+
// Take it out of the multi handle first. libcurl does not allow an easy handle to be
821+
// cleaned up while a multi handle still owns it, and the header list has to stay alive
822+
// until the handle is no longer used for a transfer. A handle that was never added
823+
// reports CURLM_OK here, so this does not need to know which is which.
824+
const CURLMcode rc = curl_multi_remove_handle(multi_handle_, resource.easy_handle);
825+
if (CURLM_OK != rc)
826+
{
827+
// The multi handle may still own it. Leaking one easy handle is the better half of
828+
// this trade: freeing it here would corrupt whatever still holds it.
829+
OTEL_INTERNAL_LOG_ERROR(
830+
"[HTTP Client Curl] curl_multi_remove_handle failed, leaving "
831+
"the handle alone: "
832+
<< curl_multi_strerror(rc));
833+
continue;
834+
}
835+
836+
if (nullptr != resource.headers_chunk)
815837
{
816-
curl_slist_free_all(removing_handle.second.headers_chunk);
838+
curl_slist_free_all(resource.headers_chunk);
839+
resource.headers_chunk = nullptr;
817840
}
818841

819-
curl_multi_remove_handle(multi_handle_, removing_handle.second.easy_handle);
820-
curl_easy_cleanup(removing_handle.second.easy_handle);
842+
curl_easy_cleanup(resource.easy_handle);
843+
resource.easy_handle = nullptr;
821844
}
822845

823846
for (auto &removing_session : pending_to_remove_sessions)

‎ext/src/http/client/curl/http_operation_curl.cc‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -542,15 +542,13 @@ void HttpOperation::Cleanup()
542542
// Only cleanup async once even in recursive calls
543543
if (async_data_)
544544
{
545-
// Just reset and move easy_handle to owner if in async mode
545+
// Move the easy handle to the owner untouched. It may still be attached to the multi handle
546+
// and running, and the thread inside curl_multi_perform owns it until the IO thread takes it
547+
// out, so writing to it here would be a write to a live transfer. It is pointless as well:
548+
// the next thing that happens to the handle is curl_easy_cleanup.
546549
Session *session = async_data_->session.exchange(nullptr, std::memory_order_acq_rel);
547550
if (session != nullptr)
548551
{
549-
if (curl_resource_.easy_handle != nullptr)
550-
{
551-
curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_PRIVATE, NULL);
552-
curl_easy_reset(curl_resource_.easy_handle);
553-
}
554552
session->GetHttpClient().ScheduleRemoveSession(session->GetSessionId(),
555553
std::move(curl_resource_));
556554
}
@@ -1525,6 +1523,14 @@ void HttpOperation::Abort()
15251523

15261524
void HttpOperation::PerformCurlMessage(CURLcode code)
15271525
{
1526+
if (is_cleaned_.load(std::memory_order_acquire))
1527+
{
1528+
// Already torn down, and the handle is queued for removal. The multi handle can still hold a
1529+
// message buffered from before that, and acting on it would dispatch a second round of
1530+
// terminal events and can put a handle that is waiting to be freed back on the retry queue.
1531+
return;
1532+
}
1533+
15281534
++retry_attempts_;
15291535
last_attempt_time_ = std::chrono::system_clock::now();
15301536
last_curl_result_ = code;

0 commit comments

Comments
 (0)