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.