netio.sys — staged iBFT WIL feature-gate removal
KB5077181
1. Overview
| Field | Value |
|---|---|
| Unpatched Binary | netio_unpatched.sys |
| Patched Binary | netio_patched.sys |
| Overall Similarity | 0.9914 |
| Matched Functions | 1901 |
| Changed Functions | 1 (NsipInitializeBootFirmwareTableData) |
| Removed Functions | 2 (WIL feature helpers for Feature_TCPIP_2025_Wave2_NsiIbftFix) |
| Added Functions | 0 |
Verdict: The only real code change is in NsipInitializeBootFirmwareTableData (iBFT lazy initialization, 0x140049B20). The patch removes a WIL feature-staging gate for Feature_TCPIP_2025_Wave2_NsiIbftFix and reverts the function to its single unconditional (feature-off) path. The two generated WIL helper functions for that feature are dropped from the build. The iBFT query, parse, count, allocate, and read-out logic (in NsipGetBootFirmwareTableFromSystemInformation, NsipInitializeBootNicAndTargets, and the caller NsipReadBootFirmwareTableData) is byte-identical between the two builds. No memory-safety behavior is added or removed in any delivered code path. This is staged-feature-gate servicing churn, not a security fix.
2. Change Summary
Finding 1: iBFT initialization — WIL feature-gate removal (no delivered security change)
| Field | Detail |
|---|---|
| Severity | None (informational) |
| Class | Feature-staging / servicing churn |
| Affected Function | NsipInitializeBootFirmwareTableData (0x140049B20) |
| Feature | Feature_TCPIP_2025_Wave2_NsiIbftFix |
What the code actually does
NsipInitializeBootFirmwareTableData performs lazy initialization of the iBFT (iSCSI Boot Firmware Table) NIC/target arrays. In the unpatched build it contains two behaviors selected by a WIL feature gate:
- It loads
Feature_TCPIP_2025_Wave2_NsiIbftFix__private_featureState(global at 0x140097370). If the cached-state bit (0x10) is set it uses the cached enabled bit (& 1); otherwise it calls the generated helperFeature_TCPIP_2025_Wave2_NsiIbftFix__private_IsEnabledDeviceUsageNoInline(0x140064BD4). This is standard WIL feature-staging inline code, not a validation routine. - Feature enabled path: after the iBFT query returns success, it additionally requires both global counters
dword_140096CF8anddword_140096CFCto be non-zero before proceeding to allocation; if either is zero it setsSTATUS_NOT_FOUND, clears the init flagbyte_140096CD8, and cleans up. - Feature disabled path: on query success it proceeds directly to allocation with no counter precondition.
The patched build removes the feature-state load, the helper call, and the entire feature-enabled branch. What remains is exactly the feature-disabled path: query success proceeds directly to allocation. The allocation block itself (two ExAllocatePool2 calls sized counter << 3 with tag 0x7249534E, two calls to the parser, and the cleanup that frees and nulls the three arrays) is functionally identical in both builds.
Why there is no delivered security change
- The allocation, parse (
NsipInitializeBootNicAndTargets), counter-write, and read-out (NsipReadBootFirmwareTableData) logic is byte-identical between the two builds. Any counter-accumulation or array-population behavior present in one build is present identically in the other. The patch does not add a counter reset, a bounds check, or a zeroing step, and it does not change the sizes, the tag, or the pool flags. - The only behavioral delta is the presence/absence of the feature-enabled precondition guard and its extra
byte_140096CD8 = 0. That code sits behind a default-off staged feature gate and, if anything, is the branch that could clear the init flag when the counters are zero. Removing it does not introduce or remove a reachable memory-safety condition. Feature_*__private_IsEnabled*WIL gating with the pre-existing path retained in theelsebranch is a staged rollout marker, not a security fix.
There is no evidence in either build of a reachable information-disclosure or denial-of-service primitive attributable to this diff.
Reachability context
NsipInitializeBootFirmwareTableData is called by NsipReadBootFirmwareTableData (0x1400498A0), which reads the parsed iBFT arrays. iBFT retrieval is exposed through NSI parameter queries reachable from user-mode network enumeration. This reachability is identical in both builds and is not altered by the patch; it is noted only for context, not as an exploit chain.
3. Pseudocode Diff
Unpatched
// NsipInitializeBootFirmwareTableData — UNPATCHED (0x140049B20)
int32_t NsipInitializeBootFirmwareTableData() {
if (byte_140096CD8 == 0) // init flag
return STATUS_NOT_FOUND;
int32_t status = NsipGetBootFirmwareTableFromSystemInformation();
// WIL feature-staging gate for Feature_TCPIP_2025_Wave2_NsiIbftFix
int32_t fs = Feature_..._NsiIbftFix__private_featureState;
int32_t enabled = (fs & 0x10) ? (fs & 1)
: Feature_..._NsiIbftFix__private_IsEnabledDeviceUsageNoInline();
if (enabled) { // feature-on path
if (status < 0) goto error;
if (dword_140096CF8 != 0 && dword_140096CFC != 0)
goto allocate; // both counters non-zero
status = STATUS_NOT_FOUND;
byte_140096CD8 = 0; // clear init flag
goto cleanup;
}
// feature-off path
if (status >= 0) goto allocate;
error:
if (status != STATUS_NOT_FOUND) goto cleanup;
byte_140096CD8 = 0; // clear init flag
goto cleanup;
allocate:
NsipInitializeBootNicAndTargets(); // counting pass
qword_140096CE8 = ExAllocatePool2(0x40, dword_140096CF8 << 3, 'NSIr');
if (!qword_140096CE8) goto cleanup;
qword_140096CF0 = ExAllocatePool2(0x40, dword_140096CFC << 3, 'NSIr');
if (!qword_140096CF0) goto cleanup;
NsipInitializeBootNicAndTargets(); // population pass
return status;
cleanup:
// frees SystemInformation, qword_140096CE8, qword_140096CF0; nulls them
return status;
}
Patched
// NsipInitializeBootFirmwareTableData — PATCHED (0x140049B20)
int32_t NsipInitializeBootFirmwareTableData() {
if (byte_140096CD8 == 0)
return STATUS_NOT_FOUND;
int32_t status = NsipGetBootFirmwareTableFromSystemInformation();
if (status < 0) {
if (status == STATUS_NOT_FOUND)
byte_140096CD8 = 0; // clear init flag
goto cleanup;
}
// proceeds directly to allocation (identical to the unpatched feature-off path)
NsipInitializeBootNicAndTargets();
qword_140096CE8 = ExAllocatePool2(0x40, dword_140096CF8 << 3, 'NSIr');
if (!qword_140096CE8) goto cleanup;
qword_140096CF0 = ExAllocatePool2(0x40, dword_140096CFC << 3, 'NSIr');
if (!qword_140096CF0) goto cleanup;
NsipInitializeBootNicAndTargets();
return status;
cleanup:
// frees SystemInformation, qword_140096CE8, qword_140096CF0; nulls them
return status;
}
Key Differences
- The
Feature_TCPIP_2025_Wave2_NsiIbftFixfeature-state load and the call toFeature_..._NsiIbftFix__private_IsEnabledDeviceUsageNoInlineare removed. - The feature-enabled branch (the counter precondition guard and its extra
byte_140096CD8 = 0) is removed. - The remaining control flow equals the unpatched feature-off path. The allocation, parse, and cleanup blocks are functionally unchanged.
4. Assembly Analysis
Unpatched NsipInitializeBootFirmwareTableData (0x140049B20)
0000000140049B20 mov [rsp+arg_8], rbx
0000000140049B25 push rdi
0000000140049B26 sub rsp, 20h
0000000140049B2A cmp cs:byte_140096CD8, 0 ; init flag
0000000140049B31 jnz short loc_140049B3D
0000000140049B33 mov eax, 0C0000225h ; STATUS_NOT_FOUND
0000000140049B38 jmp loc_140049C60
0000000140049B3D call NsipGetBootFirmwareTableFromSystemInformation
0000000140049B42 mov ebx, eax
0000000140049B44 mov edi, eax
; --- WIL feature gate for Feature_TCPIP_2025_Wave2_NsiIbftFix (removed in patch) ---
0000000140049B46 mov eax, cs:Feature_TCPIP_2025_Wave2_NsiIbftFix__private_featureState
0000000140049B4C test al, 10h ; cached-state bit
0000000140049B4E jz short loc_140049B55
0000000140049B50 and eax, 1 ; cached enabled bit
0000000140049B53 jmp short loc_140049B5A
0000000140049B55 call Feature_TCPIP_2025_Wave2_NsiIbftFix__private_IsEnabledDeviceUsageNoInline
0000000140049B5A test eax, eax
0000000140049B5C jz short loc_140049B7B ; feature off -> old path
; --- feature-on path ---
0000000140049B5E test ebx, ebx
0000000140049B60 js short loc_140049B7F
0000000140049B62 cmp cs:dword_140096CF8, 0 ; counter precondition
0000000140049B69 jz short loc_140049B74
0000000140049B6B cmp cs:dword_140096CFC, 0
0000000140049B72 jnz short loc_140049B90 ; both non-zero -> allocate
0000000140049B74 mov ebx, 0C0000225h
0000000140049B79 jmp short loc_140049B87
; --- feature-off path ---
0000000140049B7B test edi, edi
0000000140049B7D jns short loc_140049B90 ; status >= 0 -> allocate
0000000140049B7F cmp ebx, 0C0000225h
0000000140049B85 jnz short loc_140049BF1
0000000140049B87 mov cs:byte_140096CD8, 0 ; clear init flag
0000000140049B8E jmp short loc_140049BF1
; --- allocation / parse ---
0000000140049B90 call NsipInitializeBootNicAndTargets ; counting pass
0000000140049B95 mov edx, cs:dword_140096CF8
0000000140049B9B mov edi, 7249534Eh ; 'NSIr'
0000000140049BA0 shl edx, 3
0000000140049BA3 mov r8d, edi
0000000140049BA6 mov ecx, 40h
0000000140049BAB call cs:__imp_ExAllocatePool2
0000000140049BB7 mov cs:qword_140096CE8, rax
0000000140049BBE test rax, rax
0000000140049BC1 jz short loc_140049BEC
0000000140049BC3 mov edx, cs:dword_140096CFC
0000000140049BC9 mov r8d, edi
0000000140049BCC shl edx, 3
0000000140049BCF mov ecx, 40h
0000000140049BD4 call cs:__imp_ExAllocatePool2
0000000140049BE0 mov cs:qword_140096CF0, rax
0000000140049BE7 test rax, rax
0000000140049BEA jnz short loc_140049C59
0000000140049BEC mov ebx, 0C000009Ah ; STATUS_INSUFFICIENT_RESOURCES
; --- cleanup (frees arrays, nulls pointers) ---
0000000140049BF1 mov rcx, cs:SystemInformation
0000000140049BF8 test rcx, rcx
0000000140049BFB jz short loc_140049C5E
0000000140049BFD xor edx, edx
0000000140049BFF call cs:__imp_ExFreePoolWithTag
0000000140049C0B mov rcx, cs:qword_140096CE8
0000000140049C12 and cs:SystemInformation, 0
0000000140049C1A test rcx, rcx
0000000140049C1D jz short loc_140049C35
0000000140049C1F xor edx, edx
0000000140049C21 call cs:__imp_ExFreePoolWithTag
0000000140049C2D and cs:qword_140096CE8, 0
0000000140049C35 mov rcx, cs:qword_140096CF0
0000000140049C3C test rcx, rcx
0000000140049C3F jz short loc_140049C5E
0000000140049C41 xor edx, edx
0000000140049C43 call cs:__imp_ExFreePoolWithTag
0000000140049C4F and cs:qword_140096CF0, 0
0000000140049C57 jmp short loc_140049C5E
0000000140049C59 call NsipInitializeBootNicAndTargets ; population pass
0000000140049C5E mov eax, ebx
0000000140049C60 mov rbx, [rsp+28h+arg_8]
0000000140049C65 add rsp, 20h
0000000140049C69 pop rdi
0000000140049C6A retn
Patched NsipInitializeBootFirmwareTableData (0x140049B20)
0000000140049B20 push rbx
0000000140049B22 sub rsp, 20h
0000000140049B26 cmp cs:byte_140096CD8, 0
0000000140049B2D jnz short loc_140049B39
0000000140049B2F mov eax, 0C0000225h
0000000140049B34 jmp loc_140049C25
0000000140049B39 call NsipGetBootFirmwareTableFromSystemInformation
0000000140049B3E mov ebx, eax
0000000140049B40 test eax, eax
0000000140049B42 jns short loc_140049B54 ; status >= 0 -> allocate
0000000140049B44 cmp eax, 0C0000225h
0000000140049B49 jnz short loc_140049BB6
0000000140049B4B mov cs:byte_140096CD8, 0 ; clear init flag (NOT_FOUND only)
0000000140049B52 jmp short loc_140049BB6
0000000140049B54 call NsipInitializeBootNicAndTargets
0000000140049B59 mov edx, cs:dword_140096CF8
0000000140049B5F mov ecx, 40h
0000000140049B64 shl edx, 3
0000000140049B67 mov r8d, 7249534Eh
0000000140049B6D call cs:__imp_ExAllocatePool2
0000000140049B79 mov cs:qword_140096CE8, rax
0000000140049B80 test rax, rax
0000000140049B83 jz short loc_140049BB1
0000000140049B85 mov edx, cs:dword_140096CFC
0000000140049B8B mov ecx, 40h
0000000140049B90 shl edx, 3
0000000140049B93 mov r8d, 7249534Eh
0000000140049B99 call cs:__imp_ExAllocatePool2
0000000140049BA5 mov cs:qword_140096CF0, rax
0000000140049BAC test rax, rax
0000000140049BAF jnz short loc_140049C1E
0000000140049BB1 mov ebx, 0C000009Ah
0000000140049BB6 mov rcx, cs:SystemInformation
; ... cleanup identical in structure to the unpatched build ...
0000000140049C1E call NsipInitializeBootNicAndTargets
0000000140049C23 mov eax, ebx
0000000140049C25 add rsp, 20h
0000000140049C29 pop rbx
0000000140049C2A retn
What changed: the block from 0x140049B46 through 0x140049B8E (feature-state load, WIL helper call, feature-enabled counter guard, and its extra init-flag clear) is deleted. Control flow collapses to a single test eax, eax / jns on the query status. The allocation and cleanup blocks are unchanged in behavior (only register scheduling of the pool tag differs: unpatched caches 'NSIr' in edi, patched uses the immediate directly).
5. Direction and Reachability
- Direction: the patched build retains the unpatched feature-off path. The removed code is the staged
Feature_TCPIP_2025_Wave2_NsiIbftFixbranch. Neither path adds a bounds check, a counter reset, or memory zeroing relative to the other. - Query function:
NsipGetBootFirmwareTableFromSystemInformationis the same function in both builds, relocated from 0x140064C28 (unpatched) to 0x140064B94 (patched) by the removal of the two WIL helpers ahead of it. It is not a renamed or different routine. - Parser:
NsipInitializeBootNicAndTargetsis the same function in both builds, relocated from 0x140064D54 to 0x140064CC0. Its counter-write logic (dword_140096CF8,dword_140096CFC) is byte-identical between builds. - Reader/caller:
NsipReadBootFirmwareTableData(0x1400498A0) is byte-identical between builds; its read loop overdword_140096CFCentries is unchanged.
6. Global / Symbol Reference
| Address | Type | Symbol | Role |
|---|---|---|---|
| 0x140096CD8 | byte | init flag (byte_140096CD8) |
Gates iBFT initialization; cleared on STATUS_NOT_FOUND in both builds |
| 0x140096CE0 | qword | SystemInformation |
Raw iBFT buffer from the system-information query |
| 0x140096CE8 | qword | qword_140096CE8 |
Primary array pointer (size dword_140096CF8 << 3, tag 'NSIr') |
| 0x140096CF0 | qword | qword_140096CF0 |
Secondary array pointer (size dword_140096CFC << 3, tag 'NSIr') |
| 0x140096CF8 | dword | dword_140096CF8 |
Counter written by the parser; identical logic in both builds |
| 0x140096CFC | dword | dword_140096CFC |
Counter incremented by the parser; identical logic in both builds |
| 0x140097370 | dword | Feature_TCPIP_2025_Wave2_NsiIbftFix__private_featureState |
WIL feature-state cache (gate removed in patch) |
7. Changed / Removed Functions — Full Triage
NsipInitializeBootFirmwareTableData (0x140049B20) — real logic change
- Similarity: 0.8083
- Change type: feature-gate removal (staging churn), not security-relevant.
- What changed: removal of the
Feature_TCPIP_2025_Wave2_NsiIbftFixWIL gate and its feature-enabled counter-precondition branch (0x140049B46–0x140049B8E), reverting to the unconditional feature-off path. Therdisecond status copy is no longer needed. - Impact: none on delivered memory-safety behavior; the allocation/parse/cleanup blocks are functionally unchanged.
Removed functions (unpatched only)
Feature_TCPIP_2025_Wave2_NsiIbftFix__private_IsEnabledDeviceUsageNoInline(0x140064BD4) — generated WIL feature-check helper.Feature_TCPIP_2025_Wave2_NsiIbftFix__private_IsEnabledFallback(0x140064C0C) — generated WIL fallback helper.
Both are dropped because the feature was removed from the staging table.
Cosmetic-only deltas (not code changes)
An independent content diff of both builds (matching by function content across relocation) flags a set of functions — including NsiGetParameterEx, NsiEnumerateObjectsAllParametersEx, the NsipValidate*Request family, several Stream*/Wfp*/Kfd* routines, and WIL staging infrastructure (wil_InitializeFeatureStaging, wil_details_UnregisterFeatureStagingChangeNotification). Direct inspection shows these differ only in disassembler labeling of the same WPP_MAIN_CB structure fields (e.g. +148h vs .DeviceObjectExtension, .DeviceLock.Header vs .ActiveThreadCount) and in jump-table / data addresses shifted by the small data-section move caused by removing the two helpers. Instruction bytes and offsets are equivalent; none are code changes.
8. Unmatched Functions
| Direction | Count | Details |
|---|---|---|
| Removed (unpatched only) | 2 | Feature_TCPIP_2025_Wave2_NsiIbftFix__private_IsEnabledDeviceUsageNoInline (0x140064BD4), Feature_TCPIP_2025_Wave2_NsiIbftFix__private_IsEnabledFallback (0x140064C0C) |
| Added (patched only) | 0 | None |
The removals are the WIL helper functions generated for the retired feature. No sanitizer, bounds-check, or mitigation function was added.
9. Confidence & Caveats
Confidence: High
The diff is clean and the single real change is fully visible in the disassembly of both builds. The surrounding iBFT query, parse, count, allocate, and read-out functions are byte-identical (only relocated), so the change is isolated to the removal of one staged WIL feature gate.
Notes
Feature_TCPIP_2025_Wave2_NsiIbftFixis a WIL feature-staging gate. In the unpatched build both the feature-on and feature-off paths are present; the patched build removes the gate and retains the feature-off path. This is a staged-rollout / servicing pattern.- No reachable information-disclosure or denial-of-service primitive is attributable to this diff: the code that performs allocation and read-out is unchanged between builds, and no counter reset, bounds check, or zeroing was added or removed.