1. Overview

Field Value
Unpatched binary afd_unpatched.sys
Patched binary afd_patched.sys
Overall similarity 0.9683 (96.83%)
Matched functions 1093
Changed functions 33
Identical functions 1060
Unmatched (unpatched) 0
Unmatched (patched) 0

Verdict: The patch changes three functions in the Windows AFD (Winsock) driver in a security-relevant way. The primary fix is a real integer underflow in AfdSanAcceptCore (the accept IRP completion routine): a 16-bit options length is decremented by 8 without a lower-bound check, wrapping to a large value that drives an oversized memmove into the MDL-mapped output buffer. The second change adds a drain of pending listening IRPs on the failed-listen completion path in AfdTLListenComplete. The third change makes a connection-teardown state check unconditional in AfdSanFastCompleteRequest, where it was previously gated behind a feature flag. All three fixes are gated by (or, for the third, remove) EvaluateCurrentState feature flags.


2. Vulnerability Summary

Finding 1 — Integer Underflow → Kernel Pool Buffer Overflow

Field Value
Severity High
Class Integer underflow leading to heap buffer overflow (CWE-191 / CWE-122)
Function AfdSanAcceptCore (unpatched 0x1C007608C, patched 0x1C007508C)
Primitive Kernel pool / MDL-mapped buffer overflow via oversized memmove

Root cause: In the accept completion routine, after the options block is reached (guarded by options_size >= 6), one of two copy paths runs depending on the connection byte at [connection+0xAC]. When that byte is zero, the code computes the copy length as options_size - 8 in a 16-bit register. Because the only guard is options_size >= 6, the values 6 and 7 pass the guard, and options_size - 8 wraps in 16 bits to 0xFFFE (65534) or 0xFFFF (65535). That wrapped value is then used as the memmove size, copying up to ~64 KB from the transport response into the MDL-mapped output buffer that was sized for the small options field. The patched version adds a feature-flag-gated path that, when the feature is enabled, checks options_size < 8 and zero-fills with memset instead of subtracting 8.

Note that the other branch (connection byte [connection+0xAC] != 0) subtracts 2, not 8. Since the guard already forces options_size >= 6, options_size - 2 >= 4 never underflows, and that branch is unchanged between the two builds. The underflow is specific to the -8 branch.

Attacker-reachable entry point: A peer completes a connection to an AFD socket that has an accept/super-accept pending. The transport-reported options length in the connection response drives the vulnerable length math.

