ndis.sys — WIL feature-staged miniport/interface synchronization, old path retained
KB5095051
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(at0x140092D98) — gates the DPC/check-for-hang path.Feature_NDPQualitySummer26__private_IsEnabledDeviceUsageNoInline(at0x1400A4B60) — 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(unpatched0x14007CA70, patched0x14007ABE0),ndisQueuedCheckForHang(unpatched0x14007CC60, patched0x14007AE20). - 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+0x60has 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_CFHCancelTimerFixresolves enabled, a new branch acquires the+0x60spinlock first, callsKeSetEvent(+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(unpatched0x14006C070, patched0x140089D40),ndisIfRegisterInterfaceEx(unpatched0x14007AF90),IFBLOCK_DECREMENT_REF(unpatched0x140011220),ndisIfInitializePhase1(0x140193604), plus watchdog/timeout reporting helpers. - What the patch stages: When
Feature_NDPQualitySummer26resolves enabled, interface-creation paths that find an interface block still pending deletion park a localKEVENTin the block'sPendingDeletionCompletefield and wait (ndisWaitForKernelObject) for the deletion to finish before proceeding; the ref-decrement/free path (IFBLOCK_DECREMENT_REF) signals that samePendingDeletionCompleteevent just beforeExFreePoolWithTag. When the feature is disabled, the original list lookup runs and the new event field is neither parked nor signalled. - Reachability of the pattern:
ndisIfCreateInterfaceFromPersistentStoreis reached fromndisLoadNetworkInterfaceFromPersistedState, 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. ndisIfCreateInterfaceFromPersistentStoreis invoked fromndisLoadNetworkInterfaceFromPersistedState, 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
+0x778is 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 inndisMWakeUpDpcX.
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:
+0x60miniport-block spinlock (KeAcquireSpinLockAtDpcLevel/KeAcquireSpinLockRaiseToDpc).+0x778embedded event signalled by the DPC/check-for-hang path.+0x78miniport state dword (sign bit tested to detect halting).+0x7cminiport flags (mask0x20080000tested).
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(0x14007CA70→0x14007ABE0):Feature_CFHCancelTimerFixgate added; new branch signals+0x778under the+0x60spinlock; original path retained.ndisQueuedCheckForHang(0x14007CC60→0x14007AE20): same gate; new branch signals+0x778under spinlock; original path retained.ndisIfCreateInterfaceFromPersistentStore(0x14006C070→0x140089D40):Feature_NDPQualitySummer26gate added; new branch addsKEVENT/PendingDeletionCompletewait-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 newPendingDeletionCompleteevent beforeExFreePoolWithTag; 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_NDPQualitySummer26feature helpers,ndisWdfNotifyStuckOperation,WPP_RECORDER_SF_*tracing helpers,NdisWdfRegisterCxstructure-size bump0xA0→0xA8, 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*andFeature_NDPQualitySummer26__private_IsEnabled*,ndisWdfNotifyStuckOperation, and a set ofWPP_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 thendisQueuedCheckForHangcounterpart at0x14007CF01) 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.