1. Overview

Item Value
Unpatched binary afd_unpatched.sys
Patched binary afd_patched.sys
Overall similarity 0.9846
Matched functions 1180
Changed functions 27
Identical functions 1153
Unmatched (unpatched / patched) 0 / 0

Verdict (corrected): This patch is a WIL/velocity feature-flag reshuffle. It does not deliver a fix for the headline memory-corruption bug that the original report described. The 16-bit integer-underflow out-of-bounds write in the AFD Winsock-Direct/SAN accept handlers (AfdSanConnectHandler, AfdSanAcceptCore) is real and is confirmed in the binary, but the corrected copy path is gated behind a newly added feature flag g_Feature_990641464_61054032, and the vulnerable -8 path is retained and still executes when that feature evaluates disabled. Per patch-diff rules this is a staged fix, not a delivered one: a patched machine with the feature disabled runs the exact same underflow as the unpatched build.

What the patch does deliver (all by removing an old runtime feature gate so the safe behavior becomes unconditional binary-wide):

  1. SAN accept cancellation-race / use-after-free hardeningg_Feature_556471608_60363862 gate removed (7 sites → 0). AfdSanCancelAccept and friends now unconditionally drain and cancel all queued accept IRPs before tearing down (dereference + memset) the SAN endpoint context. The unpatched off-path tore the context down first, without draining. This is a genuine delivered memory-safety fix — the original report missed it entirely.
  2. Transport-open access-control scopingg_Feature_1432032569_60360715 gate removed (12 sites → 0). Callers now unconditionally use the scoped AfdCreateConnection (MAXIMUM_ALLOWED + IO_FORCE_ACCESS_CHECK for flagged endpoints) instead of AfdCreateConnection_Old (hardcoded 0xC0100000, no forced access check).
  3. Descriptor type-tag validationg_Feature_2030245178_60366528 gate removed in AfdSanFastTransferCtx; the (desc & 7) != 3 reject is now unconditional.

Additional lifetime/UAF-prevention changes are staged, not delivered (new feature gates, old behavior retained): AfdUnBindSocket deferred-free + drain (g_Feature_2564028728, g_Feature_1596455227), AfdPnpPower prevent-unbind guard (g_Feature_2016143675), and listen-IRP drain in AfdTLListenComplete/AfdStartListen (g_Feature_2748579128, g_Feature_4139760955). AfdCommonDisconnectEventHandler (g_Feature_3301964088) and AfdSocketTransferBegin (g_Feature_3014129978) are feature-staged ordering/refactor plus WPP churn.

The original report's fabricated exploitation narrative (pool grooming/spray, KASLR defeat, token theft, HVCI/SMEP/CFG-bypass "data-only" chain, "realistic LPE to SYSTEM") is removed — none of it is evidenced by the diff.


2. Vulnerability Summary

2.1 HIGH primitive, STAGED (not delivered) — 16-bit Integer Underflow → Kernel MDL/Pool Out-of-Bounds Write (CWE-191 → CWE-787)

  • Affected functions: AfdSanConnectHandler (sub_1C007F540, unpatched) and AfdSanAcceptCore (sub_1C007E0B8, unpatched) — the Winsock-Direct/SAN accept/connect indication handlers that fill the caller's AcceptEx output buffer from captured connect data. Both contain the identical copy pattern and the identical staged fix.
  • Root cause (confirmed in ground truth): The remaining accept-buffer length is read as a 32-bit DWORD from the connection IRP's I/O-stack overlay (mov ebx, [r14+0x18] at 0x1C007FD26, i.e. p_MajorFunction[6]) and guarded only with cmp ebx, 6 / jb (0x1C007FD2A). In the *(connection+0xAC)==0 branch the code then performs the length subtraction on the low 16 bits (sub bx, 8 at 0x1C007FE58). For a length whose low word is 6 or 7, bx wraps to 0xFFFE/0xFFFF.
  • Why the clamp does not save it: The intended bound is size = min(attacker_len+4, remaining-8), implemented as cmp r9d, eax / cmovge dx, bx at 0x1C007FE5F0x1C007FE62, where eax = (uint16)len - 8 computed as a signed 32-bit value = -2 (for len 6) and r9d = attacker_len + 4 ≥ 4. The signed compare 4 ≥ -2 is always true, so cmovge installs the wrapped 16-bit bx = 0xFFFE as the copy size (movzx r8d, dx = 0xFFFE).
  • Sink: memmove(r13+8, src, 0xFFFE) at 0x1C007FE71 (call memmove, sub_1C0008740). r13 = MappedSystemVa + p_MajorFunction[2] is the MmMapLockedPages/MappedSystemVa mapping of the AcceptEx output buffer, into which only ~6–7 bytes were intended. Writing 0xFFFE bytes overruns the intended region and, for a small output buffer, runs off the locked pages into adjacent non-paged pool.
  • Primitive (what the diff actually proves): A linear kernel out-of-bounds write of a fixed size (0xFFFE/0xFFFF, not attacker-chosen) with partially attacker-influenced contents (the captured connect data). This is an OOB-write primitive; the diff does not evidence pointer control, an info leak, or any specific escalation.
  • What the patch does (STAGED): In the patched build the >= 6 outer guard is unchanged. Only when EvaluateCurrentState(&g_Feature_990641464_61054032) returns true does a new safe path run: if ((uint16)len < 8) memset(dst, 0, len); else { clamp; if (size) memmove(...); }. When the feature returns false, the patched binary executes if (!EvaluateCurrentState(...)) { ... sub bx,8 ... cmovge ... goto memmove } — the same 16-bit underflow. g_Feature_990641464_61054032 does not exist in the unpatched image (0 references) and appears only in the patched image (2 references: AfdSanConnectHandler, AfdSanAcceptCore). Because the vulnerable path is retained behind the gate, the fix is not delivered binary-wide.
  • Reachability caveats (why this is not a slam-dunk local LPE):
  • The path lives behind AFD's Winsock-Direct / Sockets-Direct (SAN) switch (magic *(v8)==0xAFD and *(v8+40)==IoGetCurrentProcess() gates, AfdSanConnectHandler/AfdSanAcceptCore). Reaching it requires a registered SAN service provider; whether an unprivileged user can reach this path on a default modern Windows 10 build is not proven by the diff. Winsock Direct is a legacy/deprecated facility.
  • The capture path runs under the 32-bit/WOW64 branch (IoIs32bitProcess(Irp)).
  • Driving p_MajorFunction[6] (the "remaining length") to exactly 6/7 is inferred from field semantics, not proven.

2.2 MEDIUM, DELIVERED — SAN accept cancellation-race / use-after-free hardening (CWE-416)

  • Affected functions: AfdSanCancelAccept (sub_1C007F030), with the same abort-flag protocol in AfdSanAcquireContext (sub_1C007E730), AfdSanRedirectRequest (sub_1C00820A8), AfdSanFastCompleteRequest (sub_1C0080DF0), and initialization in AfdSanInitEndpoint (sub_1C0081DA8).
  • Root cause (unpatched): The cancellation path was gated by g_Feature_556471608_60363862. When that feature was disabled, AfdSanCancelAccept immediately tore the SAN endpoint context down:
// unpatched AfdSanCancelAccept, feature-OFF branch (0x1C007F030):
AfdDereferenceEndpointInline(*(PSLIST_ENTRY *)(v5 + 80));
memset((void *)(v5 + 80), 0, 0x48u);   // context zeroed
*(_DWORD *)(v5 + 8) = 0; *(_WORD *)v5 = -20528; *(_BYTE *)(v5 + 2) = 1;

without first draining the accept IRPs still queued on the context's list at +112. A cancel racing with in-flight queued accepts could complete/reference a context that was concurrently dereferenced and zeroed — a use-after-free / cancellation race. - What the patch delivers: g_Feature_556471608_60363862 is removed binary-wide (7 references → 0). The abort-and-drain protocol is now unconditional: set the abort byte *(v5+145)=1, unlink every queued IRP from the +112 list (with __fastfail(3) LIST_ENTRY integrity checks), complete each with STATUS_CANCELLED (0xC0000120), and only then dereference + memset the context. The sibling handlers (AfdSanAcquireContext, AfdSanRedirectRequest, AfdSanFastCompleteRequest) now unconditionally refuse to queue new IRPs onto a context whose abort byte +145 is set, completing them STATUS_CANCELLED instead. This is a delivered memory-safety fix. - Severity rationale: Kernel UAF/cancellation race — High-class in isolation, but reachable only via the SAN accept path (same legacy-provider constraint as 2.1) and requires a precise cancel/teardown race, so rated Medium here pending reachability proof.