Call chain: 1. NtDeviceIoControlFile / WSAAccept / AcceptEx (user mode) 2. AFD IRP_MJ_DEVICE_CONTROL dispatch 3. AfdAccept / AfdServiceSuperAccept / AfdServiceWaitForListen (accept service paths) 4. AfdSanAcceptCore — accept IRP completion routine (copies options data from the connection response into the user's MDL-mapped buffer)


Finding 2 — Pending Listen IRPs Not Drained on Failed-Listen Completion

Field Value
Severity High
Class Use-after-free / stale IRP access (CWE-416)
Function AfdTLListenComplete (unpatched 0x1C005AB60, patched 0x1C0059850)
Primitive Pending IRPs left queued across endpoint dereference on the listen-abort path

Root cause: AfdTLListenComplete runs the abort branch only when the completion status a2 < 0 (a failed listen). In that branch the unpatched code clears state flags ([conn+8] &= ~3, *conn &= ~4), writes conn[0xA8]/conn[0xAA] from the IRP, and releases the connection spinlock (at conn+0x30) without draining the pending listen-IRP list at conn+0x70. Immediately after, AfdDerefTLBaseEndpoint decrements the endpoint reference. Any IRPs still queued on conn+0x70 therefore survive across the endpoint dereference. The patch inserts a feature-flag-gated call to AfdDrainListeningIrps(conn, &LockHandle, status) before the lock is released; that helper walks the conn+0x70 list, recovers each IRP (list entry lies at IRP+0xA8, so IRP = entry - 0xA8), clears its cancel routine, completes it with the failing status, and re-acquires the spinlock.

Attacker-reachable entry point: A listening AFD socket whose listen completes with a failure status while listen/wait IRPs are queued on the endpoint.

Call chain: 1. NtDeviceIoControlFile / WSAIoctl / connect (user mode) 2. AFD IRP_MJ_DEVICE_CONTROL dispatch 3. AfdTLListenComplete — TDI listen completion handler (failure/abort branch)


Finding 3 — Connection-Teardown State Check Made Unconditional (downgraded)

Field Value
Severity Low
Class Missing state validation under feature-flag-off configuration (hardening)
Function AfdSanFastCompleteRequest (unpatched 0x1C0078630, patched 0x1C0077700)
Primitive Receive IRP queued onto a connection whose teardown-state byte is set

Root cause and downgrade rationale: In the completion path, under the connection spinlock (conn+0x30), the code sets Irp->CancelRoutine = AfdSanCancelRequest (Irp+0x68), checks Irp->Cancel (Irp+0x44), and decides whether to queue the IRP onto the conn+0x70 list. The unpatched queue condition is Irp->Cancel == 0 && (!EvaluateCurrentState(flag) || conn[0x91] == 0). When the feature flag is disabled, !EvaluateCurrentState(flag) is true, so the teardown-state byte conn[0x91] is not checked and the IRP is queued regardless. The patched condition is simply Irp->Cancel == 0 && conn[0x91] == 0 — the conn[0x91] check is unconditional.

The report's original framing of this as a time-of-check/time-of-use race is not supported by the binaries: the cancel-routine set, both checks, and the list insertion all execute while the connection spinlock is held, and AfdSanCancelRequest itself takes the same lock, so no concurrent cancel can interleave inside this region. There is no widened race window and no double list insertion. The real change is narrower: a connection-teardown state check that was skipped when a feature flag was off is now always enforced. The cancel routine is AfdSanCancelRequest in both builds (the earlier claim of two different cancel-routine addresses was an artifact of address rebasing). This is a defensive hardening change, downgraded accordingly.

Call chain: 1. NtDeviceIoControlFile / WSARecv / recv (user mode) 2. AFD IRP_MJ_DEVICE_CONTROL dispatch 3. AfdSanFastCompleteRequest — receive completion path with cancel-routine setup


3. Pseudocode Diff

Finding 1 — AfdSanAcceptCore (Accept Completion — Integer Underflow)

// ============================================================
// UNPATCHED — AfdSanAcceptCore @ 0x1C007608C
// ============================================================
if ( v15[6] >= 6u )                       // options size guard: >= 6
{
    v31 = *((_WORD *)v15 + 12);           // v31 = options size (16-bit)
    v32 = (unsigned __int16 *)((char *)v49 + v49[10] + 24);   // source in response
    // ... map output buffer, v35 = &mapped[v15[2]] ...
    if ( *(_BYTE *)(v46 + 172) != 0 )     // connection byte [conn+0xAC] != 0 : subtract-2 path
    {
        v36 = *v32;
        if ( v36 + 2 >= (unsigned __int64)v31 - 2 )   // v31 >= 6, so v31-2 >= 4, no underflow
            v37 = v31 - 2;
        else
            v37 = v36 + 2;
        memmove(v35, v32 + 1, v37);
        *(_WORD *)&v35[v15[6] - 2] = v37;
    }
    else                                  // [conn+0xAC] == 0 : subtract-8 path (VULNERABLE)
    {
        v39 = *v32;
        if ( v39 + 4 >= v31 - 8 )         // v31 in {6,7}: v31-8 is negative; branch taken
            v40 = v31 - 8;                // v40 is unsigned __int16 -> 0xFFFE / 0xFFFF
        else
            v40 = v39 + 4;
        *(_DWORD *)v35 = 0;
        *((_DWORD *)v35 + 1) = 1;
        memmove(v35 + 8, v32, v40);       // OVERSIZED copy (up to ~65534 bytes)
    }
}

// ============================================================
// PATCHED — AfdSanAcceptCore @ 0x1C007508C
// ============================================================
if ( *(_BYTE *)(v52 + 172) != 0 )         // subtract-2 path: unchanged
{
    // ... identical to unpatched subtract-2 path ...
}
else if ( EvaluateCurrentState(&g_Feature_1191968056_61054031_FeatureDescriptorDetails) )
{
    if ( v35 < 8u )                       // NEW: explicit lower-bound check
    {
        memset(v39, 0, v35);              // SAFE: zero-fill, no subtraction
        goto done;
    }
    v43 = v35 - 8;                        // safe here: v35 >= 8
    // ... bounded copy of at most (v35 - 8) bytes ...
}
else
{
    // feature OFF: old subtract-8 path is preserved (still underflows for size 6/7)
    if ( v47 + 4 >= v35 - 8 )
        v48 = v35 - 8;
    else
        v48 = v47 + 4;
    // ... memmove with v48 ...
}

Finding 2 — AfdTLListenComplete (Failed-Listen Completion — Missing IRP Drain)

// ============================================================
// UNPATCHED — AfdTLListenComplete @ 0x1C005AB60
// ============================================================
if ( a2 < 0 )                             // failed-listen (abort) branch
{
    KeAcquireInStackQueuedSpinLock((PKSPIN_LOCK)FsContext + 6, &LockHandle);  // lock at conn+0x30
    *((_DWORD *)FsContext + 2) &= 0xFFFFFFFC;      // [conn+8] &= ~3
    FsContext[84] = *((_WORD *)&Irp->Tail.CompletionKey + 12);   // conn[0xA8]
    FsContext[85] = (unsigned __int16)Irp->Tail...Flink;         // conn[0xAA]
    *FsContext &= ~4u;                     // clear flag 0x4
    // NO drain of the pending listen-IRP list at conn+0x70
    KeReleaseInStackQueuedSpinLock(&LockHandle);
}
// ... AfdDerefTLBaseEndpoint(FsContext); IofCompleteRequest(Irp, ...) ...

// ============================================================
// PATCHED — AfdTLListenComplete @ 0x1C0059850
// ============================================================
if ( a2 < 0 )
{
    KeAcquireInStackQueuedSpinLock((PKSPIN_LOCK)FsContext + 6, &LockHandle);
    *((_DWORD *)FsContext + 2) &= 0xFFFFFFFC;
    FsContext[84] = *((_WORD *)&Irp->Tail.CompletionKey + 12);
    FsContext[85] = (unsigned __int16)Irp->Tail...Flink;
    *FsContext &= ~4u;
    if ( EvaluateCurrentState(&g_Feature_2949905720_61057653_FeatureDescriptorDetails) )
        AfdDrainListeningIrps((KSPIN_LOCK *)FsContext, &LockHandle, a2);   // NEW: drain conn+0x70 list
    KeReleaseInStackQueuedSpinLock(&LockHandle);
}

Finding 3 — AfdSanFastCompleteRequest (Receive Completion — State Check Made Unconditional)

// ============================================================
// UNPATCHED — AfdSanFastCompleteRequest @ 0x1C0078630
// ============================================================
KeAcquireInStackQueuedSpinLock((PKSPIN_LOCK)(v18 + 48), &LockHandle);   // lock at conn+0x30
_InterlockedExchange64(v20 + 13, (__int64)AfdSanCancelRequest);         // Irp->CancelRoutine = Irp+0x68
if ( *((_BYTE *)v20 + 68) == 0                                          // Irp->Cancel (Irp+0x44) == 0
  && (!EvaluateCurrentState(&g_Feature_757798200_60363861_FeatureDescriptorDetails)
   || *(_BYTE *)(v18 + 145) == 0) )                                     // conn[0x91] checked only if flag on
{
    // queue IRP list entry (Irp+0xA8) onto the conn+0x70 list
    v27 = v20 + 21;
    v28 = (_QWORD *)(v18 + 112);
    v29 = *(_QWORD *)(v18 + 112);
    if ( *(_QWORD *)(v29 + 8) != v18 + 112 ) __fastfail(3u);
    *v27 = v29; v27[1] = v28; *(_QWORD *)(v29 + 8) = v27; *v28 = v27;
    KeReleaseInStackQueuedSpinLock(&LockHandle);
}

// ============================================================
// PATCHED — AfdSanFastCompleteRequest @ 0x1C0077700
// ============================================================
KeAcquireInStackQueuedSpinLock((PKSPIN_LOCK)(v18 + 48), &LockHandle);
_InterlockedExchange64(v20 + 13, (__int64)AfdSanCancelRequest);
if ( *((_BYTE *)v20 + 68) == 0 && *(_BYTE *)(v18 + 145) == 0 )          // conn[0x91] check now unconditional
{
    // identical list insertion onto conn+0x70
    v27 = v20 + 21;
    v28 = (_QWORD *)(v18 + 112);
    v29 = *(_QWORD *)(v18 + 112);
    if ( *(_QWORD *)(v29 + 8) != v18 + 112 ) __fastfail(3u);
    *v27 = v29; v27[1] = v28; *(_QWORD *)(v29 + 8) = v27; *v28 = v27;
    KeReleaseInStackQueuedSpinLock(&LockHandle);
}

4. Assembly Analysis

Finding 1 — AfdSanAcceptCore Vulnerable Options Copy

Unpatched assembly (options guard and the vulnerable subtract-8 branch):

00000001C0076401  cmp     dword ptr [rbx+18h], 6      ; options size < 6 -> skip copy
00000001C0076405  jb      loc_1C00764DF
00000001C007640B  movzx   r14d, word ptr [rbx+18h]    ; r14 = options size (16-bit)
; ... output buffer mapped into r15 ...
00000001C0076452  mov     rax, [rsp+0A8h+var_48]
00000001C0076457  cmp     byte ptr [rax+0ACh], 0      ; connection byte [conn+0xAC]
00000001C007645E  jz      short loc_1C007649E         ; == 0 -> subtract-8 (vulnerable) path
; ( [conn+0xAC] != 0 falls through to the subtract-2 path at 0x1C0076460, which cannot
;   underflow because size >= 6, and is unchanged between builds )
00000001C007649E  movzx   r8d, word ptr [rdx]         ; r8 = 16-bit value from response
00000001C00764A2  mov     r9d, 4
00000001C00764A8  lea     ecx, [r9+r8]                ; ecx = r8 + 4
00000001C00764AC  movzx   eax, r14w                   ; eax = options size
00000001C00764B0  sub     eax, 8                      ; eax = size - 8
00000001C00764B3  cmp     ecx, eax
00000001C00764B5  jge     short loc_1C00764BD
00000001C00764B7  lea     r14d, [r9+r8]               ; r14 = r8 + 4
00000001C00764BB  jmp     short loc_1C00764C6
00000001C00764BD  mov     eax, 0FFF8h
00000001C00764C2  add     r14w, ax                    ; r14 = size - 8 in 16 bits -> 0FFFEh/0FFFFh for size 6/7
00000001C00764C6  and     dword ptr [r15], 0
00000001C00764CA  mov     dword ptr [r15+4], 1
00000001C00764D2  movzx   r8d, r14w                    ; Size = underflowed length
00000001C00764D6  lea     rcx, [r15+8]                 ; dst = mapped output buffer + 8
00000001C00764DA  call    memmove                      ; oversized copy -> buffer overflow

Patched assembly (same subtract-8 branch, now feature-gated with a < 8 check):

00000001C0075457  cmp     [rax+0ACh], dil              ; connection byte [conn+0xAC] == 0 ?
00000001C007545E  jz      short loc_1C00754A5
00000001C00754A5  lea     rcx, g_Feature_1191968056_61054031_FeatureDescriptorDetails
00000001C00754AC  call    EvaluateCurrentState
00000001C00754B1  mov     ebx, 8
00000001C00754B6  test    eax, eax
00000001C00754B8  jz      short loc_1C0075512          ; feature OFF -> old subtract-8 path (still underflows)
00000001C00754BA  cmp     si, bx                       ; options size < 8 ?
00000001C00754BD  jb      short loc_1C0075502          ; yes -> zero-fill instead of subtracting
00000001C00754BF  sub     si, bx                       ; size - 8 (safe: size >= 8 here)
; ...
00000001C0075502  movzx   r8d, si                      ; Size = options size (< 8)
00000001C0075506  xor     edx, edx                     ; Val = 0
00000001C0075508  mov     rcx, r15                     ; dst
00000001C007550B  call    memset                       ; SAFE: zero the small buffer

Finding 2 — AfdTLListenComplete Missing IRP Drain Before Lock Release

Unpatched assembly (failed-listen branch):

00000001C005ABB6  test    ebp, ebp                     ; completion status
00000001C005ABB8  js      short loc_1C005AC1B          ; status < 0 -> abort branch
; ...
00000001C005AC1B  lea     rcx, [rbx+30h]               ; SpinLock (conn+0x30)
00000001C005AC1F  lea     rdx, [rsp+88h+LockHandle]
00000001C005AC24  call    cs:__imp_KeAcquireInStackQueuedSpinLock
00000001C005AC2A  and     dword ptr [rbx+8], 0FFFFFFFCh
00000001C005AC33  movzx   eax, word ptr [rsi+90h]
00000001C005AC3A  mov     [rbx+0A8h], ax
00000001C005AC41  movzx   eax, word ptr [rsi+78h]
00000001C005AC45  mov     [rbx+0AAh], ax
00000001C005AC4C  mov     eax, 0FFFBh
00000001C005AC51  and     [rbx], ax                    ; clear flag 0x4
; *** no drain of the conn+0x70 list ***
00000001C005AC54  call    cs:__imp_KeReleaseInStackQueuedSpinLock

Patched assembly (same branch, drain added before lock release):

00000001C005990B  lea     rcx, [rbx+30h]               ; SpinLock (conn+0x30)
00000001C005990F  lea     rdx, [rsp+88h+LockHandle]
00000001C0059914  call    cs:__imp_KeAcquireInStackQueuedSpinLock
00000001C005991A  and     dword ptr [rbx+8], 0FFFFFFFCh
00000001C005991E  lea     rcx, g_Feature_2949905720_61057653_FeatureDescriptorDetails
00000001C0059925  movzx   eax, word ptr [rsi+90h]
00000001C005992C  mov     [rbx+0A8h], ax
00000001C0059933  movzx   eax, word ptr [rsi+78h]
00000001C0059937  mov     [rbx+0AAh], ax
00000001C005993E  mov     eax, 0FFFBh
00000001C0059943  and     [rbx], ax
00000001C0059946  call    EvaluateCurrentState
00000001C005994B  test    eax, eax
00000001C005994D  jz      short loc_1C005995F          ; feature OFF -> skip drain
00000001C005994F  mov     r8d, ebp                     ; arg3 = completion status
00000001C0059952  lea     rdx, [rsp+88h+LockHandle]    ; arg2 = &LockHandle
00000001C0059957  mov     rcx, rbx                     ; arg1 = connection
00000001C005995A  call    AfdDrainListeningIrps        ; NEW: drain + complete pending listen IRPs
00000001C005995F  lea     rcx, [rsp+88h+LockHandle]
00000001C0059964  call    cs:__imp_KeReleaseInStackQueuedSpinLock

Finding 3 — AfdSanFastCompleteRequest Teardown-State Check

Unpatched assembly (state check gated by the feature flag):

00000001C0078958  lea     rcx, [r15+30h]               ; SpinLock (conn+0x30)
00000001C007895C  lea     rdx, [rsp+0B8h+LockHandle]
00000001C0078964  call    cs:__imp_KeAcquireInStackQueuedSpinLock
00000001C007896A  lea     rax, AfdSanCancelRequest
00000001C0078971  xchg    rax, [rsi+68h]               ; Irp->CancelRoutine = AfdSanCancelRequest
00000001C0078975  cmp     byte ptr [rsi+44h], 0        ; Irp->Cancel
00000001C0078979  jnz     short loc_1C00789D2          ; cancelled -> complete as cancelled
00000001C007897B  lea     rcx, g_Feature_757798200_60363861_FeatureDescriptorDetails
00000001C0078982  call    EvaluateCurrentState
00000001C0078987  test    eax, eax
00000001C0078989  jz      short loc_1C0078995          ; feature OFF -> SKIP the [conn+0x91] check, queue IRP
00000001C007898B  cmp     byte ptr [r15+91h], 0        ; teardown-state byte
00000001C0078993  jnz     short loc_1C00789D2
00000001C0078995  add     rsi, 0A8h                    ; Irp list entry (Irp+0xA8)
00000001C007899C  lea     rax, [r15+70h]               ; list head (conn+0x70)
00000001C00789A0  mov     rcx, [rax]
00000001C00789A3  cmp     [rcx+8], rax
00000001C00789A7  jz      short loc_1C00789B0
00000001C00789A9  mov     ecx, 3
00000001C00789AE  int     29h                          ; RtlFailFast on list corruption
00000001C00789B0  mov     [rsi], rcx                   ; queue IRP onto conn+0x70 list
00000001C00789B3  mov     [rsi+8], rax
00000001C00789B7  mov     [rcx+8], rsi
00000001C00789BB  mov     [rax], rsi

Patched assembly (state check unconditional, feature-flag call removed):

00000001C0077A3A  lea     rax, AfdSanCancelRequest
00000001C0077A41  xchg    rax, [rsi+68h]               ; Irp->CancelRoutine = AfdSanCancelRequest
00000001C0077A45  cmp     byte ptr [rsi+44h], 0        ; Irp->Cancel
00000001C0077A49  jnz     short loc_1C0077A92          ; cancelled -> complete as cancelled
00000001C0077A4B  cmp     byte ptr [r15+91h], 0        ; teardown-state byte (always checked)
00000001C0077A53  jnz     short loc_1C0077A92
00000001C0077A55  add     rsi, 0A8h
00000001C0077A5C  lea     rax, [r15+70h]
00000001C0077A60  mov     rcx, [rax]
00000001C0077A63  cmp     [rcx+8], rax
00000001C0077A67  jz      short loc_1C0077A70
00000001C0077A69  mov     ecx, 3
00000001C0077A6E  int     29h
00000001C0077A70  mov     [rsi], rcx                   ; queue IRP onto conn+0x70 list
00000001C0077A73  mov     [rsi+8], rax
00000001C0077A77  mov     [rcx+8], rsi
00000001C0077A7B  mov     [rax], rsi

5. Trigger Conditions

Finding 1 — Integer Underflow / Pool Overflow

  1. Open an AFD socket via WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP) or CreateFileW(L"\\\\.\\Device\\Afd", ...).
  2. bind() and listen(), then post an accept operation (AcceptEx / WSAAccept), which reaches AfdSanAcceptCore via AfdAccept / AfdServiceSuperAccept / AfdServiceWaitForListen.
  3. The completion must reach the options block: options_size (the field at [connection+0x18], v15[6]) must be >= 6, and the connection byte at [connection+0xAC] must be 0 so the subtract-8 branch runs.
  4. Drive the options length to exactly 6 or 7: options_size - 8 wraps in 16 bits to 0xFFFE/0xFFFF.
  5. Observable effect: memmove copies ~65534/65535 bytes from the transport response into the MDL-mapped output buffer. The overflow is bounded to at most ~64 KB (the length is a 16-bit value), but that is far larger than the small options field the buffer was sized for. Expect pool/adjacent-page corruption and a BSOD such as PAGE_FAULT_IN_NONPAGED_AREA or BAD_POOL_HEADER.

