December 2025 Patch Tuesday
Published:
CVE-2025-62221 - Windows Cloud Files Mini Filter Driver Elevation of Privilege Vulnerability
A Windows Cloud Files Mini Filter Driver (cldflt.sys) bug (CVE-2025-62221) is being actively exploited this month and has been patched in this month’s Patch Tuesday. In October, there was a TOCTOU bug (CVE-2025-55680) patched in the same driver. This time around, the bug is a Use-after-Free (UaF). I looked at the patch made to the driver and shared my findings in this blog.
Diff
The diff I generated using ghidriff is available at cldflt.10.0.19041.6456.sys-cldflt.10.0.19045.6691.sys.md
Analysis
Looking at the diff, I found two places where a Use-after-Free can occur.
CldSyncDisconnectRoot
The function returns 0 (success) even when the sync root lookup fails (puVar2 == NULL), the sync root is disconnected or the output parameter param_3 is not set.
undefined8 CldSyncDisconnectRoot(longlong param_1,undefined8 param_2,undefined8 *param_3)
{
longlong lVar1;
undefined8 *puVar2;
puVar2 = (undefined8 *)0x0;
FltAcquirePushLockExclusiveEx(param_1 + 8,0);
lVar1 = RtlLookupElementGenericTableAvl(param_1 + 0x10);
if (lVar1 != 0) {
puVar2 = *(undefined8 **)(lVar1 + 8);
}
FltReleasePushLockEx(param_1 + 8);
if (puVar2 == (undefined8 *)0x0) {
// only logs error, doesn't set error code
if ((((undefined **)WPP_GLOBAL_Control != &WPP_GLOBAL_Control) &&
((*(uint *)(WPP_GLOBAL_Control + 0x2c) & 0x100) != 0)) &&
(3 < (byte)WPP_GLOBAL_Control[0x29])) {
WPP_SF_id(*(undefined8 *)(WPP_GLOBAL_Control + 0x18),0x25,
&WPP_69db2c92eaf0323eb14c6b31d757a425_Traceguids,param_1);
}
}
else {
CldSyncDisconnectRootByObject(puVar2,0); // disconnect
if ((((undefined **)WPP_GLOBAL_Control != &WPP_GLOBAL_Control) &&
((*(uint *)(WPP_GLOBAL_Control + 0x2c) & 0x100) != 0)) &&
(3 < (byte)WPP_GLOBAL_Control[0x29])) {
WPP_SF_id(*(undefined8 *)(WPP_GLOBAL_Control + 0x18),0x26,
&WPP_69db2c92eaf0323eb14c6b31d757a425_Traceguids,param_1);
}
*param_3 = puVar2; // return the object
}
return 0; // always return 0 (success)
}
However, looking at CldiPortProcessServiceCommands function which calls CldSyncDisconnectRoot, this behavior results to a call to RtlDeleteElementGenericTableAvl and a dangling pointer a1 + 0x68.
uVar10 = CldSyncDisconnectRoot((longlong)local_98,uVar15,&local_b0);
piVar13 = (int *)(uVar10 & 0xffffffff);
HsmDbgBreakOnStatus((int)uVar10);
if (-1 < (int)uVar10) {
FltAcquirePushLockExclusiveEx(param_1 + 0x60,0);
RtlDeleteElementGenericTableAvl(param_1 + 0x68,&local_b8);
FltReleasePushLockEx(param_1 + 0x60,0);
piVar14 = (int *)(param_1 + 0x5c);
*piVar14 = *piVar14 + -1;
if (*piVar14 == 0) {
lVar8 = IoGetCurrentProcess();
HsmOsClearProcessAsSyncProvider(lVar8);
*(undefined8 *)(param_1 + 0x50) = 0;
}
goto LAB_1c005fa46;
}
HsmiGrantLockRequest
The function manages cross-references between two lists:
- Temporary list (
local_48): Created temporarily during function execution - Persistent list (at
param_1 + 0x40): Lives beyond the function’s lifetime
Around label LAB_8 in the old code, nodes from the temporary list are linked into the persistent list:
ppppuVar19[4] = ppppuVar4; // persistent list node -> temp list node
ppppuVar4[5] = ppppuVar19; // temp list node -> persistent list node
Then at cleanup (LAB_3), the temporary list nodes are freed:
HsmiFreePropertyLock((longlong)ppppuVar1); // FREE the temp node
The persistent list still has pointers (at offset [4]) pointing to the now-freed temporary list nodes. Any subsequent access through these pointers would trigger a use-after-free.
Patch
CldSyncDisconnectRoot
The patch makes sure the function returns proper error codes instead of always returning success.
undefined4 CldSyncDisconnectRoot(longlong param_1, undefined8 param_2,
longlong param_3, undefined8 *param_4)
{
longlong lVar1;
undefined4 uVar2;
longlong *plVar4;
uVar2 = 0; // Success by default
plVar4 = (longlong *)0x0;
FltAcquirePushLockExclusiveEx(param_1 + 8, 0);
lVar1 = RtlLookupElementGenericTableAvl(param_1 + 0x10);
if (lVar1 != 0) {
plVar4 = *(longlong **)(lVar1 + 8);
}
FltReleasePushLockEx(param_1 + 8);
if (plVar4 == (longlong *)0x0) {
uVar3 = 0xc000cf0b; // Set error code
uVar2 = 0xc000cf0b;
[...snipped...]
} else if (*plVar4 == param_3) { // Validate handle matches
CldSyncDisconnectRootByObject(plVar4, 0);
*param_4 = plVar4;
} else {
uVar2 = 0xc000cf0b; // Error if handle mismatch
[...snipped...]
}
return uVar2; // Return actual status
}
HsmiGrantLockRequest
The patch adds cleanup code before freeing temp nodes:
ppppppuVar13 = (undefined8 ******)ppppppuVar1[5]; // Get linked persistent node
if (ppppppuVar13 != (undefined8 ******)0x0) { // If cross-link exists
ppppppuVar13[4] = (undefined8 *****)0x0; // NULL out the back-pointer
}
ExFreePoolWithTag(ppppppuVar1,0x72507348); // NOW it's safe to free
Additionally, the patch adds code to free the dynamically allocated owner arrays stored in temp nodes:
if (*(int *)(local_48 + 4) != 0) { // If array was allocated
ExFreePoolWithTag(*pppppppuVar6,0x72507348); // Free it first
}
Conclusion
This bug is interesting especially because it is actively exploited and I want to learn mini filter driver exploitation. I am doing some more analysis on the topic & I initially plan and started to write an in-depth analysis in this blog post but then decided it deserves a blog post on its own. Hopefully, I’ll be able craft my own exploit in the coming weeks (months?), time willing (and skill willing).
References
- https://www.zerodayinitiative.com/blog/2025/12/9/the-december-2025-security-update-review
