1. Overview

Field Value
Unpatched binary dxgmms2_unpatched.sys
Patched binary dxgmms2_patched.sys
Overall similarity 0.991
Matched functions 2033
Changed functions 7
Identical functions 2026
Unmatched (unpatched → patched) 0 / 0

Verdict: The patch retrofits owner-thread tracking onto the VIDMM_GLOBAL_ALLOC EX_PUSH_LOCK (at owner+0x158) used across the VIDMM / CVirtualAddressAllocator VA-range paths, and in AddVaRangeToVadRangeListWithFix gates the acquire behind the WIL feature-staging flag Feature_Servicing_PteDeadLock so a thread that already owns the lock skips re-acquisition. The unpatched driver acquires this lock without a self-ownership check, allowing a deterministic self-deadlock (DoS) if the same thread reenters an acquiring path while already holding it. Because the fix is feature-staged, the patched binary still ships the old unconditional path (selected when the flag is off). The separate CVirtualAddressAllocator DXGPUSHLOCK (this+7) was already owner-checked in both builds; the memory-corruption escalation is speculative and not demonstrated.


2. Vulnerability Summary

Finding 1 — Pushlock Self-Deadlock (MEDIUM)

  • Severity: Medium — local kernel denial of service (thread self-deadlock → BSOD). The only established primitive is availability loss; no privilege escalation or memory corruption is demonstrated, and the exact user-mode sequence that produces the reentrant acquire was not isolated from the binaries.
  • Vulnerability class: CWE-662 (Improper Synchronization) — self-acquisition of a non-recursive EX_PUSH_LOCK (the VIDMM_GLOBAL_ALLOC pushlock) by a thread that already holds it.
  • Affected functions: CVirtualAddressAllocator::AddVaRangeToVadRangeListWithFix (sub_1c005f13c, primary), CVirtualAddressAllocator::RemoveVaRangeFromVad (sub_1c006029c), VIDMM_MAPPED_VA_RANGE::~VIDMM_MAPPED_VA_RANGE (sub_1c000136c), VIDMM_GLOBAL::MemoryTransferInternal (sub_1c008bd80), CheckUniqueGpuVaMapping (sub_1c00875b4) (owner-tag hardening); VIDMM_GLOBAL::CreateOneAllocation (sub_1c005d34c, struct init); VIDMM_GLOBAL::CloseOneAllocation (sub_1c0069f40, a separate VA-range race-condition servicing fix).

Root cause

EX_PUSH_LOCK is a lightweight Windows kernel synchronization primitive that does not support recursive (reentrant) acquisition. If a thread that already holds a pushlock exclusively calls ExAcquirePushLockExclusiveEx on the same lock again, the thread blocks forever waiting for itself — a guaranteed self-deadlock.

The relevant lock is the EX_PUSH_LOCK inside the VIDMM_GLOBAL_ALLOC object (the object returned by GetVidMmGlobalAllocFromOwner), at offset +0x158 (owner-thread slot at +0x160). This is distinct from the CVirtualAddressAllocator's own DXGPUSHLOCK at this+7, which already carries an owner check (*(this+8) != KeGetCurrentThread()) in both builds. Several functions acquire the VIDMM_GLOBAL_ALLOC lock without a self-ownership check, with the pattern:

if (owner) {
    KeEnterCriticalRegion();
    ExAcquirePushLockExclusiveEx(owner + 0x158, 0);   // no ownership check (unpatched)
    /* ... work ... */
    ExReleasePushLockExclusiveEx(owner + 0x158, 0);
    KeLeaveCriticalRegion();
}

Failure mode: if the same thread reenters one of these acquiring functions while it already holds the VIDMM_GLOBAL_ALLOC lock for the same object, the second ExAcquirePushLockExclusiveEx blocks the thread on itself. The DPC watchdog eventually fires DPC_WATCHDOG_VIOLATION (0x133) or DRIVER_POWER_STATE_FAILURE (0x9F).

