1. Overview

  • Unpatched Binary: afd_unpatched.sys
  • Patched Binary: afd_patched.sys
  • Overall Similarity Score: 0.9805 (98.05%)
  • Diff Statistics: 1098 matched functions (1084 identical, 14 changed), 0 unmatched functions in either direction.

Verdict: The patch introduces a synchronization (drain-wait) mechanism around the endpoint dispatch-pointer swap in the TDI listen-completion callback AfdTLListenComplete, addressing a race condition on that swap. It also caches a state flag before a reference drop in the datagram-connect cleanup path AfdDoDatagramConnect (a defensive lifetime hardening), and makes a device-consistency validation in AfdCreateConnection/AfdCreateConnection_Old unconditional. The first two changes are staged behind runtime feature flags with the original behavior retained when the flag is disabled; the device-validation change is delivered unconditionally (the gate is removed).


2. Vulnerability Summary

Finding 1: Race Condition on Endpoint Dispatch-Pointer Swap

  • Severity: Medium
  • CWE: CWE-362 (Concurrent Execution using Shared Resource with Improper Synchronization / Race Condition)
  • Vulnerability Class: Race Condition / TOCTOU
  • Affected Function: AfdTLListenComplete (unpatched @ 0x1C00597D0, patched @ 0x1C005A960)
  • Delivery: Feature-gated (staged rollout) behind g_Feature_3289849145_60513071; original no-wait behavior retained when the flag is disabled.

Root Cause: In the success path (IRP status >= 0), the unpatched listen-completion callback loads the endpoint's current dispatch context ([rbx+0x10]) and dispatch function pointer ([rbx+0x18]), overwrites both with the new values passed in (r15, r14), and then immediately invokes the previously loaded (old) function pointer through the CFG guard dispatch. It does this without waiting for any TDI I/O-control operations that are concurrently in flight on the same endpoint to complete. The patch adds a per-endpoint in-flight counter at [rbx+0xf8]: send/recv/query dispatch helpers increment it before their TDI dispatch call and decrement it after, and AfdTLListenComplete spin-waits for that counter to reach zero (draining in-flight operations) after swapping the pointers and before invoking the old function pointer. Both the counter maintenance and the spin-wait are gated by EvaluateCurrentState(&g_Feature_3289849145_60513071).

Reachability: AfdTLListenComplete is the TDI listen-completion callback for an AFD endpoint. The in-flight counter it drains is maintained by the endpoint TDI I/O-control dispatch helpers (AfdGetAddress @ 0x1C0035A00, AfdQueryCompartmentId @ 0x1C00357D0, AfdTLPauseUnPause @ 0x1C0058550) and AfdAddConnectedReference @ 0x1C0047C0C, all reachable from AFD IOCTL dispatch via NtDeviceIoControlFile on \Device\Afd.

Finding 2: Read of State Flag After Reference Drop (potential Use-After-Free)

  • Severity: Low
  • CWE: CWE-416 (Use After Free) — potential; defensive hardening
  • Vulnerability Class: TOCTOU / lifetime hardening
  • Affected Function: AfdDoDatagramConnect (unpatched @ 0x1C0059588, patched @ 0x1C005A358)
  • Delivery: Feature-gated behind g_Feature_566223161_61092118; original post-dereference read retained when the flag is disabled.

Root Cause: In the cleanup/error path (LABEL_42), the code calls ObfDereferenceObject(a1) and then reads *(v5+8) & 0x100 to decide whether to free MasterIrp with the AfdI pool tag, where v5 = *(a1+0x18). If dropping the reference on a1 were to free the allocation backing v5, this read would touch freed memory. The patch, when the feature is enabled, caches bit 0x100 of *(v5+8) into a byte before the dereference and uses the cached value for the free decision; when the feature is disabled it retains the original post-dereference read. Whether the dereference actually frees the memory backing v5 is not demonstrable from these binaries, so this is best characterized as a defensive lifetime hardening rather than a proven reachable UAF.

Reachability: AfdDoDatagramConnect is reached from the AFD connect dispatch path via NtDeviceIoControlFile on \Device\Afd.

Finding 3: Feature-Gated Device-Consistency Validation Made Unconditional

  • Severity: Medium
  • CWE: CWE-20 (Improper Input Validation)
  • Vulnerability Class: Missing / conditionally-skipped validation
  • Affected Functions: AfdCreateConnection (unpatched @ 0x1C00491B0, patched @ 0x1C0049240) and AfdCreateConnection_Old (@ 0x1C0015AF8 in both builds)
  • Delivery: Delivered unconditionally (the feature gate is removed).

Root Cause: During connection endpoint creation, when a file handle is supplied, the code validates that the handle's underlying device stack matches the connection's transport device (via ObReferenceObjectByHandle + IoGetDeviceAttachmentBaseRef comparison, returning STATUS_INVALID_PARAMETER on mismatch). In the unpatched build this entire validation block is gated by EvaluateCurrentState(&g_Feature_1484999993_59735560): when the feature is disabled, the check is skipped and a handle from a different device can be associated with the AFD connection. The patched build removes the gate and performs the validation whenever a handle is supplied. The gate symbol g_Feature_1484999993_59735560 appears in the unpatched decompilation of both functions and is absent from the patched build entirely.

Reachability: Reached from AFD connect IOCTL dispatch via NtDeviceIoControlFile on \Device\Afd with a caller-supplied file handle.


3. Pseudocode Diff

Finding 1: AfdTLListenComplete (Race Condition)

// --- UNPATCHED: AfdTLListenComplete @ 0x1C00597D0 (success path, status >= 0) ---
old_func_ptr = *(rbx + 0x18);      // load old dispatch function pointer
old_context  = *(rbx + 0x10);      // load old dispatch context
*(rbx + 0x10) = new_context;       // overwrite context (no wait)
*(rbx + 0x18) = new_func_ptr;      // overwrite function pointer (no wait)
(*old_func_ptr)(old_context, ...); // invoke old pointer via CFG guard dispatch

// --- PATCHED: AfdTLListenComplete @ 0x1C005A960 (success path, status >= 0) ---
old_context  = *(rbx + 0x10);
old_func_ptr = *(rbx + 0x18);
*(rbx + 0x10) = new_context;
*(rbx + 0x18) = new_func_ptr;
if ( EvaluateCurrentState(&g_Feature_3289849145_60513071) )
    while ( *(rbx + 0xf8) != 0 )   // drain in-flight TDI operations
        _mm_pause();
(*old_func_ptr)(old_context, ...);

Finding 2: AfdDoDatagramConnect (state flag read after reference drop)

// --- UNPATCHED: AfdDoDatagramConnect @ 0x1C0059588, LABEL_42 ---
ObfDereferenceObject(a1);
if ( (*(_DWORD *)(v5 + 8) & 0x100) == 0 )       // read after deref
    ExFreePoolWithTag(MasterIrp, 'AfdI');

// --- PATCHED: AfdDoDatagramConnect @ 0x1C005A358, LABEL_42 ---
if ( EvaluateCurrentState(&g_Feature_566223161_61092118) )
    v6 = BYTE1(*(_DWORD *)(v5 + 8)) & 1;        // cache bit 0x100 before deref
ObfDereferenceObject(a1);
if ( EvaluateCurrentState(&g_Feature_566223161_61092118) )
    free_it = (v6 == 0);                        // use cached value
else
    free_it = ((*(_DWORD *)(v5 + 8) & 0x100) == 0); // retained old read
if ( free_it )
    ExFreePoolWithTag(MasterIrp, 'AfdI');

Finding 3: AfdCreateConnection (device-consistency validation)

// --- UNPATCHED: AfdCreateConnection @ 0x1C00491B0 (handle supplied) ---
if ( handle != 0 ) {
    if ( EvaluateCurrentState(&g_Feature_1484999993_59735560) ) {   // gate
        ObReferenceObjectByHandle(handle, ...);
        base_a = IoGetDeviceAttachmentBaseRef(*(v13 + 3));
        base_b = IoGetDeviceAttachmentBaseRef(IoGetRelatedDeviceObject(...));
        if ( base_a != base_b )
            return STATUS_INVALID_PARAMETER;
    }
    // when feature disabled: validation skipped
}

