# CVE-2026-59125 — Windows `vhdmp.sys` Elevation of Privilege via Rundown-Nesting Use-After-Free in `VhdmpiQueueIoRequest`

---

## Summary

| **Product**           | Microsoft Windows — `vhdmp.sys` (Virtual Hard Disk Miniport Driver) |
|-----------------------|----------------------------------------------------------------------|
| **Vendor**            | Microsoft Corporation |
| **Severity**          | Important |
| **Impact**            | Elevation of Privilege — local user to SYSTEM |
| **CVE ID**            | CVE-2026-59125 |
| **CVSS (MSRC)**       | 7.0 — Exploitation Less Likely |
| **CWE**               | CWE-416: Use After Free |
| **Patch Date**        | August 11, 2026 |
| **Pre-patch binary**  | `vhdmp.sys` 10.0.26100.8972 (2026-07-28, KB5101684) — SHA256 `A5E97F3B5A2E49FA74DC104118667250BC8331B0BAEF5D0597F3919B301E8A8D` |
| **Post-patch binary** | `vhdmp.sys` 10.0.26100.9168 (2026-08-11, KB5121003) — SHA256 `690C256F6C5045ADBEB68452A0147F802A89B3BCD49532CAA26C89654113C739` |

---

## Product Description

`vhdmp.sys` is the kernel miniport behind every virtual hard disk format Windows
supports natively — VHD and VHDX. Attaching a `.vhd` or `.vhdx` (Hyper-V, Windows
Backup, `Mount-VHD`, WIM deployment) opens the file, parses its header and
block-allocation tables, and builds an in-memory object tree that downstream I/O
travels through.

**Attach is unprivileged.** Any local user can mount an image they own, which
makes every field the parser reads off that image attacker-controlled, and makes
every lifetime rule inside the resulting object tree security-relevant.

---

## Vulnerability Summary

Each queued I/O carries a back-pointer to the virtual disk it targets. The disk
object owns a rundown reference at `+0xd8` that pins its whole allocation, plus a
pointer at `+0xc8` to a *parent* object — the active surface hosting the prefetch
machinery. That parent carries its own inner rundown at `+0x610` and a
prefetch-state pointer at `+0x5f8`.

`VhdmpiQueueIoRequest` walked from the request, through the virtual disk, into the
parent's prefetch state, and took **only the parent's inner rundown**. The virtual
disk it read that parent pointer *from* was never pinned. A concurrent detach can
therefore tear down the virtual disk in the window between the `+0xc8` read and the
`+0x610` acquire — and because the parent pointer lives inside the disk's own
allocation, the rundown acquire and the subsequent prefetch-state walk operate on
reclaimed pool.

The Windows rundown idiom has one nesting rule that matters here: **the object you
used to reach an inner object must stay pinned for as long as you hold the inner
one.** It is the object-lifetime analogue of "lock the parent before the child".

---

## Prerequisites and Constraints

- Local authenticated session (standard user; no administrative rights)
- Ability to attach a VHD/VHDX the attacker owns — unprivileged by design
- Ability to issue I/O against the mounted volume (any read/write)
- Ability to detach the same VHD concurrently — same user, so the race is self-staged
- Prefetch must be active on the surface (`PrefetchState->Ready` at `+0xac` non-zero)
- No kernel driver, special hardware, or pre-existing elevated token required

The trigger is entirely self-contained: one thread drives I/O, another detaches.
Both are ordinary operations available to the user who mounted the image.

---

## Vulnerability Details

### Call Chain (Ghidra MCP–Verified)

Verified by `get_function_xrefs` against the pre-patch binary
(`vhdmp.sys` 10.0.26100.8972). All call edges are `UNCONDITIONAL_CALL`.

```
User mode:
  Mount-VHD / AttachVirtualDisk()      → unprivileged attach
  ReadFile / WriteFile on the volume   → SCSI request into the miniport
                                          ↓
Kernel mode (vhdmp.sys):
  VhdmpParserStartIo              @ 14000365a  ┐
  VhdmpiExecuteScsi               @ 14000477b  │
    → VhdmpiExecuteScsiRequest    @ 140003eb4  ├→ VhdmpiQueueIoRequest @ 140004cc0
  VhdmpiHandleScsiParsingResult   @ 140002edb  │        [*** VULNERABLE ***]
  VhdmpiCompleteParserRequest     @ 140006621  │
  VhdmpiIssueLowerIoContext       @ 1400b117b  ┘
      → VhdmpiRecordForPrefetchWorker           [sink: writes through freed state]
```

Every read and write to a mounted VHD passes through this function.

### Pre-patch decompilation (10.0.26100.8972)

```c
// VhdmpiQueueIoRequest — VULNERABLE
lVar15 = *(longlong *)(*param_2 + 200);              /* param_2[0] = VirtualDisk; +200 = 0xC8 → Parent
                                                        read with NO rundown held on the disk */
if (lVar15 != 0) {
    if (ExAcquireRundownProtection(lVar15 + 0x610)) { /* +0x610 — inner rundown ONLY */
        p_Var4 = *(_VHD_PREFETCH_STATE **)(lVar15 + 0x5f8);   /* +0x5f8 — prefetch state */
        if (p_Var4[0xac] != 0)                                /* +0xac — "recordable" flag */
            VhdmpiRecordForPrefetchWorker((_VHD_SRB_EXTENSION *)param_2, p_Var4);
        ExReleaseRundownProtection(lVar15 + 0x610);
    }
}
```

### The sink

`VhdmpiRecordForPrefetchWorker` immediately dereferences the prefetch state and
performs an interlocked increment plus an indexed write:

```c
lVar3 = *(longlong *)(param_2 + 0x98);      // log segment, read from the (possibly freed) state
LOCK(); *(uint *)(lVar3 + 0x10) += 1;       // interlocked increment into reclaimed pool
...
*(ulonglong *)(lVar3 + 0x18 + uVar6 * 8) = uVar4;   // indexed write, index from the same block
```

So the UAF is not a read-only touch: it produces an atomic increment **and** an
8-byte write at an index derived from the freed allocation's own contents.

### Root Cause

Three separate mistakes compound:

1. `Request->VirtualDisk` (`+0xc8`) is dereferenced with no rundown held on the
   virtual disk.
2. Only the *inner* rundown (`Parent->+0x610`) is acquired — the object that
   owns the pointer used to find it is left unprotected.
3. The prefetch state pointer (`+0x5f8`) is read from the same unprotected
   allocation and passed to a function that writes through it.

The codebase already had the correct idiom available —
`VhdmpiAddRundownRefSurfaceByVirtualDisk` @ `140002e14` exists precisely to take a
surface reference *via* a pinned virtual disk. `VhdmpiQueueIoRequest` simply
didn't use it.

`EX_RUNDOWN_REF` is a small, frequently allocated kernel object, so the freed slot
sits in a predictable pool bucket. Reclaiming it with a controlled allocation
before the acquire turns the increment and the indexed write into a
corrupted-kernel-object primitive rather than a bare bugcheck.

---

### Full-diff findings (ghidriff, 10.0.26100.8972 → 10.0.26100.9168)

Targeted decompilation of `VhdmpiQueueIoRequest` confirmed the prefetch fix but
did **not** show the whole patch. The full binary diff reports 5 added and 7
modified functions, and **two** new Controlled Feature Rollout flags:

| | |
|---|---|
| `Feature_3417294137` | prefetch rundown reorder (analysed above) |
| `Feature_1070577979` | **backing-store access — a second, separate lifetime fix** |

Added: `VhdmpiReleaseBackingStoreAccessLocked`.
Modified: `VhdmpiAcquireBackingStoreAccessLocked`,
`VhdmpiReleaseBackingStoreAccessForSecurityContext`, `VhdmpiQueueIoRequest`
(matched at only **28%**).

The backing-store change is the same *class* of fix — acquire/release ordering
on a shared object, with a new explicitly-`Locked` variant introduced — but it
is a distinct code path from the prefetch bug. MSRC published exactly one VHD
Miniport CVE for August 2026, so either both changes belong to CVE-2026-59125,
or one of them is a silent fix shipped alongside it. **This is not resolved
here.**

Methodological note: the targeted-decompile approach confirms a hypothesis
efficiently but systematically under-reports scope. The full diff is what
surfaces the parts of a patch nobody told you to look for.

---

## Patch Analysis

### Patch Mechanism

The fix takes the virtual disk's rundown **first**, holds it across the parent
access, and releases it **last** — outer-before-inner, release in reverse:

```c
// VhdmpiQueueIoRequest — PATCHED (feature-enabled branch)
lVar12 = *(longlong *)param_2;
if (ExAcquireRundownProtection(lVar12 + 0xd8)) {              /* +0xd8 — pin the DISK first */
    lVar12 = *(longlong *)(*(longlong *)param_2 + 200);       /* +0xc8 — now safe */
    if (lVar12 != 0) {
        if (ExAcquireRundownProtection(lVar12 + 0x610)) {     /* +0x610 — inner */
            p_Var4 = *(_VHD_PREFETCH_STATE **)(lVar12 + 0x5f8);
            if (p_Var4[0xac] != 0)
                VhdmpiRecordForPrefetchWorker(param_2, p_Var4);
            ExReleaseRundownProtection(lVar12 + 0x610);
        }
    }
    ExReleaseRundownProtection(*(longlong *)param_2 + 0xd8);  /* release LAST */
}
```

Once the disk rundown is held, the `+0xc8` slot cannot be freed until it is
released, which makes the parent access and everything below it safe.

### Patch Completeness Assessment

Two observations that materially change the security picture, neither of which
appears in the public write-up of this bug.

**1. The fix is gated behind a Controlled Feature Rollout flag.**

```c
if (Feature_3417294137__private_IsEnabledDeviceUsageNoInline() == 0) {
    /* ORIGINAL VULNERABLE CODE — SHIPS VERBATIM IN THE PATCHED BINARY */
    lVar12 = *(longlong *)(*(longlong *)param_2 + 200);   // Parent, no disk pin
    if (lVar12 != 0) {
        if (ExAcquireRundownProtection(lVar12 + 0x610)) { ... }
    }
} else {
    /* fixed path shown above */
}
```

Both branches are present in `10.0.26100.9168`. Which one executes depends on
`Feature_3417294137`. This is an **input-gated fix, not a root-cause fix** — on a
fully patched machine with the flag disabled, the original UAF is still reachable.
Any assessment that treats "patched build installed" as equivalent to "not
vulnerable" is incomplete for this CVE.

**2. A sibling call site with the same shape was not changed.**

The write path, roughly forty lines earlier in the same function, is byte-identical
between the two builds and sits **outside** the feature gate:

```c
lVar15 = *(longlong *)(param_1 + 200);              // VirtualDisk->Parent — still no disk pin
if (!ExAcquireRundownProtection(lVar15 + 0x50)) {   // different inner rundown
    // trace: "Failed to reference an active surface for write"
    return 0xc0000010;
}
```

Same object, reached the same unsafe way, different inner rundown (`+0x50`).
Whether this is independently exploitable depends on whether the SRB path holds a
disk reference by other means — not established here, so this is a **variant lead,
not a confirmed second vulnerability.** Structurally it is the exact pattern the
patch was written to eliminate.

---

## Attack Path

```
Thread A (I/O)                     Thread B (detach)
──────────────                     ─────────────────
VhdmpiQueueIoRequest
  reads VirtualDisk->Parent (+0xc8)
  ── no rundown held on the disk ──
                                   AttachVirtualDisk detach path
                                   drains the disk rundown (+0xd8)
                                   frees the virtual disk allocation
  ExAcquireRundownProtection(
      Parent + 0x610)              ← writes into reclaimed pool
  reads PrefetchState (+0x5f8)
  VhdmpiRecordForPrefetchWorker
      LOCK inc [seg + 0x10]        ← atomic increment into reclaimed pool
      write [seg + 0x18 + idx*8]   ← 8-byte write, index from freed contents
```

Both threads belong to the same unprivileged user; the race needs no external
trigger and can be retried indefinitely.

---

## Detection Guidance

**Crash signature.** Bugchecks inside `vhdmp!VhdmpiQueueIoRequest` or
`vhdmp!VhdmpiRecordForPrefetchWorker`, typically `0xC4` (Driver Verifier
Detected Violation) or `0x50` (PAGE_FAULT_IN_NONPAGED_AREA), on a machine with
VHD attach activity. Special Pool on `vhdmp.sys` plus Driver Verifier makes the
window far more observable.

**Behavioural.** Rapid attach/detach cycling of a VHD or VHDX by a
non-administrative account, concurrent with sustained I/O to the same volume, is
the trigger shape. Legitimate mount workloads rarely detach while I/O is in
flight, and effectively never in a tight loop.

**Event sources.**
- `Microsoft-Windows-VHDMP/Operational` — attach and detach records
- `Microsoft-Windows-Hyper-V-VMMS/Storage`
- ETW provider `Microsoft-Windows-VHDMP` (`VHD_START_IO` is emitted from the
  vulnerable function itself)

**Hunting query shape.** Correlate `AttachVirtualDisk` / `DetachVirtualDisk` calls
per-process per-minute; alert on non-SYSTEM processes exceeding a low threshold
against the same image path.

**Config note.** Because the fix is CFR-gated, endpoint verification should not
rely on file version alone. Confirming `Feature_3417294137` is enabled is the
only way to establish that the fixed path is actually live.

---

## References

- MSRC advisory — CVE-2026-59125, August 2026
- Winbindex — `vhdmp.sys` version index
- Independent write-up: `tinysec.net/post/2026/08/vhdmp-vhdmpi-queue-io-request-prefetch-uaf`
  (documents the `+0x610` prefetch site; does not cover the feature gate or the
  `+0x50` sibling site)
