User-mode code that needs a scratch buffer calls into a heap; the kernel has the exact same need and answers it with the pool. The pool is the kernel’s general-purpose dynamic memory allocator — the region from which the executive and every loaded driver carve the structures that back handles, I/O request packets, registry key control blocks, network buffers, and countless other objects. If you have ever read a bugcheck that blamed a “pool” corruption, or watched a server slowly leak nonpaged memory until it fell over, you were looking at this subsystem. This article explains how the pool is organized, how allocations are tagged and tracked, how modern Windows has re-implemented the allocator, and why the pool’s integrity is a security boundary worth understanding from a defensive standpoint.
Paged vs Nonpaged Pool
The pool is not one region but several, split first by whether the memory is allowed to be paged out to disk. This distinction is not cosmetic — it is dictated by the interrupt request level (IRQL) at which the memory will be touched.
- Nonpaged pool is guaranteed to stay resident in physical memory at all times. Because it never faults, it is the only kind of memory that can be accessed at or above
DISPATCH_LEVEL(IRQL 2) — for example inside a DPC, an ISR-adjacent routine, or while holding a spinlock. It is a scarce, precious resource. - Paged pool can be trimmed and written to the paging file like ordinary user memory. It may only be touched at low IRQL (below
DISPATCH_LEVEL), where a page fault can be serviced. Most large, infrequently accessed structures live here.
Drivers historically requested pool through ExAllocatePoolWithTag, passing a POOL_TYPE such as NonPagedPool or PagedPool. Modern Windows deprecates that in favor of ExAllocatePool2, which takes a richer POOL_FLAG_* bitmask (for example POOL_FLAG_NON_PAGED, POOL_FLAG_PAGED, and POOL_FLAG_ZERO_ALLOCATION to return zeroed memory). A significant hardening milestone was the introduction of NonPagedPoolNx — nonpaged pool mapped non-executable. Older nonpaged pool was executable, which meant an attacker who wrote shellcode into a pool buffer could try to run it directly; making the default data pool non-executable removed that shortcut and is now the norm for driver allocations.
Pool Tags
Every pool allocation carries a 4-byte tag — four ASCII characters chosen by the caller, such as 'File' for the file-system runtime or 'Even' for event objects. The tag is stored with the allocation and serves two purposes: it makes memory accounting attributable to a specific component, and it turns an otherwise anonymous leak into a solvable puzzle. When nonpaged pool is being exhausted, you do not have to guess which driver is responsible — you look at which tag is consuming the most bytes and has an ever-growing allocation count.
The tooling built around tags is what makes them useful. poolmon.exe (from the WDK) shows a live, sortable table of every tag with its allocation count, free count, and total bytes. Microsoft ships a pooltag.txt database that maps well-known tags back to the driver and structure they belong to, so a mystery leak under tag 'Ntfx' can be traced to its owner in seconds.
Pool Structure
Under the classic allocator, pool memory is carved into chunks, each prefixed by a POOL_HEADER. That header records the block’s size (in units of the pool granularity, 16 bytes on x64), its pool type, and its 4-byte tag. Adjacent free chunks of the same region are coalesced to fight fragmentation, and the allocator keeps free lists of available blocks so a request can be satisfied without walking the whole region.
Allocation size drives the strategy. Small, frequently requested sizes are served from fast per-size caches (lookaside lists, below). Requests that are a page or larger bypass the small-block machinery entirely and are satisfied directly by the memory manager as whole pages — a large-pool allocation tracked separately from the chunked small-pool. This is why very large kernel buffers and small ones behave differently under a debugger and in the accounting tables.
The Segment Heap for Pool
The description above is the legacy pool. Modern Windows (from Windows 10 19H1 onward) re-implemented the kernel pool on top of the segment heap — the same allocator architecture that already backed user-mode heaps in ntdll. The kernel pool is now essentially a set of segment heaps, and the old monolithic POOL_HEADER layout no longer describes every allocation the way it once did. Metadata moved, size classes are organized into backends (a low-fragmentation-style bucket path, a variable-size path, and a large path), and the on-disk layout an analyst sees in memory differs from the pre-19H1 structures.
For a defender or reverse engineer, the practical takeaway is: the pool’s internal layout is version-dependent. Tooling and analysis notes written for the legacy pool do not map one-to-one onto the segment-heap pool, and any assumption about where a header or size field sits must be re-validated against the specific Windows build in front of you.
Lookaside Lists
Because the kernel allocates and frees certain fixed-size structures constantly, taking a global lock for each operation would be a serious bottleneck. Lookaside lists solve this: they are per-processor caches of fixed-size blocks. When a driver frees a block of a cached size, it goes onto the current processor’s lookaside list rather than back to the general pool; the next allocation of that size pops it straight off the list — no lock, no coalescing, no free-list walk. The system tunes the depth of these lists dynamically based on demand. Lookaside lists are why kernel allocation of common object sizes is so cheap, and they are another reason the “live” state of the pool is more complex than a single free list.
Inspecting the Pool
All of this is observable with the standard kernel-debugging and driver-testing tools:
# WinDbg: describe the pool chunk containing an address -
# its size, pool type, tag, and whether it is allocated or free
!pool 0xffffca80`12345000
# Summaries of pool usage by tag (bytes and allocation counts);
# /t sorts, and you can filter to a single tag
!poolused 2
!poolused 4 Even
# Find every allocation with a given tag
!poolfind Even
# poolmon.exe (WDK) - live, sortable per-tag table:
# b = sort by bytes, look for a tag whose count only ever grows
poolmon.exe
# Driver Verifier: enable Special Pool + pool tracking on a driver
verifier /flags 0x1 /driver suspect.sys
!pool is the first stop when a bugcheck points at a pool address — it tells you what allocation you are standing in and whether the neighboring headers look sane. !poolused and poolmon are the leak-hunter’s tools: watch the tag whose byte count climbs and never falls. The most powerful diagnostic, though, is Driver Verifier’s Special Pool: when enabled for a suspect driver, each allocation is placed on its own page with an adjacent guard page, so a one-byte overflow past the end of a buffer faults immediately and bugchecks with the offending driver on the stack, instead of silently corrupting a neighbor and crashing later somewhere unrelated.
Security Relevance
The pool matters for security because a memory-safety bug in any kernel-mode driver plays out in the pool, and the pool is shared by the entire kernel. A driver that writes past the end of a pool buffer is not corrupting its own private heap — it is corrupting whatever allocation happens to sit next to it, which may belong to the executive or another driver. This is why pool bugs are a classic route from a driver flaw to full kernel compromise, and why the topic is worth understanding defensively.
The important part, from a blue-team and hardening perspective, is how Windows has steadily raised the cost of turning pool corruption into control:
- NonPagedPoolNx makes data pool non-executable, so corrupted or attacker-populated pool buffers cannot simply be executed as code.
- Pool header integrity checks and cookies let the allocator detect that a header was overwritten and bugcheck rather than trust corrupted metadata.
- Special Pool and Driver Verifier catch overflows and use-after-free at the moment they happen during testing, so buggy drivers are found before they ship.
- Kernel Control Flow Guard (kCFG) and related control-flow integrity mitigations constrain where a corrupted function pointer can redirect execution, blunting the payoff of overwriting kernel structures.
Attackers respond to these with “pool grooming” — arranging allocations so that a target structure lands predictably next to a buffer they control — but the aim here is not to teach that. The defensive lessons are concrete: enable Driver Verifier (with Special Pool) when qualifying third-party drivers, keep drivers patched because pool bugs are how they escalate, monitor for the abnormal nonpaged-pool growth that precedes both leaks and exploitation attempts, and treat the ability to load an unsigned or vulnerable driver as the serious pool-integrity risk that it is. Everything here is for defensive understanding and authorized testing only.
Conclusion
The kernel pool is the beating heart of dynamic memory in ring 0: split into paged and nonpaged regions, labeled with tags for accountability, served through free lists and per-processor lookaside caches, and — on modern Windows — re-built on the segment heap. Its integrity is a genuine security boundary, defended by non-executable pool, header checks, Special Pool, and control-flow integrity. Understanding how allocations are structured and tracked is what lets you read a pool bugcheck, hunt a leak by tag, and reason precisely about kernel memory safety. In the next article we build directly on this foundation and survey the modern kernel protections — PatchGuard, HVCI, and CFG — that guard these structures.
You Might Also Like
This article is part of the Windows Internals series. These related deep-dives cover adjacent parts of the system:



Comments