2.3 LOW–MEDIUM, DELIVERED — Access-control scoping on transport-file open (CWE-862)

  • Affected functions: call sites in AfdGetFreeConnection (sub_1C005572C), AfdAddFreeConnection (sub_1C003F37C), AfdSanConnectHandler, and ~9 others; target AfdCreateConnection_Old (sub_1C0007DE4, unpatched) vs AfdCreateConnection (sub_1C0055A00).
  • Root cause (unpatched): AfdCreateConnection_Old opens the transport file with hardcoded DesiredAccess = 0xC0100000 (GENERIC_READ|GENERIC_WRITE|SYNCHRONIZE) and Options = 0x100IO_NO_PARAMETER_CHECKING only, no IO_FORCE_ACCESS_CHECK:
// unpatched AfdCreateConnection_Old @ 0x1C0007DE4:
Status = IoCreateFile((PHANDLE)(Connection+168), 0xC0100000, &ObjectAttributes,
                      &IoStatusBlock, 0,0,0, 2u, 0, EaBuffer, 0x22u,
                      CreateFileTypeNone, nullptr, /*Options*/0x100u);

Which of AfdCreateConnection (scoped) vs AfdCreateConnection_Old (unscoped) was used was selected at runtime by EvaluateCurrentState(&g_Feature_1432032569_60360715). - What the patch delivers: g_Feature_1432032569_60360715 is removed binary-wide (12 references → 0). All callers unconditionally call the scoped AfdCreateConnection, which for flagged endpoints ((*(a1+0x10)&0x10000) or (*(a1+8)&0x8000000)) uses DesiredAccess = 0x2000000 (MAXIMUM_ALLOWED) and Options = 0x101 (adds IO_FORCE_ACCESS_CHECK). The unsafe _Old open is no longer referenced anywhere in the patched image. AfdCreateConnection (the scoped variant) is byte-identical across both builds; only the call sites and the gate changed. - Severity: Access-control scoping/defense-in-depth on a kernel-initiated open; no memory-corruption primitive. Low–Medium.

2.4 LOW, DELIVERED — Descriptor type-tag validation now unconditional

  • Affected function: AfdSanFastTransferCtx (sub_1C00465B0).
  • Unpatched gated the tag check behind g_Feature_2030245178_60366528: if (EvaluateCurrentState(...) && (desc[0] & 7) != 3) return STATUS_INVALID_PARAMETER;. Patched makes the reject unconditional, validating the descriptor tag before the subsequent AfdCalcBufferArrayByteLength + overflow check + pool copy. Delivered input-validation hardening; low severity.

2.5 INFORMATIONAL, STAGED — additional lifetime/UAF-prevention staging (not delivered)

New feature gates whose safe behavior is not the default (old behavior retained when disabled): - AfdUnBindSocket (sub_1C003E950): g_Feature_2564028728_60975736 defers the free of the block at endpoint+224 until after a g_Feature_1596455227_60987273 drain spin-wait (while (*(endpoint+248) != 0) _mm_pause();). UAF/lifetime hardening, staged. - AfdPnpPower (sub_1C0042174): g_Feature_2016143675_61232256 wraps AfdPassQueryDeviceRelation in AfdPreventUnbind/AfdReallowUnbind with a state re-check (TOCTOU/lifetime guard), staged. - AfdTLListenComplete (sub_1C00603E0) / AfdStartListen (sub_1C0060070): g_Feature_2748579128_61057654 / g_Feature_4139760955_62124451 add AfdDrainListeningIrps on the listen-failure paths to complete queued listening IRPs before the endpoint is reset, staged.


3. Pseudocode Diff

3.1 AfdSanConnectHandler / AfdSanAcceptCore — the staged underflow

// UNPATCHED (AfdSanConnectHandler @ 0x1C007F540) — the -8 branch
v53 = v47[6];                         // 0x1C007FD26 remaining length (32-bit read), guarded >= 6
if (v53 >= 6) {
  ...
  else {                              // *(connection+0xAC)==0  → the -8 path
    *(_DWORD *)v56 = 0; *((_DWORD *)v56 + 1) = 1;
    v70 = v58 + 4;                    // attacker_len + 4  (>= 4)
    v71 = (uint16)v53 - 8;            // 0x1C007FE55 signed int32: 6-8 = -2
    v72 = v53 - 8;                    // 0x1C007FE58 16-bit sub -> 0xFFFE for v53==6
    v73 = v58 + 4;
    if (v70 >= v71) v73 = v72;        // 0x1C007FE62 4 >= -2 ALWAYS TRUE -> v73 = 0xFFFE
    memmove(v56 + 8, v57, v73);       // 0x1C007FE71 OOB WRITE, size 0xFFFE
  }
}
// PATCHED (AfdSanConnectHandler @ 0x1C007E580) — SAME bug retained behind the gate
if (v40 < 6) goto done;              // outer guard UNCHANGED (>= 6)
...
if (*(connection+0xAC) == 0) {
  if (!EvaluateCurrentState(&g_Feature_990641464_61054032)) {
    // ===== FEATURE DISABLED: identical vulnerable path =====
    v68 = v40 - 8;                    // 16-bit underflow: 0xFFFE for v40==6
    v69 = v65 + 4; if (v66 >= v67) v69 = v68;
    v64 = v69; goto do_copy;          // memmove(v43+8, src, 0xFFFE)
  }
  // ===== FEATURE ENABLED: new safe path =====
  if ((uint16)v40 < 8) { memset(v43, 0, (uint16)v40); goto done; }   // <-- the actual fix
  ... v40 >= 8, bounded clamp, memmove only if size != 0 ...
}

Key point: the safe < 8 → memset special-case exists only on the feature-enabled path. The feature-disabled path is byte-for-byte the unpatched underflow. g_Feature_990641464_61054032: 0 refs unpatched, 2 refs patched.

3.2 AfdSanCancelAccept — the delivered UAF fix (gate removed)

// UNPATCHED @ 0x1C007F030
if ((*(v5+8) & 0x800) == 0) {
  if (EvaluateCurrentState(&g_Feature_556471608_60363862)) {
    *(BYTE*)(v5+145) = 1;             // abort flag; drain queued IRPs from list @ +112 ...
  } else {
    AfdDereferenceEndpointInline(*(v5+80));   // <-- teardown WITHOUT draining
    memset((void*)(v5+80), 0, 0x48u);
    *(DWORD*)(v5+8)=0; *(WORD*)v5=-20528; *(BYTE*)(v5+2)=1;
  }
}
// PATCHED @ 0x1C007E0C0 — gate gone, drain-before-teardown is unconditional
if ((*(v5+8) & 0x800) == 0) {
  *(BYTE*)(v5+145) = 1;              // abort flag ALWAYS set
  // unlink every queued IRP from list @ +112 (with __fastfail integrity checks)
}
... release locks ...
while (v3) { ... IofCompleteRequest(queued_irp, STATUS_CANCELLED); }  // drain+cancel first
KeAcquireInStackQueuedSpinLock(...);
if ((*(v5+8) & 0x800) == 0) {        // THEN teardown
  AfdDereferenceEndpointInline(*(v5+80)); memset((void*)(v5+80), 0, 0x48u); ...
}

3.3 AfdGetFreeConnection / AfdAddFreeConnection — the delivered access-control fix (gate removed)

// UNPATCHED
if (EvaluateCurrentState(&g_Feature_1432032569_60360715))
    AfdCreateConnection(endpoint, ...);       // scoped, IO_FORCE_ACCESS_CHECK for flagged endpoints
else
    AfdCreateConnection_Old(...);             // hardcoded 0xC0100000, no forced access check
// PATCHED — gate removed, unconditional
AfdCreateConnection(endpoint, ...);

4. Assembly Analysis (verified against afd_unpatched.asm)

AfdSanConnectHandler, the underflow window (base image 0x1C0000000):

