1. Overview

  • Unpatched Binary: lan7800-x64-n650f_unpatched.sys
  • Patched Binary: lan7800-x64-n650f_patched.sys
  • Overall Similarity: 0.978
  • Diff Statistics: 244 matched functions, 28 changed, 216 identical, 0 unmatched in either direction.
  • Verdict: The patch adds power-state synchronization to the USB receive path. MPIdleNotification now sets a "selective-suspend pending" flag (bit 27, 0x8000000, at adapter offset 0x44) before confirming USB idle, and the bulk-in receive processing loop NICProcessBulkInWaitList now checks that flag and stops processing once selective suspend is confirmed. Without it, the receive loop could keep dequeuing, processing, and freeing Receive Control Blocks (RCBs) concurrently with selective-suspend teardown, a race that can lead to use-after-free of receive control blocks (CWE-416). A second reported path (NICStopRxPath / NICCollectSplitRxPackets) was examined and found NOT to be a use-after-free; it is a robustness change only (see Finding 2).

2. Vulnerability Summary

Finding 1: Receive-processing loop continues during selective-suspend teardown

  • Severity: Medium
  • Vulnerability Class: Race condition (CWE-362) leading to use-after-free (CWE-416)
  • Affected Functions: MPIdleNotification, NICProcessBulkInWaitList

Root Cause: When the USB host controller decides the LAN7800 adapter is idle, the NDIS selective-suspend engine calls MPIdleNotification, which confirms the idle transition to D3 via NdisMIdleNotificationConfirm. In the unpatched driver this confirmation happens without setting any state flag that the concurrent bulk-in receive processing loop can observe. NICProcessBulkInWaitList walks the bulk-in wait list, dequeues RCBs, calls NICProcessSingleReceivePacket / NICProcessMultipleReceivePackets, decrements each RCB's reference count, and frees the RCB via NICFreeRCB when the count reaches 1. Its only exit conditions in the unpatched build are the wait list becoming empty and an elapsed-time cap of 0x989680 (10,000,000 units of the shared-data interrupt-time counter, roughly one second). It has no way to observe that selective suspend has begun tearing down receive resources, so it can keep processing and freeing RCBs during that teardown.

The patched driver sets a suspend-pending flag before confirming idle and makes the receive loop check it, so the loop stops promptly once suspend is confirmed. This closes the race window in which the receive path and the suspend teardown path can both operate on the same receive control blocks.

Call Chain: 1. NDIS USB selective-suspend engine triggers on device idle. 2. MPIdleNotification is called. 3. MPIdleNotification calls NdisMIdleNotificationConfirm, beginning the D3 transition and receive teardown. 4. Concurrently, NICProcessBulkInWaitList continues iterating over the bulk-in wait list. 5. It calls NICProcessSingleReceivePacket / NICProcessMultipleReceivePackets and frees RCBs, which can race with the teardown of those same RCBs.

What was demonstrable vs. inferred: The added flag set in MPIdleNotification and the two added flag checks in NICProcessBulkInWaitList are directly visible in both builds' disassembly (Section 4). The use-after-free consequence is the condition the added synchronization guards against; no exploit primitive or attacker-controlled reclamation of freed memory was demonstrated from the binaries.

Finding 2: Low-power guard added before split-RX drain wait — NOT a use-after-free

  • Severity: No security-relevant change (robustness / latency)
  • Vulnerability Class: None (originally reported as CWE-416; downgraded)
  • Affected Functions: NICStopRxPath, NICCollectSplitRxPackets

Assessment: The patched NICStopRxPath adds a check of the low-power flag (bit 26, 0x4000000, at adapter offset 0x44) before calling NICCollectSplitRxPackets. Examination of NICCollectSplitRxPackets in both builds shows it is a bounded poll-wait: it reads a split-RX pending counter ([rdi+0xF78] unpatched, [rdi+0xFA8] patched), and while that counter is non-zero it sleeps 10 ms via NdisMSleep and re-reads, exiting when the counter reaches 0 or after 100 iterations (roughly one second). It makes no other calls and does not dereference any RCB, NET_BUFFER_LIST, MDL, or USB transfer buffer. It therefore cannot cause a use-after-free.

