afd.sys — SAN/Winsock-Direct accept path: 16-bit integer-underflow OOB-write fix is STAGED behind a feature flag (not delivered); the delivered changes are a SAN accept cancellation-race/UAF hardening and transport-open access-control scoping
KB5094123
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):
- SAN accept cancellation-race / use-after-free hardening —
g_Feature_556471608_60363862gate removed (7 sites → 0).AfdSanCancelAcceptand 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. - Transport-open access-control scoping —
g_Feature_1432032569_60360715gate removed (12 sites → 0). Callers now unconditionally use the scopedAfdCreateConnection(MAXIMUM_ALLOWED +IO_FORCE_ACCESS_CHECKfor flagged endpoints) instead ofAfdCreateConnection_Old(hardcoded0xC0100000, no forced access check). - Descriptor type-tag validation —
g_Feature_2030245178_60366528gate removed inAfdSanFastTransferCtx; the(desc & 7) != 3reject 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) andAfdSanAcceptCore(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]at0x1C007FD26, i.e.p_MajorFunction[6]) and guarded only withcmp ebx, 6 / jb(0x1C007FD2A). In the*(connection+0xAC)==0branch the code then performs the length subtraction on the low 16 bits (sub bx, 8at0x1C007FE58). For a length whose low word is 6 or 7,bxwraps to0xFFFE/0xFFFF. - Why the clamp does not save it: The intended bound is
size = min(attacker_len+4, remaining-8), implemented ascmp r9d, eax / cmovge dx, bxat0x1C007FE5F–0x1C007FE62, whereeax = (uint16)len - 8computed as a signed 32-bit value =-2(for len 6) andr9d = attacker_len + 4 ≥ 4. The signed compare4 ≥ -2is always true, socmovgeinstalls the wrapped 16-bitbx = 0xFFFEas the copy size (movzx r8d, dx=0xFFFE). - Sink:
memmove(r13+8, src, 0xFFFE)at0x1C007FE71(call memmove,sub_1C0008740).r13 = MappedSystemVa + p_MajorFunction[2]is theMmMapLockedPages/MappedSystemVamapping of the AcceptEx output buffer, into which only ~6–7 bytes were intended. Writing0xFFFEbytes 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
>= 6outer guard is unchanged. Only whenEvaluateCurrentState(&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 executesif (!EvaluateCurrentState(...)) { ... sub bx,8 ... cmovge ... goto memmove }— the same 16-bit underflow.g_Feature_990641464_61054032does 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)==0xAFDand*(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 inAfdSanAcquireContext(sub_1C007E730),AfdSanRedirectRequest(sub_1C00820A8),AfdSanFastCompleteRequest(sub_1C0080DF0), and initialization inAfdSanInitEndpoint(sub_1C0081DA8). - Root cause (unpatched): The cancellation path was gated by
g_Feature_556471608_60363862. When that feature was disabled,AfdSanCancelAcceptimmediately 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; targetAfdCreateConnection_Old(sub_1C0007DE4, unpatched) vsAfdCreateConnection(sub_1C0055A00). - Root cause (unpatched):
AfdCreateConnection_Oldopens the transport file with hardcodedDesiredAccess = 0xC0100000(GENERIC_READ|GENERIC_WRITE|SYNCHRONIZE) andOptions = 0x100—IO_NO_PARAMETER_CHECKINGonly, noIO_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 subsequentAfdCalcBufferArrayByteLength+ 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)
- 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)==0xAFDand*(v8+40)==IoGetCurrentProcess(). - The capture runs under the 32-bit/WOW64 branch (
IoIs32bitProcess(Irp)), with input length*(arg2+0x10)in[0x20, 0xFFFFFFF3]. - 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. - On the unpatched build (and on a patched build with
g_Feature_990641464_61054032disabled),sub bx,8wraps to0xFFFE,cmovgeinstalls it as the size, andmemmovecopies ~65534 bytes into a buffer intended to receive ~6–7. - 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 → memsetpath only behindg_Feature_990641464_61054032, retaining the vulnerable-8path when disabled. Also picks up theAfdCreateConnection_Old → AfdCreateConnectiongate removal (2.3).AfdSanAcceptCore(security_relevant, STAGED): same underflow pattern, sameg_Feature_990641464_61054032staging. (The original report mis-triaged this as cosmetic "cluster churn.")AfdSanCancelAccept/AfdSanAcquireContext/AfdSanRedirectRequest/AfdSanFastCompleteRequest/AfdSanInitEndpoint(security_relevant, DELIVERED):g_Feature_556471608_60363862removed → 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_60360715removed → unconditional scopedAfdCreateConnection(2.3). Backlog/push-to-free-list reorderings are De-Morgan-equivalent.AfdSanFastTransferCtx(security_relevant, DELIVERED):g_Feature_2030245178_60366528removed → 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_Oldopen is folded away in the patched image — it is no longer referenced by any caller (the disambiguated_Oldsymbol appears only in the unpatched decompilation), while the scopedAfdCreateConnectionis byte-identical across builds.
10. Confidence & Caveats
- Confidence: high on the structural facts (verified against both
.cand.asm): the underflow arithmetic; that its fix is gated behind a newly addedg_Feature_990641464_61054032with the vulnerable path retained (0 refs unpatched / 2 patched); and thatg_Feature_556471608,g_Feature_1432032569,g_Feature_2030245178gates 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/AfdSanAcceptCorewithp_MajorFunction[6]low word == 6/7; (b) the runtime default ofg_Feature_990641464_61054032; (c) that the OOBmemmoveat0x1C007FE71actually crosses the output-buffer MDL for a chosen small buffer (capture the bugcheck).