CVE-2025-55680 - Windows Cloud Files Mini Filter Driver Elevation of Privilege Vulnerability
Table of Contents
The vulnerability exists in HsmpOpCreatePlaceholders(), which uses MmMapLockedPagesSpecifyCache() to map userspace buffers into kernel space, creating shared physical memory. During placeholder file creation, the driver validates filenames for path traversal characters (\, :) by reading from this shared memory, then uses the same memory for FltCreateFileEx2() file creation. An attacker exploits the race window by rapidly flipping a single character (e.g., 'D' → \') in the shared buffer between validation (“JUSTASTRINGDfile.dll” passes) and file creation (“JUSTASTRING\file.dll” enables traversal). Combined with a pre-created junction (JUSTASTRING → C:\Windows\System32), this allows creating arbitrary files in System32 with SYSTEM privileges, leading to privilege escalation via DLL hijacking.
CVE-2025-55680: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-55680
Vulnerability Type: Time-of-check Time-of-use (TOCTOU) Race Condition
Driver Version: cldlft.sys - 10.0.26100.6725
Vulnerability analysis
The function HsmpOpCreatePlaceholders() was identified as the vulnerable routine addressed in the latest CVE. By reviewing the call graph, the shortest path to this function originates from HsmFltPreFILE_SYSTEM_CONTROL().

HsmFltPreFILE_SYSTEM_CONTROL():
HsmFltPreFILE_SYSTEM_CONTROL() is a pre-operation callback invoked by the Filter Manager before the actual I/O operation occurs (“Pre” indicates this; a corresponding “Post” callback also exists).
This function effectively acts as an IOCTL handler. If the IOCTL code equals 0x903BC, it calls HsmFltProcessHSMControl(), which is one step closer to the vulnerable function.
This IOCTL is triggered when user space calls CfCreatePlaceholders(), which creates one or more placeholder files or directories under a registered sync root. The API resides in cldapi.dll, which issues the IOCTL. The I/O manager (ntoskrnl.exe) processes the request, and then the Filter Manager (fltmgr.sys) invokes the HsmFltPreFILE_SYSTEM_CONTROL callback.

The CallbackData passed to the HsmFltProcessHSMControl() function is a _FLT_CALLBACK_DATA structure. Where Iopb is an important member, because it contains all the user-provided parameters.
typedef struct _FLT_CALLBACK_DATA {
FLT_CALLBACK_DATA_FLAGS Flags;
PETHREAD Thread;
PFLT_IO_PARAMETER_BLOCK Iopb;
IO_STATUS_BLOCK IoStatus;
struct _FLT_TAG_DATA_BUFFER *TagData;
union {
struct {
LIST_ENTRY QueueLinks;
PVOID QueueContext[2];
};
PVOID FilterContext[4];
};
KPROCESSOR_MODE RequestorMode;
} FLT_CALLBACK_DATA, *PFLT_CALLBACK_DATA;
The CfCreatePlaceholders() function accepts the following user input, where BaseDirectoryPath represents the registered sync root directory, PlaceholderArray is the pointer to an array of CF_PLACEHOLDER_CREATE_INFO.
HRESULT CfCreatePlaceholders(
[in] LPCWSTR BaseDirectoryPath,
[in, out] CF_PLACEHOLDER_CREATE_INFO *PlaceholderArray,
[in] DWORD PlaceholderCount,
[in] CF_CREATE_FLAGS CreateFlags,
[out] PDWORD EntriesProcessed
);
Each placeholder describes either a file or a directory:
typedef struct CF_PLACEHOLDER_CREATE_INFO {
LPCWSTR RelativeFileName;
CF_FS_METADATA FsMetadata;
LPCVOID FileIdentity;
DWORD FileIdentityLength;
CF_PLACEHOLDER_CREATE_FLAGS Flags;
HRESULT Result;
USN CreateUsn;
} CF_PLACEHOLDER_CREATE_INFO;
When creating placeholders, each file or subfolder under the sync root receives one CF_PLACEHOLDER_CREATE_INFO. It is also valid to create a placeholder even when the file does not yet exist because placeholders represent metadata.
BaseDirectoryPath (the sync root)
│
├─── Placeholder 1 (file or subfolder)
├─── Placeholder 2 (file or subfolder)
├─── Placeholder 3 (file or subfolder)
└─── ...
After the IOCTL is issued, the input buffer is converted into an undocumented structure referred to here as CREATE_PLACEHOLDER_STRUCT.
Offset Length Field Description
------- ------- ------------------------- ---------------------------------------------
0x00 4 Tag Set to 0x9000001A.
0x04 4 OpType Set to 0xC0000001.
...
0x0C 4 size Size of the 'CREATE_PLACEHOLDER_STRUCT'.
...
0x10 8 placeholder_payload Pointer to the 'CREATE_PLACEHOLDER_STRUCT' data structure
The CREATE_PLACEHOLDER_STRUCT (placeholder_payload) is similar to CF_PLACEHOLDER_CREATE_INFO structure with few changes.
Offset Length Field Description
------- ------- ------------------------- ---------------------------------------------
0x08 2 relativeName_offset The offset to the 'RelativeFileName' start.
0x0A 2 relativeName_len The 'RelativeFileName' size.
0x0C 2 fileidentity_offset The offset to the 'FileIdentity' start.
0x0e 2 fileidentity_len The 'FileIdentityLength' size.
...
0x2e 4 fileAttributes The file attributes to apply to the created file.
...
0x50 VAR relName The relative file name content.
VAR VAR fileid The file identity content.
Basically the following structure is sent as input buffer to 0x903BC IOCTL call.
typedef struct ioctl_0x903BC {
DWORD Tag; // 0x9000001A
DWORD OpType; // 0xC0000001
DWORD unknown1;
DWORD size;
DWORD unknown2;
PLACEHOLDER_CREATE_KERNEL* placeholder_payload; // ← USERSPACE POINTER!
// ...
} ioctl_0x903BC;
From the IOCTL code, we can determine the method being used, because the Iopb contains the user input and we need to know whether its Buffered or Direct or Neither.

HsmFltProcessHSMControl():
Before calling HsmFltProcessCreatePlaceholders() function, it checks if the input buffer length is not lesser than 0x98 bytes. Then it checks if OpType is 0xC0000001, this is set by cldapi.dll when it makes the IOCTL call.

HsmFltProcessCreatePlaceholders()
- The function checks the user input buffer size (1️⃣) and and it calls
HsmpRelativeStreamOpen()function (2️⃣) which verifies the directory provided inBaseDirectoryPath(member ofCfCreatePlaceholders()function) is a registered sync root and check permissions and returns a handle. - Finally, it calls (3️⃣) the vulnerable function
HsmpOpCreatePlaceholders()with the user buffer, also with the sync root directory handle.

HsmpOpCreatePlaceholders()
The CREATE_PLACEHOLDER_STRUCT structure is Payload here and first it calls IoAllocateMdl() to allocates a memory descriptor list (MDL) large enough to map a buffer, this is to map the user space buffer to kernel space.
Following that, ProbeForRead() is called which validates that the buffer (Payload) is in userspace and readable and calls MmProbeAndLockPages() which locks the physical pages in memory and prevents pages from being paged out to disk, the parameter 1 = UserMode (indicates this is a userspace buffer).
Finally, MmMapLockedPagesSpecifyCache() maps those locked physical pages that are described by the MDL (v9) into kernel virtual address space (MappedSystemVa_Payload). This returns a kernel virtual address (KVA) that the driver can use to directly access the same physical memory as the user buffer. No copying occurs. Because both the original user-mode pointer and the new kernel virtual address refer to the same underlying physical memory, any changes made through the user buffer will be visible through the kernel mapping—and any changes made through the kernel mapping will be visible to user space.
A while loop begins where it allocates some space in stack and copies the first placeholder values from the virtual kernel address (v17) to the stack (Payload_in_Stack) but this does not copy everything, the relative file name (relName) is not copied yet.
Another nested while loop, where it checks all the wide characters of the relative file name to see if any of the characters is equal to the\or the:character. This check is implemented to avoid any path traversal or junction in the file name (..\..\Windows\System32\file.dll or C:\Windows\file.dll), this is the TIME-OF-CHECK (TOC). It as a vulnerability fix implemented in CVE-2020-17136 and it’s writeup here.
Finally it calls, FltCreateFileEx2() function to create a new file or open an existing file, where ObjectAttributes (_OBJECT_ATTRIBUTES) structure contains ObjectName member which is pointer to aUnicode stringthat contains the name of the object for which a handle is to be opened.Here you see the relative file name is passed to it, but it takes it directly from the kernel virtual address (v68 → v54 → v17 → MappedSystemVa_Payload). This is the TIME-OF-USE (TOU).
Because the relative file name is taken from the KVA directly, if it’s changed in user space address, it will be changed here as well. So after the path traversal or junction check there is a time window available before it calls FltCreateFileEx2(). If we tamper the name after the check then with race condition it is possible to exploit this. Also, junction does not requires any elevation privilege.
__int64 __fastcall HsmpOpCreatePlaceholders(
PFLT_INSTANCE *a1,
__m128i *a2,
int a3,
void *Payload,
ULONG Length,
_DWORD *a6)
{
[::]
v9 = IoAllocateMdl(Payload, Length, 0, 0, 0);
Mdl = v9;
if ( !v9 )
{
[::]
ProbeForRead(Payload, Length, 4u);
v13 = Feature_2594491707__private_IsEnabledDeviceUsageNoInline() != 0;
MmProbeAndLockPages(v9, 1, v13);
if ( (v9->MdlFlags & 5) != 0 )
MappedSystemVa_Payload = (char *)v9->MappedSystemVa;
else
MappedSystemVa_Payload = (char *)MmMapLockedPagesSpecifyCache(v9, 0, MmCached, 0, 0, 0x40000010u);
v48 = MappedSystemVa_Payload;
if ( !MappedSystemVa_Payload )
{
[::]
while ( 1 )
{
memset(Payload_in_Stack, 0, sizeof(Payload_in_Stack));
v17 = (__m128i *)&MappedSystemVa_Payload[v56];
v54 = v17;
v68 = 0;
memset(&ObjectAttributes, 0, 44);
IoStatusBlock = 0;
[::]
Payload_in_Stack[0] = *v17;
Payload_in_Stack[1] = v17[1];
Payload_in_Stack[2] = v17[2];
Payload_in_Stack[3] = v17[3];
Payload_in_Stack[4] = v17[4];
[::]
while ( 1 )
{
v24 = *(__int16 *)((char *)&v54->m128i_i16[v23] + epi16);
if ( v24 == '\\' || v24 == ':' )
break;
if ( ++v23 >= (unsigned __int16)(WORD1(v19) >> 1) )
goto LABEL_52;
}
[::]
[<------- RACE WINDOW -------->]
[::]
*((_QWORD *)&v68 + 1) = (char *)v54 + Payload_in_Stack[0].m128i_u16[4];
LOWORD(v68) = Payload_in_Stack[0].m128i_i16[5];
WORD1(v68) = Payload_in_Stack[0].m128i_i16[5];
ObjectAttributes.Length = 48;
ObjectAttributes.RootDirectory = v57;
ObjectAttributes.Attributes = 576;
ObjectAttributes.ObjectName = (PUNICODE_STRING)&v68; // Relative Name of the File in Memory Mapped Region
*(_OWORD *)&ObjectAttributes.SecurityDescriptor = 0;
CreateOptions = (v34 != 0) | 0x208020;
LODWORD(v25) = FltCreateFileEx2(
Filter,
Instance,
&FileHandle,
&FileObject,
0x100180u,
&ObjectAttributes,
&IoStatusBlock,
&AllocationSize,
FileAttributes,
0,
2u,
CreateOptions,
0,
0,
0x800u,
&DriverContext);
[::]
} [ WHILE LOOP CONTINUES TO NEXT PLACEHOLDER ]
[::]
Triggering the Vulnerability
To exploit this vulnerability,
Step 1: Register a directory as sync root directory, which can be done by using the CfRegisterSyncRoot() API.
Step 2: Create a new directory inside this sync root directory, for e.g, JUSTASTRING.
Step 3: Create a junction for this JUSTASTRING to C:\Windows\System32\ directory (which is non-writable by normal user but junction can be created).
Step 4: Create 3 threads
- First thread will keep checking if
C:\Windows\System32\newfile.dllis created. - Second thread will keep modifying the filename string which is
"JUSTASTRINGDnewfile.dll"in the shared memory buffer, changing the character at position 11 from'D'to'\'and back repeatedly. This causes the filename to oscillate between"JUSTASTRINGDnewfile.dll"(safe) and"JUSTASTRING\newfile.dll"(exploits junction). - Third thread will keep making IOCTL call to 0x903BC, inorder to invoke the
HsmpOpCreatePlaceholders()vulnerable function.
The function will create JUSTASTRINGDnewfile.dll file but once the validation for the relName field is success, i.e. check that the relName does not contain any \ character, reaching the FltCreateFileEx2() function with relName set to JUSTASTRING\newfile.dll. Once this is set, the junction will create the file in C:\Windows\System32\newfile.dll.
Step 5: Once we have written a DLL in C:\Windows\System32\, then we can directly change the content of the DLL, so if a privileged service load this DLL we can escalate the privilege.
Patch Analysis
Analysing the patched HsmpOpCreatePlaceholders() function, instead of directly calling IoAllocateMdl() to allocate a memory descriptor list, it allocates a POOL_FLAG_PAGED pool called as Pool2 (also saved as Src) and then copies the data to that pool (Line 123). The old code path is still there for backward compatibility (Line 126).

Now all the user input values are retrieved directly from the newly allocated kernel memory (Src).

References: