afd.sys — Integer underflow in accept options copy leading to kernel pool buffer overflow (CWE-191) fixed
KB5094122
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
- Open an AFD socket via
WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP)orCreateFileW(L"\\\\.\\Device\\Afd", ...). bind()andlisten(), then post an accept operation (AcceptEx/WSAAccept), which reachesAfdSanAcceptCoreviaAfdAccept/AfdServiceSuperAccept/AfdServiceWaitForListen.- 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. - Drive the options length to exactly 6 or 7:
options_size - 8wraps in 16 bits to0xFFFE/0xFFFF. - Observable effect:
memmovecopies ~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 asPAGE_FAULT_IN_NONPAGED_AREAorBAD_POOL_HEADER.
Finding 2 — Pending Listen IRPs Not Drained
- Create a listening AFD socket and arrange for the listen to complete with a failure status (
a2 < 0), which selects the abort branch inAfdTLListenComplete. - Have listen/wait IRPs queued on the endpoint's
conn+0x70list at the time of the failed completion. - In the unpatched build the abort branch clears the state flags and releases the
conn+0x30spinlock without drainingconn+0x70, thenAfdDerefTLBaseEndpointruns. - Observable effect: IRPs remaining on
conn+0x70after the endpoint dereference are stale; later access/completion of them can touch a freed endpoint. At theKeReleaseInStackQueuedSpinLockin the abort branch,[conn+0x70] != conn+0x70indicates undrained IRPs.
Finding 3 — Teardown-State Check (hardening)
- Establish a TCP connection over an AFD socket and issue receive operations that reach
AfdSanFastCompleteRequest. - Arrange for the connection to be in teardown (
conn[0x91] != 0) at completion time. - With the relevant feature flag disabled, the unpatched build skips the
conn[0x91]check and queues the receive IRP ontoconn+0x70even though the connection is tearing down. The patched build always checksconn[0x91]and instead completes the IRP as cancelled. - This is a state-validation gap, not a timing race — the entire sequence runs under the
conn+0x30spinlock.
6. Exploit Primitive & Development Notes
Finding 1 — Pool / Mapped-Buffer Overflow Primitive
- Primitive: Bounded kernel buffer overflow.
memmovecopies up to a 16-bit length (max0xFFFF) 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+0x70acrossAfdDerefTLBaseEndpointon 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) |
0x1C005AC2A–0x1C005AC51 |
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 - 816-bit underflow (for options size 6/7, past the>= 6guard) and the patchedsize < 8zero-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+0x70list 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
- Feature flags gate the fixes. Findings 1 and 2 are only active when their
EvaluateCurrentStatefeature 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. - 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. sub_1C00529A0is 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, andg_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
AfdDerefTLBaseEndpointon the abort path can free the endpoint while IRPs remain onconn+0x70.