The low-power flag itself is set and cleared by NICSetPowerState in BOTH builds (via bts/btr on bit 26), so the flag lifecycle is not new. The only new element is NICStopRxPath consulting it. Because NICCollectSplitRxPackets never dereferences freed memory, the guard does not fix a memory-safety bug; its effect is to skip the poll-wait when the device is in a low-power state, where the pending counter would not drain and the routine would otherwise busy-wait up to its ~1-second cap. This is a robustness/latency improvement, not a use-after-free fix. The original UAF claim for this path is withdrawn.

3. Pseudocode Diff

MPIdleNotification & NICProcessBulkInWaitList

// --- UNPATCHED MPIdleNotification ---
NDIS_STATUS MPIdleNotification(Adapter *adapter) {
    NICWaitForReset(adapter);
    if (adapter->power_state_cache /* [+0x104] */ == -1) {
        // no suspend-pending flag set here
        NdisMIdleNotificationConfirm(adapter->ndis_handle /* [+0x78] */, D3);
        return NDIS_STATUS_PENDING;
    }
    return 0;
}

// --- PATCHED MPIdleNotification ---
NDIS_STATUS MPIdleNotification(Adapter *adapter) {
    NICWaitForReset(adapter);
    if (adapter->power_state_cache /* [+0x12C] */ == -1) {
        adapter->flags /* [+0x44] */ |= 0x8000000; // ADDED: set bit 27 (suspend pending)
        NdisMIdleNotificationConfirm(adapter->ndis_handle /* [+0x98] */, D3);
        return NDIS_STATUS_PENDING;
    }
    return 0;
}

// --- UNPATCHED NICProcessBulkInWaitList loop (exit only on empty list / ~1s time cap) ---
// no suspend-flag check
for (;;) {
    rcb = dequeue(waitlist);       // bounded by the 0x989680 elapsed-time cap
    process_and_maybe_free(rcb);
}

// --- PATCHED NICProcessBulkInWaitList loop ---
if (adapter->flags & 0x8000000) goto done;   // ADDED: early exit if suspending
for (;;) {
    rcb = dequeue(waitlist);
    process_and_maybe_free(rcb);
    if (/* empty list */ || /* >0x989680 elapsed */) break;
    if (adapter->flags & 0x8000000) break;    // ADDED: exit if suspend now pending
}

NICStopRxPath & NICCollectSplitRxPackets

// --- UNPATCHED NICStopRxPath ---
void NICStopRxPath(Adapter *adapter, unsigned char is_power_down) {
    // ...
    NICStopBulkIn(adapter);
    if (is_power_down == 0 && adapter->split_rx_pending /* [+0xFF8] */ == 0) {
        NICCollectSplitRxPackets(adapter);   // bounded poll-wait, see below
    }
}

// --- PATCHED NICStopRxPath ---
void NICStopRxPath(Adapter *adapter, unsigned char is_power_down) {
    // ...
    NICStopBulkIn(adapter);
    if (is_power_down == 0 && adapter->split_rx_pending /* [+0x1028] */ == 0
        && (adapter->flags /* [+0x44] */ & 0x4000000) == 0) {   // ADDED low-power guard
        NICCollectSplitRxPackets(adapter);
    }
}

// --- NICCollectSplitRxPackets (both builds, identical logic) ---
void NICCollectSplitRxPackets(Adapter *adapter) {
    int i = 0;
    while (adapter->split_rx_pending != 0) {   // [+0xF78] unpatched / [+0xFA8] patched
        i++;
        if (i % 100 == 0) break;               // cap at 100 iterations
        NdisMSleep(10000);                     // 10 ms
    }
    // no RCB / NBL / MDL / USB buffer access anywhere in this function
}

4. Assembly Analysis

MPIdleNotification (Unpatched @ 0x140001030)

