Loading learning content...
While user-mode applications allocate memory through heaps and VirtualAlloc, the Windows kernel and device drivers have their own specialized memory allocation infrastructure: memory pools. These pool allocators provide fast, efficient memory allocation for kernel-mode code while enforcing critical constraints about when memory can be paged.
Understanding memory pools is essential for kernel developers, driver writers, and anyone investigating system-wide memory consumption. Pool leaks from drivers are a common cause of system degradation and crashes—knowledge of pool mechanics helps diagnose these issues. Even for those not writing kernel code, pool analysis reveals how much memory the OS itself and its drivers consume.
By the end of this page, you will understand the difference between paged and non-paged pools, how the pool allocator works internally, pool tagging and how to use it for debugging, how to identify pool memory leaks and their causes, and modern pool improvements in Windows 10/11.
Windows provides two primary kernel memory pools, distinguished by their pageability:
Non-Paged Pool (NonPagedPool, NonPagedPoolNx):
Memory that is guaranteed to be resident in physical RAM at all times. This memory cannot be paged to disk under any circumstances. It's required for:
Non-paged pool is a precious, limited resource. Exhausting it causes system crashes.
Paged Pool (PagedPool):
Memory that can be paged to disk when physical RAM is needed. It follows the same rules as user-mode pageable memory. It's suitable for:
Paged pool is more plentiful than non-paged pool but still finite.
| Characteristic | Non-Paged Pool | Paged Pool |
|---|---|---|
| Pageability | Never paged, always in RAM | Can be paged to disk |
| Access IRQL | Any IRQL (including DPC/ISR) | PASSIVE_LEVEL or APC_LEVEL only |
| Size limit | Smaller (hundreds of MB to GB) | Larger (depends on commit limit) |
| Cost | More expensive (consumes RAM) | Less expensive (shares paging file) |
| Typical use | DMA buffers, ISR data | Object names, security descriptors |
| Exhaustion impact | System crash (BSOD) | Allocation failures, instability |
| NX variant | NonPagedPoolNx (no execute) | N/A (already NX by default) |
Modern drivers should use NonPagedPoolNx (non-executable non-paged pool) by default. The original NonPagedPool was executable, which security vulnerabilities could exploit. Windows 8+ introduced NonPagedPoolNx, and modern driver development guidelines require its use. Legacy drivers using executable non-paged pool are a security risk.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
#include <wdm.h> // Modern pool allocation with tagging#define MY_DRIVER_TAG 'MyDv' // 4-char tag (shown reversed in pool display) NTSTATUS AllocateDriverBuffer(void) { // Non-paged pool: Use for DPC-accessible data PVOID nonPagedBuffer = ExAllocatePoolWithTag( NonPagedPoolNx, // Modern non-executable pool 4096, // Size in bytes MY_DRIVER_TAG // Tag for debugging/tracking ); if (nonPagedBuffer == NULL) { KdPrint(("Failed to allocate non-paged pool\n")); return STATUS_INSUFFICIENT_RESOURCES; } // This buffer can be accessed at any IRQL // For example, in a DPC or ISR context // Paged pool: Use for normal driver data PVOID pagedBuffer = ExAllocatePoolWithTag( PagedPool, 65536, // 64 KB - larger allocation OK in paged MY_DRIVER_TAG ); if (pagedBuffer == NULL) { ExFreePoolWithTag(nonPagedBuffer, MY_DRIVER_TAG); return STATUS_INSUFFICIENT_RESOURCES; } // This buffer must ONLY be accessed at IRQL < DISPATCH_LEVEL // Accessing at DISPATCH_LEVEL = BSOD! // Modern Windows 10+ API: ExAllocatePool2 // Provides additional flags and better defaults PVOID modernBuffer = ExAllocatePool2( POOL_FLAG_NON_PAGED, // Or POOL_FLAG_PAGED 4096, MY_DRIVER_TAG ); // With zeroing flag (important for security): PVOID zeroedBuffer = ExAllocatePool2( POOL_FLAG_NON_PAGED | POOL_FLAG_ZERO, // Pre-zeroed 4096, MY_DRIVER_TAG ); // Cleanup ExFreePoolWithTag(nonPagedBuffer, MY_DRIVER_TAG); ExFreePoolWithTag(pagedBuffer, MY_DRIVER_TAG); if (modernBuffer) ExFreePool2(modernBuffer, MY_DRIVER_TAG, 4096, 0); if (zeroedBuffer) ExFreePool2(zeroedBuffer, MY_DRIVER_TAG, 4096, 0); return STATUS_SUCCESS;} // Lookaside lists for frequent fixed-size allocationsLOOKASIDE_LIST_EX myLookasideList; NTSTATUS InitializeLookaside(void) { // Lookaside lists cache freed allocations for reuse // Much faster than pool allocation for repeated same-size allocations NTSTATUS status = ExInitializeLookasideListEx( &myLookasideList, NULL, // Allocate function (NULL = default) NULL, // Free function (NULL = default) NonPagedPoolNx, // Pool type 0, // Flags 256, // Size of each entry MY_DRIVER_TAG, // Tag 0 // Depth (0 = system decides) ); return status;} // Usage:// PVOID entry = ExAllocateFromLookasideListEx(&myLookasideList);// ExFreeToLookasideListEx(&myLookasideList, entry);The Windows pool allocator is sophisticated, designed for performance in a multi-processor environment while providing debugging capabilities.
Pool Pages and Blocks:
Pools are organized as collections of pages. Each page can contain multiple allocation blocks:
Within a pool page, a header structure tracks free blocks and allocated regions:
Pool Header Structure:
Each pool allocation has a hidden header preceding the returned pointer:
[Pool Header][Allocation Data] ← Caller receives pointer here
The header contains:
Free List Management:
Free blocks are organized in lists by size class for fast allocation:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
Pool Page Layout (Conceptual):════════════════════════════════════════════════════════════════════════════ Each pool page (4 KB on x64) managed by the pool allocator: ┌───────────────────────────────────────────────────────────────────────┐│ Pool Page (4096 bytes) │├───────────────────────────────────────────────────────────────────────┤│ ┌─────────────┬───────────────────────────────────────────────────┐ ││ │Pool Hdr (16)│ Allocation #1 Data │ ││ └─────────────┴───────────────────────────────────────────────────┘ ││ ┌─────────────┬───────────────────────────────────────────────────┐ ││ │Pool Hdr (16)│ Allocation #2 Data │ ││ └─────────────┴───────────────────────────────────────────────────┘ ││ ┌─────────────┬────────────────────────────────────────┐ ││ │Pool Hdr (16)│ FREE BLOCK (on free list) │ ││ └─────────────┴────────────────────────────────────────┘ ││ ┌─────────────┬───────────────────┐ ││ │Pool Hdr (16)│ Allocation #3 │ ││ └─────────────┴───────────────────┘ ││ [Free Space or more allocations] │└───────────────────────────────────────────────────────────────────────┘ Pool Header (Simplified, x64):┌─────────────────────────────────────────────────────────────────────────┐│ Offset │ Field │ Size │ Description │├────────┼─────────────────┼──────┼──────────────────────────────────────┤│ 0x00 │ PreviousSize │ 2 │ Previous block size (for coalesce) ││ 0x02 │ PoolIndex │ 1 │ Which pool this belongs to ││ 0x03 │ BlockSize │ 1 │ This block's size in 16-byte units ││ 0x04 │ PoolType │ 1 │ NonPaged, Paged, etc. ││ 0x05 │ PoolTagHash │ 1 │ Tag hash for quick lookup ││ 0x06 │ AllocatorBackT. │ 2 │ Additional tracking ││ 0x08 │ PoolTag │ 4 │ 4-char tag (e.g., 'Proc') ││ 0x0C │ Padding/CRC │ 4 │ Alignment or CRC in checked builds │└─────────────────────────────────────────────────────────────────────────┘Total header: 16 bytes on x64 (8 bytes on x86) Large Allocations (> PAGE_SIZE - header):─────────────────────────────────────────────────────────────────────────Instead of carving from pool pages:1. Allocate contiguous pages directly2. Add a "big page" header at the start3. Track in separate big allocation list4. Returned memory may be page-aligned These have more overhead but avoid fragmenting pool pages.Like user-mode heaps, pools can suffer fragmentation. Many small allocations interspersed with frees leave gaps too small for new allocations. The pool allocator uses coalescing (merging adjacent free blocks) and per-size free lists to mitigate this. Lookaside lists for common sizes help avoid fragmentation entirely by reusing freed blocks directly.
Pool tags are 4-character identifiers attached to every pool allocation, providing crucial debugging and tracking capabilities. They enable identification of which driver or component owns any given allocation.
Tag Convention:
Tags are 4-byte values, often represented as 4 ASCII characters:
'Proc' – Process objects'Thre' – Thread objects'File' – File objects'Ntfx' – NTFS file system'MyDv')The tag is stored in the pool header and survives the allocation's lifetime, even in crash dumps.
Pool Tag Database:
Microsoft maintains a list of known pool tags in pooltag.txt (included with debugging tools). This file maps tags to their owning components:
Proc - nt!ps - Process objects
Thre - nt!ps - Thread objects
Ntfx - ntfs - NTFS general allocation
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
Using PoolMon (Windows SDK) for Live Pool Monitoring:════════════════════════════════════════════════════════════════════════════ PoolMon is a real-time pool usage monitor. Run from elevated command prompt: > poolmon.exe Memory:16773160K Avail:11237528K PageFlts: 1345 InRam Krnl: 9876K P:45678K Commit:5678912K Limit:34567890K Peak:6789012K Pool N:456000K P:890000K Tag Type Allocs Frees Diff Bytes Per Alloc CM31 Nonp 12345 ( 12) 1234 ( 1) 11111 45678912 ( 12345) 4109 Ntfs Paged 234567 ( 123) 123456 ( 12) 111111 123456789 (- 1234) 1111 MmSt Nonp 5678 ( 5) 2345 ( 2) 3333 23456789 ( 5678) 7036 FMfn Paged 67890 ( 67) 45678 ( 45) 22212 12345678 ( 2345) 555 ... Key: ( 12) = change since last sample Diff = outstanding allocations (Allocs - Frees) Bytes = total bytes for this tag ( 12345) = byte change since last sample Commands: p - Sort by paged pool usage n - Sort by non-paged pool usage d - Sort by Diff (outstanding allocations) b - Sort by bytes t - Sort by tag alphabetically e - Show tag explanation (from pooltag.txt) Identifying Leaks:─────────────────────────────────────────────────────────────────────────Watch for tags where:1. Allocs continuously increases2. Frees stays same or grows slower3. Diff grows unbounded4. Bytes grows unbounded Example leak pattern over 5-minute observation: Tag Allocs Frees Diff Bytes XyzD 1000 5 995 1024000 ← Minute 0 XyzD 2000 10 1990 2048000 ← Minute 1 XyzD 3000 15 2985 3072000 ← Minute 2 ... This tag is leaking! Track down the driver that uses 'XyzD'.To identify which driver owns a pool tag: (1) Check pooltag.txt in debugging tools, (2) Search Microsoft documentation, (3) Use strings on driver files to find matching tags, (4) Use WinDbg with symbols to trace allocation call stacks. Many third-party driver tags aren't documented—you may need to contact the vendor.
Pool memory is finite, and exhaustion can cause severe system problems. Understanding the limits helps prevent issues and diagnose them when they occur.
Non-Paged Pool Limits:
The non-paged pool has a hard limit determined by system configuration:
| System RAM | Typical NP Limit | Notes |
|---|---|---|
| 4 GB | ~256 MB | x86 systems |
| 8 GB | ~1 GB | Modern minimum |
| 16 GB | ~2 GB | Common workstation |
| 64+ GB | ~4 GB | Servers |
These are rough guidelines—actual limits depend on Windows version, configuration, and other factors. The limit doesn't scale linearly with RAM.
Paged Pool Limits:
Paged pool limits are more generous, tied to the commit limit:
Exhaustion Symptoms:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
# Pool health monitoring scriptfunction Monitor-PoolHealth { param( [int]$IntervalSeconds = 5, [int]$Samples = 0 # 0 = continuous ) # Thresholds (adjust based on your system's pool limits) $nonPagedWarning = 300MB $nonPagedCritical = 500MB $pagedWarning = 2GB $pagedCritical = 3GB $counters = @( '\Memory\Pool Nonpaged Bytes', '\Memory\Pool Paged Bytes', '\Memory\Free System Page Table Entries' ) $collectParams = @{ Counter = $counters SampleInterval = $IntervalSeconds } if ($Samples -gt 0) { $collectParams.MaxSamples = $Samples } else { $collectParams.Continuous = $true } Get-Counter @collectParams | ForEach-Object { $sample = $_ $time = Get-Date -Format 'HH:mm:ss' $nonPaged = ($sample.CounterSamples | Where-Object { $_.Path -like '*Nonpaged*' }).CookedValue $paged = ($sample.CounterSamples | Where-Object { $_.Path -like '*Paged Bytes' }).CookedValue $freePTEs = ($sample.CounterSamples | Where-Object { $_.Path -like '*Page Table*' }).CookedValue # Determine status $status = "OK" $color = "Green" if ($nonPaged -gt $nonPagedCritical) { $status = "CRITICAL - NonPaged" $color = "Red" } elseif ($nonPaged -gt $nonPagedWarning) { $status = "WARNING - NonPaged" $color = "Yellow" } elseif ($paged -gt $pagedCritical) { $status = "CRITICAL - Paged" $color = "Red" } elseif ($paged -gt $pagedWarning) { $status = "WARNING - Paged" $color = "Yellow" } Write-Host "$time | NP: $([math]::Round($nonPaged/1MB))MB | P: $([math]::Round($paged/1MB))MB | PTEs: $freePTEs | $status" -ForegroundColor $color # Alert if approaching limits if ($status -like "CRITICAL*") { Write-Host " ACTION NEEDED: Identify pool consumer with poolmon or WinDbg" -ForegroundColor Red # Could add email/event log alert here } }} # Run monitoringMonitor-PoolHealth -IntervalSeconds 10 # Quick one-time checkfunction Get-PoolSnapshot { $os = Get-CimInstance Win32_OperatingSystem $mem = Get-Counter '\Memory\Pool*Bytes' -ErrorAction SilentlyContinue Write-Host "Pool Memory Snapshot" -ForegroundColor Cyan Write-Host "═══════════════════════════════════════════" $mem.CounterSamples | ForEach-Object { $name = ($_.Path -split '\')[-1] $valueMB = [math]::Round($_.CookedValue / 1MB, 1) Write-Host "$name`: $valueMB MB" }} Get- PoolSnapshotPool leaks almost always originate in kernel-mode drivers. If you identify a leaking pool tag, the solution is to update or remove the responsible driver. Third-party drivers (security software, hardware drivers, virtualization) are common culprits. Use Driver Verifier to help identify the problematic driver.
Windows 10 and later introduced significant improvements to the pool allocator, enhancing security, performance, and debuggability.
Non-Executable Non-Paged Pool (NX):
Introduced in Windows 8, NonPagedPoolNx is now the default and recommended pool type. It marks memory as non-executable, preventing exploitation of pool overflows to execute arbitrary code. Modern drivers must use this pool type for certification.
Pool Allocation API v2 (ExAllocatePool2):
Windows 10 version 2004 introduced a new allocation API with better defaults:
PVOID ExAllocatePool2(
POOL_FLAGS Flags, // Combination of flags
SIZE_T NumberOfBytes,
ULONG Tag
);
Key improvements:
POOL_FLAG_ZERO for pre-zeroed memory (security best practice)POOL_FLAG_FAIL_ON_LOW_RESERVATIONSegment Heap for Pool:
Windows 10 version 2004+ adopted the Segment Heap architecture for kernel pools:
| Feature | Introduced | Benefit |
|---|---|---|
| NonPagedPoolNx | Windows 8 | DEP for kernel pool - security |
| Pool tagging mandatory | Windows 8 | Better tracking/debugging |
| ExAllocatePool2 API | Windows 10 2004 | Safer defaults, flag-based |
| Segment Heap for pool | Windows 10 2004 | Better performance/efficiency |
| Pool encryption | Windows 11 | Security against cold boot attacks |
| Pool CRC checks | Various | Corruption detection |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
// Modern pool allocation best practices (Windows 10 version 2004+)#include <wdm.h> #define MY_POOL_TAG 'MyDv' // PREFERRED: ExAllocatePool2 with explicit flagsPVOID AllocateSecureBuffer(SIZE_T Size) { // Recommended flags combination: // - POOL_FLAG_NON_PAGED for non-pageable memory // - POOL_FLAG_ZERO to pre-zero memory (prevents info disclosure) PVOID Buffer = ExAllocatePool2( POOL_FLAG_NON_PAGED | POOL_FLAG_ZERO, Size, MY_POOL_TAG ); return Buffer; // NULL on failure} // For paged pool:PVOID AllocatePagedBuffer(SIZE_T Size) { return ExAllocatePool2( POOL_FLAG_PAGED | POOL_FLAG_ZERO, Size, MY_POOL_TAG );} // With cache alignment for DMA:PVOID AllocateDmaBuffer(SIZE_T Size) { return ExAllocatePool2( POOL_FLAG_NON_PAGED | POOL_FLAG_ZERO | POOL_FLAG_CACHE_ALIGNED, Size, MY_POOL_TAG );} // Fail if system is low on memory (don't wait):PVOID AllocateNonBlocking(SIZE_T Size) { return ExAllocatePool2( POOL_FLAG_NON_PAGED | POOL_FLAG_FAIL_ON_LOW_RESERVATION, Size, MY_POOL_TAG );} // Modern freeing (with size, for efficiency):void FreeSecureBuffer(PVOID Buffer, SIZE_T Size) { if (Buffer) { // Zero before free for security RtlSecureZeroMemory(Buffer, Size); // Free with size hint (more efficient) ExFreePool2(Buffer, MY_POOL_TAG, Size, 0); }} // Compatibility wrapper for older WindowsPVOID CompatibleAlloc(POOL_TYPE PoolType, SIZE_T Size, ULONG Tag) {#if (NTDDI_VERSION >= NTDDI_WIN10_VB) // 2004+ POOL_FLAGS Flags = POOL_FLAG_ZERO; if (PoolType == NonPagedPoolNx || PoolType == NonPagedPool) { Flags |= POOL_FLAG_NON_PAGED; } else { Flags |= POOL_FLAG_PAGED; } return ExAllocatePool2(Flags, Size, Tag);#else // Legacy path for older Windows POOL_TYPE ActualType = (PoolType == NonPagedPool) ? NonPagedPoolNx : PoolType; PVOID Buffer = ExAllocatePoolWithTag(ActualType, Size, Tag); if (Buffer) { RtlZeroMemory(Buffer, Size); } return Buffer;#endif}Enable Driver Verifier's Special Pool option for your driver during development. It places allocations on page boundaries, immediately detecting buffer overflows and use-after-free bugs. Also enable pool tracking to catch leaks. These options add overhead but catch bugs that would otherwise cause crashes in production.
We've explored the Windows kernel memory pool infrastructure—the specialized allocation system that supports the kernel and all device drivers. Let's consolidate the key takeaways:
ExAllocatePool2 with NonPagedPoolNx and zeroing should be the default.What's Next:
With pool memory understood, we'll explore memory compression—the modern Windows feature that compresses infrequently-used pages in RAM rather than writing them to disk, dramatically improving performance for memory-constrained scenarios.
You now understand Windows kernel memory pools—the infrastructure supporting all kernel-mode memory allocation. This knowledge is essential for driver development, system debugging, and understanding overall Windows memory consumption.