1. Overview

Field Value
Unpatched binary afd_unpatched.sys
Patched binary afd_patched.sys
Overall similarity 0.9806
Matched functions 1228
Changed functions 16
Identical functions 1212
Unmatched (unpatched) 0
Unmatched (patched) 0

Verdict: The patch adds a new, feature-staged endpoint state byte at offset +0x91 in the AFD SAN endpoint structure. When an endpoint's accept has been cancelled/drained (AfdSanCancelAccept), the byte is set, and the accept/receive redirect handlers then complete an incoming request IRP with STATUS_CANCELLED instead of linking it onto the endpoint's pending-IRP list at +0x70. This closes a window where a request IRP could be (re)queued onto an endpoint whose accept processing has already been torn down — a dangling pending-IRP / use-after-free class condition. The change is entirely gated behind the WIL feature Feature_623580472__private_IsEnabledDeviceUsage, which exists only in the patched binary, so this is a staged rollout with the pre-patch behavior retained in the feature-off branch. A concrete, reachable memory-corruption primitive is not demonstrable from the two binaries; severity is therefore Low. A secondary, also feature-gated hardening tightens a TDI device-name blocklist from exact-match to substring-match.


2. Vulnerability Summary

Finding #1 — Low — Missing post-cancel state check in the AFD accept-redirect IRP handler

  • Function (unpatched): AfdSanRedirectRequest @ 0x1C0080198
  • Function (patched): AfdSanRedirectRequest @ 0x1C0082338
  • Class: Missing state validation guarding a dangling pending-IRP / use-after-free condition (CWE-416, as the defended class)
  • Entry point: NtDeviceIoControlFile(\Device\Afd, …) accept-side request handled by the AFD SAN dispatch

What actually changed. In both builds the handler runs under the endpoint spinlock (KeAcquireInStackQueuedSpinLock on [ep+0x30]), verifies the endpoint signature 0x1AFD ([ep+0x00]), state 4 ([ep+0x02]), and status 0x103/0xC0000016 ([ep+0x8c]), sets the IRP Information ([irp+0x78]) and CancelRoutine ([irp+0x68] = AfdSanCancelRequest), then checks IRP->Cancel ([irp+0x44]). If the IRP is not cancelled, it links the IRP into the endpoint's pending list at [ep+0x70] and returns STATUS_PENDING.

The patched build inserts one additional gate after the IRP->Cancel test: it calls Feature_623580472__private_IsEnabledDeviceUsage(); if the feature is off it queues exactly as before, and if the feature is on it also reads the new byte [ep+0x91]. If that byte is non-zero, the handler skips queuing and instead completes the IRP with STATUS_CANCELLED (0xC0000120).

What +0x91 means. The byte is new to the patched build (it is referenced nowhere in the unpatched binary). AfdSanInitEndpoint zero-initializes it (only when the feature is on), and AfdSanCancelAccept sets it to 1 when it cancels an endpoint's accept and drains the pending list. So [ep+0x91] != 0 marks "this endpoint's accept has been cancelled/drained; do not queue new request IRPs onto it." The guard prevents a request IRP from being linked onto the pending list of an endpoint whose accept teardown has already run — the condition that could otherwise leave an IRP referencing an endpoint that is being torn down.

Direction / staging. The change makes the patched build strictly more conservative (adds a guard). It is entirely feature-gated; with the feature off, the patched code path is identical to the unpatched one, and the pre-patch queuing behavior is retained in that branch.

Finding #2 — Low — Same guard on the AFD fast-complete (receive/accept) IRP handler

  • Function (unpatched): AfdSanFastCompleteRequest @ 0x1C007EF30
  • Function (patched): AfdSanFastCompleteRequest @ 0x1C0081030
  • Class: Same as Finding #1
  • Entry point: NtDeviceIoControlFile(\Device\Afd, …) receive/accept-side request handled by the AFD SAN dispatch

The same guard is added here. Under the endpoint spinlock, after setting the cancel routine and checking IRP->Cancel ([irp+0x44]), the patched build calls Feature_623580472__private_IsEnabledDeviceUsage() and, when enabled, checks [ep+0x91] (cmp byte [r15+0x91], 0 at 0x1C0081445) before linking the IRP into [ep+0x70]. If the byte is set, it releases the lock and takes the cancel path instead of queuing. The unpatched handler has neither the feature call nor the +0x91 check.

The same +0x91 read guard is also present in a third consumer, AfdSanAcquireContext (patched 0x1C007E830, at 0x1C007EE79 and 0x1C007EEB4), which the changed-function triage originally mislabeled (see §8).

Finding #3 — Low — TDI device-name blocklist tightened from exact-match to substring-match

  • Function (unpatched / patched): AfdIsValidTdiDeviceName @ 0x1C000896C (same address in both builds)
  • Class: Incomplete list of disallowed inputs (CWE-184)
  • Entry point: socket/transport creation path that validates a caller-supplied TDI device name against AfdTdiInvalidDeviceNames

The unpatched function compares the caller-supplied name against the one-entry blocklist AfdTdiInvalidDeviceNames using exact RtlEqualUnicodeString (case-insensitive). The patched function adds a Feature_2480880955__private_IsEnabledDeviceUsage() gate: when the feature is off it keeps the exact RtlEqualUnicodeString comparison; when on it uses RtlFindUnicodeSubstring(callerName, blockedName) instead, so a caller-supplied name that merely contains the blocked name is now rejected. This closes a bypass where the blocked device name is embedded inside a longer string. Impact is limited to reaching a name that should have been rejected; no memory-safety impact. Also feature-gated.


3. Pseudocode Diff

Finding #1 — Accept-redirect handler

// ===== UNPATCHED AfdSanRedirectRequest @ 0x1C0080198 =====
// (under endpoint spinlock; signature/state/status already checked)
Irp->Information   = 0;                       // [irp+0x78]
Irp->CancelRoutine = AfdSanCancelRequest;     // [irp+0x68]
if (!Irp->Cancel) {                           // [irp+0x44]  -- the ONLY gate
    // link IRP into endpoint pending list at [ep+0x70]
    return STATUS_PENDING;                     // 0x103
}
// else: pull cancel routine, complete with STATUS_CANCELLED

// ===== PATCHED AfdSanRedirectRequest @ 0x1C0082338 =====
Irp->Information   = 0;
Irp->CancelRoutine = AfdSanCancelRequest;
if (Irp->Cancel) goto cancel_path;            // [irp+0x44]
if (Feature_623580472__private_IsEnabledDeviceUsage()   // NEW feature gate
    && *(BYTE*)(ep + 0x91) != 0)               // NEW post-cancel state check
    goto cancel_path;                          // endpoint accept cancelled -> refuse
// else: link IRP into pending list at [ep+0x70], return STATUS_PENDING
cancel_path:
    Irp->IoStatus.Status = STATUS_CANCELLED;   // 0xC0000120
    IofCompleteRequest(Irp, AfdPriorityBoost);

The new +0x91 byte — producer and initializer

// ===== NEW AfdSanInitEndpoint @ 0x1C0082020 =====
// zero-inits endpoint fields; the +0x91 write is feature-gated
*(ep + 0x68) = 0;  *(DWORD*)(ep + 0x88) = 0;  *(DWORD*)(ep + 0x8c) = 0;
*(ep + 0x70) = *(ep + 0x78) = &ep[0x70];      // self-linked list anchor
*(ep + 0x50) = arg1;  *(ep + 0x58) = arg2;  *(ep + 0x60) = arg3;
*(DWORD*)(ep + 0x84) = 1;
*(BYTE*)(ep + 0x90) = 0;                        // unconditional
if (Feature_623580472__private_IsEnabledDeviceUsage())
    *(BYTE*)(ep + 0x91) = 0;                    // feature-gated
*(WORD*)(ep + 0x00) = 0x1AFD;
*(BYTE*)(ep + 0x48) = 1;

// ===== AfdSanCancelAccept sets the byte (patched @ 0x1C007F140) =====
// unpatched @ 0x1C007D130 has NO +0x91 write and does not drain [ep+0x70]
if (Feature_623580472__private_IsEnabledDeviceUsage()) {
    *(BYTE*)(ep + 0x91) = 1;                    // mark accept cancelled
    // then drain any IRPs already pending on [ep+0x70]
}

Finding #2 — Fast-complete handler

// ===== UNPATCHED AfdSanFastCompleteRequest @ 0x1C007EF30 =====
if (!Irp->Cancel) { /* link IRP into [ep+0x70] */ }

// ===== PATCHED AfdSanFastCompleteRequest @ 0x1C0081030 =====
if (Irp->Cancel) goto cancel_path;             // [irp+0x44]
if (Feature_623580472__private_IsEnabledDeviceUsage()
    && *(BYTE*)(ep + 0x91) != 0)               // NEW post-cancel state check
    goto cancel_path;
/* link IRP into [ep+0x70] */

Finding #3 — TDI device-name validation

// ===== UNPATCHED AfdIsValidTdiDeviceName @ 0x1C000896C =====
RtlInitUnicodeString(&blocked, AfdTdiInvalidDeviceNames[0]);
if (RtlEqualUnicodeString(&blocked, callerName, /*CaseInsensitive*/1))
    return FALSE;   // blocked (exact match only)

// ===== PATCHED AfdIsValidTdiDeviceName @ 0x1C000896C =====
RtlInitUnicodeString(&blocked, AfdTdiInvalidDeviceNames[0]);
if (Feature_2480880955__private_IsEnabledDeviceUsage())
    hit = RtlFindUnicodeSubstring(callerName, &blocked, 1) != NULL;  // substring
else
    hit = RtlEqualUnicodeString(&blocked, callerName, 1);            // exact
if (hit) return FALSE;   // blocked

4. Assembly Analysis

Finding #1 — AfdSanRedirectRequest (unpatched, 0x1C0080198)

The queuing path, as it exists in the unpatched binary:

0x1C00801D3  call    KeAcquireInStackQueuedSpinLock   ; lock [ep+0x30]
0x1C00801DF  mov     eax, 1AFDh
0x1C00801E4  cmp     [rsi], ax                        ; ep signature == 0x1AFD?
0x1C00801E7  jnz     loc_1C0080387
0x1C00801ED  cmp     byte ptr [rsi+2], 4              ; ep state == 4?
0x1C00801F1  jnz     loc_1C0080387
0x1C00801F7  mov     ebx, [rsi+8Ch]                   ; ep status
0x1C00801FD  cmp     ebx, 103h                        ; STATUS_PENDING?
0x1C0080203  jz      short loc_1C0080263
0x1C0080205  cmp     ebx, 0C0000016h                  ; STATUS_MORE_PROCESSING_REQUIRED?
0x1C008020B  jz      short loc_1C0080263
; ...
0x1C008026B  mov     [rdi+78h], rbx                   ; IRP->Information = 0
0x1C008026F  lea     rax, AfdSanCancelRequest
0x1C0080276  xchg    rax, [rdi+68h]                   ; IRP->CancelRoutine = AfdSanCancelRequest
0x1C008027A  cmp     byte ptr [rdi+44h], 0            ; IRP->Cancel?  (the ONLY gate)
0x1C008027E  lea     rcx, [rdi+0A8h]
0x1C0080285  jz      short loc_1C00802D6              ; not cancelled -> queue
; ... cancel path completes with STATUS_CANCELLED ...
0x1C00802D6  mov     rax, [rdi+0B8h]
0x1C00802DD  or      byte ptr [rax+3], 1
0x1C00802E1  lea     rax, [rsi+70h]                   ; &ep pending list
0x1C00802E5  mov     rdx, [rax+8]
0x1C00802E9  cmp     [rdx], rax                       ; list integrity
0x1C00802EC  jz      short loc_1C00802F5
0x1C00802EE  mov     ecx, 3
0x1C00802F3  int     29h                              ; RtlFailFast if list corrupt
0x1C00802F5  mov     [rcx], rax                       ; link IRP into pending list
0x1C00802F8  mov     [rcx+8], rdx
0x1C00802FC  mov     [rdx], rcx
0x1C00802FF  mov     [rax+8], rcx
0x1C0080308  call    KeReleaseInStackQueuedSpinLock
0x1C0080380  mov     eax, 103h                        ; return STATUS_PENDING

There is no read of [rsi+0x91] anywhere in this build — the offset does not exist in the unpatched code.

Patched side (the added gate), AfdSanRedirectRequest @ 0x1C0082338

0x1C008241A  cmp     byte ptr [rbx+44h], 0                       ; IRP->Cancel
0x1C008241E  jnz     loc_1C00824F5                               ; cancelled -> cancel path
0x1C0082424  call    Feature_623580472__private_IsEnabledDeviceUsage   ; NEW feature gate
0x1C0082429  test    eax, eax
0x1C008242B  jz      short loc_1C008243A                         ; feature off -> queue (legacy)
0x1C008242D  cmp     byte ptr [rsi+91h], 0                       ; NEW post-cancel state check
0x1C0082434  jnz     loc_1C00824F5                               ; byte set -> cancel path
0x1C008243A  ; ... link IRP into [rsi+0x70] pending list, return STATUS_PENDING ...
0x1C00824F5  ; cancel path: pull cancel routine, ...
0x1C008253E  mov     edi, 0C0000120h                             ; STATUS_CANCELLED
0x1C0082564  mov     [rbx+30h], edi
0x1C0082567  call    IofCompleteRequest

The producer — AfdSanCancelAccept @ 0x1C007F140 (patched)

0x1C007F1EE  call    Feature_623580472__private_IsEnabledDeviceUsage
0x1C007F1F3  test    eax, eax
0x1C007F1F5  jz      short loc_1C007F235
0x1C007F1F7  mov     byte ptr [rdi+91h], 1                       ; NEW: mark accept cancelled
0x1C007F1FE  lea     rcx, [rdi+70h]                              ; then drain the pending list
0x1C007F202  mov     rax, [rcx]
; ... unlink loop over [ep+0x70] entries ...

The unpatched AfdSanCancelAccept @ 0x1C007D130 neither writes [ep+0x91] nor drains [ep+0x70]; it unlinks only the single cancelling IRP and completes it with STATUS_CANCELLED.

Finding #3 — AfdIsValidTdiDeviceName @ 0x1C000896C

; --- unpatched ---
0x1C00089AE  call    RtlEqualUnicodeString                       ; exact match only
; --- patched ---
0x1C00089A3  call    Feature_2480880955__private_IsEnabledDeviceUsage
0x1C00089AD  jz      short loc_1C00089C8                         ; feature off -> exact match
0x1C00089B7  call    RtlFindUnicodeSubstring                     ; feature on -> substring match
; ...
0x1C00089D0  call    RtlEqualUnicodeString                       ; (feature-off branch)

5. Trigger Conditions

The behavioral difference between the two builds is observable only when Feature_623580472__private_IsEnabledDeviceUsage() returns non-zero (the feature is absent from the unpatched binary entirely). Under that condition:

  1. Create an AFD SAN endpoint via the standard Winsock/\Device\Afd path (socket create, bind/listen).
  2. Drive the endpoint's accept to be cancelled/torn down so that AfdSanCancelAccept runs. In the patched build this sets [ep+0x91] = 1 and drains [ep+0x70]; in the unpatched build it does neither.
  3. Cause a further accept/receive request IRP to reach AfdSanRedirectRequest / AfdSanFastCompleteRequest for the same endpoint while its signature is still 0x1AFD.

In the patched build the incoming IRP is completed with STATUS_CANCELLED rather than linked onto [ep+0x70]. In the unpatched build the IRP is linked onto the pending list of an endpoint whose accept was already cancelled.

Note that the endpoint spinlock ([ep+0x30]) serializes the cancel-accept drain and the queuing paths in both builds, and pending IRPs always carry a cancel routine (AfdSanCancelRequest). Whether the unpatched ordering can be driven to an actual free-then-use of the endpoint is not demonstrable from these two binaries; see §6 and §10.

For Finding #3, supply a TDI device name that contains a blocklist entry as a substring but is not byte-equal to it; it is accepted by the unpatched build and (with the feature on) rejected by the patched build.


6. Exploit Primitive & Development Notes

No exploit primitive is demonstrable from the two binaries. The patched change is a state-validation guard added in the accept/receive redirect path; the pre-patch code lacks that guard but the material required to prove a reachable, controllable memory-corruption primitive (an actual free of the endpoint while a stranded IRP still references it, followed by an attacker-controlled reclaim and use) is not present in the disassembly or decompilation. The endpoint queuing and the cancel-accept drain both execute under the endpoint spinlock, and pending IRPs carry a cancel routine, so the ordering the guard defends against does not, on the evidence here, establish a controllable use-after-free. Any pool-grooming, information-leak, or token-manipulation follow-on would be speculation and is not asserted.


7. Debugger Notes

For a follow-on analyst with a kernel debugger attached, the verifiable observation points are:

Finding #1 / #2 — the added guard

bp afd_patched!AfdSanRedirectRequest+... ; at 0x1C0082424 (the Feature_623580472 call before queuing)
bp afd_patched!AfdSanFastCompleteRequest+... ; at 0x1C008143C (same call in the receive/accept handler)
bp afd_patched!AfdSanCancelAccept+... ; at 0x1C007F1F7 (the byte [ep+0x91] = 1 write)
  • At the guard, rsi/r15 is the AFD endpoint (signature 0x1AFD at [ep+0x00]); inspect [ep+0x91]. When the feature is on and the byte is non-zero, execution takes the STATUS_CANCELLED path instead of linking the IRP into [ep+0x70].
  • The unpatched binary has no [ep+0x91] reference at all; confirm by disassembling AfdSanRedirectRequest @ 0x1C0080198 and observing that the only pre-queue gate is cmp byte [rdi+0x44], 0 at 0x1C008027A.

Finding #3 — name validation

bp afd_patched!AfdIsValidTdiDeviceName ; 0x1C000896C
  • Observe the Feature_2480880955__private_IsEnabledDeviceUsage call at 0x1C00089A3 selecting RtlFindUnicodeSubstring (feature on) versus RtlEqualUnicodeString (feature off). The unpatched function calls only RtlEqualUnicodeString.

Endpoint offsets referenced

Offset Field
+0x00 Signature (0x1AFD; 0xAFD0 after full teardown)
+0x02 State (4 = accepting/listening)
+0x30 Endpoint spinlock
+0x50/+0x58/+0x60 Parent/context pointers (set by init)
+0x70/+0x78 Pending-IRP list anchor
+0x84 Sequence counter (init to 1)
+0x8c Status (0x103, 0xC0000016)
+0x90 New byte, unconditionally zero-inited (patched)
+0x91 New state byte; zero-inited feature-gated, set to 1 by AfdSanCancelAccept (patched only)

8. Changed Functions — Triage

Delivered state-guard change (feature-staged behind Feature_623580472__private_IsEnabledDeviceUsage):

  • AfdSanRedirectRequest (0x1C00801980x1C0082338) (sim 0.9456) — accept-redirect handler; adds the [ep+0x91] gate before queuing. Finding #1.
  • AfdSanFastCompleteRequest (0x1C007EF300x1C0081030) (sim 0.9609) — receive/accept fast-complete handler; adds the same [ep+0x91] gate. Finding #2.
  • AfdSanAcquireContext (unpatched 0x1C007C860 → patched 0x1C007E830) (listed in the diff as sub_1C007C860, sim 0.3398) — a third consumer of the guard; reads [ep+0x91] at 0x1C007EE79 and 0x1C007EEB4. This is a real security-relevant change, not a cosmetic register reshuffle.
  • AfdSanCancelAccept (unpatched 0x1C007D130 → patched 0x1C007F140) (listed in the diff as sub_1C007D130, sim 0.2588) — the producer: sets [ep+0x91] = 1 and drains the [ep+0x70] pending list on accept cancel. The diff's matched patched counterpart (sub_1c0048800, described as a TDI transmit/receive handler) is a same-address mismatch; the correct counterpart is AfdSanCancelAccept @ 0x1C007F140.
  • AfdSanAcceptCore (0x1C007C0CC0x1C007E0CC) (sim 0.9912) — TDI accept path; inline endpoint init replaced by a call to the new AfdSanInitEndpoint, which also zero-inits +0x90/+0x91.
  • AfdSanFastCementEndpoint (0x1C007E6B00x1C0080800) (sim 0.9898) — connect/listen path; same migration to AfdSanInitEndpoint.