0000000140001030  push    rbx
0000000140001032  sub     rsp, 20h
0000000140001036  mov     rbx, rcx
0000000140001039  call    NICWaitForReset
000000014000103E  mov     eax, [rbx+104h]
0000000140001044  cmp     eax, 0FFFFFFFFh
0000000140001047  jnz     short loc_14000105C
0000000140001049  mov     rcx, [rbx+78h]                 ; NDIS adapter handle
000000014000104D  mov     edx, 3                         ; NdisDeviceStateD3
0000000140001052  call    NdisMIdleNotificationConfirm_0 ; no suspend-pending flag set first
0000000140001057  mov     eax, 103h
000000014000105C  add     rsp, 20h
0000000140001060  pop     rbx
0000000140001061  retn

MPIdleNotification (Patched @ 0x140001034)

0000000140001034  push    rbx
0000000140001036  sub     rsp, 20h
000000014000103A  mov     rbx, rcx
000000014000103D  call    NICWaitForReset
0000000140001042  mov     eax, [rbx+12Ch]
0000000140001048  cmp     eax, 0FFFFFFFFh
000000014000104B  jnz     short loc_140001068
000000014000104D  bts     dword ptr [rbx+44h], 1Bh       ; ADDED: set bit 27 (0x8000000) suspend-pending
0000000140001052  mov     edx, 3                         ; NdisDeviceStateD3
0000000140001057  mov     rcx, [rbx+98h]                 ; NDIS adapter handle
000000014000105E  call    NdisMIdleNotificationConfirm_0
0000000140001063  mov     eax, 103h

NICProcessBulkInWaitList loop exit — unpatched vs patched

; UNPATCHED @ 0x140010C4C  only exit conditions are empty list and the elapsed-time cap:
0000000140010D98  mov     rax, [r13+0]     ; r13 = KUSER_SHARED_DATA interrupt-time
0000000140010D9C  sub     rax, r15
0000000140010D9F  cmp     rax, 989680h     ; 10,000,000 (~1 second)
0000000140010DA5  ja      short loc_140010DDF   ; exit if elapsed
; (no suspend-flag test anywhere in the unpatched function)

; PATCHED @ 0x140010FA0  adds two suspend-flag tests:
0000000140010FE4  test    dword ptr [rbx+44h], 8000000h   ; ADDED: early exit before loop
0000000140010FEB  jnz     loc_14001112A
...
0000000140011115  cmp     rax, 989680h                    ; same ~1s time cap as unpatched
000000014001111B  ja      short loc_14001112A
000000014001111D  test    dword ptr [rbx+44h], 8000000h   ; ADDED: re-check each iteration
0000000140011124  jz      loc_140010FFF                   ; continue only if not suspending
000000014001112A  ; loop exit

NICStopRxPath vulnerable-call comparison

; UNPATCHED @ 0x140012418:
00000001400124BE  test    sil, sil
00000001400124C1  jnz     short loc_1400124D4
00000001400124C3  cmp     dword ptr [rdi+0FF8h], 0
00000001400124CA  jnz     short loc_1400124D4
00000001400124CC  mov     rcx, rdi
00000001400124CF  call    NICCollectSplitRxPackets

; PATCHED @ 0x140012748:
00000001400127EE  test    sil, sil
00000001400127F1  jnz     short loc_14001280D
00000001400127F3  cmp     dword ptr [rdi+1028h], 0
00000001400127FA  jnz     short loc_14001280D
00000001400127FC  test    dword ptr [rdi+44h], 4000000h   ; ADDED: low-power (bit 26) guard
0000000140012803  jnz     short loc_14001280D
0000000140012805  mov     rcx, rdi
0000000140012808  call    NICCollectSplitRxPackets

NICCollectSplitRxPackets (Unpatched @ 0x140010444) — bounded poll-wait, no buffer access

0000000140010444  mov     [rsp+arg_0], rbx
0000000140010449  push    rdi
000000014001044A  sub     rsp, 20h
000000014001044E  mov     rdi, rcx
0000000140010451  xor     ebx, ebx
0000000140010453  cmp     dword ptr [rdi+0F78h], 0        ; poll split-RX pending counter
000000014001045A  jz      short loc_14001047B            ; return when it reaches 0
000000014001045C  inc     ebx
000000014001045E  mov     eax, 51EB851Fh
0000000140010463  mul     ebx
0000000140010465  shr     edx, 5
0000000140010468  imul    ecx, edx, 64h
000000014001046B  cmp     ebx, ecx                        ; ebx == (ebx/100)*100 ?
000000014001046D  jz      short loc_14001047B            ; return after 100 iterations
000000014001046F  mov     ecx, 2710h                      ; 10000 us = 10 ms
0000000140010474  call    NdisMSleep_0
0000000140010479  jmp     short loc_140010453
000000014001047B  mov     rbx, [rsp+28h+arg_0]
0000000140010480  add     rsp, 20h
0000000140010484  pop     rdi
0000000140010485  retn

The patched copy (@ 0x1400107B0) is identical except the counter offset is 0xFA8. Neither version dereferences an RCB, NET_BUFFER_LIST, MDL, or USB transfer buffer.

NICSetPowerState low-power flag (present in BOTH builds)

; UNPATCHED @ 0x14000E254:
000000014000E287  bts     dword ptr [rcx+80h], 1Ah   ; set bit 26 (0x4000000) on entry to low power
000000014000E322  btr     dword ptr [rbx+80h], 1Ah   ; clear bit 26 on resume

; PATCHED @ 0x14000E5E4 (same logic at the shifted offset 0x44):
000000014000E617  bts     dword ptr [rcx+44h], 1Ah
000000014000E6AF  btr     dword ptr [rbx+44h], 1Ah

5. Trigger Conditions

To reach the Finding 1 race window: 1. The LAN7800 USB Ethernet adapter is connected and its driver loaded. 2. USB selective suspend is enabled for the device (registry or OS default). 3. Inbound receive traffic keeps the bulk-in wait list populated so NICProcessBulkInWaitList is actively iterating. 4. The device goes idle and the selective-suspend timer fires, so MPIdleNotification confirms the D3 transition while receive processing is still draining the wait list.

The race is a narrow, timing-dependent window during the power transition and depends on receive processing overlapping with suspend teardown. No attacker-controlled data flow into the freed contents was demonstrated.

Finding 2 is not a vulnerability; no trigger is provided.

6. Exploit Primitive & Development Notes

No exploit primitive was demonstrated from the binaries. The evidence establishes a missing synchronization between the selective-suspend confirmation path and the bulk-in receive processing loop, and the patched build adds a suspend-pending flag plus the corresponding loop checks. Whether the resulting use-after-free of a receive control block can be developed into a controlled read/write or code-execution primitive is not shown by the disassembly and is not claimed here.

7. Debugger PoC Playbook

If attached to a kernel debugger on the unpatched system, the following breakpoints observe the missing synchronization:

  • lan7800!MPIdleNotification (0x140001030) — the selective-suspend confirmation. In the unpatched build no flag is set at [rbx+0x80] before the NdisMIdleNotificationConfirm call at 0x140001052. rbx holds the adapter context.
  • lan7800!NICProcessBulkInWaitList (0x140010C4C) — the receive processing loop. In the unpatched build there is no suspend-flag test; the loop exits only on an empty wait list or when the elapsed-time counter exceeds 0x989680 at 0x140010D9F.

Key offsets for the unpatched binary: - Adapter flags field: +0x80 (bit 26 0x4000000 = low-power state, set/cleared by NICSetPowerState; bit 27 0x8000000 = suspend-pending, which the patched build adds and the unpatched build never sets). - Split-RX pending counter: +0xFF8 (checked by NICStopRxPath) and +0xF78 (polled by NICCollectSplitRxPackets).

Note: the adapter context struct was reorganized between builds; the flags field is at +0x80 in the unpatched binary and +0x44 in the patched binary. Use the unpatched offsets when inspecting the unpatched image.

8. Changed Functions — Full Triage

Security / Behavioral: - MPIdleNotification (0.986): Adds bts dword ptr [rbx+44h], 1Bh (set bit 27, 0x8000000, suspend-pending) before NdisMIdleNotificationConfirm. Security-relevant (Finding 1). - NICProcessBulkInWaitList (0.920): Adds an early test [rbx+44h], 8000000h exit and a per-iteration test [rbx+44h], 8000000h loop-exit check. Both builds already bound the loop by the 0x989680 elapsed-time cap; the change is the added suspend-flag check, not a change from an infinite loop. Security-relevant (Finding 1). - NICStopRxPath (0.945): Adds test [rdi+44h], 4000000h (low-power bit 26) before calling NICCollectSplitRxPackets. Robustness only — the callee is a poll-wait, not a memory-touching routine (Finding 2, downgraded). - NICSetPowerState (0.980): Sets/clears the 0x4000000 low-power flag on D3 entry/resume — this logic is present in BOTH builds. The only differences here are the struct-offset shift and error-log suppression. Not a patch-introduced security change. - NICResetWorkItemCallBack (0.967): Adds a device-removed (0x10) flag check to suppress an error-log write; a caller of NICStopRxPath(adapter, 0). Error-log hardening, not security-relevant. - NICResetAdapter (0.960): Gates its reset/re-init path on a state-flag combination so it no longer re-inits while a state flag is set, and gates its own error-log write behind that flag. Same defensive family as Finding 1; not a memory-safety fix. - USBAbortPipe (0.290): Rebuilt from a bare tail-call stub into a full routine with request setup, error logging, and NTSTATUS translation. USB error-handling modernization, not security-relevant. - USBGetPortStatus (0.487): Adds device-removed detection/flagging (0xC000009D → set 0x10) and error logging. Error handling, not security-relevant. - USBResetPipe (0.985): Now calls the rebuilt USBAbortPipe instead of inlining the WDF abort. Refactor, not security-relevant. - NICReadRegister (0.930) & NICWriteRegister (0.915): Add a 0x10 device-removed flag check before NdisWriteErrorLogEntry to suppress error-log storms. Not security-relevant. - NICInitUSB (0.738): Restructured enumeration with device-removed checks before error logging. Not security-relevant. - NICConfigureTRRegs (0.854): Adds PHY register configuration writes (NICWriteTRPhy). Not security-relevant.

Cosmetic / Struct Shifts: - NICHandleDriverCmd, NICQueryInformation, NICSetInformation, NICRequestWorkItemCallback, NICPeriodicCheckWorkItemCallBack, NICProcessMultipleReceivePackets, NICProcessSingleReceivePacket, NICSetMediaType, NICDeconfigureDevice, NICReadRegParameters, NICPhySpecificInitialization, NICInitializeMedia, NICAddWOLSample, NICAddWolPattern, NICProcessWakeReason. (These are predominantly struct field-offset shifts (for example 0x80 moving to 0x44, indicating a base struct layout change between the compiled versions), decompiler variable-name churn, and minor local additions such as PHY-init register writes, OID feature handlers with their own length validation, and error-log suppression. In NICQueryInformation / NICSetInformation a proprietary OID sets a config bit based on a 4-byte buffer whose length is validated, so there is no over-read. No memory-safety behavioral change identified in this group.)

9. Unmatched Functions

No functions were strictly added or removed; all changes were in-place modifications of existing routines.

10. Confidence & Caveats

  • Confidence Level: High for the direct evidence. The added flag set in MPIdleNotification and the two added flag checks in NICProcessBulkInWaitList are visible in both disassemblies, as is the fact that both loops were already bounded by the 0x989680 time cap. The determination that NICCollectSplitRxPackets is a pure poll-wait (and therefore that Finding 2 is not a use-after-free) is likewise directly readable from the function body.
  • Inferred, not demonstrated: The use-after-free consequence guarded by Finding 1 is inferred from the receive loop freeing RCBs while suspend tears down receive resources. No exploit primitive, freed-memory reclamation, or control-flow hijack was demonstrated.
  • Struct layout: Most adapter-context fields shifted between builds (for example the flags field from +0x80 to +0x44). Use the per-build offsets shown in Section 4 when inspecting memory.