ndis.sys — feature-staged length pre-check on a kernel-only status-indication path; old unchecked path retained
KB5083768
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(entry0x14005E940); StatusCode 0x40030053 case begins at0x14005F17A - Patched equivalent:
ndis!ndisMRawIndicateStatusEx(entry0x14005D9A0); the case body was moved intondis!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 / ndisMpHookRawIndicateStatus → ndisMRawIndicateStatusEx. 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(unpatched0x14005E940/ patched0x14005D9A0, sim 0.75). For StatusCode 0x40030053 the inline body was moved intondisMIndicatePMCapabilities(0x1400B60E8) and aStatusBufferSize >= 0x34pre-check was added, but gated behindFeature_NDPSfiSpring2026with 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 unpatched0x14005FB00/ patched0x14005EB30(callingndisFilterIndicatePMCapabilities) is byte-identical between builds and is unrelated to the size question.ndisNsiSetThreadInformation(unpatched0x14005BE90/ patched0x1400892B0). Adds a NULL check on the request buffer pointer at[req+0x28]before dereferencing it (returningSTATUS_INVALID_PARAMETER_MIX,0xC000000D), but gated behind the WIL featureFeature_TCPIP_SFI_60615470_Fixwith 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 newNdisTriageDatabugcheck-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.