mqac.sys — compiler rebuild with function inlining
KB5094127
1. Overview
- Unpatched Binary:
mqac_unpatched.sys - Patched Binary:
mqac_patched.sys - Overall Similarity Score: 0.6206 (62.06%)
- Diff Statistics:
- Matched Functions: 517
- Changed Functions: 413
- Identical Functions: 104
- Unmatched Functions: 0 (in both directions)
Verdict: No security-relevant change was found. The two behaviors originally suspected as vulnerabilities are both artifacts of compiler inlining: the patched build inlines two small helper routines (CSMAllocator::ReleaseFreeHeaps and CTransaction::RestorePacket) into their callers, which changes the shape of the decompilation but not the semantics. Every validation, cleanup call, and error path present after the patch is already present, byte-for-byte in effect, before the patch. The remaining differences across the driver are toolchain churn: WIL feature-staging code, renamed WPP trace macros, alternate memset intrinsic variants, Control Flow Guard dispatch thunks, ProbeForRead inlining, and NonPagedPoolNx allocation modernization. None of these alters an attacker-reachable security decision.
2. Vulnerability Summary
Finding 1: Suspected Use-After-Free via incomplete cleanup — NOT A VULNERABILITY
- Status: No security-relevant change (compiler inlining)
- Affected Function:
ACDeviceControl(IOCTL dispatcher) — unpatched@ 0x14000B100, patched@ 0x1C000C880, case0x1965108F
Analysis:
In the unpatched dispatcher, IOCTL 0x1965108F calls CSMAllocator::ReleaseFreeHeaps(*(DeviceExtension + 0x140)) (a single call). That helper, CSMAllocator::ReleaseFreeHeaps @ 0x1400117E0, is itself three lines: it calls CPoolAllocator::PurgeHeaps on this+0x10, this+0x50, and this+0x90. The patched dispatcher performs those same three CPoolAllocator::PurgeHeaps calls at +0x10, +0x50, +0x90 directly inside the case, because the compiler inlined ReleaseFreeHeaps. The single unpatched call therefore does exactly what the three patched calls do. No list entry or sub-context is left dangling in either build, so there is no use-after-free. CPoolAllocator::PurgeHeaps is an allocator-maintenance routine that releases already-free heap views; it is not object teardown that could leave live references stale.
Finding 2: Suspected null-pointer dereference in dispatch handler — NOT A VULNERABILITY
- Status: No security-relevant change (compiler inlining)
- Affected Function:
ACPutRestoredPacket— unpatched@ 0x140008AD4, patched@ 0x1C0009F50
Analysis:
ACPutRestoredPacket obtains a packet buffer via CQEntry::Buffer @ 0x14000263C, which returns NULL when the entry's handle field (*(this+0x10)) is 0xFFFFFFFF. Both builds immediately fold that NULL into v12 = (buffer + 4) & -(buffer != 0) and branch if (v12 == 0) return 0xC000009A; before any dereference of v12, so a NULL buffer is handled identically in both builds. On the transactional tail, the unpatched build calls CTransaction::RestorePacket @ 0x140021A64; the patched build inlines that function's body into ACPutRestoredPacket. The "added" NULL check (if (result) ... else return error) and the "added" sign check (if (*(result+0xC) >= 0)) are already present inside the unpatched CTransaction::RestorePacket (see the assembly in section 4). Nothing was added; a helper was inlined.
3. Pseudocode Diff
Finding 1: IOCTL case 0x1965108F (dispatcher)
// === UNPATCHED ACDeviceControl @ 0x14000B100, case 0x1965108F ===
case 0x1965108Fu:
CSMAllocator::ReleaseFreeHeaps(*((CSMAllocator **)a1->DeviceExtension + 40)); // +0x140
v13 = 0;
goto LABEL_239;
// === UNPATCHED CSMAllocator::ReleaseFreeHeaps @ 0x1400117E0 (the callee) ===
void CSMAllocator::ReleaseFreeHeaps(CSMAllocator *this)
{
CPoolAllocator::PurgeHeaps((__int64)this + 16); // +0x10
CPoolAllocator::PurgeHeaps((__int64)this + 80); // +0x50
CPoolAllocator::PurgeHeaps((__int64)this + 144); // +0x90
}
// === PATCHED ACDeviceControl @ 0x1C000C880, case 0x1965108F (ReleaseFreeHeaps inlined) ===
case 0x1965108Fu:
v24 = *((_QWORD *)a1->DeviceExtension + 40); // +0x140
CPoolAllocator::PurgeHeaps(v24 + 16); // +0x10
CPoolAllocator::PurgeHeaps(v24 + 80); // +0x50
CPoolAllocator::PurgeHeaps(v24 + 144); // +0x90
v12 = 0;
goto LABEL_247;
Both builds purge the same three offsets (+0x10, +0x50, +0x90) of the context at DeviceExtension+0x140. Semantically identical.
Finding 2: ACPutRestoredPacket — buffer NULL guard and transactional tail
// === Buffer NULL guard — IDENTICAL in both builds ===
// UNPATCHED ACPutRestoredPacket @ 0x140008AD4:
v11 = CQEntry::Buffer(v10); // returns NULL if *(v10+0x10)==0xFFFFFFFF
v12 = ((unsigned __int64)v11 + 4) & -(__int64)(v11 != nullptr);
if ( v12 == 0 )
return 3221225626LL; // 0xC000009A, before any deref
// PATCHED ACPutRestoredPacket @ 0x1C0009F50 (CQEntry::Buffer inlined):
if ( *((_DWORD *)v10 + 4) == -1 ) AccessibleBuffer = 0;
else AccessibleBuffer = CMMFAllocator::GetAccessibleBuffer(*((_QWORD *)v10 + 3), *((unsigned int *)v10 + 4));
v12 = (AccessibleBuffer + 4) & -(__int64)(AccessibleBuffer != 0);
if ( v12 == 0 )
return 3221225626LL; // 0xC000009A, same guard
// === Transactional tail ===
// UNPATCHED: calls the helper (which contains the null + sign checks):
if ( operator!=<BOID>(...) && (v21 = CTransaction::Find(a1, (BOID *)(v12 + 16))) != nullptr )
return CTransaction::RestorePacket(v21, v5, v10, v15); // helper @ 0x140021A64
else
return CQueue::RestorePacket(v5, v10, v15);
// UNPATCHED CTransaction::RestorePacket @ 0x140021A64 (the callee) already contains both checks:
v8 = CQEntry::Buffer(a3);
v9 = ((unsigned __int64)v8 + 4) & -(__int64)(v8 != nullptr);
if ( v9 == 0 ) return 3221225626LL; // NULL check
if ( *(char *)(v9 + 12) >= 0 ) { CQueue::RestorePacket(...); return CTransaction::ProcessReceivedPacket(...); } // sign check
else { CTransaction::SendPacket(...); return 0; }
// PATCHED ACPutRestoredPacket @ 0x1C0009F50 (CTransaction::RestorePacket inlined):
if ( RtlCompareMemory((v12 + 16), &unk, 0x10) == 16 ) return CQueue::RestorePacket(v5, v10, v15);
v24 = CTransaction::Find(a1, (BOID *)(v12 + 16));
if ( v24 == nullptr ) return CQueue::RestorePacket(v5, v10, v15);
// re-fetch buffer, then the SAME null + sign checks that were inside the helper:
if ( (v26 & -(v27 != 0)) != 0 ) {
if ( *(char *)((v26 & ...) + 0xC) >= 0 ) { CQueue::RestorePacket(...); return CTransaction::ProcessReceivedPacket(v24, v10, nullptr); }
else CTransaction::SendPacket(v24, v5, v10);
} else return -1073741670; // 0xC000009A
4. Assembly Analysis
Finding 1: dispatcher case 0x1965108F and the inlined helper
; --- UNPATCHED ACDeviceControl @ 0x14000B100, case 0x1965108F ---
000000014000B6A5 mov rcx, [r15+40h]
000000014000B6A9 mov rcx, [rcx+140h] ; DeviceExtension+0x140
000000014000B6B0 call ?ReleaseFreeHeaps@CSMAllocator@@QEAAXXZ ; single call
000000014000B6B5 xor ebx, ebx ; STATUS_SUCCESS
; --- UNPATCHED CSMAllocator::ReleaseFreeHeaps @ 0x1400117E0 (the callee) ---
00000001400117E9 add rcx, 10h
00000001400117ED call ?PurgeHeaps@CPoolAllocator@@... ; +0x10
00000001400117F2 lea rcx, [rbx+50h]
00000001400117F6 call ?PurgeHeaps@CPoolAllocator@@... ; +0x50
00000001400117FB lea rcx, [rbx+90h]
0000000140011802 call ?PurgeHeaps@CPoolAllocator@@... ; +0x90
; --- PATCHED ACDeviceControl @ 0x1C000C880, case 0x1965108F (ReleaseFreeHeaps inlined) ---
00000001C000CDC1 mov rax, [rsi+40h]
00000001C000CDC5 mov rbx, [rax+140h] ; DeviceExtension+0x140
00000001C000CDCC lea rcx, [rbx+10h]
00000001C000CDD0 call ?PurgeHeaps@CPoolAllocator@@... ; +0x10
00000001C000CDD5 lea rcx, [rbx+50h]
00000001C000CDD9 call ?PurgeHeaps@CPoolAllocator@@... ; +0x50
00000001C000CDDE lea rcx, [rbx+90h]
00000001C000CDE5 call ?PurgeHeaps@CPoolAllocator@@... ; +0x90
00000001C000CDEA xor ebx, ebx ; STATUS_SUCCESS
The unpatched single ReleaseFreeHeaps call and the patched three PurgeHeaps calls are the same three-offset purge; the only difference is inlining.
Finding 2: the NULL + sign checks already present in the unpatched helper
; --- UNPATCHED CTransaction::RestorePacket @ 0x140021A64 ---
0000000140021A87 call ?Buffer@CQEntry@@... ; get packet buffer
0000000140021A8C lea r10, [rax+4]
0000000140021A90 neg rax
0000000140021A93 sbb rax, rax
0000000140021A96 and rax, r10 ; (buf+4) & -(buf!=0)
0000000140021A99 jnz short loc_140021AA2 ; NULL check already here
0000000140021A9B mov eax, 0C000009Ah ; error return on NULL
0000000140021AA0 jmp short loc_140021AD7
0000000140021AA2 mov al, [rax+0Ch]
0000000140021AA5 test al, al
0000000140021AA7 jns short loc_140021ABB ; sign check already here
Address 0x140021A99 is the NULL check and 0x140021AA5–0x140021AA7 is the sign check that the original report attributed to the patch. Both exist in the unpatched build; the patch merely inlines this routine into ACPutRestoredPacket.
5. Trigger Conditions
No reachable vulnerable condition exists in either build for the two behaviors examined.
- Finding 1: Sending IOCTL
0x1965108Fto\\.\MQACpurges the three free-heap lists atDeviceExtension+0x140(+0x10,+0x50,+0x90) in both the unpatched and patched drivers. No dangling reference is produced, so no follow-up IOCTL can reach freed memory as a result of this call. - Finding 2: Reaching
ACPutRestoredPacketwith an entry whose handle field is0xFFFFFFFFcausesCQEntry::Bufferto return NULL, which both builds detect viaif (v12 == 0) return 0xC000009A;before dereferencing. No NULL dereference occurs in either build.
6. Exploit Primitive & Development Notes
No exploit primitive is available from the behaviors examined. Neither the free-heap purge (Finding 1) nor the buffer/transaction handling (Finding 2) yields a use-after-free, out-of-bounds access, or NULL dereference in either build, because the cleanup and validation are semantically identical across the two builds. No arbitrary read/write, information leak, or privilege-escalation primitive was demonstrated or is supported by the code.
7. Debugger PoC Playbook
No proof-of-concept is applicable, because no vulnerability was confirmed. For anyone independently re-verifying the two behaviors:
- Finding 1: Break on
mqac_unpatched.sys!CSMAllocator::ReleaseFreeHeaps(0x1400117E0) and on the patched dispatcher case (0x1C000CDCC). In both cases the first argument isDeviceExtension+0x140, andCPoolAllocator::PurgeHeapsis invoked on+0x10,+0x50,+0x90. Confirm the free-heap lists at those offsets are drained equivalently. - Finding 2: Break on
mqac_unpatched.sys!CTransaction::RestorePacket(0x140021A64). Confirm the NULL branch at0x140021A99and the sign branch at0x140021AA5execute before any dereference of the buffer, matching the inlined logic in the patchedACPutRestoredPacket(0x1C0009F50).
8. Changed Functions — Full Triage
The two functions originally flagged as security-relevant are refactors only. The remaining changes across the driver are toolchain/rebuild churn. Function names below are the demangled symbols.
ACDeviceControl(IOCTL dispatcher) — unpatched0x14000B100, patched0x1C000C880- Change Type: Not security-relevant (inlining).
- Notes: Case
0x1965108FinlinesCSMAllocator::ReleaseFreeHeapsinto threeCPoolAllocator::PurgeHeapscalls at+0x10/+0x50/+0x90; identical to the unpatched single-call behavior. Other cases showProbeForReadinlining and buffer-size checks that are byte-equivalent in effect. ACPutRestoredPacket(restored-packet handler) — unpatched0x140008AD4, patched0x1C0009F50- Change Type: Not security-relevant (inlining).
- Notes: Inlines
CQEntry::Buffer,CQEntry::ReleaseBuffer, andCTransaction::RestorePacket. The NULL check and sign check exist unchanged in the unpatchedCTransaction::RestorePacket @ 0x140021A64. ACCreateRemoteProxy—0x14000644C- Change Type: Not security-relevant (modernization).
- Notes:
ProbeForReadinlined;ExAllocatePoolWithTagreplaced withExAllocatePoolWithTagPriority(NonPagedPoolNx, ...); custom free wrapper replaced with directExFreePoolWithTag. No semantic change. ACBeginGetPacket2Remote—0x140005328- Change Type: Not security-relevant.
- Notes: GUID comparison helper inlined to
RtlCompareMemory; equivalent. ACAssociateQueue—0x140004E38- Change Type: Not security-relevant (modernization).
- Notes:
ProbeForReadinlined; error-path free modernized toExFreePoolWithTag; share-access checks preserved. ACCreateQueue—0x140005F20- Change Type: Not security-relevant (modernization).
- Notes: Probes inlined; pool allocation modernized to
NonPagedPoolNx. Index-guard logic present in both builds. CUserHeaderconstructor —0x1400138D8- Change Type: Cosmetic.
- Notes: Subtraction-based pointer comparison rewritten as direct equality; semantically equivalent.
ACSendMessage—0x140009478- Change Type: Not security-relevant.
- Notes: Helper inlining and
ProbeForReadinlining. The local zero-fill ismemset(..., 0x118)in both builds (identical size; the original claim of a0x110→0x118extension and an added+0x78zero-init is not present). ACPutRemotePacket—0x140008950- Change Type: Not security-relevant.
- Notes: Helper inlining, inlined refcount decrement, and added WPP trace of the refcount value.
- Driver-wide churn (many functions): WIL feature-staging code (
Feature_*__private_IsEnabled*,wil_details_*), renamed WPP trace macros (e.g.WPP_SF_qdd→WPP_SF_qDd), alternatememsetintrinsic variants (__memset_repmovs,__memset_spec_ermsb, etc.), Control Flow Guard dispatch thunks (_guard_dispatch_icall), andProbeForRead/pool-allocation modernization. These are rebuild artifacts, not security fixes.
9. Unmatched Functions
There were no unmatched (added or removed) functions in this patch by content. The apparent name-only differences (functions present under one symbol in one build and inlined in the other, plus renamed WPP/WIL helper symbols) are consistent with a full toolchain rebuild rather than a structural code change.
10. Confidence & Caveats
- Confidence Level: High. Both suspected findings were disproven directly against the disassembly and decompilation of both builds: the "missing" cleanup calls and the "missing" NULL/sign checks are present in the unpatched build inside helper routines that the patched build inlines.
- Basis: Real symbols are present in both builds. Finding 1 rests on
CSMAllocator::ReleaseFreeHeaps @ 0x1400117E0(threePurgeHeapscalls) versus the inlined patched dispatcher case at0x1C000CDCC. Finding 2 rests onCTransaction::RestorePacket @ 0x140021A64(NULL check at0x140021A99, sign check at0x140021AA5) versus the inlined patchedACPutRestoredPacket @ 0x1C0009F50. - Scope: The remaining changed functions were surveyed by content across the relocation; no changed function introduces a new attacker-reachable validation, bounds check, or object-lifetime fix. No omitted security-relevant change was found.