Finding 2 — Pending Listen IRPs Not Drained

  1. Create a listening AFD socket and arrange for the listen to complete with a failure status (a2 < 0), which selects the abort branch in AfdTLListenComplete.
  2. Have listen/wait IRPs queued on the endpoint's conn+0x70 list at the time of the failed completion.
  3. In the unpatched build the abort branch clears the state flags and releases the conn+0x30 spinlock without draining conn+0x70, then AfdDerefTLBaseEndpoint runs.
  4. Observable effect: IRPs remaining on conn+0x70 after the endpoint dereference are stale; later access/completion of them can touch a freed endpoint. At the KeReleaseInStackQueuedSpinLock in the abort branch, [conn+0x70] != conn+0x70 indicates undrained IRPs.

Finding 3 — Teardown-State Check (hardening)

  1. Establish a TCP connection over an AFD socket and issue receive operations that reach AfdSanFastCompleteRequest.
  2. Arrange for the connection to be in teardown (conn[0x91] != 0) at completion time.
  3. With the relevant feature flag disabled, the unpatched build skips the conn[0x91] check and queues the receive IRP onto conn+0x70 even though the connection is tearing down. The patched build always checks conn[0x91] and instead completes the IRP as cancelled.
  4. This is a state-validation gap, not a timing race — the entire sequence runs under the conn+0x30 spinlock.

