ntalloc
A #[global_allocator] for Windows kernel drivers written in #![no_std] Rust.
ntalloc serves the alloc crate (Vec, Box, String and the rest) from the
kernel pool through ExAllocatePool2, on stable Rust. With the allocator-api
feature (Rust 1.100 or newer) it also implements the Allocator trait, so a
single collection can use a pool of its own.
Highlights
- Builds on stable Rust 1.85; the opt-in
AllocatorAPI needs 1.100, and only its marker traits need nightly - One zero-sized type, configured entirely at compile time: the pool flags, the pool tag, and whether freeing checks the tag
- Any alignment a
Layoutasks for, including#[repr(align(64))]types and alignments larger than a page POOL_FLAG_RAISE_ON_FAILUREand invalid pool tags are refused when the driver is built, not discovered when it runs- Testable in user mode with the
usermode-backendfeature, and checked under Miri
Requirements
- Windows 10, version 2004 or later.
ExAllocatePool2does not exist on earlier versions. - Rust 1.85 or later.
panic = "abort"in the driver's own profiles. A kernel driver cannot unwind, and profile settings in a dependency never apply to the crate that uses it.
[]
= "abort"
[]
= "abort"
Quick start
[]
= "0.1"
In the driver's lib.rs, next to #![no_std] and its panic handler:
extern crate alloc;
use Vec;
use NonPagedAlloc;
static GLOBAL: NonPagedAlloc = NonPagedAlloc ;
// Anywhere in the driver, the alloc types now draw on the non-paged pool.
let ids: = .collect;
assert_eq!;
Every allocation the driver makes through alloc now comes from the non-paged
pool under the tag Rust, and goes back to it when freed.
Choosing a pool
| Alias | Pool flag | Allocate at | Touch the memory at |
|---|---|---|---|
NonPagedAlloc |
POOL_FLAG_NON_PAGED |
IRQL ≤ DISPATCH_LEVEL |
any IRQL |
PagedAlloc |
POOL_FLAG_PAGED |
IRQL ≤ APC_LEVEL |
IRQL ≤ APC_LEVEL |
A global allocator serves every allocation in the driver, including those made
in a DPC or while a spin lock is held, so NonPagedAlloc is the safe choice for
#[global_allocator]. PagedAlloc suits code that never allocates or touches
the memory at DISPATCH_LEVEL, typically through the Allocator API below.
When the pool runs out
ExAllocatePool2 returns null when it cannot serve a request, and ntalloc
passes that on. Fallible APIs such as Vec::try_reserve report it as an error.
Infallible ones such as Vec::push and Box::new call handle_alloc_error,
which in a #![no_std] build panics and hands control to the driver's panic
handler. Where running out of pool must be survivable, reserve with the
fallible APIs first.
Tags and flags
Both aliases are shorthand for
NtAlloc<ALLOC_POOL_FLAGS, MEMORY_TAG, TAGGED_DEALLOC, REALIGN>, which takes
any combination of pool flags:
use ;
// Shows up as "MyDr" in !poolused and !poolfind.
type DriverAlloc = u32from_ne_bytes }>;
// Non-paged pool without the zeroing ExAllocatePool2 does by default.
type ScratchAlloc = POOL_FLAG_NON_PAGED | POOL_FLAG_UNINITIALIZED }>;
assert!;
assert!;
MEMORY_TAGdefaults toDEFAULT_MEMORY_TAG, which the debugger shows asRust. Build your own withu32::from_ne_bytes, as above, and it appears exactly as written. A tag is one to four printable ASCII characters, padded with zero bytes at the end when shorter, and needs at least one letter or digit.TAGGED_DEALLOC,trueby default, frees withExFreePoolWithTag, which checks the tag;falsefrees withExFreePool.REALIGN,trueby default, serves aLayoutaligned more strictly than the pool guarantees by over-allocating and realigning, as described under Alignment;falseturns that off, for drivers that never need more than the pool's own alignment.ExAllocatePool2zeroes every block unlessPOOL_FLAG_UNINITIALIZEDis set, andPOOL_ZEROESsays which applies.alloc_zeroedandallocate_zeroedreturn zeroed memory either way. Microsoft advises against skipping the zeroing for memory that may be copied to user mode or over the network.- The flags exported with the type
POOL_FLAGSarePOOL_FLAG_PAGED,POOL_FLAG_NON_PAGED,POOL_FLAG_UNINITIALIZED,POOL_FLAG_CACHE_ALIGNED,POOL_FLAG_USE_QUOTAandPOOL_FLAG_SPECIAL_POOL. Other flags fromwdm.hcan be passed as raw values, with the one exception below. - Some configurations fail the build:
POOL_FLAG_RAISE_ON_FAILURE, which would make a failed allocation raise an SEH exception Rust cannot handle; a zero tag, which makes every allocation fail; any other tag that breaks the rules above; and"BIG ", a tag the kernel reserves. The error, E0080, comes fromcargo build;cargo checkdoes not report it.
Alignment
ExAllocatePool2 aligns a block to MIN_ALIGNMENT, 16 bytes on 64-bit
Windows, or to the page size for a block of a page or more; kernel_alignment
gives the figure for a size. A Layout that asks for more is served by
allocating layout.align() extra bytes, rounding the pointer up, and storing
the pool's own pointer in the POINTER_SIZE bytes just before the one handed
out. GlobalAlloc and Allocator take care of both directions.
The same steps are available as functions. Memory from allocate_aligned must go
back through deallocate_aligned with the same Layout; allocate and
deallocate never realign, and pair only with each other:
use Layout;
use NonPagedAlloc;
// Call through an alias like this one: `NonPagedAlloc::allocate_aligned(..)`
// cannot infer the defaults of the alias's parameters (error E0284).
type Alloc = NonPagedAlloc;
let layout = from_size_align.unwrap;
let ptr = allocate_aligned;
assert!;
// SAFETY: `ptr` came from `allocate_aligned` with this layout, and is freed once.
unsafe ;
kernel_allocate is allocate without the test backend: it always reaches the
pool, with or without usermode-backend.
Realignment is on by default. A driver that never needs more than the pool's
own alignment can turn it off by setting REALIGN to false, as in
NonPagedAlloc<DEFAULT_MEMORY_TAG, true, false>: every request then goes to the
pool by size, and the alignment a Layout asks for is ignored. Only do so if
nothing allocated through it needs more, including types from other crates: a
misaligned Box or reference is undefined behaviour, which rules out
#[repr(align(32))] and larger types, 256- and 512-bit SIMD types, and
crossbeam's CachePadded.
The Allocator API (Rust 1.100)
With the allocator-api feature, NtAlloc implements core::alloc::Allocator,
so one collection can use a different pool or tag from the global allocator:
use PagedAlloc;
// A paged-pool buffer, for code that runs at PASSIVE_LEVEL.
let mut name: = Vecnew_in;
name.extend;
allocator-api-markers adds the StaticAllocator and AllocatorClone marker
traits, which Box::pin_in, cloning an Arc or Rc, and BTreeMap require.
They are still unstable, so it needs a nightly compiler from October 2026 or
later.
Testing in user mode
With the usermode-backend feature, code that allocates through NtAlloc runs
in ordinary tests, on the process heap instead of the kernel pool:
[]
= { = "0.1", = ["usermode-backend"] }
Where the pool would bugcheck with BAD_POOL_CALLER, the test process prints
what went wrong and aborts instead: on a double free, on a pointer the allocator
never handed out, and, with TAGGED_DEALLOC, on the wrong tag. Windows reports
any abort as STATUS_STACK_BUFFER_OVERRUN, so run the tests with
-- --test-threads=1 to see which test it was.
Enable the feature only under [dev-dependencies]. It brings in std, so as a
normal dependency it reaches the driver itself, which then fails to build with a
duplicate panic_impl lang item.
Feature flags
| Feature | Toolchain | Adds |
|---|---|---|
allocator-api |
Rust 1.100 | the Allocator impl |
allocator-api-markers |
nightly | StaticAllocator and AllocatorClone; implies allocator-api |
usermode-backend |
stable | the process-heap backend, for [dev-dependencies] only |
There are no default features.
Public API at a glance
| Item | What it is |
|---|---|
NtAlloc |
the allocator: pool flags, tag, free routine, realignment |
PagedAlloc, NonPagedAlloc |
NtAlloc on the paged and non-paged pool |
POOL_FLAGS, POOL_FLAG_* |
the pool flags ExAllocatePool2 takes |
DEFAULT_MEMORY_TAG |
the default tag, Rust |
MIN_ALIGNMENT, POINTER_SIZE |
the pool's smallest alignment; a stashed pointer's size |
POOL_ZEROES |
whether a configuration's blocks arrive zeroed |
kernel_alignment |
the alignment the pool guarantees for a size |
allocate, deallocate |
a block of a given size, at the pool's own alignment |
allocate_aligned, deallocate_aligned |
a block for a Layout, realigned when needed |
kernel_allocate |
allocate, always from the pool |
GlobalAlloc, Allocator |
the trait impls; Allocator with the allocator-api feature |
The full reference is on docs.rs.
Minimum supported Rust version
1.85, set by the 2024 edition. The allocator-api feature requires Rust
1.100, where the Allocator trait became stable, and allocator-api-markers a
nightly compiler; everything else builds on 1.85.
The MSRV is treated as a compatibility promise: raising it is a breaking change and will come with a minor version bump before 1.0.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.