1. Overview

  • Unpatched Binary: ndis_unpatched.sys
  • Patched Binary: ndis_patched.sys
  • Overall Similarity Score: 0.9423
  • Diff Statistics (name-matched, content-normalized):
  • Named functions unpatched / patched: 3582 / 3697
  • Changed (name present in both, body differs after masking relocated addresses): 913
  • Symbols only in patched: 156
  • Symbols only in unpatched: 41
  • Verdict: No delivered security fix. The change set is a routine servicing rebuild: rotation of WIL feature-staging flags between servicing waves, new crash-triage (KTRIAGE) C++ helper classes, and compiler-rebuild churn (register allocation, inlining, helper relocation). The function originally flagged as a use-after-free fix, ndisNicQuietCheckRef, has an identical state-guard in both builds; the only real behavioral delta in it is a WIL feature-staging gate (Feature_SSSurpriseRemoval_Fix) whose off-by-default path preserves the unpatched behavior.

2. Vulnerability Summary

ndisNicQuietCheckRef — claimed "missing 0x17c state check" is not missing

  • Severity: None (no security-relevant change)
  • Vulnerability Class: N/A — the state guard is present and identical in both builds
  • Affected Function: ndisNicQuietCheckRef (unpatched 0x14004BFD0, patched 0x140048990)

What was actually checked: ndisNicQuietCheckRef acquires the AoAc (Always On / Always Connected) sub-structure spin lock from *(MiniportBlock + 0x1168) and only proceeds to the NIC-quiet / idle-submit path when three state fields are all zero: +0x34, +0x17c, and +0x178.

  • In the unpatched build the +0x34 and +0x17c tests are performed inside the helper ndisAoAcIsD0Required (0x14004C1E0), which returns 1 if either field is non-zero. The caller proceeds only when that helper returns 0 and *(SpinLock + 0x178) == 0.
  • In the patched build the same helper was inlined: the caller directly tests +0x34, +0x17c, and +0x178. The helper itself still exists (relocated to 0x14004A090) and still checks +0x34 then +0x17c.

The gating condition for proceeding is therefore byte-for-byte equivalent between builds. There is no added state check and no removed state check. The +0x17c field is validated in both directions. The earlier characterization of this function as an "adapter halt/pause state machine" with a missing transitional-state check is not supported by the disassembly.

The one real behavioral delta: the patched build adds a WIL feature-staging gate. After releasing the lock, patched ndisNicQuietCheckRef calls Feature_SSSurpriseRemoval_Fix__private_IsEnabledDeviceUsageNoInline (0x140091624). When the feature is disabled (the default state for a staged flag), control follows the same path as the unpatched build, calling ndisSelectiveSuspendStop. When the feature is enabled, it additionally consults adapter flag bit 31 at *(MiniportBlock + 0x78) to choose between ndisSelectiveSuspendStop and the newly added ndisSelectiveSuspendStopWdf. This is a staged rollout with the old path retained, not a delivered fix, and it does not alter the state guard analyzed above.


3. Pseudocode Diff

The guard that decides whether ndisNicQuietCheckRef proceeds is equivalent in both builds. The only difference is that the patched build inlines the helper.

// Unpatched ndisNicQuietCheckRef @ 0x14004BFD0
// helper ndisAoAcIsD0Required @ 0x14004C1E0:
//   return (*(SpinLock+0x34) != 0) || (*(SpinLock+0x17c) != 0);
SpinLock = *(MiniportBlock + 0x1168);
KeAcquireSpinLockRaiseToDpc(SpinLock);
if (ndisAoAcIsD0Required(SpinLock))       // tests +0x34 AND +0x17c
    ... release, return 0;                 // does not enter quiet path
if (*(SpinLock + 0x178) != 0)
    ... release, return 0;
// proceed to NIC-quiet / idle submit

// Patched ndisNicQuietCheckRef @ 0x140048990 (same guard, helper inlined)
SpinLock = *(MiniportBlock + 0x1168);
KeAcquireSpinLockRaiseToDpc(SpinLock);
if (*(SpinLock + 0x34) || *(SpinLock + 0x17c) || *(SpinLock + 0x178))
    ... release, return 0;
// proceed to NIC-quiet / idle submit
// ... later, after releasing the lock:
if (Feature_SSSurpriseRemoval_Fix__private_IsEnabledDeviceUsageNoInline()) {
    // staged path: pick ndisSelectiveSuspendStop vs ndisSelectiveSuspendStopWdf
    // based on bit 31 of *(MiniportBlock + 0x78)
} // else: same call as unpatched (ndisSelectiveSuspendStop)

