From 7b43dd4504e1a3764cda2cca23ab77fa0f50b9b9 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Wed, 10 Jun 2026 13:44:38 -0500 Subject: [PATCH 1/6] fix: guard HAVE_MAT_LIVEEVENTINSPECTOR/PRIVACYGUARD against redefinition Under -Werror on Linux/macOS, the modules-repo CI (build-posix-latest-exp) has been failing for ~2 weeks with: config-default.h:36: error: 'HAVE_MAT_LIVEEVENTINSPECTOR' macro redefined [-Werror,-Wmacro-redefined] config-default.h:37: error: 'HAVE_MAT_PRIVACYGUARD' macro redefined tests/functests/CMakeLists.txt and tests/unittests/CMakeLists.txt add -DHAVE_MAT_LIVEEVENTINSPECTOR / -DHAVE_MAT_PRIVACYGUARD on the command line when BUILD_LIVEEVENTINSPECTOR / BUILD_PRIVACYGUARD (default YES) and the respective module dir exists. The three config-default headers then redefined them unconditionally, which is fatal under -Werror (added by #1415). Wrapping the two defines in #ifndef in all three config-default*.h headers preserves all existing behavior: - Without command-line -D: macros get defined here as before. - With command-line -D: header skips the redefinition, no warning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/include/mat/config-default-cs4.h | 8 ++++++++ lib/include/mat/config-default-exp.h | 8 ++++++++ lib/include/mat/config-default.h | 8 ++++++++ 3 files changed, 24 insertions(+) diff --git a/lib/include/mat/config-default-cs4.h b/lib/include/mat/config-default-cs4.h index 7aae9fc7e..71a79c10f 100644 --- a/lib/include/mat/config-default-cs4.h +++ b/lib/include/mat/config-default-cs4.h @@ -27,8 +27,16 @@ /* #define HAVE_MAT_EVT_TRACEID */ #define HAVE_MAT_STORAGE #define HAVE_MAT_DEFAULT_HTTP_CLIENT +// The two macros below are also added on the command line by +// tests/{functests,unittests}/CMakeLists.txt when BUILD_LIVEEVENTINSPECTOR +// / BUILD_PRIVACYGUARD are ON. Guard against -Wmacro-redefined under +// -Werror on Linux/macOS. +#ifndef HAVE_MAT_LIVEEVENTINSPECTOR #define HAVE_MAT_LIVEEVENTINSPECTOR +#endif +#ifndef HAVE_MAT_PRIVACYGUARD #define HAVE_MAT_PRIVACYGUARD +#endif //#define HAVE_MAT_DEFAULT_FILTER #if defined(_WIN32) && !defined(_WINRT_DLL) #define HAVE_MAT_NETDETECT diff --git a/lib/include/mat/config-default-exp.h b/lib/include/mat/config-default-exp.h index 609692a01..256dfe615 100644 --- a/lib/include/mat/config-default-exp.h +++ b/lib/include/mat/config-default-exp.h @@ -25,8 +25,16 @@ /* #define HAVE_MAT_EVT_TRACEID */ #define HAVE_MAT_STORAGE #define HAVE_MAT_DEFAULT_HTTP_CLIENT +// The two macros below are also added on the command line by +// tests/{functests,unittests}/CMakeLists.txt when BUILD_LIVEEVENTINSPECTOR +// / BUILD_PRIVACYGUARD are ON. Guard against -Wmacro-redefined under +// -Werror on Linux/macOS. +#ifndef HAVE_MAT_LIVEEVENTINSPECTOR #define HAVE_MAT_LIVEEVENTINSPECTOR +#endif +#ifndef HAVE_MAT_PRIVACYGUARD #define HAVE_MAT_PRIVACYGUARD +#endif //#define HAVE_MAT_DEFAULT_FILTER #if defined(_WIN32) && !defined(_WINRT_DLL) #define HAVE_MAT_NETDETECT diff --git a/lib/include/mat/config-default.h b/lib/include/mat/config-default.h index 9617611c9..2ddce7dfc 100644 --- a/lib/include/mat/config-default.h +++ b/lib/include/mat/config-default.h @@ -33,8 +33,16 @@ /* #define HAVE_MAT_EVT_TRACEID */ #define HAVE_MAT_STORAGE #define HAVE_MAT_DEFAULT_HTTP_CLIENT +// The two macros below are also added on the command line by +// tests/{functests,unittests}/CMakeLists.txt when BUILD_LIVEEVENTINSPECTOR +// / BUILD_PRIVACYGUARD are ON. Guard against -Wmacro-redefined under +// -Werror on Linux/macOS. +#ifndef HAVE_MAT_LIVEEVENTINSPECTOR #define HAVE_MAT_LIVEEVENTINSPECTOR +#endif +#ifndef HAVE_MAT_PRIVACYGUARD #define HAVE_MAT_PRIVACYGUARD +#endif //#define HAVE_MAT_DEFAULT_FILTER #if defined(_WIN32) && !defined(_WINRT_DLL) #define HAVE_MAT_NETDETECT From fc7375aaf2a28566ea0fd1c59e2f67a3f314ba4d Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Thu, 11 Jun 2026 03:16:33 -0500 Subject: [PATCH 2/6] fix: prevent EDEADLK self-join in ~CurlHttpOperation on async-thread destruction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The modules-repo CI test ECSClientFuncTests.GetConfigs (and every test in the ECSClientFuncTests suite) crashed on Linux/macOS with: terminate called after throwing an instance of 'std::system_error' what(): Resource deadlock avoided Aborted (core dumped) Root cause ========== SendAsync() runs Send() + the user callback on a std::async worker thread. The callback owns a strong ref to CurlHttpOperation, so when it releases the last ref the ~CurlHttpOperation destructor runs on the async thread itself. libstdc++'s std::future<>::~future implicitly calls _Async_state_impl::~_Async_state_impl, which calls _M_complete_async -> _M_join via std::call_once. On the async thread that's a self-join; call_once throws std::system_error(EDEADLK). Because the throw escapes a noexcept destructor, terminate() aborts the process. A try/catch around the future cannot rescue this — destructors of std::future are noexcept. Fix === Move the future onto a detached helper thread before its destructor runs. The helper is by definition NOT the async thread (we'd only be on the async thread if its work already finished), so the implicit join completes immediately. On the common path (destruction from the caller thread) it costs one short-lived thread spawn that exits in microseconds. Verified locally with sister + modules linked: all 113 FuncTests pass, including all 25 ECSClientFuncTests (which include the formerly-fatal GetConfigs). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/http/HttpClient_Curl.hpp | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/http/HttpClient_Curl.hpp b/lib/http/HttpClient_Curl.hpp index 972cc4fec..012a2fd99 100644 --- a/lib/http/HttpClient_Curl.hpp +++ b/lib/http/HttpClient_Curl.hpp @@ -169,11 +169,26 @@ class CurlHttpOperation { */ virtual ~CurlHttpOperation() { - // Given the request has not been aborted we should wait for completion here - // This guarantees the lifetime of this request. + // libstdc++'s std::future<>::~future implicitly joins the async + // thread via call_once during destruction. If this destructor runs + // ON that same async thread (e.g. the user callback released the + // last shared_ptr from inside the lambda), the implicit self-join + // throws std::system_error("Resource deadlock avoided"), and because + // the throw originates inside a noexcept destructor it aborts the + // process. try/catch around `result` cannot rescue it. + // + // Defuse by moving the future onto a detached helper thread that is + // by definition NOT the async thread, so its implicit join succeeds + // immediately (the work has already finished, which is the only way + // we could be running this destructor on the async thread). On the + // common path (destructed from the caller thread) this just spawns + // a no-op helper that exits in microseconds. if (result.valid()) { - result.wait(); + std::thread([f = std::move(result)]() mutable { + // f goes out of scope here. ~future joins on this new + // thread (!= the original async thread), so no EDEADLK. + }).detach(); } DispatchEvent(OnDestroy); res = CURLE_OK; From 3554d8df14784caffb4b91a62c28cd2436cf12d2 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Sat, 13 Jun 2026 16:47:37 -0500 Subject: [PATCH 3/6] Make ~CurlHttpOperation detach conditional on self-join (fix UAF regression) Code review found that the previous fix detached the async future's join for EVERY destruction. That removed the cross-thread lifetime guarantee the old result.wait() provided: when the operation is destroyed from another thread while the async Send() is still running, the destructor would proceed to curl_easy_cleanup()/ReleaseResponse() and destroy the by-reference request body while the worker thread is still using them -> use-after-free. Restore the guarantee while keeping the EDEADLK self-join fix: - Record the async task's thread id (atomic) when SendAsync's task starts. - In the destructor, compare std::this_thread::get_id(): * self-join (destroyed from within our own async callback, e.g. EraseRequest drops the last reference): the work is necessarily complete, so defer the future's join to a detached helper thread instead of joining on this (the async) thread, avoiding EDEADLK. * cross-thread: result.wait() to keep the curl handle, response buffer and by-reference request body alive until the async Send() finishes. - Heap-allocate the deferred future first so a rare std::thread spawn failure leaks the already-finished future rather than self-joining (EDEADLK) or letting std::system_error escape this noexcept destructor (std::terminate). - Refresh the stale HttpClient_Curl.cpp lifetime comment. Logic validated with a standalone C++11 repro under AddressSanitizer: the cross-thread path waits (no UAF) and the self-join path does not deadlock. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/http/HttpClient_Curl.cpp | 5 ++- lib/http/HttpClient_Curl.hpp | 61 ++++++++++++++++++++++++++---------- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/lib/http/HttpClient_Curl.cpp b/lib/http/HttpClient_Curl.cpp index b910cdf28..50ab062f4 100644 --- a/lib/http/HttpClient_Curl.cpp +++ b/lib/http/HttpClient_Curl.cpp @@ -84,7 +84,10 @@ namespace MAT_NS_BEGIN { auto curlOperation = std::make_shared(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. + // The lifetime of curlOperation across the async Send is guaranteed by + // ~CurlHttpOperation: when this shared_ptr is released from another + // thread it waits for the async result; when the callback below drops + // the last reference (EraseRequest) it defers the join instead. curlOperation->SendAsync([this, callback, requestId](CurlHttpOperation& operation) { this->EraseRequest(requestId); diff --git a/lib/http/HttpClient_Curl.hpp b/lib/http/HttpClient_Curl.hpp index 6b5530ad0..5d2d6cfd9 100644 --- a/lib/http/HttpClient_Curl.hpp +++ b/lib/http/HttpClient_Curl.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -173,26 +174,46 @@ class CurlHttpOperation { */ virtual ~CurlHttpOperation() { - // libstdc++'s std::future<>::~future implicitly joins the async - // thread via call_once during destruction. If this destructor runs - // ON that same async thread (e.g. the user callback released the - // last shared_ptr from inside the lambda), the implicit self-join - // throws std::system_error("Resource deadlock avoided"), and because - // the throw originates inside a noexcept destructor it aborts the - // process. try/catch around `result` cannot rescue it. + // libstdc++'s std::future<>::~future implicitly joins the async thread + // during destruction. If this destructor runs ON that same async thread + // (the Send() callback dropped the last reference to us, e.g. via + // EraseRequest), that join is a self-join and throws + // std::system_error("Resource deadlock avoided"); since it originates in + // this noexcept destructor it aborts the process. // - // Defuse by moving the future onto a detached helper thread that is - // by definition NOT the async thread, so its implicit join succeeds - // immediately (the work has already finished, which is the only way - // we could be running this destructor on the async thread). On the - // common path (destructed from the caller thread) this just spawns - // a no-op helper that exits in microseconds. + // Distinguish the two cases by the thread id recorded when the async + // task started: + // * self-join -> the work is necessarily complete; defer the + // future's join to a detached helper thread instead + // of blocking/joining on this (the async) thread. + // * cross-thread -> the async Send() may still be running, so wait() + // to keep the curl handle, response buffer and the + // by-reference request body alive until it finishes. if (result.valid()) { - std::thread([f = std::move(result)]() mutable { - // f goes out of scope here. ~future joins on this new - // thread (!= the original async thread), so no EDEADLK. - }).detach(); + if (std::this_thread::get_id() == m_asyncThreadId.load(std::memory_order_acquire)) + { + // Heap-allocate first so a rare std::thread spawn failure leaks + // the already-finished future rather than joining it on this + // async thread (EDEADLK) or letting std::system_error escape + // this noexcept destructor. + std::future* pending = new (std::nothrow) std::future(std::move(result)); + if (pending != nullptr) + { + try + { + std::thread([pending]() { delete pending; }).detach(); + } + catch (...) + { + // Thread exhaustion: intentionally leak *pending. + } + } + } + else + { + result.wait(); + } } DispatchEvent(OnDestroy); res = CURLE_OK; @@ -334,6 +355,7 @@ class CurlHttpOperation { std::future & SendAsync(std::function callback = nullptr) { result = std::async(std::launch::async, [this, callback] { + m_asyncThreadId.store(std::this_thread::get_id(), std::memory_order_release); long result = Send(); if (callback!=nullptr) callback(*this); @@ -452,6 +474,11 @@ class CurlHttpOperation { CURL *curl; // Local curl instance CURLcode res = CURLE_OK; // Curl result OR HTTP status code if successful + + // Id of the thread running the async Send() task (set when the task starts). + // Lets ~CurlHttpOperation detect a self-join (destruction from within the + // async callback) and avoid the EDEADLK that joining the future would raise. + std::atomic m_asyncThreadId{}; IHttpResponseCallback* m_callback = nullptr; From b10fa89ae55affdea4e45e9cb6bfcc684335840f Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Sat, 13 Jun 2026 17:21:28 -0500 Subject: [PATCH 4/6] Address Copilot round 2 on #1481: include , handle nothrow-new failure - Add #include so std::nothrow is not relied on transitively (review). - If new (std::nothrow) returns nullptr (OOM), result stays valid and would self-join (EDEADLK) at end of the noexcept dtor; abort() as a last resort instead of falling through to that, per review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/http/HttpClient_Curl.hpp | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/http/HttpClient_Curl.hpp b/lib/http/HttpClient_Curl.hpp index 5d2d6cfd9..c7902a3f2 100644 --- a/lib/http/HttpClient_Curl.hpp +++ b/lib/http/HttpClient_Curl.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -198,16 +199,22 @@ class CurlHttpOperation { // async thread (EDEADLK) or letting std::system_error escape // this noexcept destructor. std::future* pending = new (std::nothrow) std::future(std::move(result)); - if (pending != nullptr) + if (pending == nullptr) { - try - { - std::thread([pending]() { delete pending; }).detach(); - } - catch (...) - { - // Thread exhaustion: intentionally leak *pending. - } + // Out of memory: `result` is still valid and would self-join + // (EDEADLK) when destroyed on this async thread at the end of + // the destructor, and there is no allocation-free way to move + // it off-thread. Abort as a last resort rather than fall + // through to a guaranteed EDEADLK abort. + std::abort(); + } + try + { + std::thread([pending]() { delete pending; }).detach(); + } + catch (...) + { + // Thread exhaustion: intentionally leak *pending. } } else From 3dda53bc8ce2c0fb6f3c016eb542aac84fcc36dd Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Sat, 13 Jun 2026 17:37:50 -0500 Subject: [PATCH 5/6] Address Copilot round 3 on #1481: avoid atomic, fix lifetime comments - Replace std::atomic (not guaranteed supported across standard libraries) with a plain std::thread::id published via an std::atomic flag using release/acquire ordering. - Correct the lifetime comments: the operation's last shared_ptr is held by the owning CurlHttpRequest (via SetOperation), not by EraseRequest (which only removes the raw id from m_requests). The self-join occurs when the async callback leads to that request being destroyed on the async thread (OnHttpResponse -> EventsUploadContext::clear()). Re-validated the wait-vs-detach logic with a standalone C++11 repro under AddressSanitizer + UBSan. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/http/HttpClient_Curl.cpp | 9 ++++++--- lib/http/HttpClient_Curl.hpp | 29 ++++++++++++++++++----------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/lib/http/HttpClient_Curl.cpp b/lib/http/HttpClient_Curl.cpp index 50ab062f4..e8c620d61 100644 --- a/lib/http/HttpClient_Curl.cpp +++ b/lib/http/HttpClient_Curl.cpp @@ -85,9 +85,12 @@ namespace MAT_NS_BEGIN { curlRequest->SetOperation(curlOperation); // The lifetime of curlOperation across the async Send is guaranteed by - // ~CurlHttpOperation: when this shared_ptr is released from another - // thread it waits for the async result; when the callback below drops - // the last reference (EraseRequest) it defers the join instead. + // ~CurlHttpOperation. After this function returns, the only remaining + // shared_ptr is the one held by the owning CurlHttpRequest. When that + // request is destroyed from another thread, the destructor waits for the + // async result; if the callback below leads to the request being + // destroyed on the async thread itself (OnHttpResponse -> + // EventsUploadContext::clear()), the destructor defers the join instead. curlOperation->SendAsync([this, callback, requestId](CurlHttpOperation& operation) { this->EraseRequest(requestId); diff --git a/lib/http/HttpClient_Curl.hpp b/lib/http/HttpClient_Curl.hpp index c7902a3f2..58ec0c85c 100644 --- a/lib/http/HttpClient_Curl.hpp +++ b/lib/http/HttpClient_Curl.hpp @@ -177,12 +177,13 @@ class CurlHttpOperation { { // libstdc++'s std::future<>::~future implicitly joins the async thread // during destruction. If this destructor runs ON that same async thread - // (the Send() callback dropped the last reference to us, e.g. via - // EraseRequest), that join is a self-join and throws - // std::system_error("Resource deadlock avoided"); since it originates in - // this noexcept destructor it aborts the process. + // (the async callback led to the owning CurlHttpRequest being destroyed + // on that thread, e.g. OnHttpResponse -> EventsUploadContext::clear()), + // that join is a self-join and throws std::system_error("Resource + // deadlock avoided"); since it originates in this noexcept destructor it + // aborts the process. // - // Distinguish the two cases by the thread id recorded when the async + // Distinguish the two cases by the thread id published when the async // task started: // * self-join -> the work is necessarily complete; defer the // future's join to a detached helper thread instead @@ -192,7 +193,8 @@ class CurlHttpOperation { // by-reference request body alive until it finishes. if (result.valid()) { - if (std::this_thread::get_id() == m_asyncThreadId.load(std::memory_order_acquire)) + if (m_asyncThreadIdSet.load(std::memory_order_acquire) && + std::this_thread::get_id() == m_asyncThreadId) { // Heap-allocate first so a rare std::thread spawn failure leaks // the already-finished future rather than joining it on this @@ -362,7 +364,8 @@ class CurlHttpOperation { std::future & SendAsync(std::function callback = nullptr) { result = std::async(std::launch::async, [this, callback] { - m_asyncThreadId.store(std::this_thread::get_id(), std::memory_order_release); + m_asyncThreadId = std::this_thread::get_id(); + m_asyncThreadIdSet.store(true, std::memory_order_release); long result = Send(); if (callback!=nullptr) callback(*this); @@ -482,10 +485,14 @@ class CurlHttpOperation { CURL *curl; // Local curl instance CURLcode res = CURLE_OK; // Curl result OR HTTP status code if successful - // Id of the thread running the async Send() task (set when the task starts). - // Lets ~CurlHttpOperation detect a self-join (destruction from within the - // async callback) and avoid the EDEADLK that joining the future would raise. - std::atomic m_asyncThreadId{}; + // Id of the thread running the async Send() task, published via the + // atomic flag below (release/acquire). ~CurlHttpOperation uses these + // to detect a self-join (destruction from within the async callback) and + // avoid the EDEADLK that joining the future would raise. A plain thread::id + // plus an atomic flag is used instead of std::atomic, + // which is not guaranteed to be supported across standard libraries. + std::thread::id m_asyncThreadId{}; + std::atomic m_asyncThreadIdSet{ false }; IHttpResponseCallback* m_callback = nullptr; From 7e22ed22d85329419bbc7727241ccecf60b36d66 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Sat, 13 Jun 2026 17:50:09 -0500 Subject: [PATCH 6/6] Address Copilot round 4 on #1481: precise self-join comment, reset flag on reuse - Reword the self-join comment: in that case Send() has returned (we are in its callback) but the async task itself has not yet returned (the destructor runs inside it), so the deferred helper's ~future join completes only after this destructor unwinds. Avoids implying the async task is already finished. - Reset m_asyncThreadIdSet to false at the start of SendAsync so self-join detection stays correct if the operation were ever reused (it is single-use today: one SendAsync per request). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/http/HttpClient_Curl.hpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/http/HttpClient_Curl.hpp b/lib/http/HttpClient_Curl.hpp index 58ec0c85c..33d217924 100644 --- a/lib/http/HttpClient_Curl.hpp +++ b/lib/http/HttpClient_Curl.hpp @@ -185,9 +185,13 @@ class CurlHttpOperation { // // Distinguish the two cases by the thread id published when the async // task started: - // * self-join -> the work is necessarily complete; defer the - // future's join to a detached helper thread instead - // of blocking/joining on this (the async) thread. + // * self-join -> Send() has returned (we are running inside its + // callback), but the async task itself has not yet + // returned (this destructor is executing inside it), + // so defer the future's join to a detached helper + // thread rather than joining on this (the async) + // thread; the helper's join completes once the task + // returns after this destructor unwinds. // * cross-thread -> the async Send() may still be running, so wait() // to keep the curl handle, response buffer and the // by-reference request body alive until it finishes. @@ -363,6 +367,10 @@ class CurlHttpOperation { } std::future & SendAsync(std::function callback = nullptr) { + // Reset the publication flag before launching so self-join detection + // stays correct even if this operation were ever reused (today each + // CurlHttpOperation is single-use: one SendAsync call per request). + m_asyncThreadIdSet.store(false, std::memory_order_release); result = std::async(std::launch::async, [this, callback] { m_asyncThreadId = std::this_thread::get_id(); m_asyncThreadIdSet.store(true, std::memory_order_release);