6. Exploit Primitive & Development Notes

Finding 1 — Pool / Mapped-Buffer Overflow Primitive

  • Primitive: Bounded kernel buffer overflow. memmove copies up to a 16-bit length (max 0xFFFF) from the transport connection-response options data into the MDL-mapped output buffer. The copy overruns the buffer's mapped extent into adjacent kernel memory.
  • Overflow characteristics: The length is capped by the 16-bit wrap (≤ ~64 KB), so this is not an unbounded copy. The source is the connection-response options region; how much of its content is attacker-controlled depends on the transport. The destination is the accept output buffer described by the user-supplied MDL.
  • Turning into a full exploit: Standard kernel pool-overflow techniques would apply (grooming an adjacent target object, corrupting a data-only field such as token privileges), but the bounded length and the requirement that the feature flag be in the vulnerable state constrain reliability. No pointer leak is required for the overflow itself.
  • Mitigations: Pool integrity checks, randomized pool layout, and HVCI complicate exploitation as usual.

Finding 2 — Stale IRP Primitive

  • Primitive: IRPs left queued on conn+0x70 across AfdDerefTLBaseEndpoint on the failed-listen path. If the dereference frees the endpoint, later handling of those IRPs references freed memory.
  • Turning into a full exploit: Would require reclaiming the freed endpoint allocation and steering a subsequent IRP-driven access, which is timing- and layout-dependent.

Finding 3 — State-Validation Hardening

  • Not a standalone exploit primitive. The change removes a configuration (feature-flag-off) in which a receive IRP could be queued onto a connection already in teardown. Impact depends on downstream handling of that queued IRP and is not demonstrable from the diff alone.

7. Debugger PoC Playbook

Finding 1 — AfdSanAcceptCore (Integer Underflow → Overflow)

Breakpoints:

bp afd_unpatched+0x7608C

Break at the accept completion routine entry.

bp afd_unpatched+0x764C2

Break at add r14w, ax (the 0FFF8h subtraction). Inspect r14 immediately after: for options size 6 it becomes 0xFFFE, for 7 0xFFFF.

bp afd_unpatched+0x764DA

Break at the memmove in the subtract-8 branch and read r8 (the size argument).

What to inspect:

Register / Memory Meaning
word [connection+0x18] Options size; must be 6 or 7 to trigger
byte [connection+0xAC] Must be 0 to reach the subtract-8 branch
r14 at 0x1C00764C2 Copy length after the 16-bit subtraction (0xFFFE/0xFFFF on underflow)
r15 Destination: mapped output buffer base used by the copy
rdx Source pointer into the connection-response options data

Key instruction offsets:

Offset Instruction Significance
0x1C0076401 cmp dword [rbx+18h], 6 Only guard on options size
0x1C007645E jz loc_1C007649E Selects the subtract-8 (vulnerable) branch when [conn+0xAC]==0
0x1C00764C2 add r14w, ax 16-bit size - 8 underflow
0x1C00764DA call memmove Oversized copy

Expected observation: At the memmove, r8 holds 0xFFFE/0xFFFF, and the copy overruns the mapped output buffer, corrupting adjacent memory (BSOD PAGE_FAULT_IN_NONPAGED_AREA / BAD_POOL_HEADER).


Finding 2 — AfdTLListenComplete (Missing IRP Drain)

Breakpoints:

bp afd_unpatched+0x5AB60

Entry of the listen completion handler. rbx becomes the connection object; the pending listen-IRP list head is conn+0x70.

bp afd_unpatched+0x5AC54

Break at the KeReleaseInStackQueuedSpinLock in the abort branch. Check whether [conn+0x70] still points at queued IRPs.

What to inspect:

Register / Memory Meaning
rbx Connection object
[rbx+0x70] Pending listen-IRP list Flink; != rbx+0x70 means undrained IRPs
[rbx+8] State dword; watch &= ~3
word [rbx] Flags; watch &= ~4
[rbx+0xA8], [rbx+0xAA] State values written from the IRP

Key instruction offsets:

Offset Instruction Significance
0x1C005ABB8 js loc_1C005AC1B Selects the abort branch (status < 0)
0x1C005AC2A0x1C005AC51 State-flag updates Connection state transition
0x1C005AC54 call KeReleaseInStackQueuedSpinLock Lock release without drain (unpatched)

Finding 3 — AfdSanFastCompleteRequest (Teardown-State Check)

Breakpoints:

bp afd_unpatched+0x78630

Entry of the receive completion path. rsi = IRP, r15 = connection object.

bp afd_unpatched+0x78982

Break at the feature-flag call EvaluateCurrentState. If it returns 0, the [r15+0x91] teardown check is skipped and the IRP is queued unconditionally.

bp afd_unpatched+0x789B0

Break at the list insertion onto conn+0x70.

What to inspect:

Register / Memory Meaning
rsi IRP pointer
[rsi+0x44] Irp->Cancel
[rsi+0x68] Irp->CancelRoutine, set to AfdSanCancelRequest
[r15+0x91] Connection teardown-state byte
[r15+0x70] Pending-IRP list head
[rsi+0xA8], [rsi+0xB0] IRP list-entry fields queued onto the list

Key instruction offsets:

Offset Instruction Significance
0x1C0078971 xchg [rsi+68h], rax Sets the cancel routine (under the lock)
0x1C0078975 cmp byte [rsi+44h], 0 Irp->Cancel check
0x1C0078982 call EvaluateCurrentState Feature-flag gate; when off, [r15+0x91] is not checked
0x1C00789B0 mov [rsi], rcx IRP queued onto conn+0x70

8. Changed Functions — Full Triage

Security-Relevant Changes (4 functions)

Function Similarity Change Type Note
AfdSanAcceptCore (0x1C007608C) 0.8803 Security Accept completion — patched adds a feature-gated options_size < 8 zero-fill (memset), preventing the 16-bit size - 8 underflow in the copy length
AfdTLListenComplete (0x1C005AB60) 0.8635 Security Failed-listen completion — patched adds a feature-gated AfdDrainListeningIrps call to drain the conn+0x70 IRP list before releasing the lock
AfdSanFastCompleteRequest (0x1C0078630) 0.9817 Hardening Receive completion — the conn[0x91] teardown-state check, previously feature-flag-gated, is made unconditional; not a race (all under the connection spinlock)
sub_1C00529A0 0.8387 Security-relevant (candidate) Connection abort/cleanup — restructured drain logic with added feature-gated state checks; specific mechanism not further verified here

Behavioral Changes (19 functions)

These functions share a common pattern: addition (or removal) of EvaluateCurrentState feature-flag checks that gate new code paths, consolidation of dual code paths into single calls, and connection-state validation refactoring.

