vhdmp.sys VhdmpiAcquireBackingStoreAccessLocked type confusion
vhdmp.sys parses every supported VHD and VHDX format natively in the kernel,
and any local user can mount an image they own. A mounted image carries a
chain of backing stores — the differencing-disk chain — and each store has a
Type field that selects the vtable its operations dispatch through. The
Type tag derives from the on-disk image, so it is attacker input. Whatever
handler sits behind a vtable slot has to either be valid for every Type the
dispatcher will name, or the dispatcher has to guard on Type before calling.
Those are the only two safe designs, and the backing-store refcount machinery
did neither.
Two helpers — VhdmpiAcquireBackingStoreAccessLocked on the 0->1 refcount
transition and VhdmpiReleaseBackingStoreAccessLocked on the 1->0
transition — call straight through Store->pVtbl->AcquireWriteAccess (slot
+0x40) and its ReleaseWriteAccess twin (slot +0x60) regardless of
Type. The vulnerable build dispatched unconditionally, gated only by the
refcount transition itself and the store’s flag byte:
/* VhdmpiAcquireBackingStoreAccessLocked - vulnerable build (10.0.26100.33158)
Store = _VHD_BACKING_STORE_HEADER*, SecurityContext, Mode = access mode */
if (Store == &VhdmpiEmptyISOBackingStore)
return (Mode == BackingStoreWriteAccess) ? 0xC00000A2 : 0;
NTSTATUS Status = VhdmpiAcquireFileWrapperAccess(&Store->FileWrapper, /* +0x48 */
SecurityContext, Mode);
if (Status < 0)
return Status;
if (Mode != BackingStoreWriteAccess) /* only the write path drives refcount + vtable */
return 0;
LONG64 NewRef = ++Store->WriteRefCount; /* +0x440 - the 0->1 transition is watched here */
if (NewRef != 1 || Store->Flags == 0) /* +0x8 - and only when the store flag byte is set */
return 0;
/* No Type check: the dispatch table at +0x0 is trusted for every store kind. */
NTSTATUS Result = Store->pVtbl->AcquireWriteAccess(Store); /* +0x0 -> +0x40 */
if (Result >= 0)
return 0;
Store->WriteRefCount -= 1; /* +0x440 - unwind the transition */
VhdmpiReleaseFileWrapperAccess(&Store->FileWrapper, Mode); /* +0x48 */
VhdmpiCheckFileWrapperForError(Store);
return Result;
The vtable is selected by Store->pVtbl->Type /* +0x10 */. For stores of
Type == 1 the callbacks at slot +0x40 and its +0x60 release twin are
not valid handlers — the state they mutate is not legal for that
backing-store kind. Because Type derives from the on-disk image, this is
an attacker-shaped type confusion: the dispatcher trusts a data-driven tag
and runs the wrong virtual handler. Acquire and release are a matched pair,
so the same unconditional dispatch sits on the back-side — in the
vulnerable build it was inlined straight into
VhdmpiReleaseBackingStoreAccessForSecurityContext at the matching 1->0
transition; the patch lifts it into its own
VhdmpiReleaseBackingStoreAccessLocked and applies the same Type guard
there. Touching only one of the two would unbalance the refcount machine;
both halves shipped with the bug, which is the signal that the dispatch
itself was the issue, not one call site.
The patch
Both helpers now short-circuit when the type tag is 1:
/* VhdmpiAcquireBackingStoreAccessLocked - patched build (10.0.26100.33296) */
if (Store == &VhdmpiEmptyISOBackingStore)
return (Mode == BackingStoreWriteAccess) ? 0xC00000A2 : 0;
NTSTATUS Status = VhdmpiAcquireFileWrapperAccess(&Store->FileWrapper, /* +0x48 */
SecurityContext, Mode);
if (Status < 0)
return Status;
if (Mode != BackingStoreWriteAccess)
return 0;
LONG64 NewRef = ++Store->WriteRefCount; /* +0x440 */
if (NewRef != 1 || Store->Flags == 0) /* +0x8 */
return 0;
struct _VHD_BACKING_STORE_VTBL* pVtbl = Store->pVtbl; /* +0x0 */
if (pVtbl->Type == 1) /* +0x10 - type 1 has no write-access transition callback */
return 0;
NTSTATUS Result = pVtbl->AcquireWriteAccess(Store); /* +0x40 */
if (Result >= 0)
return 0;
Store->WriteRefCount -= 1; /* +0x440 */
VhdmpiReleaseFileWrapperAccess(&Store->FileWrapper, BackingStoreWriteAccess); /* +0x48 */
VhdmpiCheckFileWrapperForError(Store);
return Result;
The mirrored guard lands in VhdmpiReleaseBackingStoreAccessLocked too. The
vtable is still selected by Type, but the dispatcher now decides whether a
given Type has a handler before invoking it, instead of trusting the slot to
be valid for every Type.
Attack path
flowchart TD
A["crafted VHD with a Type = 1 backing store"] --> B["write-access acquire hits the 0-to-1 refcount transition"]
B --> C["VhdmpiAcquireBackingStoreAccessLocked dispatches AcquireWriteAccess with no Type guard"]
C --> D["wrong handler runs for a type 1 store, backing-store state corrupted"]
D --> E["type confusion on the backing-store object toward EoP"]
A vtable pointer selected by an on-disk type field is a type confusion waiting to happen. When the dispatch table is chosen by data the attacker influences — a backing-store type tag derived from the VHD image — every callback slot must be valid for every Type the dispatcher will name, or the dispatcher has to guard on Type before calling. Acquire/release pairs that change together in the fix are the second signal: a patch that touches both halves of a refcount transition is admitting the mismatched handler was the real bug, not one isolated call site.