0x1C007FD26  mov     ebx, [r14+18h]        ; remaining length (32-bit read); r14 = &IO_STACK.MajorFunction
0x1C007FD2A  cmp     ebx, 6                 ; GUARD >= 6 (unchanged by the patch)
0x1C007FD2D  jb      loc_1C007FDB6
...
0x1C007FD64  cmp     byte ptr [rdi+0ACh], 0 ; branch selector
0x1C007FD6B  jz      loc_1C007FE39          ; == 0 -> the -8 path
0x1C007FE4E  movzx   eax, bx                ; zx(len)
0x1C007FE51  lea     r12d, [rcx+4]          ; r12d = 8
0x1C007FE55  sub     eax, r12d              ; eax = zx(len) - 8   (signed int32 = -2 for len 6)
0x1C007FE58  sub     bx, r12w               ; 16-bit UNDERFLOW: bx = 0xFFFE for len 6
0x1C007FE5C  add     dx, cx                 ; dx = attacker_len + 4
0x1C007FE5F  cmp     r9d, eax               ; r9d (>=4) vs eax (-2), signed
0x1C007FE62  cmovge  dx, bx                 ; ALWAYS TAKEN -> dx = 0xFFFE
0x1C007FE66  movzx   r8d, dx                ; Size = 0xFFFE
0x1C007FE6A  lea     rcx, [r13+8]           ; dest = MappedSystemVa + off + 8
0x1C007FE6E  mov     rdx, r10               ; src  = captured connect data
0x1C007FE71  call    memmove                ; OOB WRITE

All addresses, the 16-bit sub bx, r12w, the cmovge dx, bx, and the memmove sink (0x1C0008740 = memmove) are confirmed in afd_unpatched.asm.


5. Trigger Conditions (underflow, 2.1)

  1. Reach the AFD Winsock-Direct/SAN accept/connect indication path (requires a registered SAN service provider; reachability from low privilege on a default modern build is unproven). The dispatcher checks *(v8)==0xAFD and *(v8+40)==IoGetCurrentProcess().
  2. The capture runs under the 32-bit/WOW64 branch (IoIs32bitProcess(Irp)), with input length *(arg2+0x10) in [0x20, 0xFFFFFFF3].
  3. Arrange the pended accept so the connection IRP's remaining-length field (p_MajorFunction[6], [r14+0x18]) has low word 6 or 7, and take the branch where *(connection+0xAC)==0.
  4. On the unpatched build (and on a patched build with g_Feature_990641464_61054032 disabled), sub bx,8 wraps to 0xFFFE, cmovge installs it as the size, and memmove copies ~65534 bytes into a buffer intended to receive ~6–7.
  5. Observable effect: an out-of-bounds write past the intended AcceptEx output region; for a small output buffer this overruns the locked-pages mapping into adjacent non-paged pool. Likely bugchecks: PAGE_FAULT_IN_NONPAGED_AREA (0x50), BAD_POOL_HEADER (0x19), or kernel heap corruption.

6. Delivered vs Staged — bottom line

  • Delivered by this patch: SAN accept cancellation-race/UAF hardening (2.2), transport-open access-control scoping (2.3), descriptor tag validation (2.4). All are gate removals → safe behavior binary-wide.
  • Staged, NOT delivered: the 16-bit underflow OOB-write fix (2.1) and the unbind/pnp/listen lifetime hardening (2.5). All are new gates with the old behavior retained when the feature is disabled.
  • No evidenced exploitation chain. The diff proves an OOB-write primitive (fixed size, partial content control) and a cancellation-race UAF; it does not evidence pointer control, an info leak, KASLR defeat, pool-grooming reliability, token/EPROCESS offsets, or any SMEP/CFG/HVCI-bypass path. Those claims from the original report are removed as unsupported.

7. Debugger Notes (UNPATCHED image; RVA = addr − 0x1C0000000, add live lm m afd base)

AfdSanConnectHandler underflow:

bp afd+0x7fd26   ; mov ebx,[r14+18h]  — remaining length; low word must be 6/7
bp afd+0x7fd6b   ; branch selector *(connection+0xAC) must be 0
bp afd+0x7fe58   ; sub bx,8 — single-step, watch bx 0006 -> fffe
bp afd+0x7fe71   ; call memmove — r8d = size (expect 0000fffe); rcx=dest, rdx=src

On a patched image, break at the EvaluateCurrentState(&g_Feature_990641464_61054032) test to see which path is taken; the underflow only fires on the feature-disabled path.

AfdSanCancelAccept (delivered UAF fix):

bp afd+0x7f030   ; unpatched: EvaluateCurrentState(&g_Feature_556471608...) selects drain vs immediate teardown
                 ; patched: no such test — drain-then-teardown is unconditional

8. Changed Functions — Corrected Triage

  • AfdSanConnectHandler (0.942, security_relevant, STAGED): contains the 16-bit underflow OOB-write; patched adds the safe < 8 → memset path only behind g_Feature_990641464_61054032, retaining the vulnerable -8 path when disabled. Also picks up the AfdCreateConnection_Old → AfdCreateConnection gate removal (2.3).
  • AfdSanAcceptCore (security_relevant, STAGED): same underflow pattern, same g_Feature_990641464_61054032 staging. (The original report mis-triaged this as cosmetic "cluster churn.")
  • AfdSanCancelAccept / AfdSanAcquireContext / AfdSanRedirectRequest / AfdSanFastCompleteRequest / AfdSanInitEndpoint (security_relevant, DELIVERED): g_Feature_556471608_60363862 removed → unconditional abort-and-drain before context teardown (2.2). The original report mis-triaged this whole cluster as "no independent security-relevant delta."
  • AfdGetFreeConnection (0.9322) / AfdAddFreeConnection (0.7583) (security_relevant, DELIVERED): g_Feature_1432032569_60360715 removed → unconditional scoped AfdCreateConnection (2.3). Backlog/push-to-free-list reorderings are De-Morgan-equivalent.
  • AfdSanFastTransferCtx (security_relevant, DELIVERED): g_Feature_2030245178_60366528 removed → unconditional descriptor tag validation (2.4).
  • AfdUnBindSocket, AfdPnpPower, AfdTLListenComplete, AfdStartListen (security_relevant, STAGED): lifetime/UAF-prevention behind new gates (2.5); old behavior retained when disabled.
  • AfdCommonDisconnectEventHandler (0.8557, behavioral) / AfdSocketTransferBegin (0.8667, behavioral): feature-staged completion-ordering / unbind-prevention refactor plus WPP trace-GUID churn; no isolated memory-safety delta.
  • Remaining functions in the 27: register-allocation churn and WPP trace-GUID renames (WPP_8d9ab41c... → WPP_f5b9d845...) consistent with recompilation of the touched clusters.

9. Unmatched Functions

  • Removed / Added: none reported (0 / 0). Note: the unsafe AfdCreateConnection_Old open is folded away in the patched image — it is no longer referenced by any caller (the disambiguated _Old symbol appears only in the unpatched decompilation), while the scoped AfdCreateConnection is byte-identical across builds.

10. Confidence & Caveats

  • Confidence: high on the structural facts (verified against both .c and .asm): the underflow arithmetic; that its fix is gated behind a newly added g_Feature_990641464_61054032 with the vulnerable path retained (0 refs unpatched / 2 patched); and that g_Feature_556471608, g_Feature_1432032569, g_Feature_2030245178 gates were removed binary-wide (safe behavior now unconditional).
  • Medium/low confidence on: exploitability and low-privilege reachability of the SAN accept/connect path (Winsock Direct is legacy; the SAN provider requirement is not proven from the diff); the default enabled/disabled state of the staged features (WIL feature default state is not encoded in the decompilation and would need the runtime feature configuration to confirm whether patched-but-feature-disabled machines are exposed to 2.1).
  • To verify before any PoC: (a) whether an unprivileged process can register/reach a SAN provider and drive AfdSanConnectHandler/AfdSanAcceptCore with p_MajorFunction[6] low word == 6/7; (b) the runtime default of g_Feature_990641464_61054032; (c) that the OOB memmove at 0x1C007FE71 actually crosses the output-buffer MDL for a chosen small buffer (capture the bugcheck).