dxgmms2.sys — Improper synchronization / EX_PUSH_LOCK self-deadlock (CWE-662) in CVirtualAddressAllocator::AddVaRangeToVadRangeListWithFix fixed
KB5078885
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(theVIDMM_GLOBAL_ALLOCpushlock) 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_ALLOCobject from0x1f8(504) →0x208(520) bytes. - In the constructor (
CreateOneAllocation), zero-inits the new owner-thread slotobj[44](+0x160) and writes the type tag0x35at+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 flagFeature_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 av68flag, and release only when this call acquired. - The staging-flag calls
Feature_3895685435__private_IsEnabledDeviceUsage(sub_1c00179a4→sub_1c00179ec) andFeature_Servicing_VARangeHoldReference__private_IsEnabledDeviceUsage(sub_1c00179f8→sub_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 aFeature_Servicing_VARangeRaceCondition-gated deferred-cleanup path (QueueSystemCleanupCommandAndWait). TheTerminateOneAllocationcall is unchanged (noarg2 → 0).
Attacker-reachable entry point and data flow
- Attacker process opens a Direct3D device (or uses
D3DKMT*thunks) and obtains a handle to a VidPn source / allocation set. - User-mode calls trigger
dxgkrnl.sys, which dispatches intodxgmms2.sys. - VA-range operations land in
CVirtualAddressAllocator::AddVaRangeToVadRangeList(sub_1c0087078). - That invokes
AddVaRangeToVadRangeListWithFix(sub_1c005f13c), which acquires theVIDMM_GLOBAL_ALLOCpushlock atowner+0x158. - 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
- Obtain a graphics handle. Open a Direct3D 11/12 device via
D3D11CreateDevice/D3D12CreateDevice, or use theD3DKMT*API (D3DKMTOpenAdapterFromHdc,D3DKMTCreateDevice) to obtain anhDeviceagainst a WDDM-driven GPU. - Allocate resources that share a per-device context object. Create several textures or buffers (
D3DKMTCreateAllocationwith the samehDevice/hResource) so that their allocation ranges share a single per-object pushlock atctx+0x158. - Induce overlapping allocation ranges. Allocate resources whose virtual address ranges overlap, then perform operations that force the range manager to reconcile them:
D3DKMTRename(renaming allocations in place),OfferResources/ReclaimResourcescycles,- destroy (
D3DKMTDestroyAllocation) while a concurrentMap/Unmapis in flight on the same device, - or saturate the same VidPn source with allocation create/destroy churn.
- Force a reentrant acquire of
owner+0x158. The bug requires the same thread to reenter one of theVIDMM_GLOBAL_ALLOC-pushlock acquiring functions while it already holds that lock for the same object. Note theAddVaRangeToVadRangeListWithFix → RemoveVaRangeFromVadedge passesa5=1and 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. - No special race timing is needed for the deadlock mode — once the reentrant acquire of
owner+0x158is reached, the thread blocks deterministically on itself. - Observable effect.
- Deadlock path: system hangs; bug check
0x133 DPC_WATCHDOG_VIOLATIONor0x9F DRIVER_POWER_STATE_FAILURE. In the debugger, the faulting thread sits inKeWaitForSingleObject←ExfAcquirePushLockExclusiveon the lock value at[owner+0x158]. - 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_ALLOCpushlock, once a thread reenters an acquiring path while already holdingowner+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+0x158on 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+0x158runs 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 0shows the offending thread inKeWaitForSingleObject←ExfAcquirePushLockExclusive. The pushlock atctx+0x158reads0x1. Bug check0x133 DPC_WATCHDOG_VIOLATIONafter ~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_1c00179a4→sub_1c00179ec) and Feature_Servicing_VARangeHoldReference (sub_1c00179f8→sub_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.