Fix WinHttpRequest destructor hang when the request context is never bound - #7353
Merged
Anton Kolesnyk (antkmsft) merged 1 commit intoSep 18, 2026
Conversation
|
Azure Pipelines: Successfully started running 2 pipeline(s). 8 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Dan Cristoloveanu (dcristoloveanu)
force-pushed
the
dcristo/winhttp-request-context-hang
branch
from
August 20, 2026 05:52
d9e7d0c to
8623ac0
Compare
Dan Cristoloveanu (dcristoloveanu)
marked this pull request as ready for review
August 20, 2026 06:06
Dan Cristoloveanu (dcristoloveanu)
requested a review
from Rick Winter (RickWinter)
as a code owner
August 20, 2026 06:06
|
Azure Pipelines: Successfully started running 2 pipeline(s). 8 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Copilot started reviewing on behalf of
Dan Cristoloveanu (dcristoloveanu)
August 20, 2026 06:06
View session
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a WinHTTP adapter teardown deadlock in azure-core where WinHttpRequest::~WinHttpRequest() could block forever if the request is destroyed before WinHttpSendRequest() associates the callback context with the request handle. The change ensures the destructor’s existing “wait for WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING” barrier is always satisfiable, and adds a unit test to prevent regressions.
Changes:
- Bind the WinHTTP context value in
WinHttpRequestconstruction viaWinHttpSetOption(WINHTTP_OPTION_CONTEXT_VALUE, ...)before registering the status callback. - Add a WinHTTP-focused unit test that deterministically throws during request setup (before
WinHttpSendRequest) and asserts the call returns (does not hang). - Wire the new test file into the azure-core unit test target.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| sdk/core/azure-core/src/http/winhttp/win_http_transport.cpp | Sets WINHTTP_OPTION_CONTEXT_VALUE during request construction so HANDLE_CLOSING callbacks always have a valid context and the destructor barrier can complete. |
| sdk/core/azure-core/test/ut/win_http_transport_test.cpp | Adds WinHttpTransport.RequestThatFailsDuringSetupDoesNotHang regression test covering the pre-WinHttpSendRequest failure window. |
| sdk/core/azure-core/test/ut/CMakeLists.txt | Includes the new WinHTTP transport unit test source in the test executable build. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
…bound ~WinHttpRequest closes the request handle and then waits for WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING so that no WinHTTP worker thread can dereference the WinHttpAction after it is freed. WinHttpAction::StatusCallback discards every notification that arrives with dwContext == 0, and the context is associated with the handle only by WinHttpSendRequest, while the status callback is registered at the end of the constructor. A request destroyed between those two points therefore waits for a notification that is discarded, on a default-constructed Azure::Core::Context that is never cancelled, and the calling thread is lost for the lifetime of the process. Bind the context with WinHttpSetOption(WINHTTP_OPTION_CONTEXT_VALUE) in the constructor so HANDLE_CLOSING is always delivered and the existing barrier always completes. WinHttpSendRequest continues to pass the same value, which is idempotent. Adding a timeout to the destructor's wait would not be a valid fix: abandoning the barrier lets a WinHTTP worker thread invoke the callback after the objects are destroyed, turning a hang into a use-after-free. Adds a regression test that constructs a request whose body stream throws from Length(), which enters the window without requiring any network traffic. Fixes Azure#7352
Dan Cristoloveanu (dcristoloveanu)
force-pushed
the
dcristo/winhttp-request-context-hang
branch
from
August 20, 2026 23:00
8623ac0 to
f677594
Compare
Anton Kolesnyk (antkmsft)
approved these changes
Sep 18, 2026
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.
Fixes #7352
Problem
WinHttpRequest::~WinHttpRequest()closes the request handle and then waits forWINHTTP_CALLBACK_STATUS_HANDLE_CLOSINGbefore returning. That barrier is deliberate andnecessary: WinHTTP is used in async mode (
WINHTTP_FLAG_ASYNC), the callback context is a rawpointer to the
WinHttpActionowned by the request, and the callback also dereferencesm_httpRequest. Returning beforeHANDLE_CLOSINGwould let a WinHTTP worker thread touch freedmemory.
However,
WinHttpAction::StatusCallback()discards every notification that arrives withdwContext == 0, and the context is bound to the handle only byWinHttpSendRequest()— whilethe status callback is registered much earlier, at the end of the constructor. Any request
destroyed in between (for example when an exception is thrown while preparing headers or querying
the body stream length) therefore closes its handle, receives
HANDLE_CLOSINGwith no context, hasit dropped, and blocks in
WaitForActionforever. The wait uses a default-constructedAzure::Core::Context, soThrowIfCancelled()never fires and the poll loop spins indefinitely —the thread is lost for the lifetime of the process.
CompleteActionWithError()intentionally does not signal while waiting forHANDLE_CLOSING, sothe
REQUEST_ERROR/ERROR_WINHTTP_OPERATION_CANCELLEDnotification that WinHTTP raises whenclosing a handle with a pending operation does not release the waiter either.
This is the hang that remains in the window #6637 addressed for the FailFast case; that PR's own
comment already observes that "the
initiateActioncall is a call toWinHttpSendRequestwhichestablishes the SendContext".
Fix
Bind the context to the request handle in the constructor with
WinHttpSetOption(WINHTTP_OPTION_CONTEXT_VALUE, ...), immediately before registering the statuscallback.
HANDLE_CLOSINGis then always delivered with a valid context, so the destructor'sbarrier always completes.
WinHttpSendRequest()continues to pass the same value, which isidempotent.
The barrier itself is unchanged — the fix makes it satisfiable rather than removing it.
Alternatives considered
reason the bug exists (with no context every callback is already dropped, so there is nothing to
synchronize against), but smaller in scope and leaves
HANDLE_CLOSINGunobservable.thread to invoke the callback after
WinHttpAction/WinHttpRequestare destroyed, converting ahang into a use-after-free.
CompleteActionWithError()during close. Rejected for the same reason:it would release the waiter before
HANDLE_CLOSING, which is precisely what the barrier prevents.Test
sdk/core/azure-core/test/ut/win_http_transport_test.cppaddsWinHttpTransport.RequestThatFailsDuringSetupDoesNotHang.SendRequest()callsrequest.GetBodyStream()->Length()beforeWinHttpSendRequest(), andBodyStream::Length()is notnoexcept, so a body stream that throws fromLength()enters thewindow deterministically. The test needs no network:
WinHttpOpen(),WinHttpConnect()andWinHttpOpenRequest()only allocate handles, and the request fails before anything is sent.The work runs on a detached thread guarded by a 30 s future wait, so a regression fails the test
instead of hanging the CI run (a blocked destructor cannot be joined).
Verified against the vendored 1.16.3 sources in a downstream consumer: an equivalent test hangs
and fails on the 30 s timeout without the product change, and passes in 0.76 s with it. A leak
checker also reported the abandoned
WinHttpRequestfromWinHttpTransportImpl::CreateRequestHandle()before the fix, and no leaks after.The full CI matrix is green on this PR, including the
Win2022_Win32Api_debug_tests_winhttp_x64and
x86legs that build and run the new test.Pull Request Checklist
Authored with GitHub Copilot CLI — session ID
6afdad25-6ece-4e65-991c-aa8d88d138fa