use std::collections::{HashMap, HashSet};
use std::sync::{Arc, OnceLock};
fn unwind_trace_enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| std::env::var_os("NTOSEYE_UNWIND_TRACE").is_some())
}
macro_rules! unwind_trace {
($($arg:tt)*) => {
if unwind_trace_enabled() {
eprintln!($($arg)*);
}
};
}
use std::cmp::Ordering;
use pelite::pe64::{
Pe, PeView,
image::{
IMAGE_DIRECTORY_ENTRY_EXCEPTION, IMAGE_SCN_MEM_EXECUTE, RUNTIME_FUNCTION,
UNW_FLAG_CHAININFO, UWOP_ALLOC_LARGE, UWOP_ALLOC_SMALL, UWOP_PUSH_MACHFRAME,
UWOP_PUSH_NONVOL, UWOP_SAVE_NONVOL, UWOP_SAVE_NONVOL_FAR, UWOP_SAVE_XMM128,
UWOP_SAVE_XMM128_FAR, UWOP_SET_FPREG,
},
};
use crate::{
backend::MemoryOps,
bugchecks::looks_like_kernel_pointer,
error::{Error, Result},
gdb::RegisterMap,
guest::{Guest, ModuleInfo, PeImage, ProcessInfo, read_pe_image, read_pe_image_from_file},
memory::{AddressSpace, DTB_IDENTITY},
phys::PhysMem,
symbols::{SourceLocation, SymbolStore},
target::{SavedThreadRegisters, Target, ThreadInfo},
trapframe::{decode_kswitch_frame_seed, decode_ktrap_frame_for_thread},
types::{Arch, Dtb, VirtAddr},
};
const CR3_PAGE_MASK: u64 = 0x000F_FFFF_FFFF_F000;
const STACK_SCAN_BYTES: usize = 0x1000;
const MAX_CHAIN_DEPTH: usize = 32;
const MAX_UNWIND_FRAMES: usize = 1024;
const UWOP_EPILOG: u8 = 6;
const UWOP_SPARE_CODE: u8 = 7;
const UNWIND_REG_NAMES: [&str; 16] = [
"rax", "rcx", "rdx", "rbx", "rsp", "rbp", "rsi", "rdi", "r8", "r9", "r10", "r11", "r12", "r13",
"r14", "r15",
];
#[derive(Debug, Clone)]
pub struct ThreadTraceContext {
pub description: String,
pub active_dtb: Dtb,
pub kernel_dtb: Dtb,
pub process_dtb: Option<Dtb>,
pub kernel_modules: Vec<ModuleInfo>,
pub process_modules: Vec<ModuleInfo>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameSource {
Current,
Seed,
Unwind,
Scan,
}
impl FrameSource {
pub fn as_str(self) -> &'static str {
match self {
FrameSource::Current => "current",
FrameSource::Seed => "seed",
FrameSource::Unwind => "unwind",
FrameSource::Scan => "scan",
}
}
}
#[derive(Debug, Clone)]
pub struct StackFrame {
pub sp: u64,
pub ip: u64,
pub symbol: String,
pub source: FrameSource,
pub source_location: Option<SourceLocation>,
}
#[derive(Debug, Clone, Default)]
pub struct StackTrace {
pub frames: Vec<StackFrame>,
pub truncated: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThreadStackSource {
TrapFrame { address: VirtAddr },
ContextSwitch { kernel_stack: VirtAddr },
}
impl ThreadStackSource {
pub fn as_str(self) -> &'static str {
match self {
Self::TrapFrame { .. } => "ktrap-frame",
Self::ContextSwitch { .. } => "kernel-stack",
}
}
}
#[derive(Debug, Clone)]
pub struct ThreadStackTrace {
pub source: ThreadStackSource,
pub stacktrace: StackTrace,
}
#[derive(Clone, Debug)]
struct RegisterContext {
rip: u64,
rsp: u64,
regs: [Option<u64>; 16],
}
#[derive(Debug, Clone)]
struct CachedModule {
info: ModuleInfo,
image: Arc<PeImage>,
executable_ranges: Vec<(u32, u32)>,
}
#[derive(Debug, Clone)]
struct OwnedModule {
info: ModuleInfo,
dtb: Dtb,
}
#[derive(Debug, Clone, Copy)]
struct UnwindCodeSlot {
code_offset: u8,
unwind_op: u8,
op_info: u8,
raw_op_info: u8,
}
#[derive(Debug, Clone)]
struct ParsedUnwindInfo {
size_of_prolog: u8,
frame_register: u8,
frame_offset: u8,
codes: Vec<UnwindCodeSlot>,
chained_unwind_data: Option<u32>,
}
enum UnwindStep {
Continue,
MachineFrame,
}
enum Unwound {
Stop,
Frame { stack_switch: bool },
}
struct StackTracer<'a> {
trace: &'a ThreadTraceContext,
phys: &'a PhysMem,
symbols: &'a SymbolStore,
memory: AddressSpace<'a, PhysMem>,
modules: HashMap<(Dtb, u64), CachedModule>,
}
pub fn resolve_thread_trace_context(debugger: &Target, cr3: u64) -> ThreadTraceContext {
let cr3_masked = cr3 & CR3_PAGE_MASK;
let kernel_dtb = debugger.kernel_dtb();
let kernel_dtb_masked = kernel_dtb & CR3_PAGE_MASK;
if kernel_dtb == DTB_IDENTITY || cr3_masked == kernel_dtb_masked {
return ThreadTraceContext {
description: "kernel".to_string(),
active_dtb: kernel_dtb,
kernel_dtb,
process_dtb: None,
kernel_modules: debugger.kernel_modules().unwrap_or_default(),
process_modules: Vec::new(),
};
}
if let Some(proc_info) = find_process_by_cr3(debugger, cr3_masked) {
let process_modules = debugger
.guest
.as_ref()
.map(|g| g.process_modules(&proc_info).unwrap_or_default())
.unwrap_or_default();
return ThreadTraceContext {
description: format!("{} ({})", proc_info.name, proc_info.pid),
active_dtb: cr3_masked,
kernel_dtb,
process_dtb: Some(proc_info.dtb),
kernel_modules: debugger.kernel_modules().unwrap_or_default(),
process_modules,
};
}
ThreadTraceContext {
description: "unknown".to_string(),
active_dtb: cr3_masked,
kernel_dtb,
process_dtb: None,
kernel_modules: debugger.kernel_modules().unwrap_or_default(),
process_modules: Vec::new(),
}
}
pub fn try_format_symbol(
debugger: &Target,
trace: &ThreadTraceContext,
addr: u64,
) -> Option<String> {
let try_format = |dtb| {
debugger
.symbols
.format_closest_symbol_for_address(dtb, VirtAddr(addr))
};
if let Some(module) = trace.module_for_address(addr) {
return Some(try_format(module.dtb).unwrap_or_else(|| {
let offset = addr.saturating_sub(module.info.base_address.0);
format!("{}+{:#x}", module.info.short_name, offset)
}));
}
if let Some(process_dtb) = trace.process_dtb
&& let Some(symbol) = try_format(process_dtb)
{
return Some(symbol);
}
try_format(trace.kernel_dtb)
}
pub fn format_symbol(debugger: &Target, trace: &ThreadTraceContext, addr: u64) -> String {
try_format_symbol(debugger, trace, addr).unwrap_or_else(|| format!("{addr:#x}"))
}
pub fn preferred_code_dtb(trace: &ThreadTraceContext, addr: u64) -> Dtb {
trace
.module_for_address(addr)
.map(|module| module.dtb)
.unwrap_or(trace.active_dtb)
}
fn frame_source_location(
debugger: &Target,
trace: &ThreadTraceContext,
address: u64,
) -> Option<SourceLocation> {
let module = trace.module_for_address(address)?;
debugger
.symbols
.source_location(module.dtb, VirtAddr(address))
}
fn image_u32(bytes: &[u8], offset: usize) -> Option<u32> {
Some(u32::from_le_bytes(
bytes.get(offset..offset.checked_add(4)?)?.try_into().ok()?,
))
}
fn arm64_function_length(image: &PeImage, unwind_data: u32) -> Option<u32> {
let instructions = match unwind_data & 0b11 {
0 => {
let xdata_rva = (unwind_data & !0b11) as usize;
let header = image_u32(image.present_slice(xdata_rva, 4)?, 0)?;
header & 0x3ffff
}
1 | 2 => (unwind_data >> 2) & 0x7ff,
_ => return None,
};
(instructions != 0).then(|| instructions * 4)
}
fn lookup_arm64_runtime_function(image: &PeImage, pdata: &[u8], rva: u32) -> Option<(u32, u32)> {
let count = pdata.len() / 8;
let mut low = 0usize;
let mut high = count;
while low < high {
let mid = low + (high - low) / 2;
let begin = image_u32(pdata, mid * 8)?;
if begin <= rva {
low = mid + 1;
} else {
high = mid;
}
}
let index = low.checked_sub(1)?;
let begin = image_u32(pdata, index * 8)?;
let unwind_data = image_u32(pdata, index * 8 + 4)?;
let end = begin.checked_add(arm64_function_length(image, unwind_data)?)?;
(rva >= begin && rva < end).then_some((begin, end))
}
pub fn function_range(
debugger: &Target,
trace: &ThreadTraceContext,
address: u64,
) -> Option<(u64, u64)> {
fn range(image: &PeImage, base: u64, address: u64, arch: Arch) -> Option<(u64, u64)> {
let view = PeView::from_bytes(image.as_slice()).ok()?;
let rva = u32::try_from(address.checked_sub(base)?).ok()?;
let (begin, end) = match arch {
Arch::Amd64 => {
let functions = view.exception().ok()?;
let function = lookup_runtime_function(functions.image(), rva)?;
(function.BeginAddress, function.EndAddress)
}
Arch::Arm64 => {
let directory = view.data_directory().get(IMAGE_DIRECTORY_ENTRY_EXCEPTION)?;
let pdata = image
.present_slice(directory.VirtualAddress as usize, directory.Size as usize)?;
lookup_arm64_runtime_function(image, pdata, rva)?
}
};
Some((base + u64::from(begin), base + u64::from(end)))
}
let mut tracer = StackTracer::new(debugger, trace);
let base = tracer.module_containing(address)?.info.base_address.0;
let arch = debugger.arch();
let image = tracer.module_image(address)?;
if let Some(found) = range(&image, base, address, arch) {
return Some(found);
}
if !image.is_complete() && tracer.upgrade_module_image(address) {
let image = tracer.module_image(address)?;
return range(&image, base, address, arch);
}
None
}
fn function_body_address(
debugger: &Target,
trace: &ThreadTraceContext,
address: u64,
) -> Option<u64> {
let (_, end) = function_range(debugger, trace, address)?;
let mut tracer = StackTracer::new(debugger, trace);
let base = tracer.module_containing(address)?.info.base_address.0;
let mut image = tracer.module_image(address)?;
let mut resolved = resolve_function(&image, base, address);
if matches!(resolved, Resolve::Holed)
&& !image.is_complete()
&& tracer.upgrade_module_image(address)
{
image = tracer.module_image(address)?;
resolved = resolve_function(&image, base, address);
}
let Resolve::Function { unwind_data, begin } = resolved else {
return None;
};
let unwind = parse_unwind_info(&image, unwind_data)?;
let body = base
.checked_add(u64::from(begin))?
.checked_add(u64::from(unwind.size_of_prolog))?;
(body < end).then_some(body)
}
fn recover_context_switch_seed(
debugger: &Target,
process_dtb: Dtb,
kernel_stack: VirtAddr,
) -> Result<RegisterContext> {
let ntoskrnl = &debugger.guest()?.ntoskrnl;
let swap_context = ntoskrnl.symbol("SwapContext")?.address().0;
let ki_swap_context = ntoskrnl.symbol("KiSwapContext")?.address().0;
let trace = resolve_thread_trace_context(debugger, process_dtb);
let body_rip = function_body_address(debugger, &trace, swap_context)
.ok_or_else(|| Error::DebugInfo("SwapContext has no usable PE unwind metadata".into()))?;
let ki_swap_range = function_range(debugger, &trace, ki_swap_context)
.ok_or_else(|| Error::DebugInfo("KiSwapContext has no usable PE unwind metadata".into()))?;
let mut tracer = StackTracer::new(debugger, &trace);
let mut seed = RegisterContext {
rip: body_rip,
rsp: kernel_stack.0,
regs: [None; 16],
};
if !matches!(
tracer.unwind_once(&mut seed),
Unwound::Frame {
stack_switch: false
}
) {
return Err(Error::DebugInfo(
"failed to unwind the saved SwapContext frame".into(),
));
}
if seed.rsp <= kernel_stack.0 || !(ki_swap_range.0..ki_swap_range.1).contains(&seed.rip) {
return Err(Error::DebugInfo(format!(
"SwapContext returned outside KiSwapContext ({:#x}, RSP {:#x})",
seed.rip, seed.rsp
)));
}
let mut caller = seed.clone();
if !matches!(
tracer.unwind_once(&mut caller),
Unwound::Frame {
stack_switch: false
}
) || caller.rsp <= seed.rsp
|| !tracer.is_executable_address(caller.rip)
{
return Err(Error::DebugInfo(
"failed to validate the saved KiSwapContext frame".into(),
));
}
Ok(seed)
}
pub fn build_stacktrace(
debugger: &Target,
register_map: &RegisterMap,
regs: &[u8],
limit: usize,
) -> StackTrace {
if debugger.arch() == Arch::Arm64 {
return build_stacktrace_arm64(debugger, register_map, regs, limit);
}
let cr3 = register_map.read_u64("cr3", regs).unwrap_or(0);
let trace = resolve_thread_trace_context(debugger, cr3);
build_stacktrace_seeded(
debugger,
&trace,
RegisterContext::from_registers(register_map, regs),
FrameSource::Current,
limit,
)
}
fn strip_pac(addr: u64) -> u64 {
((addr << 8) as i64 >> 8) as u64
}
fn build_stacktrace_arm64(
debugger: &Target,
register_map: &RegisterMap,
regs: &[u8],
limit: usize,
) -> StackTrace {
let cr3 = register_map.read_u64("cr3", regs).unwrap_or(0);
let trace = resolve_thread_trace_context(debugger, cr3);
let seed_pc = register_map.read_u64("rip", regs).unwrap_or(0);
let seed_sp = register_map.read_u64("rsp", regs).unwrap_or(0);
let mut raw: Vec<(u64, u64, FrameSource)> = vec![(seed_sp, seed_pc, FrameSource::Current)];
let memory = debugger.address_space(trace.active_dtb);
let mut fp = register_map.read_u64("fp", regs).unwrap_or(0);
for _ in 0..MAX_UNWIND_FRAMES {
let mut buf = [0u8; 16];
if memory.read_bytes(VirtAddr(fp), &mut buf).is_err() {
break;
}
let next_fp = u64::from_le_bytes(buf[0..8].try_into().unwrap());
let ra = strip_pac(u64::from_le_bytes(buf[8..16].try_into().unwrap()));
if ra == 0 || next_fp == 0 || next_fp <= fp {
break;
}
raw.push((fp + 16, ra, FrameSource::Unwind));
fp = next_fp;
}
ensure_frame_module_symbols(debugger, &trace, raw.iter().map(|(_, ip, _)| *ip));
let mut stacktrace = StackTrace::default();
for (sp, ip, source) in raw {
record_stack_frame(
&mut stacktrace,
limit,
StackFrame {
sp,
ip,
symbol: format_symbol(debugger, &trace, ip),
source,
source_location: frame_source_location(debugger, &trace, ip),
},
);
}
stacktrace
}
fn switch_seed_is_plausible(thread: &ThreadInfo, seed: &RegisterContext) -> bool {
let (Some(kernel_stack), Some(stack_limit), Some(stack_base)) =
(thread.kernel_stack, thread.stack_limit, thread.stack_base)
else {
return false;
};
thread.kernel_stack_resident != Some(false)
&& looks_like_kernel_pointer(seed.rip)
&& kernel_stack >= stack_limit
&& kernel_stack < stack_base
&& seed.rsp >= stack_limit.0
&& seed.rsp <= stack_base.0
}
pub fn build_parked_thread_stack(
debugger: &Target,
thread: &ThreadInfo,
limit: usize,
) -> Result<ThreadStackTrace> {
let process_dtb = debugger.thread_process_dtb(thread).ok_or_else(|| {
Error::DebugInfo("parked thread owning process DTB is unavailable".into())
})?;
let trace = resolve_thread_trace_context(debugger, process_dtb);
let mut failures = Vec::new();
if let Some(address) = thread.trap_frame {
match decode_ktrap_frame_for_thread(debugger, process_dtb, address)
.ok()
.and_then(|registers| RegisterContext::from_saved(®isters))
{
Some(seed) if seed.rip != 0 && seed.rsp != 0 => {
return Ok(ThreadStackTrace {
source: ThreadStackSource::TrapFrame { address },
stacktrace: build_stacktrace_seeded(
debugger,
&trace,
seed,
FrameSource::Seed,
limit,
),
});
}
_ => failures.push("KTHREAD.TrapFrame is absent or unusable".to_string()),
}
} else {
failures.push("KTHREAD.TrapFrame is not present".to_string());
}
if let Some(kernel_stack) = thread.kernel_stack {
let pdb_seed = decode_kswitch_frame_seed(debugger, process_dtb, kernel_stack)
.ok()
.and_then(|registers| RegisterContext::from_saved(®isters));
let seed = match pdb_seed {
Some(seed) => Ok(seed),
None => recover_context_switch_seed(debugger, process_dtb, kernel_stack),
};
match seed {
Ok(seed) if switch_seed_is_plausible(thread, &seed) => {
return Ok(ThreadStackTrace {
source: ThreadStackSource::ContextSwitch { kernel_stack },
stacktrace: build_stacktrace_seeded(
debugger,
&trace,
seed,
FrameSource::Seed,
limit,
),
});
}
Ok(_) => failures.push(
"context-switch seed is outside the captured resident kernel stack".to_string(),
),
Err(error) => failures.push(format!("context-switch seed is unavailable: {error}")),
}
} else {
failures.push("KTHREAD.KernelStack is not present".to_string());
}
Err(Error::DebugInfo(format!(
"parked thread stack unavailable: {}",
failures.join("; ")
)))
}
fn build_stacktrace_seeded(
debugger: &Target,
trace: &ThreadTraceContext,
mut context: RegisterContext,
initial_source: FrameSource,
limit: usize,
) -> StackTrace {
let limit = limit.max(1);
let mut raw: Vec<(u64, u64, FrameSource)> = vec![(context.rsp, context.rip, initial_source)];
let mut tracer = StackTracer::new(debugger, trace);
let mut seen = HashSet::from([context.rip]);
for _ in 0..MAX_UNWIND_FRAMES {
let previous_rip = context.rip;
let previous_rsp = context.rsp;
let stack_switch = match tracer.unwind_once(&mut context) {
Unwound::Stop => break,
Unwound::Frame { stack_switch } => stack_switch,
};
if context.rip == 0 || context.rip == previous_rip {
break;
}
if !stack_switch && context.rsp <= previous_rsp {
break;
}
seen.insert(context.rip);
raw.push((context.rsp, context.rip, FrameSource::Unwind));
}
for (sp, ip) in tracer.scan_stack(context.rsp, &seen) {
raw.push((sp, ip, FrameSource::Scan));
}
ensure_frame_module_symbols(debugger, trace, raw.iter().map(|(_, ip, _)| *ip));
let mut stacktrace = StackTrace::default();
for (sp, ip, source) in raw {
record_stack_frame(
&mut stacktrace,
limit,
StackFrame {
sp,
ip,
symbol: format_symbol(debugger, trace, ip),
source,
source_location: frame_source_location(debugger, trace, ip),
},
);
}
stacktrace
}
fn ensure_frame_module_symbols(
debugger: &Target,
trace: &ThreadTraceContext,
ips: impl Iterator<Item = u64>,
) {
let mut by_dtb: HashMap<Dtb, Vec<ModuleInfo>> = HashMap::new();
let mut seen: HashSet<(Dtb, u64)> = HashSet::new();
for ip in ips {
let Some(module) = trace.module_for_address(ip) else {
continue;
};
let key = (module.dtb, module.info.base_address.0);
if seen.insert(key)
&& debugger
.symbols
.module_symbol_status(module.dtb, module.info.base_address)
.is_none()
{
by_dtb.entry(module.dtb).or_default().push(module.info);
}
}
for (dtb, modules) in by_dtb {
let _ = if let Some(g) = debugger.guest.as_ref() {
g.load_symbols_for_modules(&debugger.phys, &debugger.symbols, modules, dtb)
} else {
Guest::load_module_symbols(
&debugger.phys,
&debugger.symbols,
modules,
dtb,
false,
debugger.arch(),
)
};
}
}
fn record_stack_frame(stacktrace: &mut StackTrace, limit: usize, frame: StackFrame) {
if stacktrace.frames.len() < limit {
stacktrace.frames.push(frame);
} else {
stacktrace.truncated += 1;
}
}
fn find_process_by_cr3(debugger: &Target, cr3_masked: u64) -> Option<ProcessInfo> {
debugger
.guest
.as_ref()?
.enumerate_processes()
.ok()?
.into_iter()
.find(|proc| (proc.dtb & CR3_PAGE_MASK) == cr3_masked)
}
impl RegisterContext {
fn from_registers(register_map: &RegisterMap, regs: &[u8]) -> Self {
let mut register_values = [None; 16];
for (index, name) in UNWIND_REG_NAMES.iter().enumerate() {
register_values[index] = register_map.read_u64(*name, regs).ok();
}
Self {
rip: register_map.read_u64("rip", regs).unwrap_or(0),
rsp: register_map.read_u64("rsp", regs).unwrap_or(0),
regs: register_values,
}
}
fn from_saved(registers: &SavedThreadRegisters) -> Option<Self> {
let rip = registers.rip?;
let rsp = registers.rsp?;
let mut values = [None; 16];
for (index, name) in UNWIND_REG_NAMES.iter().enumerate() {
values[index] = registers.get(name);
}
Some(Self {
rip,
rsp,
regs: values,
})
}
fn get(&self, register: u8) -> Option<u64> {
match register {
4 => Some(self.rsp),
_ => self.regs.get(register as usize).copied().flatten(),
}
}
fn set(&mut self, register: u8, value: u64) {
if register == 4 {
self.rsp = value;
}
if let Some(slot) = self.regs.get_mut(register as usize) {
*slot = Some(value);
}
}
}
impl ThreadTraceContext {
fn module_for_address(&self, address: u64) -> Option<OwnedModule> {
self.kernel_modules
.iter()
.find(|module| module.contains_address(VirtAddr(address)))
.cloned()
.map(|info| OwnedModule {
info,
dtb: self.kernel_dtb,
})
.or_else(|| {
self.process_modules
.iter()
.find(|module| module.contains_address(VirtAddr(address)))
.cloned()
.map(|info| OwnedModule {
info,
dtb: self.process_dtb.unwrap_or(self.active_dtb),
})
})
}
}
impl<'a> StackTracer<'a> {
fn new(debugger: &'a Target, trace: &'a ThreadTraceContext) -> Self {
Self {
trace,
phys: &debugger.phys,
symbols: &debugger.symbols,
memory: debugger.address_space(trace.active_dtb),
modules: HashMap::new(),
}
}
fn unwind_once(&mut self, context: &mut RegisterContext) -> Unwound {
unwind_trace!("unwind: rip={:#x} rsp={:#x}", context.rip, context.rsp);
let Some(base_address) = self
.module_containing(context.rip)
.map(|module| module.info.base_address.0)
else {
unwind_trace!("unwind: no module for rip -> leaf");
return self.unwind_leaf(context);
};
let Some(mut image) = self.module_image(context.rip) else {
return Unwound::Stop;
};
let mut resolved = resolve_function(&image, base_address, context.rip);
if matches!(resolved, Resolve::Holed)
&& !image.is_complete()
&& self.upgrade_module_image(context.rip)
{
let Some(upgraded) = self.module_image(context.rip) else {
return Unwound::Stop;
};
image = upgraded;
resolved = resolve_function(&image, base_address, context.rip);
}
let (mut unwind_data, begin) = match resolved {
Resolve::Function { unwind_data, begin } => (unwind_data, begin),
Resolve::Leaf => {
unwind_trace!("unwind: no unwind info for rip -> leaf (true leaf)");
return self.unwind_leaf(context);
}
Resolve::Holed => {
unwind_trace!("unwind: unwind data paged out and unrecoverable -> stop");
return Unwound::Stop;
}
};
let rva = (context.rip - base_address) as u32;
let rip_offset = rva.saturating_sub(begin);
let mut primary = true;
for _ in 0..MAX_CHAIN_DEPTH {
let Some(unwind_info) = parse_unwind_info(&image, unwind_data) else {
unwind_trace!(
"unwind: parse_unwind_info failed/holed at unwind_data={unwind_data:#x} -> stop"
);
return Unwound::Stop;
};
unwind_trace!(
"unwind: rva={:#x} begin={:#x} prolog={:#x} codes={} chained={} in_prolog={}",
rva,
begin,
unwind_info.size_of_prolog,
unwind_info.codes.len(),
unwind_info.chained_unwind_data.is_some(),
primary && rip_offset < unwind_info.size_of_prolog as u32,
);
let in_prolog = primary && rip_offset < unwind_info.size_of_prolog as u32;
match self.apply_unwind_codes(context, &unwind_info, in_prolog, rip_offset) {
Some(UnwindStep::Continue) => {}
Some(UnwindStep::MachineFrame) => {
unwind_trace!(
"unwind: machine frame -> rip={:#x} rsp={:#x}",
context.rip,
context.rsp
);
return Unwound::Frame { stack_switch: true };
}
None => {
unwind_trace!("unwind: malformed unwind codes -> stop");
return Unwound::Stop;
}
}
match unwind_info.chained_unwind_data {
Some(next) => {
unwind_data = next;
primary = false;
}
None => {
let Ok(return_address) = self.memory.read::<u64>(VirtAddr(context.rsp)) else {
unwind_trace!(
"unwind: return-address read failed at rsp={:#x} -> stop",
context.rsp
);
return Unwound::Stop;
};
unwind_trace!(
"unwind: pop return -> rip={return_address:#x} rsp={:#x}",
context.rsp.saturating_add(8)
);
context.rip = return_address;
context.rsp = context.rsp.saturating_add(8);
return Unwound::Frame {
stack_switch: false,
};
}
}
}
Unwound::Stop
}
fn apply_unwind_codes(
&self,
context: &mut RegisterContext,
unwind_info: &ParsedUnwindInfo,
in_prolog: bool,
rip_offset: u32,
) -> Option<UnwindStep> {
let original_context = context.clone();
let mut index = 0usize;
while index < unwind_info.codes.len() {
let slot = unwind_info.codes[index];
let slots_used = unwind_slot_count(slot.unwind_op, slot.op_info);
if slots_used == 0 || index + slots_used > unwind_info.codes.len() {
return None;
}
let executed = !in_prolog || u32::from(slot.code_offset) <= rip_offset;
if executed
&& let UnwindStep::MachineFrame =
self.apply_unwind_code(context, &original_context, unwind_info, index)?
{
return Some(UnwindStep::MachineFrame);
}
index += slots_used;
}
Some(UnwindStep::Continue)
}
fn unwind_leaf(&mut self, context: &mut RegisterContext) -> Unwound {
let Ok(return_address) = self.memory.read::<u64>(VirtAddr(context.rsp)) else {
return Unwound::Stop;
};
if !self.is_executable_address(return_address) {
return Unwound::Stop;
}
context.rip = return_address;
context.rsp = context.rsp.saturating_add(8);
Unwound::Frame {
stack_switch: false,
}
}
fn apply_unwind_code(
&self,
context: &mut RegisterContext,
original_context: &RegisterContext,
unwind_info: &ParsedUnwindInfo,
index: usize,
) -> Option<UnwindStep> {
let slot = unwind_info.codes[index];
match slot.unwind_op {
UWOP_PUSH_NONVOL => {
let saved = self.memory.read::<u64>(VirtAddr(context.rsp)).ok()?;
context.set(slot.op_info, saved);
context.rsp = context.rsp.saturating_add(8);
}
UWOP_ALLOC_SMALL => {
context.rsp = context
.rsp
.saturating_add(((u64::from(slot.op_info) + 1) * 8).max(8));
}
UWOP_ALLOC_LARGE => {
let allocation = if slot.op_info == 0 {
u64::from(slot_u16(&unwind_info.codes, index + 1)?) * 8
} else if slot.op_info == 1 {
u64::from(slot_u16(&unwind_info.codes, index + 1)?)
| (u64::from(slot_u16(&unwind_info.codes, index + 2)?) << 16)
} else {
return None;
};
context.rsp = context.rsp.saturating_add(allocation);
}
UWOP_SET_FPREG => {
context.rsp = frame_base(context, unwind_info)?;
}
UWOP_EPILOG | UWOP_SPARE_CODE => {
}
UWOP_SAVE_NONVOL | UWOP_SAVE_XMM128 => {
let offset = if slot.unwind_op == UWOP_SAVE_NONVOL {
u64::from(slot_u16(&unwind_info.codes, index + 1)?) * 8
} else {
u64::from(slot_u16(&unwind_info.codes, index + 1)?) * 16
};
if slot.unwind_op == UWOP_SAVE_NONVOL {
let base = frame_base(original_context, unwind_info)?;
let saved = self.memory.read::<u64>(VirtAddr(base + offset)).ok()?;
context.set(slot.op_info, saved);
}
}
UWOP_SAVE_NONVOL_FAR | UWOP_SAVE_XMM128_FAR => {
let offset = u64::from(slot_u16(&unwind_info.codes, index + 1)?)
| (u64::from(slot_u16(&unwind_info.codes, index + 2)?) << 16);
let scaled = if slot.unwind_op == UWOP_SAVE_NONVOL_FAR {
offset
} else {
offset * 16
};
if slot.unwind_op == UWOP_SAVE_NONVOL_FAR {
let base = frame_base(original_context, unwind_info)?;
let saved = self.memory.read::<u64>(VirtAddr(base + scaled)).ok()?;
context.set(slot.op_info, saved);
}
}
UWOP_PUSH_MACHFRAME => {
let base = if slot.op_info == 1 {
context.rsp.saturating_add(8)
} else {
context.rsp
};
let return_rip = self.memory.read::<u64>(VirtAddr(base)).ok()?;
let return_rsp = self
.memory
.read::<u64>(VirtAddr(base.saturating_add(24)))
.ok()?;
context.rip = return_rip;
context.rsp = return_rsp;
return Some(UnwindStep::MachineFrame);
}
_ => return None,
}
Some(UnwindStep::Continue)
}
fn scan_stack(&mut self, start_rsp: u64, seen: &HashSet<u64>) -> Vec<(u64, u64)> {
let mut frames = Vec::new();
let mut failures = 0usize;
for slot in 0..(STACK_SCAN_BYTES / 8) {
if failures >= 32 {
break;
}
let sp = start_rsp.saturating_add((slot * 8) as u64);
let potential_ip = match self.memory.read::<u64>(VirtAddr(sp)) {
Ok(addr) => {
failures = 0;
addr
}
Err(_) => {
failures += 1;
continue;
}
};
if seen.contains(&potential_ip) || !self.is_executable_address(potential_ip) {
continue;
}
frames.push((sp, potential_ip));
}
frames
}
fn is_executable_address(&mut self, address: u64) -> bool {
let Some(module) = self.module_containing(address) else {
return false;
};
let rva = (address - module.info.base_address.0) as u32;
module
.executable_ranges
.iter()
.any(|(start, end)| rva >= *start && rva < *end)
}
fn module_containing(&mut self, address: u64) -> Option<&CachedModule> {
let module = self.trace.module_for_address(address)?;
self.ensure_module_loaded(&module)?;
self.modules.get(&(module.dtb, module.info.base_address.0))
}
fn module_image(&self, address: u64) -> Option<Arc<PeImage>> {
let module = self.trace.module_for_address(address)?;
self.modules
.get(&(module.dtb, module.info.base_address.0))
.map(|cached| cached.image.clone())
}
fn upgrade_module_image(&mut self, address: u64) -> bool {
let Some(module) = self.trace.module_for_address(address) else {
return false;
};
let key = (module.dtb, module.info.base_address.0);
let disk = {
let Some(cached) = self.modules.get(&key) else {
return false;
};
if cached.image.is_complete() {
return false;
}
self.load_on_disk_image(&cached.image, &module.info)
};
let Some(disk) = disk else {
return false;
};
unwind_trace!(
"unwind: recovered on-disk image for {} (in-memory unwind data paged out)",
module.info.short_name
);
let executable_ranges = executable_ranges(&disk);
self.modules.insert(
key,
CachedModule {
info: module.info.clone(),
image: Arc::new(disk),
executable_ranges,
},
);
true
}
fn ensure_module_loaded(&mut self, module: &OwnedModule) -> Option<()> {
let key = (module.dtb, module.info.base_address.0);
if self.modules.contains_key(&key) {
return Some(());
}
let image_memory = AddressSpace::new(self.phys, module.dtb);
let image = match read_pe_image(module.info.base_address, &image_memory) {
Ok(img) => img,
Err(_) => {
let tds = module.info.time_date_stamp?;
unwind_trace!(
"unwind: in-memory PE unreadable for {}, downloading via timestamp",
module.info.short_name
);
let path = self
.symbols
.ensure_module_image_on_disk(&module.info.name, tds, module.info.size)
.ok()?;
read_pe_image_from_file(&path).ok()?
}
};
let executable_ranges = executable_ranges(&image);
self.modules.insert(
key,
CachedModule {
info: module.info.clone(),
image: Arc::new(image),
executable_ranges,
},
);
Some(())
}
fn load_on_disk_image(&self, image: &PeImage, info: &ModuleInfo) -> Option<PeImage> {
let view = PeView::from_bytes(image.as_slice()).ok()?;
let time_date_stamp = view.file_header().TimeDateStamp;
let size_of_image = view.optional_header().SizeOfImage;
let path = self
.symbols
.ensure_module_image_on_disk(&info.name, time_date_stamp, size_of_image)
.ok()?;
read_pe_image_from_file(&path).ok()
}
}
fn executable_ranges(image: &PeImage) -> Vec<(u32, u32)> {
let Ok(view) = PeView::from_bytes(image.as_slice()) else {
return Vec::new();
};
view.section_headers()
.iter()
.filter_map(|section| {
if section.Characteristics & IMAGE_SCN_MEM_EXECUTE == 0 {
return None;
}
let size = section.VirtualSize.max(section.SizeOfRawData);
if size == 0 {
return None;
}
Some((
section.VirtualAddress,
section.VirtualAddress.saturating_add(size),
))
})
.collect()
}
enum Resolve {
Leaf,
Function { unwind_data: u32, begin: u32 },
Holed,
}
fn resolve_function(image: &PeImage, base_address: u64, rip: u64) -> Resolve {
let Ok(view) = PeView::from_bytes(image.as_slice()) else {
return Resolve::Leaf;
};
let Ok(exception) = view.exception() else {
return Resolve::Leaf;
};
let rva = (rip - base_address) as u32;
match lookup_runtime_function(exception.image(), rva) {
Some(function) if image.is_present(function.UnwindData as usize, 4) => Resolve::Function {
unwind_data: function.UnwindData,
begin: function.BeginAddress,
},
Some(_) => Resolve::Holed,
None => {
let pdata_present = view
.data_directory()
.get(IMAGE_DIRECTORY_ENTRY_EXCEPTION)
.map(|d| {
d.Size == 0 || image.is_present(d.VirtualAddress as usize, d.Size as usize)
})
.unwrap_or(true);
if pdata_present {
Resolve::Leaf
} else {
Resolve::Holed
}
}
}
}
fn lookup_runtime_function(functions: &[RUNTIME_FUNCTION], rva: u32) -> Option<&RUNTIME_FUNCTION> {
functions
.binary_search_by(|rf| {
if rva < rf.BeginAddress {
Ordering::Greater
} else if rva >= rf.EndAddress {
Ordering::Less
} else {
Ordering::Equal
}
})
.ok()
.map(|index| &functions[index])
}
fn parse_unwind_info(image: &PeImage, unwind_rva: u32) -> Option<ParsedUnwindInfo> {
let offset = unwind_rva as usize;
let header = image.present_slice(offset, 4)?;
let version_flags = header[0];
let count_of_codes = header[2] as usize;
let frame_register_offset = header[3];
let codes_offset = offset + 4;
let codes_bytes = image.present_slice(codes_offset, count_of_codes.checked_mul(2)?)?;
let aligned_code_count = (count_of_codes + 1) & !1;
let tail_offset = offset + 4 + aligned_code_count * 2;
let chained_unwind_data = if (version_flags >> 3) & UNW_FLAG_CHAININFO != 0 {
let tail = image.present_slice(tail_offset, 12)?;
Some(u32::from_le_bytes([tail[8], tail[9], tail[10], tail[11]]))
} else {
None
};
let mut codes = Vec::with_capacity(count_of_codes);
for raw in codes_bytes.chunks_exact(2) {
codes.push(UnwindCodeSlot {
code_offset: raw[0],
unwind_op: raw[1] & 0x0f,
op_info: raw[1] >> 4,
raw_op_info: raw[1],
});
}
Some(ParsedUnwindInfo {
size_of_prolog: header[1],
frame_register: frame_register_offset & 0x0f,
frame_offset: frame_register_offset >> 4,
codes,
chained_unwind_data,
})
}
fn frame_base(context: &RegisterContext, unwind_info: &ParsedUnwindInfo) -> Option<u64> {
if unwind_info.frame_register == 0 {
return Some(context.rsp);
}
let frame_register = context.get(unwind_info.frame_register)?;
frame_register.checked_sub(u64::from(unwind_info.frame_offset) * 16)
}
fn unwind_slot_count(unwind_op: u8, op_info: u8) -> usize {
match unwind_op {
UWOP_PUSH_NONVOL | UWOP_ALLOC_SMALL | UWOP_SET_FPREG | UWOP_PUSH_MACHFRAME
| UWOP_EPILOG => 1,
UWOP_ALLOC_LARGE => {
if op_info == 0 {
2
} else {
3
}
}
UWOP_SAVE_NONVOL | UWOP_SAVE_XMM128 => 2,
UWOP_SAVE_NONVOL_FAR | UWOP_SAVE_XMM128_FAR | UWOP_SPARE_CODE => 3,
_ => 0,
}
}
fn slot_u16(codes: &[UnwindCodeSlot], index: usize) -> Option<u16> {
let slot = codes.get(index)?;
Some(u16::from_le_bytes([slot.code_offset, slot.raw_op_info]))
}
#[cfg(test)]
mod tests {
use super::{
FrameSource, ParsedUnwindInfo, PeImage, RUNTIME_FUNCTION, RegisterContext, StackFrame,
StackTrace, UnwindCodeSlot, frame_base, lookup_arm64_runtime_function,
lookup_runtime_function, parse_unwind_info, record_stack_frame, slot_u16,
unwind_slot_count,
};
use crate::target::SavedThreadRegisters;
#[test]
fn lookup_runtime_function_resolves_across_a_large_sorted_table() {
let funcs: Vec<RUNTIME_FUNCTION> = (0..64u32)
.map(|i| RUNTIME_FUNCTION {
BeginAddress: i * 0x100,
EndAddress: i * 0x100 + 0x40,
UnwindData: i,
})
.collect();
assert_eq!(
lookup_runtime_function(&funcs, 0x0).unwrap().BeginAddress,
0x0
);
assert_eq!(
lookup_runtime_function(&funcs, 0x310).unwrap().BeginAddress,
0x300
);
assert_eq!(
lookup_runtime_function(&funcs, 0x3f00)
.unwrap()
.BeginAddress,
0x3f00
);
assert!(lookup_runtime_function(&funcs, 0x350).is_none());
assert!(lookup_runtime_function(&funcs, 0x10000).is_none());
}
#[test]
fn lookup_arm64_runtime_function_decodes_packed_and_xdata_lengths() {
let mut image_bytes = vec![0u8; 0x2100];
image_bytes[0x2000..0x2004].copy_from_slice(&(0x80u32 / 4).to_le_bytes());
let image = PeImage::complete(image_bytes);
let mut pdata = Vec::new();
pdata.extend_from_slice(&0x1000u32.to_le_bytes());
pdata.extend_from_slice(&(((0x40u32 / 4) << 2) | 1).to_le_bytes());
pdata.extend_from_slice(&0x1100u32.to_le_bytes());
pdata.extend_from_slice(&0x2000u32.to_le_bytes());
assert_eq!(
lookup_arm64_runtime_function(&image, &pdata, 0x103c),
Some((0x1000, 0x1040))
);
assert!(lookup_arm64_runtime_function(&image, &pdata, 0x1040).is_none());
assert_eq!(
lookup_arm64_runtime_function(&image, &pdata, 0x117c),
Some((0x1100, 0x1180))
);
assert!(lookup_arm64_runtime_function(&image, &pdata, 0x1180).is_none());
}
#[test]
fn parse_unwind_info_reads_chained_parent() {
let blob = [
0x21, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, ];
let info =
parse_unwind_info(&PeImage::complete(blob.to_vec()), 0).expect("unwind info parses");
assert_eq!(info.chained_unwind_data, Some(0x2000));
}
#[test]
fn parse_unwind_info_without_chain_flag_has_no_parent() {
let blob = [0x01, 0x00, 0x00, 0x00];
let info =
parse_unwind_info(&PeImage::complete(blob.to_vec()), 0).expect("unwind info parses");
assert_eq!(info.chained_unwind_data, None);
}
#[test]
fn slot_count_matches_opcode_encoding() {
assert_eq!(unwind_slot_count(0, 0), 1);
assert_eq!(unwind_slot_count(1, 0), 2);
assert_eq!(unwind_slot_count(1, 1), 3);
assert_eq!(unwind_slot_count(4, 0), 2);
assert_eq!(unwind_slot_count(5, 0), 3);
assert_eq!(unwind_slot_count(6, 0), 1); assert_eq!(unwind_slot_count(7, 0), 3); }
#[test]
fn slot_u16_reads_little_endian_slot_data() {
let codes = vec![
UnwindCodeSlot {
code_offset: 0x34,
unwind_op: 0,
op_info: 0,
raw_op_info: 0x12,
},
UnwindCodeSlot {
code_offset: 0x78,
unwind_op: 0,
op_info: 0,
raw_op_info: 0x56,
},
];
assert_eq!(slot_u16(&codes, 0), Some(0x1234));
assert_eq!(slot_u16(&codes, 1), Some(0x5678));
}
#[test]
fn frame_base_uses_frame_register_when_present() {
let mut regs = [None; 16];
regs[5] = Some(0x2000);
let context = RegisterContext {
rip: 0,
rsp: 0x1800,
regs,
};
let unwind = ParsedUnwindInfo {
size_of_prolog: 0,
frame_register: 5,
frame_offset: 2,
codes: Vec::new(),
chained_unwind_data: None,
};
assert_eq!(frame_base(&context, &unwind), Some(0x1fe0));
}
#[test]
fn record_stack_frame_counts_truncated_frames() {
let mut stacktrace = StackTrace::default();
for ip in [0x1000, 0x2000, 0x3000] {
record_stack_frame(
&mut stacktrace,
2,
StackFrame {
sp: 0,
ip,
symbol: String::new(),
source: FrameSource::Current,
source_location: None,
},
);
}
assert_eq!(stacktrace.frames.len(), 2);
assert_eq!(stacktrace.truncated, 1);
}
#[test]
fn saved_register_context_preserves_missing_values() {
let registers = SavedThreadRegisters {
rip: Some(0xffff_f800_1000),
rsp: Some(0xffff_a000_2000),
rbp: Some(0xffff_a000_2100),
..SavedThreadRegisters::default()
};
let context = RegisterContext::from_saved(®isters).unwrap();
assert_eq!(context.rip, 0xffff_f800_1000);
assert_eq!(context.rsp, 0xffff_a000_2000);
assert_eq!(context.get(5), Some(0xffff_a000_2100));
assert_eq!(context.get(3), None);
let missing_rsp = SavedThreadRegisters {
rip: Some(0xffff_f800_1000),
..SavedThreadRegisters::default()
};
assert!(RegisterContext::from_saved(&missing_rsp).is_none());
}
}