1. Overview

Field Value
Unpatched binary ndis_unpatched.sys
Patched binary ndis_patched.sys
Overall similarity 0.9424
Matched functions 3576
Changed functions 379
Identical functions 3197
Unmatched (unpatched / patched) 0 / 0

Verdict. No delivered security fix is present. The one code difference the report originally targeted is in ndisMRawIndicateStatusEx (unpatched 0x14005E940, patched 0x14005D9A0), the miniport status-indication handler. For NDIS StatusCode 0x40030053, the patched build moves the inline body into a new callee ndisMIndicatePMCapabilities (0x1400B60E8) and adds a StatusBufferSize >= 0x34 pre-check — but that check runs only when the WIL feature Feature_NDPSfiSpring2026 is enabled; when the feature is off the patched code calls the same unchecked handler, identical to the unpatched behaviour. This is a staged rollout (WIL feature staging with the old path retained), not a delivered fix. The path is reachable only from a miniport driver calling NdisMIndicateStatusEx (kernel mode), not from a user-mode OID request; the StatusBuffer and StatusBufferSize are supplied by the indicating driver, not attacker-sized from user mode.


2. Vulnerability Summary

Finding #1 — Feature-staged length pre-check in ndisMRawIndicateStatusEx StatusCode 0x40030053 path

  • Severity: None (no security-relevant change delivered)
  • Class: At most a latent out-of-bounds read (CWE-125) on a kernel-to-kernel status-indication path; not a delivered fix and not a user-mode-reachable primitive
  • Affected function (unpatched): ndis!ndisMRawIndicateStatusEx (entry 0x14005E940); StatusCode 0x40030053 case begins at 0x14005F17A
  • Patched equivalent: ndis!ndisMRawIndicateStatusEx (entry 0x14005D9A0); the case body was moved into ndis!ndisMIndicatePMCapabilities (0x1400B60E8)

What the code does. ndisMRawIndicateStatusEx(NDIS_MINIPORT_ADAPTER_HANDLE*, NDIS_STATUS_INDICATION*) dispatches on the indication's StatusCode (in r13d). The second argument (r15) is an _NDIS_STATUS_INDICATION; [r15+0x30] is its StatusBuffer and [r15+0x38] is its StatusBufferSize (loaded into edi at function entry). For StatusCode 0x40030053 the unpatched handler loads StatusBuffer into rbx and reads DWORDs at rbx+0x30, rbx+0x08, rbx+0x28, and rbx+0x1c (highest byte accessed 0x33), writing derived flag bits into miniport-block fields rsi+0x464, rsi+0x468, rsi+0x46c. In this case StatusBufferSize is not compared against 0x34 before those reads. The immediately following case (StatusCode 0x40030054, at 0x14005F207) does validate its buffer: cmp ecx, 34h / jb at 0x14005F21C with ecx = [r15+0x38].

Patch behaviour. The patched dispatcher, for StatusCode 0x40030053:

if (Feature_NDPSfiSpring2026__private_IsEnabled...() == 0   // feature OFF
    || StatusBufferSize >= 0x34)                            // feature ON: size ok
    ndisMIndicatePMCapabilities(miniport, indication);      // same field reads as before
else
    <WPP trace record, skip>;

Key points: - The size check cmp edi, 0x34 runs only when Feature_NDPSfiSpring2026 is enabled. When the feature is off, the jz at 0x14005E1E5 jumps straight to the ndisMIndicatePMCapabilities call with no length check — behaviourally identical to the unpatched inline handler. - ndisMIndicatePMCapabilities (0x1400B60E8, present only in the patched build) contains no internal size check; its body reads StatusBuffer+0x30/+0x08/+0x28/+0x1c exactly like the unpatched inline code. - Feature_NDPSfiSpring2026__private_IsEnabled... is a WIL feature-staging gate (the symbol exists only in the patched build). This is a staged-rollout mechanism, not a shipped-enabled bounds check.

Reachability. ndisMRawIndicateStatusEx is called from ndisMApplyInterfaceChange (0x1401687E0, the call at 0x140168989) and from the ndisMpHookRawIndicateStatus / ndisMpHookDefaultIndicateStatus wrappers (0x14005E90A, 0x14005E924). These are the internal status-indication plumbing driven by a miniport driver calling NdisMIndicateStatusEx. The StatusBuffer and StatusBufferSize originate from that kernel-mode driver; there is no user-mode OID/DeviceIoControl entry into this path, and StatusCode 0x40030053 (a PM-capabilities indication) is generated by the miniport from its own fixed-size capability structure.


3. Pseudocode Diff

