1. Overview

  • Unpatched Binary: ndis_unpatched.sys
  • Patched Binary: ndis_patched.sys
  • Overall Similarity: 0.9858
  • Diff Statistics: 3697 matched functions, 29 changed functions, 3668 identical functions.
  • Verdict: The patched build adds two Windows Feature-Staging (WIL "velocity") gates and, behind them, stages new synchronization behavior in (a) the miniport check-for-hang / wake-up DPC path and (b) the NDIS interface-block (_NDIS_IF_BLOCK) create/delete lifecycle. In every affected function the pre-existing code path is fully retained and executes unchanged when the feature is disabled. No security-relevant behavior change is delivered unconditionally by this build; the changes are a staged rollout controlled at runtime by the feature-management store.

The two gates are:

  • Feature_CFHCancelTimerFix__private_IsEnabledDeviceUsageNoInline (at 0x140092D98) — gates the DPC/check-for-hang path.
  • Feature_NDPQualitySummer26__private_IsEnabledDeviceUsageNoInline (at 0x1400A4B60) — gates the interface-block lifecycle path.

Both are standard WIL feature checks: IsEnabled reads a runtime featureState byte (and otherwise falls back to the compiled feature descriptor). When the feature resolves to disabled, the original path runs.


2. Change Summary

Change 1: Miniport wake-up / check-for-hang DPC — KeSetEvent moved under spinlock (feature-gated)

  • Delivered severity: None (feature-staged, old path retained).
  • Affected functions: ndisMWakeUpDpcX (unpatched 0x14007CA70, patched 0x14007ABE0), ndisQueuedCheckForHang (unpatched 0x14007CC60, patched 0x14007AE20).
  • Nature of the pre-existing pattern: In the unpatched code these DPC/work-item callbacks signal the miniport block's embedded event at +0x778 (KeSetEvent) after the adapter spinlock at +0x60 has been released, and then re-acquire the spinlock to touch the block again. That is a window in which the signalled event's owner could act on the block before the callback finishes touching it.
  • What the patch stages: When Feature_CFHCancelTimerFix resolves enabled, a new branch acquires the +0x60 spinlock first, calls KeSetEvent(+0x778) while holding it, and releases afterward. When the feature is disabled, the function runs the original (lock-released-then-signal) path, byte-for-byte equivalent to the unpatched build.
  • Reachability of the pattern: These are internal miniport-block callbacks driven by the NDIS check-for-hang timer / wake-up DPC and adapter halt/reset sequencing. They are not reachable as an unprivileged user-mode request; there is no demonstrated attacker-controlled trigger for the teardown race in this corpus.

Change 2: NDIS interface-block create/delete handshake (feature-gated)

  • Delivered severity: None (feature-staged, old path retained).
  • Affected functions (representative): ndisIfCreateInterfaceFromPersistentStore (unpatched 0x14006C070, patched 0x140089D40), ndisIfRegisterInterfaceEx (unpatched 0x14007AF90), IFBLOCK_DECREMENT_REF (unpatched 0x140011220), ndisIfInitializePhase1 (0x140193604), plus watchdog/timeout reporting helpers.
  • What the patch stages: When Feature_NDPQualitySummer26 resolves enabled, interface-creation paths that find an interface block still pending deletion park a local KEVENT in the block's PendingDeletionComplete field and wait (ndisWaitForKernelObject) for the deletion to finish before proceeding; the ref-decrement/free path (IFBLOCK_DECREMENT_REF) signals that same PendingDeletionComplete event just before ExFreePoolWithTag. When the feature is disabled, the original list lookup runs and the new event field is neither parked nor signalled.
  • Reachability of the pattern: ndisIfCreateInterfaceFromPersistentStore is reached from ndisLoadNetworkInterfaceFromPersistedState, i.e. loading persisted interface state from the registry during interface initialization — not an unprivileged user-mode OID/IOCTL request. The interface list is protected by the same spinlock (WPP_MAIN_CB.Reserved) in both builds and in both branches.

3. Pseudocode Diff

ndisMWakeUpDpcX (unpatched 0x14007CA70 → patched 0x14007ABE0)

// === UNPATCHED (early-exit / halting path) ===
KeReleaseSpinLockFromDpcLevel(block + 0x60);   // 0x14007CBE9 spinlock released
KeSetEvent(block + 0x778, 0, 0);               // 0x14007CC01 event signalled, lock not held

// === UNPATCHED (deferred/bpl path) ===
KeSetEvent(block + 0x778, 0, 0);               // 0x14007CB79 event signalled, lock not held
KeAcquireSpinLockAtDpcLevel(block + 0x60);     // 0x14007CB89 re-acquired afterward

// === PATCHED ===
if (Feature_CFHCancelTimerFix__private_IsEnabledDeviceUsageNoInline()) { // 0x14007ACF9
    KeAcquireSpinLockAtDpcLevel(block + 0x60);   // 0x14007AD14 acquire FIRST
    block[0x208] = <current thread ptr>;
    if (need_signal) KeSetEvent(block + 0x778, 0, 0); // 0x14007AD41 under spinlock
    ...
    KeReleaseSpinLockFromDpcLevel(block + 0x60); // 0x14007ADB9 release AFTER
} else {
    // 0x14007AD54: ORIGINAL path retained, byte-equivalent to unpatched
    if (need_signal) KeSetEvent(block + 0x778, 0, 0); // 0x14007AD65 lock not held
    KeAcquireSpinLockAtDpcLevel(block + 0x60);        // 0x14007AD7A re-acquire afterward
    ...
}

ndisIfCreateInterfaceFromPersistentStore (unpatched 0x14006C070 → patched 0x140089D40)

// === PATCHED entry ===
if (Feature_NDPQualitySummer26__private_IsEnabledDeviceUsageNoInline() == 0) {
    // ORIGINAL path retained: look up interface by NetLuid under the same
    // spinlock (WPP_MAIN_CB.Reserved), release, proceed. Equivalent to unpatched.
} else {
    KeInitializeEvent(&Event, NotificationEvent, 0);
    // if the matching block is pending deletion, park &Event in
    // block->PendingDeletionComplete and wait via ndisWaitForKernelObject,
    // then re-verify before continuing.
}
// === PATCHED IFBLOCK_DECREMENT_REF (frees the block) ===
if (block->AsyncEvent) KeSetEvent(block->AsyncEvent, 0, 0);
if (Feature_NDPQualitySummer26__private_IsEnabledDeviceUsageNoInline() != 0) {
    if (block->PendingDeletionComplete)
        KeSetEvent(block->PendingDeletionComplete, 0, 0);   // new handshake, gated
}
ExFreePoolWithTag(block, 0);

4. Assembly Analysis

ndisMWakeUpDpcX unpatched (0x14007CA70) — signal after spinlock release

; deferred/bpl path
000000014007CAEC  lea     rcx, [rbx+60h]                 ; SpinLock
000000014007CAFB  call    cs:__imp_KeReleaseSpinLockFromDpcLevel
...
000000014007CB6D  lea     rcx, [rbx+778h]                ; Event
000000014007CB79  call    cs:__imp_KeSetEvent            ; signalled, +0x60 lock not held
000000014007CB85  lea     rcx, [rbx+60h]
000000014007CB89  call    cs:__imp_KeAcquireSpinLockAtDpcLevel

; early-exit / halting path
000000014007CBDA  lea     rcx, [rbx+60h]
000000014007CBE9  call    cs:__imp_KeReleaseSpinLockFromDpcLevel
000000014007CBF5  lea     rcx, [rbx+778h]                ; Event
000000014007CC01  call    cs:__imp_KeSetEvent            ; signalled after release

ndisMWakeUpDpcX patched (0x14007ABE0) — feature gate with retained old path

000000014007ACF9  call    Feature_CFHCancelTimerFix__private_IsEnabledDeviceUsageNoInline
000000014007ACFE  test    eax, eax
000000014007AD00  jz      short loc_14007AD54            ; feature OFF -> original path

