afd.sys — feature-staged abort-state hardening check in AFD SAN request handlers
KB5078938
1. Overview
- Unpatched Binary:
afd_unpatched.sys - Patched Binary:
afd_patched.sys - Overall Similarity Score:
0.9742 - Diff Statistics: 1093 matched functions, 21 changed functions, 1072 identical functions, 0 unmatched functions in either direction.
- Verdict: The patch adds a feature-staged state check inside two AFD SAN (Winsock Sockets-Direct) request handlers. Under the endpoint spinlock that already guards the IRP-list insertion, when a staging feature flag is enabled the handler now refuses to queue an IRP onto an endpoint whose abort byte (
[endpoint+0x91]) is set, completing the IRP withSTATUS_CANCELLEDinstead. When the flag is disabled the prior queuing behavior is retained unchanged. This is a defense-in-depth hardening check, gated behind a Windows feature-staging flag with the old path kept in the else branch. It is not a demonstrable, reachable use-after-free.
2. Vulnerability Summary
Finding 1: AfdSanFastCompleteRequest abort-state hardening check
- Severity: Informational (no demonstrable reachable impact)
- Vulnerability Class: Defense-in-depth state check (hardening against a lifetime/teardown edge case). Not a demonstrated CWE-416/CWE-362.
- Affected Function:
AfdSanFastCompleteRequest(0x1C0077700unpatched /0x1C00787A0patched) — AFD SAN (Sockets-Direct) completion handler.
What actually changed:
In both builds the handler acquires the endpoint spinlock (KeAcquireInStackQueuedSpinLock on [endpoint+0x30]), sets the IRP cancel routine (AfdSanCancelRequest), checks IRP->Cancel ([IRP+0x44]), and, if the IRP is not cancelled, inserts the IRP into the endpoint pending list at [endpoint+0x70] — all while holding that spinlock. The insertion is therefore already serialized against other holders of the endpoint lock.
The patch inserts, still under the lock and only after the IRP->Cancel check, a call to EvaluateCurrentState(&g_Feature_757798200_60363861_FeatureDescriptorDetails). This is a Windows feature-staging (velocity) gate. If the feature returns disabled, control falls through to the exact same queuing code as the unpatched build. If the feature is enabled and [endpoint+0x91] is non-zero, the IRP is not queued; the lock is released and the IRP is completed with STATUS_CANCELLED (0xC0000120).
Why this is not a demonstrable UAF:
- The IRP-list insertion is not unsynchronized: it runs under [endpoint+0x30], the same lock the added check runs under.
- The added byte is an endpoint abort/teardown state indicator; the new code is a state check ("do not queue onto an endpoint flagged as aborting"), not the removal of a lock or a lifetime/refcount change.
- The change is gated behind a feature-staging flag with the previous behavior retained in the else branch, i.e. a staged rollout rather than an unconditionally delivered fix.
- No freed-endpoint dereference, controllable object, or exploit primitive is present in or reachable from these functions on the evidence in the two builds.
Call Chain (reachability):
AfdSanFastCompleteRequest is an AFD SAN (Sockets-Direct) completion handler. It is not reached by ordinary Winsock receive/send IOCTLs on a normal socket; it belongs to the SAN switch/helper request path. Its sibling AfdSanRedirectRequest (Finding 2) is invoked from AfdDispatch, AfdReceive, and AfdSend, but only when the endpoint is a SAN/switch endpoint. Reachability is therefore limited to the Sockets-Direct provider path.
Finding 2: AfdSanRedirectRequest abort-state hardening check
- Severity: Informational (no demonstrable reachable impact)
- Vulnerability Class: Defense-in-depth state check (same as Finding 1). Not a demonstrated CWE-416/CWE-362.
- Affected Function:
AfdSanRedirectRequest(0x1C00785E4unpatched /0x1C0079734patched) — AFD SAN (Sockets-Direct) redirect-request handler.
What actually changed:
Same pattern as Finding 1. In both builds the handler runs entirely under the endpoint spinlock ([endpoint+0x30]), validates the endpoint (magic 0x1AFD at [endpoint+0], type byte 4 at [endpoint+2], connection state at [endpoint+0x8C]), sets AfdSanCancelRequest, checks IRP->Cancel, and links the IRP into the endpoint list at [endpoint+0x70]. The patch adds, under the lock and after the IRP->Cancel check, the same EvaluateCurrentState(&g_Feature_757798200_60363861_FeatureDescriptorDetails) gate followed by cmp byte [endpoint+0x91], 0. If the feature is enabled and the endpoint is aborting, the IRP is completed with STATUS_CANCELLED instead of being queued; otherwise the original queuing path runs.
Call Chain (reachability):
1. NtDeviceIoControlFile on an AFD socket handle bound to a SAN/switch endpoint.
2. afd!AfdDispatch / afd!AfdReceive / afd!AfdSend (the three call sites of AfdSanRedirectRequest).
3. afd!AfdSanRedirectRequest, which links the IRP into the endpoint list under [endpoint+0x30].
3. Pseudocode Diff
AfdSanFastCompleteRequest (0x1C0077700 unpatched / 0x1C00787A0 patched)
// --- UNPATCHED ---
// (endpoint spinlock [endpoint+0x30] already held here)
Irp->CancelRoutine = AfdSanCancelRequest; // xchg [IRP+0x68]
if (Irp->Cancel == 0) { // [IRP+0x44]
InsertTailList(&Endpoint->PendingIrpList, // [endpoint+0x70], under lock
&Irp->ListEntry); // [IRP+0xA8]
}
// release [endpoint+0x30]
// --- PATCHED ---
// (endpoint spinlock [endpoint+0x30] already held here)
Irp->CancelRoutine = AfdSanCancelRequest;
if (Irp->Cancel == 0) {
// Added, still under the lock, after the Cancel check:
if (EvaluateCurrentState(&g_Feature_757798200_60363861_FeatureDescriptorDetails)
&& Endpoint->AbortByte /* [endpoint+0x91] */ != 0) {
// feature enabled AND endpoint aborting -> do not queue
Irp->IoStatus.Status = STATUS_CANCELLED; // 0xC0000120
// (release lock, complete IRP)
} else {
// feature disabled OR endpoint not aborting -> unchanged old path
InsertTailList(&Endpoint->PendingIrpList, &Irp->ListEntry);
}
}
4. Assembly Analysis
Function: AfdSanFastCompleteRequest — unpatched 0x1C0077700 vs patched 0x1C00787A0
Unpatched IRP queuing path (under the endpoint spinlock):
00000001C0077A15 lea rcx, [r15+30h] ; endpoint spinlock
00000001C0077A19 lea rdx, [rsp+...LockHandle]
00000001C0077A21 call cs:__imp_KeAcquireInStackQueuedSpinLock
00000001C0077A27 lea rax, AfdSanCancelRequest ; cancel routine
00000001C0077A2E xchg rax, [rdi+68h] ; set IRP->CancelRoutine
00000001C0077A32 cmp byte ptr [rdi+44h], 0 ; IRP->Cancel
00000001C0077A36 jnz short loc_1C0077A75 ; if cancelled -> cancel path
00000001C0077A38 add rdi, 0A8h ; &IRP list entry
00000001C0077A3F lea rax, [r15+70h] ; endpoint IRP list head
00000001C0077A53 mov [rdi], rcx ; InsertTailList (under lock)
00000001C0077A69 call cs:__imp_KeReleaseInStackQueuedSpinLock
Patched IRP queuing path (same lock, added feature-gated abort check):
00000001C0078AC8 lea rcx, [r15+30h] ; endpoint spinlock
00000001C0078AD4 call cs:__imp_KeAcquireInStackQueuedSpinLock
00000001C0078ADA lea rax, AfdSanCancelRequest
00000001C0078AE1 xchg rax, [rsi+68h] ; set IRP->CancelRoutine
00000001C0078AE5 cmp byte ptr [rsi+44h], 0 ; IRP->Cancel
00000001C0078AE9 jnz short loc_1C0078B42 ; if cancelled -> cancel path
00000001C0078AEB lea rcx, g_Feature_757798200_60363861_FeatureDescriptorDetails
00000001C0078AF2 call EvaluateCurrentState ; WIL feature-staging gate
00000001C0078AF7 test eax, eax
00000001C0078AF9 jz short loc_1C0078B05 ; feature OFF -> old queuing path
00000001C0078AFB cmp byte ptr [r15+91h], 0 ; endpoint abort state
00000001C0078B03 jnz short loc_1C0078B42 ; if aborting -> STATUS_CANCELLED
00000001C0078B05 add rsi, 0A8h ; queue (under lock)
00000001C0078B36 call cs:__imp_KeReleaseInStackQueuedSpinLock
Function: AfdSanRedirectRequest — unpatched 0x1C00785E4 vs patched 0x1C0079734
Unpatched (IRP linked into list under the lock, no abort-byte check):
00000001C00786A4 lea rax, AfdSanCancelRequest
00000001C00786AB xchg rax, [rbx+68h] ; set IRP->CancelRoutine
00000001C00786AF cmp byte ptr [rbx+44h], 0 ; IRP->Cancel
00000001C00786B3 jz short loc_1C00786F2 ; not cancelled -> queue
00000001C00786F2 ; ... link IRP into [rsi+70h] under spinlock, then release
Patched (adds the feature-gated abort-byte check before queuing):
00000001C00797F4 lea rax, AfdSanCancelRequest
00000001C00797FB xchg rax, [rdi+68h] ; set IRP->CancelRoutine
00000001C00797FF cmp byte ptr [rdi+44h], 0 ; IRP->Cancel
00000001C0079803 jnz loc_1C00798AA ; if cancelled -> cancel path
00000001C0079809 lea rcx, g_Feature_757798200_60363861_FeatureDescriptorDetails
00000001C0079810 call EvaluateCurrentState ; WIL feature-staging gate
00000001C0079815 test eax, eax
00000001C0079817 jz short loc_1C0079826 ; feature OFF -> old queuing path
00000001C0079819 cmp byte ptr [rsi+91h], 0 ; endpoint abort state
00000001C0079820 jnz loc_1C00798AA ; if aborting -> STATUS_CANCELLED
00000001C0079826 ; ... link IRP into [rsi+70h] under spinlock, then release
5. Trigger Conditions
No exploitable condition is demonstrable from the two builds. The changed code path is the AFD SAN (Sockets-Direct) request path, reached only for SAN/switch endpoints. Both builds perform the IRP-list insertion under the endpoint spinlock ([endpoint+0x30]); the patch only adds, under that same lock and behind a feature-staging flag, a refusal to queue onto an endpoint whose abort byte ([endpoint+0x91]) is already set. There is no removed lock, no lifetime/refcount change, and no reachable freed-object dereference in these functions.
6. Exploit Primitive & Development Notes
No exploit primitive is present. The added instructions are a state check performed while the endpoint spinlock is held; they neither create nor expose a memory-corruption primitive. Any claim of a kernel read/write primitive, pool grooming, or protection bypass would be speculation unsupported by the binaries and is therefore not made.
7. Debugger PoC Playbook
There is no vulnerability to reproduce. For inspection of the changed behavior only:
- bp afd!AfdSanFastCompleteRequest and bp afd!AfdSanRedirectRequest.
- In the patched build, single-step to the EvaluateCurrentState(&g_Feature_757798200_60363861_FeatureDescriptorDetails) call and the following cmp byte [endpoint+0x91], 0. The endpoint pointer is in r15 (AfdSanFastCompleteRequest) / rsi (AfdSanRedirectRequest); the endpoint spinlock is at [endpoint+0x30] and is held across the check and the list insertion.
- In the unpatched build the same insertion runs under the same lock without the feature gate or the +0x91 check.
8. Changed Functions — Full Triage
AfdSanFastCompleteRequest(0x1C0077700/0x1C00787A0) (Hardening, feature-staged): Adds, under the existing endpoint spinlock, aEvaluateCurrentState(&g_Feature_757798200_60363861_FeatureDescriptorDetails)gate pluscmp byte [endpoint+0x91], 0; when enabled and aborting, the IRP is completedSTATUS_CANCELLEDinstead of queued. Old path retained when the feature is off.AfdSanCancelRequestis the same cancel routine in both builds (relocated0x1C0076090->0x1C0077190). TheAccessMode && Length!=0guard before the buffer poke is present identically in both builds (register renaming only).AfdSanRedirectRequest(0x1C00785E4/0x1C0079734) (Hardening, feature-staged): Same feature-gated[endpoint+0x91]abort-state check under the endpoint spinlock.sub_1C003B310/sub_1C003C3F0(Behavioral): Connection endpoint creation refactored into a feature-flagged dispatch between two IoCreateFile-based creation paths. Servicing refactor; no security-relevant logic change.sub_1C0058000/sub_1C00592B0(Behavioral): Connection endpoint pool management (SLIST reuse). Refactoring of the connection-creation call and register/variable reassignments.sub_1C0046CD4/sub_1C007DA4(Behavioral): IRP completion helper updated for the connection-creation refactoring.sub_1C007508C/sub_1C007608C(Behavioral): Inline endpoint initialization extracted into helpersub_1c0079474; feature-flag address rebased.sub_1C0076FF0/sub_1C00780E0(Behavioral): Same endpoint-initialization extraction; lock/reference helpers rebased.sub_1C0046EC0/sub_1C0047FD0(Behavioral): Large AFD connect/bind IOCTL handler. Rebasing and helper extraction.sub_1C0046010/sub_1C0047000(Behavioral): Large dispatch handler. Address rebasing.- Cosmetic/False-Positive Matches:
sub_1C0052810,sub_1C0046CA0,sub_1C000CE50,sub_1C00379D0,sub_1C005988A,sub_1C00761A0,sub_1C0075770,sub_1C0040EF0,sub_1C0033AD0,sub_1C00156F4, andsub_1C0047F50are either different functions matched by the diff due to heavy restructuring, or minor decompiler artifacts (__noreturnstubs).
9. Unmatched Functions
No functions were uniquely added or removed. The patch relies on injecting the feature-staged state check into the two existing SAN handlers and on refactoring connection-object initialization.
10. Confidence & Caveats
- Confidence Level: High that the described diff is exactly what the two builds contain; the queuing already runs under the endpoint spinlock in both builds, and the added check is gated behind
g_Feature_757798200_60363861with the prior behavior retained when disabled. [endpoint+0x91]is treated as an abort/teardown state byte on the AFD endpoint; the added code refuses to queue onto an endpoint with that byte set. This is inferred from the patch context and the surrounding state validation (magic0x1AFD, connection state at[endpoint+0x8C]).- Severity rationale: Rated Informational because the surrounding IRP-list work is already lock-protected, the change is a staged (feature-gated) hardening state check with the old path retained, the affected code is the niche AFD SAN (Sockets-Direct) path, and no reachable, demonstrable use-after-free or exploit primitive is present in the binaries.
DONE