// --- PATCHED: AfdCreateConnection @ 0x1C0049240 (handle supplied) ---
if ( handle != 0 ) {
    // no feature gate - validation always runs
    ObReferenceObjectByHandle(handle, ...);
    base_a = IoGetDeviceAttachmentBaseRef(*(v13 + 3));
    base_b = IoGetDeviceAttachmentBaseRef(IoGetRelatedDeviceObject(...));
    if ( base_a != base_b )
        return STATUS_INVALID_PARAMETER;
}

4. Assembly Analysis

Finding 1: AfdTLListenComplete

UNPATCHED success path at 0x1C00597D0 (no wait between the swap and the indirect call):

00000001C005982A  mov     rax, [rbx+18h]                 ; load old function pointer
00000001C005982E  lea     rdx, AfdTLDontCareIOCompletion
00000001C0059835  mov     rcx, [rbx+10h]                 ; load old context
00000001C0059843  mov     [rbx+10h], r15                 ; overwrite context (no wait)
00000001C0059847  mov     [rbx+18h], r14                 ; overwrite function ptr (no wait)
00000001C0059850  mov     rax, [rax]
00000001C0059853  call    cs:__guard_dispatch_icall_fptr ; CFG-guarded indirect call

PATCHED success path at 0x1C005A960 (adds the feature-gated drain-wait on [rbx+0xf8]):

00000001C005A9BA  mov     r12, [rbx+10h]                 ; old context
00000001C005A9BE  lea     rcx, g_Feature_3289849145_60513071_FeatureDescriptorDetails
00000001C005A9C5  mov     r13, [rbx+18h]                 ; old function pointer
00000001C005A9D5  mov     [rbx+10h], r15                 ; overwrite context
00000001C005A9D9  mov     [rbx+18h], r14                 ; overwrite function ptr
00000001C005A9DD  call    EvaluateCurrentState
00000001C005A9E2  test    eax, eax
00000001C005A9E4  jz      short loc_1C005A9F6            ; feature off -> skip wait
00000001C005A9E8  pause
00000001C005A9EA  mov     rax, [rbx+0F8h]                ; in-flight counter
00000001C005A9F1  test    rax, rax
00000001C005A9F4  jnz     short loc_1C005A9E8            ; spin until drained
00000001C005AA0C  mov     rax, [r13+0]
00000001C005AA13  call    cs:__guard_dispatch_icall_fptr

Finding 2: AfdDoDatagramConnect

UNPATCHED LABEL_42 at 0x1C0064510 (reads flag after the dereference):

00000001C0064510  call    cs:__imp_ObfDereferenceObject
00000001C0064516  test    dword ptr [rbx+8], 100h        ; read after deref
00000001C006451D  jnz     short loc_1C0064532
00000001C006451F  mov     edx, 49646641h                 ; 'AfdI'
00000001C0064524  mov     rcx, r15                       ; MasterIrp
00000001C0064527  call    cs:__imp_ExFreePoolWithTag

PATCHED LABEL_42 at 0x1C005A719 (caches the flag before the dereference when enabled):

00000001C005A719  call    EvaluateCurrentState
00000001C005A720  jz      short loc_1C005A72E
00000001C005A722  mov     r15d, [rbx+8]                  ; cache before deref
00000001C005A726  shr     r15d, 8
00000001C005A72A  and     r15b, 1                        ; bit 0x100
00000001C005A732  call    cs:__imp_ObfDereferenceObject
00000001C005A73F  call    EvaluateCurrentState
00000001C005A748  test    r15b, r15b                     ; use cached value
00000001C005A74D  test    dword ptr [rbx+8], 100h        ; else retained old read
00000001C005A756  mov     edx, 49646641h                 ; 'AfdI'
00000001C005A75E  call    cs:__imp_ExFreePoolWithTag

