Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7b43dd4
fix: guard HAVE_MAT_LIVEEVENTINSPECTOR/PRIVACYGUARD against redefinition
bmehta001 Jun 10, 2026
fc7375a
fix: prevent EDEADLK self-join in ~CurlHttpOperation on async-thread …
bmehta001 Jun 11, 2026
e76fabf
Merge branch 'main' into bhamehta/fix-curl-async-self-join
bmehta001 Jun 11, 2026
3554d8d
Make ~CurlHttpOperation detach conditional on self-join (fix UAF regr…
bmehta001 Jun 13, 2026
b10fa89
Address Copilot round 2 on #1481: include <new>, handle nothrow-new f…
bmehta001 Jun 13, 2026
3dda53b
Address Copilot round 3 on #1481: avoid atomic<thread::id>, fix lifet…
bmehta001 Jun 13, 2026
7e22ed2
Address Copilot round 4 on #1481: precise self-join comment, reset fl…
bmehta001 Jun 13, 2026
dcbc2a2
Merge branch 'main' into bhamehta/fix-curl-async-self-join
bmehta001 Jun 30, 2026
7a14e76
Merge branch 'main' into bhamehta/fix-curl-async-self-join
bmehta001 Jul 8, 2026
6bf8f55
Merge remote-tracking branch 'origin/main' into bhamehta/fix-curl-asy…
bmehta001 Jul 8, 2026
150e376
Replace std::async with a self-keepalive detached worker (real fix fo…
bmehta001 Jul 8, 2026
854f2dd
Merge remote-tracking branch 'fork/bhamehta/fix-curl-async-self-join'…
bmehta001 Jul 8, 2026
a12525e
Address Copilot round on #1481: own the body, catch worker exceptions…
bmehta001 Jul 8, 2026
23486a6
Address Copilot round 2 on #1481: move body, deterministic test host,…
bmehta001 Jul 8, 2026
4ccc9ea
Address Copilot round 3 on #1481: guard worker-thread start, harden t…
bmehta001 Jul 8, 2026
f9262e0
Address Copilot round 5 on #1481: <limits> include, broaden thread-st…
bmehta001 Jul 9, 2026
a1da06f
Address Copilot round 6 on #1481: guard shared_from_this() in SendAsync
bmehta001 Jul 9, 2026
23b6f9a
Correct the body-move comment in SendRequestAsync (#1481 review round 7)
bmehta001 Jul 9, 2026
62395c7
Harden NoSelfJoin test timeout path (#1481 review round 8)
bmehta001 Jul 9, 2026
9ae7dd5
Move worker-lambda construction inside the try in SendAsync (#1481 re…
bmehta001 Jul 9, 2026
1ff4b52
Drop issue-number references from code comments
bmehta001 Jul 9, 2026
b1e03d8
Guarantee the callback fires even when Send() throws
bmehta001 Jul 9, 2026
cf9bc95
Fix use-after-free dispatching OnDestroy after the completion callback
bmehta001 Jul 10, 2026
f2af5ec
Address review: include <utility>, correct OnDestroy comment, harden …
bmehta001 Jul 10, 2026
4af8401
Fix two curl-client lifetime issues found in review
bmehta001 Jul 10, 2026
89491fd
Harden curl-client shutdown: complete-on-send and skip unsafe global …
bmehta001 Jul 10, 2026
9d69b19
Harden the completion-guard regression test against the timeout path
bmehta001 Jul 10, 2026
bfabf8c
Clarify ~CurlHttpOperation comment for never-sent and synchronous-fal…
bmehta001 Jul 10, 2026
e25c43c
Correct the OnDestroy comment: suppressed once a send is attempted
bmehta001 Jul 10, 2026
8e6575b
Wait for the operation to be destroyed in the self-join test's timeou…
bmehta001 Jul 10, 2026
d2c69dd
Wait for the operation to be destroyed on the self-join test's succes…
bmehta001 Jul 10, 2026
8650ce1
Guarantee async ops are destroyed before teardown in both self-join t…
bmehta001 Jul 10, 2026
bd49de4
Hard-stop if a detached curl worker refuses to drain before teardown
bmehta001 Jul 10, 2026
42d448c
Fix curl worker shutdown lifetime
bmehta001 Jul 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 85 additions & 22 deletions lib/http/HttpClient_Curl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,44 @@ namespace MAT_NS_BEGIN {

HttpClient_Curl::~HttpClient_Curl()
{
curl_global_cleanup();
auto state = m_state;
auto activeOps = state->activeOps;

// Detached worker threads run curl_easy_cleanup in ~CurlHttpOperation after
// the request callback has already been removed from HttpClientManager's
// tracking, so waiting only on that tracking is not enough. Wait (bounded)
// for all in-flight operations to finish their easy-handle cleanup before
// curl_global_cleanup, which must not run concurrently with it.
bool drained;
{
std::unique_lock<std::mutex> lock(activeOps->mtx);
drained = activeOps->cv.wait_for(lock, std::chrono::seconds(5),
[activeOps] { return activeOps->inFlight == 0; });
if (!drained)
{
TRACE("~HttpClient_Curl: %d operation(s) still in flight after 5s; skipping curl_global_cleanup\n", activeOps->inFlight);
}
}
if (!drained)
{
activeOps->abandonCallbacks.store(true, std::memory_order_release);
std::lock_guard<std::mutex> lock(state->requestsMtx);
// Detached workers capture this shared state, not HttpClient_Curl. If the
// bounded drain times out, the client object is about to be destroyed; do
// not retain raw request pointers or dispatch late response/logging
// callbacks that may refer to shutdown-owned state. The worker will erase
// no-op and drop the response instead of dereferencing the destroyed client.
state->requests.clear();
}
// curl_global_cleanup must not run concurrently with any other libcurl use,
// including the curl_easy_cleanup that in-flight CurlHttpOperation destructors
// run on their detached workers. If the drain timed out, skip it: leaking
// libcurl's global state once at shutdown is safer than the crash/UB of tearing
// it down while an easy handle is still live on another thread.
if (drained)
{
curl_global_cleanup();
}
TRACE("Destroyed HttpClient_Curl.\n");
};

Expand All @@ -65,6 +102,8 @@ namespace MAT_NS_BEGIN {

void HttpClient_Curl::SendRequestAsync(IHttpRequest* request, IHttpResponseCallback* callback)
{
auto state = m_state;

// Note: 'request' is never owned by IHttpClient and gets deleted in EventsUploadContext.clear()
AddRequest(request);
auto curlRequest = static_cast<CurlHttpRequest*>(request);
Expand All @@ -77,16 +116,43 @@ namespace MAT_NS_BEGIN {

std::string sslCaInfo;
{
std::lock_guard<std::mutex> lock(m_requestsMtx);
sslCaInfo = m_sslCaInfo;
std::lock_guard<std::mutex> lock(state->requestsMtx);
sslCaInfo = state->sslCaInfo;
}

// Copy the request body into the operation instead of moving it out. The
// detached send needs an owned buffer (the request can be released -- e.g. by
// cancellation -- while the worker is still sending), but the request's
// m_body is also read again after the send: HttpResponseDecoder emits the
// request payload on EVT_HTTP_OK / EVT_HTTP_ERROR when requestDone runs the
Comment thread
bmehta001 marked this conversation as resolved.
// decode chain, before the request is released. Moving it out would leave
// those debug events with an empty payload -- a curl-only regression versus
// the WinInet and NSURLSession clients, which leave the request intact.
auto curlOperation = std::make_shared<CurlHttpOperation>(curlRequest->m_method, curlRequest->m_url, callback, requestHeaders, curlRequest->m_body, false, HTTP_CONN_TIMEOUT, m_sslVerify, sslCaInfo);
// Count this operation before the async send starts so ~HttpClient_Curl waits
// for its curl_easy_cleanup to complete before curl_global_cleanup.
curlOperation->trackWith(state->activeOps);
curlRequest->SetOperation(curlOperation);

// The lifetime of curlOperation is guarnteed by the call to result.wait() in the d'tor.
curlOperation->SendAsync([this, callback, requestId](CurlHttpOperation& operation) {
this->EraseRequest(requestId);

// The async Send() runs on a detached worker that holds its own shared_ptr
// to curlOperation (see CurlHttpOperation::SendAsync), so the operation --
// and its curl handle, response buffer and owned copy of the request body --
// stay alive until Send() and the callback below have finished, regardless
// of when the owning CurlHttpRequest is released. If the callback leads to
// that request being destroyed on the worker thread (OnHttpResponse ->
// EventsUploadContext::clear()), the operation is simply destroyed there
// once the worker returns; there is no future to join.
curlOperation->SendAsync([state, callback, requestId](CurlHttpOperation& operation) {
const bool abandonCallback = state->activeOps->abandonCallbacks.load(std::memory_order_acquire);
{
std::lock_guard<std::mutex> lock(state->requestsMtx);
state->requests.erase(requestId);
}
if (abandonCallback)
{
TRACE("HttpClient_Curl shutdown abandoned response callback for %s\n", requestId.c_str());
return;
}

auto response = std::unique_ptr<SimpleHttpResponse>(new SimpleHttpResponse(requestId));
response->m_result = HttpResult_OK;
Expand Down Expand Up @@ -116,14 +182,16 @@ namespace MAT_NS_BEGIN {

void HttpClient_Curl::CancelRequestAsync(std::string const& id)
{
auto state = m_state;
CurlHttpRequest* request = nullptr;
{
// Hold the lock only while iterating over the list of requests
std::lock_guard<std::mutex> lock(m_requestsMtx);
if (m_requests.find(id) != m_requests.cend()) {
request = static_cast<CurlHttpRequest*>(m_requests[id]);
std::lock_guard<std::mutex> lock(state->requestsMtx);
auto requestIt = state->requests.find(id);
if (requestIt != state->requests.cend()) {
request = static_cast<CurlHttpRequest*>(requestIt->second);
LOG_TRACE("HTTP request=%p id=%s being aborted...", request, id.c_str());
m_requests.erase(id);
state->requests.erase(requestIt);
}
}

Expand All @@ -142,23 +210,18 @@ namespace MAT_NS_BEGIN {
void HttpClient_Curl::SetSslVerification(bool sslVerify, const std::string& caInfo)
{
m_sslVerify = sslVerify;
std::lock_guard<std::mutex> lock(m_requestsMtx);
m_sslCaInfo = caInfo;
}

void HttpClient_Curl::EraseRequest(std::string const& id)
{
std::lock_guard<std::mutex> lock(m_requestsMtx);
m_requests.erase(id);
auto state = m_state;
std::lock_guard<std::mutex> lock(state->requestsMtx);
state->sslCaInfo = caInfo;
}

void HttpClient_Curl::AddRequest(IHttpRequest* request)
{
std::lock_guard<std::mutex> lock(m_requestsMtx);
m_requests[request->GetId()] = request;
auto state = m_state;
std::lock_guard<std::mutex> lock(state->requestsMtx);
state->requests[request->GetId()] = request;
}

} MAT_NS_END

#endif

Loading
Loading