use std::cell::RefCell;
use std::collections::BTreeMap;
use thiserror::Error;
use crate::dbgeng::{DbgEngError, DebugEngine};
const MAX_ENTRIES: usize = if cfg!(all(test, miri)) { 32 } else { 65_536 };
const MAX_COMPONENTS: usize = 32;
const MAX_NAME_BYTES: usize = 1024;
const MAX_TARGET_BYTES: usize = 32_768;
const MAX_STRUCTURE: usize = 4096;
const MAX_BUCKETS: usize = 1024;
const DIRECTORY: &str = "Directory";
const SYMBOLIC_LINK_CALLBACK: u32 = 0x10;
const ROOT_SYMBOL: &str = "nt!ObpRootDirectoryObject";
const OFFSETS_SYMBOL: &str = "nt!ObpInfoMaskToOffset";
#[derive(Debug, Error, PartialEq, Eq)]
pub enum ObjectError {
#[error("could not read {len} bytes of the object namespace at {at:#x}")]
Unreadable { at: u64, len: usize },
#[error("the object namespace is malformed: {reason}")]
Malformed { reason: &'static str },
#[error("{path:?} is not an object path: {reason}")]
BadPath { path: String, reason: &'static str },
#[error("{component:?} is not in {directory:?}")]
NotFound {
directory: String,
component: String,
},
#[error("{component:?} is not a directory, so {rest:?} cannot be under it")]
NotADirectory { component: String, rest: String },
#[error("{component:?} could not be typed, so this walk will not descend through it")]
Untyped { component: String },
#[error("this target does not resolve {what}, which this operation reads")]
Unavailable { what: &'static str },
#[error("{what} exceeded its bound of {bound}, so this list would be shorter than the truth")]
TooMany { what: &'static str, bound: usize },
#[error("the namespace walk was stopped before it reached {what}")]
Halted { what: String },
#[error(
"{component:?} is not among the entries of {directory:?} that could be named \
({unreadable} would not read, {malformed} contradict themselves) -- so this cannot say \
it is absent"
)]
NotFoundInPart {
directory: String,
component: String,
unreadable: usize,
malformed: usize,
},
}
pub fn same_object_name(one: &str, other: &str) -> bool {
Upcase::of_host().same_name(one, other)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UpcaseTable {
pub silo_globals: u64,
pub nls_state: u32,
pub upcase_table: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpcaseFrom {
Target(u64),
Mixed { at: u64 },
Host,
}
pub struct Upcase<'a> {
target: Option<TargetTable<'a>>,
known: RefCell<Known>,
}
#[derive(Clone, Copy)]
struct TargetTable<'a> {
memory: &'a dyn Memory,
table: UpcaseTable,
pointer: usize,
}
#[derive(Debug, Default)]
struct Known {
base: Option<Option<u64>>,
units: BTreeMap<u16, u16>,
host_folded: bool,
}
impl<'a> Upcase<'a> {
pub fn of_target(memory: &'a dyn Memory, table: UpcaseTable, pointer: usize) -> Self {
Self {
target: Some(TargetTable {
memory,
table,
pointer,
}),
known: RefCell::default(),
}
}
pub fn of_host() -> Self {
Self {
target: None,
known: RefCell::default(),
}
}
pub fn source(&self) -> UpcaseFrom {
let base = self.base();
match (base, self.known.borrow().host_folded) {
(Some(at), true) => UpcaseFrom::Mixed { at },
(Some(at), false) => UpcaseFrom::Target(at),
(None, _) => UpcaseFrom::Host,
}
}
pub fn same_name(&self, one: &str, other: &str) -> bool {
self.fold(one) == self.fold(other)
}
fn fold(&self, name: &str) -> Vec<u16> {
name.encode_utf16().map(|unit| self.unit(unit)).collect()
}
fn unit(&self, unit: u16) -> u16 {
match unit {
0x61..=0x7a => unit - 0x20,
0..=0xbf => unit,
_ => {
if let Some(remembered) = self.known.borrow().units.get(&unit) {
return *remembered;
}
let folded = self.uncached(unit);
self.known.borrow_mut().units.insert(unit, folded);
folded
}
}
}
fn uncached(&self, unit: u16) -> u16 {
let (Some(target), Some(base)) = (self.target, self.base()) else {
return Self::on_host(unit);
};
if base == 0 {
return unit;
}
match Self::from_table(target.memory, base, unit) {
Some(folded) => folded,
None => {
self.known.borrow_mut().host_folded = true;
Self::on_host(unit)
}
}
}
fn base(&self) -> Option<u64> {
if let Some(asked) = self.known.borrow().base {
return asked;
}
let target = self.target?;
let at = target
.table
.silo_globals
.wrapping_add(u64::from(target.table.nls_state))
.wrapping_add(u64::from(target.table.upcase_table));
let base = match target.pointer {
4 => target.memory.read(at, 4).and_then(|bytes| {
bytes
.first_chunk::<4>()
.map(|four| u64::from(u32::from_le_bytes(*four)))
}),
8 => target.memory.read(at, 8).and_then(|bytes| {
bytes
.first_chunk::<8>()
.map(|eight| u64::from_le_bytes(*eight))
}),
_ => None,
};
self.known.borrow_mut().base = Some(base);
base
}
fn from_table(memory: &dyn Memory, base: u64, unit: u16) -> Option<u16> {
let element = |index: u32| -> Option<u16> {
let bytes = memory.read(base.wrapping_add(u64::from(index) * 2), 2)?;
bytes
.first_chunk::<2>()
.map(|pair| u16::from_le_bytes(*pair))
};
let level1 = u32::from(element(u32::from(unit >> 8))?);
let level2 = u32::from(element(level1 + u32::from((unit >> 4) & 0xf))?);
let delta = element(level2 + u32::from(unit & 0xf))?;
Some(unit.wrapping_add(delta))
}
fn on_host(unit: u16) -> u16 {
match cfg!(miri) {
true => unit,
false => unsafe { windows::Wdk::System::SystemServices::RtlUpcaseUnicodeChar(unit) },
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Listing {
pub objects: Vec<KernelObject>,
pub unreadable: usize,
pub malformed: usize,
pub halted: bool,
}
impl Listing {
pub fn skipped(&self) -> usize {
self.unreadable + self.malformed
}
pub fn is_complete(&self) -> bool {
!self.halted && self.skipped() == 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KernelObject {
pub address: u64,
pub name: String,
pub exact_name: bool,
pub type_name: Option<String>,
pub security_descriptor: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Layout {
pub pointer: usize,
pub buckets: usize,
pub hash_buckets: u32,
pub entry_chain: u32,
pub entry_object: u32,
pub header_body: u32,
pub header_type_index: u32,
pub header_info_mask: u32,
pub header_security: u32,
pub name_info_name: u32,
pub name_info_size: u32,
pub unicode_length: u32,
pub unicode_buffer: u32,
pub link_target: u32,
pub link_flags: u32,
pub type_name: u32,
}
impl Layout {
fn check(&self) -> Result<(), ObjectError> {
let bad = |reason| Err(ObjectError::Malformed { reason });
if !matches!(self.pointer, 4 | 8) {
return bad("a pointer on this target is neither four bytes nor eight");
}
if self.buckets == 0 || self.buckets > MAX_BUCKETS {
return bad("a directory's bucket count is not one a directory has");
}
let fields = [
self.hash_buckets,
self.entry_chain,
self.entry_object,
self.header_body,
self.header_type_index,
self.header_info_mask,
self.header_security,
self.name_info_name,
self.name_info_size,
self.unicode_length,
self.unicode_buffer,
self.link_target,
self.link_flags,
self.type_name,
];
if fields.iter().any(|offset| *offset as usize > MAX_STRUCTURE) {
return bad("a field sits further into its structure than any of these reach");
}
let unicode = (self.unicode_buffer as usize) + self.pointer;
if (self.unicode_length as usize) + 4 > unicode {
return bad("a UNICODE_STRING's lengths sit outside the structure");
}
if (self.name_info_name as usize) + unicode > self.name_info_size as usize {
return bad("a name header's string sits outside the name header");
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Globals {
pub root: Option<u64>,
pub info_mask_to_offset: Option<u64>,
pub header_cookie: Option<u64>,
pub type_index_table: Option<u64>,
pub upcase: Option<UpcaseTable>,
}
const INFO_MASK_NAME: u8 = 0x02;
pub trait Memory {
fn read(&self, address: u64, len: usize) -> Option<Vec<u8>>;
}
impl<F> Memory for F
where
F: Fn(u64, usize) -> Option<Vec<u8>>,
{
fn read(&self, address: u64, len: usize) -> Option<Vec<u8>> {
self(address, len)
}
}
pub struct Namespace<'a> {
memory: &'a dyn Memory,
layout: Layout,
globals: Globals,
halt: Option<&'a dyn Fn() -> bool>,
upcase: Upcase<'a>,
}
impl<'a> Namespace<'a> {
pub fn new(
memory: &'a dyn Memory,
layout: Layout,
globals: Globals,
) -> Result<Self, ObjectError> {
layout.check()?;
let upcase = match globals.upcase {
Some(table) => Upcase::of_target(memory, table, layout.pointer),
None => Upcase::of_host(),
};
Ok(Self {
memory,
layout,
globals,
halt: None,
upcase,
})
}
pub fn upcase(&self) -> &Upcase<'a> {
&self.upcase
}
#[must_use]
pub fn halting(mut self, halt: &'a dyn Fn() -> bool) -> Self {
self.halt = Some(halt);
self
}
fn stopped(&self) -> bool {
self.halt.is_some_and(|halt| halt())
}
fn needs(&self, global: Option<u64>, what: &'static str) -> Result<u64, ObjectError> {
global.ok_or(ObjectError::Unavailable { what })
}
fn read(&self, at: u64, len: usize) -> Result<Vec<u8>, ObjectError> {
match self.memory.read(at, len) {
Some(bytes) if bytes.len() >= len => Ok(bytes),
_ => Err(ObjectError::Unreadable { at, len }),
}
}
fn pointer_at(&self, at: u64) -> Result<u64, ObjectError> {
let bytes = self.read(at, self.layout.pointer)?;
match self.layout.pointer {
4 => Ok(u64::from(u32::from_le_bytes(
bytes[..4].try_into().unwrap_or_default(),
))),
8 => Ok(u64::from_le_bytes(
bytes[..8].try_into().unwrap_or_default(),
)),
_ => Err(ObjectError::Malformed {
reason: "a pointer on this target is neither four bytes nor eight",
}),
}
}
fn field_at(bytes: &[u8], offset: u32) -> Result<u16, ObjectError> {
bytes
.get(offset as usize..)
.and_then(|rest| rest.first_chunk::<2>())
.map(|pair| u16::from_le_bytes(*pair))
.ok_or(ObjectError::Malformed {
reason: "a field sits outside the structure this layout describes",
})
}
fn entries_of(&self, directory: u64) -> Result<(Vec<u64>, bool), ObjectError> {
let mut out = Vec::new();
let mut followed = 0usize;
for bucket in 0..self.layout.buckets {
if self.stopped() {
return Ok((out, true));
}
let at = directory
.wrapping_add(u64::from(self.layout.hash_buckets))
.wrapping_add((bucket * self.layout.pointer) as u64);
let mut entry = self.pointer_at(at)?;
while entry != 0 {
if self.stopped() {
return Ok((out, true));
}
followed += 1;
if followed > MAX_ENTRIES {
return Err(ObjectError::TooMany {
what: "a directory's entries",
bound: MAX_ENTRIES,
});
}
let object =
self.pointer_at(entry.wrapping_add(u64::from(self.layout.entry_object)))?;
if object != 0 {
out.push(object);
}
entry = self.pointer_at(entry.wrapping_add(u64::from(self.layout.entry_chain)))?;
}
}
Ok((out, false))
}
fn header_of(&self, body: u64) -> u64 {
body.wrapping_sub(u64::from(self.layout.header_body))
}
fn unicode_at(
&self,
at: u64,
bound: usize,
what: &'static str,
) -> Result<(String, bool), ObjectError> {
let size = (self.layout.unicode_buffer as usize) + self.layout.pointer;
let bytes = self.read(at, size)?;
let length = Self::field_at(&bytes, self.layout.unicode_length)? as usize;
if length > Self::field_at(&bytes, self.layout.unicode_length + 2)? as usize {
return Err(ObjectError::Malformed {
reason: "a UNICODE_STRING is longer than its own maximum",
});
}
let buffer = self.pointer_at(at.wrapping_add(u64::from(self.layout.unicode_buffer)))?;
if length == 0 {
return Ok((String::new(), true));
}
if buffer == 0 {
return Err(ObjectError::Malformed {
reason: "a UNICODE_STRING has a length and no buffer",
});
}
if !length.is_multiple_of(2) {
return Err(ObjectError::Malformed {
reason: "a UNICODE_STRING's length is not a whole number of UTF-16 units",
});
}
if length > bound {
return Err(ObjectError::TooMany { what, bound });
}
let raw = self.read(buffer, length)?;
Ok(utf16(&raw[..length]))
}
fn name_of(&self, body: u64) -> Result<Option<(String, bool)>, ObjectError> {
let header = self.header_of(body);
let mask = self.read(
header.wrapping_add(u64::from(self.layout.header_info_mask)),
1,
)?[0];
if mask & INFO_MASK_NAME == 0 {
return Ok(None);
}
let index = mask & (INFO_MASK_NAME | (INFO_MASK_NAME - 1));
let distance = self.read(
self.needs(self.globals.info_mask_to_offset, OFFSETS_SYMBOL)?
.wrapping_add(u64::from(index)),
1,
)?[0];
if u32::from(distance) < self.layout.name_info_size {
return Err(ObjectError::Malformed {
reason: "the name header's distance is shorter than the name header",
});
}
let name_info = header.wrapping_sub(u64::from(distance));
Ok(Some(self.unicode_at(
name_info.wrapping_add(u64::from(self.layout.name_info_name)),
MAX_NAME_BYTES,
"an object name",
)?))
}
fn security_of(&self, body: u64) -> Result<Option<u64>, ObjectError> {
let at = self
.header_of(body)
.wrapping_add(u64::from(self.layout.header_security));
let counted = (2 * self.layout.pointer as u64) - 1;
let descriptor = self.pointer_at(at)? & !counted;
Ok((descriptor != 0).then_some(descriptor))
}
fn type_of(&self, body: u64) -> Option<String> {
let (cookie, table) = (self.globals.header_cookie?, self.globals.type_index_table?);
let header = self.header_of(body);
let raw = self
.read(
header.wrapping_add(u64::from(self.layout.header_type_index)),
1,
)
.ok()?[0];
let cookie = self.read(cookie, 1).ok()?[0];
let index = raw ^ cookie ^ ((header >> 8) as u8);
let entry = self
.pointer_at(table.wrapping_add((usize::from(index) * self.layout.pointer) as u64))
.ok()?;
if entry == 0 {
return None;
}
let (name, _) = self
.unicode_at(
entry.wrapping_add(u64::from(self.layout.type_name)),
MAX_NAME_BYTES,
"a type name",
)
.ok()?;
(!name.is_empty()).then_some(name)
}
fn named_in(&self, directory: u64) -> Result<Listing, ObjectError> {
let mut objects = Vec::new();
let mut unreadable = 0usize;
let mut malformed = 0usize;
let (bodies, mut halted) = self.entries_of(directory)?;
for body in bodies {
if halted {
break;
}
if self.stopped() {
halted = true;
break;
}
let named = match self.name_of(body) {
Ok(named) => named,
Err(ObjectError::Unreadable { .. }) => {
unreadable += 1;
continue;
}
Err(ObjectError::Malformed { .. }) => {
malformed += 1;
continue;
}
Err(fatal) => return Err(fatal),
};
let Some((name, exact_name)) = named else {
continue;
};
let security_descriptor = match self.security_of(body) {
Ok(found) => found,
Err(ObjectError::Unreadable { .. }) => {
unreadable += 1;
continue;
}
Err(ObjectError::Malformed { .. }) => {
malformed += 1;
continue;
}
Err(fatal) => return Err(fatal),
};
objects.push(KernelObject {
address: body,
name,
exact_name,
type_name: self.type_of(body),
security_descriptor,
});
}
Ok(Listing {
objects,
unreadable,
malformed,
halted,
})
}
pub fn object_at(&self, path: &str) -> Result<KernelObject, ObjectError> {
let components = components_of(path)?;
if components.is_empty() {
return Err(ObjectError::BadPath {
path: path.to_string(),
reason: "it names the root directory rather than an object in it",
});
}
let mut directory = self.pointer_at(self.needs(self.globals.root, ROOT_SYMBOL)?)?;
if directory == 0 {
return Err(ObjectError::Malformed {
reason: "the root directory pointer is null",
});
}
let mut walked = String::from("\\");
let last = components.len() - 1;
for (at, component) in components.iter().enumerate() {
let listing = self.named_in(directory)?;
let skipped = (listing.unreadable, listing.malformed);
if listing.halted {
return Err(ObjectError::Halted {
what: component.clone(),
});
}
let found = listing
.objects
.into_iter()
.find(|object| object.exact_name && self.upcase.same_name(&object.name, component))
.ok_or_else(|| match skipped {
(0, 0) => ObjectError::NotFound {
directory: walked.clone(),
component: component.clone(),
},
(unreadable, malformed) => ObjectError::NotFoundInPart {
directory: walked.clone(),
component: component.clone(),
unreadable,
malformed,
},
})?;
if at == last {
return Ok(found);
}
match found.type_name.as_deref() {
Some(DIRECTORY) => {}
Some(_) => {
return Err(ObjectError::NotADirectory {
component: component.clone(),
rest: components[at + 1..].join("\\"),
});
}
None => {
return Err(ObjectError::Untyped {
component: component.clone(),
});
}
}
directory = found.address;
if walked.len() > 1 {
walked.push('\\');
}
walked.push_str(component);
}
unreachable!("a path with no components is refused above")
}
pub fn objects_in(&self, path: &str) -> Result<Listing, ObjectError> {
let directory = match components_of(path)?.is_empty() {
true => self.pointer_at(self.needs(self.globals.root, ROOT_SYMBOL)?)?,
false => {
let found = self.object_at(path)?;
match found.type_name.as_deref() {
Some(DIRECTORY) => {}
Some(_) => {
return Err(ObjectError::NotADirectory {
component: found.name,
rest: String::new(),
});
}
None => {
return Err(ObjectError::Untyped {
component: found.name,
});
}
}
found.address
}
};
self.named_in(directory)
}
pub fn link_target(&self, link: u64) -> Result<String, ObjectError> {
let flags = self.read(
link.wrapping_add(u64::from(self.layout.link_flags)),
size_of::<u32>(),
)?;
let flags = u32::from_le_bytes(flags[..4].try_into().unwrap_or_default());
if flags & SYMBOLIC_LINK_CALLBACK != 0 {
return Err(ObjectError::Malformed {
reason: "this link resolves through a callback, so it has no target to read",
});
}
let at = link.wrapping_add(u64::from(self.layout.link_target));
let size = (self.layout.unicode_buffer as usize) + self.layout.pointer;
let bytes = self.read(at, size)?;
let length = Self::field_at(&bytes, self.layout.unicode_length)?;
let maximum = Self::field_at(&bytes, self.layout.unicode_length + 2)?;
if length == 0 || !length.is_multiple_of(2) || length > maximum {
return Err(ObjectError::Malformed {
reason: "the link target is not a string this can vouch for",
});
}
let (target, exact) = self.unicode_at(at, MAX_TARGET_BYTES, "a link target")?;
if !exact {
return Err(ObjectError::Malformed {
reason: "the link target is not text, so it is not a path to follow",
});
}
Ok(target)
}
}
fn components_of(path: &str) -> Result<Vec<String>, ObjectError> {
let bad = |reason| ObjectError::BadPath {
path: path.to_string(),
reason,
};
if !path.starts_with('\\') {
return Err(bad("an object path begins at the root, with a backslash"));
}
if path == "\\" {
return Ok(Vec::new());
}
let body = path.strip_suffix('\\').unwrap_or(path);
let parts: Vec<String> = body[1..].split('\\').map(str::to_string).collect();
if parts.iter().any(String::is_empty) {
return Err(bad("it has a component with no name in it"));
}
if parts.len() > MAX_COMPONENTS {
return Err(bad("it has more components than the namespace is deep"));
}
Ok(parts)
}
fn utf16(bytes: &[u8]) -> (String, bool) {
let (pairs, _) = bytes.as_chunks::<2>();
let units: Vec<u16> = pairs.iter().copied().map(u16::from_le_bytes).collect();
match String::from_utf16(&units) {
Ok(exact) => (exact, true),
Err(_) => (String::from_utf16_lossy(&units), false),
}
}
impl DebugEngine {
pub fn object_layout(&self) -> Result<Layout, DbgEngError> {
let module = self.kernel_base()?;
let of = |type_name: &str, field: &str| -> Result<u32, DbgEngError> {
let id = self.type_id(module, type_name)?;
self.field_offset(module, id, field)
};
let entry_chain = of("_OBJECT_DIRECTORY_ENTRY", "ChainLink")?;
let entry_object = of("_OBJECT_DIRECTORY_ENTRY", "Object")?;
let pointer = entry_object
.checked_sub(entry_chain)
.filter(|width| matches!(width, 4 | 8))
.ok_or(DbgEngError::InvalidCommand)? as usize;
let hash_buckets = of("_OBJECT_DIRECTORY", "HashBuckets")?;
let buckets = (of("_OBJECT_DIRECTORY", "Lock")?.saturating_sub(hash_buckets) as usize)
.checked_div(pointer)
.filter(|count| *count > 0)
.ok_or(DbgEngError::InvalidCommand)?;
let name_info = self.type_id(module, "_OBJECT_HEADER_NAME_INFO")?;
Ok(Layout {
pointer,
buckets,
hash_buckets,
entry_chain,
entry_object,
header_body: of("_OBJECT_HEADER", "Body")?,
header_type_index: of("_OBJECT_HEADER", "TypeIndex")?,
header_info_mask: of("_OBJECT_HEADER", "InfoMask")?,
header_security: of("_OBJECT_HEADER", "SecurityDescriptor")?,
name_info_name: self.field_offset(module, name_info, "Name")?,
name_info_size: self.type_size(module, name_info)?,
unicode_length: of("_UNICODE_STRING", "Length")?,
unicode_buffer: of("_UNICODE_STRING", "Buffer")?,
link_target: of("_OBJECT_SYMBOLIC_LINK", "LinkTarget")?,
link_flags: of("_OBJECT_SYMBOLIC_LINK", "Flags")?,
type_name: of("_OBJECT_TYPE", "Name")?,
})
}
pub fn object_globals(&self) -> Result<Globals, DbgEngError> {
Ok(Globals {
root: self.symbol_offset("nt!ObpRootDirectoryObject").ok(),
info_mask_to_offset: self.symbol_offset("nt!ObpInfoMaskToOffset").ok(),
header_cookie: self.symbol_offset("nt!ObHeaderCookie").ok(),
type_index_table: self.symbol_offset("nt!ObTypeIndexTable").ok(),
upcase: self.object_upcase_table(),
})
}
fn object_upcase_table(&self) -> Option<UpcaseTable> {
let module = self.kernel_base().ok()?;
let of = |type_name: &str, field: &str| -> Option<u32> {
let id = self.type_id(module, type_name).ok()?;
self.field_offset(module, id, field).ok()
};
Some(UpcaseTable {
silo_globals: self.symbol_offset("nt!PspHostSiloGlobals").ok()?,
nls_state: of("_ESERVERSILO_GLOBALS", "RtlNlsState")?,
upcase_table: of("_RTL_NLS_STATE", "UnicodeUpcaseTable844")?,
})
}
pub fn object_at(&self, path: &str) -> Result<KernelObject, ObjectError> {
self.with_namespace(|namespace| namespace.object_at(path))
}
pub fn objects_in(&self, path: &str) -> Result<Listing, ObjectError> {
self.with_namespace(|namespace| namespace.objects_in(path))
}
pub fn symbolic_link_target(&self, link: u64) -> Result<String, ObjectError> {
self.with_namespace(|namespace| namespace.link_target(link))
}
fn with_namespace<T>(
&self,
answer: impl FnOnce(&Namespace<'_>) -> Result<T, ObjectError>,
) -> Result<T, ObjectError> {
let layout = self.object_layout().map_err(|_| ObjectError::Malformed {
reason: "this target has no type information for the object manager's structures",
})?;
let globals = self.object_globals().map_err(|_| ObjectError::Malformed {
reason: "this target does not resolve the object manager's globals",
})?;
let read = |at: u64, len: usize| self.read_memory(at, len).ok();
answer(&Namespace::new(&read, layout, globals)?)
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
fn layout() -> Layout {
Layout {
pointer: 8,
buckets: 37,
hash_buckets: 0x00,
entry_chain: 0x00,
entry_object: 0x08,
header_body: 0x30,
header_type_index: 0x18,
header_info_mask: 0x1a,
header_security: 0x28,
name_info_name: 0x08,
name_info_size: 0x20,
unicode_length: 0x00,
unicode_buffer: 0x08,
link_target: 0x08,
link_flags: 0x1c,
type_name: 0x10,
}
}
const ROOT_POINTER: u64 = 0xffff_f800_0000_1000;
const INFO_OFFSETS: u64 = 0xffff_f800_0000_2000;
const COOKIE: u64 = 0xffff_f800_0000_3000;
const TYPE_TABLE: u64 = 0xffff_f800_0000_4000;
const COOKIE_VALUE: u8 = 0x5a;
fn globals() -> Globals {
Globals {
root: Some(ROOT_POINTER),
info_mask_to_offset: Some(INFO_OFFSETS),
header_cookie: Some(COOKIE),
type_index_table: Some(TYPE_TABLE),
upcase: None,
}
}
#[derive(Default)]
struct Fake {
bytes: BTreeMap<u64, u8>,
}
impl Memory for Fake {
fn read(&self, address: u64, len: usize) -> Option<Vec<u8>> {
(0..len)
.map(|step| self.bytes.get(&(address + step as u64)).copied())
.collect()
}
}
impl Fake {
fn put(&mut self, at: u64, bytes: &[u8]) {
for (step, byte) in bytes.iter().enumerate() {
self.bytes.insert(at + step as u64, *byte);
}
}
fn pointer(&mut self, at: u64, value: u64) {
self.put(at, &value.to_le_bytes());
}
fn string(&mut self, at: u64, buffer: u64, text: &str) {
let units: Vec<u8> = text
.encode_utf16()
.flat_map(|unit| unit.to_le_bytes())
.collect();
let length = units.len() as u16;
self.put(at, &[0u8; 16]);
self.put(at, &length.to_le_bytes());
self.put(at + 2, &length.to_le_bytes());
self.pointer(at + 8, buffer);
self.put(buffer, &units);
}
#[allow(clippy::too_many_arguments)]
fn object(&mut self, body: u64, name: &str, type_index: u8, security: u64) {
let header = body - 0x30;
self.put(header + 0x18, &[type_index]);
self.put(header + 0x1a, &[0x02]);
self.pointer(header + 0x28, security);
self.put(INFO_OFFSETS + u64::from(0x02u8 & 0x03), &[0x20]);
let name_info = header - 0x20;
self.string(name_info + 0x08, name_info + 0x1000, name);
}
fn directory(&mut self, at: u64, objects: &[u64]) {
for bucket in 0..37u64 {
self.pointer(at + bucket * 8, 0);
}
for (index, object) in objects.iter().enumerate() {
let entry = at + 0x2000 + (index as u64) * 0x20;
let bucket = (index as u64) % 37;
let was =
u64::from_le_bytes(self.read(at + bucket * 8, 8).unwrap().try_into().unwrap());
self.pointer(entry, was);
self.pointer(entry + 0x08, *object);
self.pointer(at + bucket * 8, entry);
}
}
fn units_named(&mut self, body: u64, units: &[u16], type_index: u8) {
let header = body - 0x30;
self.put(header + 0x18, &[type_index]);
self.put(header + 0x1a, &[0x02]);
self.pointer(header + 0x28, 0);
self.put(INFO_OFFSETS + u64::from(0x02u8 & 0x03), &[0x20]);
let name_info = header - 0x20;
let bytes: Vec<u8> = units.iter().flat_map(|unit| unit.to_le_bytes()).collect();
let length = bytes.len() as u16;
self.put(name_info + 0x08, &[0u8; 16]);
self.put(name_info + 0x08, &length.to_le_bytes());
self.put(name_info + 0x0a, &length.to_le_bytes());
self.pointer(name_info + 0x10, name_info + 0x1000);
self.put(name_info + 0x1000, &bytes);
}
fn flags(&mut self, link: u64, flags: u32) {
self.put(link + 0x1c, &flags.to_le_bytes());
}
fn kind(&mut self, index: u8, at: u64, name: &str) {
self.pointer(TYPE_TABLE + u64::from(index) * 8, at);
self.string(at + 0x10, at + 0x1000, name);
}
}
fn obfuscated(kind: u8, body: u64) -> u8 {
let header = body - 0x30;
kind ^ COOKIE_VALUE ^ ((header >> 8) as u8)
}
const ROOT: u64 = 0xffff_a000_0000_0000;
const DEVICE_DIR: u64 = 0xffff_a000_0010_0000;
const DEVICE: u64 = 0xffff_a000_0020_0000;
fn namespace() -> Fake {
let mut fake = Fake::default();
fake.put(COOKIE, &[COOKIE_VALUE]);
fake.pointer(ROOT_POINTER, ROOT);
fake.kind(3, 0xffff_a000_0030_0000, "Directory");
fake.kind(4, 0xffff_a000_0031_0000, "Device");
fake.kind(5, 0xffff_a000_0032_0000, "SymbolicLink");
fake.object(DEVICE_DIR, "Device", obfuscated(3, DEVICE_DIR), 0);
fake.directory(ROOT, &[DEVICE_DIR]);
fake.object(
DEVICE,
"MountPointManager",
obfuscated(4, DEVICE),
0xffff_b000_0000_000f,
);
fake.directory(DEVICE_DIR, &[DEVICE]);
fake
}
#[test]
fn test_a_path_resolves_to_the_object_filed_under_it() {
let fake = namespace();
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
let found = namespace
.object_at("\\Device\\MountPointManager")
.expect("the device is in the namespace");
assert_eq!(
(
found.address,
found.name.as_str(),
found.type_name.as_deref(),
found.security_descriptor
),
(
DEVICE,
"MountPointManager",
Some("Device"),
Some(0xffff_b000_0000_0000)
),
"the descriptor field is a fast reference, and the count in its low bits is not \
part of the address"
);
}
#[test]
fn test_a_name_is_matched_without_regard_to_case() {
let fake = namespace();
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace
.object_at("\\device\\MOUNTPOINTMANAGER")
.map(|found| found.address),
Ok(DEVICE)
);
}
#[cfg_attr(miri, ignore = "folds through ntdll; see `Upcase::on_host`")]
#[test]
fn test_a_name_is_folded_by_the_object_managers_own_table_and_not_by_unicodes() {
assert!(same_object_name("MountPointManager", "MOUNTPOINTMANAGER"));
assert!(!same_object_name("\u{00b5}", "\u{039c}"));
assert!(same_object_name("K\u{e4}se", "K\u{c4}SE"));
assert!(
!same_object_name("\u{0131}", "I"),
"dotless i is not I here"
);
assert!(!same_object_name("\u{017f}", "S"), "long s is not S here");
assert!(
!same_object_name("\u{01c5}", "\u{01c4}"),
"a titlecase digraph is not its uppercase here"
);
assert!(
same_object_name("\u{1f80}", "\u{1f88}"),
"the table maps U+1F80 to U+1F88, one unit"
);
assert!(
!same_object_name("\u{00df}", "SS"),
"and leaves U+00DF alone, so it is not SS"
);
assert!(
same_object_name("\u{0391}\u{03A3}", "\u{0391}\u{03C3}"),
"one object spelt with either sigma is still one object"
);
assert!(
!same_object_name("\u{0130}", "i\u{0307}"),
"a fold that expands would make one object out of two"
);
assert!(!same_object_name("\u{10400}", "\u{10428}"));
}
const SILO_GLOBALS: u64 = 0xffff_f800_0005_0000;
const NLS_STATE: u32 = 0x408;
const UPCASE_TABLE: u32 = 0xa8;
const TABLE: u64 = 0xffff_f800_0006_0000;
fn upcase_globals() -> UpcaseTable {
UpcaseTable {
silo_globals: SILO_GLOBALS,
nls_state: NLS_STATE,
upcase_table: UPCASE_TABLE,
}
}
impl Fake {
fn nls_pointer(&mut self, table: u64) {
self.pointer(
SILO_GLOBALS + u64::from(NLS_STATE) + u64::from(UPCASE_TABLE),
table,
);
}
fn element(&mut self, base: u64, index: u32, value: u16) {
self.put(base + u64::from(index) * 2, &value.to_le_bytes());
}
}
fn one_fold_table(delta: u16) -> Fake {
one_fold_table_at(TABLE, delta)
}
fn one_fold_table_at(base: u64, delta: u16) -> Fake {
let mut fake = Fake::default();
fake.nls_pointer(base);
for high in 0..=0xffu32 {
fake.element(base, high, 0x100);
}
fake.element(base, 0x00, 0x120);
for nibble in 0..0x10u32 {
fake.element(base, 0x100 + nibble, 0x110);
fake.element(base, 0x110 + nibble, 0);
fake.element(base, 0x120 + nibble, 0x110);
fake.element(base, 0x130 + nibble, 0);
}
fake.element(base, 0x120 + 0xe, 0x130);
fake.element(base, 0x130 + 0x9, delta);
fake
}
#[test]
fn test_the_trie_is_walked_from_the_base_and_its_leaf_is_a_delta() {
let fake = one_fold_table(0xffe0);
let upcase = Upcase::of_target(&fake, upcase_globals(), layout().pointer);
assert_eq!(upcase.source(), UpcaseFrom::Target(TABLE));
assert_eq!(
upcase.unit(0x00e9),
0x00c9,
"the leaf is added to the unit, not substituted for it"
);
assert_eq!(
upcase.unit(0x00ea),
0x00ea,
"a zero leaf is a unit that does not fold"
);
assert_eq!(
upcase.unit(0x01e9),
0x01e9,
"another high byte reaches the block that folds nothing"
);
}
#[test]
fn test_the_targets_own_table_answers_and_not_this_hosts() {
let fake = one_fold_table(0x0001);
let upcase = Upcase::of_target(&fake, upcase_globals(), layout().pointer);
assert_eq!(upcase.unit(0x00e9), 0x00ea);
assert!(
upcase.same_name("\u{00e9}", "\u{00ea}"),
"on this target's table they are one name"
);
}
#[test]
fn test_the_walk_matches_a_component_through_the_targets_table() {
let mut fake = namespace();
const ODD: u64 = 0xffff_a000_0060_0000;
fake.object(ODD, "\u{00e9}", obfuscated(4, ODD), 0);
fake.directory(DEVICE_DIR, &[DEVICE, ODD]);
for (index, value) in one_fold_table(0x0001).bytes {
fake.bytes.insert(index, value);
}
let globals = Globals {
upcase: Some(upcase_globals()),
..globals()
};
let namespace = Namespace::new(&fake, layout(), globals)
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.upcase().source(),
UpcaseFrom::Target(TABLE),
"the walk folds on the target's table"
);
assert_eq!(
namespace
.object_at("\\Device\\\u{00ea}")
.map(|found| found.address),
Ok(ODD),
"which is the only table that makes these one name"
);
}
#[test]
fn test_a_null_table_pointer_folds_no_further_than_ascii() {
let mut fake = Fake::default();
fake.nls_pointer(0);
let upcase = Upcase::of_target(&fake, upcase_globals(), layout().pointer);
assert_eq!(upcase.source(), UpcaseFrom::Target(0));
assert_eq!(upcase.unit(0x00e9), 0x00e9, "no table, so no fold");
assert_eq!(upcase.unit(0x0061), 0x0041, "the ASCII band still folds");
}
#[cfg_attr(
miri,
ignore = "the fallback folds through ntdll; see `Upcase::on_host`"
)]
#[test]
fn test_a_table_that_cannot_be_reached_falls_back_to_the_host_and_reports_it() {
let empty = Fake::default();
let unreadable = Upcase::of_target(&empty, upcase_globals(), layout().pointer);
assert_eq!(unreadable.source(), UpcaseFrom::Host);
assert_eq!(
unreadable.unit(0x00e9),
Upcase::of_host().unit(0x00e9),
"the host's table stands in"
);
assert_eq!(Upcase::of_host().source(), UpcaseFrom::Host);
}
#[cfg_attr(
miri,
ignore = "the fallback folds through ntdll; see `Upcase::on_host`"
)]
#[test]
fn test_a_table_located_but_unreadable_reports_a_fold_of_both_machines() {
let mut fake = Fake::default();
fake.nls_pointer(TABLE);
let upcase = Upcase::of_target(&fake, upcase_globals(), layout().pointer);
assert_eq!(
upcase.source(),
UpcaseFrom::Target(TABLE),
"before anything needs a page of it, the table is simply where it says"
);
assert_eq!(
upcase.unit(0x00e9),
Upcase::of_host().unit(0x00e9),
"the unit still folds, on the host"
);
assert_eq!(
upcase.source(),
UpcaseFrom::Mixed { at: TABLE },
"and the answer now carries both machines, which is what a caller has to be able to see"
);
assert!(upcase.same_name("Device", "DEVICE"));
assert_eq!(
upcase.source(),
UpcaseFrom::Mixed { at: TABLE },
"one unit folded on the wrong machine is not undone by a later one that needed no table"
);
}
#[test]
fn test_a_32_bit_targets_table_pointer_is_read_at_its_own_width() {
const TABLE32: u64 = 0x8006_0000;
let mut fake = one_fold_table_at(TABLE32, 0x0001);
let at = SILO_GLOBALS + u64::from(NLS_STATE) + u64::from(UPCASE_TABLE);
fake.put(at, &(TABLE32 as u32).to_le_bytes());
fake.put(at + 4, &0xdead_beefu32.to_le_bytes());
let upcase = Upcase::of_target(&fake, upcase_globals(), 4);
assert_eq!(
upcase.source(),
UpcaseFrom::Target(TABLE32),
"the pointer is four bytes wide on this target"
);
assert_eq!(
upcase.unit(0x00e9),
0x00ea,
"and the fold is the one this target's table performs"
);
let eight = Upcase::of_target(&fake, upcase_globals(), 8);
assert_eq!(
eight.source(),
UpcaseFrom::Target(0xdead_beef_8006_0000),
"which is not where any table is"
);
let odd = Upcase::of_target(&fake, upcase_globals(), 2);
assert_eq!(odd.source(), UpcaseFrom::Host);
}
#[test]
fn test_the_walk_folds_at_the_pointer_width_its_layout_derived() {
const TABLE32: u64 = 0x8006_0000;
let mut fake = one_fold_table_at(TABLE32, 0x0001);
let at = SILO_GLOBALS + u64::from(NLS_STATE) + u64::from(UPCASE_TABLE);
fake.put(at, &(TABLE32 as u32).to_le_bytes());
fake.put(at + 4, &0xdead_beefu32.to_le_bytes());
let narrow = Layout {
pointer: 4,
..layout()
};
let globals = Globals {
upcase: Some(upcase_globals()),
..globals()
};
let namespace = Namespace::new(&fake, narrow, globals)
.expect("a four-byte pointer is a width this crate builds");
assert_eq!(
namespace.upcase().source(),
UpcaseFrom::Target(TABLE32),
"the walk read the table pointer at its own target's width"
);
}
struct Counting<'a> {
inner: &'a dyn Memory,
reads: RefCell<usize>,
}
impl Memory for Counting<'_> {
fn read(&self, address: u64, len: usize) -> Option<Vec<u8>> {
*self.reads.borrow_mut() += 1;
self.inner.read(address, len)
}
}
#[test]
fn test_an_ascii_name_costs_no_target_read_and_a_folded_unit_costs_one_walk() {
let table = one_fold_table(0xffe0);
let counting = Counting {
inner: &table,
reads: RefCell::new(0),
};
let upcase = Upcase::of_target(&counting, upcase_globals(), layout().pointer);
assert!(upcase.same_name("MountPointManager", "MOUNTPOINTMANAGER"));
assert_eq!(
*counting.reads.borrow(),
0,
"an ASCII name never asks where the table is"
);
assert!(upcase.same_name("\u{00e9}", "\u{00c9}"));
let first = *counting.reads.borrow();
assert_eq!(
first, 7,
"the pointer, then three levels for each of two units"
);
assert!(upcase.same_name("\u{00e9}", "\u{00c9}"));
assert_eq!(
*counting.reads.borrow(),
first,
"folding the same units again reads nothing"
);
}
fn trie_encoding(fold: impl Fn(u16) -> u16) -> Fake {
let mut elements: Vec<u16> = vec![0; 0x100];
let mut leaves: BTreeMap<Vec<u16>, u16> = BTreeMap::new();
let mut level_twos: BTreeMap<Vec<u16>, u16> = BTreeMap::new();
for high in 0..=0xffu32 {
let mut block = Vec::new();
for middle in 0..0x10u32 {
let deltas: Vec<u16> = (0..0x10u32)
.map(|low| {
let unit = ((high << 8) | (middle << 4) | low) as u16;
fold(unit).wrapping_sub(unit)
})
.collect();
let at = match leaves.get(&deltas) {
Some(at) => *at,
None => {
let at = elements.len() as u16;
elements.extend_from_slice(&deltas);
leaves.insert(deltas, at);
at
}
};
block.push(at);
}
let at = match level_twos.get(&block) {
Some(at) => *at,
None => {
let at = elements.len() as u16;
elements.extend_from_slice(&block);
level_twos.insert(block, at);
at
}
};
elements[high as usize] = at;
}
let mut fake = Fake::default();
fake.nls_pointer(TABLE);
for (index, value) in elements.iter().enumerate() {
fake.put(TABLE + (index as u64) * 2, &value.to_le_bytes());
}
fake
}
#[cfg_attr(miri, ignore = "builds from ntdll's fold; see `Upcase::on_host`")]
#[test]
fn test_the_trie_walk_reproduces_the_routine_across_every_code_unit() {
let host = Upcase::of_host();
let fake = trie_encoding(|unit| host.unit(unit));
let upcase = Upcase::of_target(&fake, upcase_globals(), layout().pointer);
let moved = (0..=0xffffu32)
.filter(|unit| host.unit(*unit as u16) != *unit as u16)
.count();
assert!(
moved > 900,
"this host's table moves {moved} code units, too few to be a real upcase table -- and \
a fixture where none moved would pass whatever the walk did"
);
let wrong: Vec<u16> = (0..=0xffffu32)
.map(|unit| unit as u16)
.filter(|unit| upcase.unit(*unit) != host.unit(*unit))
.collect();
assert!(
wrong.is_empty(),
"the walk disagrees with the routine on {} code units, first {:04x?}",
wrong.len(),
&wrong[..wrong.len().min(8)]
);
}
#[cfg_attr(miri, ignore = "folds through ntdll; see `Upcase::on_host`")]
#[test]
fn test_a_name_is_matched_through_the_object_managers_fold_and_not_through_ascii() {
let mut fake = namespace();
const ACCENTED: u64 = 0xffff_a000_0070_0000;
fake.object(ACCENTED, "K\u{e4}se", obfuscated(4, ACCENTED), 0);
fake.directory(DEVICE_DIR, &[DEVICE, ACCENTED]);
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace
.object_at("\\Device\\K\u{c4}SE")
.map(|found| found.address),
Ok(ACCENTED),
"the object manager files these under one name, so this resolves to one object"
);
}
#[cfg_attr(miri, ignore = "folds through ntdll; see `Upcase::on_host`")]
#[test]
fn test_a_name_the_table_does_not_fold_together_is_two_objects() {
let mut fake = namespace();
const DOTLESS: u64 = 0xffff_a000_0071_0000;
fake.object(DOTLESS, "\u{0131}", obfuscated(4, DOTLESS), 0);
fake.directory(DEVICE_DIR, &[DEVICE, DOTLESS]);
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace
.object_at("\\Device\\\u{0131}")
.map(|found| found.address),
Ok(DOTLESS),
"its own name still resolves"
);
assert_eq!(
namespace.object_at("\\Device\\I"),
Err(ObjectError::NotFound {
directory: "\\Device".to_string(),
component: "I".to_string(),
}),
"and `I` is a different object, which Unicode's simple mapping would have merged"
);
}
#[test]
fn test_a_missing_component_names_the_directory_it_was_not_in() {
let fake = namespace();
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.object_at("\\Device\\Nothing"),
Err(ObjectError::NotFound {
directory: "\\Device".to_string(),
component: "Nothing".to_string(),
})
);
}
#[test]
fn test_a_leaf_is_not_walked_through() {
let fake = namespace();
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.object_at("\\Device\\MountPointManager\\Deeper"),
Err(ObjectError::NotADirectory {
component: "MountPointManager".to_string(),
rest: "Deeper".to_string(),
})
);
}
#[test]
fn test_a_directory_lists_what_it_holds() {
let fake = namespace();
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.objects_in("\\Device").map(|found| found
.objects
.into_iter()
.map(|one| one.name)
.collect::<Vec<_>>()),
Ok(vec!["MountPointManager".to_string()])
);
}
#[test]
fn test_a_target_with_no_namespace_says_so_rather_than_answering_empty() {
let fake = Fake::default();
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.object_at("\\Device"),
Err(ObjectError::Unreadable {
at: ROOT_POINTER,
len: 8
})
);
}
#[test]
fn test_a_chain_of_entries_that_name_nothing_is_bounded_as_well() {
let mut fake = namespace();
const EMPTY: u64 = DEVICE_DIR + 0x8000;
fake.pointer(DEVICE_DIR, EMPTY);
fake.pointer(EMPTY, EMPTY);
fake.pointer(EMPTY + 0x08, 0);
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.objects_in("\\Device"),
Err(ObjectError::TooMany {
what: "a directory's entries",
bound: MAX_ENTRIES
})
);
}
#[test]
fn test_a_leaf_is_not_listed_as_a_directory() {
let fake = namespace();
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.objects_in("\\Device\\MountPointManager"),
Err(ObjectError::NotADirectory {
component: "MountPointManager".to_string(),
rest: String::new(),
})
);
}
#[test]
fn test_a_length_with_no_buffer_is_malformed_rather_than_empty() {
let mut fake = namespace();
const LINK: u64 = 0xffff_a000_0042_0000;
fake.flags(LINK, 0);
fake.string(LINK + 0x08, LINK + 0x1000, "\\Device\\X");
fake.pointer(LINK + 0x10, 0);
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.link_target(LINK),
Err(ObjectError::Malformed {
reason: "a UNICODE_STRING has a length and no buffer"
})
);
}
#[test]
fn test_a_looping_chain_is_refused_rather_than_walked() {
let mut fake = namespace();
let entry = DEVICE_DIR + 0x2000;
fake.pointer(DEVICE_DIR, entry);
fake.pointer(entry, entry);
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.objects_in("\\Device"),
Err(ObjectError::TooMany {
what: "a directory's entries",
bound: MAX_ENTRIES
})
);
}
#[test]
fn test_a_link_target_is_checked_before_it_is_decoded() {
let mut fake = namespace();
const LINK: u64 = 0xffff_a000_0040_0000;
fake.flags(LINK, 0);
fake.string(LINK + 0x08, LINK + 0x1000, "\\Device\\MountPointManager");
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.link_target(LINK).as_deref(),
Ok("\\Device\\MountPointManager")
);
const CALLBACK: u64 = 0xffff_a000_0041_0000;
fake.flags(CALLBACK, 0);
fake.pointer(CALLBACK + 0x08, 0xffff_f805_cb41_2341);
fake.pointer(CALLBACK + 0x10, 0xffff_a000_0050_0000);
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.link_target(CALLBACK),
Err(ObjectError::Malformed {
reason: "the link target is not a string this can vouch for"
}),
"a code address is not a whole number of UTF-16 units"
);
const DOS: u64 = 0xffff_a000_0043_0000;
fake.flags(DOS, 0);
fake.string(DOS + 0x08, DOS + 0x1000, "C:\\Windows\\System32");
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.link_target(DOS).as_deref(),
Ok("C:\\Windows\\System32"),
"the flag said this is a target, so what it holds is the answer"
);
}
#[test]
fn test_a_callback_link_has_no_target_to_read() {
let mut fake = namespace();
const LINK: u64 = 0xffff_a000_0044_0000;
fake.string(LINK + 0x08, LINK + 0x1000, "\\Device\\X");
fake.flags(LINK, 0);
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.link_target(LINK).as_deref(),
Ok("\\Device\\X"),
"with the bit clear the union holds the target"
);
fake.flags(LINK, SYMBOLIC_LINK_CALLBACK);
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.link_target(LINK),
Err(ObjectError::Malformed {
reason: "this link resolves through a callback, so it has no target to read"
}),
"and with it set the same bytes are a callback and a context"
);
}
#[test]
fn test_an_object_of_unknown_type_is_not_treated_as_a_directory() {
let mut fake = namespace();
fake.pointer(TYPE_TABLE + 3 * 8, 0);
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.object_at("\\Device\\MountPointManager"),
Err(ObjectError::Untyped {
component: "Device".to_string(),
}),
"it refuses, and says it could not tell rather than that this is something else"
);
assert!(
matches!(
namespace.objects_in("\\Device"),
Err(ObjectError::Untyped { .. })
),
"and listing it is the same refusal"
);
}
#[test]
fn test_a_name_longer_than_its_own_maximum_is_dropped_rather_than_taken_or_refused() {
let mut fake = namespace();
let header = DEVICE - 0x30;
let name_info = header - 0x20;
fake.put(name_info + 0x08, &200u16.to_le_bytes());
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
let listed = namespace
.objects_in("\\Device")
.expect("the directory still lists");
assert_eq!(
(listed.objects.len(), listed.unreadable, listed.malformed),
(0, 0, 1),
"the torn entry is dropped, and counted as the structural fault it is"
);
assert_eq!(listed.skipped(), 1);
assert!(
matches!(
namespace.object_at("\\Device\\MountPointManager"),
Err(ObjectError::NotFoundInPart {
unreadable: 0,
malformed: 1,
..
})
),
"and a lookup says it cannot call this absent, rather than that it is -- naming \
the structural fault rather than a page that was out"
);
}
#[test]
fn test_a_global_the_target_lacks_is_not_swallowed_by_the_skip() {
let fake = namespace();
let nameless = Globals {
info_mask_to_offset: None,
..globals()
};
let namespace = Namespace::new(&fake, layout(), nameless)
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.objects_in("\\"),
Err(ObjectError::Unavailable {
what: "nt!ObpInfoMaskToOffset"
}),
"a directory whose every entry is unnameable for want of a global is refused, not \
answered as empty"
);
assert_eq!(
namespace.object_at("\\Device"),
Err(ObjectError::Unavailable {
what: "nt!ObpInfoMaskToOffset"
}),
"and so is a lookup, rather than reporting the name absent"
);
}
#[test]
fn test_a_lookup_stopped_after_naming_a_component_does_not_go_on_through_it() {
let mut two = namespace();
const SECOND: u64 = ROOT + 0x4_0000;
two.object(SECOND, "Second", obfuscated(3, SECOND), 0);
two.directory(ROOT, &[DEVICE_DIR, SECOND]);
let polls = std::cell::Cell::new(0usize);
let between_namings = || {
polls.set(polls.get() + 1);
polls.get() == layout().buckets + 2 + 2
};
let looking = Namespace::new(&two, layout(), globals())
.expect("the fixture layout is one this crate builds")
.halting(&between_namings);
assert!(
matches!(
looking.object_at("\\Device\\MountPointManager"),
Err(ObjectError::Halted { .. })
),
"the lookup was stopped holding `Device`, and must not walk on into it"
);
let polls = std::cell::Cell::new(0usize);
let between_namings = || {
polls.set(polls.get() + 1);
polls.get() == layout().buckets + 2 + 2
};
let looking = Namespace::new(&two, layout(), globals())
.expect("the fixture layout is one this crate builds")
.halting(&between_namings);
assert!(
matches!(
looking.object_at("\\Device\\MountPointManager"),
Err(ObjectError::Halted { what }) if what == "Device"
),
"the halt names the component the walk was stopped at"
);
}
#[test]
fn test_a_halt_the_walk_already_reported_is_not_polled_for_a_second_time() {
let fake = namespace();
let polls = std::cell::Cell::new(0usize);
let once = || {
polls.set(polls.get() + 1);
polls.get() == 3
};
let walk = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds")
.halting(&once);
let listed = walk
.objects_in("\\")
.expect("a stopped enumeration still answers");
assert_eq!(
(listed.objects.len(), listed.halted),
(0, true),
"the entry was gathered before the stop and must not be named after it: {listed:?}"
);
assert_eq!(
polls.get(),
3,
"and nothing asked again once the halt came back, which is what consumes it"
);
let again = std::cell::Cell::new(0usize);
let once_more = || {
again.set(again.get() + 1);
again.get() == 3
};
let looking = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds")
.halting(&once_more);
assert!(
matches!(
looking.object_at("\\Device"),
Err(ObjectError::Halted { .. })
),
"a stopped lookup does not answer with the object it had already gathered"
);
}
#[test]
fn test_a_walk_stops_where_its_caller_asks_and_keeps_what_it_read() {
let fake = namespace();
let asked = std::cell::Cell::new(0usize);
let at_once = || {
asked.set(asked.get() + 1);
true
};
let stopped = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds")
.halting(&at_once);
let listed = stopped
.objects_in("\\")
.expect("a stopped enumeration still answers");
assert!(listed.halted, "it says the list is a prefix: {listed:?}");
assert!(
asked.get() > 0,
"and the predicate was polled inside the enumeration, not around it"
);
let mut looping = namespace();
const EMPTY: u64 = ROOT + 0x8000;
looping.pointer(ROOT, EMPTY);
looping.pointer(EMPTY, EMPTY);
looping.pointer(EMPTY + 0x08, 0);
let bounded = Namespace::new(&looping, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert!(
matches!(bounded.objects_in("\\"), Err(ObjectError::TooMany { .. })),
"unstoppable, this walks to its bound"
);
let halting = Namespace::new(&looping, layout(), globals())
.expect("the fixture layout is one this crate builds")
.halting(&|| true);
assert_eq!(
halting.objects_in("\\").map(|found| found.halted),
Ok(true),
"and stopped, it comes back at once rather than running to the bound"
);
let mut bare = namespace();
bare.directory(ROOT, &[]);
let empty = Namespace::new(&bare, layout(), globals())
.expect("the fixture layout is one this crate builds")
.halting(&|| true);
let listed = empty
.objects_in("\\")
.expect("an empty directory still lists");
assert!(
listed.halted,
"an empty directory is buckets to read, and stopping means not reading them: {listed:?}"
);
assert!(
!listed.is_complete(),
"and a halted listing is not a complete one, whatever its counts say"
);
let mut two = namespace();
const SECOND: u64 = ROOT + 0x4_0000;
two.object(SECOND, "Second", obfuscated(3, SECOND), 0);
two.directory(ROOT, &[DEVICE_DIR, SECOND]);
let counted = std::cell::Cell::new(0usize);
let never = || {
counted.set(counted.get() + 1);
false
};
let whole = Namespace::new(&two, layout(), globals())
.expect("the fixture layout is one this crate builds")
.halting(&never);
let all = whole.objects_in("\\").expect("nothing stopped this one");
assert_eq!(
(all.objects.len(), all.halted),
(2, false),
"the fixture holds two and nothing stopped the walk: {all:?}"
);
assert_eq!(
counted.get(),
layout().buckets + 2 + 2,
"a poll per bucket, then per chain link, then per entry -- the three phases the \
threshold below counts past"
);
let polls = std::cell::Cell::new(0usize);
let after_the_first_naming = || {
polls.set(polls.get() + 1);
polls.get() > layout().buckets + 2 + 1
};
let late = Namespace::new(&two, layout(), globals())
.expect("the fixture layout is one this crate builds")
.halting(&after_the_first_naming);
let listed = late
.objects_in("\\")
.expect("a stopped enumeration answers");
assert_eq!(
(listed.objects.len(), listed.halted),
(1, true),
"one of the two was named and the walk then stopped, which no other poll can do: \
{listed:?}"
);
let stopped = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds")
.halting(&|| true);
assert!(
matches!(
stopped.object_at("\\Device"),
Err(ObjectError::Halted { .. })
),
"a stopped lookup says it was stopped, not that the name is not there"
);
}
#[test]
fn test_a_directory_survives_an_entry_whose_name_will_not_read() {
let mut fake = namespace();
let header = DEVICE - 0x30;
let name_info = header - 0x20;
fake.put(name_info + 0x10, &0xffff_c000_dead_0000u64.to_le_bytes());
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
let listed = namespace
.objects_in("\\Device")
.expect("the directory still lists");
assert_eq!(
(listed.objects.len(), listed.unreadable, listed.malformed),
(0, 1, 0),
"the entry is dropped and counted as the transient thing it is"
);
assert!(
matches!(
namespace.object_at("\\Device\\MountPointManager"),
Err(ObjectError::NotFoundInPart {
unreadable: 1,
malformed: 0,
..
})
),
"a page that was out is reported as one, not as corruption"
);
assert_eq!(
namespace.objects_in("\\").map(|found| found
.objects
.into_iter()
.map(|one| one.name)
.collect::<Vec<_>>()),
Ok(vec!["Device".to_string()]),
"a neighbouring directory is untouched by it"
);
}
#[test]
fn test_a_name_that_is_not_text_lists_but_does_not_resolve() {
let mut fake = namespace();
const ODD: u64 = 0xffff_a000_0060_0000;
const OTHER: u64 = 0xffff_a000_0061_0000;
fake.units_named(ODD, &[0x41, 0xd800, 0x42], obfuscated(4, ODD));
fake.units_named(OTHER, &[0x41, 0xdbff, 0x42], obfuscated(4, OTHER));
fake.directory(DEVICE_DIR, &[DEVICE, ODD, OTHER]);
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
let listed = namespace
.objects_in("\\Device")
.expect("the directory lists");
let rendered: Vec<&str> = listed
.objects
.iter()
.filter(|one| !one.exact_name)
.map(|one| one.name.as_str())
.collect();
assert_eq!(
rendered.len(),
2,
"both are listed, so neither object is lost: {listed:?}"
);
assert_eq!(
rendered[0], rendered[1],
"and they render alike, which is what makes the rendering useless as an identity"
);
assert!(
matches!(
namespace.object_at(&format!("\\Device\\{}", rendered[0])),
Err(ObjectError::NotFound { .. })
),
"so neither answers to it"
);
assert_eq!(
namespace
.object_at("\\Device\\MountPointManager")
.map(|found| found.address),
Ok(DEVICE),
"and the ordinary name beside them still resolves"
);
}
#[test]
fn test_a_target_that_cannot_name_types_still_answers_what_needs_none() {
let fake = namespace();
let untyped = Globals {
header_cookie: None,
type_index_table: None,
..globals()
};
let namespace = Namespace::new(&fake, layout(), untyped)
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.object_at("\\Device").map(|found| found.address),
Ok(DEVICE_DIR),
"one component needs no type"
);
assert_eq!(
namespace.objects_in("\\").map(|found| found
.objects
.into_iter()
.map(|one| one.name)
.collect::<Vec<_>>()),
Ok(vec!["Device".to_string()]),
"nor does listing the root"
);
assert_eq!(
namespace.object_at("\\Device\\MountPointManager"),
Err(ObjectError::Untyped {
component: "Device".to_string()
}),
"and descending is refused, saying which of the two it is"
);
}
#[test]
fn test_a_link_is_read_with_none_of_the_namespaces_globals() {
let mut fake = namespace();
const LINK: u64 = 0xffff_a000_0045_0000;
fake.flags(LINK, 0);
fake.string(LINK + 0x08, LINK + 0x1000, "\\Device\\MountPointManager");
let nothing = Globals {
root: None,
info_mask_to_offset: None,
header_cookie: None,
type_index_table: None,
upcase: None,
};
let namespace = Namespace::new(&fake, layout(), nothing)
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.link_target(LINK).as_deref(),
Ok("\\Device\\MountPointManager"),
"the link reads, because it needs none of them"
);
assert_eq!(
namespace.object_at("\\Device"),
Err(ObjectError::Unavailable {
what: "nt!ObpRootDirectoryObject"
}),
"and a walk says which one it wanted"
);
}
#[test]
fn test_a_link_target_is_bounded_as_a_path_rather_than_as_a_name() {
let mut fake = namespace();
const LINK: u64 = 0xffff_a000_0046_0000;
let long = format!("\\Device\\{}", "D".repeat(MAX_NAME_BYTES));
fake.flags(LINK, 0);
fake.string(LINK + 0x08, LINK + 0x1000, &long);
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert_eq!(
namespace.link_target(LINK).as_deref(),
Ok(long.as_str()),
"a target this long is a path, not a name that got out of hand"
);
}
#[test]
fn test_a_layout_this_crate_did_not_build_is_refused_rather_than_panicked_on() {
let fake = namespace();
let refused = |layout: Layout| match Namespace::new(&fake, layout, globals()) {
Err(ObjectError::Malformed { reason }) => reason,
other => panic!("a layout that is not one was accepted: {:?}", other.is_ok()),
};
assert_eq!(
refused(Layout {
pointer: 2,
..layout()
}),
"a pointer on this target is neither four bytes nor eight"
);
assert_eq!(
refused(Layout {
pointer: usize::MAX,
..layout()
}),
"a pointer on this target is neither four bytes nor eight"
);
assert_eq!(
refused(Layout {
buckets: 0,
..layout()
}),
"a directory's bucket count is not one a directory has"
);
assert_eq!(
refused(Layout {
buckets: MAX_BUCKETS + 1,
..layout()
}),
"a directory's bucket count is not one a directory has"
);
assert_eq!(
refused(Layout {
unicode_length: 0x40,
..layout()
}),
"a UNICODE_STRING's lengths sit outside the structure"
);
assert_eq!(
refused(Layout {
name_info_name: 0x40,
..layout()
}),
"a name header's string sits outside the name header"
);
assert_eq!(
refused(Layout {
pointer: 4,
unicode_buffer: u32::MAX - 4,
name_info_name: 0,
name_info_size: u32::MAX,
..layout()
}),
"a field sits further into its structure than any of these reach"
);
assert!(Namespace::new(&fake, layout(), globals()).is_ok());
}
#[test]
fn test_listing_with_no_path_is_refused_rather_than_answered_about_the_root() {
let fake = namespace();
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
for path in ["", "Device", "\\\\", "\\\\\\"] {
assert!(
matches!(namespace.objects_in(path), Err(ObjectError::BadPath { .. })),
"{path:?} is not the root"
);
assert!(
matches!(namespace.object_at(path), Err(ObjectError::BadPath { .. })),
"{path:?} is not an object either, and the two agree now"
);
}
assert!(
matches!(
namespace.object_at("\\Device\\\\MountPointManager"),
Err(ObjectError::BadPath { .. })
),
"an empty component is refused rather than dropped"
);
let root = |path: &str| {
namespace.objects_in(path).map(|found| {
found
.objects
.into_iter()
.map(|one| one.name)
.collect::<Vec<_>>()
})
};
assert_eq!(
root("\\"),
Ok(vec!["Device".to_string()]),
"while the root itself lists"
);
assert_eq!(
namespace
.objects_in("\\Device\\")
.map(|found| found.objects.len()),
Ok(1),
"and one trailing separator is still a caller's convenience"
);
}
#[test]
fn test_a_path_that_is_not_one_is_refused() {
let fake = namespace();
let namespace = Namespace::new(&fake, layout(), globals())
.expect("the fixture layout is one this crate builds");
assert!(matches!(
namespace.object_at("Device"),
Err(ObjectError::BadPath { .. })
));
assert!(matches!(
namespace.object_at("\\"),
Err(ObjectError::BadPath { .. })
));
}
}