Fix WinHttpRequest destructor hang when the request context is never bound - #7353
Draft
Dan Cristoloveanu (dcristoloveanu) wants to merge 1 commit into
Draft
Conversation
…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
|
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. |
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.Pull Request Checklist