afd.sys — Pending IRPs left queued on a recycled connection during AFD SAN teardown (CWE-416) fixed
KB5078752
1. Overview
| Item | Value |
|---|---|
| Unpatched binary | afd_unpatched.sys |
| Patched binary | afd_patched.sys |
| Overall similarity | 0.9835 (16 changed of 1179 matched) |
| Identical functions | 1163 |
| Changed functions | 16 |
| Unmatched (added/removed) | 0 / 0 |
Verdict. The patch adds a feature-staged correctness fix to the AFD "San" connection cancel/teardown path. In the unpatched build, AfdSanCancelAccept cancels one IRP and then resets the connection object without draining the other IRPs still queued on the connection's pending-IRP list at conn+0x70. Those siblings are left linked on a list whose head is about to be zeroed, on a connection object that is reset and recycled — a use-after-free / dangling-pointer condition (CWE-416) with a teardown race (CWE-362). The patch adds a drain loop, a teardown flag at conn+0x91, and matching gates in the send/receive and connection-creation paths. Every one of these changes is gated behind the Windows feature g_Feature_556471608_60363862; when that feature is disabled the patched binary still executes the original (unsafe) path, so this is a staged rollout rather than an unconditional fix. The connection is reset/recycled in place, not freed through ExFreePool, so the demonstrable impact is a list-integrity bugcheck / operation on a recycled object, not a proven arbitrary read/write primitive.
2. Vulnerability Summary
Finding 1 — Medium: pending IRPs not drained on teardown (AfdSanCancelAccept)
| Field | Value |
|---|---|
| Severity | Medium |
| Class | Use-After-Free of a recycled object (CWE-416) + teardown race (CWE-362) |
| Function (unpatched) | AfdSanCancelAccept @ 0x1C007E110 (cancel routine for AFD San accept IRPs) |
| Function (patched) | AfdSanCancelAccept @ 0x1C007F120 (relocated) |
| Feature gate | g_Feature_556471608_60363862 via EvaluateCurrentState (old path retained when disabled) |
| Entry point | NtDeviceIoControlFile on \Device\Afd, AFD San accept/redirect IOCTL path |
Root cause. When an AFD San accept IRP is cancelled, AfdSanCancelAccept runs as the IRP's cancel routine. It:
1. Acquires the connection spinlock at conn+0x30.
2. Unlinks the current IRP from the connection's pending-IRP list (the IRP's own list entry is at irp+0xA8).
3. If the connection is not already marked closed (conn+0x08 & 0x800), it drops the endpoint reference held at conn+0x50 (AfdDereferenceEndpointInline), then memsets 0x48 bytes starting at conn+0x50, clears the state word at conn+0x08, and rewrites the type tag at conn+0x00 to 0xAFD0 (the recycled/free marker) with the byte at conn+0x02 set to 1.
4. Completes only the current IRP with STATUS_CANCELLED (0xC0000120).
The problem: the pending-IRP list head lives at conn+0x70/+0x78, which is inside the memset range (conn+0x50 .. conn+0x98). Any other IRPs that were queued on that list (their entries at irp+0xA8) are never unlinked or completed. After the memset zeroes the list head and the type tag marks the connection recycled, those sibling IRPs still point at the connection. When one of them is later cancelled or completed, its cancel routine (AfdSanCancelRequest @ 0x1C007E3A0) re-acquires the connection lock and manipulates list linkage on the zeroed head / recycled object → list-integrity __fastfail (int 29h) bugcheck, or operation on a connection object that has been reset and may have been re-handed-out by the AFD SLIST allocator.
What the patch does. The patched AfdSanCancelAccept @ 0x1C007F120, when g_Feature_556471608_60363862 is enabled:
1. Sets a teardown flag at conn+0x91 = 1 before touching the list.
2. Walks the entire pending-IRP list at conn+0x70, unlinking every entry into a local drain chain (linked through the IRP list-entry fields); the connection is not reset in this first locked pass.
3. Releases the spinlock, then completes each drained IRP with STATUS_CANCELLED (0xC0000120) outside the lock (clearing each IRP's cancel routine and synchronising with the cancel spinlock first).
4. Re-acquires the connection spinlock a second time and only then runs the endpoint-deref + memset reset sequence.
When the feature is disabled, the patched routine falls through the else branch to the exact original sequence (endpoint deref, memset at conn+0x50, type reset) with no drain — identical to the unpatched behaviour.
Entry point & call chain.
1. A Winsock operation issues NtDeviceIoControlFile on \Device\Afd reaching the AFD San accept path.
2. AfdSanAcceptCore @ 0x1C007D0B8 installs AfdSanCancelAccept as the accept IRP's CancelRoutine (lea rax, AfdSanCancelAccept at 0x1C007D2B5).
3. When the IRP is cancelled (endpoint closed/aborted), AfdSanCancelAccept runs and performs the teardown described above.
Finding 2 — Low: missing teardown-flag gate in send/receive insert paths
| Field | Value |
|---|---|
| Severity | Low (race-window amplifier for Finding 1; same feature gate) |
| Class | Missing state check / TOCTOU (CWE-362) |
| Functions (unpatched) | AfdSanFastCompleteRequest @ 0x1C007FE70, AfdSanRedirectRequest @ 0x1C0081074 |
| Functions (patched) | AfdSanFastCompleteRequest @ 0x1C0080F80, AfdSanRedirectRequest @ 0x1C0082238 (both relocated) |
| Patched behavior | Before inserting an IRP into the pending list at conn+0x70, they now additionally require !EvaluateCurrentState(g_Feature_556471608_60363862) || conn+0x91 == 0. |
The unpatched insert paths queue an IRP into conn+0x70 after only checking the IRP-cancelled flag (irp+0x68). They have no notion of a teardown flag (none exists in the unpatched build). The patched paths add the conn+0x91 teardown-flag check so that, once teardown has begun, new IRPs are not queued onto a connection that is being drained. Both the relocated cancel routine reference (AfdSanCancelRequest, relocated 0x1C007E3A0 → 0x1C007F4D0) and this gate belong to the same staged feature.
Finding 3 — Low: device-attachment validation in connection creation (AfdCreateConnection)
| Field | Value |
|---|---|
| Severity | Low (defense-in-depth; same feature gate, old path retained) |
| Class | Improper input validation (CWE-20) |
| Function (unpatched) | AfdCreateConnection @ 0x1C0055AB0 |
| Function (patched) | AfdCreateConnection @ 0x1C0055BF0 (new); original logic retained as AfdCreateConnection_Old @ 0x1C0007DE4 |
| Patched behavior | The new routine adds IoGetDeviceAttachmentBaseRef-based device-attachment validation (the count of IoGetDeviceAttachmentBaseRef call sites rises from 3 to 5 across the binary) plus DesiredAccess/Options checks. On mismatch it fails the request. |
This is a hardening of connection creation, not the primary teardown issue. It is gated behind the same feature; when the feature is disabled the retained AfdCreateConnection_Old runs instead. Note that address 0x1C0007DE4 holds a different function in the unpatched build (rbc_InitializeFeatureStaging); AfdCreateConnection_Old is the relocated pre-patch connection-creation logic and must be matched by content, not by that reused address.
3. Pseudocode Diff
AfdSanCancelAccept (0x1C007E110 → 0x1C007F120)
// ====== UNPATCHED AfdSanCancelAccept @ 0x1C007E110 ======
void AfdSanCancelAccept(void* a1, IRP* a2) {
void* ctx = a2->Tail.Overlay.CurrentStackLocation->...NamedPipeType; // context
conn = ctx[3]; // *(ctx+0x18)
KeAcquireInStackQueuedSpinLockAtDpcLevel(conn+0x30, &LockHandle);
// Unlink ONLY the current IRP from the pending list (entry at irp+0xA8)
Flink = a2->Tail.Overlay.ListEntry.Flink;
if (Flink) {
// integrity-checked RemoveEntryList of the current IRP
Blink->Flink = Flink; Flink->Blink = Blink;
if ((*(uint*)(conn+0x08) & 0x800) == 0) { // not already closed
// *** NO DRAIN OF THE OTHER PENDING IRPs ***
AfdDereferenceEndpointInline(*(void**)(conn+0x50)); // drop endpoint ref
memset(conn+0x50, 0, 0x48); // zeroes conn+0x50..0x98,
// INCLUDING the list head at +0x70/+0x78
*(uint*)(conn+0x08) = 0;
*(ushort*)conn = 0xAFD0; // recycled/free marker
*(byte*)(conn+0x02) = 1;
}
KeReleaseInStackQueuedSpinLockFromDpcLevel(&LockHandle);
IoReleaseCancelSpinLock(a2->CancelIrql);
_InterlockedExchange(conn+0x158, 0);
ObfDereferenceObject(ctx); // deref the context object
a2->IoStatus.Status = STATUS_CANCELLED; // 0xC0000120
IofCompleteRequest(a2, AfdPriorityBoost); // completes ONLY this IRP
// *** any other IRPs still on conn+0x70 now dangle on a zeroed head ***
} else {
KeReleaseInStackQueuedSpinLockFromDpcLevel(&LockHandle);
IoReleaseCancelSpinLock(a2->CancelIrql);
}
}
// ====== PATCHED AfdSanCancelAccept @ 0x1C007F120 ======
void AfdSanCancelAccept(void* a1, IRP* a2) {
conn = ctx[3];
KeAcquireInStackQueuedSpinLockAtDpcLevel(conn+0x30, &LockHandle);
// ... identical integrity-checked RemoveEntryList of the current IRP ...
void* drain = NULL;
if ((*(uint*)(conn+0x08) & 0x800) == 0) {
if (EvaluateCurrentState(&g_Feature_556471608_60363862)) {
*(byte*)(conn+0x91) = 1; // << NEW teardown flag
// << NEW: drain EVERY IRP off the list at conn+0x70 into 'drain'
for (head = conn+0x70; *head != head; ) {
entry = *head;
// integrity-checked unlink of 'entry'
*head = entry->Flink; entry->Flink->Blink = head;
entry->Flink = NULL; entry->Blink = drain; drain = entry;
}
} else {
// OLD path retained (feature disabled): reset immediately, no drain
AfdDereferenceEndpointInline(*(void**)(conn+0x50));
memset(conn+0x50, 0, 0x48);
*(uint*)(conn+0x08) = 0; *(ushort*)conn = 0xAFD0; *(byte*)(conn+0x02) = 1;
}
}
KeReleaseInStackQueuedSpinLockFromDpcLevel(&LockHandle);
IoReleaseCancelSpinLock(a2->CancelIrql);
if (EvaluateCurrentState(&g_Feature_556471608_60363862)) {
// << NEW: complete every drained IRP outside the lock, clearing its
// cancel routine and syncing with the cancel spinlock first.
while (drain) {
IRP* p = (IRP*)((char*)drain - 0xA8);
drain = drain->Blink;
if (_InterlockedExchange64(&p->CancelRoutine, 0) == 0) {
IoAcquireCancelSpinLock(&Irql); IoReleaseCancelSpinLock(Irql);
}
p->IoStatus.Information = 0;
p->IoStatus.Status = STATUS_CANCELLED; // 0xC0000120
IofCompleteRequest(p, AfdPriorityBoost);
}
// << NEW: re-acquire the lock and only NOW reset the connection.
KeAcquireInStackQueuedSpinLock(conn+0x30, &LockHandle);
if ((*(uint*)(conn+0x08) & 0x800) == 0) {
AfdDereferenceEndpointInline(*(void**)(conn+0x50));
memset(conn+0x50, 0, 0x48);
*(uint*)(conn+0x08) = 0; *(ushort*)conn = 0xAFD0; *(byte*)(conn+0x02) = 1;
}
KeReleaseInStackQueuedSpinLock(&LockHandle);
}
_InterlockedExchange(conn+0x158, 0);
ObfDereferenceObject(ctx);
a2->IoStatus.Status = STATUS_CANCELLED;
IofCompleteRequest(a2, AfdPriorityBoost);
}
AfdSanFastCompleteRequest / AfdSanRedirectRequest (teardown-flag gate)
// UNPATCHED (insert into pending list at conn+0x70)
KeAcquireInStackQueuedSpinLock(conn+0x30, &LockHandle);
_InterlockedExchange64(&irp->CancelRoutine, AfdSanCancelRequest);
if (*(byte*)(irp+0x44) == 0) { // IRP not already cancelled
InsertTailList(conn+0x70, irp+0xA8); // unconditional insert
}
// PATCHED (adds the teardown-flag gate under the same feature)
if (*(byte*)(irp+0x44) == 0
&& (!EvaluateCurrentState(&g_Feature_556471608_60363862)
|| *(byte*)(conn+0x91) == 0)) { // reject if teardown in progress
InsertTailList(conn+0x70, irp+0xA8);
}
4. Assembly Analysis
Unpatched AfdSanCancelAccept @ 0x1C007E110 — full listing with inline annotations
; Prologue
00000001C007E110 mov r11, rsp
00000001C007E113 mov [r11+8], rbx
00000001C007E117 mov [r11+10h], rbp
00000001C007E11B mov [r11+18h], rsi
00000001C007E11F push rdi
00000001C007E120 sub rsp, 40h
; rdx = IRP. Resolve context and connection object.
00000001C007E124 mov rax, [rdx+0B8h] ; IRP->Tail.Overlay
00000001C007E12B mov rsi, rdx ; rsi = IRP
00000001C007E12E lea rdx, [r11-28h] ; LockHandle (stack)
00000001C007E132 mov rbp, [rax+20h] ; rbp = context object
00000001C007E136 mov rdi, [rbp+18h] ; rdi = connection object
00000001C007E13A lea rcx, [rdi+30h] ; &conn->SpinLock (conn+0x30)
00000001C007E13E call cs:__imp_KeAcquireInStackQueuedSpinLockAtDpcLevel
; Integrity-checked RemoveEntryList of the CURRENT IRP (entry at irp+0xA8)
00000001C007E14A lea rax, [rsi+0A8h] ; &irp->list_entry
00000001C007E151 mov rdx, [rax] ; Flink
00000001C007E154 test rdx, rdx
00000001C007E157 jz loc_1C007E230 ; null → early release
00000001C007E15D cmp [rdx+8], rax ; Flink->Blink == us?
00000001C007E161 jnz loc_1C007E229 ; corrupt → __fastfail
00000001C007E167 mov rcx, [rax+8] ; Blink
00000001C007E16B cmp [rcx], rax ; Blink->Flink == us?
00000001C007E16E jnz loc_1C007E229
00000001C007E174 mov [rcx], rdx ; Blink->Flink = Flink
00000001C007E177 mov [rdx+8], rcx ; Flink->Blink = Blink
00000001C007E17B test dword ptr cs:WPP_MAIN_CB.Queue, 40000h ; WPP trace flag
00000001C007E185 jz short loc_1C007E19E
; Is the connection already closed?
00000001C007E19E test dword ptr [rdi+8], 800h
00000001C007E1A5 jnz short loc_1C007E1CF ; if closed, skip reset
; ===== NO DRAIN OF SIBLING IRPs — straight to connection reset =====
00000001C007E1A7 mov rcx, [rdi+50h] ; endpoint pointer at conn+0x50
00000001C007E1AB call AfdDereferenceEndpointInline ; drop endpoint reference
00000001C007E1B0 xor edx, edx
00000001C007E1B2 lea rcx, [rdi+50h] ; dst = conn+0x50
00000001C007E1B6 lea r8d, [rdx+48h] ; size = 0x48 (covers +0x50..+0x98)
00000001C007E1BA call memset ; zeroes list head at +0x70/+0x78 too
00000001C007E1BF and dword ptr [rdi+8], 0 ; state = 0
00000001C007E1C3 mov eax, 0AFD0h
00000001C007E1C8 mov [rdi], ax ; type = 0xAFD0 (recycled marker)
00000001C007E1CB mov byte ptr [rdi+2], 1
; ===== other IRPs on the conn+0x70 list now dangle on a zeroed head =====
00000001C007E1CF lea rcx, [rsp+48h+LockHandle]
00000001C007E1D4 call cs:__imp_KeReleaseInStackQueuedSpinLockFromDpcLevel
00000001C007E1E0 mov cl, [rsi+45h] ; IRP cancel IRQL
00000001C007E1E3 call cs:__imp_IoReleaseCancelSpinLock
00000001C007E1EF xor eax, eax
00000001C007E1F1 mov rcx, rbp ; rcx = context object
00000001C007E1F4 xchg eax, [rdi+158h] ; clear field at conn+0x158
00000001C007E1FA call cs:__imp_ObfDereferenceObject ; deref context (not conn)
00000001C007E206 mov dl, cs:AfdPriorityBoost
00000001C007E20C mov rcx, rsi ; rcx = current IRP
00000001C007E20F and qword ptr [rsi+38h], 0 ; Information = 0
00000001C007E214 mov dword ptr [rsi+30h], 0C0000120h ; STATUS_CANCELLED
00000001C007E21B call cs:__imp_IofCompleteRequest ; completes ONLY this IRP
00000001C007E227 jmp short loc_1C007E250
; List-corruption fail-fast:
00000001C007E229 mov ecx, 3
00000001C007E22E int 29h
; Null-Flink early release:
00000001C007E230 lea rcx, [rsp+48h+LockHandle]
00000001C007E235 call cs:__imp_KeReleaseInStackQueuedSpinLockFromDpcLevel
00000001C007E241 mov cl, [rsi+45h]
00000001C007E244 call cs:__imp_IoReleaseCancelSpinLock
00000001C007E250 mov rbx, [rsp+48h+arg_0]
00000001C007E255 mov rbp, [rsp+48h+arg_8]
00000001C007E25A mov rsi, [rsp+48h+arg_10]
00000001C007E25F add rsp, 40h
00000001C007E263 pop rdi
00000001C007E264 retn
What the patch changes (patched AfdSanCancelAccept @ 0x1C007F120). After the same "already closed" check, the patched routine evaluates g_Feature_556471608_60363862 (call EvaluateCurrentState at 0x1C007F1C7). If the feature is enabled it sets conn+0x91 = 1 (0x1C007F1D0) and runs a drain loop over the list head at conn+0x70 (0x1C007F1D7–0x1C007F20C), unlinking every entry into a local chain without resetting the connection in that pass. If the feature is disabled it branches to loc_1C007F20E, which is the original endpoint-deref + memset(conn+0x50, 0, 0x48) + type-reset sequence — identical to the unpatched code. After releasing the first spinlock it re-evaluates the feature (0x1C007F251); when enabled it completes each drained IRP with STATUS_CANCELLED outside the lock (0x1C007F26D–0x1C007F2C5), then re-acquires the connection spinlock via KeAcquireInStackQueuedSpinLock (0x1C007F2D0) and only then runs the reset sequence (0x1C007F2E5–0x1C007F308). Ordering when enabled: drain → release lock → complete siblings → re-acquire lock → reset.
5. Trigger Conditions
- Open an AFD endpoint and reach the AFD San accept path (
AfdSanAcceptCore @ 0x1C007D0B8), which installsAfdSanCancelAcceptas the accept IRP's cancel routine. - Queue more than one IRP on the connection's pending list at
conn+0x70. The insert pathsAfdSanFastCompleteRequest/AfdSanRedirectRequestadd IRPs (entry atirp+0xA8) to this list. - Cancel one queued IRP (endpoint close/abort) so
AfdSanCancelAcceptruns while at least one sibling IRP is still on the list. In the unpatched build (and in the patched build withg_Feature_556471608_60363862disabled) the connection is reset without draining the siblings. - Observable effect. When a surviving sibling IRP is subsequently cancelled/completed,
AfdSanCancelRequest @ 0x1C007E3A0re-acquires the connection lock and operates on list linkage whose head (conn+0x70/+0x78) was zeroed by the resetmemset, on a connection whose type tag is now0xAFD0. The integrity checks in that path (cmp [Flink+8], entryetc.) fail, driving a__fastfail(3)/int 29hbugcheck. If the reset connection has already been re-handed-out by the AFD SLIST allocator, the sibling operates on a different live object.
Because this is a cancel/teardown race across two IRPs on the same connection, reliable triggering requires racing a cancel against at least one still-queued sibling IRP.
6. Impact Assessment
Primitive. The unpatched teardown leaves sibling IRPs linked on a connection object that has been reset in place: the endpoint reference at conn+0x50 is dropped, the 0x48-byte region at conn+0x50 (including the pending-list head at conn+0x70/+0x78) is zeroed, and the type tag is set to the recycled marker 0xAFD0. The object is not freed to the pool in this routine — there is no ExFreePool here; it is recycled in place and returned to the AFD connection allocator for reuse.
Demonstrable impact.
- A subsequent cancel/completion of a stale sibling IRP hits the list-integrity checks in AfdSanCancelRequest on a zeroed list head, producing a __fastfail/int 29h bugcheck — a reliable local denial of service.
- Because the connection is recycled rather than freed, a stale IRP can also end up operating on a re-issued connection object under lock (use of a recycled object, CWE-416). Turning that into a controlled read/write would require winning the reallocation race and controlling the reused object's contents; that is not demonstrable from these binaries and is not claimed here.
Why Medium, not Critical.
- The fix is feature-staged: the drain, the teardown flag, and the device-attachment check are all gated behind g_Feature_556471608_60363862. With the feature disabled the patched binary still runs the original unsafe path, so the vulnerable behaviour is not unconditionally removed.
- The object is reset/recycled, not pool-freed, so there is no clean pool-allocation UAF primitive and no demonstrable arbitrary read/write.
- Reaching the bug requires the AFD San accept/redirect path plus a cancel-versus-sibling race.
No exploit chain (pool spray, information leak, KASLR defeat, SMEP/CFG/HVCI bypass, or token theft) is asserted, because none is provable from the binaries.
7. Debugger Notes
Target: afd_unpatched.sys (or afd_patched.sys with g_Feature_556471608_60363862 disabled), kernel debugger attached. Symbols resolve afd!AfdSanCancelAccept.
Breakpoints
; (1) Entry to the teardown/cancel routine
bp afd_unpatched+0x7e110
; rdx = IRP being cancelled. context = *(*(rdx+0xB8)+0x20); connection = *(context+0x18) (rdi).
; (2) Start of the reset sequence (no drain performed first)
bp afd_unpatched+0x7e1a7
; rdi = connection. Inspect the pending-list head at rdi+0x70 / rdi+0x78:
; if Flink (rdi+0x70) != &rdi+0x70, sibling IRPs remain and will be left dangling.
; (3) The memset that resets the connection (and zeroes the list head)
bp afd_unpatched+0x7e1ba
; rcx = rdi+0x50, r8d = 0x48 — confirm the range covers rdi+0x70/+0x78.
; (4) Cancel routine for the SURVIVING sibling IRPs
bp afd_unpatched+0x7e3a0 ; AfdSanCancelRequest
; Runs later on a stale sibling IRP; it re-acquires the connection lock and
; manipulates list linkage on the (now zeroed) head — the fault/failfast site.
What to inspect
| BP | Register / memory | Why |
|---|---|---|
+0x7e110 |
rdx = IRP; *(*(rdx+0xB8)+0x20) → context; *(context+0x18) → connection |
Resolves the connection being torn down. |
+0x7e1a7 |
rdi+0x70 (Flink), rdi+0x78 (Blink) |
If Flink ≠ &rdi+0x70, siblings remain undrained. |
+0x7e1ba |
rcx = rdi+0x50, r8d = 0x48 |
Confirms the reset range covers the list head at +0x70. |
+0x7e1f4 |
[rdi+0x158] after xchg |
Field at +0x158 cleared during teardown. |
+0x7e3a0 |
connection as seen by a surviving sibling IRP | Where the stale reference is used after reset. |
Connection-structure layout
| Offset | Field |
|---|---|
+0x00 |
Type tag (0xAFD0 written on reset/recycle) |
+0x02 |
Byte set to 1 on reset |
+0x08 |
State flags (bit 0x800 = closed) |
+0x30 |
KSPIN_LOCK (connection lock) |
+0x50 |
Endpoint back-pointer (AfdDereferenceEndpointInline); start of the 0x48-byte reset memset |
+0x70 |
Pending-IRP list head Flink (initialised to self; inside the reset range) |
+0x78 |
Pending-IRP list head Blink |
+0x91 |
Teardown flag (patched only, feature-gated) |
+0x158 |
Field cleared by xchg during teardown |
8. Changed Functions — Full Triage
Security-relevant (behavioural)
| Function | Sim. | Change | Note |
|---|---|---|---|
AfdSanCancelAccept @ 0x1C007E110 → 0x1C007F120 |
0.60 | security | Adds feature-gated pending-IRP drain + teardown flag conn+0x91; old (undrained) path retained under the disabled branch. Primary finding. |
AfdSanAcceptCore @ 0x1C007D0B8 |
0.98 | security | Installs the (relocated) AfdSanCancelAccept as the accept IRP cancel routine; connection init refactor. |
AfdSanFastCompleteRequest @ 0x1C007FE70 → 0x1C0080F80 |
0.97 | security | Adds conn+0x91 teardown-flag gate before list insert; cancel routine relocated to AfdSanCancelRequest @ 0x1C007F4D0. |
AfdSanRedirectRequest @ 0x1C0081074 → 0x1C0082238 |
0.95 | security | Same teardown-flag gate; cancel routine relocated. |
AfdCreateConnection @ 0x1C0055AB0 → 0x1C0055BF0 |
0.10 | security | New routine adds IoGetDeviceAttachmentBaseRef device-attachment validation + access/option checks; original logic retained as AfdCreateConnection_Old @ 0x1C0007DE4. Feature-gated. |
Behavioural but not directly exploitable
| Function | Sim. | Change | Note |
|---|---|---|---|
sub_1C00589F0 |
0.91 | behavioural | Feature-gated dispatch between old and new connection-creation paths. |
sub_1C003F0CC |
0.76 | behavioural | Feature-gated dispatch between AfdCreateConnection (0x1C0055AB0) and new 0x1C0055BF0. |
sub_1C005591C |
0.93 | behavioural | Connection/endpoint SLIST allocator; feature-gated alloc path + refactor. |
sub_1C0057ED0 |
0.92 | behavioural | IOCTL dispatch; connection-alloc path gated; stack layout shift. |
sub_1C005A210 |
0.92 | behavioural | IOCTL dispatch; connection-alloc paths gated. |
AfdSanConnectHandler @ 0x1C007E500 |
0.96 | behavioural | Cancel-routine address + WPP trace changes; feature-gated paths. |
AfdCreateConnection_Old @ 0x1C0007DE4 |
0.36 | behavioural | Relocated pre-patch AfdCreateConnection logic kept for the feature-disabled fallback. Address 0x1C0007DE4 is a different function (rbc_InitializeFeatureStaging) in the unpatched build — match by content. |
Cosmetic / register-allocation
sub_1C0046230(0.98) andsub_1C00379D0(0.94) — stack-variable renames and WPP/telemetry address changes; no semantic effect.EvaluateCurrentState @ 0x1C0007AF8— the Windows feature-staging evaluator used to gate the patched paths. It is present at the same address in both builds (not a new function); the diff attributes low similarity to relocation/inlining churn, not new logic.
9. Unmatched Functions
None added or removed. All changes are in-place edits or relocations of existing routines. The "new" AfdCreateConnection @ 0x1C0055BF0 and AfdCreateConnection_Old @ 0x1C0007DE4 are relocated/feature-staged variants of the original AfdCreateConnection @ 0x1C0055AB0, not net additions.
Implication: No mitigation was removed. The added surface is the feature-gated teardown drain, the conn+0x91 teardown flag, and the device-attachment validation — all controlled by g_Feature_556471608_60363862.
10. Confidence & Caveats
Confidence: High on mechanism, deliberately conservative on impact. The unpatched teardown without a sibling-IRP drain, and the patched feature-gated drain + teardown flag, are unambiguous in both the disassembly and the decompilation. The connection is reset/recycled in place (type 0xAFD0, memset at conn+0x50), not freed to the pool; there is no ExFreePool on this object in the cancel path.
Facts anchored to the binaries.
- AfdSanCancelAccept reset sequence: endpoint deref at conn+0x50 (AfdDereferenceEndpointInline), memset(conn+0x50, 0, 0x48), type 0xAFD0, int 29h fail-fast on list corruption (0x1C007E1A7–0x1C007E22E).
- Patched drain over conn+0x70 and second locked reset (0x1C007F1C7–0x1C007F318).
- Teardown-flag gate in the insert paths keyed on conn+0x91 and EvaluateCurrentState(g_Feature_556471608_60363862).
- AfdSanAcceptCore @ 0x1C007D0B8 installs the cancel routine (lea rax, AfdSanCancelAccept at 0x1C007D2B5).
Not claimed (no supporting evidence in the binaries). Pool-spray reclamation, information leaks, KASLR defeat, SMEP/CFG/HVCI bypass, arbitrary kernel read/write, and privilege escalation to SYSTEM. The demonstrable impact is a list-integrity bugcheck (local DoS) and use of a recycled connection object; anything beyond that would require winning a reallocation race and controlling the reused object, which is not shown here.
Staging. Because the drain, the teardown flag, and the device check are all gated behind g_Feature_556471608_60363862 with the original paths retained, the patched binary only exhibits the corrected behaviour when that feature is enabled at runtime.