Fix runtime data races, memory leak, and shutdown safety#1429
Fix runtime data races, memory leak, and shutdown safety#1429bmehta001 wants to merge 39 commits into
Conversation
- HttpClient_Apple: scope Cancel() to m_dataTask only instead of blanket-cancelling every task on the shared session. Fix torn read on m_requests.empty() in CancelAllRequests spin loop. - HttpClientManager: fix torn read on m_httpCallbacks.empty() in cancelAllRequests spin loop — read under lock. - HttpResponseDecoder: add missing delete ctx->httpResponse before nullptr in Abort and RetryNetwork paths (memory leak). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Only delete queued tasks after successful join (not after detach, where the thread may still access them — undefined behavior) - Replace catch(...) with std::system_error and std::exception handlers that log error code and message - Log pending queue sizes in both join and detach paths Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both variables are read and written from different threads during normal upload scheduling. Declare as std::atomic to eliminate data races per the C++ memory model. Add .load() for variadic LOG_TRACE calls. Add comment explaining why unlocked stores in uploadAsync are safe. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove LOG_TRACE from Logger destructor — it triggers a crash on iOS simulator when the recursive_mutex used by logging has already been destroyed during static destruction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
3f0289c to
de46cb2
Compare
Reject new worker-thread tasks once shutdown starts so queue cleanup cannot race with late producers, and move the TPM scheduled-upload state back under a single mutex so latency/next-upload decisions stay consistent without mixed atomic and mutex access. Files changed: - lib/pal/WorkerThread.cpp - lib/tpm/TransmissionPolicyManager.cpp - lib/tpm/TransmissionPolicyManager.hpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR targets runtime correctness in the SDK by addressing thread-safety issues, shutdown safety, and a per-request memory leak in HTTP response handling.
Changes:
- Tighten HTTP request cancellation scoping and fix torn reads in request/callback tracking loops.
- Fix a
SimpleHttpResponseleak on aborted/network-failure decode paths. - Rework worker-thread shutdown behavior to avoid unsafe queue cleanup after
detach()and improve error logging. - Refactor
TransmissionPolicyManagerscheduling state synchronization (mutex + newcancelUploadTaskLocked()helper).
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/tpm/TransmissionPolicyManager.hpp | Changes upload-scheduling state fields and adds a locked cancellation helper declaration. |
| lib/tpm/TransmissionPolicyManager.cpp | Moves upload-scheduling state access under a mutex and adjusts cancellation/scheduling flow. |
| lib/pal/WorkerThread.cpp | Makes shutdown/join behavior safer and improves exception handling/logging during join/detach. |
| lib/http/HttpResponseDecoder.cpp | Deletes ctx->httpResponse on Abort/RetryNetwork paths to prevent leaks. |
| lib/http/HttpClient_Apple.mm | Limits cancellation to the instance’s task and fixes a torn read in the shutdown wait loop. |
| lib/http/HttpClientManager.cpp | Fixes a torn read in the shutdown wait loop by locking around empty-check. |
| lib/api/Logger.cpp | Removes destructor logging to avoid iOS static-destruction-order crash. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Keep the scheduled-upload state mutex-based, but stop holding m_scheduledUploadMutex across DeferredCallbackHandle::Cancel so shutdown and pause paths do not block uploadAsync behind the same lock. While touching the path, use std::chrono::milliseconds for the bandwidth-controller reschedule call so ENABLE_BW_CONTROLLER builds cleanly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Keep forced upload scheduling atomic around no-wait cancellation and preserve HTTP responses until downstream abort/network-failure handlers finish. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When scheduleUpload is called with force=true (or zero delay) and the previously scheduled upload task is currently executing on the worker, the no-wait cancel returns false and m_isUploadScheduled stays set. The existing m_isUploadScheduled check then skipped scheduling a new task, silently dropping the requested latency for force-scheduled profile changes. Propagate the requested latency to m_runningLatency under the same mutex when this race occurs. uploadAsync re-reads m_runningLatency inside its own LOCKGUARD, so a task that hasn't yet entered that critical section will pick up the new latency. If uploadAsync has already cleared m_isUploadScheduled (past its LOCKGUARD), the existing fallthrough at line 184 schedules a fresh task with the new latency. Add a regression test using a fake dispatcher whose Cancel always returns false, asserting that a force-scheduled call updates m_runningLatency without enqueueing a duplicate task. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use the existing LOCKGUARD helper because scheduled upload cancellation does not need movable lock ownership. Consolidate the duplicated Issue 388 cancellation note so the PR keeps the remaining limitation documented without repeating the same TODO. Files changed: - lib/tpm/TransmissionPolicyManager.cpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace tight current-time assertions with a direct comparison against the original delayed schedule time. This keeps coverage for the forced immediate upload race while reducing timing sensitivity in CI. Files changed: - tests/unittests/TransmissionPolicyManagerTests.cpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Restore the existing Issue 388 wording in the remaining cancellation comment while keeping the duplicated helper comment removed. Files changed: - lib/tpm/TransmissionPolicyManager.cpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fix printf-style logging arguments for scheduled upload delays and queued worker task pointers. Ensure the blocking cancel test releases the dispatcher before failing so async futures cannot hang the test runner. Files changed: - lib/pal/WorkerThread.cpp - lib/tpm/TransmissionPolicyManager.cpp - tests/unittests/TransmissionPolicyManagerTests.cpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
4178021 to
d357260
Compare
Addresses three Copilot review comments (TransmissionPolicyManager.cpp:119, 202, 266). This PR changed these LOG_TRACE format strings from %d to %lld but passed std::chrono::milliseconds::rep directly. That rep is implementation- defined and is long on LP64 (Linux/macOS), so %lld (which expects long long) is a -Wformat mismatch -- an error under the project's -Wall -Werror in logging-enabled (HAVE_MAT_LOGGING) builds, and formally UB in the varargs call. Cast each count() to long long so the format always matches on every data model. This mirrors the cast this PR already applies to delta (static_cast<unsigned long long> with %llu) a few lines up. Verified: clang 18 -Wall -Werror -Wextra flags the uncast %lld as "format specifies type 'long long' but the argument has type 'rep' (aka 'long')" and accepts the cast form. TransmissionPolicyManagerTests (40) pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reword the comment to describe the test without citing a tracking number; no code change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reword the three `enabled` comments to describe the behavior without citing a tracking number; no code change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
# Conflicts: # lib/pal/WorkerThread.cpp # tests/unittests/TaskDispatcherCAPITests.cpp
…own thread The process-wide PAL WorkerThread is shared by reference count. A task running on the worker thread can drop the last reference (e.g. by tearing down its LogManager/PAL), which ran ~WorkerThread -> Join() synchronously inside the task: Join() detached the thread and returned, freeing the object while threadFunc was still on the stack below the task. threadFunc then kept touching freed members (m_itemInProgress, the locks, and the queues it drains at shutdown) -- a use-after-free / heap corruption confirmed by AddressSanitizer. Give the worker a custom shared_ptr deleter: when the last reference is released on the worker thread itself, detach and defer destruction to the thread, which deletes itself only after its loop has broken and all member access is done. On any other thread the object is deleted immediately as before (~WorkerThread joins the worker first). Add a PalTests regression test that drops the last reference from within a task running on the worker thread; it is clean under AddressSanitizer with the fix and reports heap-use-after-free without it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
onLastReferenceReleased() decided whether it was running on its own worker thread via m_hThread.get_id(), which returns the default not-a-thread id after detach(). If Join() had already run on the worker thread (its self-path detaches m_hThread), a later last-reference drop on that same thread would miss the self-check and delete the object while threadFunc was still executing below it -- the same UAF this change set fixes. Capture the worker's id in an atomic at threadFunc start and compare against that instead, so detection is correct regardless of detach ordering. Not reachable through current SDK code (the default WorkerThread is never explicitly Join()-ed), so this is defense-in-depth. Validated: ASAN dispatcher tests (PalTests incl WorkerThreadSelfDisposeOnOwnThreadIsSafe, TaskDispatcherCAPITests) all pass clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two issues raised on the previous commit: - std::atomic<std::thread::id> is not portable (std::thread::id is not guaranteed trivially copyable). Store m_workerId as a plain std::thread::id guarded by the existing recursive m_lock instead; the self-dispose check reads it under the lock. - Passing std::thread::id to LOG_INFO's printf-style '%u' is undefined behavior (varargs). Format the id with std::hash<std::thread::id> and '%zu' at both log sites (the constructor's 'Started new thread' and threadFunc's 'Running thread'). This was pre-existing; the surrounding change touches these lines. Validated: ASAN dispatcher tests (PalTests incl WorkerThreadSelfDisposeOnOwnThreadIsSafe, TaskDispatcherCAPITests) 15/15 pass clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes runtime thread-safety, shutdown-safety, response-lifetime, and resource-leak issues that affect normal SDK operation. Split out from #1415 per reviewer request so runtime behavior changes stay separate from CI/build/test fixes.
Fix HTTP cancellation and callback tracking
HttpClient_Apple.mm
m_dataTaskinstead of canceling every task on the shared staticNSURLSession.CancelAllRequests()wait loop som_requests.empty()is checked while holdingm_requestsMtx.HttpClientManager.cpp
cancelAllRequests()wait loop som_httpCallbacks.empty()is checked while holdingm_httpCallbacksMtx.HttpClient_WinInet.cpp
m_hWinInetSessionwas closed only inside theif (m_hWinInetRequest != nullptr)block, so ifHttpOpenRequestAfailed afterInternetConnectAsucceeded (session set, request null) the session handle leaked.Fix HTTP response lifetime on abort/network-failure paths
HttpResponseDecoder.cpp
ctx->httpResponsethroughrequestAborted(ctx)andtemporaryNetworkFailure(ctx)so downstream storage/statistics handlers can still read status and headers.EventsUploadContext, whoseclear()path deletes the response.HttpResponseDecoderTests.cpp
Fix WorkerThread shutdown safety and task leak
ITaskDispatcher.hpp / TaskDispatcher.hpp
QueueWithResult(Task*)interface method that reports whether the dispatcher accepted the task (returnsfalseif it was dropped, e.g. during shutdown). It is declared afterCanceland defaulted to delegate toQueue()so existing dispatchers and vtable slot indices are unchanged.WorkerThread.cpp
m_shuttingDown): lateQueue()/QueueWithResult()calls are rejected (and the task deleted) once teardown starts, andQueueWithResultreturnsfalseso callers can drop a pointer they retained for cancellation.join()the owning thread deletes them; on the self-Join()path it detaches and the worker drains and deletes its own remaining tasks before exiting, so neither path leaks.TaskDispatcherCAPITests.cpp
QueueWithResultbehavior on the C-API dispatcher.Fix Flush teardown deadlock
OfflineStorageHandler.cpp
Flush()could early-return (whenStartActivity()fails during teardown) without posting flush completion, soWaitForFlush()blocked forever. Signal completion on the early-return path so teardown cannot deadlock.BasicFuncTests.cpp
MAX_TEARDOWN_TIME = 0, large payloads against the slow endpoint) and asserts shutdown completes cleanly; it also asserts the/slow/endpoint rewrite actually happened so the coverage can’t silently lapse.Make TransmissionPolicyManager scheduling consistently mutex-guarded
TransmissionPolicyManager.cpp / .hpp
m_isUploadScheduled,m_runningLatency, andm_scheduledUploadTimeconsistently withm_scheduledUploadMutex.m_scheduledUploadMutexacross potentially blocking cancellation during stop/shutdown.std::chrono::millisecondsvalue in the bandwidth-controller reschedule path, and caststd::chronocounts tolong longin the%lldLOG_TRACEcalls (the rep islongon LP64, a-Wformatmismatch in logging-enabled builds).TransmissionPolicyManagerTests.cpp
Fix Logger static-destruction-order crash
Logger.cpp
Logger::~Logger()because it can run after logging infrastructure has already been destroyed, causing crashes during static teardown.Known parity gap (separate repo, follow-up):
AIHttpResponseDecoder::handleDecodein thelib/modulessubmodule (lib/modules/azmon/AIHttpResponseDecoder.cpp:105,118) still setsctx->httpResponse = nullptr;on the aborted/failure paths — the same response-lifetime leak fixed here inlib/http/HttpResponseDecoder.cpp. It lives in a different repository (thelib/modulessubmodule), so it must be fixed there and pulled in via a submodule bump; it is out of scope for this PR.