// UNPATCHED — ndis!ndisMRawIndicateStatusEx, StatusCode 0x40030053 branch (@ 0x14005F17A)
if (r13d == 0x40030053) {
    void *buf = *(void**)(r15 + 0x30);          // StatusBuffer  (no size check)
    *(uint64_t*)(rsi + 0x208) = __readgsqword(0x188);
    int32_t v30 = *(int32_t*)(buf + 0x30);       // read StatusBuffer+0x30
    if (v30 == 2 || (uint32_t)(v30 - 3) <= 1)
        *(uint32_t*)(rsi + 0x46c) = 1;
    else
        *(uint32_t*)(rsi + 0x46c) &= 0xFFFFFFFE;
    int32_t v08 = *(int32_t*)(buf + 0x08) & *(uint32_t*)(rsi + 0x464); // read +0x08
    *(uint32_t*)(rsi + 0x464) = v08;
    int32_t v28 = *(int32_t*)(buf + 0x28);       // read +0x28
    if (v28 == 2 || (uint32_t)(v28 - 3) <= 1)
        *(uint32_t*)(rsi + 0x464) = v08 | 2;
    *(uint32_t*)(rsi + 0x468) &= *(int32_t*)(buf + 0x1c); // read +0x1c
    goto release_spinlock;
}

// PATCHED — ndis!ndisMRawIndicateStatusEx, StatusCode 0x40030053 branch (@ 0x14005E1D5)
if (r13d == 0x40030053) {
    if (Feature_NDPSfiSpring2026__private_IsEnabled...() == 0   // feature OFF -> old path
        || StatusBufferSize >= 0x34)                           // feature ON  -> size gate
        ndisMIndicatePMCapabilities(rsi, r15);   // 0x1400B60E8: same reads, no internal check
    else
        <WPP_RECORDER_SF_ trace, skip>;
    goto label_14005DEF3;
}

Notes: - The unsafe reads themselves are unchanged; they now live in ndisMIndicatePMCapabilities. The only new code is the feature-gated StatusBufferSize >= 0x34 pre-check, which is bypassed entirely when Feature_NDPSfiSpring2026 is off. - With the feature off, the patched and unpatched builds perform the identical unchecked field reads.


4. Assembly Analysis

UNPATCHED — ndis!ndisMRawIndicateStatusEx (0x14005E940), StatusCode 0x40030053 path

000000014005F17A  cmp     r13d, 40030053h
000000014005F181  jnz     loc_14005F207            ; not this StatusCode -> next case
000000014005F187  mov     rbx, [r15+30h]           ; rbx = StatusBuffer (no size check)
000000014005F18B  lea     rcx, [rsi+60h]           ; acquire miniport spinlock
000000014005F18F  call    __imp_KeAcquireSpinLockRaiseToDpc
000000014005F19B  mov     rcx, gs:188h
000000014005F1A7  mov     [rsi+208h], rcx
000000014005F1AE  mov     ecx, [rbx+30h]           ; read StatusBuffer+0x30
000000014005F1B1  cmp     ecx, 2
000000014005F1B6  add     ecx, 0FFFFFFFDh
000000014005F1BE  and     dword ptr [rsi+46Ch], 0FFFFFFFEh
000000014005F1C7  mov     dword ptr [rsi+46Ch], 1
000000014005F1D1  mov     ecx, [rbx+8]             ; read StatusBuffer+0x08
000000014005F1D4  and     ecx, [rsi+464h]
000000014005F1E0  mov     eax, [rbx+28h]           ; read StatusBuffer+0x28
000000014005F1F9  mov     eax, [rbx+1Ch]           ; read StatusBuffer+0x1c
000000014005F1FC  and     [rsi+468h], eax
000000014005F202  jmp     loc_14005F2F0            ; release spinlock

The maximum in-buffer byte accessed is 0x33 (DWORD at +0x30), so the block assumes StatusBufferSize >= 0x34 without checking it. The next case handles StatusCode 0x40030054 and does validate:

000000014005F207  cmp     r13d, 40030054h
000000014005F214  mov     ecx, [r15+38h]           ; StatusBufferSize (this is the 0x40030054 case)
000000014005F218  mov     rbx, [r15+30h]
000000014005F21C  cmp     ecx, 34h                 ; validated here
000000014005F21F  jb      loc_14005EE98
000000014005F225  movzx   eax, word ptr [rbx+2]
000000014005F229  cmp     ecx, eax
000000014005F22B  jb      loc_14005EE98

(The read of [r15+38h] at 0x14005F214 belongs to the 0x40030054 case, not the 0x40030053 case.)

PATCHED — ndis!ndisMRawIndicateStatusEx (0x14005D9A0), StatusCode 0x40030053 path

000000014005D9D1  mov     edi, [rdx+38h]           ; edi = StatusBufferSize (set at entry)
...
000000014005E1D5  cmp     r13d, 40030053h
000000014005E1DC  jnz     short loc_14005E23F
000000014005E1DE  call    Feature_NDPSfiSpring2026__private_IsEnabledDeviceUsageNoInline
000000014005E1E3  test    eax, eax
000000014005E1E5  jz      short loc_14005E1EC      ; feature OFF -> call handler unchecked
000000014005E1E7  cmp     edi, 34h                 ; feature ON  -> StatusBufferSize gate
000000014005E1EA  jb      short loc_14005E1FC      ; too small -> WPP trace, skip
000000014005E1EC  mov     rdx, r15
000000014005E1EF  mov     rcx, rsi
000000014005E1F2  call    ?ndisMIndicatePMCapabilities@@...  ; 0x1400B60E8
000000014005E1F7  jmp     loc_14005DEF3
000000014005E1FC  lea     rax, WPP_RECORDER_INITIALIZED
000000014005E203  cmp     cs:WPP_RECORDER_INITIALIZED, rax
000000014005E20A  jz      loc_14005DEF3
000000014005E235  call    WPP_RECORDER_SF_
000000014005E23A  jmp     loc_14005DEF3

PATCHED — extracted callee ndis!ndisMIndicatePMCapabilities (0x1400B60E8)

00000001400B60F2  mov     rdi, [rdx+30h]           ; StatusBuffer (no size check here either)
00000001400B6103  call    NDIS_ACQUIRE_MINIPORT_SPIN_LOCK
00000001400B6108  mov     eax, [rdi+30h]           ; read StatusBuffer+0x30
00000001400B612B  mov     ecx, [rdi+8]             ; read StatusBuffer+0x08
00000001400B613A  mov     eax, [rdi+28h]           ; read StatusBuffer+0x28
00000001400B6153  mov     eax, [rdi+1Ch]           ; read StatusBuffer+0x1c
00000001400B615A  and     [rbx+468h], eax

The extracted function performs the same four reads and the same writes to +0x464/+0x468/+0x46c as the unpatched inline block, with no size validation of its own. The only added guard is the feature-gated cmp edi, 34h at the call site.


5. Trigger Conditions

This path is not reachable from a user-mode OID request. It executes when a miniport driver calls NdisMIndicateStatusEx with an NDIS_STATUS_INDICATION whose StatusCode == 0x40030053, flowing ndisMApplyInterfaceChange / ndisMpHookRawIndicateStatusndisMRawIndicateStatusEx. The StatusBuffer and StatusBufferSize are provided by that kernel-mode driver. StatusCode 0x40030053 is a PM-capabilities indication generated by the miniport from its own capability structure, so the buffer size is fixed by the indicating driver rather than being attacker-controlled across a user/kernel trust boundary.

Because the added StatusBufferSize >= 0x34 check is gated behind Feature_NDPSfiSpring2026 with the old unchecked call retained for the feature-off case, the shipping default behaviour is unchanged from the unpatched build. There is no demonstrable, delivered, reachable primitive to trigger.


6. Exploit Primitive & Development Notes

No exploit primitive is established by this change. The reads occur on a kernel-to-kernel status-indication path with a caller-supplied buffer, and the patched build does not deliver a bounds check by default (it is feature-staged). Claims of user-mode reachability, information disclosure, KASLR bypass, pool-spraying, or state-injection are not supported by the binaries and are omitted.


7. Debugger Reference

The relevant module is ndis. Absolute addresses below are from these two builds and can be used with bp ndis+<rva> after resolving the module base.

Build Address Instruction Note
unpatched 0x14005F17A cmp r13d, 40030053h StatusCode 0x40030053 dispatch in ndisMRawIndicateStatusEx
unpatched 0x14005F187 mov rbx, [r15+30h] StatusBuffer load (no size check)
unpatched 0x14005F1AE mov ecx, [rbx+30h] read StatusBuffer+0x30
unpatched 0x14005F1D1 mov ecx, [rbx+8] read StatusBuffer+0x08
unpatched 0x14005F1E0 mov eax, [rbx+28h] read StatusBuffer+0x28
unpatched 0x14005F1F9 mov eax, [rbx+1Ch] read StatusBuffer+0x1c
unpatched 0x14005F21C cmp ecx, 34h validation present in the sibling 0x40030054 case
patched 0x14005E1DE call Feature_NDPSfiSpring2026... WIL feature-staging gate
patched 0x14005E1E7 cmp edi, 34h feature-gated StatusBufferSize check
patched 0x1400B60E8 function entry ndisMIndicatePMCapabilities (extracted body, no internal check)

Struct / offset notes: - _NDIS_STATUS_INDICATION (r15 / arg2): +0x30 = StatusBuffer, +0x38 = StatusBufferSize. - Miniport block (rsi / arg1): +0x464, +0x468, +0x46c are the flag DWORDs written from the buffer reads; +0x208 holds a per-processor value from gs:188h. - StatusBuffer offsets read in the 0x40030053 block: 0x08, 0x1c, 0x28, 0x30.


8. Changed Functions — Full Triage

  • ndisMRawIndicateStatusEx (unpatched 0x14005E940 / patched 0x14005D9A0, sim 0.75). For StatusCode 0x40030053 the inline body was moved into ndisMIndicatePMCapabilities (0x1400B60E8) and a StatusBufferSize >= 0x34 pre-check was added, but gated behind Feature_NDPSfiSpring2026 with the old unchecked call retained when the feature is off. Staged rollout via WIL feature staging; no bounds check delivered by default. The StatusCode 0x40030053 filter counterpart at unpatched 0x14005FB00 / patched 0x14005EB30 (calling ndisFilterIndicatePMCapabilities) is byte-identical between builds and is unrelated to the size question.
  • ndisNsiSetThreadInformation (unpatched 0x14005BE90 / patched 0x1400892B0). Adds a NULL check on the request buffer pointer at [req+0x28] before dereferencing it (returning STATUS_INVALID_PARAMETER_MIX, 0xC000000D), but gated behind the WIL feature Feature_TCPIP_SFI_60615470_Fix with the old unchecked dereference retained when the feature is off. Same staged-rollout pattern; not a delivered fix. Would be a NULL-pointer dereference (CWE-476, denial of service) rather than the reported out-of-bounds read.
  • sub_14007ba60 / sub_140089340 (sim 0.68). Pool allocation size and structure-magic bump plus new field handling. Structural/feature churn, not a vulnerability fix.
  • sub_1400826b0 (sim 0.98). New field initialization matching the size bump above. Not security-relevant.
  • sub_1400ce584 (sim 0.96). Statistics counter-update refactor (removed conditional call, counter global relocated). Not security-relevant.
  • Bulk of the 379 changed functions. Dominated by servicing churn: WIL feature-staging gates (Feature_NDPSfiSpring2026, Feature_TCPIP_SFI_60615470_Fix, Feature_SSSurpriseRemoval_Fix, Feature_CFHCancelTimerFix, Feature_NDPQualitySpring26, Feature_NDPQualitySummer26), WPP/ETW tracing, and the new NdisTriageData bugcheck-triage feature (*TriageData*, *DataCollector, IsMemoryValid, etc.). None of these are runtime data-path bounds fixes.

No changed function delivers a reachable, default-on security fix in these builds.


9. Unmatched Functions

{ "removed": [], "added": [] }

No functions were added or deleted outright per the diff counts. The patched-only routine ndisMIndicatePMCapabilities (0x1400B60E8) is the extracted body of the 0x40030053 case; it is not a mitigation in itself (it contains no size check).


10. Confidence & Caveats

Confidence: High that this is not a delivered security fix.

Rationale: - The added StatusBufferSize >= 0x34 check is guarded by the WIL feature Feature_NDPSfiSpring2026; when the feature is off the patched code calls the same unchecked handler (ndisMIndicatePMCapabilities) as the unpatched inline block. Old path retained = staged rollout. - ndisMIndicatePMCapabilities has no internal size validation; its reads mirror the unpatched code exactly. - The path is ndisMRawIndicateStatusEx, a miniport status-indication handler reached from ndisMApplyInterfaceChange / the ndisMpHook*IndicateStatus wrappers, not a user-mode OID Set-Information dispatcher. 0x40030053 is an NDIS StatusCode, not an OID, and the buffer/size come from the indicating kernel-mode driver.

Notes: - The runtime default state of Feature_NDPSfiSpring2026 cannot be determined from the static image; the observable code structure (feature-gated check with the unchecked call retained in the feature-off branch) is the staged-rollout pattern, so no bounds check is guaranteed to execute in the shipping default configuration. - The unpatched 0x40030053 block does assume StatusBufferSize >= 0x34 (unlike the sibling 0x40030054 case, which validates). If reached with a shorter buffer it would read past the buffer end — a read, i.e. CWE-125. That remains a latent robustness issue on a kernel-to-kernel path, not a delivered fix or a user-mode-reachable primitive.