Finding 3: AfdCreateConnection

UNPATCHED gate at 0x1C00494AD (skips device validation when the feature is disabled):

00000001C00494A6  lea     rcx, g_Feature_1484999993_59735560_FeatureDescriptorDetails
00000001C00494AD  call    EvaluateCurrentState
00000001C00494B4  jz      loc_1C0049575                  ; feature off -> skip validation
00000001C00494D3  call    cs:__imp_ObReferenceObjectByHandle
00000001C0049503  call    cs:__imp_IoGetDeviceAttachmentBaseRef

PATCHED at 0x1C0049240 performs ObReferenceObjectByHandle and IoGetDeviceAttachmentBaseRef (0x1C0049510 / 0x1C0049567) with no EvaluateCurrentState gate in front of the validation.


5. Trigger Conditions

Finding 1 (Race Condition): 1. Open \Device\Afd / create an AFD endpoint. 2. Issue TDI I/O-control operations that reach the endpoint dispatch helpers, so the in-flight counter at endpoint +0xf8 is non-zero (only meaningful when g_Feature_3289849145_60513071 is enabled). 3. Concurrently drive a TDI listen completion so AfdTLListenComplete runs with status >= 0, swapping the endpoint dispatch pointers. 4. Without the drain, the old function pointer is invoked while an in-flight dispatch is still running on the same endpoint.

Finding 2 (state flag read after deref): 1. Create an AFD socket and drive a datagram connect that reaches the AfdDoDatagramConnect cleanup path (LABEL_42). 2. The path calls ObfDereferenceObject(a1) and then, when the feature is disabled, reads *(v5+8) to decide whether to free MasterIrp.

Finding 3 (device validation): 1. Open \Device\Afd and create a socket. 2. Prepare a file handle whose device stack differs from the AFD transport device. 3. Issue the AFD connect IOCTL supplying that handle. 4. On unpatched systems where g_Feature_1484999993_59735560 is disabled, the device-consistency check is skipped and the handle is accepted; the patched build returns STATUS_INVALID_PARAMETER.


6. Impact Assessment

  • Finding 1: The change adds draining of in-flight TDI operations before the old endpoint dispatch pointer is invoked. Without it, the pointer swap and the old-pointer invocation are not synchronized against concurrent endpoint operations, which is a kernel data race on the endpoint dispatch fields. The indirect call itself is CFG-guarded (__guard_dispatch_icall_fptr). A demonstrable exploitation primitive beyond a potential race-induced fault is not established from these binaries, and the fix is feature-gated, so realistic impact is rated Medium.
  • Finding 2: The change caches a state flag before a reference drop. Whether the dereference frees the memory backing v5 is not demonstrable from these binaries; the flag only controls whether MasterIrp is freed. This is a defensive lifetime hardening; no reachable memory-corruption primitive is established, so it is rated Low.
  • Finding 3: Making the device-consistency validation unconditional prevents associating a handle from a mismatched device stack with an AFD connection when the feature would otherwise be disabled. Impact is a logic/validation gap; downstream type-confusion consequences are not demonstrated here. Rated Medium.

7. Debugger Playbook

For an analyst with a kernel debugger attached to afd_unpatched.sys:

Finding 1: AfdTLListenComplete

  • Breakpoints: text bp afd_unpatched.sys+0x597d0 ; AfdTLListenComplete entry bp afd_unpatched.sys+0x5982a ; loads old function pointer bp afd_unpatched.sys+0x59853 ; guarded indirect call
  • At entry, rbx (set at 0x1C005980C from [r10+18h]) is the endpoint; ebp is the IRP status (must be >= 0 for the success path). Inspect [rbx+0x10] and [rbx+0x18] before and after the swap at 0x1C0059843/0x1C0059847.

Finding 2: AfdDoDatagramConnect

  • Breakpoint: text bp afd_unpatched.sys+0x64510 ; ObfDereferenceObject in LABEL_42
  • After the call returns, the instruction at 0x1C0064516 (test dword ptr [rbx+8], 100h) reads the state flag; inspect the pool backing rbx to determine validity.

