Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 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
6b793ad
Simplify curl worker lifetime handling
bmehta001 Jul 30, 2026
88f5c8f
Preserve curl completion semantics on worker failures
bmehta001 Jul 30, 2026
48ad566
Merge microsoft/main into the curl self-join fix
bmehta001 Jul 30, 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
7 changes: 2 additions & 5 deletions lib/http/HttpClient_Curl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,9 @@ namespace MAT_NS_BEGIN {

auto curlOperation = std::make_shared<CurlHttpOperation>(curlRequest->m_method, curlRequest->m_url, callback, requestHeaders, curlRequest->m_body, false, HTTP_CONN_TIMEOUT, m_sslVerify, sslCaInfo);
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);

curlOperation->SendAsync([this, callback, requestId](CurlHttpOperation& operation) {
EraseRequest(requestId);
auto response = std::unique_ptr<SimpleHttpResponse>(new SimpleHttpResponse(requestId));
response->m_result = HttpResult_OK;

Expand Down Expand Up @@ -161,4 +159,3 @@ namespace MAT_NS_BEGIN {
} MAT_NS_END

#endif

129 changes: 106 additions & 23 deletions lib/http/HttpClient_Curl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@
#include <sstream>
#include <vector>
#include <iterator>
#include <map>

#include <algorithm>
#include <numeric>
#include <future>
#include <limits>
#include <atomic>
#include <thread>
#include <mutex>
#include <stdexcept>

#include <poll.h>
#include <curl/curl.h>
Expand Down Expand Up @@ -94,9 +98,10 @@ class CurlHttpOperation {
std::string method,
std::string url,
IHttpResponseCallback* callback,
// requestHeaders is copied into the curl_slist during construction
// and need not outlive this operation. requestBody is stored by
// reference and read by Send(), so it must outlive this operation.
// requestHeaders is copied into the curl_slist during construction and
// need not outlive this operation. requestBody is stored by reference;
// CurlHttpRequest destroys this operation (which joins the worker) before
// destroying its inherited request-body storage.
const std::map<std::string, std::string>& requestHeaders,
const std::vector<uint8_t>& requestBody,
// Default connectivity and response size options
Expand Down Expand Up @@ -173,13 +178,22 @@ class CurlHttpOperation {
*/
virtual ~CurlHttpOperation()
{
// Given the request has not been aborted we should wait for completion here
// This guarantees the lifetime of this request.
if (result.valid())
if (m_worker.joinable())
{
result.wait();
if (m_worker.get_id() == std::this_thread::get_id())
{
// The completion callback can release the owning request on this
// worker. Detach rather than joining the current thread; Send() has
// finished and the worker does not touch this operation afterward.
m_worker.detach();
}
else
{
m_worker.join();
}
}
DispatchEvent(OnDestroy);

DispatchDestroyEvent();
res = CURLE_OK;
curl_easy_cleanup(curl);
curl_slist_free_all(m_headersChunk);
Expand Down Expand Up @@ -317,14 +331,46 @@ class CurlHttpOperation {
return res;
}

std::future<long> & SendAsync(std::function<void(CurlHttpOperation &)> callback = nullptr) {
result = std::async(std::launch::async, [this, callback] {
long result = Send();
if (callback!=nullptr)
callback(*this);
return result;
});
return result;
void SendAsync(std::function<void(CurlHttpOperation &)> callback = nullptr) {
// A newly created std::thread may run before it is assigned to m_worker.
// Hold this gate until the assignment completes so a fast failure cannot
// destroy the operation from its callback while SendAsync still uses it.
{
std::lock_guard<std::mutex> startGuard(m_workerStartMtx);
if (m_sendAttempted)
{
throw std::logic_error("CurlHttpOperation is single-use");
}
m_sendAttempted = true;

try
{
m_worker = std::thread([this, callback]() {
{
std::lock_guard<std::mutex> startGuard(m_workerStartMtx);
}
try
{
Send();
}
catch (...)
{
// std::async stored worker exceptions in its unobserved
// future. A raw thread must contain them.
res = CURLE_FAILED_INIT;
}
Complete(callback);
});
return;
}
catch (...)
{
// Callable allocation/copy or std::thread creation failed.
}
}

res = CURLE_FAILED_INIT;
Complete(callback);
}

/**
Expand Down Expand Up @@ -437,17 +483,15 @@ class CurlHttpOperation {

CURL *curl; // Local curl instance
CURLcode res = CURLE_OK; // Curl result OR HTTP status code if successful

IHttpResponseCallback* m_callback = nullptr;

// Request values
std::string m_method;
std::string m_url;
std::string m_sslCaInfo;
// The SDK upload path keeps the owning IHttpRequest alive through the
// callback context until Send() completes; copying this body would duplicate
// every upload payload. Unlike CURLOPT_CAINFO, the body pointer is set and
// consumed during Send(), not retained from construction.
// The owning CurlHttpRequest destroys this operation before its inherited
// request-body storage, and cross-thread destruction joins the worker.
const std::vector<uint8_t>& requestBody;
struct curl_slist *m_headersChunk = nullptr;

Expand All @@ -464,7 +508,46 @@ class CurlHttpOperation {
size_t sendlen = 0; // # bytes sent by client
size_t acklen = 0; // # bytes ack by server

std::future<long> result;
std::mutex m_workerStartMtx;
bool m_sendAttempted = false;
std::thread m_worker;
std::atomic<bool> m_destroyEventDispatched { false };

void DispatchDestroyEvent() noexcept
{
bool expected = false;
if (m_destroyEventDispatched.compare_exchange_strong(
expected, true, std::memory_order_acq_rel))
{
try
{
DispatchEvent(OnDestroy);
}
catch (...)
{
// State observers must not terminate the worker or destructor.
}
}
}

void Complete(const std::function<void(CurlHttpOperation &)>& callback) noexcept
{
// Preserve the documented state event while m_callback is still valid.
// The completion callback can release the last owner, so this must remain
// the worker's final access to the operation.
DispatchDestroyEvent();
try
{
if (callback != nullptr)
{
callback(*this);
}
}
catch (...)
{
// Match the old unobserved-future behavior at the thread boundary.
}
}

/**
* Helper routine to wait for data on socket
Expand Down
75 changes: 72 additions & 3 deletions tests/unittests/HttpClientCurlTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@
#include "http/HttpClient_Curl.hpp"
#include "config/RuntimeConfig_Default.hpp"

#include <future>
#include <chrono>
#include <memory>
#include <atomic>
#include <cstdlib>
#include <functional>
#include <stdexcept>
#include <utility>

using namespace testing;
using namespace MAT;

Expand Down Expand Up @@ -127,6 +136,68 @@ TEST_F(HttpClientCurlTests, SetSslVerification_ConcurrentCallsNoRace)
SUCCEED();
}

// --- Regression: EDEADLK self-join in ~CurlHttpOperation ---

TEST_F(HttpClientCurlTests, SendAsync_DestroyOnWorkerThread_NoSelfJoin)
{
struct TrackingCallback : public IHttpResponseCallback
{
std::atomic<int> destroyEvents { 0 };
void OnHttpResponse(IHttpResponse* response) override { delete response; }
void OnHttpStateEvent(HttpStateEvent state, void*, size_t) override
{
if (state == OnDestroy)
{
++destroyEvents;
}
}
};

auto callback = std::make_shared<TrackingCallback>();
auto callbackDone = std::make_shared<std::promise<void>>();
auto done = callbackDone->get_future();

auto op = std::make_shared<CurlHttpOperation>(
"GET", "://malformed", callback.get(), m_headers, m_body,
false, 1 /*connTimeout*/, false /*sslVerify*/, "");

auto box = std::make_shared<std::shared_ptr<CurlHttpOperation>>(std::move(op));
(*box)->SendAsync([box, callback, callbackDone](CurlHttpOperation&) {
box->reset();
callbackDone->set_value();
});

if (done.wait_for(std::chrono::seconds(5)) != std::future_status::ready)
{
ADD_FAILURE() << "curl worker did not finish before fixture teardown";
std::abort();
}
EXPECT_EQ(callback->destroyEvents.load(), 1);
}

TEST_F(HttpClientCurlTests, SendAsync_CallbackCopyFailureStillCompletes)
{
struct ThrowOnCopy
{
explicit ThrowOnCopy(bool& invoked) : invoked(&invoked) {}
ThrowOnCopy(ThrowOnCopy&&) = default;
ThrowOnCopy(const ThrowOnCopy&) { throw std::logic_error("copy failed"); }
void operator()(CurlHttpOperation&) const { *invoked = true; }
bool* invoked;
};

CurlHttpOperation op(
"GET", "://malformed", nullptr, m_headers, m_body,
false, 1 /*connTimeout*/, false /*sslVerify*/, "");
bool callbackInvoked = false;
std::function<void(CurlHttpOperation&)> callback { ThrowOnCopy(callbackInvoked) };

EXPECT_NO_THROW(op.SendAsync(std::move(callback)));
EXPECT_TRUE(callbackInvoked);
EXPECT_EQ(op.GetResponseCode(), CURLE_FAILED_INIT);
EXPECT_THROW(op.SendAsync(), std::logic_error);
}

// --- Response-size cap (memory-amplification DoS hardening) ---

class HttpClientCurlResponseCapTests : public ::testing::Test,
Expand All @@ -138,9 +209,7 @@ class HttpClientCurlResponseCapTests : public ::testing::Test,
HttpClient_Curl m_client;
// The client never takes ownership of the request (it only stores a raw pointer
// and erases it); the fixture owns it and frees it in TearDown -- on the main
// thread, after the transfer has completed. Freeing it inside OnHttpResponse
// would destroy the CurlHttpOperation from within its own async task, whose
// destructor waits on that task (a self-join deadlock).
// thread, after the transfer has completed.
std::unique_ptr<IHttpRequest> m_request;
std::string m_hostname;
size_t m_responseBodySize {0};
Expand Down
Loading