December 2025 Patch Tuesday

4 minute read

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:

  1. Temporary list (local_48): Created temporarily during function execution
  2. 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