; feature ON: signal under spinlock
000000014007AD14  call    cs:__imp_KeAcquireSpinLockAtDpcLevel   ; acquire FIRST
000000014007AD35  lea     rcx, [rbx+778h]
000000014007AD41  call    cs:__imp_KeSetEvent                    ; under spinlock
000000014007ADB9  call    cs:__imp_KeReleaseSpinLockFromDpcLevel ; release AFTER

; feature OFF (loc_14007AD54): identical to unpatched
000000014007AD59  lea     rcx, [rbx+778h]
000000014007AD65  call    cs:__imp_KeSetEvent                    ; lock not held
000000014007AD7A  call    cs:__imp_KeAcquireSpinLockAtDpcLevel   ; re-acquire afterward

ndisQueuedCheckForHang patched (0x14007AE20) — same gate

000000014007B0B5  call    Feature_CFHCancelTimerFix__private_IsEnabledDeviceUsageNoInline
000000014007B0BA  test    eax, eax
000000014007B0BC  jnz     loc_14007B197                  ; feature ON -> signal under spinlock

; feature OFF fallthrough (original path, matches unpatched 0x14007CF01)
000000014007B0C2  lea     rcx, [rdi+778h]
000000014007B0CE  call    cs:__imp_KeSetEvent            ; lock not held
000000014007B0EC  call    cs:__imp_KeAcquireSpinLockRaiseToDpc   ; re-acquire afterward

; feature ON (loc_14007B197): acquire first, signal under lock, release after
000000014007B19B  call    cs:__imp_KeAcquireSpinLockRaiseToDpc
000000014007B1BC  lea     rcx, [rdi+778h]
000000014007B1C6  call    cs:__imp_KeSetEvent            ; under spinlock
000000014007B207  call    cs:__imp_KeReleaseSpinLock

The unpatched ndisQueuedCheckForHang counterpart signals at 0x14007CEF5/0x14007CF01 (KeSetEvent [rdi+778h]) with the +0x60 spinlock not held, matching the patched feature-OFF branch.


5. Trigger / Reachability

  • The DPC/check-for-hang callbacks (ndisMWakeUpDpcX, ndisQueuedCheckForHang) run on the NDIS check-for-hang timer and wake-up/reset sequencing for a miniport block. They are internal to the miniport lifecycle and are not driven by an unprivileged user-mode request in this corpus.
  • ndisIfCreateInterfaceFromPersistentStore is invoked from ndisLoadNetworkInterfaceFromPersistedState, i.e. reconstructing interface state from persisted (registry) data during interface initialization. It is not an OID/IOCTL handler.
  • Because the security-relevant behavior is gated by runtime feature flags with the original path retained, no behavior change is delivered unconditionally, and there is no demonstrated attacker-reachable trigger that this build closes.

6. Exploitability

No exploit primitive is demonstrable from the two builds:

  • The change is a staged synchronization tightening, not the removal of an established, reachable primitive. With the feature disabled, the patched build behaves exactly like the unpatched build.
  • The embedded event at miniport-block offset +0x778 is real (lea rcx, [rbx+778h] / [rdi+778h]). No pool tag, allocation size, or object-replacement path for a use-after-free is evidenced by these binaries.
  • Whether the pre-existing lock-released-then-signal ordering is an exploitable use-after-free or a benign timing hardening is not determinable from this corpus; the callbacks reference-count the miniport block (ndisReferenceMiniport / ndisDereferenceMiniport) around the work in ndisMWakeUpDpcX.

7. Analyst Reference

Real functions and offsets for anyone tracing these paths (no exploitation steps are implied):

ndisMWakeUpDpcX          unpatched 0x14007CA70   patched 0x14007ABE0
ndisQueuedCheckForHang   unpatched 0x14007CC60   patched 0x14007AE20
ndisIfCreateInterfaceFromPersistentStore  unpatched 0x14006C070  patched 0x140089D40
Feature_CFHCancelTimerFix  IsEnabled @ 0x140092D98
Feature_NDPQualitySummer26 IsEnabled @ 0x1400A4B60

