afd.sys — Use-after-free hardening via reference-count integrity fastfail checks (CWE-416) fixed
KB5087545
1. Overview
- Unpatched Binary:
afd_unpatched.sys - Patched Binary:
afd_patched.sys - Overall Similarity Score:
0.6712(67.12%) - Diff Statistics:
- Matched Functions: 1170
- Identical Functions: 254
- Changed Functions: 916
- Unmatched (Unpatched/Patched): 0 / 0
- Verdict: The dominant security-relevant change is a driver-wide rollout of reference-count integrity checks. Plain interlocked reference-count increments were replaced with checked 64-bit interlocked increments that immediately bugcheck (
__fastfail(0xE), FAST_FAIL_INVALID_REFERENCE_COUNT) when the resulting count is not greater than 1, i.e. when code takes a new reference on an object whose count was already 0. This is a defense-in-depth mitigation against reference-count-based use-after-free. The unpatched build has this check at 2 sites; the patched build has it at 227. No out-of-bounds read/write bug was found in the buffer-handling paths: the AFD accept/listen data path bounds checks are logically equivalent between the two builds.
2. Vulnerability Summary
Finding 1: AfdServiceWaitForListen — buffer validation is unchanged; only the reference-count integrity check was added
- Severity: N/A (no buffer bug); the only behavioral change is defense-in-depth hardening (see Finding 2)
- Vulnerability Class: None demonstrable from the diff (buffer validation unchanged)
- Affected Function:
AfdServiceWaitForListen @ 0x1C00197C0(patched@ 0x140043C80) - What actually changed: The bounds-check block was refactored from a
goto-based sequence into one compound conditional, but the conditions are logically identical in both builds. When endpoint flag bit0x100is clear, both builds check only the primary size field (*(a2+0xa0) <= Parameters.Create.Options) and do not check the secondary field; when bit0x100is set, both builds additionally check*(a2+0xa0)+6and, if the secondary maximum is non-zero,*(a2+0xa8)+6. Both builds read the copy source from the same indirect pointer*(a2+0x98)with length*(*(a2+0x98)+4)+2. The struct field offsets shifted (the endpoint context grew), which accounts for most of the byte-level diff. The one behavioral addition is the reference-count integrity fastfail described in Finding 2, which this function also received. - Note: An earlier hypothesis of an out-of-bounds write via a skipped secondary-buffer check is not supported by the binaries. The "unvalidated secondary buffer" branch exists identically in the patched build, and the copy already uses the validated
*(a2+0x98)pointer in both builds.
Finding 2: Reference-count integrity fastfail added to interlocked increments (use-after-free hardening)
- Severity: Medium (defense-in-depth hardening; no specific exploitable root cause is visible in the diff)
- Vulnerability Class: Use-after-free / reference-count integrity (CWE-416)
- Affected Functions (examples):
AfdServiceWaitForListen @ 0x1C00197C0,AfdRestartBufferReceiveWithUserIrp @ 0x1C0005A10,AfdTLTPacketsSend @ 0x1C003EFE8, and ~225 further sites across the driver - Root Cause / Mechanism: In the unpatched build, taking a new reference on an AFD object is a plain 32-bit interlocked increment (
lock inc dword ptr [obj+off]) with no validation of the result. If a reference is taken on an object whose reference count has already reached 0 (a use-after-free / reference-count-underflow condition), the unpatched code silently revives the dangling object and continues. The patched build performs the increment as a 64-bit interlocked add and then checks the result: if the new count is not greater than 1, it executes__fastfail(0xE)(int 29hwithecx = 0xE, FAST_FAIL_INVALID_REFERENCE_COUNT), halting the machine before the corrupted object can be used. - Entry Point & Data Flow:
- An AFD object (endpoint / connection / receive-repost context) reaches reference count 0 through a teardown path.
- A concurrent path (pending I/O completion, receive repost, or accept/listen servicing) takes a fresh reference on that object.
- Unpatched: the increment succeeds silently and the object is used after free.
- Patched: the checked increment observes a resulting count of 1 or less and bugchecks via
__fastfail(0xE). - Honest scope: This is a systematic mitigation applied broadly (227 sites), not a single targeted logic fix. The diff shows the guard being added, but it does not by itself prove a reachable, attacker-controllable underflow. Treat this as reference-count hardening rather than a demonstrated exploitable primitive.
3. Pseudocode Diff
Bounds check in AfdServiceWaitForListen — unchanged logic (refactor only)
// === UNPATCHED (0x1C00197C0) ===
if ( (*(_DWORD *)(FsContext + 8) & 0x100) == 0 ) // flag bit 0x100 clear
{
if ( *(_DWORD *)(a2 + 0xA0) <= CurrentStackLocation->Parameters.Create.Options )
goto MAP_AND_COPY; // primary only; secondary not checked
goto ERROR; // -1073741789 (0xC0000023)
}
if ( *(_DWORD *)(a2 + 0xA0) + 6 > CurrentStackLocation->Parameters.Create.Options )
goto ERROR;
LowPart = CurrentStackLocation->Parameters.Read.ByteOffset.LowPart; // secondary maximum
if ( LowPart != 0 && *(_DWORD *)(a2 + 0xA8) + 6 > LowPart )
goto ERROR;
MAP_AND_COPY:
v15 = AfdMapMdlChain(Irp->MdlAddress);
// ... copy uses *(a2+0x98) as source in both builds ...
memmove(dst, (const void *)(*(_QWORD *)(a2 + 0x98) + 6), *(_WORD *)(*(_QWORD *)(a2 + 0x98) + 4) + 2);
// === PATCHED (0x140043C80) ===
// Same three conditions, expressed as one compound predicate (logically identical):
if ( ((flags & 0x100) != 0 || *(a2 + 0xA0) <= max_primary)
&& ((flags & 0x100) == 0 || *(a2 + 0xA0) + 6 <= max_primary)
&& ((flags & 0x100) == 0 || (v9 = max_secondary) == 0 || *(a2 + 0xA8) + 6 <= v9) )
{
v10 = AfdMapMdlChain(Irp->MdlAddress);
// ... identical copy: source *(a2+0x98)+6, length *(*(a2+0x98)+4)+2 ...
}
else
return STATUS_INVALID_PARAMETER; // 0xC0000023
Reference-count integrity check in AfdRestartBufferReceiveWithUserIrp
// === UNPATCHED (0x1C0005A10) ===
*(_DWORD *)(a3 + 4) |= 0x10000000u;
_InterlockedIncrement((volatile signed __int32 *)(a3 + 0x30)); // 32-bit, result ignored
AfdLRListAddItem((PSLIST_ENTRY)(a3 + 0xC0));
// === PATCHED (0x14000C0B0) ===
*(_DWORD *)(a3 + 4) |= 0x10000000u;
if ( _InterlockedIncrement64((volatile signed __int64 *)(a3 + 0x30)) <= 1 )
__fastfail(0xEu); // FAST_FAIL_INVALID_REFERENCE_COUNT
AfdLRListAddItem((PSLIST_ENTRY)(a3 + 0xC0));
The same increment-then-check transformation appears in AfdServiceWaitForListen (field +0x40, was +0x38) and twice in AfdTLTPacketsSend (fields +0x30 and +0x40, the latter was +0x38). The field-offset shifts reflect struct-layout growth, not a logic change.
4. Assembly Analysis
AfdRestartBufferReceiveWithUserIrp — the reference-count fastfail
// === UNPATCHED AfdRestartBufferReceiveWithUserIrp @ 0x1C0005A10 ===
00000001C002290E mov [rdi+4], r8d
00000001C0022912 lock inc dword ptr [rdi+30h] ; refcount += 1 (32-bit, unchecked)
00000001C0022916 lea rcx, [rdi+0C0h] ; ListEntry
00000001C002291D lea rdx, AfdLRRepostReceive
00000001C0022924 call AfdLRListAddItem
// === PATCHED AfdRestartBufferReceiveWithUserIrp @ 0x14000C0B0 ===
000000014000C249 mov rax, rsi ; rsi = 1
000000014000C24C mov [rbx+4], edx
000000014000C24F lock xadd [rbx+30h], rax ; rax = old refcount; refcount += 1 (64-bit)
000000014000C255 add rax, rsi ; rax = new refcount
000000014000C258 cmp rax, rsi ; new count vs 1
000000014000C25B jg short loc_14000C264 ; new > 1 -> continue
000000014000C25D mov ecx, 0Eh ; FAST_FAIL_INVALID_REFERENCE_COUNT
000000014000C262 int 29h ; __fastfail(0xE)
000000014000C264 lea rcx, [rbx+0C0h] ; ListEntry
000000014000C26B lea rdx, AfdLRRepostReceive
000000014000C272 call AfdLRListAddItem
AfdServiceWaitForListen — same transformation
// === UNPATCHED AfdServiceWaitForListen @ 0x1C00197C0 ===
00000001C002D7B4 lock inc dword ptr [r14+38h] ; refcount += 1 (32-bit, unchecked)
00000001C002D7B9 lea rcx, [r14+80h]
00000001C002D7C0 mov rdx, [rcx+8]
// === PATCHED AfdServiceWaitForListen @ 0x140043C80 ===
0000000140043F69 mov rax, r12 ; r12 = 1
0000000140043F6C lock xadd [r14+40h], rax ; rax = old refcount; refcount += 1 (64-bit)
0000000140043F72 add rax, r12 ; rax = new refcount
0000000140043F75 cmp rax, r12 ; new count vs 1
0000000140043F78 jg short loc_140043F81 ; new > 1 -> continue
0000000140043F7A mov ecx, 0Eh ; FAST_FAIL_INVALID_REFERENCE_COUNT
0000000140043F7F int 29h ; __fastfail(0xE)
0000000140043F81 lea rcx, [r14+90h]
AfdTLTPacketsSend — same transformation plus allocation API update
// === UNPATCHED AfdTLTPacketsSend @ 0x1C003EFE8 ===
00000001C003F09B lock inc dword ptr [r12+38h] ; refcount += 1 (32-bit, unchecked)
00000001C003F0A6 lock inc dword ptr [rax+30h] ; refcount += 1 (32-bit, unchecked)
00000001C003F116 call cs:__imp_ExAllocatePoolWithQuotaTag ; ntoskrnl (size 0x30, tag 'AfdF')
// === PATCHED AfdTLTPacketsSend @ 0x140053CE8 ===
0000000140053DC6 lock xadd [r13+40h], rax ; checked increment -> __fastfail(0xE) on <= 1
0000000140053DD7 int 29h
0000000140053DDE lock xadd [rcx+30h], rax ; checked increment
0000000140053DEC mov ecx, 0Eh
0000000140053DF1 int 29h
0000000140053E6F call cs:__imp_ExAllocatePool2 ; ntoskrnl (size 0x30/48, tag 'AfdF')
The allocation change in AfdTLTPacketsSend is ExAllocatePoolWithQuotaTag(NonPagedPool|Quota, 0x30, 'AfdF') becoming ExAllocatePool2(0x61, 48, 'AfdF'). The size (0x30 = 48) and pool tag (0x46646641 = 'AfdF') are identical; this is the standard modernization to the ExAllocatePool2 API, not a size correction.
5. Trigger Conditions
Reaching the reference-count fastfail
The fastfail fires only if an AFD object's reference count is already at or below 0 at the moment a new reference is taken. From user mode this requires driving the driver into a reference-count underflow, which is not demonstrated by the diff. A plausible race to probe:
1. Create a TCP socket via WSASocket(AF_INET, SOCK_STREAM, 0) and connect/bind it.
2. Issue an overlapped WSARecv (mapped to AFD_RECV) so a receive-repost context is live.
3. From a second thread, call closesocket() while the receive is pending, racing endpoint teardown against AfdRestartBufferReceiveWithUserIrp / AfdTLTPacketsSend reference operations.
4. On a patched machine, if the race drives the count to 0 before a reference is re-taken, the checked increment executes __fastfail(0xE) and the machine bugchecks (0x139 KERNEL_SECURITY_CHECK_FAILURE with fastfail code 0xE). On an unpatched machine the same race would silently continue with a revived (dangling) object.
Note: reproducing this deterministically is not established here; the guard is defense-in-depth.
AFD accept/listen data path (AfdServiceWaitForListen)
No buffer-validation bug was found in this path. The accept/listen data delivery reached via NtDeviceIoControlFile → AfdWaitForListen → AfdServiceWaitForListen performs the same bounds checks and the same MDL-mapped copy in both builds.
6. Exploit Primitive & Development Notes
- Primitive: None is provided by the diff. The reference-count fastfail is a mitigation: it converts a would-be use-after-free (reference taken on a freed object) into an immediate, non-exploitable bugcheck. It does not by itself expose an out-of-bounds write or a controllable free.
- What a researcher would still need to confirm:
- Whether any AFD reference count can actually be driven to 0 while a concurrent path still takes a reference (the underflow precondition). The diff shows the guard, not a reachable trigger.
- Which specific object types (endpoint, connection, receive-repost context) are reachable with attacker-controlled timing.
- Mitigation nature: The
__fastfail(0xE)/ FAST_FAIL_INVALID_REFERENCE_COUNT pattern is a runtime integrity check. It stops exploitation of a reference-count underflow at the point of detection; it does not remove a root-cause logic defect, because none is visible in this diff.
7. Debugger PoC Playbook
Attach a kernel debugger (WinDbg/KD) to the target machine.
Observing the reference-count check
- Breakpoints:
text bp afd+0xC24F ; patched AfdRestartBufferReceiveWithUserIrp checked increment (0x14000C24F) bp afd+0xC262 ; the int 29h fastfail site (0x14000C262)(On the unpatched build the corresponding increment is atAfdRestartBufferReceiveWithUserIrp + 0x22912,lock inc dword ptr [rdi+30h], with no following check.) - What to Inspect:
- At the
lock xadd [rbx+30h], raxsite: read[rbx+0x30]before the increment. A value of0(or negative) means the next instruction sequence will takerax/new-count<= 1and route toint 29h. - At the fastfail:
ecx = 0xEconfirms FAST_FAIL_INVALID_REFERENCE_COUNT. The resulting bugcheck is0x139(KERNEL_SECURITY_CHECK_FAILURE) with subcode0xE. - Trigger Setup:
- Run a tight loop of overlapped
WSARecvimmediately followed byclosesocket()from separate threads across many sockets to maximize the teardown-vs-repost race window. - Expected Observation:
- On patched: an immediate
__fastfail-driven bugcheck (0x139, subcode0xE) if and only if the race drives a reference count to0before a new reference is taken. - On unpatched: no fastfail; the reference count is silently incremented from
0, leaving a revived object that may be used after free later.
Comparing the accept/listen data path
- Breakpoints:
text bp afd!AfdServiceWaitForListen ; entry, both builds - What to Inspect:
- Confirm the bounds checks and the
memmovesource (*(a2+0x98)+6) and length (*(*(a2+0x98)+4)+2) are the same in both builds. This path is unchanged apart from the reference-count fastfail and struct-offset shifts.
8. Changed Functions — Full Triage
(Note: Many functions changed only by register reassignment, struct-offset shifts from context-structure growth, or the reference-count fastfail rollout. Those are grouped accordingly.)
Reference-count integrity hardening (security-relevant)
AfdServiceWaitForListen @ 0x1C00197C0(Sim: 0.9073): Bounds-check block refactored to an equivalent compound conditional (no logic change); reference-count increment at field+0x38became a checked 64-bit increment at+0x40with__fastfail(0xE). No OOB write present.AfdRestartBufferReceiveWithUserIrp @ 0x1C0005A10(Sim: 0.8368): Reference-count increment at[obj+0x30]changed fromlock inc(32-bit) tolock xadd(64-bit) with a__fastfail(0xE)guard when the new count is<= 1.AfdTLTPacketsSend @ 0x1C003EFE8(Sim: 0.827): Two reference-count increments (+0x30,+0x38→+0x40) hardened with__fastfail(0xE); allocation modernized fromExAllocatePoolWithQuotaTag(0x30, 'AfdF')toExAllocatePool2(0x61, 48, 'AfdF')— same size (0x30) and tag ('AfdF').WskKnrCompletePending @ 0x1C0013068(Sim: 0.9054, Behavioral): Reference-count decrement handling restructured (fields consolidated); the driver-wide fastfail pattern is consistent with the rest of the patch. No independent exploitable change identified.
Other changed functions (behavioral / cosmetic)
AfdRioCreateRqPair @ 0x1C0048484(Sim: 0.9023): Registered-I/O buffer registration (MDL allocate + probe/lock + map). UsesAfdRioCheckRqBufferSize @ 0x1C0047030to validatecount*0x48+0x10 == total_sizewithcount <= 0x38E38E2(integer-overflow guard). Error paths were consolidated; no new bug identified.AfdFastTransmitFile @ 0x1C003D7B4(Sim: 0.9225, Behavioral): Fast-path file send usingFsRtlCopyRead/FsRtlMdlRead. Sums three sizes and uses an int-sized allocation length on the buffered path; the large-transfer path uses MDL-based I/O. No security-relevant change confirmed in the diff.AfdRioCheckRqBufferSize @ 0x1C0047030(Sim: 0.9524, Cosmetic): Integer-overflow guard (arg1 * 0x48 + 0x10 == arg2,arg1 <= 0x38E38E2). Equivalent logic in both builds.AFDETW_TRACERECVDATAGRAM @ 0x1C000AC10(Sim: 0.9077, Cosmetic): ETW trace logging; register/data-reference changes only.WskTdiCreateAO @ 0x1C00751B0(Sim: 0.9526, Behavioral): Device file creation viaIoCreateFileExwith an ECP list; allocation wrapper changed. No security-relevant change identified.AfdCommonDelAddressHandler @ 0x1C00668D0(Sim: 0.9561, Cosmetic): List management; register renaming, equivalent logic.
9. Unmatched Functions
There were no unmatched functions in either build. All modifications were in-place, consistent with an iterative patch (a broad reference-count-integrity hardening pass plus API modernization) rather than an architectural refactor.
10. Confidence & Caveats
- Confidence Level: High for the description of the change (reference-count increments hardened with
__fastfail(0xE)at multiple sites in both the disassembly and decompilation). Low for any claim of a concrete exploitable primitive, because the diff shows only the guard being added, not a reachable underflow. - Corrections to earlier hypotheses:
- The reported out-of-bounds write in
AfdServiceWaitForListenis not supported by the binaries: the bounds checks are logically identical between builds and the copy source is the same validated pointer in both. - The reported "non-atomic decrement / double-free" mechanism is not present. Both builds use atomic operations. The real change is a checked increment with
__fastfail(0xE), not a decrement guard, and the fastfail code is0xE, not0xD. - The
AfdTLTPacketsSendallocation change does not fix an undersized allocation; size and tag are unchanged. - Verification Required:
- To upgrade Finding 2 beyond hardening, demonstrate a reachable reference-count underflow for a specific AFD object under attacker-influenced timing.