# netio.sys Patch Diff — CVE-2026-50475

| | |
|---|---|
| Binary | netio.sys (Network I/O Subsystem) |
| Pre-patch version | 10.0.26100.8737 |
| Post-patch version | 10.0.26100.8875 |
| KB | KB5101650 |
| CVE | CVE-2026-50475 — Use of Out-of-Range Pointer Offset (CWE-823), Information Disclosure, CVSS 5.5 |
| Diff tool | ghidriff (Ghidra VersionTrackingDiff engine) |
| Functions changed | 1 with code changes (out of 1,842 total) |
| Functions added | 0 |

## Summary

The fix is a single-character bounds-check correction in
`NsipGetAllInformationProviderParameters` — the function that services IOCTL
`0x120007` to `\\Device\\Nsi` for querying network module parameters by vtable
index.

Before the patch, the comparison between the user-supplied `TableIndex` and the
module's `maxVtableIndex` used `<=` (less-or-equal). When
`TableIndex == maxVtableIndex`, the check passed and the function read 0x10 bytes
from one entry **past** the end of the vtable array — an off-by-one
out-of-bounds read. The leaked bytes are kernel pointers (tcpip.sys function
addresses from adjacent pool data), defeating KASLR.

The patch changes `<=` to `<`:

```c
// PRE-PATCH (VULNERABLE):
if (*(uint *)(plVar2 + 3) <= *(uint *)(*(longlong *)(piVar5 + 0xc) + 4))

// POST-PATCH (FIXED):
if (*(uint *)(plVar2 + 3) <  *(uint *)(*(longlong *)(piVar5 + 0xc) + 4))
```

When `TableIndex == maxVtableIndex` (e.g. `0x26` for `TcpNsiInterfaceDispatch`),
the pre-patch code falls through to the array access and copies 0x10 bytes of
kernel pointers to the user-mode output buffer. The post-patch code correctly
rejects it with `STATUS_NOT_FOUND` (`0xc0000225`).

### Vulnerable decompilation (pre-patch, Ghidra-verified)

`NsipGetAllInformationProviderParameters @ 0x1400638a0`:

```c
ulonglong NsipGetAllInformationProviderParameters(longlong param_1)
{
  int iVar1;
  longlong *plVar2;
  ulonglong uVar7;
  int *piVar5;
  undefined8 *puVar6;

  plVar2 = *(longlong **)(param_1 + 0x10);  // user-controlled input buffer
  iVar1 = *(int *)(param_1 + 0x20);          // operation type

  if (iVar1 == 0) {
    piVar5 = NsipGetNmpContext((uint *)&local_38);  // lookup by GUID
    if (piVar5 != (int *)0x0) {

      // ▼▼▼ VULNERABLE BOUNDS CHECK ▼▼▼
      if (*(uint *)(plVar2 + 3) <= *(uint *)(*(longlong *)(piVar5 + 0xc) + 4)) {
      //  ^^^^^^^^^^^^^^^^^^^^^^^^    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      //  user-supplied TableIndex    maxVtableIndex from NMP context
      //
      //  BUG: uses <= (less-or-equal) instead of < (less-than)

        uVar7 = 0;
        puVar6 = (undefined8 *)
                 ((ulonglong)*(uint *)(plVar2 + 3) * 0x68 +
                  *(longlong *)(*(longlong *)(piVar5 + 0xc) + 8));
        //        vtable_base + (TableIndex * sizeof(entry))
        //        With TableIndex == maxVtableIndex, reads one entry past the array.

        goto LAB_14006394b;  // copies 0x10 bytes from puVar6 to output
      }
      NsipDereferenceNmpContext(piVar5);
    }
    uVar7 = 0xc0000225;  // STATUS_NOT_FOUND
  }
  // ...

LAB_14006394b:
    puVar3 = *(undefined8 **)(param_1 + 0x48);
    if (puVar3 != (undefined8 *)0x0) {
      *(undefined4 *)(param_1 + 0x50) = 0x10;  // output size = 16 bytes
      *puVar3 = *puVar6;       // first 8 bytes  → leaked kernel pointer
      puVar3[1] = puVar6[1];   // second 8 bytes → leaked kernel pointer
    }
}
```

## Call chain

```
User-mode:
  CreateFileW(L"\\\\.\\Nsi", ...)
    → NtCreateFile → opens \\Device\\Nsi

  DeviceIoControl(hDevice, 0x120007, inBuf, inSize, outBuf, outSize, ...)
    → NtDeviceIoControlFile

Kernel (netio.sys):
  IRP_MJ_DEVICE_CONTROL handler
    → dispatches IOCTL 0x120007
      → NsipGetAllInformationProviderParameters @ 0x1400638a0
        → NsipGetNmpContext @ 0x140029290        (GUID lookup)
        → vtable[TableIndex] read                 (off-by-one HERE)
        → copies 0x10 bytes to output buffer
```

### IOCTL input buffer layout

| Offset | Size | Field | Description |
|--------|------|-------|-------------|
| +0x00 | 16 | ModuleId (GUID) | NMP module GUID — identifies which network module |
| +0x10 | 4 | OperationType | Must be 0 for the vulnerable path |
| +0x18 | 4 | TableIndex | Index into the vtable array — the attacker-controlled value |

### Trigger

Target GUID `{eb004a03-9b1a-11d4-9123-0050047759bc}` (`TcpNsiInterfaceDispatch`,
registered by tcpip.sys). Its `maxVtableIndex` is `0x26`. Setting
`TableIndex = 0x26` passes the `<=` check, reading 0x10 bytes from one entry past
the vtable — tcpip.sys kernel pointers from adjacent pool structures. No
privileges beyond a standard user are required; `\\Device\\Nsi` is accessible by
default.

## Functions changed

### NsipGetAllInformationProviderParameters

| | |
|---|---|
| Address | 1400638a0 |
| Change type | code |
| Similarity | 0.98 |
| Fix | Bounds check `<=` → `<` on `TableIndex` vs `maxVtableIndex` |

Single instruction change: the comparison opcode switches from `JBE` (jump if
below or equal, unsigned `<=`) to `JB` (jump if below, unsigned `<`). No other
code in the function is modified. No feature flags, no new functions added.

---

<sub>Source: ghidriff diff of netio-2026-06.sys (10.0.26100.8737, pre-patch)
vs netio-2026-07.sys (10.0.26100.8875, post-patch) —
[download pre](/data/patch_diffs/binaries/netio-2026-06.sys) /
[download post](/data/patch_diffs/binaries/netio-2026-07.sys).</sub>
