Describe the bug
Title
NULL-deref (0xc0000005) in QuicCryptoProcessTlsCompletion — unguarded Connection->SourceCids.Next walk in the HANDSHAKE_COMPLETE path races connection close during deferred custom certificate validation
Summary
Under high connect/teardown churn, msquic.dll takes an access violation reading a NULL Connection->SourceCids.Next in the handshake-complete branch of QuicCryptoProcessTlsCompletion. The faulting op is a deferred custom-certificate-validation completion (QuicCryptoCustomCertValidationComplete) draining on the connection worker at the same time the connection is being closed/torn down (MsQuicConnectionClose, driven here by .NET's System.Net.Quic). The teardown has already unregistered/freed the connection's source CIDs, so when the handshake-complete code retires the initial source CID it walks an empty list with no NULL guard → crash.
This is a robustness/ordering bug: QuicCryptoProcessTlsCompletion assumes Connection->SourceCids.Next is non-NULL while a concurrent close path can empty that list.
Environment
- msquic: 2.4.18 (Schannel TLS provider), x64 — the build bundled with .NET 10.0.9
- module hash
{5065a15a-c3ca-f332-46c4-f48e5f21cdd4}
- OS: Windows 11 Pro 10.0.26200
- Host: .NET 10.0.9 (
coreclr 10.0.926.27113), System.Net.Quic
- TLS: Schannel, custom/deferred certificate validation in use — the application sets a
RemoteCertificateValidationCallback (SPKI-fingerprint pinning), which routes handshakes through msquic's custom certificate-validation-completion path.
Faulting instruction & stack (WinDbg !analyze -v / .ecxr)
Exception: c0000005 (Access violation, read)
Faulting IP:
msquic!QuicCryptoProcessTlsCompletion+0x391:
00007ff8`67d59be5 488b01 mov rax,qword ptr [rcx] ds:00000000`00000000=????????????????
FAILURE_BUCKET_ID: INVALID_POINTER_READ_c0000005_msquic.dll!QuicCryptoProcessTlsCompletion
FAULTING_SOURCE_FILE: C:\__w\1\s\msquic\src\core\crypto.c
STACK_TEXT (faulting thread — msquic connection worker):
msquic!QuicCryptoProcessTlsCompletion+0x391
msquic!QuicCryptoProcessDataComplete+0x60
msquic!QuicCryptoCustomCertValidationComplete+0x80
msquic!QuicConnProcessApiOperation+0x26d
(QuicConnDrainOperations -> QuicWorkerProcessConnection)
Root cause (disassembly of +0x391)
mov rcx,[rbx+4F0h] ; rcx = Connection->SourceCids.Next -> NULL
mov rax,[rcx] ; rax = SourceCids.Next->Next <-- AV here (rcx = 0)
lea r14,[rax-18h] ; CONTAINING_RECORD(entry, QUIC_CID_HASH_ENTRY, offset 0x18)
mov rax,[rax]
mov [rcx],rax ; SourceCids.Next->Next = SourceCids.Next->Next->Next (unlink + free initial CID)
This is the HANDSHAKE_COMPLETE cleanup that retires the initial source connection ID (after
QuicCryptoHandshakeConfirmed + QuicSendSetSendFlag(HANDSHAKE_DONE)), i.e. the source line of the form:
Connection->SourceCids.Next->Next = Connection->SourceCids.Next->Next->Next;
Connection->SourceCids.Next is dereferenced with no NULL check. It is NULL because a concurrent connection
close/teardown (MsQuicConnectionClose) has already unregistered and freed the connection's source CIDs. The
deferred QuicCryptoCustomCertValidationComplete then resumes handshake finalization on that connection and walks
the now-empty list.
How it is triggered from .NET (System.Net.Quic)
The racing MsQuicConnectionClose originates inside the .NET runtime, not application code. Walking the managed
async continuation chain in the crash dump shows:
QuicConnection.DisposeAsync (running on a thread-pool thread, blocked in MsQuicConnectionClose /
WaitForSingleObjectEx on the faulting worker thread's handle)
awaited by
QuicListener.StartConnectionHandshake (continuation root)
i.e. an inbound handshake completes, but the QuicListener is being disposed (its _disposeCts fires),
so StartConnectionHandshake disposes the just-finished QuicConnection instead of enqueuing it for
AcceptConnectionAsync. That QuicConnection.DisposeAsync → MsQuicConnectionClose races the msquic worker
still draining QuicCryptoCustomCertValidationComplete for the same connection. (Confirmed same-connection:
the disposing thread's MsQuicConnectionClose is blocked in WaitForSingleObjectEx waiting on the faulting
worker thread's TID.)
Reproduction conditions
Not deterministic — load/timing-triggered. Reproduces under a test suite that rapidly creates and tears down many
QuicListeners / connections concurrently (per-test-case mesh spin-up + teardown, cross-dial collisions), with:
- a custom
RemoteCertificateValidationCallback on the accepting side (forces deferred cert validation), and
QuicListener.DisposeAsync being called while an inbound handshake is mid-completion.
The window is exactly: connection close/teardown vs. the deferred cert-validation-completion op on the same
connection, at handshake-completion time. Higher connect/handshake churn widens exposure.
Suggested fix
Guard the source-CID walk in the HANDSHAKE_COMPLETE branch of QuicCryptoProcessTlsCompletion against an empty
SourceCids list (NULL Connection->SourceCids.Next), and/or ensure a connection whose close/teardown has begun
does not process a queued QuicCryptoCustomCertValidationComplete that assumes source CIDs are still present.
Affected OS
Additional OS information
Edition Windows 11 Pro
Version 25H2
Installed on 8/11/2025
OS build 26200.8737
Experience Windows Feature Experience Pack 1000.26100.334.0
MsQuic version
msquic: 2.4.18 (Schannel TLS provider), x64 — the build bundled with .NET 10.0.9
- module hash
{5065a15a-c3ca-f332-46c4-f48e5f21cdd4}
Steps taken to reproduce bug
Not deterministic — load/timing-triggered. Reproduces under a test suite that rapidly creates and tears down many
QuicListeners / connections concurrently (per-test-case mesh spin-up + teardown, cross-dial collisions), with:
- a custom
RemoteCertificateValidationCallback on the accepting side (forces deferred cert validation), and
QuicListener.DisposeAsync being called while an inbound handshake is mid-completion.
The window is exactly: connection close/teardown vs. the deferred cert-validation-completion op on the same
connection, at handshake-completion time. Higher connect/handshake churn widens exposure.
Expected behavior
No crash
Actual outcome
NULL-deref (0xc0000005) in `QuicCryptoProcessTlsCompletion
Additional details
No response
Describe the bug
Title
NULL-deref (0xc0000005) in
QuicCryptoProcessTlsCompletion— unguardedConnection->SourceCids.Nextwalk in the HANDSHAKE_COMPLETE path races connection close during deferred custom certificate validationSummary
Under high connect/teardown churn,
msquic.dlltakes an access violation reading a NULLConnection->SourceCids.Nextin the handshake-complete branch ofQuicCryptoProcessTlsCompletion. The faulting op is a deferred custom-certificate-validation completion (QuicCryptoCustomCertValidationComplete) draining on the connection worker at the same time the connection is being closed/torn down (MsQuicConnectionClose, driven here by .NET'sSystem.Net.Quic). The teardown has already unregistered/freed the connection's source CIDs, so when the handshake-complete code retires the initial source CID it walks an empty list with no NULL guard → crash.This is a robustness/ordering bug:
QuicCryptoProcessTlsCompletionassumesConnection->SourceCids.Nextis non-NULL while a concurrent close path can empty that list.Environment
{5065a15a-c3ca-f332-46c4-f48e5f21cdd4}coreclr10.0.926.27113),System.Net.QuicRemoteCertificateValidationCallback(SPKI-fingerprint pinning), which routes handshakes through msquic's custom certificate-validation-completion path.Faulting instruction & stack (WinDbg
!analyze -v/.ecxr)Root cause (disassembly of
+0x391)This is the HANDSHAKE_COMPLETE cleanup that retires the initial source connection ID (after
QuicCryptoHandshakeConfirmed+QuicSendSetSendFlag(HANDSHAKE_DONE)), i.e. the source line of the form:Connection->SourceCids.Nextis dereferenced with no NULL check. It is NULL because a concurrent connectionclose/teardown (
MsQuicConnectionClose) has already unregistered and freed the connection's source CIDs. Thedeferred
QuicCryptoCustomCertValidationCompletethen resumes handshake finalization on that connection and walksthe now-empty list.
How it is triggered from .NET (
System.Net.Quic)The racing
MsQuicConnectionCloseoriginates inside the .NET runtime, not application code. Walking the managedasync continuation chain in the crash dump shows:
i.e. an inbound handshake completes, but the
QuicListeneris being disposed (its_disposeCtsfires),so
StartConnectionHandshakedisposes the just-finishedQuicConnectioninstead of enqueuing it forAcceptConnectionAsync. ThatQuicConnection.DisposeAsync→MsQuicConnectionCloseraces the msquic workerstill draining
QuicCryptoCustomCertValidationCompletefor the same connection. (Confirmed same-connection:the disposing thread's
MsQuicConnectionCloseis blocked inWaitForSingleObjectExwaiting on the faultingworker thread's TID.)
Reproduction conditions
Not deterministic — load/timing-triggered. Reproduces under a test suite that rapidly creates and tears down many
QuicListeners / connections concurrently (per-test-case mesh spin-up + teardown, cross-dial collisions), with:RemoteCertificateValidationCallbackon the accepting side (forces deferred cert validation), andQuicListener.DisposeAsyncbeing called while an inbound handshake is mid-completion.The window is exactly: connection close/teardown vs. the deferred cert-validation-completion op on the same
connection, at handshake-completion time. Higher connect/handshake churn widens exposure.
Suggested fix
Guard the source-CID walk in the HANDSHAKE_COMPLETE branch of
QuicCryptoProcessTlsCompletionagainst an emptySourceCidslist (NULLConnection->SourceCids.Next), and/or ensure a connection whose close/teardown has begundoes not process a queued
QuicCryptoCustomCertValidationCompletethat assumes source CIDs are still present.Affected OS
Additional OS information
Edition Windows 11 Pro
Version 25H2
Installed on 8/11/2025
OS build 26200.8737
Experience Windows Feature Experience Pack 1000.26100.334.0
MsQuic version
msquic: 2.4.18 (Schannel TLS provider), x64 — the build bundled with .NET 10.0.9
{5065a15a-c3ca-f332-46c4-f48e5f21cdd4}Steps taken to reproduce bug
Not deterministic — load/timing-triggered. Reproduces under a test suite that rapidly creates and tears down many
QuicListeners / connections concurrently (per-test-case mesh spin-up + teardown, cross-dial collisions), with:RemoteCertificateValidationCallbackon the accepting side (forces deferred cert validation), andQuicListener.DisposeAsyncbeing called while an inbound handshake is mid-completion.The window is exactly: connection close/teardown vs. the deferred cert-validation-completion op on the same
connection, at handshake-completion time. Higher connect/handshake churn widens exposure.
Expected behavior
No crash
Actual outcome
NULL-deref (0xc0000005) in `QuicCryptoProcessTlsCompletion
Additional details
No response