Fix WinHttpProvider caching a null WinHTTP session permanently - #1010
Merged
Andy McCalib (amccalib) merged 1 commit intoAug 4, 2026
Merged
Conversation
GetHSession() could return a null HINTERNET as a *successful* Result, which was then cached in m_hSessions forever. Result<T>'s single-argument constructor is the "construct successful result" overload, so callers' RETURN_IF_FAILED(result.hr) checks passed and a null session was handed to WinHttpConnection::Initialize. Because m_hSessions is keyed only by securityProtocolFlags and is cleared only by Suspend() (which never runs on desktop platforms), one transient failure poisoned the provider for the lifetime of the process. Every later request short-circuited on the cached null and failed silently, emitting no trace output at all. Three related fixes, all in GetHSession(): 1. When the fallback WinHttpOpen(WINHTTP_FLAG_ASYNC) returns null, return the error instead of warning and falling through to cache the null. 2. Only inspect GetLastError() after WinHttpOpen has actually failed. It was read unconditionally, so a stale ERROR_INVALID_PARAMETER from an unrelated call could cause a second session to be opened over a successfully opened first one, leaking the first handle. 3. Map a zero GetLastError() to E_FAIL on the initial open failure path. HRESULT_FROM_WIN32(0) is S_OK, which would construct a "successful" Result with no payload via the Result(HRESULT) overload, making Payload() assert and ExtractPayload() undefined. Verified with a standalone Win32 harness that injects WinHttpOpen / WinHttpSetOption failures via IAT patching, so the library builds unmodified. Before: after a simulated outage the provider failed every subsequent request against a healthy network. After: it recovers on the next request. Note that WinHttpSetOption(WINHTTP_OPTION_SECURE_PROTOCOLS) fails with ERROR_ACCESS_DENIED on current Windows whenever the session was opened with WINHTTP_FLAG_SECURE_DEFAULTS and the requested mask includes TLS 1.0/1.1 (which the Win32 default mask does), so the close-and-reopen fallback is the normal path for HTTPS sessions rather than a rare edge case. That reopen also drops SECURE_DEFAULTS; addressing that is left to a separate change.
Andy McCalib (amccalib)
force-pushed
the
user/amccalib/fix-winhttp-null-session-cache
branch
from
August 4, 2026 20:14
cbe1cb0 to
d94b749
Compare
Jason Sandlin (jasonsandlin)
approved these changes
Aug 4, 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.
Problem
WinHttpProvider::GetHSession()could return a nullHINTERNETas a successfulResult, and then cache it permanently.Result<T>'s single-argument constructor is documented as "Construct successful result" (Source/Common/Result.h:13-14), soreturn hSession;with a null handle produceshr == S_OK. Both call sites check onlyRETURN_IF_FAILED(getHSessionResult.hr)and never null-check the payload, so the null was passed straight toWinHttpConnection::Initialize.The cache makes it permanent:
m_hSessionsis keyed only bysecurityProtocolFlags.Suspend(), which never runs on desktop platforms.m_hSessions.find()early return.So a single transient failure poisons the provider for the lifetime of the process, and does so silently — the short-circuit path emits no trace output whatsoever.
Why this is reachable in practice
WinHttpSetOption(WINHTTP_OPTION_SECURE_PROTOCOLS)fails withERROR_ACCESS_DENIEDwhenever the session was opened withWINHTTP_FLAG_SECURE_DEFAULTSand the requested mask includes TLS 1.0/1.1 — and the Win32 default mask (winhttp_provider.cpp:306-309) does exactly that.SECURE_DEFAULTSrequires TLS 1.2+ and rejects attempts to re-enable older protocols.I observed this on stock Windows 11 with no fault injection at all. That means the close-and-reopen fallback is the normal path for every HTTPS session, not a rare edge case — so only the second
WinHttpOpenhas to fail to poison the cache.Changes
All three are in
GetHSession()and are the same defect family — a failed or null session being treated as success:WinHttpOpen(WINHTTP_FLAG_ASYNC)returns null, return the error instead of warning and falling through to cache the null. This is the actual fix for the bug above.GetLastError()after a failure. It was read unconditionally afterWinHttpOpen; its value is undefined after success. A staleERROR_INVALID_PARAMETERcould open a second session over a successful first one and leak the first handle.GetLastError()toE_FAILon the initial-open failure path.HRESULT_FROM_WIN32(0)isS_OK, which selects theResult(HRESULT)"failed result (no payload)" overload while reporting success —Payload()then asserts andExtractPayload()is undefined.Verification
A standalone Win32 harness injects
WinHttpOpen/WinHttpSetOptionfailures by patching the harness's own import address table, so libHttpClient itself compiles and runs completely unmodified — no test hooks or#ifdefs in the library.Before the fix, phase 2 produced zero trace output: the cached null short-circuits before any logging can happen. That silent-failure signature is what makes this so hard to diagnose from customer logs.
Built and compiles clean in both
libHttpClient.143.Win32.CandlibHttpClient.143.GDK.C.Deliberately out of scope
Two adjacent issues found while investigating, left for separate changes to keep this reviewable:
WINHTTP_FLAG_SECURE_DEFAULTSand re-permits TLS 1.0/1.1, and also loses the "no TLS fallback" behavior. The better fix is to skipSECURE_PROTOCOLSentirely when the session already opened withSECURE_DEFAULTS.m_hSessionsis keyed only bysecurityProtocolFlags, but session setup also depends onisHttps— and on Win32GetSecurityInformation()returns the same constant mask for both schemes. An HTTP request that populates the entry first will hand a later HTTPS request a session that never had secure protocols configured, making behavior order-dependent.