use std::num::NonZeroUsize;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use std::time::Instant;
use thiserror::Error;
use windows::Win32::System::Diagnostics::Debug::Extensions::{
DEBUG_STATUS_BREAK, DEBUG_STATUS_NO_DEBUGGEE,
};
use windows::Win32::System::SystemInformation::{
IMAGE_FILE_MACHINE_AMD64, IMAGE_FILE_MACHINE_ARM64,
};
use super::decode::parse_tag;
use super::index::{PoolIndex, SnapshotCache};
use super::layout::{LayoutCache, LayoutTarget, SessionKey};
use super::snapshot::{PoolMatchLimit, SnapshotError, SnapshotWalker, WalkStalls};
use super::{PoolBackend, PoolDiagnostics, PoolSpan, PoolState};
use crate::dbgeng::{DbgEngError, DebugEngine};
static SESSION_GENERATION: AtomicU64 = AtomicU64::new(1);
pub(crate) fn generation() -> u64 {
SESSION_GENERATION.load(Ordering::Acquire)
}
pub(crate) fn bump_generation() {
SESSION_GENERATION.fetch_add(1, Ordering::AcqRel);
}
pub(crate) fn snapshots() -> &'static SnapshotCache {
static CACHE: std::sync::OnceLock<SnapshotCache> = std::sync::OnceLock::new();
CACHE.get_or_init(SnapshotCache::default)
}
pub(crate) fn invalidate_session() {
bump_generation();
snapshots().invalidate();
LayoutCache::global().invalidate();
}
pub fn invalidate_allocator_caches() {
invalidate_session();
crate::heap::invalidate_caches();
}
pub fn invalidate_allocator_snapshots() {
snapshots().invalidate();
crate::heap::invalidate_snapshot();
}
#[derive(Debug, Error)]
pub enum PoolQueryError {
#[error("pool queries require a kernel target")]
NotKernelTarget,
#[error("no accessible target is attached")]
NoDebuggee,
#[error("target is running; break in before taking a pool snapshot")]
TargetRunning,
#[error("pool walking supports x64 and ARM64 targets only (machine {machine:#x})")]
UnsupportedArchitecture { machine: u32 },
#[error(
"tag must be 1..4 ASCII bytes (\"Tgsm\") or the raw form 0x + 8 hex digits (\"0x5467736d\")"
)]
InvalidTag,
#[error("resolving pool layout failed ({0}); run `.reload /f nt` and retry")]
Layout(String),
#[error("the pool walk was interrupted on request")]
Interrupted,
#[error("walking the pool failed: {0}")]
Walk(String),
#[error(transparent)]
Engine(#[from] DbgEngError),
}
pub const DEFAULT_WALK_BUDGET: Duration = Duration::from_secs(120);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PoolWalk {
pub refresh: bool,
pub budget: Option<Duration>,
}
impl PoolWalk {
pub fn cached() -> Self {
Self {
refresh: false,
budget: Some(DEFAULT_WALK_BUDGET),
}
}
pub fn refreshed() -> Self {
Self {
refresh: true,
..Self::cached()
}
}
pub fn within(self, budget: Duration) -> Self {
Self {
budget: Some(budget),
..self
}
}
pub fn unbounded(self) -> Self {
Self {
budget: None,
..self
}
}
}
impl From<bool> for PoolWalk {
fn from(refresh: bool) -> Self {
Self {
refresh,
..Self::cached()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PoolPageFilter {
Paged,
NonPaged,
}
#[derive(Debug, Clone)]
pub struct PoolNeighbourhood {
pub chunk: PoolSpan,
pub previous: Option<PoolSpan>,
pub next: Option<PoolSpan>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PoolTagSummary {
pub display_tag: String,
pub raw_tag: u32,
pub allocations: usize,
pub total_bytes: u64,
pub paged_allocations: usize,
pub nonpaged_allocations: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PoolSnapshotReport {
pub layout: crate::allocator::LayoutProvenance,
pub total_chunks: usize,
pub allocated_chunks: usize,
pub coverage: WalkCoverage,
pub stopped_after_matches: Option<usize>,
pub diagnostics: PoolDiagnostics,
pub stalls: WalkStalls,
pub refused_chunks: u64,
pub unplaced_bytes: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WalkCoverage {
Complete,
BudgetExpired,
Partial,
}
impl WalkCoverage {
pub fn complete(self) -> bool {
self == Self::Complete
}
}
#[derive(Debug, Clone)]
pub struct PoolAnswer<T> {
pub found: T,
pub walk: PoolSnapshotReport,
}
pub fn snapshot_report(
engine: &DebugEngine,
walk: impl Into<PoolWalk>,
) -> Result<PoolSnapshotReport, PoolQueryError> {
let index = prepare_index(engine, walk.into())?;
Ok(report_of(&index))
}
pub(crate) fn report_of(index: &PoolIndex) -> PoolSnapshotReport {
PoolSnapshotReport {
layout: index.layout.clone(),
total_chunks: index.spans.len(),
allocated_chunks: index
.spans
.iter()
.filter(|span| span.state == PoolState::Allocated)
.count(),
coverage: match (index.complete, index.budget_expired) {
(true, _) => WalkCoverage::Complete,
(false, true) => WalkCoverage::BudgetExpired,
(false, false) => WalkCoverage::Partial,
},
stopped_after_matches: index.stopped_after_matches,
diagnostics: index.diagnostics.clone(),
stalls: index.stalls,
refused_chunks: index.refused_chunks,
unplaced_bytes: index.unplaced_bytes,
}
}
pub(crate) fn prepare_index(
engine: &DebugEngine,
walk: PoolWalk,
) -> Result<PoolIndex, PoolQueryError> {
prepare_index_until(engine, walk, None)
}
fn prepare_index_until(
engine: &DebugEngine,
walk: PoolWalk,
match_limit: Option<PoolMatchLimit>,
) -> Result<PoolIndex, PoolQueryError> {
let started = Instant::now();
if !engine.is_kernel_target()? {
return Err(PoolQueryError::NotKernelTarget);
}
let status = engine.execution_status()?;
if status == DEBUG_STATUS_NO_DEBUGGEE {
return Err(PoolQueryError::NoDebuggee);
}
if status != DEBUG_STATUS_BREAK {
return Err(PoolQueryError::TargetRunning);
}
let machine = engine.processor_type()?;
if ![IMAGE_FILE_MACHINE_AMD64, IMAGE_FILE_MACHINE_ARM64]
.iter()
.any(|supported| u32::from(supported.0) == machine)
{
return Err(PoolQueryError::UnsupportedArchitecture { machine });
}
let key = SessionKey {
image: engine.kernel_image()?,
session: generation(),
target: engine.target_identity(),
};
let layout = LayoutCache::global()
.get_or_resolve(engine, key, LayoutTarget::Kernel)
.map_err(|error| PoolQueryError::Layout(error.to_string()))?;
let module = engine.module_identity("nt")?;
if !module.symbols.has_type_info() || module.symbol_file.is_empty() {
return Err(PoolQueryError::Layout(
"DbgEng did not load private PDB type information for nt".into(),
));
}
let provenance = layout
.provenance(module)
.map_err(|error| PoolQueryError::Layout(error.to_string()))?;
let remaining = walk
.budget
.map(|budget| budget.saturating_sub(started.elapsed()));
snapshots()
.get_or_refresh(key, walk.refresh, || {
let walker = SnapshotWalker {
memory: engine,
layout: &layout,
traversal_limit: 1_000_000,
};
let mut snapshot = match match_limit {
Some(limit) => walker.walk_until(remaining, Some(limit))?,
None => walker.walk(remaining)?,
};
snapshot.layout = provenance;
Ok(snapshot)
})
.map_err(|error| match error {
SnapshotError::Interrupted => PoolQueryError::Interrupted,
other => PoolQueryError::Walk(other.to_string()),
})
}
pub fn find_tag(
engine: &DebugEngine,
tag: &str,
filter: Option<PoolPageFilter>,
stop_after_matches: Option<NonZeroUsize>,
walk: impl Into<PoolWalk>,
) -> Result<PoolAnswer<Vec<PoolSpan>>, PoolQueryError> {
let raw_tag = parse_tag(tag).ok_or(PoolQueryError::InvalidTag)?;
let match_limit = stop_after_matches.map(|matches| {
PoolMatchLimit::new(
raw_tag,
filter.map(|filter| filter == PoolPageFilter::Paged),
matches,
)
});
let index = prepare_index_until(engine, walk.into(), match_limit)?;
Ok(PoolAnswer {
found: collect_tag(&index, raw_tag, filter),
walk: report_of(&index),
})
}
fn collect_tag(index: &PoolIndex, raw_tag: u32, filter: Option<PoolPageFilter>) -> Vec<PoolSpan> {
index
.postings
.get(&raw_tag)
.into_iter()
.flatten()
.filter_map(|&position| index.spans.get(position))
.filter(|span| match filter {
None => true,
Some(PoolPageFilter::Paged) => span.pool_kind.is_paged(),
Some(PoolPageFilter::NonPaged) => !span.pool_kind.is_paged(),
})
.cloned()
.collect()
}
pub fn chunk_at(
engine: &DebugEngine,
address: u64,
walk: impl Into<PoolWalk>,
) -> Result<PoolAnswer<Option<PoolNeighbourhood>>, PoolQueryError> {
let index = prepare_index(engine, walk.into())?;
Ok(PoolAnswer {
found: neighbourhood_at(&index, address),
walk: report_of(&index),
})
}
fn neighbourhood_at(index: &PoolIndex, address: u64) -> Option<PoolNeighbourhood> {
let position = index
.spans
.iter()
.position(|span| span.contains_address(address))?;
let chunk = index.spans[position].clone();
let touching = |left: &PoolSpan, right: &PoolSpan| {
left.backend == PoolBackend::Vs || left.end() == right.header_address
};
Some(PoolNeighbourhood {
previous: index
.predecessor(position)
.and_then(|previous| index.spans.get(previous))
.filter(|previous| touching(previous, &chunk))
.cloned(),
next: index
.successor(position)
.and_then(|next| index.spans.get(next))
.filter(|next| touching(&chunk, next))
.cloned(),
chunk,
})
}
pub fn tag_census(
engine: &DebugEngine,
walk: impl Into<PoolWalk>,
) -> Result<PoolAnswer<Vec<PoolTagSummary>>, PoolQueryError> {
let index = prepare_index(engine, walk.into())?;
Ok(PoolAnswer {
found: summarize_tags(&index),
walk: report_of(&index),
})
}
fn summarize_tags(index: &PoolIndex) -> Vec<PoolTagSummary> {
let mut summaries: std::collections::HashMap<u32, PoolTagSummary> =
std::collections::HashMap::new();
for span in index
.spans
.iter()
.filter(|span| span.state == PoolState::Allocated)
{
let entry = summaries
.entry(span.raw_tag)
.or_insert_with(|| PoolTagSummary {
display_tag: span.display_tag.clone(),
raw_tag: span.raw_tag,
allocations: 0,
total_bytes: 0,
paged_allocations: 0,
nonpaged_allocations: 0,
});
entry.allocations += 1;
entry.total_bytes = entry.total_bytes.saturating_add(span.size);
if span.pool_kind.is_paged() {
entry.paged_allocations += 1;
} else {
entry.nonpaged_allocations += 1;
}
}
let mut census: Vec<_> = summaries.into_values().collect();
census.sort_by(|left, right| {
right
.total_bytes
.cmp(&left.total_bytes)
.then_with(|| left.display_tag.cmp(&right.display_tag))
});
census
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pool::{HeapIdentity, PoolKind};
fn heap(id: u64) -> HeapIdentity {
HeapIdentity {
pool_state: id,
heap: id,
special: false,
}
}
fn index_of(spans: Vec<PoolSpan>) -> PoolIndex {
PoolIndex::build(crate::pool::PoolSnapshot {
spans,
complete: true,
..crate::pool::PoolSnapshot::default()
})
}
fn allocation(
address: u64,
size: u64,
tag: &[u8; 4],
kind: PoolKind,
heap_id: u64,
) -> PoolSpan {
PoolSpan::allocation(
address,
size,
u32::from_le_bytes(*tag),
kind,
heap(heap_id),
PoolBackend::Lfh,
)
}
#[test]
fn test_unsupported_architecture_names_the_machine() {
assert_eq!(
PoolQueryError::UnsupportedArchitecture { machine: 0x014c }.to_string(),
"pool walking supports x64 and ARM64 targets only (machine 0x14c)"
);
}
#[test]
fn test_the_gate_admits_x64_and_arm64() {
assert_eq!(u32::from(IMAGE_FILE_MACHINE_AMD64.0), 0x8664);
assert_eq!(u32::from(IMAGE_FILE_MACHINE_ARM64.0), 0xaa64);
}
#[test]
fn test_a_bare_refresh_flag_still_carries_a_budget() {
assert_eq!(
PoolWalk::from(false),
PoolWalk {
refresh: false,
budget: Some(DEFAULT_WALK_BUDGET)
}
);
assert_eq!(PoolWalk::from(true), PoolWalk::refreshed());
assert_eq!(
PoolWalk::cached().within(Duration::from_secs(5)).budget,
Some(Duration::from_secs(5))
);
assert_eq!(
PoolWalk::refreshed().unbounded(),
PoolWalk {
refresh: true,
budget: None
}
);
}
#[test]
fn test_find_tag_returns_only_matching_allocations() {
let index = index_of(vec![
allocation(0x1000, 0x68, b"Tgsm", PoolKind::NonPagedNx, 1),
allocation(0x2000, 0x40, b"Tfub", PoolKind::Paged, 1),
allocation(0x3000, 0x68, b"Tgsm", PoolKind::NonPagedNx, 1),
]);
let found = collect_tag(&index, u32::from_le_bytes(*b"Tgsm"), None);
assert_eq!(found.len(), 2);
assert!(found.iter().all(|span| span.display_tag == "Tgsm"));
}
#[test]
fn test_a_binary_tag_is_found_by_its_bytes_not_by_its_rendering() {
let binary = [0x00u8, 0x01, 0x80, 0xff];
let index = index_of(vec![
allocation(0x1000, 0x68, &binary, PoolKind::NonPagedNx, 1),
allocation(0x2000, 0x40, b"....", PoolKind::NonPagedNx, 1),
]);
assert_eq!(
crate::pool::decode::display_tag(u32::from_le_bytes(binary)),
crate::pool::decode::display_tag(u32::from_le_bytes(*b"....")),
"the premise: these two are indistinguishable once rendered"
);
let rendered = parse_tag("....").expect("four ASCII bytes parse");
let by_rendering = collect_tag(&index, rendered, None);
assert_eq!(by_rendering.len(), 1);
assert_eq!(by_rendering[0].usable_address, 0x2000);
let raw = parse_tag(&crate::pool::raw_tag_hex(u32::from_le_bytes(binary)))
.expect("the raw form parses");
let by_bytes = collect_tag(&index, raw, None);
assert_eq!(by_bytes.len(), 1);
assert_eq!(by_bytes[0].usable_address, 0x1000);
}
#[test]
fn test_page_filter_splits_paged_from_nonpaged() {
let index = index_of(vec![
allocation(0x1000, 0x68, b"Tsst", PoolKind::NonPagedNx, 1),
allocation(0x2000, 0x40, b"Tsst", PoolKind::Paged, 1),
]);
let raw = u32::from_le_bytes(*b"Tsst");
assert_eq!(
collect_tag(&index, raw, Some(PoolPageFilter::NonPaged)).len(),
1
);
assert_eq!(
collect_tag(&index, raw, Some(PoolPageFilter::Paged)).len(),
1
);
assert_eq!(collect_tag(&index, raw, None).len(), 2);
}
#[test]
fn test_unknown_tag_yields_no_matches() {
let index = index_of(vec![allocation(
0x1000,
0x68,
b"Tgsm",
PoolKind::NonPagedNx,
1,
)]);
assert!(collect_tag(&index, u32::from_le_bytes(*b"zzzz"), None).is_empty());
}
#[test]
fn test_chunk_lookup_reports_neighbours_within_one_heap() {
let index = index_of(vec![
allocation(0x1000, 0x100, b"Aaaa", PoolKind::NonPagedNx, 1),
allocation(0x1100, 0x100, b"Bbbb", PoolKind::NonPagedNx, 1),
allocation(0x1200, 0x100, b"Cccc", PoolKind::NonPagedNx, 1),
]);
let found = neighbourhood_at(&index, 0x1180).expect("address is covered");
assert_eq!(found.chunk.display_tag, "Bbbb");
assert_eq!(found.previous.unwrap().display_tag, "Aaaa");
assert_eq!(found.next.unwrap().display_tag, "Cccc");
}
fn vs_allocation(chunk: u64, chunk_size: u64, tag: &[u8; 4], heap_id: u64) -> PoolSpan {
const VS_HEADER: u64 = 0x10;
const POOL_HEADER: u64 = 0x10;
let raw_tag = u32::from_le_bytes(*tag);
PoolSpan {
header_address: chunk + VS_HEADER,
usable_address: chunk + VS_HEADER + POOL_HEADER,
size: chunk_size - VS_HEADER - POOL_HEADER,
requested_size: None,
raw_tag,
display_tag: crate::pool::decode::display_tag(raw_tag),
pool_kind: PoolKind::NonPagedNx,
numa_node: 0,
heap: heap(heap_id),
subsegment: Some(0x9000),
backend: PoolBackend::Vs,
state: PoolState::Allocated,
size_class: chunk_size as u32,
}
}
#[test]
fn test_vs_neighbours_are_reported_despite_their_chunk_header() {
let index = index_of(vec![
vs_allocation(0x1000, 0x100, b"Aaaa", 1),
vs_allocation(0x1100, 0x100, b"Bbbb", 1),
vs_allocation(0x1200, 0x100, b"Cccc", 1),
]);
let found = neighbourhood_at(&index, 0x1150).expect("address is covered");
assert_eq!(found.chunk.display_tag, "Bbbb");
assert_eq!(found.previous.unwrap().display_tag, "Aaaa");
assert_eq!(found.next.unwrap().display_tag, "Cccc");
}
fn lfh_allocation(slot: u64, unit: u64, tag: &[u8; 4], heap_id: u64) -> PoolSpan {
const POOL_HEADER: u64 = 0x10;
let raw_tag = u32::from_le_bytes(*tag);
PoolSpan {
header_address: slot,
usable_address: slot + POOL_HEADER,
size: unit - POOL_HEADER,
requested_size: None,
raw_tag,
display_tag: crate::pool::decode::display_tag(raw_tag),
pool_kind: PoolKind::NonPagedNx,
numa_node: 0,
heap: heap(heap_id),
subsegment: Some(0x8000),
backend: PoolBackend::Lfh,
state: PoolState::Allocated,
size_class: unit as u32,
}
}
#[test]
fn test_neighbours_must_actually_touch() {
let index = index_of(vec![
lfh_allocation(0x1000, 0x100, b"Aaaa", 1),
lfh_allocation(0x1200, 0x100, b"Bbbb", 1),
]);
let found = neighbourhood_at(&index, 0x1250).expect("address is covered");
assert_eq!(found.chunk.display_tag, "Bbbb");
assert!(
found.previous.is_none(),
"slack between spans must not count as bordering"
);
let index = index_of(vec![
lfh_allocation(0x1000, 0x100, b"Aaaa", 1),
lfh_allocation(0x1100, 0x100, b"Bbbb", 1),
]);
let found = neighbourhood_at(&index, 0x1150).expect("address is covered");
assert_eq!(found.previous.unwrap().display_tag, "Aaaa");
}
#[test]
fn test_neighbours_do_not_cross_a_heap_boundary() {
let index = index_of(vec![
allocation(0x1000, 0x100, b"Aaaa", PoolKind::NonPagedNx, 1),
allocation(0x1100, 0x100, b"Bbbb", PoolKind::NonPagedNx, 2),
]);
let found = neighbourhood_at(&index, 0x1150).expect("address is covered");
assert_eq!(found.chunk.display_tag, "Bbbb");
assert!(found.previous.is_none());
}
#[test]
fn test_address_outside_every_span_is_not_found() {
let index = index_of(vec![allocation(
0x1000,
0x100,
b"Aaaa",
PoolKind::NonPagedNx,
1,
)]);
assert!(neighbourhood_at(&index, 0x9999).is_none());
}
#[test]
fn test_empty_walk_still_reports_why() {
let index = PoolIndex::build(crate::pool::PoolSnapshot {
diagnostics: PoolDiagnostics::from_iter(["cannot read pool node 0 heap 2".to_string()]),
..crate::pool::PoolSnapshot::default()
});
let report = report_of(&index);
assert_eq!(report.total_chunks, 0);
assert_eq!(report.allocated_chunks, 0);
assert_eq!(
report.diagnostics.examples(),
["cannot read pool node 0 heap 2"]
);
assert_eq!(report.diagnostics.emitted(), 1);
}
#[test]
fn test_coverage_says_which_way_a_walk_fell_short() {
let walked = |complete, budget_expired| {
report_of(&PoolIndex::build(crate::pool::PoolSnapshot {
complete,
budget_expired,
..crate::pool::PoolSnapshot::default()
}))
.coverage
};
assert_eq!(walked(true, false), WalkCoverage::Complete);
assert_eq!(walked(false, true), WalkCoverage::BudgetExpired);
assert_eq!(walked(false, false), WalkCoverage::Partial);
assert!(WalkCoverage::Complete.complete());
assert!(!WalkCoverage::BudgetExpired.complete());
assert!(!WalkCoverage::Partial.complete());
}
#[test]
fn test_intentional_match_stop_is_reported_beside_coverage() {
let report = report_of(&PoolIndex::build(crate::pool::PoolSnapshot {
complete: false,
stopped_after_matches: Some(3),
..crate::pool::PoolSnapshot::default()
}));
assert_eq!(report.coverage, WalkCoverage::Partial);
assert_eq!(report.stopped_after_matches, Some(3));
let deadline = report_of(&PoolIndex::build(crate::pool::PoolSnapshot {
complete: false,
budget_expired: true,
..crate::pool::PoolSnapshot::default()
}));
assert_eq!(deadline.coverage, WalkCoverage::BudgetExpired);
assert_eq!(deadline.stopped_after_matches, None);
}
#[test]
fn test_report_counts_allocated_separately_from_total() {
let index = index_of(vec![
allocation(0x1000, 0x68, b"Tgsm", PoolKind::NonPagedNx, 1),
allocation(0x1100, 0x68, b"Tgsm", PoolKind::NonPagedNx, 1),
]);
let report = report_of(&index);
assert_eq!(report.total_chunks, 2);
assert_eq!(report.allocated_chunks, 2);
assert!(report.diagnostics.is_empty());
}
#[test]
fn test_census_totals_by_tag_and_sorts_by_bytes() {
let index = index_of(vec![
allocation(0x1000, 0x68, b"Tgsm", PoolKind::NonPagedNx, 1),
allocation(0x1100, 0x68, b"Tgsm", PoolKind::NonPagedNx, 1),
allocation(0x1200, 0x400, b"Tfub", PoolKind::Paged, 1),
]);
let census = summarize_tags(&index);
assert_eq!(census[0].display_tag, "Tfub");
assert_eq!(census[0].total_bytes, 0x400);
assert_eq!(census[0].paged_allocations, 1);
assert_eq!(census[1].display_tag, "Tgsm");
assert_eq!(census[1].allocations, 2);
assert_eq!(census[1].total_bytes, 0xd0);
assert_eq!(census[1].nonpaged_allocations, 2);
}
}