Function Similarity Key Change
sub_1C00357C0 0.8794 Feature flag added; state validation split into separate branches
sub_1C003EC54 0.8796 Feature flag added; lock/unlock wrapping around dispatch made conditional
sub_1C005B360 0.8438 Feature flags added; connection validation split
sub_1C005B600 0.8438 Same restructuring as sub_1C005B360
sub_1C0032B70 0.9212 Feature flag; condition split; explicit handle variable
sub_1C0047DC4 0.9067 Removed dual path; unified to sub_1C0047FF0
sub_1C003C5C0 0.6169 Removed dual path; unified to sub_1C0047FF0; code deduplication
sub_1C005A280 0.9236 Removed dual path; unified to sub_1C0047FF0; reversed condition order
sub_1C0076DB0 0.8696 Removed feature-flag-gated dual path; unconditional IRP draining and reset
sub_1C0077F70 0.9893 Inlined connection setup; fields set directly
sub_1C0047000 0.9077 Dual code path consolidation; variable/address renames
sub_1C0047FF0 0.9373 Unified function replacing two prior helpers
sub_1C0034AD0 0.9541 Feature flag; condition split
AfdSanConnectHandler (0x1C00771B0) 0.9371 Variable renaming; dual code path consolidation
sub_1C00548E0 0.8961 Feature flag; condition split
sub_1C0035A70 0.342 Significant restructuring; feature flag + path consolidation
sub_1C00360C0 0.423 Significant restructuring; matched to a different function in the patched binary
sub_1C0042230 0.373 Significant restructuring; matched to a different function in the patched binary
sub_1C003BD30 0.391 Matched to a different function; likely false match

Cosmetic / False Matches (10 functions)

Function Similarity Note
sub_1C0047D90 0.918 Address changes only
sub_1C00795C4 0.970 Variable renaming only
sub_1C0053FA0 0.115 False match — unrelated functions
sub_1C0068600 0.130 False match
sub_1C0076630 0.168 False match
sub_1C0015AF8 0.110 False match
sub_1C0065D98 0.006 False match
sub_1C0069608 0.095 False match
sub_1C000CEB0 0.329 False match or trivial function
sub_1C00529A0 (listed above) Also has cosmetic restructuring alongside the candidate security change

9. Unmatched Functions

No functions were added or removed between the two binaries. All 33 changed functions were matched one-to-one. The 10 low-similarity matches (similarity < 0.33) are almost certainly false matches — unrelated functions paired due to address proximity or structural coincidence.


10. Confidence & Caveats

Confidence

  • Finding 1 (High confidence): The unpatched size - 8 16-bit underflow (for options size 6/7, past the >= 6 guard) and the patched size < 8 zero-fill are both directly visible in the disassembly and decompilation of both builds. The underflow is bounded to a 16-bit length (≤ ~64 KB), not an unbounded copy.
  • Finding 2 (Medium-High confidence): The added drain of the conn+0x70 list before lock release on the failed-listen path is clearly present in the patched build and absent in the unpatched build. The precise exploitability (use-after-free vs. leaked/stuck IRP) is not fully established from the diff.
  • Finding 3 (Low confidence / downgraded): The change is real (the conn[0x91] teardown-state check made unconditional) but is a hardening improvement, not a race. The earlier TOCTOU characterization is not supported by the code, which runs the whole sequence under the connection spinlock.

Assumptions

  1. Feature flags gate the fixes. Findings 1 and 2 are only active when their EvaluateCurrentState feature flag is enabled; with the flag off, the patched binary still contains the vulnerable path. Finding 3 is the reverse: the patched build removes the flag gate so the check is always enforced. The runtime flag state on a target should be confirmed.
  2. Options length source (Finding 1). The options size at [connection+0x18] is assumed to derive from the transport connection response. The degree of attacker control over reaching exactly 6/7 depends on the transport and was not further traced.
  3. sub_1C00529A0 is listed as a security-relevant candidate based on its restructured cleanup logic; its exact mechanism was not verified in this pass.

What to Verify Manually Before Writing a PoC

  • Options length control (Finding 1): Confirm how the transport populates [connection+0x18] and whether values 6/7 are reachable for a peer.
  • Feature flag states: Confirm the runtime state of g_Feature_1191968056_61054031, g_Feature_2949905720_61057653, and g_Feature_757798200_60363861.
  • Output buffer sizing (Finding 1): Determine the mapped extent of the accept output buffer to characterize how far past it the ~64 KB copy reaches.
  • Endpoint lifetime (Finding 2): Confirm whether AfdDerefTLBaseEndpoint on the abort path can free the endpoint while IRPs remain on conn+0x70.