What is Double-Free?
A double-free vulnerability occurs when a program frees the same memory block multiple times. This typically happens when ExFreePoolWithTag
or ExFreePool
is called twice on the same pointer, causing corruption in the Windows kernel memory allocator. If an attacker can predict or control the reallocation of this memory, they may be able to corrupt memory structures, overwrite critical pointers, or redirect execution flow to controlled memory regions. Double-free vulnerabilities often lead to heap corruption, kernel crashes (BSOD), or even arbitrary code execution, if exploited properly.
1. Classic Double-Free (Same Function Call Twice)
Concept:
The driver allocates memory using ExAllocatePoolWithTag
and frees it twice using ExFreePoolWithTag
. This causes corruption in the pool allocator, potentially leading to heap corruption or arbitrary execution. In this example, we implement a custom kernel driver that allocates a pool of memory, frees twice it, and then intentionally accesses it, triggering a BSOD.

The vulnerability occurs because g_DoubleFreeMemory
is freed twice using ExFreePoolWithTag
, leading to a double-free bug. After the first free, the pointer still holds the now-invalid memory address, allowing a second ExFreePoolWithTag
call on an already freed block. This can lead to memory corruption, potential use-after-free (UAF) scenarios, and arbitrary code execution if an attacker reallocates the freed memory.
Simple BSOD with Double-Free in a Custom Driver
The exploit will follow the same pattern as previously explained, as shown with the blue screen below.

2. Double free via Memory Descriptor List
IoFreeMdl
is used to release a Memory Descriptor List (MDL) in Windows kernel mode. Incorrect handling, such as double-freeing an MDL, can lead to system crashes or exploitation opportunities. This guide demonstrates creating a custom kernel driver that contains a double-free vulnerability and a user-mode PoC to trigger it.

In this code, an MDL (g_Mdl
) is allocated using IoAllocateMdl
, and its successful allocation is logged. The first call to IoFreeMdl
(g_Mdl
) correctly frees the MDL. A KeDelayExecutionThread
introduces a 1-second delay before attempting to free the already-freed MDL again, triggering a double-free vulnerability.
Simple BSOD with Double-Free in a Custom Driver
This user-mode PoC opens a handle to the vulnerable driver (DoubleFreeLink
) and sends an IOCTL request (IOCTL_TRIGGER_DOUBLE_FREE
) to trigger the double-free vulnerability in the kernel driver. If successful, the exploit could lead to a system crash or potential exploitation.

BSOD Triggered: The system crashes with a BUGCHECK_CODE: 0x4E (PFN_LIST_CORRUPT) due to the double-free of an MDL in the kernel driver.

Making Double-Free More Challenging
We’ve explored basic use-after-free (UAF) and double-free vulnerabilities, which might seem easy to understand. However, in real-world scenarios, these bugs are much harder to detect and exploit. Unlike simple examples, real UAF and double-free issues are rare and often require luck to find. Now, let’s step up the challenge—I’ll introduce a slightly more complex case that mirrors real-world scenarios but remains understandable.
0. Setup: Struct-Based Resource Handling
Before diving into allocation, let’s understand the structure.

This struct mimics a common pattern in driver development wrapping raw buffers inside helper structures. These wrappers often abstract buffer ownership and lifecycle management, but when misused, they also obscure bugs like double-free and UAF. That’s exactly what happens here.
1. Allocation Phase
This setup is clean and typical in real-world Windows drivers. But here’s the catch: no centralized memory tracking, no flags, and no safe-guard against double cleanup. A disaster waiting to happen if callbacks are reused.

In this step, we allocate memory twice:
- First, for the structure
pDummy
, which will manage the lifetime of an internal buffer. - Second, for the actual buffer inside the structure (
pDummy->Buffer
) a 0x100-sized non-paged memory block.
2. Double-Free via Wrapped FreeHandle Routine
The double-free vulnerability is triggered when the buffer pDummy->Buffer
is first manually freed.

This simulates a typical cleanup scenario like 𝙲𝙳𝚘𝚠𝚗𝚕𝚘𝚊𝚍𝙱𝚞𝚏𝚏𝚎𝚛::𝚁𝚎𝚕𝚎𝚊𝚜𝚎() but the buffer pointer is never nullified or flagged as freed. Later, the driver calls a helper routine wrapped around the cleanup phase:
Inside FreeHandle()
, the same buffer is freed again without validation.

Because FreeHandle()
blindly assumes ownership and responsibility for cleanup, it unknowingly triggers a second free on an already-freed memory block. This cleanup wrapping common in error handling paths, DriverUnload
, or exception-safe routines makes such bugs deceptively difficult to detect in large codebases. The result? A dangerous double-free that can corrupt memory or open the door to further exploitation.
Summary: Wrapping Around Danger – Double-Free in Disguise
This driver shows a classic double-free bug: memory is freed once directly, then again via a cleanup callback (FreeHandle
). The issue lies in freeing pDummy->Buffer
twice without resetting or checking ownership.
What makes it tricky is how the second free is wrapped in a callback just like real-world code, where cleanup is scattered across destructors or handlers, making such bugs harder to catch in large systems.
Double-Free (Mitigation):
Double-free vulnerabilities can be avoided by nullifying pointers after the first free, and checking their state before every deallocation. In complex code with shared pointers or cleanup callbacks, use flags or state checks to ensure memory is freed only once.

Bonus Tip: Spotting Double-Free in Windows Drivers
To identify double-free vulnerabilities, start by looking for deallocation functions. In user-mode, watch for free
, delete
, GlobalFree
, or Release
. In Windows kernel drivers, key functions include ExFreePoolWithTag
, IoFreeMdl
, ObDereferenceObject
, MmFreeContiguousMemory
, and RtlFreeHeap
. Many of these calls are wrapped inside internal cleanup functions or callbacks (like CDownloadBuffer::Release
or FreeHandle
), which can obscure the actual free. Always trace pointer lifecycle: if it’s freed and still accessed or freed again, that’s a bug. Check if the pointer is nullified or checked post-free—if not, it might be reused unsafely.