Key structure offsets observed in the disassembly:

  • +0x60 miniport-block spinlock (KeAcquireSpinLockAtDpcLevel / KeAcquireSpinLockRaiseToDpc).
  • +0x778 embedded event signalled by the DPC/check-for-hang path.
  • +0x78 miniport state dword (sign bit tested to detect halting).
  • +0x7c miniport flags (mask 0x20080000 tested).

The interface-block handshake uses the block's PendingDeletionComplete field, signalled in IFBLOCK_DECREMENT_REF and waited on in the interface-create paths, both under Feature_NDPQualitySummer26.


8. Changed Functions — Triage

All security-relevant deltas are one of the two feature-staged groups above; the remainder is tracing/telemetry and initialization churn.

  • ndisMWakeUpDpcX (0x14007CA700x14007ABE0): Feature_CFHCancelTimerFix gate added; new branch signals +0x778 under the +0x60 spinlock; original path retained.
  • ndisQueuedCheckForHang (0x14007CC600x14007AE20): same gate; new branch signals +0x778 under spinlock; original path retained.
  • ndisIfCreateInterfaceFromPersistentStore (0x14006C0700x140089D40): Feature_NDPQualitySummer26 gate added; new branch adds KEVENT/PendingDeletionComplete wait-for-deletion handshake; original lookup path retained (feature == 0).
  • ndisIfRegisterInterfaceEx (0x14007AF90): same feature gate; adds the pending-deletion wait handshake around interface registration; original path retained.
  • IFBLOCK_DECREMENT_REF (0x140011220): same feature gate; signals the new PendingDeletionComplete event before ExFreePoolWithTag; original free path otherwise unchanged.
  • ndisNsiEnumerateAllInterfaceInformation (0x140011840), ndisNsiGetInterfaceInformation (0x140013A20), ndisNsiGetAllInterfaceInformation (0x140016110): NSI (Network Store Interface) query paths; interface-list traversal adjusted alongside the same interface-lifecycle work; no delivered behavior change with the feature off.
  • ndisIfInitializePhase1 (0x140193604): initialization of the additional interface list used by the gated handshake.
  • Other changed functions (Feature_CFHCancelTimerFix/Feature_NDPQualitySummer26 feature helpers, ndisWdfNotifyStuckOperation, WPP_RECORDER_SF_* tracing helpers, NdisWdfRegisterCx structure-size bump 0xA00xA8, and various init reordering): telemetry/tracing, structure-size, and staging-support churn; not security fixes.

9. Unmatched / Added-Removed Functions

  • Added in patched: the two WIL feature helpers Feature_CFHCancelTimerFix__private_IsEnabled* and Feature_NDPQualitySummer26__private_IsEnabled*, ndisWdfNotifyStuckOperation, and a set of WPP_RECORDER_SF_* tracing thunks.
  • Removed in unpatched: the older Feature_2332236090__private_IsEnabled* helpers and a few interface-index/lambda helpers that were folded into the reorganized code.
  • These reflect the feature-staging reorganization, not new standalone security logic.

10. Confidence & Caveats

  • Confidence: High that this build delivers no unconditional security-relevant change. Both affected code groups are gated by WIL feature checks (0x140092D98, 0x1400A4B60) with the pre-existing path retained and selected when the feature resolves disabled.
  • What is genuine: the unpatched lock-released-then-signal ordering at 0x14007CC01/0x14007CB79 (and the ndisQueuedCheckForHang counterpart at 0x14007CF01) exists as described, and the patched build stages a version that signals under the spinlock.
  • What is not supported by these binaries: any unprivileged user-mode OID/IOCTL trigger, any pool tag or allocation size, and any use-after-free-to-code-execution primitive. Those are not present in the disassembly and are omitted.
  • Runtime default: whether either feature resolves enabled at runtime is decided by the feature-management store, not by these binaries; it cannot be asserted from static analysis.