Fix cleanup/request-kickoff races (#1018, #1019) - #1020
Open
Jason Sandlin (jasonsandlin) wants to merge 1 commit into
Open
Fix cleanup/request-kickoff races (#1018, #1019)#1020Jason Sandlin (jasonsandlin) wants to merge 1 commit into
Jason Sandlin (jasonsandlin) wants to merge 1 commit into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes two races between
HCCleanupAsyncand HTTP request kickoff, reported in #1018 and #1019. Both are crashes in the field: a null-handle dereference during cancellation, and a use-after-free onNetworkState. They're related to #999 but distinct, and both persist after #1000.Each fix is accompanied by a deterministic unit test that reproduces the exact reported interleaving. Both tests were confirmed to fail before the corresponding fix and pass after.
Issue #1018 — cancel races perform startup
NetworkState::HttpCallPerformAsyncProviderinserts a request intom_activeHttpRequestsbefore callingHC_CALL::PerformAsync. Cleanup can therefore snapshot and cancel a request whoseHC_CALL::PerfomAsyncProvider::Beginis still running:HttpCallPerformAsyncProvider::Begininserts the requestHC_CALL::PerfomAsyncProvider::BeginCleanupAsyncProvider::Beginsnapshots and callsXAsyncCancelPerfomAsyncProvider::CancelcallsXTaskQueueTerminate(context->workQueue, ...)If step 4 lands before
BegincreatesworkQueue, the handle isnullptr.XTaskQueueTerminatepasses it toGetQueue, which dereferenceshandle->m_signature— an access violation.Fix: a
startupMutexonPerformContextorders publication of the work queues against the cancel. If a cancel arrives first it recordscancelRequestedand returns instead of terminating a null handle;Beginobserves the flag once it publishes the queues and completes the perform as canceled (E_ABORT) — the same outcome a later cancel produces.Issue #1019 — refused request touches freed
NetworkStateThe
m_cleanupStartedguard added in #1000 correctly refuses a perform that arrives after cleanup began, but the refused request'sXAsyncOp::Cleanupstill dereferencedNetworkStateto takem_mutexand callerase().That op runs asynchronously (completion port, after the client callback), and a refused request was never tracked — so nothing holds a singleton reference on its behalf. By the time it runs, cleanup may already have destroyed the singleton and the
NetworkStateit owns, so the deref is a use-after-free.The existing
erase() != 0check prevented a spurious reschedule, but it could not prevent the dereference: it already required touching freed memory.Fix: an
admittedflag is set underm_mutexwhen the request is actually inserted. A request that was never admitted has no bookkeeping to undo and now touchesNetworkStatenot at all.The WebSocket path is already safe here —
WebSocketConnectAsyncProvider'sCleanupop only reclaims its context, andWebSocketConnectCompletenever runs for a refused connect. No change needed.Tests
New
Tests/UnitTests/Tests/CleanupRaceTests.cppwith one test per issue. Both force the reported interleaving deterministically rather than relying on stress, and both convert the crash into a normal assertion failure so a regression reports as a failed test rather than tearing down the test host.HC_UNITTEST_APIonly) that parks a perform inside the startup window, before the work queues exist, then runs cleanup. It asserts no fault and that cleanup and the canceled perform both actually complete — so the test can't pass by simply never reaching the window.HCMemSetFunctionshook. Freed libHttpClient allocations are quarantinedPAGE_NOACCESSinstead of returned to the CRT, so a dangling access faults at the exact instruction. Cleanup and the refused request run on separate manual queues so cleanup can be driven to completion — destroyingNetworkState— while the refused request's deferred cleanup is still pending. Setup assertions confirm the ordering actually occurred before the result is trusted.Verified red → green: with the two fixes reverted (test seam retained), both tests fail with the fault assertion; with them applied, both pass.
Validation
libHttpClient.UnitTest.TE, x64 Debug: 111/111 pass (109 pre-existing + 2 new), no regressionslhcdevice scenarios pass, including two new ones that overlap a request burst with cleanup at varying timing offsetsNotes for reviewers
NetworkState/HC_CALLcode, so the fixes apply to all platforms, not just Windows.HC_UNITTEST_API-guarded test hook inhttpcall.h/httpcall.cpp; it compiles out of shipping builds.Fixes #1018
Fixes #1019