Finding 3: AfdCreateConnection

  • Breakpoint: text bp afd_unpatched.sys+0x494ad ; EvaluateCurrentState gate
  • Observe the return value in eax; when 0, the jz at 0x1C00494B4 skips ObReferenceObjectByHandle and the device-match comparison.

8. Changed Functions — Full Triage

Security-relevant changes: - AfdAddConnectedReference @ 0x1C0047C0C (Sim: 0.2164): Adds the g_Feature_3289849145_60513071 gate; when enabled, moves the connected-reference counter update to be atomic with the lock. Part of the in-flight synchronization mechanism (Finding 1). - AfdTLListenComplete @ 0x1C00597D0 (Sim: 0.2987): Adds the feature-gated spin-wait on the endpoint in-flight counter [rbx+0xf8] after swapping the dispatch context/function pointer and before invoking the old function pointer (Finding 1). - AfdTLPauseUnPause @ 0x1C0058550 (Sim: 0.8123): Wraps its TDI dispatch call with the feature-gated in-flight counter increment/decrement (Finding 1). - AfdQueryCompartmentId @ 0x1C00357D0 (Sim: 0.8946): Wraps its TDI dispatch call with the feature-gated in-flight counter increment/decrement at endpoint +0xf8 (Finding 1). - AfdDoDatagramConnect @ 0x1C0059588 (Sim: 0.9056): Caches the MasterIrp free flag before ObfDereferenceObject in the cleanup path, gated by g_Feature_566223161_61092118 (Finding 2). - AfdCreateConnection @ 0x1C00491B0 (Sim: 0.9365): Removes the g_Feature_1484999993_59735560 gate, making the device-consistency validation unconditional (Finding 3). - AfdCreateConnection_Old @ 0x1C0015AF8 (Sim: 0.9600): Same gate removal / unconditional validation as AfdCreateConnection (Finding 3). - AfdGetAddress @ 0x1C0035A00 (Sim: 0.9733): Wraps its recv-path TDI dispatch call with the feature-gated in-flight counter increment/decrement at endpoint +0xf8 (Finding 1).

Behavioral / structural changes (endpoint field inserted at +0xf8, shifting later offsets, e.g. 0x158 -> 0x160): - AfdConnect @ 0x1C0047FD0: Connect/accept dispatch; offset shifts and updated callee addresses. - AfdTLConnectEventHandler @ 0x1C0056800: Large function; offset shifts and callee address changes. - AfdSetInformation @ 0x1C00543F0: Offset shifts and callee address changes; no new security checks. - AfdTLDelayAcceptListenComplete @ 0x1C0068280: Dispatcher; updated callee addresses. - AfdCleanupCore @ 0x1C004A560: Endpoint teardown; offset shifts and callee address updates. - AfdGetInformation @ 0x1C0036010: Offset shifts and callee address changes; no new security checks.


9. Unmatched Functions

  • Added: None
  • Removed: None

10. Confidence & Caveats

Confidence Level: High on the mechanism and direction of each change; measured on impact.

Caveats: - Findings 1 and 2 are staged behind runtime feature flags (g_Feature_3289849145_60513071 and g_Feature_566223161_61092118) via EvaluateCurrentState, with the original behavior retained when the flag is disabled. Their effect at runtime depends on the feature's deployed state, which cannot be determined from the binaries alone. - Finding 3 is delivered unconditionally: the gate symbol g_Feature_1484999993_59735560 is present in the unpatched decompilation of AfdCreateConnection and AfdCreateConnection_Old and absent from the patched build. - For Finding 2, whether ObfDereferenceObject(a1) frees the allocation backing v5 = *(a1+0x18) is not demonstrable from these binaries; the change is characterized as a defensive lifetime hardening rather than a proven use-after-free. - The indirect call in Finding 1 is CFG-guarded; no exploitation primitive beyond a potential race-induced fault is established here.