afd.sys — manual user-buffer probe idiom replaced with equivalent ProbeForRead calls
KB5094128
1. Overview
- Unpatched Binary:
afd_unpatched.sys - Patched Binary:
afd_patched.sys - Overall Similarity Score:
0.6716 - Diff Statistics: 1177 matched functions, 921 changed functions, 256 identical functions, and 0 unmatched functions in either direction.
- Verdict: The changed IOCTL-path functions replace a hand-rolled
MmUserProbeAddressprobe idiom with calls to the standardProbeForRead()API. In every case the replacement validates the same address range with the same alignment and under the sameRequestorModeguard, so the user-buffer validation is behaviorally identical between builds. The CMSG length routine inlines several overflow-checkedRtl*arithmetic helpers but keeps the same overflow checks. A spinlock acquisition in the send-message handler is placed behind a WIL feature-staging gate. None of these constitute a security-relevant behavioral change: the unpatched code already performs the same bounds, alignment, and overflow validation.
2. Vulnerability Summary
Finding 1: Probe idiom modernized in AFD Send-Message Handler — No security-relevant change
- Severity: None (informational)
- Vulnerability Class: N/A (API modernization / defense-in-depth cleanup)
- Affected Function:
AfdExtractAfdSendMsgInfo(unpatched0x1C003A738, patched0x14004EC68) - Root Cause / Nature: The unpatched function validates the user-mode message structure and its embedded buffers with the classic hand-rolled probe sequence: a single-byte clamp-and-touch against
MmUserProbeAddressfor the fixed-size header, and an end-address range check (end = base + length; if (end > *MmUserProbeAddress || end < base) fault) before eachRtlCopyMemory. The patched function replaces these withProbeForRead(Address, Length, Alignment)calls. The header probe usesLength = 1, Alignment = 4in both builds; the pre-copy probes useLength = <copy length>, Alignment = 1in both builds, guarded by the sameRequestorModebyte ([endpoint+0x40]).ProbeForReadperforms a range and alignment check identical to the hand-rolled code and does not touch intermediate pages either, so there is no added page validation, no removed check, and no double-fetch removed (the length and pointer fields are each read once in both builds). This is an API substitution, not a fix. - Attacker-Reachable Entry Point:
NtDeviceIoControlFileon\Device\Afd(send-message path). Reachable, but no vulnerable behavior exists in either build. - Call Chain:
NtDeviceIoControlFile-> AFD IOCTL dispatchAfdExtractAfdSendMsgInfoprobes the user structure and embedded buffers (identically in both builds).AfdComputeCMSGLengthcomputes the CMSG allocation size (overflow-checked in both builds).memmove(RtlCopyMemory) copies validated user data into a kernel pool allocation.
Finding 2: Probe idiom modernized in MDL Lock Helper — No security-relevant change
- Severity: None (informational)
- Vulnerability Class: N/A (API modernization)
- Affected Functions:
AfdAllocateMdlChain(unpatched0x1C00073B0, patched0x140004CF0) andAfdAllocateMdlChain32(unpatched0x1C0017638, patched0x140004E60) - Root Cause / Nature: This helper validates a user-supplied buffer-descriptor array. The unpatched build computes
end = base + count * entrySizeand checksend > *MmUserProbeAddressandend < base(wrap), preceded by an alignment check (test <base>, 3, i.e. 4-byte alignment). The patched build callsProbeForRead(base, count * entrySize, 4). Both validate the identical[base, base + count*entrySize)range with 4-byte alignment; both leave per-page validity to be enforced at access time byMmProbeAndLockPages.ProbeForReaddoes not validate intermediate pages, so the claim of "intermediate page" protection does not apply to either build. The null-pointer andcount-1 <= 0xFFFFFFEguards are present and identical in both builds. Equivalent validation, no behavioral change. - Attacker-Reachable Entry Point: AFD IOCTL dispatch (buffer-descriptor locking path).
- Call Chain:
NtDeviceIoControlFile-> AFD IOCTL dispatch.AfdAllocateMdlChainvalidates the descriptor array, then iteratesIoAllocateMdl+MmProbeAndLockPages(identically in both builds).
Finding 3: CMSG Length Computation helper inlining — No security-relevant change
- Severity: None (informational)
- Vulnerability Class: N/A (compiler inlining of overflow-checked helpers)
- Affected Function:
AfdComputeCMSGLength(unpatched0x1C0034BAC, patched0x140030540) - Root Cause / Nature: Both builds parse attacker-controlled TLV-structured data to compute a total allocation size, and both check every arithmetic step for overflow. In the unpatched build the checks are performed by the overflow-checked helpers
RtlULongAlignUp,RtlULongSub, andRtlULongAdd, each followed bytest eax, eax; js <error>. In the patched build some of these helpers are inlined: for example the(entry_size - 0xc) + 0x10addition, which the unpatched build performs viaRtlULongAddat0x1C0034C2B(withjson overflow), is inlined in the patched build aslea ecx, [rax+10h]; cmp ecx, eax; jb <error>at0x1400305D0. The overflow protection is identical; only the code shape differs. There is no gap in the unpatched build for an intermediate value to wrap unchecked. No behavioral change. - Attacker-Reachable Entry Point:
AfdExtractAfdSendMsgInfo->AfdComputeCMSGLength. - Call Chain:
NtDeviceIoControlFile-> AFD IOCTL dispatch.AfdExtractAfdSendMsgInfo->AfdComputeCMSGLengthcomputes the size (overflow-checked in both builds) and returns it for pool allocation.
3. Pseudocode Diff
AfdExtractAfdSendMsgInfo — header probe (equivalent)
// UNPATCHED (0x1C003A738): hand-rolled single-byte probe, alignment 4
if (endpoint->RequestorMode) {
if (ptr & 3) ExRaiseDatatypeMisalignment();
char* limit = *MmUserProbeAddress;
if (ptr >= limit) ptr = limit; // clamp
(void)*ptr; // touch one byte
}
// PATCHED (0x14004EC68): standard API, same length (1) and alignment (4)
if (endpoint->RequestorMode)
ProbeForRead(header, /*Length*/ 1, /*Alignment*/ 4);
AfdExtractAfdSendMsgInfo — pre-copy range probe (equivalent)
// UNPATCHED: end-address range check before RtlCopyMemory
if (endpoint->RequestorMode) {
void* end = base + length;
char* limit = *MmUserProbeAddress;
if (end > limit || end < base) *limit = 0; // faults if out of range
}
memmove(dst, base, length);
// PATCHED: same range, alignment 1, same RequestorMode guard
if (endpoint->RequestorMode)
ProbeForRead(base, length, 1);
memmove(dst, base, length);
AfdComputeCMSGLength — overflow check (equivalent)
// UNPATCHED (0x1C0034BAC): overflow checked by the helper
status = RtlULongAdd(entry_size_minus_0xc, 0x10, &payload); // 0x1C0034C2B
if (status < 0) break; // js on overflow
// PATCHED (0x140030540): same check, inlined
ecx = payload + 0x10; // lea ecx, [rax+10h] (0x1400305D0)
if (ecx < payload) break; // cmp ecx, eax; jb -> overflow
payload = ecx;
4. Assembly Analysis
AfdExtractAfdSendMsgInfo header probe
; UNPATCHED (64-bit path, header probe)
00000001C003A8D9 mov rax, cs:MmUserProbeAddress
00000001C003A8E0 mov rcx, [rax]
00000001C003A8E3 cmp rdx, rcx
00000001C003A8E6 cmovnb rdx, rcx ; clamp to limit
00000001C003A8EA mov al, [rdx] ; touch one byte
; PATCHED (64-bit path, header probe) — length 1, alignment 4
000000014004EE2C mov edx, 1 ; Length = 1
000000014004EE31 lea r8d, [rdx+3] ; Alignment = 4
000000014004EE35 mov rcx, r14 ; Address
000000014004EE38 call cs:__imp_ProbeForRead
AfdExtractAfdSendMsgInfo pre-copy range probe
; UNPATCHED (range check before RtlCopyMemory of the CMSG buffer)
00000001C003AAD9 mov rcx, [rsp+0F8h+var_80] ; base
00000001C003AADE lea rdx, [rcx+rsi] ; end = base + length
00000001C003AAE2 mov rax, cs:MmUserProbeAddress
00000001C003AAE9 mov r8, [rax]
00000001C003AAEC cmp rdx, r8
00000001C003AAEF ja short loc_1C003AAF6
00000001C003AAF1 cmp rdx, rcx
00000001C003AAF4 jnb short loc_1C003AAF9
00000001C003AAF6 mov [r8], bl ; fault if out of range
; PATCHED (same range, alignment 1, same RequestorMode guard)
000000014004F0DA mov edx, r13d ; Length
000000014004F0DD mov r8d, 1 ; Alignment
000000014004F0E3 mov rcx, rsi ; Address
000000014004F0E6 call cs:__imp_ProbeForRead
AfdAllocateMdlChain descriptor-array probe
; UNPATCHED (0x1C00073B0): end-address range check, alignment 4
00000001C000741E test dil, 3 ; 4-byte alignment
00000001C0007428 lea rcx, [rax+rdx] ; end = base + count*16
00000001C000742C mov rax, cs:MmUserProbeAddress
00000001C0007433 mov rdx, [rax]
00000001C0007436 cmp rcx, rdx
00000001C0007439 ja loc_1C00074F2 ; fail if end > limit
00000001C000743F cmp rcx, rdi
00000001C0007442 jb loc_1C00074F2 ; fail if wrapped
; PATCHED (0x140004CF0): equivalent ProbeForRead
0000000140004D48 mov edx, edi
0000000140004D4A shl rdx, 4 ; Length = count*16
0000000140004D4E lea r8d, [rsi+4] ; Alignment = 4
0000000140004D52 mov rcx, rbx ; Address = base
0000000140004D55 call cs:__imp_ProbeForRead
AfdComputeCMSGLength overflow check
; UNPATCHED (0x1C0034BAC): overflow checked by helper
00000001C0034C26 mov edx, 10h
00000001C0034C2B call RtlULongAdd ; (entry-0xc) + 0x10
00000001C0034C30 test eax, eax
00000001C0034C32 js short loc_1C0034C6F ; branch on overflow
; PATCHED (0x140030540): same check, inlined
00000001400305D0 lea ecx, [rax+10h]
00000001400305D3 cmp ecx, eax
00000001400305D5 jb short loc_14003060C ; branch on overflow
5. Trigger Conditions
There is no security-relevant condition to trigger. AfdExtractAfdSendMsgInfo is reachable from user mode via NtDeviceIoControlFile on \Device\Afd (the send-message path that carries ancillary/control data), and AfdComputeCMSGLength / AfdAllocateMdlChain are reached from it. However, the user-buffer validation performed on this path is identical in the unpatched and patched builds:
- The fixed-size header is probed for one byte with 4-byte alignment in both builds.
- Each attacker-supplied buffer is range-checked over its full copy length (aligned to 1) under the
RequestorModeguard in both builds, immediately before the correspondingRtlCopyMemory. - The CMSG size accumulation is overflow-checked at every step in both builds.
A crafted IOCTL that supplies an out-of-range buffer, an unmapped page, or an overflowing TLV size field is rejected identically by both binaries. No mismatched-size copy, undersized allocation, or missing bounds check is present in the unpatched build.
6. Exploit Primitive & Development Notes
No exploit primitive is provided by this change. The transformation is a source-level replacement of a hand-rolled probe idiom with the ProbeForRead API (plus inlining of overflow-checked arithmetic helpers and a feature-staging gate on a spinlock). Because the range, alignment, and mode-guard semantics are identical between builds, there is no reachable memory-safety primitive introduced or removed. Statements about pool grooming, KASLR bypass, token corruption, or arbitrary read/write have no basis in either binary and are not applicable.
7. Debugger PoC Playbook
There is no vulnerability to reproduce. For anyone confirming the equivalence directly:
- Break on the header probe: unpatched
mov al, [rdx]at0x1C003A8EA(single-byte touch) vs. patched__imp_ProbeForReadat0x14004EE38withLength = 1,Alignment = 4. Same effect. - Break on the pre-copy probe: unpatched range check at
0x1C003AAE2-0x1C003AAF6vs. patched__imp_ProbeForReadat0x14004F0E6withLength = copy length,Alignment = 1. Same range. - Break in
AfdComputeCMSGLength: unpatchedRtlULongAddat0x1C0034C2B(withjsoverflow branch) vs. patched inlinelea ecx,[rax+10h]; cmp ecx,eax; jbat0x1400305D0. Same overflow rejection.
Supplying an out-of-range buffer or an overflowing TLV size to either build yields the same rejection (STATUS_ACCESS_VIOLATION / STATUS_INTEGER_OVERFLOW surfaced through AfdExceptionFilter), confirming behavioral equivalence.
8. Changed Functions — Full Triage
AfdExtractAfdSendMsgInfo(unpatched0x1C003A738/ patched0x14004EC68, Sim: 0.7754, Not Security Relevant): Hand-rolledMmUserProbeAddressprobes replaced with equivalentProbeForReadcalls (same length, alignment, andRequestorModeguard). The spinlock acquisition around the endpoint-flag/size read is placed behind the WIL feature gateFeature_2263667000(staged rollout). No change to user-buffer validation.AfdAllocateMdlChain(unpatched0x1C00073B0/ patched0x140004CF0, Sim: 0.78, Not Security Relevant): End-address range check replaced withProbeForRead(base, count*16, 4). Equivalent range and alignment validation.AfdAllocateMdlChain32(unpatched0x1C0017638/ patched0x140004E60, Sim: 0.78, Not Security Relevant): WOW64 variant; end-address range check replaced withProbeForRead(base, count*8, 4). Equivalent.AfdComputeCMSGLength(unpatched0x1C0034BAC/ patched0x140030540, Sim: 0.75, Not Security Relevant): Overflow-checkedRtl*helpers inlined; identical overflow checks retained at every arithmetic step.AfdRoutingInterfaceChange(unpatched0x1C0037F60/ patched0x14004BC90, Sim: 0.709, Not Security Relevant):SIO_ROUTING_INTERFACE_CHANGEhandler; end-address range check (0x1C00380D2) replaced withProbeForRead(base, length, 1)(0x14004BDB9) under the sameRequestorModeguard. Equivalent.AfdRioCreateCompletionQueue(unpatched0x1C00475F0, Sim: 0.7834, Behavioral): MigratedExAllocatePoolWithQuotaTagtoExAllocatePool2and legacy queued spinlocks toNdisReleaseRWLock. Servicing/API modernization.AfdFinishConnect(unpatched0x1C00049D4, Sim: 0.7035, Behavioral): InternalAFD_ENDPOINTfield offsets shifted; added a refcount-overflow trap (trap(0xd)). Structural churn / hardening trap, not a reachable fix.AfdFastConnectionSend(unpatched0x1C000BEF8, Sim: 0.7834, Behavioral): Refcount and IRP-completion path refactored; internal lock offsets shifted. Structural churn.AfdBCommonChainedReceiveEventHandler(unpatched0x1C0007780, Sim: 0.7506, Behavioral): ETW tracing updated, feature flags shifted, refcount trap added. Servicing churn.
9. Unmatched Functions
There are 0 unmatched functions in either binary. The patch introduces no new functions and removes none; it operates entirely via inline modifications and API substitutions.
10. Confidence & Caveats
- Confidence Level: High. The changed IOCTL-path functions were compared instruction-by-instruction against both builds. In every case the replacement of the hand-rolled
MmUserProbeAddressidiom withProbeForReadpreserves the validated range, the alignment, and theRequestorModeguard; the CMSG routine preserves every overflow check; and the spinlock change is a WIL feature-staging gate. - Nature of the change: API modernization and servicing churn (hand-rolled probe ->
ProbeForRead, helper inlining, feature-staging). This is a defense-in-depth / maintainability cleanup, not a security fix, because the unpatched build already performs the same bounds, alignment, and overflow validation. - Direction check: The patched build is not stricter on the user-buffer path than the unpatched build. Where the patched build gates the spinlock behind a feature flag, the "always acquire" behavior of the unpatched build corresponds to the feature-enabled path; this is staged rollout, not a delivered lock fix.