use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::{Duration, Instant};
use thiserror::Error;
use super::decode::{
PAGE_SIZE, PoolHeaderLayout, SpecialPoolHeader, adjust_page_end_header, big_page_probe,
decode_descriptor_at, decode_large_requested_size, decode_lfh_subsegment, decode_pool_header,
decode_rb_root_for, decode_slist_header_next, decode_special_pool_header, decode_vs_chunk,
descriptor_backend, lfh_bitmap_state, read_u16, read_u32, read_u64,
valid_descriptor_tree_signature, valid_page_segment_signature, valid_vs_signature,
};
use super::{
HeapIdentity, PoolBackend, PoolKind, PoolSpan, PoolState,
layout::{LayoutError, PoolLayout},
};
type SnapshotSource = Box<dyn std::error::Error + Send + Sync>;
type SharedChunks = Arc<HashSet<u64>>;
#[derive(Debug, Error)]
pub(crate) enum SnapshotError {
#[error("read at {address:#x}+{size:#x}: {source}")]
Read {
address: u64,
size: usize,
#[source]
source: SnapshotSource,
},
#[error("valid-region query at {address:#x}+{size:#x}: {source}")]
RegionQuery {
address: u64,
size: usize,
#[source]
source: SnapshotSource,
},
#[error(
"sparse virtual range at {address:#x}+{size:#x} (valid {valid_base:#x}+{valid_size:#x})"
)]
RegionValidation {
address: u64,
size: usize,
valid_base: u64,
valid_size: usize,
},
#[error("snapshot layout lookup failed: {source}")]
Layout {
#[source]
source: LayoutError,
},
#[error("pool snapshot interrupted by Ctrl+C")]
Interrupted,
#[error("pool snapshot ran out of its walk budget")]
BudgetExpired,
#[error("interrupt-status query failed: {source}")]
InterruptQuery {
#[source]
source: SnapshotSource,
},
#[error("invalid snapshot data: {detail}")]
InvalidData { detail: String },
}
impl SnapshotError {
fn halts_walk(&self) -> bool {
matches!(self, Self::Interrupted | Self::BudgetExpired)
}
}
impl From<LayoutError> for SnapshotError {
fn from(source: LayoutError) -> Self {
Self::Layout { source }
}
}
fn missing_layout(item: impl Into<String>) -> SnapshotError {
LayoutError::Missing { item: item.into() }.into()
}
#[derive(Debug, Clone)]
pub(crate) struct PoolRegion {
pub address: u64,
pub size: usize,
pub requested_size: Option<u64>,
pub pool_kind: PoolKind,
pub numa_node: u16,
pub heap: HeapIdentity,
pub subsegment: Option<u64>,
pub backend: PoolBackend,
pub unit_size: u32,
pub bitmap: Vec<u8>,
pub heap_key: u64,
pub pool_header: PoolHeaderLayout,
pub vs_header_size: usize,
pub vs_sizes_offset: usize,
pub known_tag: Option<u32>,
pub states: Vec<PoolState>,
pub reusable_chunks: SharedChunks,
pub cached_chunks: SharedChunks,
}
pub(crate) trait PoolMemory {
fn read_exact(&self, address: u64, size: usize) -> Result<Vec<u8>, SnapshotError>;
fn valid_region(&self, address: u64, size: usize) -> Result<(u64, usize), SnapshotError>;
fn interrupted(&self) -> Result<bool, SnapshotError>;
fn out_of_budget(&self) -> bool {
false
}
}
struct Budgeted<'a, M> {
inner: &'a M,
deadline: Option<Instant>,
}
impl<'a, M> Budgeted<'a, M> {
fn new(inner: &'a M, deadline: Option<Instant>) -> Self {
Self { inner, deadline }
}
}
impl<M: PoolMemory> PoolMemory for Budgeted<'_, M> {
fn read_exact(&self, address: u64, size: usize) -> Result<Vec<u8>, SnapshotError> {
self.inner.read_exact(address, size)
}
fn valid_region(&self, address: u64, size: usize) -> Result<(u64, usize), SnapshotError> {
self.inner.valid_region(address, size)
}
fn interrupted(&self) -> Result<bool, SnapshotError> {
self.inner.interrupted()
}
fn out_of_budget(&self) -> bool {
self.deadline
.is_some_and(|deadline| Instant::now() >= deadline)
|| self.inner.out_of_budget()
}
}
impl PoolMemory for crate::dbgeng::DebugEngine {
fn read_exact(&self, address: u64, size: usize) -> Result<Vec<u8>, SnapshotError> {
self.read_memory(address, size)
.map_err(|source| SnapshotError::Read {
address,
size,
source: Box::new(source),
})
}
fn valid_region(&self, address: u64, size: usize) -> Result<(u64, usize), SnapshotError> {
self.valid_virtual_region(address, size)
.map_err(|source| SnapshotError::RegionQuery {
address,
size,
source: Box::new(source),
})
}
fn interrupted(&self) -> Result<bool, SnapshotError> {
crate::dbgeng::DebugEngine::interrupted(self).map_err(|source| {
SnapshotError::InterruptQuery {
source: Box::new(source),
}
})
}
}
fn check_budget(memory: &impl PoolMemory) -> Result<(), SnapshotError> {
if memory.interrupted()? {
return Err(SnapshotError::Interrupted);
}
if memory.out_of_budget() {
return Err(SnapshotError::BudgetExpired);
}
Ok(())
}
fn guarded_read(
memory: &impl PoolMemory,
address: u64,
size: usize,
) -> Result<Vec<u8>, SnapshotError> {
if size == 0 {
return Ok(Vec::new());
}
let (valid_base, valid_size) = memory.valid_region(address, size)?;
if valid_base != address || valid_size < size {
return Err(SnapshotError::RegionValidation {
address,
size,
valid_base,
valid_size,
});
}
memory.read_exact(address, size)
}
fn scalar(memory: &impl PoolMemory, address: u64, size: usize) -> Result<u64, SnapshotError> {
let bytes = guarded_read(memory, address, size)?;
match size {
1 => bytes
.first()
.map(|&byte| byte as u64)
.ok_or_else(|| SnapshotError::InvalidData {
detail: "short u8".into(),
}),
2 => Ok(
u16::from_le_bytes(bytes.try_into().map_err(|_| SnapshotError::InvalidData {
detail: "short u16".into(),
})?) as u64,
),
4 => Ok(
u32::from_le_bytes(bytes.try_into().map_err(|_| SnapshotError::InvalidData {
detail: "short u32".into(),
})?) as u64,
),
8 => Ok(u64::from_le_bytes(bytes.try_into().map_err(|_| {
SnapshotError::InvalidData {
detail: "short u64".into(),
}
})?)),
_ => Err(SnapshotError::InvalidData {
detail: format!("unsupported scalar size {size}"),
}),
}
}
fn walk_tree_nodes(
memory: &impl PoolMemory,
root: u64,
left_offset: usize,
right_offset: usize,
limit: usize,
label: &str,
diagnostics: &mut Vec<String>,
) -> Result<Vec<u64>, SnapshotError> {
let mut nodes = Vec::new();
let mut stack = vec![root];
let mut seen = HashSet::new();
while let Some(node) = stack.pop() {
let node = node & !0xf;
if node == 0 {
continue;
}
check_budget(memory)?;
if !seen.insert(node) {
diagnostics.push(format!("{label} cycle detected at {node:#x}"));
continue;
}
if nodes.len() >= limit {
diagnostics.push(format!("{label} traversal limit reached"));
break;
}
let size = left_offset.max(right_offset).saturating_add(8);
match guarded_read(memory, node, size) {
Ok(bytes) => {
nodes.push(node);
if let Some(right) = read_u64(&bytes, right_offset) {
stack.push(right);
}
if let Some(left) = read_u64(&bytes, left_offset) {
stack.push(left);
}
}
Err(error) => diagnostics.push(format!("unreadable {label} node {node:#x}: {error}")),
}
}
Ok(nodes)
}
fn tree_nodes(
memory: &impl PoolMemory,
layout: &PoolLayout,
tree_address: u64,
limit: usize,
label: &str,
diagnostics: &mut Vec<String>,
) -> Result<Vec<u64>, SnapshotError> {
let Ok(root_offset) = layout.field("_RTL_RB_TREE", "Root") else {
diagnostics.push(format!("cannot resolve {label} root field"));
return Ok(Vec::new());
};
let root_value = match scalar(memory, tree_address + root_offset as u64, 8) {
Ok(value) => value,
Err(error) => {
diagnostics.push(format!("cannot read {label} root: {error}"));
return Ok(Vec::new());
}
};
let encoded = if let Ok(encoded_offset) = layout.field("_RTL_RB_TREE", "Encoded") {
match scalar(memory, tree_address + encoded_offset as u64, 1) {
Ok(value) => value & 1 != 0,
Err(error) => {
diagnostics.push(format!("cannot read {label} encoded flag: {error}"));
return Ok(Vec::new());
}
}
} else {
false
};
let decoded_root = if layout.is_user() {
decode_rb_root_for(root_value, tree_address, encoded, true)
} else {
super::decode::decode_rb_root(root_value, tree_address, encoded)
};
let Some(root) = decoded_root else {
diagnostics.push(format!("rejecting corrupt {label} root {root_value:#x}"));
return Ok(Vec::new());
};
let Ok(left) = layout.field("_RTL_BALANCED_NODE", "Left") else {
return Ok(Vec::new());
};
let Ok(right) = layout.field("_RTL_BALANCED_NODE", "Right") else {
return Ok(Vec::new());
};
walk_tree_nodes(memory, root, left, right, limit, label, diagnostics)
}
fn walk_slist_nodes(
memory: &impl PoolMemory,
layout: &PoolLayout,
head: u64,
limit: usize,
label: &str,
diagnostics: &mut Vec<String>,
) -> Result<Vec<u64>, SnapshotError> {
let mut nodes = Vec::new();
let mut seen = HashSet::new();
let Ok(header) = layout.type_layout("_SLIST_HEADER") else {
diagnostics.push(format!("cannot resolve {label} SLIST header type"));
return Ok(nodes);
};
let Ok(alignment_offset) = layout.field("_SLIST_HEADER", "Alignment") else {
diagnostics.push(format!("cannot resolve {label} SLIST depth field"));
return Ok(nodes);
};
let Ok(region_offset) = layout.field("_SLIST_HEADER", "Region") else {
diagnostics.push(format!("cannot resolve {label} SLIST next field"));
return Ok(nodes);
};
let bytes = match guarded_read(memory, head, header.size as usize) {
Ok(value) => value,
Err(error) => {
diagnostics.push(format!("cannot read {label} list head: {error}"));
return Ok(nodes);
}
};
let depth = read_u16(&bytes, alignment_offset).map_or(0, usize::from);
let mut entry = read_u64(&bytes, region_offset)
.map(decode_slist_header_next)
.unwrap_or(0);
let expected = depth.min(limit);
while entry != 0 && nodes.len() < expected {
check_budget(memory)?;
if !seen.insert(entry) {
diagnostics.push(format!("{label} list cycle detected at {entry:#x}"));
break;
}
nodes.push(entry);
match scalar(memory, entry, 8) {
Ok(next) => entry = next & !0xf,
Err(error) => {
diagnostics.push(format!("unreadable {label} list entry {entry:#x}: {error}"));
break;
}
}
}
if depth > limit {
diagnostics.push(format!("{label} list traversal limit reached"));
} else if nodes.len() != depth {
diagnostics.push(format!(
"{label} list depth is {depth}, but only {} entries were readable",
nodes.len()
));
}
Ok(nodes)
}
fn insert_cached_chunk_candidates(
cached: &mut HashSet<u64>,
entry: u64,
pool_header_size: u64,
vs_header_size: u64,
) {
cached.insert(entry);
let overhead = pool_header_size.saturating_add(vs_header_size);
if let Some(header) = entry.checked_sub(overhead) {
cached.insert(header);
if header & (PAGE_SIZE - 1) == PAGE_SIZE - pool_header_size {
cached.insert(header.saturating_sub(16));
}
}
}
#[derive(Default)]
struct Discovery {
regions: Vec<PoolRegion>,
diagnostics: Vec<String>,
}
const SPECIAL_POOL_KINDS: [PoolKind; 4] = [
PoolKind::SpecialNonPaged,
PoolKind::SpecialNonPagedNx,
PoolKind::SpecialPaged,
PoolKind::SpecialPrototypePaged,
];
fn discover_pool_regions(
memory: &impl PoolMemory,
layout: &PoolLayout,
traversal_limit: usize,
discovery: &mut Discovery,
) -> Result<(), SnapshotError> {
let state_address = *layout
.globals
.get("ExPoolState")
.ok_or_else(|| missing_layout("ExPoolState"))?;
let state = layout.type_layout("_EX_POOL_HEAP_MANAGER_STATE")?;
let node = layout.type_layout("_EX_HEAP_POOL_NODE")?;
let number_offset = layout.field("_EX_POOL_HEAP_MANAGER_STATE", "NumberOfPools")?;
let node_offset = layout.field("_EX_POOL_HEAP_MANAGER_STATE", "PoolNode")?;
let special_offset = layout.field("_EX_POOL_HEAP_MANAGER_STATE", "SpecialHeaps")?;
let heaps_offset = layout.field("_EX_HEAP_POOL_NODE", "Heaps")?;
let lookasides_offset = layout.field("_EX_HEAP_POOL_NODE", "Lookasides").ok();
let dynamic_lookaside_size = layout
.type_layout("_RTL_DYNAMIC_LOOKASIDE")
.ok()
.map(|lookaside| lookaside.size as u64);
let number = scalar(memory, state_address + number_offset as u64, 4)? as usize;
if number == 0 || number > 256 {
return Err(SnapshotError::InvalidData {
detail: format!("implausible ExPoolState.NumberOfPools {number}"),
});
}
let mut heaps = Vec::new();
for numa_node in 0..number {
check_budget(memory)?;
let Some(node_address) = state_address
.checked_add(node_offset as u64)
.and_then(|address| address.checked_add(numa_node as u64 * node.size as u64))
else {
discovery
.diagnostics
.push("pool-node address overflow".into());
continue;
};
for heap_index in 0..4usize {
let pointer_address = node_address + heaps_offset as u64 + heap_index as u64 * 8;
let heap = match scalar(memory, pointer_address, 8) {
Ok(value) => value,
Err(error) => {
discovery.diagnostics.push(format!(
"cannot read pool node {numa_node} heap {heap_index}: {error}"
));
continue;
}
};
if heap != 0 {
let pool_kind = match heap_index {
0 => PoolKind::NonPagedExecutable,
1 => PoolKind::NonPagedNx,
2 => PoolKind::Paged,
_ => PoolKind::PrototypePaged,
};
let dynamic_lookaside =
lookasides_offset
.zip(dynamic_lookaside_size)
.and_then(|(offset, size)| {
node_address.checked_add(offset as u64).and_then(|base| {
base.checked_add(u64::from(pool_kind.is_paged()) * size)
})
});
heaps.push((heap, numa_node as u16, pool_kind, false, dynamic_lookaside));
}
}
}
for (special_index, pool_kind) in SPECIAL_POOL_KINDS.into_iter().enumerate() {
let pointer_address = state_address + special_offset as u64 + special_index as u64 * 8;
match scalar(memory, pointer_address, 8) {
Ok(heap) if heap != 0 => heaps.push((heap, 0, pool_kind, true, None)),
Ok(_) => {}
Err(error) => discovery.diagnostics.push(format!(
"cannot read special pool heap {special_index}: {error}"
)),
}
}
let globals_address = *layout
.globals
.get("RtlpHpHeapGlobals")
.ok_or_else(|| missing_layout("RtlpHpHeapGlobals"))?;
let heap_key = scalar(
memory,
globals_address + layout.field("_RTLP_HP_HEAP_GLOBALS", "HeapKey")? as u64,
8,
)?;
let lfh_key = scalar(
memory,
globals_address + layout.field("_RTLP_HP_HEAP_GLOBALS", "LfhKey")? as u64,
8,
)?;
for (heap_address, numa_node, pool_kind, special, dynamic_lookaside) in heaps {
let identity = HeapIdentity {
pool_state: state_address,
heap: heap_address,
special,
};
if let Err(error) = discover_heap_regions(
memory,
layout,
heap_address,
numa_node,
pool_kind,
identity,
dynamic_lookaside,
heap_key,
lfh_key,
traversal_limit,
discovery,
) {
if error.halts_walk() {
return Err(error);
}
discovery.diagnostics.push(format!(
"cannot fully discover heap {heap_address:#x}: {error}"
));
}
}
let _ = state.size;
Ok(())
}
struct VsRoot {
base: u64,
tree_offset: usize,
delay_offset: Option<usize>,
}
fn vs_roots(
memory: &impl PoolMemory,
layout: &PoolLayout,
context: u64,
diagnostics: &mut Vec<String>,
) -> Result<Vec<VsRoot>, SnapshotError> {
if let Ok(tree_offset) = layout.field("_HEAP_VS_CONTEXT", "FreeChunkTree") {
return Ok(vec![VsRoot {
base: context,
tree_offset,
delay_offset: layout.field("_HEAP_VS_CONTEXT", "DelayFreeContext").ok(),
}]);
}
let (Ok(tree_offset), Ok(back_offset), Ok(slot_map_ref_offset), Ok(affinity_offset)) = (
layout.field("_HEAP_VS_AFFINITY_SLOT", "FreeChunkTree"),
layout.field("_HEAP_VS_AFFINITY_SLOT", "VsContext"),
layout.field("_HEAP_VS_CONTEXT", "SlotMapRef"),
layout.field("_HEAP_VS_CONTEXT", "AffinityMask"),
) else {
diagnostics
.push("VS free-chunk state is in neither the context nor an affinity slot".into());
return Ok(Vec::new());
};
let (Ok(slot_map_ref), Ok(affinity_mask)) = (
scalar(memory, context + slot_map_ref_offset as u64, 2),
scalar(memory, context + affinity_offset as u64, 1),
) else {
diagnostics.push(format!(
"cannot read the VS slot map of context {context:#x}"
));
return Ok(Vec::new());
};
let entries = affinity_mask as usize + 1;
if slot_map_ref == 0 || entries > 256 {
diagnostics.push(format!(
"implausible VS slot map for context {context:#x}: ref {slot_map_ref:#x}, {entries} entries"
));
return Ok(Vec::new());
}
let entry_size = layout
.type_layout("_HEAP_VS_SLOT_MAP")
.map_or(4, |map| map.size as u64);
let slot_ref_offset = layout.field("_HEAP_VS_SLOT_MAP", "SlotRef").unwrap_or(0);
let delay_offset = layout
.field("_HEAP_VS_AFFINITY_SLOT", "DelayFreeContext")
.ok();
let slot_map = context + (slot_map_ref << 6);
let mut roots = Vec::new();
let mut seen = HashSet::new();
for index in 0..entries {
check_budget(memory)?;
let entry = slot_map + index as u64 * entry_size;
let Ok(slot_ref) = scalar(memory, entry + slot_ref_offset as u64, 2) else {
diagnostics.push(format!(
"cannot read VS slot map entry {index} at {entry:#x}; its affinity slot is omitted"
));
continue;
};
if slot_ref == 0 {
continue;
}
let slot = context + (slot_ref << 6);
if !seen.insert(slot) {
continue;
}
match scalar(memory, slot + back_offset as u64, 8) {
Ok(owner) if owner == context => roots.push(VsRoot {
base: slot,
tree_offset,
delay_offset,
}),
Ok(owner) => diagnostics.push(format!(
"VS affinity slot {slot:#x} claims context {owner:#x}, not {context:#x}; skipped"
)),
Err(error) => {
diagnostics.push(format!("cannot read VS affinity slot {slot:#x}: {error}"))
}
}
}
Ok(roots)
}
fn discover_vs_evidence(
memory: &impl PoolMemory,
layout: &PoolLayout,
heap_address: u64,
dynamic_lookaside: Option<u64>,
limit: usize,
diagnostics: &mut Vec<String>,
) -> Result<(SharedChunks, SharedChunks), SnapshotError> {
let Ok(vs_context_offset) = layout.field("_SEGMENT_HEAP", "VsContext") else {
return Ok(Default::default());
};
let context = heap_address + vs_context_offset as u64;
let roots = vs_roots(memory, layout, context, diagnostics)?;
if roots.is_empty() {
return Ok(Default::default());
}
let tree_node_offset = layout
.field("_HEAP_VS_CHUNK_FREE_HEADER", "TreeNode")
.unwrap_or(0);
let mut reusable = HashSet::new();
for root in &roots {
reusable.extend(
tree_nodes(
memory,
layout,
root.base + root.tree_offset as u64,
limit,
"VS free tree",
diagnostics,
)?
.into_iter()
.map(|node| node.saturating_sub(tree_node_offset as u64)),
);
}
let mut cached = HashSet::new();
let pool_header_size = layout
.type_layout("_POOL_HEADER")
.map_or(0, |value| value.size as u64);
let vs_header_size = layout
.type_layout("_HEAP_VS_CHUNK_HEADER")
.map_or(0, |value| value.size as u64);
if let Ok(list_offset) = layout.field("_HEAP_VS_DELAY_FREE_CONTEXT", "ListHead") {
for root in &roots {
let Some(delay_offset) = root.delay_offset else {
continue;
};
for entry in walk_slist_nodes(
memory,
layout,
root.base + delay_offset as u64 + list_offset as u64,
limit,
"VS delay-free",
diagnostics,
)? {
insert_cached_chunk_candidates(
&mut cached,
entry,
pool_header_size,
vs_header_size,
);
}
}
}
if let Some(dynamic) = dynamic_lookaside {
let bucket_count = layout
.field("_RTL_DYNAMIC_LOOKASIDE", "BucketCount")
.ok()
.and_then(|offset| scalar(memory, dynamic + offset as u64, 4).ok())
.unwrap_or(0) as usize;
let buckets_offset = layout.field("_RTL_DYNAMIC_LOOKASIDE", "Buckets").ok();
let lookaside = layout.type_layout("_RTL_LOOKASIDE").ok();
let list_offset = layout.field("_RTL_LOOKASIDE", "ListHead").ok();
if bucket_count > 64 {
diagnostics.push(format!(
"rejecting implausible VS dynamic-lookaside bucket count {bucket_count}"
));
} else if let (Some(buckets_offset), Some(lookaside), Some(list_offset)) =
(buckets_offset, lookaside, list_offset)
{
for bucket in 0..bucket_count {
check_budget(memory)?;
let Some(bucket_address) = dynamic
.checked_add(buckets_offset as u64)
.and_then(|value| value.checked_add(bucket as u64 * u64::from(lookaside.size)))
else {
diagnostics.push("VS dynamic-lookaside bucket address overflow".into());
break;
};
if let Ok(size_offset) = layout.field("_RTL_LOOKASIDE", "Size") {
match scalar(memory, bucket_address + size_offset as u64, 4) {
Ok(0) => continue,
Ok(size) if size > 0x1_0000 => {
diagnostics.push(format!(
"rejecting VS dynamic-lookaside bucket {bucket} size {size:#x}"
));
continue;
}
Ok(_) => {}
Err(error) => {
diagnostics.push(format!(
"cannot read VS dynamic-lookaside bucket {bucket} size: {error}"
));
continue;
}
}
}
for entry in walk_slist_nodes(
memory,
layout,
bucket_address + list_offset as u64,
limit,
"VS dynamic-lookaside",
diagnostics,
)? {
insert_cached_chunk_candidates(
&mut cached,
entry,
pool_header_size,
vs_header_size,
);
}
}
}
}
Ok((Arc::new(reusable), Arc::new(cached)))
}
#[allow(clippy::too_many_arguments)]
fn discover_heap_regions(
memory: &impl PoolMemory,
layout: &PoolLayout,
heap_address: u64,
numa_node: u16,
pool_kind: PoolKind,
identity: HeapIdentity,
dynamic_lookaside: Option<u64>,
heap_key: u64,
lfh_key: u64,
traversal_limit: usize,
discovery: &mut Discovery,
) -> Result<(), SnapshotError> {
let heap = layout.type_layout("_SEGMENT_HEAP")?;
let context = layout.type_layout("_HEAP_SEG_CONTEXT")?;
let contexts_offset = layout.field("_SEGMENT_HEAP", "SegContexts")?;
let (reusable_chunks, cached_chunks) = discover_vs_evidence(
memory,
layout,
heap_address,
dynamic_lookaside,
traversal_limit,
&mut discovery.diagnostics,
)?;
if let Ok(lfh_offset) = layout.field("_SEGMENT_HEAP", "LfhContext") {
let lfh = heap_address + lfh_offset as u64;
for (field, label) in [
("Buckets", "LFH buckets"),
("AffinitySlots", "LFH affinity slots"),
] {
if let Ok(offset) = layout.field("_HEAP_LFH_CONTEXT", field)
&& let Err(error) = guarded_read(memory, lfh + offset as u64, 8)
{
discovery
.diagnostics
.push(format!("cannot read {label}: {error}"));
}
}
}
for context_index in 0..2usize {
check_budget(memory)?;
let context_address =
heap_address + contexts_offset as u64 + context_index as u64 * context.size as u64;
if let Err(error) = discover_segment_context(
memory,
layout,
context_address,
numa_node,
pool_kind,
identity,
heap_key,
lfh_key,
traversal_limit,
&reusable_chunks,
&cached_chunks,
discovery,
) {
if error.halts_walk() {
return Err(error);
}
discovery.diagnostics.push(format!(
"cannot discover segment context {context_index} at {context_address:#x}: {error}"
));
}
}
discover_large_allocations(
memory,
layout,
heap_address,
numa_node,
pool_kind,
identity,
heap_key,
traversal_limit,
discovery,
)?;
let _ = heap.size;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn discover_segment_context(
memory: &impl PoolMemory,
layout: &PoolLayout,
context_address: u64,
numa_node: u16,
pool_kind: PoolKind,
identity: HeapIdentity,
heap_key: u64,
lfh_key: u64,
traversal_limit: usize,
reusable_chunks: &SharedChunks,
cached_chunks: &SharedChunks,
discovery: &mut Discovery,
) -> Result<(), SnapshotError> {
let segment = layout.type_layout("_HEAP_PAGE_SEGMENT")?;
let descriptor = layout.type_layout("_HEAP_PAGE_RANGE_DESCRIPTOR")?;
let shift = scalar(
memory,
context_address + layout.field("_HEAP_SEG_CONTEXT", "UnitShift")? as u64,
1,
)? as u32;
if !(12..=20).contains(&shift) {
return Ok(());
}
let first_descriptor = scalar(
memory,
context_address + layout.field("_HEAP_SEG_CONTEXT", "FirstDescriptorIndex")? as u64,
1,
)? as usize;
let segment_mask = scalar(
memory,
context_address + layout.field("_HEAP_SEG_CONTEXT", "SegmentMask")? as u64,
8,
)?;
let segment_size = (!segment_mask).wrapping_add(1);
let descriptor_count = (segment_size >> shift).min(4096) as usize;
if descriptor_count == 0 || first_descriptor >= descriptor_count {
return Ok(());
}
let free_tree = context_address + layout.field("_HEAP_SEG_CONTEXT", "FreePageRanges")? as u64;
let free_nodes: HashSet<_> = tree_nodes(
memory,
layout,
free_tree,
traversal_limit,
"free-page tree",
&mut discovery.diagnostics,
)?
.into_iter()
.collect();
let list_head = context_address + layout.field("_HEAP_SEG_CONTEXT", "SegmentListHead")? as u64;
let list_entry = layout.field("_HEAP_PAGE_SEGMENT", "ListEntry")?;
let desc_array = layout.field("_HEAP_PAGE_SEGMENT", "DescArray")?;
let signature_offset = layout.field("_HEAP_PAGE_SEGMENT", "Signature")?;
let unit_offset = layout.field("_HEAP_PAGE_RANGE_DESCRIPTOR", "UnitSize")?;
let flags_offset = layout.field("_HEAP_PAGE_RANGE_DESCRIPTOR", "RangeFlags")?;
let tree_signature_offset = layout.field("_HEAP_PAGE_RANGE_DESCRIPTOR", "TreeSignature")?;
let tree_node_offset = layout.field("_HEAP_PAGE_RANGE_DESCRIPTOR", "TreeNode")?;
let metadata_size = descriptor_count
.checked_mul(descriptor.size as usize)
.ok_or_else(|| SnapshotError::InvalidData {
detail: "descriptor metadata size overflow".into(),
})?;
let mut entry = scalar(memory, list_head, 8)? & !0xf;
let mut seen = HashSet::new();
while entry != 0 && entry != list_head && seen.len() < traversal_limit {
check_budget(memory)?;
if !seen.insert(entry) {
discovery
.diagnostics
.push(format!("segment-list cycle at {entry:#x}"));
break;
}
let segment_address = entry.saturating_sub(list_entry as u64);
let segment_header = match guarded_read(memory, segment_address, segment.size as usize) {
Ok(bytes) => bytes,
Err(error) => {
discovery.diagnostics.push(format!(
"cannot read segment header {segment_address:#x}: {error}"
));
match scalar(memory, entry, 8) {
Ok(next) => entry = next & !0xf,
Err(_) => break,
}
continue;
}
};
let signature = read_u64(&segment_header, signature_offset)
.or_else(|| read_u32(&segment_header, signature_offset).map(u64::from))
.unwrap_or(0);
if !valid_page_segment_signature(signature, segment_address, context_address, heap_key) {
discovery.diagnostics.push(format!(
"rejecting page segment {segment_address:#x} with invalid signature {signature:#x}"
));
entry = read_u64(&segment_header, list_entry).unwrap_or(0) & !0xf;
continue;
}
let metadata_address = segment_address + desc_array as u64;
let metadata = match guarded_read(memory, metadata_address, metadata_size) {
Ok(bytes) => bytes,
Err(error) => {
discovery.diagnostics.push(format!(
"cannot read descriptors at {metadata_address:#x}: {error}"
));
entry = read_u64(&segment_header, list_entry).unwrap_or(0) & !0xf;
continue;
}
};
let mut descriptor_index = first_descriptor;
while descriptor_index < descriptor_count {
check_budget(memory)?;
let offset = descriptor_index * descriptor.size as usize;
let Some(decoded) = decode_descriptor_at(
&metadata,
offset,
descriptor.size as usize,
unit_offset,
flags_offset,
) else {
descriptor_index += 1;
continue;
};
if decoded.first
&& !read_u32(&metadata, offset + tree_signature_offset)
.is_some_and(valid_descriptor_tree_signature)
{
discovery.diagnostics.push(format!(
"rejecting descriptor {descriptor_index} at {:#x} with invalid tree signature",
metadata_address + offset as u64
));
descriptor_index += decoded.unit_size.max(1) as usize;
continue;
}
let unit_size = decoded.unit_size as usize;
let Some(address) = segment_address.checked_add((descriptor_index as u64) << shift)
else {
break;
};
let size = unit_size.checked_shl(shift).unwrap_or(0);
if size == 0 {
descriptor_index += unit_size.max(1);
continue;
}
let backend = if identity.special {
PoolBackend::Segment
} else {
descriptor_backend(decoded.flags)
};
let mut region_address = address;
let mut region_size = size;
let mut bitmap = Vec::new();
let mut block_size = size.min(u32::MAX as usize) as u32;
if backend == PoolBackend::Lfh {
let subsegment = layout.type_layout("_HEAP_LFH_SUBSEGMENT")?;
let offsets = layout.field("_HEAP_LFH_SUBSEGMENT", "BlockOffsets")?
+ layout.field("_HEAP_LFH_SUBSEGMENT_ENCODED_OFFSETS", "EncodedData")?;
let count_offset = layout.field("_HEAP_LFH_SUBSEGMENT", "BlockCount")?;
let bitmap_offset = layout.field("_HEAP_LFH_SUBSEGMENT", "BlockBitmap")?;
let header = match guarded_read(memory, address, subsegment.size as usize) {
Ok(bytes) => bytes,
Err(error) => {
discovery
.diagnostics
.push(format!("cannot read LFH subsegment {address:#x}: {error}"));
descriptor_index += unit_size;
continue;
}
};
let encoded = read_u32(&header, offsets).unwrap_or(0);
let blocks = read_u16(&header, count_offset)
.map(usize::from)
.unwrap_or(0);
let lfh = match decode_lfh_subsegment(
encoded,
address,
lfh_key as u32,
blocks,
region_size,
) {
Ok(lfh) => lfh,
Err(rejection) => {
discovery.diagnostics.push(format!(
"rejecting LFH subsegment {address:#x}: {rejection}"
));
descriptor_index += unit_size;
continue;
}
};
block_size = lfh.block_size;
bitmap = match guarded_read(
memory,
address + bitmap_offset as u64,
lfh.blocks.div_ceil(4),
) {
Ok(bytes) => bytes,
Err(error) => {
discovery
.diagnostics
.push(format!("cannot read LFH bitmap at {address:#x}: {error}"));
descriptor_index += unit_size;
continue;
}
};
region_address += lfh.first as u64;
region_size = lfh.blocks * block_size as usize;
} else if backend == PoolBackend::Vs {
let vs = layout.type_layout("_HEAP_VS_SUBSEGMENT")?;
let header = match guarded_read(memory, address, vs.size as usize) {
Ok(bytes) => bytes,
Err(error) => {
discovery
.diagnostics
.push(format!("cannot read VS subsegment {address:#x}: {error}"));
descriptor_index += unit_size;
continue;
}
};
let signature =
read_u16(&header, layout.field("_HEAP_VS_SUBSEGMENT", "Signature")?)
.unwrap_or(0)
& 0x7fff;
let declared =
read_u16(&header, layout.field("_HEAP_VS_SUBSEGMENT", "Size")?).unwrap_or(0);
if !valid_vs_signature(signature ^ declared) {
discovery.diagnostics.push(format!(
"rejecting VS subsegment {address:#x} with invalid signature"
));
descriptor_index += unit_size;
continue;
}
let first = (vs.size as usize).next_multiple_of(16);
if first >= region_size {
descriptor_index += unit_size;
continue;
}
region_address += first as u64;
region_size -= first;
block_size = 0;
let declared_size = usize::from(declared) * 16;
if declared_size == 0 || declared_size > region_size {
discovery.diagnostics.push(format!(
"VS subsegment {address:#x} declares {declared_size:#x} of chunks where \
its page range leaves room for {region_size:#x}"
));
} else {
region_size = declared_size;
}
}
let descriptor_node = metadata_address + offset as u64 + tree_node_offset as u64;
let state = if free_nodes.contains(&descriptor_node) || !decoded.allocated() {
PoolState::ReusableFree
} else {
PoolState::Allocated
};
discovery.regions.push(PoolRegion {
address: region_address,
size: region_size,
requested_size: None,
pool_kind,
numa_node,
heap: identity,
subsegment: Some(address),
backend,
unit_size: block_size,
bitmap,
heap_key,
pool_header: layout.pool_header_layout()?,
vs_header_size: layout.type_layout("_HEAP_VS_CHUNK_HEADER")?.size as usize,
vs_sizes_offset: layout.field("_HEAP_VS_CHUNK_HEADER", "Sizes")?,
known_tag: None,
states: vec![state],
reusable_chunks: Arc::clone(reusable_chunks),
cached_chunks: Arc::clone(cached_chunks),
});
descriptor_index += unit_size;
}
entry = read_u64(&segment_header, list_entry).unwrap_or(0) & !0xf;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn discover_large_allocations(
memory: &impl PoolMemory,
layout: &PoolLayout,
heap_address: u64,
numa_node: u16,
pool_kind: PoolKind,
identity: HeapIdentity,
heap_key: u64,
traversal_limit: usize,
discovery: &mut Discovery,
) -> Result<(), SnapshotError> {
let Ok(tree_offset) = layout.field("_SEGMENT_HEAP", "LargeAllocMetadata") else {
return Ok(());
};
let Ok(large) = layout.type_layout("_HEAP_LARGE_ALLOC_DATA") else {
return Ok(());
};
let Ok(tree_node) = layout.field("_HEAP_LARGE_ALLOC_DATA", "TreeNode") else {
return Ok(());
};
let Ok(virtual_offset) = layout.field("_HEAP_LARGE_ALLOC_DATA", "VirtualAddress") else {
return Ok(());
};
let Ok(pages_offset) = layout.field("_HEAP_LARGE_ALLOC_DATA", "AllocatedPages") else {
return Ok(());
};
let tree_address = heap_address + tree_offset as u64;
let nodes = tree_nodes(
memory,
layout,
tree_address,
traversal_limit,
"large-allocation tree",
&mut discovery.diagnostics,
)?;
for node in nodes {
check_budget(memory)?;
let allocation_address = node.saturating_sub(tree_node as u64);
let allocation = match guarded_read(memory, allocation_address, large.size as usize) {
Ok(bytes) => bytes,
Err(error) => {
discovery.diagnostics.push(format!(
"cannot read large-allocation metadata {allocation_address:#x}: {error}"
));
continue;
}
};
let Some((virtual_address, pages)) = read_u64(&allocation, virtual_offset)
.zip(read_u64(&allocation, pages_offset))
.and_then(|(virtual_address, pages)| {
if layout.is_user() {
super::decode::decode_large_allocation_for(virtual_address, pages, true)
} else {
super::decode::decode_large_allocation(virtual_address, pages)
}
})
else {
continue;
};
if pages > 0x10_0000 {
discovery.diagnostics.push(format!(
"rejecting implausible large allocation at {allocation_address:#x}"
));
continue;
}
let bytes = pages.saturating_mul(PAGE_SIZE);
let validated_unused_bytes = layout
.field("_HEAP_LARGE_ALLOC_DATA", "UnusedBytes")
.is_ok_and(|offset| offset == virtual_offset);
let requested_size = read_u64(&allocation, virtual_offset)
.and_then(|word| decode_large_requested_size(word, bytes, validated_unused_bytes));
let (tag, tracked_size) = if layout.is_user() {
(0, bytes)
} else {
match lookup_big_page_target(
memory,
layout,
virtual_address,
&mut discovery.diagnostics,
)? {
Some(value) => value,
None => (0, bytes),
}
};
let size = tracked_size.min(bytes).min(usize::MAX as u64) as usize;
let Ok(pool_header) = layout.pool_header_layout() else {
continue;
};
discovery.regions.push(PoolRegion {
address: virtual_address,
size,
requested_size: layout.is_user().then_some(requested_size).flatten(),
pool_kind,
numa_node,
heap: identity,
subsegment: None,
backend: PoolBackend::Large,
unit_size: size.min(u32::MAX as usize) as u32,
bitmap: Vec::new(),
heap_key,
pool_header,
vs_header_size: 0,
vs_sizes_offset: 0,
known_tag: Some(tag),
states: vec![PoolState::Allocated],
reusable_chunks: Arc::default(),
cached_chunks: Arc::default(),
});
}
Ok(())
}
const BIG_PAGE_PROBE_BATCH: usize = 256;
fn lookup_big_page_target(
memory: &impl PoolMemory,
layout: &PoolLayout,
address: u64,
diagnostics: &mut Vec<String>,
) -> Result<Option<(u32, u64)>, SnapshotError> {
let Ok(entry) = layout.type_layout("_POOL_TRACKER_BIG_PAGES") else {
return Ok(None);
};
let entry_size = entry.size as usize;
let Ok(va_offset) = layout.field("_POOL_TRACKER_BIG_PAGES", "Va") else {
return Ok(None);
};
let Ok(tag_offset) = layout.field("_POOL_TRACKER_BIG_PAGES", "Key") else {
return Ok(None);
};
let Ok(size_offset) = layout.field("_POOL_TRACKER_BIG_PAGES", "NumberOfBytes") else {
return Ok(None);
};
let Some(&table_pointer_address) = layout.globals.get("PoolBigPageTable") else {
return Ok(None);
};
let table = match scalar(memory, table_pointer_address, 8) {
Ok(value) => value,
Err(error) => {
diagnostics.push(format!("cannot read big-page table pointer: {error}"));
return Ok(None);
}
};
let Some(&size_address) = layout.globals.get("PoolBigPageTableSize") else {
return Ok(None);
};
let count = match scalar(memory, size_address, 4).or_else(|_| scalar(memory, size_address, 8)) {
Ok(value) => value as usize,
Err(error) => {
diagnostics.push(format!("cannot read big-page table size: {error}"));
return Ok(None);
}
};
if table == 0 || count == 0 || count > 0x10_0000 || !count.is_power_of_two() {
diagnostics.push(format!("rejecting implausible big-page table size {count}"));
return Ok(None);
}
let mut probes = big_page_probe(address, count).ok_or_else(|| SnapshotError::InvalidData {
detail: format!("invalid big-page table size {count}"),
})?;
let mut remaining = count;
'probe: while let Some(first_index) = probes.next() {
check_budget(memory)?;
let batch_len = BIG_PAGE_PROBE_BATCH.min(remaining).min(count - first_index);
let byte_len =
entry_size
.checked_mul(batch_len)
.ok_or_else(|| SnapshotError::InvalidData {
detail: "big-page probe batch size overflow".into(),
})?;
let entry_address = table
.checked_add(first_index as u64 * entry.size as u64)
.ok_or_else(|| SnapshotError::InvalidData {
detail: "big-page probe address overflow".into(),
})?;
let bytes = match guarded_read(memory, entry_address, byte_len) {
Ok(bytes) => bytes,
Err(error) => {
diagnostics.push(format!(
"cannot read big-page entries {first_index}..{} at {entry_address:#x}: {error}",
first_index + batch_len
));
for _ in 1..batch_len {
let _ = probes.next();
}
remaining -= batch_len;
continue;
}
};
for batch_index in 0..batch_len {
let offset = batch_index * entry_size;
let index = first_index + batch_index;
let Some(candidate) = read_u64(&bytes, offset + va_offset) else {
diagnostics.push(format!("truncated big-page entry {index}"));
continue;
};
if candidate == 0 {
break 'probe;
}
if candidate & !1 == address {
let Some(tag) = read_u32(&bytes, offset + tag_offset) else {
diagnostics.push(format!("truncated big-page tag at entry {index}"));
continue;
};
let Some(size) = read_u64(&bytes, offset + size_offset) else {
diagnostics.push(format!("truncated big-page size at entry {index}"));
continue;
};
return Ok(Some((tag, size)));
}
}
for _ in 1..batch_len {
let _ = probes.next();
}
remaining -= batch_len;
}
diagnostics.push(format!(
"no validated big-page entry for large allocation {address:#x}"
));
Ok(None)
}
pub const DIAGNOSTIC_EXAMPLES: usize = 8;
fn diagnostic_shape(message: &str) -> String {
message
.split_whitespace()
.map(|token| {
if token.contains(|character: char| character.is_ascii_digit()) {
"#"
} else {
token
}
})
.collect::<Vec<_>>()
.join(" ")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiagnosticShape {
pub shape: String,
pub total: usize,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PoolDiagnostics {
examples: Vec<String>,
shapes: Vec<DiagnosticShape>,
positions: HashMap<String, usize>,
}
impl PoolDiagnostics {
pub(crate) fn push(&mut self, message: String) {
let shape = diagnostic_shape(&message);
let position = match self.positions.get(&shape) {
Some(&position) => position,
None => {
self.shapes.push(DiagnosticShape {
shape: shape.clone(),
total: 0,
});
let position = self.shapes.len() - 1;
self.positions.insert(shape, position);
position
}
};
let seen = &mut self.shapes[position];
seen.total += 1;
if seen.total <= DIAGNOSTIC_EXAMPLES {
self.examples.push(message);
}
}
pub fn is_empty(&self) -> bool {
self.shapes.is_empty()
}
pub fn emitted(&self) -> usize {
self.shapes.iter().map(|seen| seen.total).sum()
}
pub fn examples(&self) -> &[String] {
&self.examples
}
pub fn shapes(&self) -> &[DiagnosticShape] {
&self.shapes
}
pub fn lines(&self) -> Vec<String> {
let mut lines = self.examples.clone();
for seen in &self.shapes {
if let Some(collapsed) = seen
.total
.checked_sub(DIAGNOSTIC_EXAMPLES)
.filter(|more| *more > 0)
{
lines.push(format!("... and {collapsed} more like `{}`", seen.shape));
}
}
lines
}
}
impl Extend<String> for PoolDiagnostics {
fn extend<T: IntoIterator<Item = String>>(&mut self, messages: T) {
for message in messages {
self.push(message);
}
}
}
impl FromIterator<String> for PoolDiagnostics {
fn from_iter<T: IntoIterator<Item = String>>(messages: T) -> Self {
let mut diagnostics = Self::default();
diagnostics.extend(messages);
diagnostics
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct PoolSnapshot {
pub layout: crate::allocator::LayoutProvenance,
pub spans: Vec<PoolSpan>,
pub diagnostics: PoolDiagnostics,
pub complete: bool,
pub budget_expired: bool,
pub stalls: WalkStalls,
pub refused_chunks: u64,
pub unplaced_bytes: u64,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct WalkStalls {
pub pages: u64,
pub skipped_bytes: u64,
pub recovered_bytes: u64,
}
pub(crate) struct SnapshotWalker<'a, M> {
pub memory: &'a M,
pub layout: &'a PoolLayout,
pub traversal_limit: usize,
}
struct SpecialPlacement {
usable: u64,
size: u64,
approximate: bool,
}
fn special_pool_placement(
page: u64,
header: SpecialPoolHeader,
page_bytes: &[u8],
) -> SpecialPlacement {
let available = page_bytes.len() as u64;
let header_size = header.header_size as u64;
let aligned = u64::from(header.requested).next_multiple_of(16);
let start = (PAGE_SIZE - aligned) as usize;
if available == PAGE_SIZE {
let leading = &page_bytes[header.header_size..start];
let padding = &page_bytes[start + header.requested as usize..];
if leading
.iter()
.chain(padding)
.all(|byte| *byte == header.fill)
{
return SpecialPlacement {
usable: page + PAGE_SIZE - aligned,
size: u64::from(header.requested),
approximate: false,
};
}
}
SpecialPlacement {
usable: page + header_size,
size: available.saturating_sub(header_size),
approximate: true,
}
}
const DISCOVERY_BUDGET_SHARE: (u32, u32) = (2, 3);
const EXTENT_READ_CHUNK: usize = 256 * 1024;
const MAX_CONSECUTIVE_STALLS: u32 = 8;
fn budget_deadlines(
start: Instant,
budget: Option<Duration>,
) -> (Option<Instant>, Option<Instant>) {
let (numerator, denominator) = DISCOVERY_BUDGET_SHARE;
let Some(whole) = budget.and_then(|full| start.checked_add(full)) else {
return (None, None);
};
(
budget.and_then(|full| start.checked_add(full / denominator * numerator)),
Some(whole),
)
}
impl<'a, M: PoolMemory> SnapshotWalker<'a, M> {
pub(crate) fn walk(&self, budget: Option<Duration>) -> Result<PoolSnapshot, SnapshotError> {
let (discovery_deadline, walk_deadline) = budget_deadlines(Instant::now(), budget);
let mut snapshot = PoolSnapshot {
diagnostics: PoolDiagnostics::from_iter([
"per-session paged heaps are not included".to_string()
]),
complete: true,
..PoolSnapshot::default()
};
let discovery_clock = Budgeted::new(self.memory, discovery_deadline);
let mut discovery = Discovery::default();
let expired = match discover_pool_regions(
&discovery_clock,
self.layout,
self.traversal_limit,
&mut discovery,
) {
Ok(()) => false,
Err(SnapshotError::BudgetExpired) => true,
Err(error) => return Err(error),
};
if !discovery.diagnostics.is_empty() {
snapshot.complete = false;
}
snapshot
.diagnostics
.extend(std::mem::take(&mut discovery.diagnostics));
let walk_clock = Budgeted::new(self.memory, walk_deadline);
let walker = SnapshotWalker {
memory: &walk_clock,
layout: self.layout,
traversal_limit: self.traversal_limit,
};
walker.walk_discovered_regions(discovery.regions, budget, expired, &mut snapshot)?;
Ok(snapshot)
}
fn walk_discovered_regions(
&self,
regions: Vec<PoolRegion>,
budget: Option<Duration>,
mut expired: bool,
snapshot: &mut PoolSnapshot,
) -> Result<(), SnapshotError> {
let discovered = regions.len();
let mut walked = 0usize;
for region in regions {
let outcome = if region.backend == PoolBackend::Large {
self.walk_large(®ion, snapshot);
Ok(())
} else {
self.walk_region(®ion, snapshot)
};
match outcome {
Ok(()) => walked += 1,
Err(SnapshotError::BudgetExpired) => {
expired = true;
break;
}
Err(error) => return Err(error),
}
}
if expired {
snapshot.complete = false;
snapshot.budget_expired = true;
let allowed = match budget {
Some(budget) => format!("{budget:?} budget"),
None => "walk budget".to_string(),
};
snapshot.diagnostics.push(format!(
"the walk ran out of its {allowed}: {walked} of {discovered} discovered regions \
were walked, and region discovery itself may not have finished. What is \
reported was really there; what is missing is unknown, not absent. Allow a \
longer budget for full coverage."
));
}
snapshot
.spans
.sort_by_key(|span| (span.heap, span.usable_address));
Ok(())
}
fn walk_region(
&self,
region: &PoolRegion,
snapshot: &mut PoolSnapshot,
) -> Result<(), SnapshotError> {
let requested_end = region.address.saturating_add(region.size as u64);
let mut cursor = region.address;
let mut consecutive_stalls = 0u32;
let mut stalled_here = false;
let mut vs_chunk = Some(region.address);
while cursor < requested_end {
check_budget(self.memory)?;
let remaining = requested_end.saturating_sub(cursor).min(usize::MAX as u64) as usize;
let (reported_base, reported_size) = match self.memory.valid_region(cursor, remaining) {
Ok(valid) => valid,
Err(error) => {
snapshot.diagnostics.push(format!(
"cannot query region {cursor:#x}+{remaining:#x}: {error}"
));
self.unreadable(region, cursor, requested_end - cursor, snapshot);
break;
}
};
let valid_base = reported_base.max(cursor).min(requested_end);
if valid_base > cursor {
snapshot.diagnostics.push(format!(
"region {:#x}+{:#x} is only committed through {cursor:#x}; unreadable space extends {:#x} bytes",
region.address,
region.size,
valid_base - cursor
));
self.unreadable(region, cursor, valid_base - cursor, snapshot);
}
let valid_end = reported_base
.saturating_add(reported_size as u64)
.min(requested_end);
if valid_end <= valid_base {
if valid_base >= requested_end {
break;
}
if reported_base == 0 && reported_size == 0 {
self.unreadable(region, valid_base, requested_end - valid_base, snapshot);
break;
}
let page_end = (valid_base & !(PAGE_SIZE - 1)).saturating_add(PAGE_SIZE);
let skip = page_end.min(requested_end) - valid_base;
snapshot.diagnostics.push(format!(
"valid-region query made no progress at {valid_base:#x}: the engine answered \
{reported_base:#x}+{reported_size:#x}; stepping over the rest of the page"
));
self.unreadable(region, valid_base, skip, snapshot);
snapshot.stalls.pages += 1;
snapshot.stalls.skipped_bytes = snapshot.stalls.skipped_bytes.saturating_add(skip);
stalled_here = true;
consecutive_stalls += 1;
cursor = valid_base.saturating_add(skip);
if consecutive_stalls >= MAX_CONSECUTIVE_STALLS {
snapshot.diagnostics.push(format!(
"region {:#x}+{:#x}: giving up after consecutive pages that would not advance",
region.address, region.size
));
self.unreadable(region, cursor, requested_end - cursor, snapshot);
break;
}
continue;
}
consecutive_stalls = 0;
let bytes = match self.read_extent(valid_base, (valid_end - valid_base) as usize) {
Ok(bytes) => bytes,
Err(error) if error.halts_walk() => return Err(error),
Err(error) => {
snapshot
.diagnostics
.push(format!("cannot read region {valid_base:#x}: {error}"));
self.unreadable(region, valid_base, valid_end - valid_base, snapshot);
cursor = valid_end;
continue;
}
};
if stalled_here {
snapshot.stalls.recovered_bytes = snapshot
.stalls
.recovered_bytes
.saturating_add(bytes.len() as u64);
}
if region.heap.special {
self.walk_special_pool(region, valid_base, &bytes, snapshot);
cursor = valid_end;
continue;
}
match region.backend {
PoolBackend::Lfh => self.walk_lfh(region, valid_base, &bytes, snapshot),
PoolBackend::Vs => {
vs_chunk = self.walk_vs(region, valid_base, &bytes, vs_chunk, snapshot);
}
PoolBackend::Segment => self.walk_page_ranges(region, valid_base, &bytes, snapshot),
PoolBackend::Large => return Ok(()),
}
cursor = valid_end;
}
Ok(())
}
fn read_extent(&self, base: u64, size: usize) -> Result<Vec<u8>, SnapshotError> {
if size <= EXTENT_READ_CHUNK {
return self.memory.read_exact(base, size);
}
let mut bytes = Vec::with_capacity(size);
while bytes.len() < size {
check_budget(self.memory)?;
let take = EXTENT_READ_CHUNK.min(size - bytes.len());
let chunk = self.memory.read_exact(base + bytes.len() as u64, take)?;
if chunk.len() != take {
return Err(SnapshotError::InvalidData {
detail: format!(
"short extent read at {:#x}: asked {take:#x}, got {:#x}",
base + bytes.len() as u64,
chunk.len()
),
});
}
bytes.extend_from_slice(&chunk);
}
Ok(bytes)
}
fn walk_large(&self, region: &PoolRegion, snapshot: &mut PoolSnapshot) {
if region.size == 0 {
return;
}
snapshot.spans.push(
self.base_span(
region,
region.address,
region.address,
region.size as u64,
region.known_tag.unwrap_or(0),
region
.states
.first()
.copied()
.unwrap_or(PoolState::Allocated),
),
);
}
fn base_span(
&self,
region: &PoolRegion,
header: u64,
usable: u64,
size: u64,
tag: u32,
state: PoolState,
) -> PoolSpan {
PoolSpan {
header_address: header,
usable_address: usable,
size,
requested_size: region.requested_size,
raw_tag: tag,
display_tag: super::decode::display_tag(tag),
pool_kind: region.pool_kind,
numa_node: region.numa_node,
heap: region.heap,
subsegment: region.subsegment,
backend: region.backend,
state,
size_class: region.unit_size,
}
}
fn unreadable(
&self,
region: &PoolRegion,
address: u64,
size: u64,
snapshot: &mut PoolSnapshot,
) {
if size != 0 {
snapshot.complete = false;
snapshot.spans.push(self.base_span(
region,
address,
address,
size,
0,
PoolState::Unreadable,
));
}
}
fn walk_special_pool(
&self,
region: &PoolRegion,
base: u64,
bytes: &[u8],
snapshot: &mut PoolSnapshot,
) {
let mut page = base.next_multiple_of(PAGE_SIZE);
while let Some(offset) = page.checked_sub(base).map(|delta| delta as usize) {
if offset >= bytes.len() {
break;
}
let available = (bytes.len() - offset).min(PAGE_SIZE as usize);
let page_bytes = &bytes[offset..offset + available];
let Some(header) = decode_special_pool_header(bytes, offset, region.pool_header) else {
snapshot.diagnostics.push(format!(
"special-pool page {page:#x}: header describes no block the page could \
hold; page skipped"
));
snapshot.complete = false;
let Some(next) = page.checked_add(PAGE_SIZE) else {
break;
};
page = next;
continue;
};
if header.tag != 0 {
let placement = special_pool_placement(page, header, page_bytes);
if placement.approximate {
snapshot.diagnostics.push(format!(
"special-pool page {page:#x} tag `{}`: size not corroborated by the \
fill pattern; reporting the rest of the page as an upper bound",
super::decode::display_tag(header.tag)
));
snapshot.complete = false;
}
let (usable, size) = (placement.usable, placement.size);
snapshot.spans.push(self.base_span(
region,
page,
usable,
size,
header.tag,
PoolState::Allocated,
));
}
let Some(next) = page.checked_add(PAGE_SIZE) else {
break;
};
page = next;
}
}
fn walk_lfh(&self, region: &PoolRegion, base: u64, bytes: &[u8], snapshot: &mut PoolSnapshot) {
let unit = region.unit_size as usize;
if unit < region.pool_header.size {
snapshot.diagnostics.push(format!(
"rejecting implausible LFH unit size {} at {base:#x}",
region.unit_size
));
snapshot.complete = false;
return;
}
let slice_offset = base.saturating_sub(region.address) as usize;
let first_slot = slice_offset.div_ceil(unit);
let slice_end = slice_offset.saturating_add(bytes.len());
let mut slot = first_slot;
while let Some(slot_offset) = slot.checked_mul(unit) {
if slot_offset.saturating_add(unit) > slice_end {
break;
}
let offset = slot_offset - slice_offset;
let address = region.address + slot_offset as u64;
if address / PAGE_SIZE != (address + unit as u64 - 1) / PAGE_SIZE {
slot += 1;
continue;
}
let Some(state) = lfh_bitmap_state(®ion.bitmap, slot) else {
snapshot
.diagnostics
.push(format!("truncated LFH bitmap at slot {slot}"));
snapshot.complete = false;
break;
};
let tag = decode_pool_header(bytes, offset, region.pool_header)
.map_or(0, |header| header.tag);
let usable = address + region.pool_header.size as u64;
snapshot.spans.push(self.base_span(
region,
address,
usable,
unit as u64 - region.pool_header.size as u64,
tag,
state,
));
slot += 1;
}
}
fn walk_vs(
&self,
region: &PoolRegion,
base: u64,
bytes: &[u8],
expected: Option<u64>,
snapshot: &mut PoolSnapshot,
) -> Option<u64> {
let extent_end = base.saturating_add(bytes.len() as u64);
let Some(next) = expected.filter(|next| *next >= base) else {
snapshot.unplaced_bytes = snapshot.unplaced_bytes.saturating_add(bytes.len() as u64);
snapshot.diagnostics.push(match expected {
Some(next) => format!(
"VS extent at {base:#x} does not begin on a chunk boundary: the chunk chain \
names {next:#x}, {:#x} bytes back inside unreadable memory; {:#x} bytes not \
decoded",
base - next,
bytes.len()
),
None => format!(
"VS extent at {base:#x} cannot be placed: the chain was already lost earlier \
in this region; {:#x} bytes not decoded",
bytes.len()
),
});
snapshot.complete = false;
return None;
};
if next >= extent_end {
return Some(next);
}
let mut offset = (next - base) as usize;
let mut resume = next;
let mut lost = false;
let mut chunks = 0usize;
let header_bytes = region.vs_header_size + region.pool_header.size;
let subsegment_end = region.address.saturating_add(region.size as u64);
let mut refused = 0u64;
let mut resync_from = None;
let mut chain_breaks = 0u64;
let mut previous_chunk = None;
while offset
.saturating_add(region.vs_header_size)
.saturating_add(region.pool_header.size)
<= bytes.len()
&& chunks < self.traversal_limit
{
let header_address = base + offset as u64;
let Some(encoded) = read_u64(bytes, offset + region.vs_sizes_offset) else {
break;
};
let chunk = match decode_vs_chunk(
encoded,
header_address,
region.heap_key,
header_bytes,
subsegment_end,
) {
Ok(chunk) => chunk,
Err(rejection) => {
if refused == 0 {
snapshot.diagnostics.push(format!(
"refusing VS chunk at {header_address:#x}: {rejection}"
));
}
refused += 1;
resync_from.get_or_insert(header_address);
previous_chunk = None;
lost = true;
snapshot.complete = false;
offset = offset.saturating_add(16);
continue;
}
};
if let Some(expected) = previous_chunk
&& chunk.previous_size != expected
{
if chain_breaks == 0 {
snapshot.diagnostics.push(format!(
"VS chunk at {header_address:#x} records a previous size of \
{:#x} where the chunk before it measured {expected:#x}",
chunk.previous_size
));
}
chain_breaks += 1;
snapshot.complete = false;
}
let chunk_size = chunk.size;
if offset.saturating_add(chunk_size) > bytes.len() {
resume = header_address.saturating_add(chunk_size as u64);
snapshot.complete = false;
break;
}
let candidate = header_address + region.vs_header_size as u64;
let physical_header = if region.pool_header.size == 0 {
candidate
} else {
let Some(header) =
adjust_page_end_header(candidate, region.pool_header.size as u64)
else {
resume = header_address.saturating_add(chunk_size as u64);
snapshot.complete = false;
break;
};
header
};
let pool_offset = physical_header.saturating_sub(base) as usize;
let tag = decode_pool_header(bytes, pool_offset, region.pool_header)
.map_or(0, |header| header.tag);
let state = if region.cached_chunks.contains(&header_address) {
PoolState::CachedFree
} else if region.reusable_chunks.contains(&header_address) {
PoolState::ReusableFree
} else if chunk.allocated {
PoolState::Allocated
} else {
PoolState::ReusableFree
};
let overhead = physical_header
.saturating_sub(header_address)
.saturating_add(region.pool_header.size as u64);
let span_header = if region.pool_header.size == 0 {
header_address
} else {
physical_header
};
let mut span = self.base_span(
region,
span_header,
physical_header + region.pool_header.size as u64,
(chunk_size as u64).saturating_sub(overhead),
tag,
state,
);
span.size_class = chunk_size.min(u32::MAX as usize) as u32;
snapshot.spans.push(span);
previous_chunk = Some(chunk_size);
offset += chunk_size;
resume = base + offset as u64;
chunks += 1;
}
if let Some(from) = resync_from {
snapshot.refused_chunks = snapshot.refused_chunks.saturating_add(refused);
snapshot.diagnostics.push(format!(
"{refused} VS chunk headers refused, resynchronising from {from:#x}"
));
}
if chain_breaks > 0 {
snapshot.diagnostics.push(format!(
"{chain_breaks} VS chunks disagreed with the size of the chunk before them, \
from {base:#x}"
));
}
if chunks >= self.traversal_limit {
snapshot.complete = false;
snapshot
.diagnostics
.push(format!("VS traversal limit reached at {base:#x}"));
}
(!lost).then_some(resume)
}
fn walk_page_ranges(
&self,
region: &PoolRegion,
base: u64,
bytes: &[u8],
snapshot: &mut PoolSnapshot,
) {
let unit = region.unit_size.max(1) as usize;
let slice_offset = base.saturating_sub(region.address) as usize;
let first_slot = slice_offset.div_ceil(unit);
let slice_end = slice_offset.saturating_add(bytes.len());
let mut slot = first_slot;
while let Some(slot_offset) = slot.checked_mul(unit) {
if slot_offset >= slice_end {
break;
}
let offset = slot_offset - slice_offset;
let remaining = bytes.len() - offset;
let size = unit.min(remaining);
let state = region
.states
.get(slot)
.copied()
.or_else(|| region.states.first().copied())
.unwrap_or(PoolState::Unreadable);
let tag = region.known_tag.or_else(|| {
decode_pool_header(bytes, offset, region.pool_header).map(|header| header.tag)
});
let address = region.address + slot_offset as u64;
let header_size = region.pool_header.size.min(size);
snapshot.spans.push(self.base_span(
region,
address,
address + header_size as u64,
size.saturating_sub(header_size) as u64,
tag.unwrap_or(0),
state,
));
slot += 1;
}
}
#[cfg(test)]
pub(crate) fn lookup_big_page(
&self,
table: &[u8],
entry_size: usize,
address: u64,
) -> Option<(u32, u64)> {
if entry_size < 20 || !table.len().is_multiple_of(entry_size) {
return None;
}
let count = table.len() / entry_size;
for index in big_page_probe(address, count)? {
let offset = index * entry_size;
let candidate = read_u64(table, offset)?;
if candidate == 0 {
break;
}
if candidate & !1 == address {
return Some((read_u32(table, offset + 8)?, read_u64(table, offset + 12)?));
}
}
None
}
}
pub(crate) fn walk_user_segment_heaps<M: PoolMemory>(
memory: &M,
layout: &PoolLayout,
peb: u64,
heaps: &[u64],
budget: Option<Duration>,
traversal_limit: usize,
) -> Result<PoolSnapshot, SnapshotError> {
let (discovery_deadline, walk_deadline) = budget_deadlines(Instant::now(), budget);
let discovery_clock = Budgeted::new(memory, discovery_deadline);
let globals_address = *layout
.globals
.get("RtlpHpHeapGlobals")
.ok_or_else(|| missing_layout("RtlpHpHeapGlobals"))?;
let heap_key = scalar(
&discovery_clock,
globals_address + layout.field("_RTLP_HP_HEAP_GLOBALS", "HeapKey")? as u64,
8,
)?;
let lfh_key = scalar(
&discovery_clock,
globals_address + layout.field("_RTLP_HP_HEAP_GLOBALS", "LfhKey")? as u64,
8,
)?;
let mut discovery = Discovery::default();
let mut expired = false;
for &heap in heaps {
let identity = HeapIdentity {
pool_state: peb,
heap,
special: false,
};
match discover_heap_regions(
&discovery_clock,
layout,
heap,
0,
PoolKind::NonPagedNx,
identity,
None,
heap_key,
lfh_key,
traversal_limit,
&mut discovery,
) {
Ok(()) => {}
Err(SnapshotError::BudgetExpired) => {
expired = true;
break;
}
Err(error) if error.halts_walk() => return Err(error),
Err(error) => discovery
.diagnostics
.push(format!("cannot fully discover heap {heap:#x}: {error}")),
}
}
let mut snapshot = PoolSnapshot {
complete: discovery.diagnostics.is_empty(),
diagnostics: PoolDiagnostics::from_iter(std::mem::take(&mut discovery.diagnostics)),
..PoolSnapshot::default()
};
let walk_clock = Budgeted::new(memory, walk_deadline);
let walker = SnapshotWalker {
memory: &walk_clock,
layout,
traversal_limit,
};
walker.walk_discovered_regions(discovery.regions, budget, expired, &mut snapshot)?;
Ok(snapshot)
}
#[cfg(test)]
mod tests {
use std::{cell::Cell, collections::HashMap};
use super::*;
fn lfh_region(address: u64, unit_size: u32) -> PoolRegion {
PoolRegion {
address,
size: 0x1000,
requested_size: None,
pool_kind: PoolKind::NonPagedNx,
numa_node: 0,
heap: HeapIdentity {
pool_state: 0,
heap: 0,
special: false,
},
subsegment: None,
backend: PoolBackend::Lfh,
unit_size,
bitmap: vec![0xff],
heap_key: 0,
pool_header: PoolHeaderLayout {
size: 0x10,
previous_size: 0,
pool_index: 1,
block_size: 2,
pool_type: 3,
tag: 4,
},
vs_header_size: 0,
vs_sizes_offset: 0,
known_tag: None,
states: Vec::new(),
reusable_chunks: Arc::default(),
cached_chunks: Arc::default(),
}
}
#[test]
fn test_page_straddling_lfh_slot_is_skipped_without_complaint() {
let region = lfh_region(0x1f80, 0x100);
let memory = FlatMemory::new(0x1f80, 0x200);
let layout = vs_layout(false);
let walker = SnapshotWalker {
memory: &memory,
layout: &layout,
traversal_limit: 1000,
};
let mut snapshot = PoolSnapshot {
complete: true,
..PoolSnapshot::default()
};
walker.walk_lfh(®ion, 0x1f80, &memory.bytes, &mut snapshot);
assert_eq!(snapshot.spans.len(), 1);
assert_eq!(snapshot.spans[0].header_address, 0x2080);
assert!(
snapshot.diagnostics.is_empty(),
"normal layout must not be reported: {:?}",
snapshot.diagnostics
);
assert!(
snapshot.complete,
"a straddling slot is expected layout and must not mark the snapshot incomplete"
);
}
#[test]
fn test_user_regions_never_report_payload_bytes_as_pool_tags() {
let no_pool_header = PoolHeaderLayout {
size: 0,
previous_size: 0,
pool_index: 0,
block_size: 0,
pool_type: 0,
tag: 0,
};
let memory = FlatMemory::new(0x1000, 0x1000);
let layout = vs_layout(false);
let walker = SnapshotWalker {
memory: &memory,
layout: &layout,
traversal_limit: 1000,
};
let mut snapshot = PoolSnapshot {
complete: true,
..PoolSnapshot::default()
};
let mut lfh = lfh_region(0x1000, 0x20);
lfh.size = 0x20;
lfh.pool_header = no_pool_header;
let mut lfh_bytes = vec![0; 0x20];
lfh_bytes[..4].copy_from_slice(b"LFH!");
walker.walk_lfh(&lfh, lfh.address, &lfh_bytes, &mut snapshot);
let mut vs = vs_region(0x40);
vs.pool_header = no_pool_header;
let mut vs_bytes = vs_extent(&[(0x40, 0)]);
vs_bytes[0x10..0x14].copy_from_slice(b"VS!!");
walker.walk_vs(&vs, vs.address, &vs_bytes, Some(vs.address), &mut snapshot);
let mut page = lfh_region(0x3000, 0x20);
page.size = 0x20;
page.backend = PoolBackend::Segment;
page.pool_header = no_pool_header;
page.states = vec![PoolState::Allocated];
let mut page_bytes = vec![0; 0x20];
page_bytes[..4].copy_from_slice(b"PAGE");
walker.walk_page_ranges(&page, page.address, &page_bytes, &mut snapshot);
assert_eq!(snapshot.spans.len(), 3);
assert!(
snapshot.spans.iter().all(|span| span.raw_tag == 0),
"user payload bytes must not be decoded as kernel pool tags: {:?}",
snapshot.spans
);
}
struct FlatMemory {
base: u64,
bytes: Vec<u8>,
}
impl FlatMemory {
fn new(base: u64, len: usize) -> Self {
Self {
base,
bytes: vec![0; len],
}
}
fn put(&mut self, address: u64, data: &[u8]) {
let offset = (address - self.base) as usize;
self.bytes[offset..offset + data.len()].copy_from_slice(data);
}
fn put_u16(&mut self, address: u64, value: u16) {
self.put(address, &value.to_le_bytes());
}
fn put_u64(&mut self, address: u64, value: u64) {
self.put(address, &value.to_le_bytes());
}
}
impl PoolMemory for FlatMemory {
fn read_exact(&self, address: u64, size: usize) -> Result<Vec<u8>, SnapshotError> {
let offset = address
.checked_sub(self.base)
.and_then(|offset| usize::try_from(offset).ok())
.ok_or_else(|| SnapshotError::InvalidData {
detail: format!("read below the fixture at {address:#x}"),
})?;
self.bytes
.get(offset..offset + size)
.map(<[u8]>::to_vec)
.ok_or_else(|| SnapshotError::InvalidData {
detail: format!("read past the fixture at {address:#x}+{size:#x}"),
})
}
fn valid_region(&self, address: u64, size: usize) -> Result<(u64, usize), SnapshotError> {
Ok((address, size))
}
fn interrupted(&self) -> Result<bool, SnapshotError> {
Ok(false)
}
}
const VS_CONTEXT: u64 = 0x1000;
fn vs_layout(affinity: bool) -> PoolLayout {
let mut types = HashMap::new();
types.insert(
"_HEAP_VS_CONTEXT",
if affinity {
type_layout(0x60, &[("SlotMapRef", 0), ("AffinityMask", 2)])
} else {
type_layout(0x80, &[("FreeChunkTree", 0x10), ("DelayFreeContext", 0x30)])
},
);
if affinity {
types.insert(
"_HEAP_VS_AFFINITY_SLOT",
type_layout(
0x80,
&[
("VsContext", 0),
("FreeChunkTree", 0x10),
("DelayFreeContext", 0x40),
],
),
);
types.insert("_HEAP_VS_SLOT_MAP", type_layout(4, &[("SlotRef", 0)]));
}
PoolLayout {
key: crate::pool::layout::LayoutKey {
image: crate::dbgeng::KernelImage::default(),
session: 1,
},
globals: HashMap::new(),
types,
}
}
fn affinity_fixture() -> FlatMemory {
let mut memory = FlatMemory::new(VS_CONTEXT, 0x1200);
memory.put_u16(VS_CONTEXT, 0x10); memory.put(VS_CONTEXT + 2, &[3]); let map = VS_CONTEXT + 0x400;
for (index, slot_ref) in [0x20u16, 0x30, 0x20, 0x40].into_iter().enumerate() {
memory.put_u16(map + index as u64 * 4, slot_ref);
}
memory.put_u64(VS_CONTEXT + 0x800, VS_CONTEXT); memory.put_u64(VS_CONTEXT + 0xc00, VS_CONTEXT); memory.put_u64(VS_CONTEXT + 0x1000, 0xdead_beef); memory
}
#[test]
fn test_vs_roots_polls_the_budget_between_slot_map_entries() {
let memory = Impatient::over(affinity_fixture(), 2);
let outcome = vs_roots(&memory, &vs_layout(true), VS_CONTEXT, &mut Vec::new());
assert!(
matches!(outcome, Err(SnapshotError::BudgetExpired)),
"the slot-map loop read on past the deadline, resolving {} roots",
outcome.map_or(0, |roots| roots.len())
);
}
#[test]
fn test_vs_roots_reads_the_legacy_in_context_shape() {
let memory = FlatMemory::new(VS_CONTEXT, 0x100);
let mut diagnostics = Vec::new();
let roots = vs_roots(&memory, &vs_layout(false), VS_CONTEXT, &mut diagnostics).unwrap();
assert_eq!(roots.len(), 1);
assert_eq!(roots[0].base, VS_CONTEXT);
assert_eq!(roots[0].tree_offset, 0x10);
assert_eq!(roots[0].delay_offset, Some(0x30));
assert!(diagnostics.is_empty());
}
#[test]
fn test_vs_roots_walks_the_affinity_slots_and_dedups_them() {
let memory = affinity_fixture();
let mut diagnostics = Vec::new();
let roots = vs_roots(&memory, &vs_layout(true), VS_CONTEXT, &mut diagnostics).unwrap();
let bases: Vec<u64> = roots.iter().map(|root| root.base).collect();
assert_eq!(bases, vec![VS_CONTEXT + 0x800, VS_CONTEXT + 0xc00]);
assert!(roots.iter().all(|root| root.tree_offset == 0x10));
assert!(roots.iter().all(|root| root.delay_offset == Some(0x40)));
}
#[test]
fn test_vs_roots_rejects_a_slot_whose_back_pointer_disagrees() {
let memory = affinity_fixture();
let mut diagnostics = Vec::new();
let roots = vs_roots(&memory, &vs_layout(true), VS_CONTEXT, &mut diagnostics).unwrap();
assert!(roots.iter().all(|root| root.base != VS_CONTEXT + 0x1000));
assert_eq!(diagnostics.len(), 1);
assert!(diagnostics[0].contains("claims context"));
}
#[test]
fn test_vs_roots_refuses_an_implausible_slot_map() {
let mut memory = affinity_fixture();
memory.put_u16(VS_CONTEXT, 0);
let mut diagnostics = Vec::new();
let roots = vs_roots(&memory, &vs_layout(true), VS_CONTEXT, &mut diagnostics).unwrap();
assert!(roots.is_empty());
assert_eq!(diagnostics.len(), 1);
assert!(diagnostics[0].contains("implausible VS slot map"));
}
#[test]
fn test_vs_roots_reports_when_neither_shape_resolves() {
let memory = FlatMemory::new(VS_CONTEXT, 0x100);
let layout = PoolLayout {
key: crate::pool::layout::LayoutKey {
image: crate::dbgeng::KernelImage::default(),
session: 1,
},
globals: HashMap::new(),
types: HashMap::new(),
};
let mut diagnostics = Vec::new();
let roots = vs_roots(&memory, &layout, VS_CONTEXT, &mut diagnostics).unwrap();
assert!(roots.is_empty());
assert_eq!(diagnostics.len(), 1);
assert!(diagnostics[0].contains("neither the context nor an affinity slot"));
}
const SPECIAL_PAGE: u64 = 0xffff_8c8f_13a0_2000;
const VERIFIER_FILL: u8 = 0xfd;
const X64_POOL_HEADER: PoolHeaderLayout = PoolHeaderLayout {
size: 0x10,
previous_size: 0,
pool_index: 0,
block_size: 2,
pool_type: 2,
tag: 4,
};
fn placed(placement: SpecialPlacement) -> (u64, u64, bool) {
(placement.usable, placement.size, placement.approximate)
}
fn special_page(requested: u32, tracked: bool) -> Vec<u8> {
let mut page = vec![VERIFIER_FILL; PAGE_SIZE as usize];
let word = requested | if tracked { 0x4000 } else { 0 } | (u32::from(VERIFIER_FILL) << 16);
page[..4].copy_from_slice(&word.to_le_bytes());
page[4..8].copy_from_slice(b"Tsp1");
let start = PAGE_SIZE as usize - (requested as usize).next_multiple_of(16);
page[start..start + requested as usize].fill(0x41);
page
}
fn place(page: &[u8]) -> SpecialPlacement {
let header =
decode_special_pool_header(page, 0, X64_POOL_HEADER).expect("page describes a block");
special_pool_placement(SPECIAL_PAGE, header, page)
}
#[test]
fn test_special_pool_block_is_pushed_against_the_page_end() {
assert_eq!(
placed(place(&special_page(0x68, false))),
(SPECIAL_PAGE + 0xf90, 0x68, false)
);
assert_eq!(
placed(place(&special_page(0x40, false))),
(SPECIAL_PAGE + 0xfc0, 0x40, false)
);
}
#[test]
fn test_special_pool_reads_a_size_that_does_not_fit_eight_bits() {
let page = special_page(0x140, false);
assert_eq!(
page[0], 0x40,
"the low byte alone is the value that used to be read"
);
assert_eq!(
placed(place(&page)),
(SPECIAL_PAGE + 0xec0, 0x140, false),
"the block is 0x140 bytes at page+0xec0, not 0x40 bytes at page+0xfc0"
);
assert_eq!(
placed(place(&special_page(0xff0, false))),
(SPECIAL_PAGE + 0x10, 0xff0, false)
);
}
#[test]
fn test_special_pool_skips_verifiers_tracking_block() {
let mut page = special_page(0x68, true);
page[0x10..0x18].copy_from_slice(&0xdead_beef_u64.to_le_bytes());
let header = decode_special_pool_header(&page, 0, X64_POOL_HEADER).unwrap();
assert_eq!(header.header_size, 0x18);
assert_eq!(header.requested, 0x68);
assert_eq!(header.tag, u32::from_le_bytes(*b"Tsp1"));
assert_eq!(
placed(special_pool_placement(SPECIAL_PAGE, header, &page)),
(SPECIAL_PAGE + 0xf90, 0x68, false)
);
}
#[test]
fn test_special_pool_checks_the_padding_behind_the_block() {
let mut page = special_page(0x68, false);
page[PAGE_SIZE as usize - 1] = 0x41;
assert_eq!(
placed(place(&page)),
(SPECIAL_PAGE + 0x10, PAGE_SIZE - 0x10, true)
);
}
#[test]
fn test_special_pool_uses_the_fill_byte_the_page_records() {
let mut page = special_page(0x68, false);
page[0x10..0xf90].fill(0xc0);
page[0xff8..].fill(0xc0);
page[2] = 0xc0;
assert_eq!(
placed(place(&page)),
(SPECIAL_PAGE + 0xf90, 0x68, false),
"0xfd is Verifier's default, not part of the format"
);
page[2] = VERIFIER_FILL;
assert_eq!(
placed(place(&page)),
(SPECIAL_PAGE + 0x10, PAGE_SIZE - 0x10, true)
);
}
#[test]
fn test_special_pool_declines_a_start_aligned_page() {
let mut page = special_page(0x68, false);
page[0xf90..].fill(VERIFIER_FILL);
page[0x10..0x78].fill(0x41); assert_eq!(
placed(place(&page)),
(SPECIAL_PAGE + 0x10, PAGE_SIZE - 0x10, true)
);
}
#[test]
fn test_special_pool_header_refuses_an_impossible_block() {
let mut page = special_page(0x68, false);
page[..4].copy_from_slice(&(u32::from(VERIFIER_FILL) << 16).to_le_bytes());
assert!(decode_special_pool_header(&page, 0, X64_POOL_HEADER).is_none());
page[..4].copy_from_slice(&(0x1000 | (u32::from(VERIFIER_FILL) << 16)).to_le_bytes());
assert!(decode_special_pool_header(&page, 0, X64_POOL_HEADER).is_none());
page[..4]
.copy_from_slice(&(0xfe8 | 0x4000 | (u32::from(VERIFIER_FILL) << 16)).to_le_bytes());
assert!(decode_special_pool_header(&page, 0, X64_POOL_HEADER).is_none());
}
#[test]
fn test_special_pool_fallback_respects_the_readable_length() {
let mut partial = special_page(0x68, false);
partial.truncate(0x200);
assert_eq!(placed(place(&partial)), (SPECIAL_PAGE + 0x10, 0x1f0, true));
}
fn special_region(address: u64, pages: usize) -> PoolRegion {
PoolRegion {
address,
size: pages * PAGE_SIZE as usize,
requested_size: None,
pool_kind: PoolKind::SpecialNonPagedNx,
numa_node: 0,
heap: HeapIdentity {
pool_state: 0,
heap: 0,
special: true,
},
subsegment: None,
backend: PoolBackend::Segment,
unit_size: PAGE_SIZE as u32,
bitmap: Vec::new(),
heap_key: 0,
pool_header: X64_POOL_HEADER,
vs_header_size: 0,
vs_sizes_offset: 0,
known_tag: None,
states: Vec::new(),
reusable_chunks: Arc::default(),
cached_chunks: Arc::default(),
}
}
const VS_BASE: u64 = 0x2000;
const VS_HEAP_KEY: u64 = 0x1234_5678_9abc_def0;
fn vs_region(size: usize) -> PoolRegion {
PoolRegion {
address: VS_BASE,
size,
requested_size: None,
pool_kind: PoolKind::NonPagedNx,
numa_node: 0,
heap: HeapIdentity {
pool_state: 0,
heap: 0,
special: false,
},
subsegment: Some(VS_BASE),
backend: PoolBackend::Vs,
unit_size: 0,
bitmap: Vec::new(),
heap_key: VS_HEAP_KEY,
pool_header: X64_POOL_HEADER,
vs_header_size: 0x10,
vs_sizes_offset: 0,
known_tag: None,
states: Vec::new(),
reusable_chunks: Arc::default(),
cached_chunks: Arc::default(),
}
}
fn vs_extent(chunks: &[(usize, usize)]) -> Vec<u8> {
let mut bytes = vec![0u8; chunks.iter().map(|(size, _)| size).sum()];
let mut offset = 0;
for &(size, previous) in chunks {
let decoded =
((size as u64 / 16) << 16) | ((previous as u64 / 16) << 32) | (1u64 << 48);
let encoded = decoded ^ VS_HEAP_KEY ^ (VS_BASE + offset as u64);
bytes[offset..offset + 8].copy_from_slice(&encoded.to_le_bytes());
let pool = offset + 0x10;
bytes[pool..pool + 4].copy_from_slice(&0x0001_0000u32.to_le_bytes());
bytes[pool + 4..pool + 8].copy_from_slice(b"VS!!");
offset += size;
}
bytes
}
fn walk_vs_extent(bytes: &[u8]) -> PoolSnapshot {
let region = vs_region(bytes.len());
let memory = FlatMemory::new(VS_BASE, bytes.len());
let layout = vs_layout(false);
let walker = SnapshotWalker {
memory: &memory,
layout: &layout,
traversal_limit: 1000,
};
let mut snapshot = PoolSnapshot {
complete: true,
..PoolSnapshot::default()
};
walker.walk_vs(®ion, VS_BASE, bytes, Some(VS_BASE), &mut snapshot);
snapshot
}
#[test]
fn test_vs_refusals_are_counted_as_chunks_not_as_extents() {
let mut bytes = vs_extent(&[(0x40, 0), (0x40, 0x40), (0x40, 0x40)]);
bytes.extend_from_slice(&[0u8; 0x40]); let snapshot = walk_vs_extent(&bytes);
assert_eq!(snapshot.spans.len(), 3);
assert_eq!(snapshot.refused_chunks, 3);
assert!(!snapshot.complete);
let examples = snapshot.diagnostics.examples();
assert_eq!(
examples
.iter()
.filter(|message| message.starts_with("refusing VS chunk at"))
.count(),
1,
"the detail is a sample, not one line per refusal: {examples:?}"
);
assert!(
examples.iter().any(|message| message
== "3 VS chunk headers refused, resynchronising from 0x20c0"),
"{examples:?}"
);
}
#[test]
fn test_vs_says_which_check_a_chunk_failed() {
let refuse = |size: usize, end: u64| {
let decoded = (size as u64 / 16) << 16;
decode_vs_chunk(
decoded ^ VS_HEAP_KEY ^ VS_BASE,
VS_BASE,
VS_HEAP_KEY,
0x20,
end,
)
.unwrap_err()
};
assert_eq!(refuse(0, 0x3000).reason, "the size word decodes to zero");
assert_eq!(
refuse(0x10, 0x3000).reason,
"the chunk is smaller than its own headers"
);
assert_eq!(
refuse(0x100, 0x2080).reason,
"the chunk runs past the end of its subsegment"
);
assert_eq!(
refuse(0x10, 0x3000).encoded,
(1u64 << 16) ^ VS_HEAP_KEY ^ VS_BASE
);
assert_eq!(
decode_vs_chunk(
((0x80u64 / 16) << 16) ^ VS_HEAP_KEY ^ VS_BASE,
VS_BASE,
VS_HEAP_KEY,
0x20,
VS_BASE + 0x80,
)
.unwrap()
.size,
0x80
);
}
#[test]
fn test_vs_reports_a_chunk_that_disagrees_with_its_predecessor() {
let snapshot = walk_vs_extent(&vs_extent(&[(0x40, 0), (0x40, 0x40), (0x40, 0x20)]));
assert_eq!(
snapshot.spans.len(),
3,
"the chunk itself is still plausible"
);
assert_eq!(snapshot.refused_chunks, 0);
assert!(!snapshot.complete);
let examples = snapshot.diagnostics.examples();
assert!(
examples.iter().any(|message| message
== "VS chunk at 0x2080 records a previous size of 0x20 where the chunk before it measured 0x40"),
"{examples:?}"
);
assert!(
examples
.iter()
.any(|message| message.starts_with("1 VS chunks disagreed")),
"{examples:?}"
);
let quiet = walk_vs_extent(&vs_extent(&[(0x40, 0), (0x40, 0x40), (0x40, 0x40)]));
assert!(quiet.complete);
assert!(quiet.diagnostics.is_empty(), "{:?}", quiet.diagnostics);
}
struct HoleyMemory {
base: u64,
bytes: Vec<u8>,
holes: HashSet<u64>,
stalls: HashSet<u64>,
blind: HashSet<u64>,
queries: Cell<usize>,
}
impl HoleyMemory {
fn new(base: u64, bytes: Vec<u8>) -> Self {
Self {
base,
bytes,
holes: HashSet::new(),
stalls: HashSet::new(),
blind: HashSet::new(),
queries: Cell::new(0),
}
}
fn end(&self) -> u64 {
self.base + self.bytes.len() as u64
}
fn committed(&self, page: u64) -> bool {
page >= self.base
&& page < self.end()
&& !self.holes.contains(&page)
&& !self.stalls.contains(&page)
&& !self.blind.contains(&page)
}
}
impl PoolMemory for HoleyMemory {
fn read_exact(&self, address: u64, size: usize) -> Result<Vec<u8>, SnapshotError> {
let offset = (address - self.base) as usize;
self.bytes
.get(offset..offset + size)
.map(<[u8]>::to_vec)
.ok_or_else(|| SnapshotError::InvalidData {
detail: format!("read past the fixture at {address:#x}+{size:#x}"),
})
}
fn valid_region(&self, address: u64, size: usize) -> Result<(u64, usize), SnapshotError> {
self.queries.set(self.queries.get() + 1);
let page = address & !(PAGE_SIZE - 1);
if self.blind.contains(&page) {
return Ok((0, 0));
}
if self.stalls.contains(&page) {
return Ok((address, 0));
}
let mut next = page;
while next < self.end() && !self.committed(next) {
next += PAGE_SIZE;
}
if next >= self.end() {
return Ok((self.end(), 0));
}
let base = next.max(address);
let mut run = next;
while self.committed(run) {
run += PAGE_SIZE;
}
Ok((base, (run.min(address + size as u64) - base) as usize))
}
fn interrupted(&self) -> Result<bool, SnapshotError> {
Ok(false)
}
}
fn special_pages(count: usize) -> Vec<u8> {
let mut bytes = Vec::new();
for _ in 0..count {
bytes.extend_from_slice(&special_page(0x68, false));
}
bytes
}
fn walk_holey(memory: &HoleyMemory, region: &PoolRegion) -> PoolSnapshot {
let layout = vs_layout(false);
let walker = SnapshotWalker {
memory,
layout: &layout,
traversal_limit: 1000,
};
let mut snapshot = PoolSnapshot {
complete: true,
..PoolSnapshot::default()
};
walker.walk_region(region, &mut snapshot).unwrap();
snapshot
}
#[test]
fn test_a_stalled_query_costs_a_page_not_the_region() {
let mut memory = HoleyMemory::new(SPECIAL_PAGE, special_pages(4));
memory.stalls.insert(SPECIAL_PAGE + 0x1000);
let snapshot = walk_holey(&memory, &special_region(SPECIAL_PAGE, 4));
let allocated: Vec<_> = snapshot
.spans
.iter()
.filter(|span| span.state == PoolState::Allocated)
.map(|span| span.usable_address)
.collect();
assert_eq!(
allocated,
[
SPECIAL_PAGE + 0xf90,
SPECIAL_PAGE + 0x2f90,
SPECIAL_PAGE + 0x3f90
],
"the pages behind the stalled one must still be walked"
);
assert!(
snapshot
.spans
.iter()
.any(|span| span.state == PoolState::Unreadable
&& span.header_address == SPECIAL_PAGE + 0x1000
&& span.size == PAGE_SIZE),
"the page that stalled is still filed as unreadable"
);
assert!(!snapshot.complete);
assert_eq!(
snapshot.stalls,
WalkStalls {
pages: 1,
skipped_bytes: PAGE_SIZE,
recovered_bytes: 2 * PAGE_SIZE,
}
);
}
#[test]
fn test_a_stall_part_way_through_a_page_does_not_swallow_the_next_one() {
let mut memory = HoleyMemory::new(SPECIAL_PAGE, special_pages(3));
memory.stalls.insert(SPECIAL_PAGE);
let mut region = special_region(SPECIAL_PAGE, 3);
region.address = SPECIAL_PAGE + 0xfd0;
region.size = 3 * PAGE_SIZE as usize - 0xfd0;
let snapshot = walk_holey(&memory, ®ion);
assert_eq!(
snapshot.stalls.skipped_bytes, 0x30,
"only the rest of the page that stalled is written off"
);
let allocated: Vec<_> = snapshot
.spans
.iter()
.filter(|span| span.state == PoolState::Allocated)
.map(|span| span.usable_address)
.collect();
assert_eq!(
allocated,
[SPECIAL_PAGE + 0x1f90, SPECIAL_PAGE + 0x2f90],
"the page immediately after the stall is healthy and must still be walked"
);
}
fn walk_vs_with_holes(chunks: &[(usize, usize)], holes: &[u64]) -> PoolSnapshot {
let bytes = vs_extent(chunks);
let region = vs_region(bytes.len());
let mut memory = HoleyMemory::new(VS_BASE, bytes);
memory.holes.extend(holes.iter().copied());
walk_holey(&memory, ®ion)
}
#[test]
fn test_a_vs_extent_after_a_hole_resumes_on_the_chunk_the_chain_names() {
let snapshot = walk_vs_with_holes(
&[
(0x800, 0),
(0x800, 0x800),
(0x2800, 0x800),
(0x800, 0x2800),
],
&[VS_BASE + 0x2000],
);
assert_eq!(
snapshot.refused_chunks,
0,
"the chain names the header, so nothing has to be scanned for: {:?}",
snapshot.diagnostics.examples()
);
assert_eq!(snapshot.unplaced_bytes, 0);
let allocated: Vec<_> = snapshot
.spans
.iter()
.filter(|span| span.state == PoolState::Allocated)
.map(|span| span.header_address)
.collect();
assert_eq!(
allocated,
[VS_BASE + 0x10, VS_BASE + 0x810, VS_BASE + 0x3810],
"the chunk on the far side of the hole is found, and nothing else is invented"
);
}
#[test]
fn test_a_vs_extent_whose_chunk_boundary_fell_in_the_hole_is_not_guessed_at() {
let snapshot = walk_vs_with_holes(
&[
(0x1000, 0),
(0x1000, 0x1000),
(0x1000, 0x1000),
(0x1000, 0x1000),
],
&[VS_BASE + 0x2000],
);
assert_eq!(snapshot.refused_chunks, 0);
assert_eq!(
snapshot.unplaced_bytes, 0x1000,
"the whole undecodable extent is sized, not just noted"
);
assert!(
snapshot
.diagnostics
.examples()
.iter()
.any(|message| message.contains("does not begin on a chunk boundary")),
"{:?}",
snapshot.diagnostics.examples()
);
let allocated: Vec<_> = snapshot
.spans
.iter()
.filter(|span| span.state == PoolState::Allocated)
.map(|span| span.header_address)
.collect();
assert_eq!(
allocated,
[VS_BASE + 0x10, VS_BASE + 0x1010],
"nothing from the extent the walk could not place reaches the snapshot"
);
}
#[test]
fn test_an_extent_inside_a_single_chunk_costs_nothing() {
let snapshot = walk_vs_with_holes(&[(0x1000, 0), (0x3000, 0x1000)], &[VS_BASE + 0x2000]);
assert_eq!(snapshot.unplaced_bytes, 0);
assert_eq!(snapshot.refused_chunks, 0);
assert!(
!snapshot
.diagnostics
.examples()
.iter()
.any(|message| message.contains("does not begin on a chunk boundary")),
"{:?}",
snapshot.diagnostics.examples()
);
}
#[test]
fn test_a_query_that_finds_nothing_ends_the_region_rather_than_stepping() {
let mut memory = HoleyMemory::new(SPECIAL_PAGE, special_pages(8));
for page in 4..8 {
memory.blind.insert(SPECIAL_PAGE + page * PAGE_SIZE);
}
let snapshot = walk_holey(&memory, &special_region(SPECIAL_PAGE, 8));
assert_eq!(
memory.queries.get(),
2,
"one query to walk the committed run, one to be told nothing follows it — a page \
step would have cost four more and learnt nothing"
);
assert_eq!(
snapshot.stalls,
WalkStalls::default(),
"nothing was stepped over, so nothing is billed as stepped over"
);
assert!(
!snapshot
.diagnostics
.examples()
.iter()
.any(|message| message.contains("made no progress")),
"a region running out of readable content is not a fault: {:?}",
snapshot.diagnostics.examples()
);
assert!(!snapshot.complete);
let unreadable: u64 = snapshot
.spans
.iter()
.filter(|span| span.state == PoolState::Unreadable)
.map(|span| span.size)
.sum();
assert_eq!(unreadable, 4 * PAGE_SIZE);
let allocated = snapshot
.spans
.iter()
.filter(|span| span.state == PoolState::Allocated)
.count();
assert_eq!(
allocated, 4,
"the readable pages in front of it are still walked"
);
}
#[test]
fn test_a_region_that_never_advances_is_given_up_on() {
let pages = 4096;
let mut memory = HoleyMemory::new(SPECIAL_PAGE, vec![0u8; pages * PAGE_SIZE as usize]);
for page in 0..pages as u64 {
memory.stalls.insert(SPECIAL_PAGE + page * PAGE_SIZE);
}
let snapshot = walk_holey(&memory, &special_region(SPECIAL_PAGE, pages));
assert_eq!(
memory.queries.get(),
MAX_CONSECUTIVE_STALLS as usize,
"a dead region costs the limit in queries, and not one more"
);
assert!(!snapshot.complete);
assert_eq!(snapshot.stalls.pages, u64::from(MAX_CONSECUTIVE_STALLS));
let unreadable: u64 = snapshot
.spans
.iter()
.filter(|span| span.state == PoolState::Unreadable)
.map(|span| span.size)
.sum();
assert_eq!(unreadable, pages as u64 * PAGE_SIZE);
}
#[test]
fn test_a_region_running_out_of_committed_pages_is_not_a_stall() {
let mut memory = HoleyMemory::new(SPECIAL_PAGE, special_pages(4));
memory.holes.insert(SPECIAL_PAGE + 0x3000);
let snapshot = walk_holey(&memory, &special_region(SPECIAL_PAGE, 4));
assert_eq!(snapshot.stalls, WalkStalls::default());
assert!(
!snapshot
.diagnostics
.examples()
.iter()
.any(|message| message.contains("made no progress")),
"{:?}",
snapshot.diagnostics.examples()
);
assert!(
snapshot
.diagnostics
.examples()
.iter()
.any(|message| message.contains("only committed through")),
"the tail is still reported as the unreadable space it is"
);
assert_eq!(
snapshot
.spans
.iter()
.filter(|span| span.state == PoolState::Allocated)
.count(),
3
);
}
#[test]
fn test_a_bad_special_pool_page_costs_that_page_only() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&special_page(0x68, false));
bytes.extend_from_slice(&vec![0u8; PAGE_SIZE as usize]); bytes.extend_from_slice(&special_page(0x140, false));
let region = special_region(SPECIAL_PAGE, 3);
let memory = FlatMemory::new(SPECIAL_PAGE, bytes.len());
let layout = vs_layout(false);
let walker = SnapshotWalker {
memory: &memory,
layout: &layout,
traversal_limit: 1000,
};
let mut snapshot = PoolSnapshot {
complete: true,
..PoolSnapshot::default()
};
walker.walk_special_pool(®ion, SPECIAL_PAGE, &bytes, &mut snapshot);
let found: Vec<_> = snapshot
.spans
.iter()
.map(|span| (span.usable_address, span.size))
.collect();
assert_eq!(
found,
[
(SPECIAL_PAGE + 0xf90, 0x68),
(SPECIAL_PAGE + 0x2000 + 0xec0, 0x140)
],
"the page after the bad one must still be walked"
);
assert!(!snapshot.complete, "a skipped page is not a complete walk");
assert!(
snapshot
.diagnostics
.examples()
.iter()
.any(|message| message.contains(&format!("{:#x}", SPECIAL_PAGE + 0x1000))),
"the diagnostic has to name the page that was skipped: {:?}",
snapshot.diagnostics.examples()
);
}
use crate::pool::{
decode::{
DESCRIPTOR_FLAG_ALLOCATED, DESCRIPTOR_FLAG_FIRST, DESCRIPTOR_FLAG_SUBSEGMENT,
DESCRIPTOR_FLAG_VS,
},
layout::{LayoutKey, TypeLayout},
};
const RANGE_IN_USE: u8 = DESCRIPTOR_FLAG_ALLOCATED | DESCRIPTOR_FLAG_FIRST;
const RANGE_LFH: u8 = RANGE_IN_USE | DESCRIPTOR_FLAG_SUBSEGMENT;
const RANGE_VS: u8 = RANGE_LFH | DESCRIPTOR_FLAG_VS;
const K: u64 = 0xffff_8000_0000_0000;
const STATE: u64 = K + 0x10_0000;
const GLOBALS: u64 = K + 0x11_0000;
const BIG_TABLE_POINTER: u64 = K + 0x12_0000;
const BIG_TABLE_COUNT: u64 = K + 0x12_0010;
const HEAP: u64 = K + 0x20_0000;
const POOL_NODE: u64 = STATE + 0x40;
const DYNAMIC_LOOKASIDE: u64 = POOL_NODE + 0x20;
const SEGMENT: u64 = K + 0x30_0000;
const LARGE_META: u64 = K + 0x80_0000;
const LARGE_VA: u64 = K + 0x90_0000;
const BIG_TABLE: u64 = K + 0xa0_0000;
struct SyntheticMemory {
runs: Vec<(u64, Vec<u8>)>,
holes: Vec<(u64, u64)>,
}
#[derive(Default)]
struct Writes(Vec<(u64, Vec<u8>)>);
impl SyntheticMemory {
fn new(writes: Writes, holes: Vec<(u64, u64)>) -> Self {
let mut extents: Vec<(u64, u64)> = writes
.0
.iter()
.filter(|(_, data)| !data.is_empty())
.map(|(address, data)| (*address, address + data.len() as u64))
.collect();
extents.sort_unstable();
let mut runs: Vec<(u64, Vec<u8>)> = Vec::new();
for (start, end) in extents {
match runs.last_mut() {
Some((run_start, data)) if start <= *run_start + data.len() as u64 => {
let len = (end - *run_start) as usize;
if len > data.len() {
data.resize(len, 0);
}
}
_ => runs.push((start, vec![0; (end - start) as usize])),
}
}
for (address, data) in writes.0.into_iter().filter(|(_, data)| !data.is_empty()) {
let index = runs
.partition_point(|(start, _)| *start <= address)
.checked_sub(1)
.expect("every non-empty write contributed an extent");
let (start, run) = &mut runs[index];
let offset = (address - *start) as usize;
run[offset..offset + data.len()].copy_from_slice(&data);
}
Self { runs, holes }
}
fn run_at(&self, address: u64) -> Option<&(u64, Vec<u8>)> {
let index = self
.runs
.partition_point(|(start, _)| *start <= address)
.checked_sub(1)?;
let run = &self.runs[index];
(address < run.0 + run.1.len() as u64).then_some(run)
}
fn contains(&self, address: u64) -> bool {
self.run_at(address).is_some()
}
}
struct ShortMemory;
struct BigPageMemory {
table: Vec<u8>,
count: usize,
read_calls: Cell<usize>,
interrupt_checks: Cell<usize>,
interrupt_after_checks: Option<usize>,
}
impl PoolMemory for ShortMemory {
fn read_exact(&self, _address: u64, _size: usize) -> Result<Vec<u8>, SnapshotError> {
Ok(Vec::new())
}
fn valid_region(&self, address: u64, size: usize) -> Result<(u64, usize), SnapshotError> {
Ok((address, size))
}
fn interrupted(&self) -> Result<bool, SnapshotError> {
Ok(false)
}
}
impl PoolMemory for BigPageMemory {
fn read_exact(&self, address: u64, size: usize) -> Result<Vec<u8>, SnapshotError> {
self.read_calls.set(self.read_calls.get() + 1);
if address == BIG_TABLE_POINTER && size == 8 {
return Ok(BIG_TABLE.to_le_bytes().to_vec());
}
if address == BIG_TABLE_COUNT && size == 4 {
return Ok((self.count as u32).to_le_bytes().to_vec());
}
if address == BIG_TABLE_COUNT && size == 8 {
return Ok(((1u64 << 32) | self.count as u64).to_le_bytes().to_vec());
}
let offset = address
.checked_sub(BIG_TABLE)
.and_then(|value| usize::try_from(value).ok());
if let Some(bytes) = offset
.and_then(|offset| offset.checked_add(size).map(|end| (offset, end)))
.and_then(|(offset, end)| self.table.get(offset..end))
{
return Ok(bytes.to_vec());
}
Err(SnapshotError::Read {
address,
size,
source: Box::new(std::io::Error::other("sparse big-page memory")),
})
}
fn valid_region(&self, address: u64, size: usize) -> Result<(u64, usize), SnapshotError> {
Ok((address, size))
}
fn interrupted(&self) -> Result<bool, SnapshotError> {
let checks = self.interrupt_checks.get();
self.interrupt_checks.set(checks + 1);
Ok(self
.interrupt_after_checks
.is_some_and(|limit| checks >= limit))
}
}
impl PoolMemory for SyntheticMemory {
fn read_exact(&self, address: u64, size: usize) -> Result<Vec<u8>, SnapshotError> {
let unreadable = || SnapshotError::Read {
address,
size,
source: Box::new(std::io::Error::other("sparse synthetic memory")),
};
let (start, data) = self.run_at(address).ok_or_else(unreadable)?;
let offset = (address - start) as usize;
data.get(offset..offset + size)
.map(<[u8]>::to_vec)
.ok_or_else(unreadable)
}
fn valid_region(&self, address: u64, size: usize) -> Result<(u64, usize), SnapshotError> {
let end = address.saturating_add(size as u64);
for &(hole_start, hole_end) in &self.holes {
if address >= hole_start && address < hole_end {
return Ok((hole_end, end.saturating_sub(hole_end) as usize));
}
if address < hole_start && end > hole_start {
return Ok((address, hole_start.saturating_sub(address) as usize));
}
}
Ok((address, size))
}
fn interrupted(&self) -> Result<bool, SnapshotError> {
Ok(false)
}
}
fn type_layout(size: u32, fields: &[(&'static str, u32)]) -> TypeLayout {
TypeLayout {
size,
fields: fields.iter().copied().collect(),
}
}
fn synthetic_layout() -> PoolLayout {
let mut types = HashMap::new();
types.insert(
"_EX_POOL_HEAP_MANAGER_STATE",
type_layout(
0x200,
&[
("HeapManager", 8),
("PoolNode", 0x40),
("NumberOfPools", 0),
("SpecialHeaps", 0x20),
],
),
);
types.insert(
"_EX_HEAP_POOL_NODE",
type_layout(0x120, &[("Heaps", 0), ("Lookasides", 0x20)]),
);
types.insert(
"_SEGMENT_HEAP",
type_layout(
0x800,
&[
("SegContexts", 0x100),
("VsContext", 0x300),
("LfhContext", 0x380),
("LargeAllocMetadata", 0x400),
],
),
);
types.insert(
"_HEAP_SEG_CONTEXT",
type_layout(
0x80,
&[
("SegmentListHead", 0),
("FreePageRanges", 0x10),
("UnitShift", 0x20),
("FirstDescriptorIndex", 0x21),
("SegmentMask", 0x28),
("PagesPerUnitShift", 0x30),
],
),
);
types.insert(
"_HEAP_PAGE_SEGMENT",
type_layout(
0x100,
&[("ListEntry", 0), ("Signature", 0x20), ("DescArray", 0x100)],
),
);
types.insert(
"_HEAP_PAGE_RANGE_DESCRIPTOR",
type_layout(
0x20,
&[
("UnitSize", 0x1f),
("RangeFlags", 0x18),
("TreeNode", 0),
("TreeSignature", 0),
],
),
);
types.insert(
"_HEAP_VS_CONTEXT",
type_layout(
0x40,
&[
("FreeChunkTree", 0),
("DelayFreeContext", 0x10),
("SubsegmentList", 0x30),
],
),
);
types.insert(
"_HEAP_VS_DELAY_FREE_CONTEXT",
type_layout(0x10, &[("ListHead", 0)]),
);
types.insert(
"_HEAP_VS_SUBSEGMENT",
type_layout(0xfe0, &[("Signature", 0), ("Size", 2), ("ListEntry", 8)]),
);
types.insert(
"_HEAP_VS_CHUNK_HEADER",
type_layout(0x10, &[("Sizes", 0), ("EncodedSegmentPageOffset", 8)]),
);
types.insert(
"_HEAP_VS_CHUNK_FREE_HEADER",
type_layout(0x20, &[("TreeNode", 8)]),
);
types.insert("_HEAP_LFH_CONTEXT", type_layout(0x20, &[("Buckets", 0)]));
types.insert(
"_HEAP_LFH_SUBSEGMENT",
type_layout(
0x20,
&[
("BlockOffsets", 0),
("BlockCount", 4),
("BlockBitmap", 8),
("ListEntry", 0x10),
],
),
);
types.insert(
"_HEAP_LFH_SUBSEGMENT_ENCODED_OFFSETS",
type_layout(4, &[("EncodedData", 0)]),
);
types.insert(
"_RTLP_HP_HEAP_GLOBALS",
type_layout(0x10, &[("HeapKey", 0), ("LfhKey", 8)]),
);
types.insert(
"_RTL_RB_TREE",
type_layout(0x10, &[("Root", 0), ("Encoded", 8)]),
);
types.insert(
"_RTL_BALANCED_NODE",
type_layout(0x10, &[("Left", 0), ("Right", 8)]),
);
types.insert(
"_RTL_DYNAMIC_LOOKASIDE",
type_layout(0x80, &[("BucketCount", 8), ("Buckets", 0x40)]),
);
types.insert(
"_RTL_LOOKASIDE",
type_layout(0x40, &[("ListHead", 0), ("Size", 0x30)]),
);
types.insert(
"_SLIST_HEADER",
type_layout(0x10, &[("Alignment", 0), ("Region", 8)]),
);
types.insert(
"_POOL_HEADER",
type_layout(
0x10,
&[
("PreviousSize", 0),
("PoolIndex", 0),
("BlockSize", 2),
("PoolType", 2),
("PoolTag", 8),
],
),
);
types.insert(
"_HEAP_LARGE_ALLOC_DATA",
type_layout(
0x28,
&[
("TreeNode", 0),
("VirtualAddress", 0x18),
("AllocatedPages", 0x20),
],
),
);
types.insert(
"_POOL_TRACKER_BIG_PAGES",
type_layout(0x20, &[("Va", 0), ("Key", 8), ("NumberOfBytes", 0x10)]),
);
PoolLayout {
key: LayoutKey {
image: crate::dbgeng::KernelImage {
base: K,
..crate::dbgeng::KernelImage::default()
},
session: 1,
},
globals: [
("ExPoolState", STATE),
("RtlpHpHeapGlobals", GLOBALS),
("PoolBigPageTable", BIG_TABLE_POINTER),
("PoolBigPageTableSize", BIG_TABLE_COUNT),
]
.into_iter()
.collect(),
types,
}
}
fn put(bytes: &mut Writes, address: u64, data: &[u8]) {
bytes.0.push((address, data.to_vec()));
}
fn fill(bytes: &mut Writes, address: u64, size: usize) {
bytes.0.push((address, vec![0; size]));
}
fn put_u16(bytes: &mut Writes, address: u64, value: u16) {
put(bytes, address, &value.to_le_bytes());
}
fn put_u32(bytes: &mut Writes, address: u64, value: u32) {
put(bytes, address, &value.to_le_bytes());
}
fn put_u64(bytes: &mut Writes, address: u64, value: u64) {
put(bytes, address, &value.to_le_bytes());
}
fn packed_slist_next(entry: u64) -> u64 {
(entry << 4) | 3
}
fn pool_header(bytes: &mut Writes, address: u64, tag: &[u8; 4]) {
fill(bytes, address, 0x10);
put(bytes, address, &[1, 0, 4, 1]);
put(bytes, address + 8, tag);
}
fn synthetic_memory() -> SyntheticMemory {
synthetic_memory_declaring((0x2000 - 0xfe0) / 16)
}
fn synthetic_memory_declaring(declared: u16) -> SyntheticMemory {
let mut bytes = Writes::default();
fill(&mut bytes, STATE, 0x200);
put_u32(&mut bytes, STATE, 1);
for heap_index in 0..4 {
put_u64(&mut bytes, STATE + 0x40 + heap_index * 8, HEAP);
}
fill(&mut bytes, GLOBALS, 0x10);
let heap_key = 0x55aa_1234_9876_0000;
let lfh_key = 0xa5c3_1357;
put_u64(&mut bytes, GLOBALS, heap_key);
put_u64(&mut bytes, GLOBALS + 8, lfh_key);
fill(&mut bytes, HEAP, 0x800);
let context = HEAP + 0x100;
let list_head = context;
put_u64(&mut bytes, list_head, SEGMENT);
put_u64(&mut bytes, context + 0x10, SEGMENT + 0x100 + 6 * 0x20);
put(&mut bytes, context + 0x20, &[12, 1]);
put_u64(&mut bytes, context + 0x28, !0xffffu64);
fill(&mut bytes, SEGMENT, 0x100);
put_u64(&mut bytes, SEGMENT, list_head);
put_u64(
&mut bytes,
SEGMENT + 0x20,
SEGMENT ^ context ^ heap_key ^ super::super::decode::PAGE_SEGMENT_SIGNATURE,
);
fill(&mut bytes, SEGMENT + 0x100, 16 * 0x20);
for (index, units, flags) in [
(1u64, 2u8, RANGE_LFH),
(3, 2, RANGE_VS),
(5, 1, RANGE_IN_USE),
(6, 1, 0x00),
(7, 1, RANGE_IN_USE),
] {
let descriptor = SEGMENT + 0x100 + index * 0x20;
if flags & DESCRIPTOR_FLAG_FIRST != 0 {
put_u32(
&mut bytes,
descriptor,
super::super::decode::DESCRIPTOR_TREE_SIGNATURE,
);
}
put(&mut bytes, descriptor + 0x18, &[flags]);
put(&mut bytes, descriptor + 0x1f, &[units]);
}
let lfh = SEGMENT + 0x1000;
fill(&mut bytes, lfh, 0x20);
let decoded_offsets = u32::from(0x40u16) | (u32::from(0x40u16) << 16);
put_u32(
&mut bytes,
lfh,
decoded_offsets ^ lfh_key as u32 ^ (lfh >> 12) as u32,
);
put_u16(&mut bytes, lfh + 4, 4);
put(&mut bytes, lfh + 8, &[0x49]);
fill(&mut bytes, lfh + 0x40, 0x100);
pool_header(&mut bytes, lfh + 0x40, b"LFH!");
pool_header(&mut bytes, lfh + 0x80, b"LFHC");
pool_header(&mut bytes, lfh + 0xc0, b"LFHF");
pool_header(&mut bytes, lfh + 0x100, b"LFH2");
let vs = SEGMENT + 0x3000;
fill(&mut bytes, vs, 0x2000);
put_u16(&mut bytes, vs, 0x8000 | (0x2bed ^ declared));
put_u16(&mut bytes, vs + 2, declared);
let first_chunk = vs + 0xfe0;
let cached_chunk = first_chunk + 0x40;
let free_chunk = cached_chunk + 0x40;
for (address, allocated) in [
(first_chunk, true),
(cached_chunk, false),
(free_chunk, false),
] {
let decoded = (4u64 << 16) | (u64::from(allocated) << 48);
put_u64(&mut bytes, address, decoded ^ address ^ heap_key);
}
pool_header(&mut bytes, first_chunk + 0x10, b"VS!!");
let vs_context = HEAP + 0x300;
put_u64(&mut bytes, vs_context, free_chunk + 8);
put_u64(&mut bytes, free_chunk + 8, 0);
put_u64(&mut bytes, free_chunk + 16, 0);
let delay_head = vs_context + 0x10;
put_u16(&mut bytes, delay_head, 1);
put_u64(
&mut bytes,
delay_head + 8,
packed_slist_next(cached_chunk + 0x20),
);
put_u64(&mut bytes, cached_chunk + 0x20, 0);
fill(&mut bytes, DYNAMIC_LOOKASIDE, 0x80);
put_u32(&mut bytes, DYNAMIC_LOOKASIDE + 8, 1);
let lookaside_chunk = free_chunk + 0x40;
let decoded = 4u64 << 16;
put_u64(
&mut bytes,
lookaside_chunk,
decoded ^ lookaside_chunk ^ heap_key,
);
let lookaside_head = DYNAMIC_LOOKASIDE + 0x40;
put_u16(&mut bytes, lookaside_head, 1);
put_u64(
&mut bytes,
lookaside_head + 8,
packed_slist_next(lookaside_chunk + 0x20),
);
put_u64(&mut bytes, lookaside_chunk + 0x20, 0);
put_u32(&mut bytes, lookaside_head + 0x30, 0x40);
fill(&mut bytes, SEGMENT + 0x5000, 0x1000);
pool_header(&mut bytes, SEGMENT + 0x5000, b"SEGM");
fill(&mut bytes, SEGMENT + 0x6000, 0x1000);
fill(&mut bytes, SEGMENT + 0x7000, 0x800);
pool_header(&mut bytes, SEGMENT + 0x7000, b"SPRS");
let large_tree = HEAP + 0x400;
put_u64(&mut bytes, large_tree, LARGE_META ^ large_tree);
put(&mut bytes, large_tree + 8, &[1]);
fill(&mut bytes, LARGE_META, 0x28);
put_u64(&mut bytes, LARGE_META + 0x18, LARGE_VA | 0x1800);
put_u64(&mut bytes, LARGE_META + 0x20, (2u64 << 12) | 0x5a5);
put_u64(&mut bytes, BIG_TABLE_POINTER, BIG_TABLE);
put_u64(&mut bytes, BIG_TABLE_COUNT, 4);
fill(&mut bytes, BIG_TABLE, 4 * 0x20);
let first = super::super::decode::big_page_hash(LARGE_VA, 4).unwrap();
let adjacent = (first + 1) % 4;
let collision = BIG_TABLE + first as u64 * 0x20;
put_u64(&mut bytes, collision, LARGE_VA + 0x10_0000);
let entry = BIG_TABLE + adjacent as u64 * 0x20;
put_u64(&mut bytes, entry, LARGE_VA);
put(&mut bytes, entry + 8, b"BIG!");
put_u64(&mut bytes, entry + 0x10, 0x1800);
SyntheticMemory::new(bytes, vec![(SEGMENT + 0x7800, SEGMENT + 0x8000)])
}
fn big_page_memory(address: u64, count: usize, collision_distance: usize) -> BigPageMemory {
let mut table = vec![0; count * 0x20];
let first = super::super::decode::big_page_hash(address, count).unwrap();
for distance in 0..collision_distance {
let index = (first + distance) % count;
let offset = index * 0x20;
table[offset..offset + 8]
.copy_from_slice(&(address + (distance as u64 + 1) * 0x10_0000).to_le_bytes());
}
let index = (first + collision_distance) % count;
let offset = index * 0x20;
table[offset..offset + 8].copy_from_slice(&address.to_le_bytes());
table[offset + 8..offset + 12].copy_from_slice(b"BTCH");
table[offset + 0x10..offset + 0x18].copy_from_slice(&0x9000u64.to_le_bytes());
BigPageMemory {
table,
count,
read_calls: Cell::new(0),
interrupt_checks: Cell::new(0),
interrupt_after_checks: None,
}
}
#[test]
fn test_a_vs_subsegment_is_bounded_by_its_own_declared_size() {
let complaint = "declares";
let quiet = walk_synthetic(synthetic_memory());
assert!(
!quiet
.diagnostics
.examples()
.iter()
.any(|message| message.contains(complaint)),
"a subsegment sized like the real ones draws no complaint: {:?}",
quiet.diagnostics.examples()
);
let all = vs_chunks(&quiet);
let bounded = walk_synthetic(synthetic_memory_declaring(8));
let declared_end = SEGMENT + 0x3000 + 0xfe0 + 8 * 16;
assert!(
vs_chunks(&bounded) < all,
"bounding the subsegment has to cost the chunks past its end: {:?}",
bounded.diagnostics.examples()
);
assert!(
snapshot_vs_spans(&bounded).all(|span| span.header_address < declared_end),
"nothing past {declared_end:#x} may be decoded: {:?}",
snapshot_vs_spans(&bounded)
.map(|span| span.header_address)
.collect::<Vec<_>>()
);
assert!(
snapshot_vs_spans(&quiet).any(|span| span.header_address >= declared_end),
"and the check is only worth anything if the wider bound did reach past it"
);
assert!(
!bounded
.diagnostics
.examples()
.iter()
.any(|message| message.contains(complaint)),
"and that is the ordinary shape, not something to complain about once per \
subsegment: {:?}",
bounded.diagnostics.examples()
);
for declared in [0, 0x200] {
let bogus = walk_synthetic(synthetic_memory_declaring(declared));
assert!(
bogus
.diagnostics
.examples()
.iter()
.any(|message| message.contains(complaint)),
"declared {declared:#x}: {:?}",
bogus.diagnostics.examples()
);
assert_eq!(
vs_chunks(&bogus),
all,
"an unusable declaration falls back to the descriptor's bound"
);
}
}
fn snapshot_vs_spans(snapshot: &PoolSnapshot) -> impl Iterator<Item = &PoolSpan> {
snapshot
.spans
.iter()
.filter(|span| span.backend == PoolBackend::Vs && span.state != PoolState::Unreadable)
}
fn vs_chunks(snapshot: &PoolSnapshot) -> usize {
snapshot_vs_spans(snapshot).count()
}
fn walk_synthetic(memory: SyntheticMemory) -> PoolSnapshot {
let layout = synthetic_layout();
SnapshotWalker {
memory: &memory,
layout: &layout,
traversal_limit: 1024,
}
.walk(None)
.unwrap()
}
#[test]
fn test_pool_snapshot_walks_all_backends() {
let memory = synthetic_memory();
assert!(!memory.contains(LARGE_VA));
let layout = synthetic_layout();
let walker = SnapshotWalker {
memory: &memory,
layout: &layout,
traversal_limit: 1024,
};
let snapshot = walker.walk(None).unwrap();
for backend in [
PoolBackend::Lfh,
PoolBackend::Vs,
PoolBackend::Segment,
PoolBackend::Large,
] {
assert!(
snapshot.spans.iter().any(|span| span.backend == backend),
"missing {backend:?}"
);
}
assert!(snapshot.spans.iter().any(|span| span.pool_kind.is_paged()));
assert!(snapshot.spans.iter().any(|span| {
span.raw_tag == u32::from_le_bytes(*b"LFH!") && span.state == PoolState::Allocated
}));
assert!(snapshot.spans.iter().any(|span| {
span.backend == PoolBackend::Vs && span.state == PoolState::CachedFree
}));
assert!(snapshot.spans.iter().any(|span| {
span.backend == PoolBackend::Vs
&& span.header_address == SEGMENT + 0x40b0
&& span.state == PoolState::CachedFree
}));
assert!(
snapshot
.spans
.iter()
.filter(|span| span.backend == PoolBackend::Vs)
.all(|span| span.size_class == 0x40)
);
assert!(snapshot.spans.iter().any(|span| {
span.backend == PoolBackend::Segment && span.state == PoolState::ReusableFree
}));
assert!(
snapshot
.spans
.iter()
.any(|span| span.state == PoolState::Unreadable)
);
assert!(snapshot.spans.iter().any(|span| {
span.backend == PoolBackend::Vs
&& span.header_address == SEGMENT + 0x3ff0
&& span.usable_address == SEGMENT + 0x4000
&& span.raw_tag == u32::from_le_bytes(*b"VS!!")
}));
assert!(snapshot.spans.iter().any(|span| {
span.backend == PoolBackend::Large
&& span.raw_tag == u32::from_le_bytes(*b"BIG!")
&& span.size == 0x1800
}));
assert!(
snapshot
.diagnostics
.examples()
.iter()
.any(|message| { message.contains("per-session paged heaps are not included") })
);
assert!(
snapshot
.diagnostics
.examples()
.iter()
.any(|message| message.contains("only committed through"))
);
let mut table = vec![0u8; 8 * 24];
let address = K + 0xb0_0000;
let first = super::super::decode::big_page_hash(address, 8).unwrap();
for distance in 0..2 {
let collision = ((first + distance) % 8) * 24;
table[collision..collision + 8]
.copy_from_slice(&(address + (distance as u64 + 1) * 0x10_0000).to_le_bytes());
}
let third = ((first + 2) % 8) * 24;
table[third..third + 8].copy_from_slice(&address.to_le_bytes());
table[third + 8..third + 12].copy_from_slice(b"NEXT");
table[third + 12..third + 20].copy_from_slice(&0x7000u64.to_le_bytes());
assert_eq!(
walker.lookup_big_page(&table, 24, address),
Some((u32::from_le_bytes(*b"NEXT"), 0x7000))
);
}
struct Impatient<M> {
inner: M,
reads: Cell<usize>,
allowance: usize,
}
impl Impatient<SyntheticMemory> {
fn new(allowance: usize) -> Self {
Self::over(synthetic_memory(), allowance)
}
}
impl<M> Impatient<M> {
fn over(inner: M, allowance: usize) -> Self {
Self {
inner,
reads: Cell::new(0),
allowance,
}
}
}
impl<M: PoolMemory> PoolMemory for Impatient<M> {
fn read_exact(&self, address: u64, size: usize) -> Result<Vec<u8>, SnapshotError> {
self.reads.set(self.reads.get() + 1);
self.inner.read_exact(address, size)
}
fn valid_region(&self, address: u64, size: usize) -> Result<(u64, usize), SnapshotError> {
self.inner.valid_region(address, size)
}
fn interrupted(&self) -> Result<bool, SnapshotError> {
Ok(false)
}
fn out_of_budget(&self) -> bool {
self.reads.get() >= self.allowance
}
}
fn walk_impatiently(allowance: usize) -> PoolSnapshot {
let memory = Impatient::new(allowance);
let layout = synthetic_layout();
SnapshotWalker {
memory: &memory,
layout: &layout,
traversal_limit: 1024,
}
.walk(None)
.expect("running out of budget is an outcome, not an error")
}
#[test]
fn test_a_walk_that_runs_out_of_budget_reports_what_it_reached() {
let snapshot = walk_impatiently(40);
assert!(
!snapshot.complete,
"a truncated walk that claims completeness is the lie this whole API guards against"
);
assert!(
snapshot
.diagnostics
.examples()
.iter()
.any(|message| message.contains("ran out of its")
&& message.contains("discovered regions")),
"the snapshot has to say why it is short: {:?}",
snapshot.diagnostics
);
}
#[test]
fn test_running_out_of_budget_is_never_reported_as_a_broken_unit() {
for allowance in [10, 25, 40, 80, 120, 200, 400] {
let snapshot = walk_impatiently(allowance);
for absorbed in [
"cannot fully discover heap",
"cannot discover segment context",
] {
assert!(
!snapshot
.diagnostics
.examples()
.iter()
.any(|message| message.contains(absorbed)),
"budget {allowance}: a per-unit handler absorbed the halt as \
`{absorbed}`: {:?}",
snapshot.diagnostics
);
}
}
}
const OVERSHOOT_ALLOWED: usize = 3;
#[test]
fn test_the_walk_stops_reading_promptly_once_its_budget_is_gone() {
let overshoots: Vec<(usize, usize)> = [10, 25, 40, 80, 120, 200, 400]
.into_iter()
.map(|allowance| {
let memory = Impatient::new(allowance);
let layout = synthetic_layout();
SnapshotWalker {
memory: &memory,
layout: &layout,
traversal_limit: 1024,
}
.walk(None)
.expect("running out of budget is an outcome, not an error");
(allowance, memory.reads.get().saturating_sub(allowance))
})
.collect();
let worst = overshoots.iter().map(|(_, over)| *over).max().unwrap_or(0);
assert!(
worst <= OVERSHOOT_ALLOWED,
"{worst} reads issued after the deadline (limit {OVERSHOOT_ALLOWED}) — some \
read-issuing loop is not polling the budget. (budget, overshoot): {overshoots:?}"
);
}
fn segment_of_plain_page_ranges(heap_key: u64) -> SyntheticMemory {
let mut bytes = Writes::default();
let context = HEAP + 0x100;
fill(&mut bytes, HEAP, 0x800);
put_u64(&mut bytes, context, SEGMENT); put(&mut bytes, context + 0x20, &[12, 1]); put_u64(&mut bytes, context + 0x28, !0xffffu64);
fill(&mut bytes, SEGMENT, 0x300);
put_u64(&mut bytes, SEGMENT, context); put_u64(
&mut bytes,
SEGMENT + 0x20,
SEGMENT ^ context ^ heap_key ^ super::super::decode::PAGE_SEGMENT_SIGNATURE,
);
for (index, flags) in [(1u64, RANGE_IN_USE), (2, DESCRIPTOR_FLAG_FIRST)] {
let descriptor = SEGMENT + 0x100 + index * 0x20;
put_u32(
&mut bytes,
descriptor,
super::super::decode::DESCRIPTOR_TREE_SIGNATURE,
);
put(&mut bytes, descriptor + 0x18, &[flags]);
put(&mut bytes, descriptor + 0x1f, &[1]);
}
SyntheticMemory::new(bytes, Vec::new())
}
#[test]
fn test_a_plain_page_range_is_not_taken_for_an_lfh_subsegment() {
let heap_key = 0x55aa_1234_9876_0000;
let memory = segment_of_plain_page_ranges(heap_key);
let layout = synthetic_layout();
let mut discovery = Discovery::default();
discover_segment_context(
&memory,
&layout,
HEAP + 0x100,
0,
PoolKind::NonPagedNx,
HeapIdentity {
pool_state: STATE,
heap: HEAP,
special: false,
},
heap_key,
0xa5c3_1357,
1024,
&SharedChunks::default(),
&SharedChunks::default(),
&mut discovery,
)
.expect("the fixture is readable throughout");
assert!(
discovery.diagnostics.is_empty(),
"nothing here is a subsegment, so nothing should have been decoded as one: {:?}",
discovery.diagnostics
);
let described: Vec<_> = discovery
.regions
.iter()
.map(|region| (region.address, region.backend, region.states.as_slice()))
.collect();
assert_eq!(
described,
[
(
SEGMENT + 0x1000,
PoolBackend::Segment,
[PoolState::Allocated].as_slice()
),
(
SEGMENT + 0x2000,
PoolBackend::Segment,
[PoolState::ReusableFree].as_slice()
),
]
);
}
fn segment_of_lfh_descriptors(heap_key: u64) -> SyntheticMemory {
let mut bytes = Writes::default();
let context = HEAP + 0x100;
fill(&mut bytes, HEAP, 0x800);
put_u64(&mut bytes, context, SEGMENT); put(&mut bytes, context + 0x20, &[12, 1]); put_u64(&mut bytes, context + 0x28, !0xffffu64);
fill(&mut bytes, SEGMENT, 0x300);
put_u64(&mut bytes, SEGMENT, context); put_u64(
&mut bytes,
SEGMENT + 0x20,
SEGMENT ^ context ^ heap_key ^ super::super::decode::PAGE_SEGMENT_SIGNATURE,
);
for index in 1..16u64 {
let descriptor = SEGMENT + 0x100 + index * 0x20;
put_u32(
&mut bytes,
descriptor,
super::super::decode::DESCRIPTOR_TREE_SIGNATURE,
);
put(&mut bytes, descriptor + 0x18, &[RANGE_LFH]);
put(&mut bytes, descriptor + 0x1f, &[1]);
}
SyntheticMemory::new(bytes, Vec::new())
}
#[test]
fn test_a_segment_full_of_descriptors_polls_the_budget_between_them() {
const ALLOWANCE: usize = 10;
let heap_key = 0x55aa_1234_9876_0000;
let memory = Impatient::over(segment_of_lfh_descriptors(heap_key), ALLOWANCE);
let layout = synthetic_layout();
let mut discovery = Discovery::default();
let outcome = discover_segment_context(
&memory,
&layout,
HEAP + 0x100,
0,
PoolKind::NonPagedNx,
HeapIdentity {
pool_state: STATE,
heap: HEAP,
special: false,
},
heap_key,
0xa5c3_1357,
1024,
&SharedChunks::default(),
&SharedChunks::default(),
&mut discovery,
);
assert!(
matches!(outcome, Err(SnapshotError::BudgetExpired)),
"the descriptor loop has to report the halt: {outcome:?}"
);
assert!(
memory.reads.get() <= ALLOWANCE + OVERSHOOT_ALLOWED,
"{} reads for a budget of {ALLOWANCE}: the descriptor loop ran on past the \
deadline instead of polling it",
memory.reads.get()
);
}
fn large_allocation_tree(count: u64) -> SyntheticMemory {
let mut bytes = Writes::default();
let tree = HEAP + 0x400;
let node = |index: u64| LARGE_META + index * 0x40;
fill(&mut bytes, tree, 0x10);
put_u64(&mut bytes, tree, node(0)); for index in 0..count {
fill(&mut bytes, node(index), 0x10);
let next = if index + 1 < count {
node(index + 1)
} else {
0
};
put_u64(&mut bytes, node(index), next);
}
SyntheticMemory::new(bytes, Vec::new())
}
#[test]
fn test_the_large_allocation_loop_polls_the_budget_between_nodes() {
const NODES: u64 = 12;
const ALLOWANCE: usize = 16;
let memory = Impatient::over(large_allocation_tree(NODES), ALLOWANCE);
let layout = synthetic_layout();
let mut discovery = Discovery::default();
let outcome = discover_large_allocations(
&memory,
&layout,
HEAP,
0,
PoolKind::NonPagedNx,
HeapIdentity {
pool_state: STATE,
heap: HEAP,
special: false,
},
0,
1024,
&mut discovery,
);
assert!(
matches!(outcome, Err(SnapshotError::BudgetExpired)),
"the node loop has to report the halt: {outcome:?}"
);
assert!(
memory.reads.get() <= ALLOWANCE + OVERSHOOT_ALLOWED,
"{} reads for a budget of {ALLOWANCE}: the node loop ran on past the deadline",
memory.reads.get()
);
}
struct OneHugeExtent {
reads: Cell<usize>,
allowance: usize,
}
impl PoolMemory for OneHugeExtent {
fn read_exact(&self, _address: u64, size: usize) -> Result<Vec<u8>, SnapshotError> {
self.reads.set(self.reads.get() + 1);
Ok(vec![0; size])
}
fn valid_region(&self, address: u64, size: usize) -> Result<(u64, usize), SnapshotError> {
Ok((address, size))
}
fn interrupted(&self) -> Result<bool, SnapshotError> {
Ok(false)
}
fn out_of_budget(&self) -> bool {
self.reads.get() >= self.allowance
}
}
#[test]
fn test_a_large_extent_is_read_in_chunks_that_observe_the_deadline() {
const ALLOWANCE: usize = 2;
let memory = OneHugeExtent {
reads: Cell::new(0),
allowance: ALLOWANCE,
};
let layout = vs_layout(false);
let mut region = lfh_region(0x1000, 0x100);
region.size = EXTENT_READ_CHUNK * 8;
region.bitmap = vec![0xff; 64];
let walker = SnapshotWalker {
memory: &memory,
layout: &layout,
traversal_limit: 1000,
};
let outcome = walker.walk_region(®ion, &mut PoolSnapshot::default());
assert!(
matches!(outcome, Err(SnapshotError::BudgetExpired)),
"an extent that outlives the deadline has to stop mid-transfer: {outcome:?}"
);
assert_eq!(
memory.reads.get(),
ALLOWANCE,
"the extent was not chunked, so the deadline could not be observed while it \
transferred"
);
}
struct ShortChunks {
reads: Cell<usize>,
}
impl PoolMemory for ShortChunks {
fn read_exact(&self, _address: u64, size: usize) -> Result<Vec<u8>, SnapshotError> {
self.reads.set(self.reads.get() + 1);
Ok(vec![0; size.saturating_sub(1)])
}
fn valid_region(&self, address: u64, size: usize) -> Result<(u64, usize), SnapshotError> {
Ok((address, size))
}
fn interrupted(&self) -> Result<bool, SnapshotError> {
Ok(false)
}
}
#[test]
fn test_a_short_chunk_fails_the_extent_instead_of_misassembling_it() {
let memory = ShortChunks {
reads: Cell::new(0),
};
let layout = vs_layout(false);
let mut region = lfh_region(0x1000, 0x100);
region.size = EXTENT_READ_CHUNK * 3;
region.bitmap = vec![0xff; 64];
let walker = SnapshotWalker {
memory: &memory,
layout: &layout,
traversal_limit: 1000,
};
assert!(
matches!(
walker.read_extent(region.address, region.size),
Err(SnapshotError::InvalidData { .. })
),
"a short chunk has to fail the extent"
);
assert_eq!(
memory.reads.get(),
1,
"it must not read on after the short chunk"
);
let mut snapshot = PoolSnapshot::default();
walker.walk_region(®ion, &mut snapshot).unwrap();
assert!(
snapshot
.spans
.iter()
.all(|span| span.state == PoolState::Unreadable),
"a misassembled extent must not yield decoded allocations: {:?}",
snapshot.spans
);
assert!(
snapshot
.diagnostics
.examples()
.iter()
.any(|message| message.contains("cannot read region")),
"the failure has to be said out loud: {:?}",
snapshot.diagnostics
);
}
#[test]
fn test_a_small_extent_is_still_a_single_read() {
let memory = OneHugeExtent {
reads: Cell::new(0),
allowance: usize::MAX,
};
let layout = vs_layout(false);
let region = lfh_region(0x1000, 0x100);
let walker = SnapshotWalker {
memory: &memory,
layout: &layout,
traversal_limit: 1000,
};
walker
.walk_region(®ion, &mut PoolSnapshot::default())
.unwrap();
assert_eq!(memory.reads.get(), 1);
}
#[test]
fn test_regions_found_before_the_halt_are_not_thrown_away() {
let snapshot = walk_impatiently(120);
let summary = snapshot
.diagnostics
.examples()
.iter()
.find(|message| message.contains("discovered regions"))
.expect("the walk stopped short and must say so");
let tokens: Vec<&str> = summary.split_whitespace().collect();
let discovered: usize = tokens
.iter()
.position(|token| *token == "discovered")
.and_then(|index| index.checked_sub(1))
.and_then(|index| tokens[index].parse().ok())
.unwrap_or_else(|| panic!("no region count in `{summary}`"));
assert!(
discovered > 0,
"discovery's partial results were discarded with the halt: `{summary}`"
);
}
#[test]
fn test_discovery_gets_a_share_of_the_budget_and_the_walk_the_rest() {
let start = Instant::now();
let budget = Duration::from_secs(120);
let (discovery, whole) = budget_deadlines(start, Some(budget));
let (discovery, whole) = (discovery.unwrap(), whole.unwrap());
assert_eq!(whole, start + budget);
assert!(
discovery < whole,
"discovery must yield before the deadline"
);
assert!(
discovery > start,
"discovery must get real time, not a token slice"
);
assert_eq!(budget_deadlines(start, None), (None, None));
assert_eq!(budget_deadlines(start, Some(Duration::MAX)), (None, None));
}
#[test]
fn test_an_interrupt_still_stops_the_walk_outright() {
struct Interrupting(SyntheticMemory);
impl PoolMemory for Interrupting {
fn read_exact(&self, address: u64, size: usize) -> Result<Vec<u8>, SnapshotError> {
self.0.read_exact(address, size)
}
fn valid_region(
&self,
address: u64,
size: usize,
) -> Result<(u64, usize), SnapshotError> {
self.0.valid_region(address, size)
}
fn interrupted(&self) -> Result<bool, SnapshotError> {
Ok(true)
}
}
let memory = Interrupting(synthetic_memory());
let layout = synthetic_layout();
assert!(matches!(
SnapshotWalker {
memory: &memory,
layout: &layout,
traversal_limit: 1024,
}
.walk(Some(Duration::from_secs(600))),
Err(SnapshotError::Interrupted)
));
}
struct Fragmented {
extent: usize,
allowance: usize,
extents_read: Cell<usize>,
}
impl PoolMemory for Fragmented {
fn read_exact(&self, _address: u64, size: usize) -> Result<Vec<u8>, SnapshotError> {
Ok(vec![0; size])
}
fn valid_region(&self, address: u64, size: usize) -> Result<(u64, usize), SnapshotError> {
self.extents_read.set(self.extents_read.get() + 1);
Ok((address, size.min(self.extent)))
}
fn interrupted(&self) -> Result<bool, SnapshotError> {
Ok(false)
}
fn out_of_budget(&self) -> bool {
self.extents_read.get() >= self.allowance
}
}
#[test]
fn test_a_fragmented_region_checks_the_budget_between_extents() {
let memory = Fragmented {
extent: 0x400,
allowance: 3,
extents_read: Cell::new(0),
};
let layout = vs_layout(false);
let mut region = lfh_region(0x1000, 0x100);
region.bitmap = vec![0xff; 16];
let walker = SnapshotWalker {
memory: &memory,
layout: &layout,
traversal_limit: 1000,
};
let outcome = walker.walk_region(®ion, &mut PoolSnapshot::default());
assert!(
matches!(outcome, Err(SnapshotError::BudgetExpired)),
"the region walk has to report the halt, not absorb it: {outcome:?}"
);
assert_eq!(
memory.extents_read.get(),
3,
"the region went on reading past its deadline"
);
}
#[test]
fn test_a_generous_budget_walks_exactly_as_an_unbounded_one_does() {
let memory = synthetic_memory();
let layout = synthetic_layout();
let walker = SnapshotWalker {
memory: &memory,
layout: &layout,
traversal_limit: 1024,
};
let unbounded = walker.walk(None).unwrap();
let budgeted = walker.walk(Some(Duration::from_secs(600))).unwrap();
assert_eq!(budgeted.spans, unbounded.spans);
assert_eq!(budgeted.complete, unbounded.complete);
assert_eq!(budgeted.diagnostics, unbounded.diagnostics);
}
fn flooded() -> PoolDiagnostics {
let mut diagnostics: Vec<String> = (0..1000)
.map(|node| format!("unreadable VS free tree node {node:#x}: sparse memory"))
.collect();
diagnostics.push("rejecting implausible LFH unit size 3 at 0x1000".into());
PoolDiagnostics::from_iter(diagnostics)
}
#[test]
fn test_a_flood_of_one_complaint_becomes_examples_plus_a_count() {
let diagnostics = flooded();
assert_eq!(
diagnostics.examples().len(),
DIAGNOSTIC_EXAMPLES + 1,
"expected {DIAGNOSTIC_EXAMPLES} examples of the flood, plus the distinct message"
);
assert!(diagnostics.examples()[0].contains("unreadable VS free tree node 0x0"));
assert!(
diagnostics
.examples()
.iter()
.any(|message| message.contains("rejecting implausible LFH unit size")),
"a distinct complaint must not be collapsed away by a flood of another"
);
let totals: Vec<usize> = diagnostics.shapes().iter().map(|seen| seen.total).collect();
assert_eq!(
totals,
vec![1000, 1],
"shapes carry the whole count each, in the order first seen: {:?}",
diagnostics.shapes()
);
assert!(
diagnostics.lines().last().is_some_and(
|line| line.contains(&format!("and {} more", 1000 - DIAGNOSTIC_EXAMPLES))
),
"the rendering owes the reader the count too: {:?}",
diagnostics.lines()
);
}
#[test]
fn test_the_emitted_count_is_the_walk_not_the_sample() {
let diagnostics = flooded();
assert_eq!(diagnostics.emitted(), 1001, "every message is counted");
assert!(
diagnostics.emitted() > diagnostics.lines().len() * 50,
"a caller that counted lines ({}) instead of messages ({}) would be off by two \
orders of magnitude and never know",
diagnostics.lines().len(),
diagnostics.emitted()
);
}
#[test]
fn test_extending_merges_into_the_shapes_already_seen() {
let mut diagnostics = flooded();
diagnostics.extend(
(1000..1010)
.map(|node| format!("unreadable VS free tree node {node:#x}: sparse memory")),
);
assert_eq!(diagnostics.shapes().len(), 2, "no shape was duplicated");
assert_eq!(diagnostics.shapes()[0].total, 1010);
assert_eq!(diagnostics.emitted(), 1011);
assert_eq!(
diagnostics.examples().len(),
DIAGNOSTIC_EXAMPLES + 1,
"the cap holds across the merge"
);
}
#[test]
fn test_diagnostic_shape_ignores_numbers_but_not_wording() {
assert_eq!(
diagnostic_shape("unreadable VS free tree node 0xdeadbeef: sparse"),
diagnostic_shape("unreadable VS free tree node 0x41414141: sparse")
);
assert_ne!(
diagnostic_shape("unreadable VS free tree node 0xdeadbeef"),
diagnostic_shape("unreadable VS delay-free node 0xdeadbeef")
);
}
#[test]
fn test_special_pool_kinds_cover_all_heap_slots() {
assert_eq!(SPECIAL_POOL_KINDS.len(), 4);
assert_eq!(SPECIAL_POOL_KINDS[3], PoolKind::SpecialPrototypePaged);
assert!(SPECIAL_POOL_KINDS[3].is_paged());
}
#[test]
fn test_discovery_skips_missing_optional_large_layout() {
let memory = big_page_memory(LARGE_VA, 4, 0);
let mut layout = synthetic_layout();
layout.types.remove("_HEAP_LARGE_ALLOC_DATA");
let mut discovery = Discovery::default();
discover_large_allocations(
&memory,
&layout,
HEAP,
0,
PoolKind::NonPagedNx,
HeapIdentity {
pool_state: STATE,
heap: HEAP,
special: false,
},
0,
1024,
&mut discovery,
)
.unwrap();
assert!(discovery.regions.is_empty());
assert_eq!(memory.read_calls.get(), 0);
}
#[test]
fn test_big_page_lookup_skips_missing_optional_layout() {
let memory = big_page_memory(LARGE_VA, 4, 0);
let mut layout = synthetic_layout();
layout.types.remove("_POOL_TRACKER_BIG_PAGES");
assert_eq!(
lookup_big_page_target(&memory, &layout, LARGE_VA, &mut Vec::new()).unwrap(),
None
);
assert_eq!(memory.read_calls.get(), 0);
}
#[test]
fn test_big_page_lookup_batches_collision_chain() {
let memory = big_page_memory(LARGE_VA, 512, 300);
let layout = synthetic_layout();
let reads_before = memory.read_calls.get();
let mut diagnostics = Vec::new();
assert_eq!(
lookup_big_page_target(&memory, &layout, LARGE_VA, &mut diagnostics).unwrap(),
Some((u32::from_le_bytes(*b"BTCH"), 0x9000))
);
assert!(memory.read_calls.get() - reads_before <= 5);
assert!(diagnostics.is_empty());
}
#[test]
fn test_big_page_lookup_honors_interrupt_between_batches() {
let mut memory = big_page_memory(LARGE_VA, 512, 300);
let layout = synthetic_layout();
memory.interrupt_after_checks = Some(1);
assert!(matches!(
lookup_big_page_target(&memory, &layout, LARGE_VA, &mut Vec::new()),
Err(SnapshotError::Interrupted)
));
assert_eq!(memory.interrupt_checks.get(), 2);
}
#[test]
fn test_snapshot_errors_preserve_category_and_source() {
let memory = synthetic_memory();
let error = memory.read_exact(0, 1).unwrap_err();
assert!(matches!(&error, SnapshotError::Read { .. }));
assert!(std::error::Error::source(&error).is_some());
let error = guarded_read(&memory, SEGMENT + 0x7800, 0x10).unwrap_err();
assert!(matches!(error, SnapshotError::RegionValidation { .. }));
let error = scalar(&ShortMemory, 0x1000, 1).unwrap_err();
assert!(matches!(
error,
SnapshotError::InvalidData { detail } if detail == "short u8"
));
}
}