Name-validation change (feature-staged behind Feature_2480880955__private_IsEnabledDeviceUsage):

  • AfdIsValidTdiDeviceName (0x1C000896C) (sim 0.7674) — exact-match blocklist gains a substring-match path. Finding #3.

Feature-staged connection-creation dispatch (secondary; not a titled finding):

  • sub_1C0059BA0 (sim 0.0622) — the old connection-creation body at this address is, in the patched build, a small feature-check stub (data_1c002b9a8 & 0x10) that dispatches between the legacy path and a new path (sub_1c005ae64). The new path is reported to set Options including IO_FORCE_ACCESS_CHECK and a different DesiredAccess. This is a feature-staged dispatch (old path retained), consistent with a defense-in-depth access-check tightening; no reachable memory-safety impact is asserted.
  • sub_1C00403C4 (sim 0.7562), sub_1C004F090 (sim 0.9248), sub_1C00598CC (sim 0.9171) — add the sub_1c0009c20() feature-gate to select the same legacy-vs-new connection-creation path, plus SLIST/reference updates. Behavioral but feature-staged; no independent reachable impact identified.

Cosmetic / structural (no semantic security change identified):

  • sub_1C0047620 (sim 0.3417), sub_1C0058BA0 (sim 0.9226), sub_1C005B6B0 (sim 0.9238), sub_1C0038DB0 (sim 0.9478), sub_1C007D550 (sim 0.9601) — reorganization, register/stack-slot renaming, and address updates. The low similarity on sub_1C0047620 reflects structural reordering rather than logic change.

9. Unmatched Functions

The diff reports zero unmatched functions in either direction. The new initializer AfdSanInitEndpoint (0x1C0082020) is reached by call-rewriting inside AfdSanAcceptCore and AfdSanFastCementEndpoint and is not surfaced as a standalone added function in the diff output. The +0x91 state logic is therefore distributed across the initializer, the producer (AfdSanCancelAccept), and the three consumers (AfdSanRedirectRequest, AfdSanFastCompleteRequest, AfdSanAcquireContext), all present in the changed set once the two mismatched entries in §8 are corrected.


10. Confidence & Caveats

Confidence in the mechanism: high. Confidence in exploitable impact: low.

Verified directly against both binaries: - Offset +0x91 is referenced nowhere in the unpatched build and is introduced only in the patched build. - The full set of +0x91 sites in the patched build is: the feature-gated zero-init in AfdSanInitEndpoint (0x1C0082079), the set-to-1 in AfdSanCancelAccept (0x1C007F1F7), and the read guards in AfdSanRedirectRequest (0x1C008242D), AfdSanFastCompleteRequest (0x1C0081445), and AfdSanAcquireContext (0x1C007EE79, 0x1C007EEB4). - Both the queuing paths and the cancel-accept drain run under the endpoint spinlock at [ep+0x30] in both builds. - The whole change is gated behind Feature_623580472__private_IsEnabledDeviceUsage(), which has no counterpart in the unpatched binary — a staged rollout with the pre-patch behavior retained in the feature-off branch.

Why impact is rated Low and not High: - There is no uninitialized-memory read in the unpatched build: +0x91 simply does not exist there. This is a new state flag, not an uninitialized field, so a "read of stale pool residue" characterization does not apply. - The guard defends against a request IRP being queued onto an endpoint whose accept was cancelled (a dangling pending-IRP / use-after-free class window), but the two binaries do not establish that this ordering is reachable and drivable into a controlled free-then-use. The endpoint spinlock serialization and the always-present cancel routine argue against a straightforward controllable primitive. - No information leak, pool-grooming, or privilege-escalation chain is demonstrable, and none is claimed.

Items a follow-on analyst would confirm on a live target: the runtime default of Feature_623580472__private_IsEnabledDeviceUsage(); whether the cancel-accept-then-requeue ordering is reachable from a stock Winsock sequence; and, if so, whether the endpoint can actually be freed while a stranded IRP is linked. Absent those, this is a Low-severity, feature-staged state-validation hardening in the AFD SAN accept/receive path, plus a Low-severity feature-staged blocklist tightening in TDI device-name validation.

DONE