Two corrections to note: (1) AddVaRangeToVadRangeListWithFix calls RemoveVaRangeFromVad(this, ..., 1) with a5 = 1 in both builds; RemoveVaRangeFromVad acquires owner+0x158 only when a5 == 0, so that specific edge is not the deadlock source. (2) A premature-release / memory-corruption path is not established: the unpatched release runs only on a call that actually acquired, so no concurrent-manipulation UAF is demonstrated.

The unpatched VIDMM_GLOBAL_ALLOC object does not use the owner-thread slot at +0x160. sub_1c005d34c (CreateOneAllocation) allocates a 0x1f8-byte object; the patch grows it and zero-inits the new +0x160 slot.

What the patch does

  • Enlarges the VIDMM_GLOBAL_ALLOC object from 0x1f8 (504) → 0x208 (520) bytes.
  • In the constructor (CreateOneAllocation), zero-inits the new owner-thread slot obj[44] (+0x160) and writes the type tag 0x35 at +0x16c (*((_DWORD*)obj+91)=53). This is discrete field initialization, not a literal memset.
  • After a successful acquire in the affected functions, writes *(owner + 0x160) = KeGetCurrentThread() and clears it (= 0) before release.
  • In AddVaRangeToVadRangeListWithFix, gates the acquire behind the WIL feature-staging flag Feature_Servicing_PteDeadLock__private_IsEnabledDeviceUsage (sub_1c0018990, patched build only). Feature off: the original unconditional acquire runs. Feature on: acquire only if *(owner+0x160) != KeGetCurrentThread(), record with a v68 flag, and release only when this call acquired.
  • The staging-flag calls Feature_3895685435__private_IsEnabledDeviceUsage (sub_1c00179a4sub_1c00179ec) and Feature_Servicing_VARangeHoldReference__private_IsEnabledDeviceUsage (sub_1c00179f8sub_1c0017a40) are the same WIL functions in both builds; only their addresses relocated. They are not new or changed reentrancy helpers.
  • sub_1c0069f40 (CloseOneAllocation) is a separate change: it adds a Feature_Servicing_VARangeRaceCondition-gated deferred-cleanup path (QueueSystemCleanupCommandAndWait). The TerminateOneAllocation call is unchanged (no arg2 → 0).

Attacker-reachable entry point and data flow

  1. Attacker process opens a Direct3D device (or uses D3DKMT* thunks) and obtains a handle to a VidPn source / allocation set.
  2. User-mode calls trigger dxgkrnl.sys, which dispatches into dxgmms2.sys.
  3. VA-range operations land in CVirtualAddressAllocator::AddVaRangeToVadRangeList (sub_1c0087078).
  4. That invokes AddVaRangeToVadRangeListWithFix (sub_1c005f13c), which acquires the VIDMM_GLOBAL_ALLOC pushlock at owner+0x158.
  5. If the same thread reenters an acquiring function while already holding that lock, the second acquire self-deadlocks (unpatched). The exact user-mode sequence producing this reentrancy was not isolated statically and must be confirmed dynamically.

3. Pseudocode Diff

AddVaRangeToVadRangeListWithFix (sub_1c005f13c; patched 0x1c005f15c) — primary function

// ============================ UNPATCHED ============================
__int64 AddVaRangeToVadRangeListWithFix(this, a2, a3, a4, a5)
{
    // separate DXGPUSHLOCK at this+7 is ALREADY owner-checked (unchanged in both builds)
    if (*(this + 8) != KeGetCurrentThread()) { DXGPUSHLOCK::AcquireExclusive(this + 7); v64 = 1; }

    owner = GetVidMmGlobalAllocFromOwner(...);
    // ❌ VIDMM_GLOBAL_ALLOC lock: no self-ownership check
    if (owner) {
        KeEnterCriticalRegion();
        ExAcquirePushLockExclusiveEx(owner + 0x158, 0);        // ← self-deadlock if reentrant
    }

    /* ... work; RemoveVaRangeFromVad(this, ..., /*a5=*/1) is called here,
       which SKIPS acquiring owner+0x158 because a5!=0 ... */

    // release gated only on owner != 0
    if (owner) {
        ExReleasePushLockExclusiveEx(owner + 0x158, 0);
        KeLeaveCriticalRegion();
    }
    return status;
}

// ============================= PATCHED =============================
__int64 AddVaRangeToVadRangeListWithFix(this, a2, a3, a4, a5)
{
    if (*(this + 8) != KeGetCurrentThread()) { DXGPUSHLOCK::AcquireExclusive(this + 7); v64 = 1; }
    owner = GetVidMmGlobalAllocFromOwner(...);

    v68 = 0;
    if (Feature_Servicing_PteDeadLock__private_IsEnabledDeviceUsage() == 0) {   // sub_1c0018990 (WIL flag)
        if (owner) {                                          // OLD path preserved when feature off
            KeEnterCriticalRegion();
            ExAcquirePushLockExclusiveEx(owner + 0x158, 0);
            *(owner + 0x160) = KeGetCurrentThread();
        }
    } else if (owner && *(owner + 0x160) != KeGetCurrentThread()) {  // ✅ NOT already owner?
        KeEnterCriticalRegion();
        ExAcquirePushLockExclusiveEx(owner + 0x158, 0);
        v68 = 1;                                              // ✅ THIS call acquired
        *(owner + 0x160) = KeGetCurrentThread();              // ✅ owner tag at +0x160
    }
    // else: already owner → SKIP acquire (reentrant-safe)

    /* ... work ... */

    // ✅ CONDITIONAL release — clears owner, fires only when this call acquired
    //    (feature-off: owner!=0 ; feature-on: v68)
    ...
}

CreateOneAllocation (sub_1c005d34c) — struct initialization

// UNPATCHED: operator new size = 504 (0x1f8), discrete field zeroing
obj = operator new(504, ...);
obj[43] = 0;            // pushlock value at +0x158 (index 0x2b)
obj[59] = 0;            // field at +0x1d8 (index 0x3b)

// PATCHED: operator new size = 520 (0x208), discrete field init (no literal memset)
obj = operator new(520, ...);
obj[43] = 0;                        // pushlock at +0x158
obj[44] = 0;                        // ✅ NEW owner-thread slot at +0x160
*((_DWORD*)obj + 90) = 0;           // +0x168
*((_DWORD*)obj + 91) = 53;          // ✅ type/state tag 0x35 at +0x16c
obj[61] = 0;                        // former obj[59] shifted +0x10 to +0x1e8

4. Assembly Analysis

AddVaRangeToVadRangeListWithFix (UNPATCHED 0x1c005f13c) — VIDMM_GLOBAL_ALLOC lock, no owner check

; owner = GetVidMmGlobalAllocFromOwner(...)
; -------- ACQUIRE without self-ownership check --------
lea     rcx, [owner+0x158]              ; VIDMM_GLOBAL_ALLOC pushlock (index 0x2b)
xor     edx, edx
call    ExAcquirePushLockExclusiveEx    ; ❌ no owner comparison → self-deadlock if reentrant
; ... work; RemoveVaRangeFromVad is called with a5=1 and SKIPS re-acquiring owner+0x158 ...
; -------- release, gated only on owner != 0 --------
lea     rcx, [owner+0x158]
call    ExReleasePushLockExclusiveEx
call    KeLeaveCriticalRegion

AddVaRangeToVadRangeListWithFix (PATCHED 0x1c005f15c) — feature-staged owner gate

call    Feature_Servicing_PteDeadLock__private_IsEnabledDeviceUsage  ; sub_1c0018990 (WIL staging flag)
test    eax, eax
je      feature_off_path                ; feature off → original unconditional acquire

; ---- feature-on path ----
mov     rax, gs:[0x188]                 ; KeGetCurrentThread
cmp     [owner+0x160], rax              ; ✅ already owner?
je      skip_acquire                    ; ✅ SKIP if this thread already owns it
call    ExAcquirePushLockExclusiveEx    ; owner+0x158
mov     [owner+0x160], rax              ; ✅ owner tag; v68 = 1

; ---- release ----
; clears [owner+0x160] = 0 and releases only when this call acquired (feature-off: owner!=0 ; feature-on: v68)

RemoveVaRangeFromVad (sub_1c006029c; patched 0x1c0060348)

; acquires the SAME owner+0x158 lock, but only when its a5 argument == 0.
; called from AddVaRangeToVadRangeListWithFix with a5 = 1 in BOTH builds, so it does NOT re-acquire on that edge.
; patched adds: mov [owner+0x160], KeGetCurrentThread after acquire, and mov [owner+0x160], 0 before release.

Key observation: The fix is feature-staged. sub_1c0018990 is the WIL feature-staging flag Feature_Servicing_PteDeadLock, not a bespoke runtime reentrancy detector; when it returns 0 the patched binary runs the original unconditional acquire. The real hardening is the cmp [owner+0x160], gs:[0x188] self-ownership gate plus the owner-tag writes at owner+0x160.


5. Trigger Conditions

  1. Obtain a graphics handle. Open a Direct3D 11/12 device via D3D11CreateDevice / D3D12CreateDevice, or use the D3DKMT* API (D3DKMTOpenAdapterFromHdc, D3DKMTCreateDevice) to obtain an hDevice against a WDDM-driven GPU.
  2. Allocate resources that share a per-device context object. Create several textures or buffers (D3DKMTCreateAllocation with the same hDevice/hResource) so that their allocation ranges share a single per-object pushlock at ctx+0x158.
  3. Induce overlapping allocation ranges. Allocate resources whose virtual address ranges overlap, then perform operations that force the range manager to reconcile them:
  4. D3DKMTRename (renaming allocations in place),
  5. OfferResources / ReclaimResources cycles,
  6. destroy (D3DKMTDestroyAllocation) while a concurrent Map/Unmap is in flight on the same device,
  7. or saturate the same VidPn source with allocation create/destroy churn.
  8. Force a reentrant acquire of owner+0x158. The bug requires the same thread to reenter one of the VIDMM_GLOBAL_ALLOC-pushlock acquiring functions while it already holds that lock for the same object. Note the AddVaRangeToVadRangeListWithFix → RemoveVaRangeFromVad edge passes a5=1 and does NOT re-acquire, so the reentrancy must come from another path; the exact user-mode sequence was not isolated statically and must be confirmed dynamically.
  9. No special race timing is needed for the deadlock mode — once the reentrant acquire of owner+0x158 is reached, the thread blocks deterministically on itself.
  10. Observable effect.
  11. Deadlock path: system hangs; bug check 0x133 DPC_WATCHDOG_VIOLATION or 0x9F DRIVER_POWER_STATE_FAILURE. In the debugger, the faulting thread sits in KeWaitForSingleObjectExfAcquirePushLockExclusive on the lock value at [owner+0x158].
  12. Corruption path (speculative): not established from the binaries; the unpatched release runs only on a call that actually acquired, so a premature-release UAF is not demonstrated.

6. Exploit Primitive & Development Notes

Primitive

  • Established: Kernel denial-of-service via deterministic thread self-deadlock on the VIDMM_GLOBAL_ALLOC pushlock, once a thread reenters an acquiring path while already holding owner+0x158.
  • Not established from the binaries: A premature-release / concurrent-list-mutation route to a use-after-free is not demonstrated. In the unpatched build the release fires only on a call that actually acquired, so no corruption primitive exists to build on.

Turning the primitive into an exploit

  • For DoS: trigger the reentrant self-acquire of owner+0x158 on a thread that already holds it. The self-deadlock is deterministic once that reentrant acquire is reached; the specific user-mode path that produces the reentrancy was not isolated from the binaries and must be confirmed dynamically.
  • No privilege-escalation or memory-corruption chain is supported by the binaries. In the unpatched build the release of owner+0x158 runs only on a call that actually acquired it, so there is no premature-release / concurrent-list-mutation route to a use-after-free to build on. Only the availability (DoS) primitive is established.

Mitigations affecting exploitability

The DoS primitive is a self-deadlock that crashes the machine; it is not gated by KASLR, SMEP/SMAP, CFG/kCFG, or HVCI, none of which affect a thread blocking on a lock it already holds. Because no read/write or code-execution primitive is established, exploit-mitigation analysis for privilege escalation is not applicable here.


7. Debugger PoC Playbook

This playbook assumes WinDbg/KD is attached to the unpatched target with symbols resolved for dxgmms2.sys and ntoskrnl.exe.

Symbols & base resolution

!devnode 0 1 dxgmms         ; or
lm m dxgmms2                ; find loaded base
!for_each_module            ; enumerate

Add the offsets below to the loaded base. The addresses shown are RVAs (relative to the module load base in the JSON; subtract 0x1c0000000 to get an RVA).

Breakpoints

; Primary acquire point  AddVaRangeToVadRangeListWithFix
bp dxgmms2!?AddVaRangeToVadRangeListWithFix@CVirtualAddressAllocator@@QEAAJPEAUVIDMM_VAD@@IPEAPEAU_LIST_ENTRY@@PEAUVIDMM_MAPPED_VA_RANGE@@@Z
bp dxgmms2+0x5f13c           ; if the private symbol is unavailable (unpatched RVA)

; RemoveVaRangeFromVad  acquires owner+0x158 only when a5==0 (a5=1 from the caller skips it)
bp dxgmms2!?RemoveVaRangeFromVad@CVirtualAddressAllocator@@QEAAXPEAU_LIST_ENTRY@@EEE@Z
bp dxgmms2+0x6029c           ; unpatched RVA

; Constructor  inspect the VIDMM_GLOBAL_ALLOC object pointer being initialized
bp dxgmms2!?CreateOneAllocation@VIDMM_GLOBAL@@...
bp dxgmms2+0x5d34c           ; unpatched RVA

; Generic  fire whenever the suspect pushlock is taken
bp nt!ExAcquirePushLockExclusiveEx

Conditional breakpoint to catch the self-deadlock

; at the ExAcquirePushLockExclusiveEx call for owner+0x158, rcx = pushlock ptr = owner+0x158
bp nt!ExAcquirePushLockExclusiveEx ".if (poi(@rcx) & 1) { .echo LOCKED-ALREADY; !thread; kb; r; g } .else { g }"

This checks whether the pushlock already has its "locked" bit set when the acquire is reached, and whether gs:[0x188] (current ETHREAD) equals the owner tag at owner+0x160 — a strong signal the calling thread already owns it.

What to inspect at each breakpoint

Stop Register / Memory to watch Meaning
AddVaRangeToVadRangeListWithFix entry args in rcx/rdx/r8/r9; owner = GetVidMmGlobalAllocFromOwner(...) owner+0x158 = VIDMM_GLOBAL_ALLOC pushlock, owner+0x160 = owner-thread tag (unused in unpatched).
acquire call rcx = owner+0x158 poi(rcx) & 1 means already locked. Compare gs:[0x188] to poi(owner+0x160).
RemoveVaRangeFromVad a5 argument If a5!=0 the function skips the acquire/release of owner+0x158; called with a5=1 from AddVaRangeToVadRangeListWithFix.
CreateOneAllocation allocated object after operator new operator new size is 504 (0x1f8) unpatched vs 520 (0x208) patched. db <obj>+0x150 l20 shows the +0x160 owner slot.
nt!ExAcquirePushLockExclusiveEx rcx = pushlock dt _EX_PUSH_LOCK poi(rcx); bits as described below.

EX_PUSH_LOCK bit layout (from the value at [ctx+0x158]):

bit 0       = Locked
bit 1       = Shared waiters present
bit 2       = Exclusive waiters present
bits 3..    = Waiter count

A value of 0x1 with the current ETHREAD blocked in KeWaitForSingleObject confirms self-deadlock.

Trigger setup from user mode

Reaching the vulnerable code requires a user-mode graphics client (Direct3D / D3DKMT) that drives VIDMM allocation and GPU-VA mapping so execution enters CVirtualAddressAllocator::AddVaRangeToVadRangeList (0x1c0087078) and the other VIDMM_GLOBAL_ALLOC-pushlock acquiring functions. The concrete user-mode call sequence that makes the same thread reenter one of those functions while it already holds owner+0x158 was not isolated from the binaries and must be found dynamically. Note the AddVaRangeToVadRangeListWithFix → RemoveVaRangeFromVad edge passes a5=1 and does not re-acquire owner+0x158, so that edge is not the reentrancy source; the reentrant acquirer must come from another path.

Expected observation

  • Deadlock variant: !process 0 0 shows the offending thread in KeWaitForSingleObjectExfAcquirePushLockExclusive. The pushlock at ctx+0x158 reads 0x1. Bug check 0x133 DPC_WATCHDOG_VIOLATION after ~10 seconds.

Struct / offset cheat sheet (per-object context)

Offset Meaning Notes
+0x150 (alignment / pre-pushlock area)
+0x158 EX_PUSH_LOCK value (obj[0x2b]) The vulnerable lock.
+0x160 Owner ETHREAD* (obj[0x2c]) Uninitialized in unpatched. Patched writes gs:[0x188] here.
+0x16c Type/state tag (= 0x35 patched) New in patched.
Total size 0x1f8 unpatched / 0x208 patched Patch adds 0x10.

8. Changed Functions — Full Triage

Function Sim. Change Note
VIDMM_GLOBAL::CreateOneAllocation (sub_1c005d34c) 0.986 behavioral Object allocator. operator new size 504 → 520 (0x1f8 → 0x208); adds discrete init of new owner slot obj[44] (+0x160) and type tag *((_DWORD*)obj+91)=53 (0x35 at +0x16c). No literal memset. Backs the patch — zero-inits the owner-thread slot.
CVirtualAddressAllocator::AddVaRangeToVadRangeListWithFix (sub_1c005f13c; patched 0x1c005f15c) 0.923 security Primary function. Gates the VIDMM_GLOBAL_ALLOC lock acquire behind feature flag Feature_Servicing_PteDeadLock (sub_1c0018990), owner tag at owner+0x160, conditional release via v68.
VIDMM_GLOBAL::CloseOneAllocation (sub_1c0069f40; patched 0x1c006a030) 0.920 behavioral Adds a Feature_Servicing_VARangeRaceCondition-gated deferred-cleanup path (QueueSystemCleanupCommandAndWait); struct offsets shift +0x10. TerminateOneAllocation call unchanged (no arg2 → 0). Separate servicing fix.
VIDMM_GLOBAL::MemoryTransferInternal (sub_1c008bd80; patched 0x1c008bf20) 0.975 security Adds owner tag after acquire (*(owner+0x160) = KeGetCurrentThread()) and clear before release. Acquire itself not feature-gated here.
VIDMM_MAPPED_VA_RANGE::~VIDMM_MAPPED_VA_RANGE (sub_1c000136c) 0.981 security Destructor. Adds owner tag at owner+0x160. The Feature_3895685435 (sub_1c00179a4sub_1c00179ec) and Feature_Servicing_VARangeHoldReference (sub_1c00179f8sub_1c0017a40) calls are the SAME WIL flags, only relocated.
CVirtualAddressAllocator::RemoveVaRangeFromVad (sub_1c006029c; patched 0x1c0060348) 0.943 security Adds owner tag at owner+0x160. Acquisition of owner+0x158 already gated by the a5 argument in both builds (skipped when a5!=0); called with a5=1 from AddVaRangeToVadRangeListWithFix, so not itself the deadlock source.
CheckUniqueGpuVaMapping (sub_1c00875b4; patched 0x1c0087744) 0.987 security Adds owner tag at owner+0x160 (a1+0x160) and clear before release. Same hardening pattern.

Cosmetic / register-allocation changes (collapsed): the functions show routine shifts in struct-field offsets (+0x1f0 → +0x200, +0x168 → +0x178, etc.) driven by the VIDMM_GLOBAL_ALLOC size growth. These are not independently exploitable.

Behavioral-but-not-directly-exploitable notes: - sub_1c0069f40's real change is a Feature_Servicing_VARangeRaceCondition-gated cleanup path, a distinct servicing hardening; the previously reported arg2 → 0 cross-process-context change is not present. - sub_1c0018990 (Feature_Servicing_PteDeadLock), sub_1c00179ec (Feature_3895685435) and sub_1c0017a40 (Feature_Servicing_VARangeHoldReference) are WIL feature-staging flags, not bespoke reentrancy helpers. The last two exist in both builds and are merely relocated.


9. Unmatched Functions

None. Both unmatched_unpatched and unmatched_patched are 0. The patch is in-place. sub_1c0018990 (Feature_Servicing_PteDeadLock__private_IsEnabledDeviceUsage) is a WIL feature-staging flag present in the patched build; it is called from the existing functions. The Feature_3895685435 and Feature_Servicing_VARangeHoldReference staging flags exist in both builds and simply relocated (sub_1c00179a4→sub_1c00179ec, sub_1c00179f8→sub_1c0017a40).

Implication: The fix is feature-staged: the patched binary retains the original unconditional-acquire path, selected when Feature_Servicing_PteDeadLock is disabled. The owner-tag hardening is distributed across the consumer functions plus the constructor.


10. Confidence & Caveats

Confidence: Medium-High (for DoS) / Low (for memory corruption).

Rationale: - The acquire of owner+0x158 without a self-ownership check is directly visible in the unpatched AddVaRangeToVadRangeListWithFix (and the other acquiring functions), and the patched build adds the Feature_Servicing_PteDeadLock-gated self-ownership check plus the owner tag at owner+0x160. The self-deadlock is deterministic given a thread reenters an acquiring path while already holding the lock; the specific reentrant path was not isolated statically. - The memory-corruption path is speculative: the unpatched release runs only on a call that actually acquired, so a premature-release UAF is not demonstrated from the binaries.

Assumptions made: - Struct index [0x2b]/[0x2c] maps to byte offsets +0x158/+0x160, consistent with the code (owner + 344 / owner + 352). - The call chain Direct3D → dxgkrnl.sys → AddVaRangeToVadRangeList (0x1c0087078) → AddVaRangeToVadRangeListWithFix (0x1c005f13c) is inferred; the reentrant acquirer of owner+0x158 on the same thread was not enumerated. - The +0x160 owner slot is unused in the unpatched VIDMM_GLOBAL_ALLOC object; the patch grows the object 0x1f8 → 0x208 and zero-inits it in CreateOneAllocation (discrete field store, not a memset). Confirm with db <obj>+0x150 l20.

To verify before writing a PoC: 1. Identify a real path in which one thread reenters a VIDMM_GLOBAL_ALLOC-pushlock acquiring function while already holding owner+0x158 (the AddVaRange → RemoveVaRange edge passes a5=1 and is not it). 2. Confirm the reentrant acquirer uses the same owner pointer (!address @rcx matches the outer lock VA). 3. Identify which D3DKMT IOCTL / D3D11 operation drives that path. 4. Test the DoS self-deadlock; the corruption primitive is not established and should not be assumed. 5. Verify the Feature_Servicing_PteDeadLock state on the target build, since the fix only engages when that feature is enabled.