4. Assembly Analysis

Real instructions from both builds. The three-field guard is present in both; only the helper is inlined.

; === Unpatched ndisNicQuietCheckRef @ 0x14004BFD0 ===
000000014004BFDD  mov     rbx, [rcx+1168h]           ; SpinLock (AoAc sub-structure)
000000014004BFF6  mov     rcx, rbx
000000014004BFFD  call    ?ndisAoAcIsD0Required@@... ; checks +0x34 and +0x17c
000000014004C002  test    al, al
000000014004C004  jz      short loc_14004C042        ; helper==0 -> continue to 0x178 check
; ... fall-through / non-quiet return when helper!=0 ...
000000014004C042  cmp     dword ptr [rbx+178h], 0    ; third field
000000014004C049  jnz     short loc_14004C006        ; non-zero -> do not enter quiet path

; helper ?ndisAoAcIsD0Required@@... @ 0x14004C1E0
000000014004C1E0  cmp     dword ptr [rcx+34h], 0
000000014004C1E4  jz      short loc_14004C1EA
000000014004C1E6  mov     al, 1
000000014004C1E8  retn
000000014004C1EA  cmp     dword ptr [rcx+17Ch], 0    ; +0x17c IS checked in the unpatched build
000000014004C1F1  jnz     short loc_14004C1E6
000000014004C1F3  xor     al, al
000000014004C1F5  retn

; === Patched ndisNicQuietCheckRef @ 0x140048990 (same guard, helper inlined) ===
000000014004899C  mov     rdi, [rcx+1168h]           ; SpinLock
00000001400489B5  cmp     dword ptr [rdi+34h], 0
00000001400489B9  movzx   ebp, al
00000001400489BC  jnz     loc_140048C2C
00000001400489C2  cmp     dword ptr [rdi+17Ch], 0    ; same +0x17c check, now inline
00000001400489C9  jnz     loc_140048C2C
00000001400489CF  cmp     dword ptr [rdi+178h], 0
00000001400489D6  jnz     loc_140048C2C
; ... proceed to quiet path ...

; patched staged gate
0000000140048A8C  call    Feature_SSSurpriseRemoval_Fix__private_IsEnabledDeviceUsageNoInline
0000000140048A98  test    eax, eax
0000000140048A9A  jnz     loc_140048B7E              ; feature-on path
; loc_140048B7E selects ndisSelectiveSuspendStopWdf vs ndisSelectiveSuspendStop
0000000140048B87  mov     eax, [rbx+78h]
0000000140048B8A  test    al, al
0000000140048B8C  jns     loc_140048AA5             ; bit 31 clear -> old path
0000000140048B9A  call    ?ndisSelectiveSuspendStopWdf@@...

The helper ndisAoAcIsD0Required is retained in the patched build at 0x14004A090 with the identical +0x34 / +0x17c logic, confirming no state check was added or removed.


5. Trigger Conditions

None. Because the +0x34 / +0x17c / +0x178 guard is identical in both builds, there is no code path present in the unpatched build and absent in the patched build (or vice-versa) that a race could exercise differently. The prior scenario describing a PnP-surprise-removal race that bypasses a missing +0x17c check does not correspond to the binaries and is withdrawn.


6. Exploit Primitive & Development Notes

No exploit primitive. No use-after-free, double-completion, or pool-corruption primitive is demonstrable, because the state guard is equivalent across builds and the only behavioral delta is a feature-staging gate whose default (off) path matches the unpatched build. No heap-groom, KASLR-defeat, or mitigation-bypass discussion is applicable.


7. Debugger Notes

No vulnerability-specific breakpoint plan applies. For reference only, the state-guard fields live in the AoAc sub-structure reached from *(MiniportBlock + 0x1168):

  • +0x34 and +0x17c — checked by ndisAoAcIsD0Required (unpatched 0x14004C1E0, patched 0x14004A090) and, in the patched build, inline in ndisNicQuietCheckRef at 0x1400489B5 / 0x1400489C2.
  • +0x178 — checked in both builds (unpatched 0x14004C042, patched 0x1400489CF).

Setting a breakpoint on either build's ndisNicQuietCheckRef and observing the entry guard shows the same accept/reject decision for the same field values; there is no divergence to trigger.


8. Changed Functions — Full Triage

An independent, content-normalized, name-matched diff (masking relocated sub_/loc_/data_ tokens and absolute addresses) found 913 changed named functions, 156 symbols only in the patched build, and 41 only in the unpatched build. The dominant nature of the change set:

  • WIL feature-staging flag rotation (servicing churn, not fixes): New staged flags appear in the patched build (Feature_SSSurpriseRemoval_Fix, Feature_CFHCancelTimerFix, Feature_NDPQualitySpring26, Feature_NDPQualitySummer26, Feature_NDPSfiSpring2026, Feature_TCPIP_SFI_60615470_Fix, Feature_UDFDOD) while older ones are gone from the unpatched build (Feature_SSDeadlock_Fix, Feature_IfCompartmentKasanFix, Feature_MiniportFlagsKcsanFix, Feature_NDPQualitySpring25, Feature_NDPCyberEO2508, several Feature_TCPIP_2025_*). These Feature_*__private_IsEnabled* gates are staged-rollout controls; where present, the old path is retained. They are not delivered security changes.

  • Crash-triage (KTRIAGE) dump support (diagnostics, not a fix): Many patched-only symbols are C++ triage-dump machinery (TriageData, ObjectTriageData<...>, MakeSizedPoolPtrNP, AddDataBlock@TriageDataArray, destructors for _NDIS_*_BLOCK) plus TraceLogging/ETW template instantiations. This is added diagnostic instrumentation.

  • Compiler-rebuild churn (majority): Most of the 913 changed functions differ only in register allocation, inlining decisions, helper relocation, and stack-frame layout. This accounts for the changes in the functions listed below; none carries an attacker-reachable behavioral fix.

  • ndisNicQuietCheckRef: Helper ndisAoAcIsD0Required inlined; Feature_SSSurpriseRemoval_Fix staging gate added with the old path retained. State guard unchanged. Not security-relevant.

  • EthFilterDprIndicateReceive: Changed body, attributable to rebuild churn. The previously asserted mechanism (a data_14011ead8-gated sign-extending flag read) is not present — no such global exists in either binary. No behavioral security change confirmed.

  • NdisFSendNetBufferLists: Changed body consistent with rebuild churn (helper relocation, branch reshaping). Its runtime verifier twin ndisVerifierNdisFSendNetBufferLists is identical across builds. No list-corruption or use-after-free fix is demonstrable.

  • ndisMDeferredSendPackets (0x1400DC4F0): Changed body; the previously claimed while(true) → bit-22 loop-termination fix could not be substantiated as a security change and its stated data_14011ead8 mechanism does not exist. Treated as rebuild churn.

  • NdisCoOidRequest, NdisMOidRequestComplete, NdisMPromoteMiniport, NdisMSetAttributesEx, NdisGetReceivedPacket: Changed bodies attributable to rebuild churn. The data_14011ead8 / sub_1400537f0 sign-extending-helper mechanism previously attributed to them does not exist in either binary.


9. Unmatched Functions

The name-matched diff is not empty: 156 symbols exist only in the patched build and 41 only in the unpatched build. These are almost entirely servicing artifacts — WIL feature-staging descriptors, KTRIAGE dump C++ classes, TraceLogging template instantiations, WPP recorder helpers, and small relocated helpers (for example the patched-only ndisSelectiveSuspendStopWdf, ndisWdfNotifyStuckOperation, ndisIncrementAsyncIdleCountersLocked, and the unpatched-only ndisIfCompartmentNotificationWorker, ndisMIsOidTimedOut, ndisNsiScheduleCompartmentBlockChangeNotification). None introduces or removes a delivered mitigation that constitutes a security fix.


10. Confidence & Caveats

  • Confidence Level: High that there is no delivered, attacker-reachable security fix in this diff.
  • Rationale: The single previously claimed finding rests on a +0x17c check that is demonstrably present in both builds (unpatched via ndisAoAcIsD0Required at 0x14004C1E0, patched inline plus retained helper at 0x14004A090). The pervasive data_14011ead8 "feature flag" that the change set was attributed to does not appear anywhere in either binary's disassembly or decompilation. The remaining differences are WIL feature-staging rotation, KTRIAGE diagnostic additions, and compiler-rebuild churn.
  • Residual note: Feature_SSSurpriseRemoval_Fix and sibling staged flags indicate work that Microsoft is rolling out behind gates. Should a future build enable such a flag by default and remove the old path, that build would need to be re-evaluated; in this pair the gates leave the unpatched behavior intact by default.