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 MmUserProbeAddress probe idiom with calls to the standard ProbeForRead() API. In every case the replacement validates the same address range with the same alignment and under the same RequestorMode guard, so the user-buffer validation is behaviorally identical between builds. The CMSG length routine inlines several overflow-checked Rtl* 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 (unpatched 0x1C003A738, patched 0x14004EC68)
  • 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 MmUserProbeAddress for the fixed-size header, and an end-address range check (end = base + length; if (end > *MmUserProbeAddress || end < base) fault) before each RtlCopyMemory. The patched function replaces these with ProbeForRead(Address, Length, Alignment) calls. The header probe uses Length = 1, Alignment = 4 in both builds; the pre-copy probes use Length = <copy length>, Alignment = 1 in both builds, guarded by the same RequestorMode byte ([endpoint+0x40]). ProbeForRead performs 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: NtDeviceIoControlFile on \Device\Afd (send-message path). Reachable, but no vulnerable behavior exists in either build.
  • Call Chain:
  • NtDeviceIoControlFile -> AFD IOCTL dispatch
  • AfdExtractAfdSendMsgInfo probes the user structure and embedded buffers (identically in both builds).
  • AfdComputeCMSGLength computes 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 (unpatched 0x1C00073B0, patched 0x140004CF0) and AfdAllocateMdlChain32 (unpatched 0x1C0017638, patched 0x140004E60)
  • Root Cause / Nature: This helper validates a user-supplied buffer-descriptor array. The unpatched build computes end = base + count * entrySize and checks end > *MmUserProbeAddress and end < base (wrap), preceded by an alignment check (test <base>, 3, i.e. 4-byte alignment). The patched build calls ProbeForRead(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 by MmProbeAndLockPages. ProbeForRead does not validate intermediate pages, so the claim of "intermediate page" protection does not apply to either build. The null-pointer and count-1 <= 0xFFFFFFE guards 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.
  • AfdAllocateMdlChain validates the descriptor array, then iterates IoAllocateMdl + 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 (unpatched 0x1C0034BAC, patched 0x140030540)
  • 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, and RtlULongAdd, each followed by test eax, eax; js <error>. In the patched build some of these helpers are inlined: for example the (entry_size - 0xc) + 0x10 addition, which the unpatched build performs via RtlULongAdd at 0x1C0034C2B (with js on overflow), is inlined in the patched build as lea ecx, [rax+10h]; cmp ecx, eax; jb <error> at 0x1400305D0. 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 -> AfdComputeCMSGLength computes 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 RequestorMode guard in both builds, immediately before the corresponding RtlCopyMemory.
  • 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] at 0x1C003A8EA (single-byte touch) vs. patched __imp_ProbeForRead at 0x14004EE38 with Length = 1, Alignment = 4. Same effect.
  • Break on the pre-copy probe: unpatched range check at 0x1C003AAE2-0x1C003AAF6 vs. patched __imp_ProbeForRead at 0x14004F0E6 with Length = copy length, Alignment = 1. Same range.
  • Break in AfdComputeCMSGLength: unpatched RtlULongAdd at 0x1C0034C2B (with js overflow branch) vs. patched inline lea ecx,[rax+10h]; cmp ecx,eax; jb at 0x1400305D0. 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 (unpatched 0x1C003A738 / patched 0x14004EC68, Sim: 0.7754, Not Security Relevant): Hand-rolled MmUserProbeAddress probes replaced with equivalent ProbeForRead calls (same length, alignment, and RequestorMode guard). The spinlock acquisition around the endpoint-flag/size read is placed behind the WIL feature gate Feature_2263667000 (staged rollout). No change to user-buffer validation.
  • AfdAllocateMdlChain (unpatched 0x1C00073B0 / patched 0x140004CF0, Sim: 0.78, Not Security Relevant): End-address range check replaced with ProbeForRead(base, count*16, 4). Equivalent range and alignment validation.
  • AfdAllocateMdlChain32 (unpatched 0x1C0017638 / patched 0x140004E60, Sim: 0.78, Not Security Relevant): WOW64 variant; end-address range check replaced with ProbeForRead(base, count*8, 4). Equivalent.
  • AfdComputeCMSGLength (unpatched 0x1C0034BAC / patched 0x140030540, Sim: 0.75, Not Security Relevant): Overflow-checked Rtl* helpers inlined; identical overflow checks retained at every arithmetic step.
  • AfdRoutingInterfaceChange (unpatched 0x1C0037F60 / patched 0x14004BC90, Sim: 0.709, Not Security Relevant): SIO_ROUTING_INTERFACE_CHANGE handler; end-address range check (0x1C00380D2) replaced with ProbeForRead(base, length, 1) (0x14004BDB9) under the same RequestorMode guard. Equivalent.
  • AfdRioCreateCompletionQueue (unpatched 0x1C00475F0, Sim: 0.7834, Behavioral): Migrated ExAllocatePoolWithQuotaTag to ExAllocatePool2 and legacy queued spinlocks to NdisReleaseRWLock. Servicing/API modernization.
  • AfdFinishConnect (unpatched 0x1C00049D4, Sim: 0.7035, Behavioral): Internal AFD_ENDPOINT field offsets shifted; added a refcount-overflow trap (trap(0xd)). Structural churn / hardening trap, not a reachable fix.
  • AfdFastConnectionSend (unpatched 0x1C000BEF8, Sim: 0.7834, Behavioral): Refcount and IRP-completion path refactored; internal lock offsets shifted. Structural churn.
  • AfdBCommonChainedReceiveEventHandler (unpatched 0x1C0007780, 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 MmUserProbeAddress idiom with ProbeForRead preserves the validated range, the alignment, and the RequestorMode guard; 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.