use std::collections::{HashMap, HashSet};
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::time::{Duration, Instant};
use thiserror::Error;
use super::decode::{
MAX_POOL_HEADER_CHUNK, PAGE_SIZE, PoolHeaderLayout, SpecialPoolHeader, VS_UNIT, big_page_hash,
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, SlotBackReference, VsShape},
};
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 vs_skips_page_end: bool,
pub known_tag: Option<u32>,
pub states: Vec<PoolState>,
pub reusable_chunks: SharedChunks,
pub cached_chunks: SharedChunks,
pub big_pool: Arc<HashMap<u64, BigPageEntry>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct CommitRun {
pub end: u64,
pub committed: bool,
}
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
}
fn committed_run(&self, _address: u64) -> Option<CommitRun> {
None
}
}
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()
}
fn committed_run(&self, address: u64) -> Option<CommitRun> {
self.inner.committed_run(address)
}
}
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 committed_run(&self, address: u64) -> Option<CommitRun> {
if crate::dbgeng::DebugEngine::is_kernel_target(self).ok()? {
return None;
}
let region = self.virtual_region(address).ok()?;
if !region.contains(address) {
return None;
}
Some(CommitRun {
end: region.end()?,
committed: region.state.committed()?,
})
}
}
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() {
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> {
check_budget(memory)?;
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(VS_UNIT));
}
}
}
#[derive(Default)]
struct Discovery {
regions: Vec<PoolRegion>,
diagnostics: Vec<String>,
big_pages: Option<BigPageTable>,
}
impl Discovery {
fn big_page(
&mut self,
memory: &impl PoolMemory,
address: u64,
) -> Result<Option<BigPageEntry>, SnapshotError> {
let Self {
diagnostics,
big_pages,
..
} = self;
let Some(table) = big_pages.as_mut() else {
return Ok(None);
};
table.lookup(memory, address, diagnostics)
}
}
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,
)?;
discovery.big_pages = BigPageTable::open(memory, layout, &mut discovery.diagnostics)?;
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> {
let shape = match layout.vs_shape() {
Ok(shape) => shape,
Err(detail) => {
diagnostics.push(format!(
"VS free-chunk state is in neither the context nor an affinity slot: {detail}"
));
return Ok(Vec::new());
}
};
let (slot_map_ref_offset, affinity_offset, slot_ref_offset, tree_offset, delay_offset, back) =
match shape {
VsShape::Inline { tree, delay } => {
return Ok(vec![VsRoot {
base: context,
tree_offset: tree,
delay_offset: delay,
}]);
}
VsShape::AffinitySlots {
slot_map_ref,
affinity_mask,
slot_ref,
tree,
delay,
back,
} => (slot_map_ref, affinity_mask, slot_ref, tree, delay, back),
};
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_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;
}
let back_offset = match back {
SlotBackReference::Address(offset) | SlotBackReference::Displacement(offset) => offset,
};
let claimed = scalar(memory, slot + back_offset as u64, 8).map(|value| match back {
SlotBackReference::Address(_) => Some(value),
SlotBackReference::Displacement(_) => slot.checked_sub(value),
});
match claimed {
Ok(Some(owner)) if owner == context => roots.push(VsRoot {
base: slot,
tree_offset,
delay_offset,
}),
Ok(Some(owner)) => diagnostics.push(format!(
"VS affinity slot {slot:#x} claims context {owner:#x}, not {context:#x}; skipped"
)),
Ok(None) => diagnostics.push(format!(
"VS affinity slot {slot:#x} claims context none, not {context:#x}; skipped"
)),
Err(error) => {
diagnostics.push(format!("cannot read VS affinity slot {slot:#x}: {error}"))
}
}
}
Ok(roots)
}
const VS_CONFIG_PAGE_ALIGN: u64 = 1;
fn vs_skips_page_end(
memory: &impl PoolMemory,
layout: &PoolLayout,
heap_address: u64,
diagnostics: &mut Vec<String>,
) -> Result<bool, SnapshotError> {
let Ok(context_offset) = layout.field("_SEGMENT_HEAP", "VsContext") else {
return Ok(false);
};
let Ok(config_offset) = layout.field("_HEAP_VS_CONTEXT", "Config") else {
diagnostics.push(format!(
"VS context of heap {heap_address:#x} has no resolved Config: a user block ending \
a page is read at its chunk header"
));
return Ok(false);
};
check_budget(memory)?;
let address = heap_address + context_offset as u64 + config_offset as u64;
Ok(match scalar(memory, address, 1) {
Ok(flags) => flags & VS_CONFIG_PAGE_ALIGN != 0,
Err(error) => {
diagnostics.push(format!(
"cannot read VS context config at {address:#x}: {error}; a user block ending a \
page is read at its chunk header"
));
false
}
})
}
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,
)?;
let vs_skips_page_end =
vs_skips_page_end(memory, layout, heap_address, &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,
vs_skips_page_end,
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,
vs_skips_page_end: bool,
discovery: &mut Discovery,
) -> Result<(), SnapshotError> {
let segment = layout.type_layout("_HEAP_PAGE_SEGMENT")?;
let descriptor = layout.type_layout("_HEAP_PAGE_RANGE_DESCRIPTOR")?;
let lfh_bitmap = layout.lfh_bitmap();
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)?;
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,
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);
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);
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;
};
let descriptor_node = metadata_address + offset as u64 + tree_node_offset as u64;
if decoded.first
&& !free_nodes.contains(&descriptor_node)
&& !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;
let raw = match guarded_read(
memory,
address + bitmap_offset as u64,
lfh_bitmap.bytes_for(lfh.blocks),
) {
Ok(bytes) => bytes,
Err(error) => {
discovery
.diagnostics
.push(format!("cannot read LFH bitmap at {address:#x}: {error}"));
descriptor_index += unit_size;
continue;
}
};
bitmap = match lfh_bitmap.normalise(&raw, lfh.blocks) {
Some(busy) => busy,
None => {
discovery.diagnostics.push(format!(
"LFH bitmap at {address:#x} is {} bytes, short of its {} blocks",
raw.len(),
lfh.blocks
));
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 state = if free_nodes.contains(&descriptor_node) || !decoded.allocated() {
PoolState::ReusableFree
} else {
PoolState::Allocated
};
let mut big_pool = HashMap::new();
if backend == PoolBackend::Vs && !identity.special {
let mut page = region_address.next_multiple_of(PAGE_SIZE);
let end = region_address.saturating_add(region_size as u64);
while page < end {
if let Some(entry) = discovery.big_page(memory, page)? {
big_pool.insert(page, entry);
}
page = page.saturating_add(PAGE_SIZE);
}
}
let big_pool = Arc::new(big_pool);
let mut pool_header = layout.pool_header_layout()?;
let mut known_tag = None;
if backend == PoolBackend::Segment
&& !identity.special
&& state == PoolState::Allocated
&& let Some(big_page) = discovery.big_page(memory, region_address)?
{
known_tag = Some(big_page.tag);
pool_header.size = 0;
if big_page.size != 0 && big_page.size < region_size as u64 {
region_size = big_page.size as usize;
block_size = region_size.min(u32::MAX as usize) as u32;
}
}
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,
vs_header_size: layout.type_layout("_HEAP_VS_CHUNK_HEADER")?.size as usize,
vs_sizes_offset: layout.field("_HEAP_VS_CHUNK_HEADER", "Sizes")?,
vs_skips_page_end,
known_tag,
states: vec![state],
reusable_chunks: Arc::clone(reusable_chunks),
cached_chunks: Arc::clone(cached_chunks),
big_pool: Arc::clone(&big_pool),
});
descriptor_index += unit_size;
}
entry = read_u64(&segment_header, list_entry).unwrap_or(0);
}
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 discovery.big_page(memory, virtual_address)? {
Some(big_page) => (big_page.tag, big_page.size),
None => {
discovery.diagnostics.push(format!(
"no validated big-page entry for large allocation {virtual_address:#x}"
));
(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,
vs_skips_page_end: false,
known_tag: Some(tag),
states: vec![PoolState::Allocated],
reusable_chunks: Arc::default(),
cached_chunks: Arc::default(),
big_pool: Arc::default(),
});
}
Ok(())
}
const BIG_PAGE_BATCH: usize = 256;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct BigPageEntry {
pub tag: u32,
pub size: u64,
}
struct BigPageTable {
base: u64,
count: usize,
entry_size: usize,
va_offset: usize,
tag_offset: usize,
size_offset: usize,
batches: Vec<Option<Vec<u8>>>,
}
impl BigPageTable {
fn open(
memory: &impl PoolMemory,
layout: &PoolLayout,
diagnostics: &mut Vec<String>,
) -> Result<Option<Self>, SnapshotError> {
let (Ok(entry), Ok(va_offset), Ok(tag_offset), Ok(size_offset)) = (
layout.type_layout("_POOL_TRACKER_BIG_PAGES"),
layout.field("_POOL_TRACKER_BIG_PAGES", "Va"),
layout.field("_POOL_TRACKER_BIG_PAGES", "Key"),
layout.field("_POOL_TRACKER_BIG_PAGES", "NumberOfBytes"),
) else {
return Ok(None);
};
let (Some(&table_pointer_address), Some(&size_address)) = (
layout.globals.get("PoolBigPageTable"),
layout.globals.get("PoolBigPageTableSize"),
) else {
return Ok(None);
};
check_budget(memory)?;
let base = 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 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 base == 0 || count == 0 || count > 0x10_0000 || !count.is_power_of_two() {
diagnostics.push(format!("rejecting implausible big-page table size {count}"));
return Ok(None);
}
Ok(Some(Self {
base,
count,
entry_size: entry.size as usize,
va_offset,
tag_offset,
size_offset,
batches: vec![None; count.div_ceil(BIG_PAGE_BATCH)],
}))
}
fn lookup(
&mut self,
memory: &impl PoolMemory,
address: u64,
diagnostics: &mut Vec<String>,
) -> Result<Option<BigPageEntry>, SnapshotError> {
let mut index =
big_page_hash(address, self.count).ok_or_else(|| SnapshotError::InvalidData {
detail: format!("invalid big-page table size {}", self.count),
})?;
for _ in 0..self.count {
let entry_size = self.entry_size;
let offset = (index % BIG_PAGE_BATCH) * entry_size;
let (va_offset, tag_offset, size_offset) =
(self.va_offset, self.tag_offset, self.size_offset);
let Some(bytes) = self.batch(memory, index / BIG_PAGE_BATCH, diagnostics)? else {
return Ok(None);
};
let Some(candidate) = read_u64(bytes, offset + va_offset) else {
return Ok(None);
};
if candidate <= 1 {
return Ok(None);
}
if candidate == address {
let (Some(tag), Some(size)) = (
read_u32(bytes, offset + tag_offset),
read_u64(bytes, offset + size_offset),
) else {
diagnostics.push(format!("truncated big-page entry {index}"));
return Ok(None);
};
return Ok(Some(BigPageEntry { tag, size }));
}
index = (index + 1) % self.count;
}
Ok(None)
}
fn batch(
&mut self,
memory: &impl PoolMemory,
batch: usize,
diagnostics: &mut Vec<String>,
) -> Result<Option<&[u8]>, SnapshotError> {
if self.batches.get(batch).is_none_or(Option::is_some) {
return Ok(self.batches.get(batch).and_then(Option::as_deref));
}
check_budget(memory)?;
let first = batch * BIG_PAGE_BATCH;
let len = BIG_PAGE_BATCH.min(self.count - first);
let (Some(byte_len), Some(entry_address)) = (
self.entry_size.checked_mul(len),
self.base.checked_add(first as u64 * self.entry_size as u64),
) else {
return Err(SnapshotError::InvalidData {
detail: "big-page batch address overflow".into(),
});
};
match guarded_read(memory, entry_address, byte_len) {
Ok(bytes) => self.batches[batch] = Some(bytes),
Err(error) => {
diagnostics.push(format!(
"cannot read big-page entries {first}..{} at {entry_address:#x}: {error}",
first + len
));
self.batches[batch] = Some(Vec::new());
}
}
Ok(self.batches[batch].as_deref())
}
}
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 stopped_after_matches: Option<usize>,
pub match_limit: Option<PoolMatchLimit>,
pub matched_allocations: usize,
pub stalls: WalkStalls,
pub refused_chunks: u64,
pub unplaced_bytes: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PoolMatchLimit {
raw_tag: u32,
paged: Option<bool>,
matches: NonZeroUsize,
}
impl PoolMatchLimit {
pub(crate) fn new(raw_tag: u32, paged: Option<bool>, matches: NonZeroUsize) -> Self {
Self {
raw_tag,
paged,
matches,
}
}
fn accepts(self, span: &PoolSpan) -> bool {
span.state == PoolState::Allocated
&& span.raw_tag == self.raw_tag
&& self
.paged
.is_none_or(|paged| span.pool_kind.is_paged() == paged)
}
}
impl PoolSnapshot {
fn record_span(&mut self, span: PoolSpan) {
let matched = self.match_limit.is_some_and(|limit| limit.accepts(&span));
self.spans.push(span);
if !matched {
return;
}
self.matched_allocations = self.matched_allocations.saturating_add(1);
let Some(limit) = self.match_limit else {
return;
};
if self.matched_allocations >= limit.matches.get() {
self.complete = false;
self.stopped_after_matches = Some(limit.matches.get());
}
}
fn match_limit_reached(&self) -> bool {
self.stopped_after_matches.is_some()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct WalkStalls {
pub pages: u64,
pub skipped_bytes: u64,
pub recovered_bytes: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct VsResume {
next: u64,
covered_through: 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> {
self.walk_until(budget, None)
}
pub(crate) fn walk_until(
&self,
budget: Option<Duration>,
match_limit: Option<PoolMatchLimit>,
) -> 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,
match_limit,
..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(()) if snapshot.match_limit_reached() => break,
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> {
if region.backend == PoolBackend::Segment
&& let Some(tag) = region.known_tag
{
let state = region
.states
.first()
.copied()
.unwrap_or(PoolState::Allocated);
snapshot.record_span(self.base_span(
region,
region.address,
region.address,
region.size as u64,
tag,
state,
));
return Ok(());
}
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);
let mut vs_covered = 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);
let gap_start = cursor.max(vs_covered).min(valid_base);
if valid_base > gap_start
&& self.unreadable(region, gap_start, valid_base - gap_start, snapshot)
{
snapshot.diagnostics.push(format!(
"region {:#x}+{:#x} is only committed through {gap_start:#x}; unreadable space extends {:#x} bytes",
region.address,
region.size,
valid_base - gap_start
));
}
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 {
let gap_start = valid_base.max(vs_covered).min(requested_end);
self.unreadable(region, gap_start, requested_end - gap_start, 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"
));
let gap_start = valid_base.max(vs_covered).min(valid_base + skip);
self.unreadable(region, gap_start, valid_base + skip - gap_start, 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);
if snapshot.match_limit_reached() {
return Ok(());
}
cursor = valid_end;
continue;
}
match region.backend {
PoolBackend::Lfh => self.walk_lfh(region, valid_base, &bytes, snapshot),
PoolBackend::Vs => {
let resumed = self.walk_vs(region, valid_base, &bytes, vs_chunk, snapshot);
vs_chunk = resumed.map(|resume| resume.next);
if let Some(resume) = resumed {
vs_covered = vs_covered.max(resume.covered_through);
}
}
PoolBackend::Segment => self.walk_page_ranges(region, valid_base, &bytes, snapshot),
PoolBackend::Large => return Ok(()),
}
if snapshot.match_limit_reached() {
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.record_span(
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,
) -> bool {
let Some(end) = (size != 0).then(|| address.saturating_add(size)) else {
return false;
};
let mut cursor = address;
let mut missed = false;
while cursor < end {
let Some(run) = self
.memory
.committed_run(cursor)
.filter(|run| run.end > cursor)
else {
return self.gap(
region,
cursor,
end - cursor,
PoolState::Unreadable,
snapshot,
) || missed;
};
let stop = run.end.min(end);
let state = if run.committed {
PoolState::Unreadable
} else {
PoolState::Uncommitted
};
missed |= self.gap(region, cursor, stop - cursor, state, snapshot);
cursor = stop;
}
missed
}
fn gap(
&self,
region: &PoolRegion,
address: u64,
size: u64,
state: PoolState,
snapshot: &mut PoolSnapshot,
) -> bool {
if size == 0 {
return false;
}
let missed = state.is_coverage_gap();
if missed {
snapshot.complete = false;
}
snapshot.record_span(self.base_span(region, address, address, size, 0, state));
missed
}
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.record_span(self.base_span(
region,
page,
usable,
size,
header.tag,
PoolState::Allocated,
));
if snapshot.match_limit_reached() {
break;
}
}
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.record_span(self.base_span(
region,
address,
usable,
unit as u64 - region.pool_header.size as u64,
tag,
state,
));
if snapshot.match_limit_reached() {
break;
}
slot += 1;
}
}
fn walk_vs(
&self,
region: &PoolRegion,
base: u64,
bytes: &[u8],
expected: Option<u64>,
snapshot: &mut PoolSnapshot,
) -> Option<VsResume> {
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(VsResume {
next,
covered_through: base,
});
}
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;
let mut covered_through = base;
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;
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 big_pool = (chunk_size as u64 > MAX_POOL_HEADER_CHUNK)
.then(|| {
let end = header_address.saturating_add(chunk_size as u64);
region.big_pool.iter().find(|(address, entry)| {
(header_address..end).contains(*address)
&& address.saturating_add(entry.size) <= end
})
})
.flatten()
.map(|(address, entry)| (*address, *entry));
if let Some((address, entry)) = big_pool {
let mut span =
self.base_span(region, address, address, entry.size, entry.tag, state);
span.size_class = chunk_size.min(u32::MAX as usize) as u32;
snapshot.record_span(span);
previous_chunk = Some(chunk_size);
offset += chunk_size;
resume = base + offset as u64;
covered_through = resume;
chunks += 1;
if snapshot.match_limit_reached() {
break;
}
continue;
}
if offset.saturating_add(chunk_size) > bytes.len()
&& !self
.memory
.committed_run(base.saturating_add(bytes.len() as u64))
.is_some_and(|run| !run.committed)
{
resume = header_address.saturating_add(chunk_size as u64);
snapshot.diagnostics.push(format!(
"VS chunk at {header_address:#x} is {chunk_size:#x} bytes and runs \
{:#x} past the committed extent at {:#x}; no span is emitted for it",
offset.saturating_add(chunk_size) - bytes.len(),
base.saturating_add(bytes.len() as u64)
));
snapshot.complete = false;
break;
}
let mut physical_header = header_address + region.vs_header_size as u64;
if region.vs_skips_page_end
&& physical_header.saturating_add(VS_UNIT) & (PAGE_SIZE - 1) == 0
{
if chunk_size >= header_bytes.saturating_add(VS_UNIT as usize) {
physical_header += VS_UNIT;
} else if chunk.allocated {
snapshot.diagnostics.push(format!(
"VS chunk at {header_address:#x} is {chunk_size:#x} bytes, too short to \
hold a user block moved off the page end; read in place"
));
}
}
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 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.record_span(span);
previous_chunk = Some(chunk_size);
offset += chunk_size;
resume = base + offset as u64;
chunks += 1;
if snapshot.match_limit_reached() {
break;
}
}
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 !snapshot.match_limit_reached() && chunks >= self.traversal_limit {
snapshot.complete = false;
snapshot
.diagnostics
.push(format!("VS traversal limit reached at {base:#x}"));
}
(!lost).then_some(VsResume {
next: resume,
covered_through,
})
}
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.record_span(self.base_span(
region,
address,
address + header_size as u64,
size.saturating_sub(header_size) as u64,
tag.unwrap_or(0),
state,
));
if snapshot.match_limit_reached() {
break;
}
slot += 1;
}
}
}
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,
vs_skips_page_end: false,
known_tag: None,
states: Vec::new(),
reusable_chunks: Arc::default(),
cached_chunks: Arc::default(),
big_pool: 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(VsFixture::Inline);
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"
);
}
fn match_limited_snapshot(tag: &[u8; 4], matches: usize) -> PoolSnapshot {
PoolSnapshot {
complete: true,
match_limit: Some(PoolMatchLimit::new(
u32::from_le_bytes(*tag),
None,
NonZeroUsize::new(matches).expect("test match limits are nonzero"),
)),
..PoolSnapshot::default()
}
}
fn walk_page_tag(tag: [u8; 4], slots: usize, stop_after: usize) -> PoolSnapshot {
let bytes = vec![0; slots * 0x20];
let mut region = lfh_region(0x4000, 0x20);
region.backend = PoolBackend::Segment;
region.size = bytes.len();
region.known_tag = Some(u32::from_le_bytes(tag));
region.states = vec![PoolState::Allocated; slots];
let memory = FlatMemory::new(region.address, bytes.len());
let layout = vs_layout(VsFixture::Inline);
let walker = SnapshotWalker {
memory: &memory,
layout: &layout,
traversal_limit: 1000,
};
let mut snapshot = match_limited_snapshot(b"Tgsm", stop_after);
walker.walk_page_ranges(®ion, region.address, &bytes, &mut snapshot);
snapshot
}
#[test]
fn test_match_limit_does_not_stop_a_walk_with_zero_matches() {
let snapshot = walk_page_tag(*b"Othr", 3, 1);
assert_eq!(snapshot.spans.len(), 3);
assert_eq!(snapshot.stopped_after_matches, None);
assert!(snapshot.complete);
}
#[test]
fn test_match_limit_stops_on_the_first_match() {
let snapshot = walk_page_tag(*b"Tgsm", 3, 1);
assert_eq!(snapshot.spans.len(), 1);
assert_eq!(snapshot.stopped_after_matches, Some(1));
assert_eq!(snapshot.matched_allocations, 1);
assert!(!snapshot.complete);
}
#[test]
fn test_match_limit_stops_on_the_requested_nth_match() {
let snapshot = walk_page_tag(*b"Tgsm", 4, 2);
assert_eq!(snapshot.spans.len(), 2);
assert_eq!(snapshot.stopped_after_matches, Some(2));
assert_eq!(snapshot.matched_allocations, 2);
assert_eq!(snapshot.spans.last().unwrap().display_tag, "Tgsm");
}
#[test]
fn test_match_limit_counts_only_allocated_chunks_in_scope() {
let raw_tag = u32::from_le_bytes(*b"Tgsm");
let mut snapshot = PoolSnapshot {
complete: true,
match_limit: Some(PoolMatchLimit::new(
raw_tag,
Some(false),
NonZeroUsize::new(2).unwrap(),
)),
..PoolSnapshot::default()
};
let span = |address, kind, state| {
let mut span = PoolSpan::allocation(
address,
0x20,
raw_tag,
kind,
HeapIdentity {
pool_state: 1,
heap: 1,
special: false,
},
PoolBackend::Lfh,
);
span.state = state;
span
};
snapshot.record_span(span(0x1000, PoolKind::Paged, PoolState::Allocated));
snapshot.record_span(span(0x1020, PoolKind::NonPagedNx, PoolState::ReusableFree));
snapshot.record_span(span(0x1040, PoolKind::NonPagedNx, PoolState::Allocated));
assert_eq!(snapshot.stopped_after_matches, None);
snapshot.record_span(span(0x1060, PoolKind::NonPagedNx, PoolState::Allocated));
assert_eq!(snapshot.stopped_after_matches, Some(2));
}
#[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(VsFixture::Inline);
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;
#[derive(Clone, Copy, PartialEq, Eq)]
enum VsFixture {
Inline,
SlotsByAddress,
SlotsByDisplacement,
}
fn vs_layout(shape: VsFixture) -> PoolLayout {
let mut types = HashMap::new();
types.insert(
"_HEAP_VS_CONTEXT",
match shape {
VsFixture::Inline => {
type_layout(0x80, &[("FreeChunkTree", 0x10), ("DelayFreeContext", 0x30)])
}
_ => type_layout(0x60, &[("SlotMapRef", 0), ("AffinityMask", 2)]),
},
);
if shape != VsFixture::Inline {
let back = match shape {
VsFixture::SlotsByDisplacement => "VsContextOffset",
_ => "VsContext",
};
types.insert(
"_HEAP_VS_AFFINITY_SLOT",
type_layout(
0x80,
&[
(back, 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
}
fn displacement_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, 0x800); memory.put_u64(VS_CONTEXT + 0xc00, 0xc00); memory.put_u64(VS_CONTEXT + 0x1000, 0x1234); 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(VsFixture::SlotsByAddress),
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(VsFixture::Inline),
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(VsFixture::SlotsByAddress),
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_walks_affinity_slots_that_name_their_context_by_displacement() {
let memory = displacement_fixture();
let mut diagnostics = Vec::new();
let roots = vs_roots(
&memory,
&vs_layout(VsFixture::SlotsByDisplacement),
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_the_two_affinity_back_reference_rules_do_not_substitute_for_each_other() {
for (fixture, wrong_shape, what) in [
(
displacement_fixture(),
VsFixture::SlotsByAddress,
"displacements read as addresses",
),
(
affinity_fixture(),
VsFixture::SlotsByDisplacement,
"addresses read as displacements",
),
] {
let mut diagnostics = Vec::new();
let roots = vs_roots(
&fixture,
&vs_layout(wrong_shape),
VS_CONTEXT,
&mut diagnostics,
)
.unwrap();
assert!(
roots.is_empty(),
"{what}: {} slot(s) were accepted by the wrong rule, so the tree walk would read \
unrelated memory",
roots.len()
);
assert!(
diagnostics
.iter()
.all(|line| line.contains("claims context")),
"{what}: every slot should be rejected for naming another context, not skipped \
for some other reason: {diagnostics:?}"
);
}
}
#[test]
fn test_vs_roots_rejects_a_displacement_that_runs_off_the_bottom() {
let mut memory = displacement_fixture();
memory.put_u64(VS_CONTEXT + 0x800, u64::MAX);
let mut diagnostics = Vec::new();
let roots = vs_roots(
&memory,
&vs_layout(VsFixture::SlotsByDisplacement),
VS_CONTEXT,
&mut diagnostics,
)
.unwrap();
assert_eq!(
roots.iter().map(|root| root.base).collect::<Vec<_>>(),
vec![VS_CONTEXT + 0xc00],
"the slot with the impossible displacement should be the only one dropped"
);
assert!(
diagnostics
.iter()
.any(|line| line.contains("claims context none")),
"a displacement that cannot name a context should say so: {diagnostics:?}"
);
}
#[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(VsFixture::SlotsByAddress),
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(VsFixture::SlotsByAddress),
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,
vs_skips_page_end: false,
known_tag: None,
states: Vec::new(),
reusable_chunks: Arc::default(),
cached_chunks: Arc::default(),
big_pool: 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,
vs_skips_page_end: false,
known_tag: None,
states: Vec::new(),
reusable_chunks: Arc::default(),
cached_chunks: Arc::default(),
big_pool: 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 {
walk_vs_region(vs_region(bytes.len()), bytes)
}
fn walk_vs_region(region: PoolRegion, bytes: &[u8]) -> PoolSnapshot {
let memory = FlatMemory::new(VS_BASE, bytes.len());
let layout = vs_layout(VsFixture::Inline);
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
}
fn vs_extent_with_a_user_block_moved_off_the_page_end() -> Vec<u8> {
let mut bytes = vs_extent(&[(0xfe0, 0), (0x80, 0xfe0), (0x40, 0x80)]);
bytes[0xff0..0xff8].copy_from_slice(&0xffff_e784_5920_7e18u64.to_le_bytes());
bytes[0xff8..0x1000].copy_from_slice(&0xffff_e784_5921_3bc8u64.to_le_bytes());
bytes[0x1000..0x1004].copy_from_slice(&0x0306_0000u32.to_le_bytes());
bytes[0x1004..0x1008].copy_from_slice(b"MmDT");
bytes
}
fn span_of_class(snapshot: &PoolSnapshot, size_class: u32) -> &PoolSpan {
snapshot
.spans
.iter()
.find(|span| span.size_class == size_class)
.unwrap_or_else(|| panic!("no span of class {size_class:#x}: {:?}", snapshot.spans))
}
#[test]
fn test_a_vs_user_block_that_would_end_a_page_begins_the_next_one() {
let bytes = vs_extent_with_a_user_block_moved_off_the_page_end();
let mut region = vs_region(bytes.len());
region.vs_skips_page_end = true;
let snapshot = walk_vs_region(region, &bytes);
let moved = span_of_class(&snapshot, 0x80);
assert_eq!(moved.header_address, VS_BASE + 0x1000);
assert_eq!(moved.usable_address, VS_BASE + 0x1010);
assert_eq!(moved.size, 0x50);
assert_eq!(moved.raw_tag, u32::from_le_bytes(*b"MmDT"));
let after = span_of_class(&snapshot, 0x40);
assert_eq!(after.header_address, VS_BASE + 0x1060 + 0x10);
assert_eq!(after.raw_tag, u32::from_le_bytes(*b"VS!!"));
assert_eq!(snapshot.spans.len(), 3);
assert!(snapshot.complete, "{:?}", snapshot.diagnostics);
}
#[test]
fn test_without_the_flag_a_page_end_user_block_is_read_at_its_chunk_header() {
let bytes = vs_extent_with_a_user_block_moved_off_the_page_end();
let snapshot = walk_vs_extent(&bytes);
let unmoved = span_of_class(&snapshot, 0x80);
assert_eq!(unmoved.header_address, VS_BASE + 0xff0);
assert_eq!(unmoved.size, 0x60);
assert_eq!(unmoved.raw_tag, 0xffff_e784);
}
#[test]
fn test_a_vs_user_block_on_a_page_boundary_is_left_where_it_is() {
let bytes = vs_extent(&[(0xff0, 0), (0x40, 0xff0)]);
let mut region = vs_region(bytes.len());
region.vs_skips_page_end = true;
let snapshot = walk_vs_region(region, &bytes);
let on_boundary = span_of_class(&snapshot, 0x40);
assert_eq!(on_boundary.header_address, VS_BASE + 0x1000);
assert_eq!(on_boundary.size, 0x20);
assert_eq!(on_boundary.raw_tag, u32::from_le_bytes(*b"VS!!"));
}
#[test]
fn test_a_vs_chunk_too_short_to_move_its_user_block_is_read_in_place() {
let bytes = vs_extent(&[(0xfe0, 0), (0x20, 0xfe0), (0x40, 0x20)]);
let mut region = vs_region(bytes.len());
region.vs_skips_page_end = true;
let snapshot = walk_vs_region(region, &bytes);
let short = span_of_class(&snapshot, 0x20);
assert_eq!(short.header_address, VS_BASE + 0xff0);
assert_eq!(short.usable_address, VS_BASE + 0x1000);
assert_eq!(short.size, 0);
let after = span_of_class(&snapshot, 0x40);
assert_eq!(after.header_address, VS_BASE + 0x1010);
assert_eq!(after.raw_tag, u32::from_le_bytes(*b"VS!!"));
assert_eq!(snapshot.spans.len(), 3);
assert!(
snapshot
.diagnostics
.examples()
.iter()
.any(|line| line.contains("too short to hold a user block moved off the page end")),
"{:?}",
snapshot.diagnostics
);
}
#[test]
fn test_a_vs_chunk_too_large_for_a_pool_header_is_tagged_from_the_big_page_table() {
let mut bytes = vs_extent(&[(0x2000, 0), (0x40, 0x2000)]);
let mut region = vs_region(bytes.len());
let allocation = (VS_BASE + 0x10).next_multiple_of(PAGE_SIZE);
region.big_pool = Arc::new(HashMap::from([(
allocation,
BigPageEntry {
tag: u32::from_le_bytes(*b"BIGV"),
size: 0x1000,
},
)]));
Arc::get_mut(&mut region.big_pool)
.unwrap()
.insert(VS_BASE + 0x9000, BigPageEntry { tag: 0, size: 0x10 });
let _ = &mut bytes;
let memory = FlatMemory::new(VS_BASE, bytes.len());
let layout = vs_layout(VsFixture::Inline);
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);
let big = snapshot
.spans
.iter()
.find(|span| span.size_class == 0x2000)
.unwrap_or_else(|| panic!("the large chunk is missing: {:?}", snapshot.spans));
assert_eq!(super::super::decode::display_tag(big.raw_tag), "BIGV");
assert_eq!(big.header_address, allocation);
assert_eq!(big.usable_address, allocation);
assert_eq!(big.size, 0x1000);
let small = snapshot
.spans
.iter()
.find(|span| span.size_class == 0x40)
.unwrap_or_else(|| panic!("the small chunk is missing: {:?}", snapshot.spans));
assert_eq!(super::super::decode::display_tag(small.raw_tag), "VS!!");
}
#[test]
fn test_a_big_page_entry_too_long_for_its_chunk_is_not_claimed_by_it() {
let bytes = vs_extent(&[(0x2000, 0), (0x40, 0x2000)]);
let mut region = vs_region(bytes.len());
let allocation = (VS_BASE + 0x10).next_multiple_of(PAGE_SIZE);
region.big_pool = Arc::new(HashMap::from([(
allocation,
BigPageEntry {
tag: u32::from_le_bytes(*b"BIGV"),
size: (VS_BASE + 0x2000 - allocation) + 1,
},
)]));
let memory = FlatMemory::new(VS_BASE, bytes.len());
let layout = vs_layout(VsFixture::Inline);
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);
let big = snapshot
.spans
.iter()
.find(|span| span.size_class == 0x2000)
.unwrap_or_else(|| panic!("the large chunk is missing: {:?}", snapshot.spans));
assert_ne!(super::super::decode::display_tag(big.raw_tag), "BIGV");
assert_eq!(big.header_address, VS_BASE + 0x10);
}
#[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>,
uncommitted: Option<HashSet<u64>>,
queries: Cell<usize>,
commit_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(),
uncommitted: None,
queries: Cell::new(0),
commit_queries: Cell::new(0),
}
}
fn with_memory_manager(mut self, uncommitted: &[u64]) -> Self {
self.uncommitted = Some(uncommitted.iter().copied().collect());
self
}
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 committed_run(&self, address: u64) -> Option<CommitRun> {
let uncommitted = self.uncommitted.as_ref()?;
self.commit_queries.set(self.commit_queries.get() + 1);
let mut page = address & !(PAGE_SIZE - 1);
let committed = !uncommitted.contains(&page);
loop {
page += PAGE_SIZE;
if page >= self.end() {
return Some(CommitRun {
end: u64::MAX,
committed,
});
}
if !uncommitted.contains(&page) != committed {
return Some(CommitRun {
end: page,
committed,
});
}
}
}
}
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(VsFixture::Inline);
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 gaps(snapshot: &PoolSnapshot) -> Vec<(PoolState, u64, u64)> {
let mut gaps: Vec<_> = snapshot
.spans
.iter()
.filter(|span| !span.state.is_chunk())
.map(|span| (span.state, span.header_address, span.size))
.collect();
gaps.sort_by_key(|&(_, address, _)| address);
gaps
}
#[test]
fn test_only_the_memory_manager_decides_whether_a_gap_costs_coverage() {
let gap = SPECIAL_PAGE + PAGE_SIZE;
let walk = |memory: HoleyMemory| {
let mut memory = memory;
memory.holes.insert(gap);
let snapshot = walk_holey(&memory, &special_region(SPECIAL_PAGE, 4));
assert_eq!(
snapshot
.spans
.iter()
.filter(|span| span.state == PoolState::Allocated)
.count(),
3,
"the gap is one page of four"
);
(snapshot, memory.commit_queries.get())
};
let (nothing_there, queries) =
walk(HoleyMemory::new(SPECIAL_PAGE, special_pages(4)).with_memory_manager(&[gap]));
assert_eq!(
gaps(¬hing_there),
[(PoolState::Uncommitted, gap, PAGE_SIZE)],
"a page with nothing behind it is not a hole in what the walk saw"
);
assert!(
nothing_there.complete,
"and so the walk covered everything it set out to"
);
assert_eq!(
queries, 1,
"asked per run, not per page — a reserved tail of any length costs one query"
);
let (really_missed, _) =
walk(HoleyMemory::new(SPECIAL_PAGE, special_pages(4)).with_memory_manager(&[]));
assert_eq!(
gaps(&really_missed),
[(PoolState::Unreadable, gap, PAGE_SIZE)],
"a committed page that will not read is memory the target has and we did not see"
);
assert!(!really_missed.complete);
let (cannot_say, _) = walk(HoleyMemory::new(SPECIAL_PAGE, special_pages(4)));
assert_eq!(
gaps(&cannot_say),
gaps(&really_missed),
"a source with no answer keeps the conservative reading, not the convenient one"
);
assert!(!cannot_say.complete);
}
#[test]
fn test_a_gap_with_nothing_behind_it_is_not_worth_a_diagnostic() {
let gap = SPECIAL_PAGE + PAGE_SIZE;
let complains = |memory: HoleyMemory| {
let mut memory = memory;
memory.holes.insert(gap);
let snapshot = walk_holey(&memory, &special_region(SPECIAL_PAGE, 4));
snapshot
.diagnostics
.examples()
.iter()
.any(|message| message.contains("only committed through"))
};
assert!(
!complains(
HoleyMemory::new(SPECIAL_PAGE, special_pages(4)).with_memory_manager(&[gap])
),
"nothing was missed, so there is nothing to report"
);
assert!(
complains(HoleyMemory::new(SPECIAL_PAGE, special_pages(4)).with_memory_manager(&[])),
"a committed page that would not read is still worth saying"
);
assert!(
complains(HoleyMemory::new(SPECIAL_PAGE, special_pages(4))),
"and so is one nothing could be asked about"
);
}
#[test]
fn test_a_gap_straddling_the_commit_boundary_is_split_at_it() {
let mut memory = HoleyMemory::new(SPECIAL_PAGE, special_pages(4))
.with_memory_manager(&[SPECIAL_PAGE + 2 * PAGE_SIZE]);
memory.holes.insert(SPECIAL_PAGE + PAGE_SIZE);
memory.holes.insert(SPECIAL_PAGE + 2 * PAGE_SIZE);
let snapshot = walk_holey(&memory, &special_region(SPECIAL_PAGE, 4));
assert_eq!(
gaps(&snapshot),
[
(PoolState::Unreadable, SPECIAL_PAGE + PAGE_SIZE, PAGE_SIZE),
(
PoolState::Uncommitted,
SPECIAL_PAGE + 2 * PAGE_SIZE,
PAGE_SIZE
),
],
"two spans, each the size of what it describes"
);
assert!(
!snapshot.complete,
"half of it was memory the target has, and that half is still missing"
);
}
#[test]
fn test_a_commit_run_that_cannot_advance_is_no_answer_at_all() {
struct Stuck(HoleyMemory);
impl PoolMemory for Stuck {
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(false)
}
fn committed_run(&self, address: u64) -> Option<CommitRun> {
Some(CommitRun {
end: address,
committed: false,
})
}
}
let mut inner = HoleyMemory::new(SPECIAL_PAGE, special_pages(4));
inner.holes.insert(SPECIAL_PAGE + PAGE_SIZE);
let memory = Stuck(inner);
let layout = vs_layout(VsFixture::Inline);
let walker = SnapshotWalker {
memory: &memory,
layout: &layout,
traversal_limit: 1000,
};
let mut snapshot = PoolSnapshot {
complete: true,
..PoolSnapshot::default()
};
walker
.walk_region(&special_region(SPECIAL_PAGE, 4), &mut snapshot)
.unwrap();
assert_eq!(
gaps(&snapshot),
[(PoolState::Unreadable, SPECIAL_PAGE + PAGE_SIZE, PAGE_SIZE)],
"an answer that cannot advance is discarded, not acted on"
);
assert!(!snapshot.complete);
}
fn walk_vs_with_holes(chunks: &[(usize, usize)], holes: &[u64]) -> PoolSnapshot {
walk_vs_holes(chunks, holes, false)
}
fn walk_vs_holes(chunks: &[(usize, usize)], holes: &[u64], asked: bool) -> 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());
if asked {
memory = memory.with_memory_manager(holes);
}
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_chunk_running_into_nothing_is_still_a_chunk() {
let chunks = [(0x1000usize, 0usize), (0x3000, 0x1000)];
let holes = [VS_BASE + 0x2000];
let asked = walk_vs_holes(&chunks, &holes, true);
assert_eq!(
asked
.spans
.iter()
.filter(|span| span.state.is_chunk())
.map(|span| (span.header_address, span.size))
.collect::<Vec<_>>(),
[(VS_BASE + 0x10, 0xfe0), (VS_BASE + 0x1010, 0x2fe0)],
"the chunk that spans the hole is understood, so it is reported"
);
assert!(
asked.complete,
"and nothing about it is unknown, so the walk covered everything: {:?}",
asked.diagnostics.examples()
);
let unasked = walk_vs_holes(&chunks, &holes, false);
assert_eq!(
unasked
.spans
.iter()
.filter(|span| span.state.is_chunk())
.map(|span| span.header_address)
.collect::<Vec<_>>(),
[VS_BASE + 0x10],
"with no answer about the tail the chunk is not invented"
);
assert!(!unasked.complete);
assert!(
unasked
.diagnostics
.examples()
.iter()
.any(|message| message.contains("runs")
&& message.contains("past the committed extent")),
"and the walk says which chunk it dropped: {:?}",
unasked.diagnostics.examples()
);
}
#[test]
fn test_a_big_pool_chunk_whose_tail_is_paged_out_is_still_named_from_the_table() {
let chunks = [(0x2000usize, 0usize), (0x40, 0x2000)];
let hole = VS_BASE + 0x1000;
let allocation = hole;
let walk = |named: bool| {
let bytes = vs_extent(&chunks);
let mut region = vs_region(bytes.len());
if named {
region.big_pool = Arc::new(HashMap::from([(
allocation,
BigPageEntry {
tag: u32::from_le_bytes(*b"BIGV"),
size: 0x1000,
},
)]));
}
let mut memory = HoleyMemory::new(VS_BASE, bytes);
memory.holes.insert(hole);
let memory = memory.with_memory_manager(&[]);
walk_holey(&memory, ®ion)
};
let named = walk(true);
let big = named
.spans
.iter()
.find(|span| span.size_class == 0x2000)
.unwrap_or_else(|| panic!("the chunk the table names is missing: {:?}", named.spans));
assert_eq!(super::super::decode::display_tag(big.raw_tag), "BIGV");
assert_eq!(big.header_address, allocation);
assert_eq!(big.usable_address, allocation);
assert_eq!(big.size, 0x1000);
assert_eq!(big.state, PoolState::Allocated);
assert!(
!named
.spans
.iter()
.any(|span| span.state == PoolState::Unreadable),
"the trimmed page is inside a span the index holds, so it is not also a gap: {:?}",
named.spans
);
assert!(
named.spans.iter().any(|span| span.size_class == 0x40),
"the chain crosses the hole to the chunk after it: {:?}",
named.spans
);
assert_eq!(named.unplaced_bytes, 0);
assert!(
named.complete,
"nothing about the chunk was unknown: {:?}",
named.diagnostics.examples()
);
let unnamed = walk(false);
assert!(
!unnamed.spans.iter().any(|span| span.size_class == 0x2000),
"without the table the chunk is not invented: {:?}",
unnamed.spans
);
assert!(
unnamed
.spans
.iter()
.any(|span| span.state == PoolState::Unreadable && span.header_address == hole),
"and its trimmed page is filed as the gap it is: {:?}",
unnamed.spans
);
assert!(!unnamed.complete);
}
#[test]
fn test_a_page_range_the_table_names_is_answered_without_reading_it() {
let size = 0x2000usize;
let walk = |named: bool| {
let mut region = vs_region(size);
region.backend = PoolBackend::Segment;
region.unit_size = size as u32;
region.states = vec![PoolState::Allocated];
region.pool_header.size = 0;
region.known_tag = named.then_some(u32::from_le_bytes(*b"BIGP"));
let mut memory = HoleyMemory::new(VS_BASE, vec![0; size]);
memory.holes.extend([VS_BASE, VS_BASE + 0x1000]);
let memory = memory.with_memory_manager(&[]);
let snapshot = walk_holey(&memory, ®ion);
(snapshot, memory.queries.get())
};
let (named, queries) = walk(true);
assert_eq!(queries, 0, "the table answered; the pages were not asked");
let [span] = named.spans.as_slice() else {
panic!("one span for one allocation: {:?}", named.spans);
};
assert_eq!(super::super::decode::display_tag(span.raw_tag), "BIGP");
assert_eq!(span.header_address, VS_BASE);
assert_eq!(span.usable_address, VS_BASE);
assert_eq!(span.size, size as u64);
assert_eq!(span.state, PoolState::Allocated);
assert!(named.complete);
let (unnamed, queries) = walk(false);
assert!(queries > 0, "an unnamed range has to be read");
assert!(
unnamed
.spans
.iter()
.all(|span| span.state == PoolState::Unreadable),
"and when it cannot be, that is all the walk can say: {:?}",
unnamed.spans
);
assert!(!unnamed.complete);
}
#[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(VsFixture::Inline);
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 poke(&mut self, address: u64, byte: u8) {
let (start, data) = self
.runs
.iter_mut()
.find(|(start, data)| address >= *start && address < *start + data.len() as u64)
.expect("the address is inside a written run");
data[(address - *start) as usize] = byte;
}
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),
("Config", 0x38),
],
),
);
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"))
);
}
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 {
SyntheticMemory::new(plain_page_range_writes(heap_key), Vec::new())
}
fn plain_page_range_writes(heap_key: u64) -> Writes {
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]);
}
bytes
}
#[test]
fn test_a_page_range_the_big_page_table_names_carries_no_pool_header() {
let heap_key = 0x55aa_1234_9876_0000;
let allocated = SEGMENT + 0x1000;
let mut writes = plain_page_range_writes(heap_key);
fill(&mut writes, allocated, 0x1000);
pool_header(&mut writes, allocated, b"HBIN");
put_u64(&mut writes, BIG_TABLE_POINTER, BIG_TABLE);
put_u64(&mut writes, BIG_TABLE_COUNT, 8);
fill(&mut writes, BIG_TABLE, 8 * 0x20);
let index = super::super::decode::big_page_hash(allocated, 8).unwrap();
let entry = BIG_TABLE + index as u64 * 0x20;
put_u64(&mut writes, entry, allocated);
put(&mut writes, entry + 8, b"BIGP");
put_u64(&mut writes, entry + 0x10, 0xfc0);
let memory = SyntheticMemory::new(writes, Vec::new());
let layout = synthetic_layout();
let mut discovery = Discovery {
big_pages: BigPageTable::open(&memory, &layout, &mut Vec::new()).unwrap(),
..Discovery::default()
};
assert!(discovery.big_pages.is_some(), "the fixture has a table");
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(),
false,
&mut discovery,
)
.expect("the fixture is readable throughout");
let walker = SnapshotWalker {
memory: &memory,
layout: &layout,
traversal_limit: 1024,
};
let mut snapshot = PoolSnapshot {
complete: true,
..PoolSnapshot::default()
};
walker
.walk_discovered_regions(discovery.regions, None, false, &mut snapshot)
.expect("the fixture is readable throughout");
let span = snapshot
.spans
.iter()
.find(|span| span.header_address == allocated)
.unwrap_or_else(|| panic!("no span at the allocated range: {:?}", snapshot.spans));
assert_eq!(super::super::decode::display_tag(span.raw_tag), "BIGP");
assert_eq!(span.usable_address, allocated);
assert_eq!(span.size, 0xfc0);
}
#[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(),
false,
&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_with_an_unaligned_head_and_a_free_tree_node(heap_key: u64) -> SyntheticMemory {
let mut bytes = Writes::default();
let context = HEAP + 0x108;
let free_node = SEGMENT + 0x100 + 2 * 0x20;
fill(&mut bytes, HEAP, 0x800);
put_u64(&mut bytes, context, SEGMENT); put_u64(&mut bytes, context + 0x10, free_node); 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,
);
let allocated = SEGMENT + 0x100 + 0x20;
put_u32(
&mut bytes,
allocated,
super::super::decode::DESCRIPTOR_TREE_SIGNATURE,
);
put(&mut bytes, allocated + 0x18, &[RANGE_IN_USE]);
put(&mut bytes, allocated + 0x1f, &[1]);
put(&mut bytes, free_node + 0x18, &[DESCRIPTOR_FLAG_FIRST]);
put(&mut bytes, free_node + 0x1f, &[1]);
SyntheticMemory::new(bytes, Vec::new())
}
#[test]
fn test_an_unaligned_segment_list_head_and_a_free_tree_node_are_walked_as_ntdll_lays_them_out()
{
let heap_key = 0x55aa_1234_9876_0000;
let memory = segment_with_an_unaligned_head_and_a_free_tree_node(heap_key);
let layout = synthetic_layout();
let mut discovery = Discovery::default();
discover_segment_context(
&memory,
&layout,
HEAP + 0x108,
0,
PoolKind::NonPagedNx,
HeapIdentity {
pool_state: STATE,
heap: HEAP,
special: false,
},
heap_key,
0xa5c3_1357,
1024,
&SharedChunks::default(),
&SharedChunks::default(),
false,
&mut discovery,
)
.expect("the fixture is readable throughout");
assert!(
discovery.diagnostics.is_empty(),
"one segment, one allocated range and one free one, all well formed: {:?}",
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()
),
]
);
}
#[test]
fn test_a_tree_of_nodes_eight_past_a_boundary_is_walked_exactly() {
const ROOT: u64 = K + 0x40_0008;
const LEFT: u64 = K + 0x40_1008;
const RIGHT: u64 = K + 0x40_2008;
let mut bytes = Writes::default();
for (node, left, right) in [(ROOT, LEFT, RIGHT), (LEFT, 0, 0), (RIGHT, 0, 0)] {
put_u64(&mut bytes, node - 8, 0x84a2_3d0f_b239_be00); put_u64(&mut bytes, node, left);
put_u64(&mut bytes, node + 8, right);
}
let memory = SyntheticMemory::new(bytes, Vec::new());
let mut diagnostics = Vec::new();
let mut nodes = walk_tree_nodes(&memory, ROOT, 0, 8, 16, "VS free tree", &mut diagnostics)
.expect("the fixture is readable throughout");
nodes.sort_unstable();
assert_eq!(nodes, [ROOT, LEFT, RIGHT]);
assert!(diagnostics.is_empty(), "{diagnostics:?}");
let tree = K + 0x50_0000;
assert_eq!(
decode_rb_root_for(ROOT ^ tree, tree, true, false),
Some(ROOT)
);
}
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(),
false,
&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(VsFixture::Inline);
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(VsFixture::Inline);
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(VsFixture::Inline);
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(VsFixture::Inline);
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_reads_whether_a_vs_user_block_skips_a_page_end_from_the_context() {
let identity = HeapIdentity {
pool_state: STATE,
heap: HEAP,
special: false,
};
let discover = |memory: &SyntheticMemory, layout: &PoolLayout| {
let mut discovery = Discovery::default();
discover_heap_regions(
memory,
layout,
HEAP,
0,
PoolKind::NonPagedNx,
identity,
None,
0x55aa_1234_9876_0000,
0xa5c3_1357,
1024,
&mut discovery,
)
.expect("the fixture is readable throughout");
discovery
};
let config = HEAP + 0x300 + 0x38;
for (flags, expected) in [(0u8, false), (1, true), (2, false), (3, true)] {
let mut memory = synthetic_memory();
memory.poke(config, flags);
let discovery = discover(&memory, &synthetic_layout());
let vs: Vec<_> = discovery
.regions
.iter()
.filter(|region| region.backend == PoolBackend::Vs)
.collect();
assert!(!vs.is_empty(), "the fixture has a VS subsegment");
assert!(
vs.iter().all(|region| region.vs_skips_page_end == expected),
"config {flags:#x} should read {expected}"
);
assert!(
!discovery
.diagnostics
.iter()
.any(|line| line.contains("Config")),
"{:?}",
discovery.diagnostics
);
}
let mut memory = synthetic_memory();
memory.poke(config, 1);
let mut layout = synthetic_layout();
layout
.types
.get_mut("_HEAP_VS_CONTEXT")
.unwrap()
.fields
.remove("Config");
let discovery = discover(&memory, &layout);
assert!(
discovery
.regions
.iter()
.all(|region| !region.vs_skips_page_end)
);
assert!(
discovery
.diagnostics
.iter()
.any(|line| line.contains("has no resolved Config")),
"{:?}",
discovery.diagnostics
);
}
#[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!(
BigPageTable::open(&memory, &layout, &mut Vec::new())
.unwrap()
.is_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 mut diagnostics = Vec::new();
let mut table = BigPageTable::open(&memory, &layout, &mut diagnostics)
.unwrap()
.unwrap();
assert_eq!(
table.lookup(&memory, LARGE_VA, &mut diagnostics).unwrap(),
Some(BigPageEntry {
tag: u32::from_le_bytes(*b"BTCH"),
size: 0x9000,
})
);
assert!(memory.read_calls.get() <= 5);
assert!(diagnostics.is_empty());
let reads = memory.read_calls.get();
assert!(
table
.lookup(&memory, LARGE_VA, &mut diagnostics)
.unwrap()
.is_some()
);
assert_eq!(memory.read_calls.get(), reads);
}
#[test]
fn test_big_page_lookup_honors_interrupt_between_batches() {
let mut memory = big_page_memory(LARGE_VA, 512, 300);
let layout = synthetic_layout();
let mut table = BigPageTable::open(&memory, &layout, &mut Vec::new())
.unwrap()
.unwrap();
memory.interrupt_after_checks = Some(1);
assert!(matches!(
table.lookup(&memory, LARGE_VA, &mut Vec::new()),
Err(SnapshotError::Interrupted)
));
assert_eq!(memory.interrupt_checks.get(), 2);
}
#[test]
fn test_big_page_lookup_walks_past_a_freed_entry_to_the_live_one() {
let count = 8;
let layout = synthetic_layout();
let mut bytes = vec![0u8; count * 0x20];
let first = super::super::decode::big_page_hash(LARGE_VA, count).unwrap();
let put = |bytes: &mut Vec<u8>, index: usize, va: u64, tag: &[u8; 4], size: u64| {
let offset = (index % count) * 0x20;
bytes[offset..offset + 8].copy_from_slice(&va.to_le_bytes());
bytes[offset + 8..offset + 12].copy_from_slice(tag);
bytes[offset + 0x10..offset + 0x18].copy_from_slice(&size.to_le_bytes());
};
put(&mut bytes, first, LARGE_VA | 1, b"DEAD", 0x3000);
put(&mut bytes, first + 1, LARGE_VA, b"LIVE", 0x1800);
let memory = BigPageMemory {
table: bytes,
count,
read_calls: Cell::new(0),
interrupt_checks: Cell::new(0),
interrupt_after_checks: None,
};
let mut table = BigPageTable::open(&memory, &layout, &mut Vec::new())
.unwrap()
.unwrap();
assert_eq!(
table.lookup(&memory, LARGE_VA, &mut Vec::new()).unwrap(),
Some(BigPageEntry {
tag: u32::from_le_bytes(*b"LIVE"),
size: 0x1800,
})
);
}
#[test]
fn test_big_page_lookup_stops_at_a_never_used_slot() {
let count = 512;
let layout = synthetic_layout();
let mut bytes = vec![0u8; count * 0x20];
let absent = LARGE_VA + 0x20_0000;
let first = super::super::decode::big_page_hash(absent, count).unwrap();
for index in 0..count {
let offset = index * 0x20;
let occupant = LARGE_VA + 0x100_0000 + index as u64 * 0x1000;
bytes[offset..offset + 8].copy_from_slice(&occupant.to_le_bytes());
}
let never_used = ((first + 3) % count) * 0x20;
bytes[never_used..never_used + 8].copy_from_slice(&1u64.to_le_bytes());
assert!(
first % BIG_PAGE_BATCH + 3 < BIG_PAGE_BATCH,
"the fixture's chain has to stay inside one batch for the read count to mean anything"
);
let memory = BigPageMemory {
table: bytes,
count,
read_calls: Cell::new(0),
interrupt_checks: Cell::new(0),
interrupt_after_checks: None,
};
let mut table = BigPageTable::open(&memory, &layout, &mut Vec::new())
.unwrap()
.unwrap();
let reads = memory.read_calls.get();
assert_eq!(
table.lookup(&memory, absent, &mut Vec::new()).unwrap(),
None
);
assert_eq!(memory.read_calls.get() - reads, 1);
}
#[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"
));
}
}