Skip to content

NULL-deref (0xc0000005) in QuicCryptoProcessTlsCompletion — unguarded Connection->SourceCids.Next walk in the HANDSHAKE_COMPLETE path races connection close during deferred custom certificate validation #6138

Description

@bradpedersen

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.DisposeAsyncMsQuicConnectionClose 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

  • Windows
  • Linux
  • macOS
  • Other (specify below)

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Fields

No fields configured for Bug.

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions