Skip to content
krakenaut9Public

About

Windows kernel pool allocator (ExAllocatePool2) for no_std Rust drivers.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

43 Commits

Folders and files

Repository files navigation

ntalloc

CI crates.io docs.rs MSRV

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 Allocator API 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 Layout asks for, including #[repr(align(64))] types and alignments larger than a page
  • POOL_FLAG_RAISE_ON_FAILURE and invalid pool tags are refused when the driver is built, not discovered when it runs
  • Testable in user mode with the usermode-backend feature, and checked under Miri

Requirements

  • Windows 10, version 2004 or later. ExAllocatePool2 does 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.
[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

Quick start

[dependencies]
ntalloc = "0.1"

In the driver's lib.rs, next to #![no_std] and its panic handler:

extern crate alloc;

use alloc::vec::Vec;
use ntalloc::NonPagedAlloc;

#[global_allocator]
static GLOBAL: NonPagedAlloc = NonPagedAlloc {};

// Anywhere in the driver, the alloc types now draw on the non-paged pool.
let ids: Vec<u32> = (1..=4).collect();
assert_eq!(ids.len(), 4);

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 ntalloc::{NonPagedAlloc, NtAlloc, POOL_FLAG_NON_PAGED, POOL_FLAG_UNINITIALIZED};

// Shows up as "MyDr" in !poolused and !poolfind.
type DriverAlloc = NonPagedAlloc<{ u32::from_ne_bytes(*b"MyDr") }>;

// Non-paged pool without the zeroing ExAllocatePool2 does by default.
type ScratchAlloc = NtAlloc<{ POOL_FLAG_NON_PAGED | POOL_FLAG_UNINITIALIZED }>;

assert!(DriverAlloc::POOL_ZEROES);
assert!(!ScratchAlloc::POOL_ZEROES);
  • MEMORY_TAG defaults to DEFAULT_MEMORY_TAG, which the debugger shows as Rust. Build your own with u32::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, true by default, frees with ExFreePoolWithTag, which checks the tag; false frees with ExFreePool.
  • REALIGN, true by default, serves a Layout aligned more strictly than the pool guarantees by over-allocating and realigning, as described under Alignment; false turns that off, for drivers that never need more than the pool's own alignment.
  • ExAllocatePool2 zeroes every block unless POOL_FLAG_UNINITIALIZED is set, and POOL_ZEROES says which applies. alloc_zeroed and allocate_zeroed return 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_FLAGS are POOL_FLAG_PAGED, POOL_FLAG_NON_PAGED, POOL_FLAG_UNINITIALIZED, POOL_FLAG_CACHE_ALIGNED, POOL_FLAG_USE_QUOTA and POOL_FLAG_SPECIAL_POOL. Other flags from wdm.h can 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 from cargo build; cargo check does 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 core::alloc::Layout;
use ntalloc::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 = Layout::from_size_align(256, 64).unwrap();
let ptr = Alloc::allocate_aligned(layout);
assert!(!ptr.is_null());

// SAFETY: `ptr` came from `allocate_aligned` with this layout, and is freed once.
unsafe { Alloc::deallocate_aligned(ptr, layout) };

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 ntalloc::PagedAlloc;

// A paged-pool buffer, for code that runs at PASSIVE_LEVEL.
let mut name: Vec<u16, PagedAlloc> = Vec::new_in(PagedAlloc::default());
name.extend("rule".encode_utf16());

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:

[dev-dependencies]
ntalloc = { version = "0.1", features = ["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

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.

About

Windows kernel pool allocator (ExAllocatePool2) for no_std Rust drivers.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages