use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use iced_x86::{Code, Decoder, DecoderOptions, Instruction, Mnemonic};
use single_instance::SingleInstance;
use std::sync::Arc;
use crate::backend::MemoryOps;
use crate::bugchecks::{CURRENT_KERNEL_RELOAD_WINDOW, looks_like_kernel_pointer};
use crate::dbg_backend::{
BackendCapability, BugcheckInfo, DebugBackend, DebugCapability, DebugOutputPage, StopEvent,
};
use crate::disasm::{DisasmRow, decode_rows, disasm_formatter};
use crate::error::{Error, Result};
use crate::expr::Expr;
use crate::gdb::breakpoints::Breakpoint;
use crate::gdb::{BreakpointHitResult, BreakpointManager, RegisterMap};
use crate::kd::trace_enabled;
use crate::memory::AddressSpace;
use crate::phys::PhysMem;
use crate::target::{ReloadReport, Target, ThreadInfo};
use crate::types::VirtAddr;
use crate::unwind::{
StackTrace, build_stacktrace, preferred_code_dtb, resolve_thread_trace_context,
};
macro_rules! reload_trace {
($($arg:tt)*) => {
if trace_enabled() {
eprintln!("reload: {}", format_args!($($arg)*));
}
};
}
#[derive(Debug, Clone)]
pub enum ContinueOutcome {
Breakpoint {
id: u32,
address: u64,
symbol: Option<String>,
temporary: bool,
rip: u64,
},
Bugcheck {
rip: Option<u64>,
info: Option<BugcheckInfo>,
},
Stopped {
rip: u64,
exception_code: Option<u32>,
},
Step { rip: u64 },
TargetReloaded {
kernel_base: Option<u64>,
coherent: bool,
},
Running,
Halted { rip: u64 },
}
#[derive(Debug, Clone)]
pub struct RunStatus {
pub running: bool,
pub current_thread: String,
pub rip: Option<u64>,
pub symbol: Option<String>,
pub process: Option<(u64, String, u64)>,
pub coherent: bool,
pub kernel_base: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReloadDisposition {
Ordinary,
Reloaded { coherent: bool },
ReloadCompleted,
PendingRediscovery,
ResumePastAssist,
}
#[derive(Debug, Clone, Copy)]
pub enum StepKind {
Single,
RunTo(VirtAddr),
}
#[derive(Debug, Clone)]
pub enum BreakpointStopAction {
Hit {
id: u32,
address: u64,
symbol: Option<String>,
temporary: bool,
},
Resumed,
NotBreakpoint,
}
#[derive(Debug, Clone)]
pub struct VcpuInfo {
pub id: String,
pub rip: Option<u64>,
pub context: String,
pub symbol: Option<String>,
pub error: Option<String>,
}
const DTB_PAGE_MASK: u64 = 0x000F_FFFF_FFFF_F000;
const CONTINUE_POLL_INTERVAL: Duration = Duration::from_millis(200);
const SERVICE_IDLE_BUDGET: Duration = Duration::from_millis(5);
const STATUS_BREAKPOINT: u32 = 0x8000_0003;
const STATUS_SINGLE_STEP: u32 = 0x8000_0004;
pub struct Session {
id: usize,
pub target: Target,
pub backend: Box<dyn DebugBackend>,
pub breakpoints: BreakpointManager,
pub register_map: RegisterMap,
pub current_thread: String,
pub reload_module_list_pending: bool,
reload_surface_pending: bool,
parked_stop: Option<ContinueOutcome>,
_instance_guard: Option<InstanceGuard>,
}
impl Session {
pub fn connect<F>(
phys: Arc<PhysMem>,
target: Option<&str>,
make_backend: F,
) -> Result<Self>
where
F: FnOnce() -> Result<Box<dyn DebugBackend>>,
{
let guard = target.map(acquire_instance_guard).transpose()?;
let backend = make_backend()?;
let mut session = Self::new(phys, backend)?;
session._instance_guard = guard;
Ok(session)
}
pub fn new(phys: Arc<PhysMem>, mut backend: Box<dyn DebugBackend>) -> Result<Self> {
let target = Target::with_phys(phys)?;
backend.initialize_from_target(&target);
let register_map = backend.register_map().clone();
let has_register_context = backend
.capabilities()
.iter()
.any(|c| c.capability == DebugCapability::ReadRegisters && c.supported);
let current_thread = if has_register_context {
backend
.stopped_thread_id()
.unwrap_or_else(|_| "1".to_string())
} else {
"1".to_string()
};
static NEXT_SESSION_ID: AtomicUsize = AtomicUsize::new(1);
Ok(Self {
id: NEXT_SESSION_ID.fetch_add(1, Ordering::Relaxed),
target,
backend,
breakpoints: BreakpointManager::new(),
register_map,
current_thread,
reload_module_list_pending: false,
reload_surface_pending: false,
parked_stop: None,
_instance_guard: None,
})
}
pub fn step(&mut self) -> Result<()> {
self.parked_stop = None;
self.backend.set_current_thread(&self.current_thread)?;
if !step_over_current_breakpoint(
self.backend.as_mut(),
&self.register_map,
&self.target,
&mut self.breakpoints,
)? {
step_one_and_clear_tf(self.backend.as_mut(), &self.register_map)?;
}
let _ = self
.breakpoints
.refresh_enabled(self.backend.as_mut(), &self.target);
if let Ok(tid) = self.backend.stopped_thread_id() {
self.current_thread = tid;
}
self.refresh_context_for_current_thread();
Ok(())
}
pub fn set_current_thread(&mut self, id: &str) -> Result<()> {
self.backend.set_current_thread(id)?;
self.current_thread = id.to_string();
self.refresh_context_for_current_thread();
Ok(())
}
pub fn interrupt(&mut self) -> Result<StopEvent> {
let event = self.backend.interrupt()?;
if event.target_reloaded {
let _ = self.reload_with_hint(event.target_kernel_base_hint);
self.reload_surface_pending = true;
}
set_current_thread_from_stop(self.backend.as_mut(), &event, &mut self.current_thread);
self.refresh_context_for_current_thread();
Ok(event)
}
fn refresh_context_for_current_thread(&mut self) {
if self.backend.is_running() {
return;
}
let regs = self
.backend
.set_current_thread(&self.current_thread)
.and_then(|_| self.backend.read_registers());
let Ok(regs) = regs else {
self.target.registers = None;
self.target.clear_context_dtb_override();
return;
};
self.target.registers = Some(self.register_map.to_hashmap(®s));
match self.register_map.read_u64("cr3", ®s) {
Ok(cr3) if cr3 != 0 => self.target.set_context_dtb_override(cr3),
_ => self.target.clear_context_dtb_override(),
}
}
pub fn current_instruction(&mut self) -> Result<Instruction> {
self.backend.set_current_thread(&self.current_thread)?;
let regs = self.backend.read_registers()?;
let rip = self.register_map.read_u64("rip", ®s)?;
let cr3 = self.register_map.read_u64("cr3", ®s).unwrap_or(0);
let trace = resolve_thread_trace_context(&self.target, cr3);
let code_dtb = preferred_code_dtb(&trace, rip);
let memory = AddressSpace::new(&self.target.phys, code_dtb);
let mut bytes = [0u8; 16];
memory.read_bytes(VirtAddr(rip), &mut bytes)?;
self.breakpoints
.mask_breakpoint_bytes(VirtAddr(rip), &mut bytes, trace.active_dtb);
let mut decoder = Decoder::with_ip(64, &bytes, rip, DecoderOptions::NONE);
let instruction = decoder.decode();
if instruction.code() == Code::INVALID {
return Err(Error::DebugInfo(format!(
"failed to decode instruction at {rip:#x}"
)));
}
Ok(instruction)
}
pub fn step_over_target(&mut self) -> Result<StepKind> {
let instruction = self.current_instruction()?;
if instruction.mnemonic() == Mnemonic::Call {
Ok(StepKind::RunTo(VirtAddr(instruction.next_ip())))
} else {
Ok(StepKind::Single)
}
}
pub fn step_out_target(&mut self) -> Result<VirtAddr> {
self.backend.set_current_thread(&self.current_thread)?;
let regs = self.backend.read_registers()?;
let trace = build_stacktrace(&self.target, &self.register_map, ®s, 4);
let caller = trace
.frames
.get(1)
.ok_or_else(|| Error::DebugInfo("could not find caller return address".to_string()))?;
if caller.ip == 0 {
return Err(Error::DebugInfo(
"caller return address is null".to_string(),
));
}
Ok(VirtAddr(caller.ip))
}
pub fn run_to(&mut self, address: VirtAddr, cancel: &AtomicBool) -> Result<ContinueOutcome> {
if self
.breakpoints
.enabled_breakpoint_id_for_current_context(&self.target, address)
.is_some()
{
return self.continue_until_break(None, cancel);
}
let temp_id =
self.breakpoints
.add_temporary_code(self.backend.as_mut(), &self.target, address)?;
let outcome = self.continue_until_break(None, cancel);
if self.backend.is_running() {
let _ = self.backend.interrupt();
}
let _ = self
.breakpoints
.remove(self.backend.as_mut(), &self.target, temp_id);
match outcome? {
ContinueOutcome::Breakpoint { id, rip, .. } if id == temp_id => {
Ok(ContinueOutcome::Step { rip })
}
other => Ok(other),
}
}
pub fn step_over(&mut self, cancel: &AtomicBool) -> Result<ContinueOutcome> {
match self.step_over_target()? {
StepKind::Single => {
self.step()?;
Ok(ContinueOutcome::Step {
rip: self.current_rip(),
})
}
StepKind::RunTo(addr) => self.run_to(addr, cancel),
}
}
pub fn step_out(&mut self, cancel: &AtomicBool) -> Result<ContinueOutcome> {
let target = self.step_out_target()?;
self.run_to(target, cancel)
}
fn current_rip(&mut self) -> u64 {
self.backend
.read_registers()
.ok()
.and_then(|r| self.register_map.read_u64("rip", &r).ok())
.unwrap_or(0)
}
pub fn write_register(&mut self, name: &str, value: u64) -> Result<()> {
let mut regs = self.backend.read_registers()?;
self.register_map.write_u64(name, &mut regs, value)?;
self.backend.write_registers(®s)
}
pub fn capabilities(&self) -> Vec<BackendCapability> {
self.backend.capabilities()
}
pub fn read_debug_output(&self, since_seq: u64) -> DebugOutputPage {
self.backend.read_debug_output(since_seq)
}
pub fn kernel_coherent(&self) -> bool {
!self.reload_module_list_pending && self.target.current_kernel_mapping_is_valid()
}
pub fn settle_pending_stop(&mut self) -> Result<()> {
if !self.backend.has_pending_stop() {
return Ok(());
}
let event = self.backend.wait_for_stop()?;
set_current_thread_from_stop(self.backend.as_mut(), &event, &mut self.current_thread);
if !event.target_reloaded && !event.is_bugcheck {
let stray = stop_is_stray_single_step(&event, &self.breakpoints);
if stray || stop_is_assisted_refresh_breakin(&self.target, &self.breakpoints, &event) {
if stray {
let _ = clear_trap_flag(self.backend.as_mut(), &self.register_map);
}
self.try_finish_rediscovery_from_memory();
self.backend.continue_execution()?;
return Ok(());
}
if self.breakpoints.has_enabled_breakpoints() {
rewind_threads_off_breakpoints(
self.backend.as_mut(),
&self.register_map,
&self.breakpoints,
&self.current_thread,
);
if let Ok(regs) = self.backend.read_registers() {
let rip = self.register_map.read_u64("rip", ®s).unwrap_or(0);
let cr3 = self.register_map.read_u64("cr3", ®s).unwrap_or(0);
self.target.registers = Some(self.register_map.to_hashmap(®s));
if cr3 != 0 {
self.target.set_context_dtb_override(cr3);
}
if matches!(
self.resolve_breakpoint_stop(rip, cr3),
Ok(BreakpointStopAction::Resumed)
) {
return Ok(());
}
}
}
}
if event.target_reloaded {
let _ = self.reload_with_hint(event.target_kernel_base_hint);
self.reload_surface_pending = true;
}
self.refresh_context_for_current_thread();
Ok(())
}
pub fn service_idle(&mut self) {
if self.parked_stop.is_some() || !self.backend.has_pending_stop() {
return;
}
let never_cancel = AtomicBool::new(false);
match self.wait_for_stop_bounded(Some(SERVICE_IDLE_BUDGET), &never_cancel) {
Ok(ContinueOutcome::Running) | Err(_) => {}
Ok(ContinueOutcome::TargetReloaded { .. }) => {
self.reload_surface_pending = true;
}
Ok(outcome) => {
self.parked_stop = Some(outcome);
}
}
}
pub fn take_parked_stop(&mut self) -> Option<ContinueOutcome> {
self.parked_stop.take()
}
pub fn run_status(&mut self) -> RunStatus {
let _ = self.settle_pending_stop();
self.try_finish_rediscovery_from_memory();
self.clear_deferred_reload_surface();
let pending_stop = self.backend.has_pending_stop();
let running = self.backend.is_running() && !pending_stop;
let (rip, symbol) = if running || pending_stop {
(None, None)
} else {
let _ = self.backend.set_current_thread(&self.current_thread);
let rip = self
.backend
.read_registers()
.ok()
.and_then(|regs| self.register_map.read_u64("rip", ®s).ok());
let symbol = rip.and_then(|r| self.target.closest_symbol_current_context(VirtAddr(r)));
(rip, symbol)
};
let process = self
.target
.current_process_info
.as_ref()
.map(|p| (p.pid, p.name.clone(), p.eprocess_va.0));
RunStatus {
running,
current_thread: self.current_thread.clone(),
rip,
symbol,
process,
coherent: self.kernel_coherent(),
kernel_base: self.target.guest.ntoskrnl.base_address.0,
}
}
pub fn add_breakpoint(&mut self, addr: VirtAddr) -> Result<u32> {
self.add_breakpoint_with_condition(addr, None)
}
pub fn add_breakpoint_with_condition(
&mut self,
addr: VirtAddr,
condition: Option<String>,
) -> Result<u32> {
self.add_breakpoint_with_symbol_condition(addr, None, condition)
}
pub fn add_breakpoint_with_symbol_condition(
&mut self,
addr: VirtAddr,
symbol: Option<String>,
condition: Option<String>,
) -> Result<u32> {
self.breakpoints
.add(self.backend.as_mut(), &self.target, addr, symbol, condition)
}
pub fn remove_breakpoint(&mut self, id: u32) -> Result<()> {
self.breakpoints
.remove(self.backend.as_mut(), &self.target, id)
}
pub fn enable_breakpoint(&mut self, id: u32) -> Result<()> {
self.breakpoints
.enable(self.backend.as_mut(), &self.target, id)
}
pub fn disable_breakpoint(&mut self, id: u32) -> Result<()> {
self.breakpoints
.disable(self.backend.as_mut(), &self.target, id)
}
pub fn list_breakpoints(&self) -> Vec<&Breakpoint> {
self.breakpoints.list()
}
pub fn id(&self) -> usize {
self.id
}
pub fn breakpoint(&self, id: u32) -> Option<&Breakpoint> {
self.breakpoints.list().into_iter().find(|bp| bp.id == id)
}
pub fn vcpus(&mut self) -> Result<Vec<VcpuInfo>> {
let original = self.backend.stopped_thread_id()?;
let threads = self.backend.thread_list()?;
let processes = self.target.guest.enumerate_processes().unwrap_or_default();
let kernel_dtb_masked = self.target.guest.ntoskrnl.dtb() & DTB_PAGE_MASK;
let mut out = Vec::with_capacity(threads.len());
for thread in &threads {
let regs = self
.backend
.set_current_thread(thread)
.and_then(|_| self.backend.read_registers());
let regs = match regs {
Ok(regs) => regs,
Err(e) => {
out.push(VcpuInfo {
id: thread.clone(),
rip: None,
context: String::new(),
symbol: None,
error: Some(e.to_string()),
});
continue;
}
};
let (Ok(rip), Ok(cr3)) = (
self.register_map.read_u64("rip", ®s),
self.register_map.read_u64("cr3", ®s),
) else {
out.push(VcpuInfo {
id: thread.clone(),
rip: None,
context: String::new(),
symbol: None,
error: None,
});
continue;
};
let cr3_masked = cr3 & DTB_PAGE_MASK;
let (context, symbol) = if cr3_masked == kernel_dtb_masked {
let sym = self
.target
.guest
.ntoskrnl
.closest_symbol(VirtAddr(rip))
.map(|(s, o)| format!("{s}+{o:#x}"))
.ok();
("kernel".to_string(), sym)
} else {
match processes
.iter()
.find(|p| (p.dtb & DTB_PAGE_MASK) == cr3_masked)
{
Some(proc) => {
let sym = self
.target
.symbols
.format_closest_symbol_for_address(proc.dtb, VirtAddr(rip));
(proc.name.clone(), sym)
}
None => ("unknown".to_string(), None),
}
};
out.push(VcpuInfo {
id: thread.clone(),
rip: Some(rip),
context,
symbol,
error: None,
});
}
let _ = self.backend.set_current_thread(&original);
Ok(out)
}
pub fn active_thread_map(&mut self) -> HashMap<u64, (String, ThreadInfo)> {
let Ok(original) = self.backend.stopped_thread_id() else {
return HashMap::new();
};
let Ok(vcpus) = self.backend.thread_list() else {
return HashMap::new();
};
let mut active = HashMap::new();
for vcpu in &vcpus {
if self.backend.set_current_thread(vcpu).is_err() {
continue;
}
let Some(processor) = processor_index_from_backend_thread_id(vcpu) else {
continue;
};
if let Ok(thread) = self.target.current_windows_thread_for_processor(processor) {
active.insert(thread.ethread.0, (vcpu.clone(), thread));
}
}
let _ = self.backend.set_current_thread(&original);
active
}
pub fn windows_threads(&mut self) -> Result<(Vec<ThreadInfo>, HashMap<u64, String>)> {
let active = self.active_thread_map();
let mut threads = self.target.enumerate_threads()?;
for (_, thread) in active.values() {
if !threads.iter().any(|known| known.ethread == thread.ethread) {
threads.push(thread.clone());
}
}
threads.sort_by_key(|thread| (thread.pid.unwrap_or(u64::MAX), thread.tid));
let active_vcpus = active
.into_iter()
.map(|(ethread, (vcpu, _))| (ethread, vcpu))
.collect();
Ok((threads, active_vcpus))
}
pub fn disassemble(&self, addr: VirtAddr, count: usize) -> Result<Vec<DisasmRow>> {
let process = self.target.current_process();
let dtb = process.dtb();
let mut buf = vec![0u8; count * 16];
process.memory().read_bytes(addr, &mut buf)?;
self.breakpoints.mask_breakpoint_bytes(addr, &mut buf, dtb);
let symbols = &self.target.symbols;
let resolve = |target: u64| {
symbols
.format_closest_symbol_for_address(dtb, VirtAddr(target))
.unwrap_or_default()
};
let mut formatter = disasm_formatter();
Ok(decode_rows(
&buf,
addr.0,
Some(count),
&mut formatter,
resolve,
))
}
pub fn backtrace(&mut self, limit: usize) -> Result<StackTrace> {
self.backend.set_current_thread(&self.current_thread)?;
let regs = self.backend.read_registers()?;
Ok(build_stacktrace(
&self.target,
&self.register_map,
®s,
limit,
))
}
pub fn remove_all_breakpoints(&mut self) {
let ids: Vec<u32> = self.breakpoints.list().iter().map(|b| b.id).collect();
for id in ids {
let _ = self
.breakpoints
.remove(self.backend.as_mut(), &self.target, id);
}
}
pub fn cleanup_for_exit(&mut self) -> Result<()> {
if self.backend.is_running() {
let _ = self.backend.interrupt();
}
self.remove_all_breakpoints();
self.backend.prepare_for_exit(true)
}
pub fn resume(&mut self) -> Result<()> {
self.parked_stop = None;
self.try_finish_rediscovery_from_memory();
if self.breakpoints.has_enabled_breakpoints() {
self.backend.set_current_thread(&self.current_thread)?;
step_over_current_breakpoint(
self.backend.as_mut(),
&self.register_map,
&self.target,
&mut self.breakpoints,
)?;
}
self.breakpoints
.refresh_enabled(self.backend.as_mut(), &self.target)?;
self.backend.continue_execution()?;
self.target.registers = None;
self.target.clear_context_dtb_override();
self.target.clear_current_windows_thread_context();
Ok(())
}
pub fn resolve_breakpoint_stop(&mut self, rip: u64, cr3: u64) -> Result<BreakpointStopAction> {
match self.breakpoints.check_breakpoint_hit(rip, cr3) {
BreakpointHitResult::Hit(bp) => {
if let Some(condition) = &bp.condition
&& !eval_breakpoint_condition(condition, &self.target).unwrap_or(true)
{
step_over_current_breakpoint(
self.backend.as_mut(),
&self.register_map,
&self.target,
&mut self.breakpoints,
)?;
self.backend.continue_execution()?;
return Ok(BreakpointStopAction::Resumed);
}
let _ = self
.breakpoints
.refresh_enabled(self.backend.as_mut(), &self.target);
Ok(BreakpointStopAction::Hit {
id: bp.id,
address: bp.address.0,
symbol: bp.symbol.clone(),
temporary: bp.temporary,
})
}
BreakpointHitResult::NotBreakpoint => {
if self.breakpoints.breakpoint_id_at_address(rip).is_some() {
step_over_current_breakpoint(
self.backend.as_mut(),
&self.register_map,
&self.target,
&mut self.breakpoints,
)?;
self.backend.continue_execution()?;
return Ok(BreakpointStopAction::Resumed);
}
Ok(BreakpointStopAction::NotBreakpoint)
}
}
}
pub fn continue_until_break(
&mut self,
timeout: Option<Duration>,
cancel: &AtomicBool,
) -> Result<ContinueOutcome> {
if !self.backend.is_running() {
self.resume()?;
}
self.wait_for_stop_bounded(timeout, cancel)
}
pub fn wait_for_stop_bounded(
&mut self,
timeout: Option<Duration>,
cancel: &AtomicBool,
) -> Result<ContinueOutcome> {
let deadline = timeout.map(|t| Instant::now() + t);
loop {
if cancel.load(Ordering::Relaxed) {
return Ok(ContinueOutcome::Running);
}
let poll = match deadline {
Some(dl) => {
let remaining = dl.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Ok(ContinueOutcome::Running);
}
remaining.min(CONTINUE_POLL_INTERVAL)
}
None => CONTINUE_POLL_INTERVAL,
};
let mut event = match self.backend.try_wait_for_stop(poll)? {
Some(event) => event,
None => {
if !self.backend.is_running() {
if self.reload_surface_pending {
self.reload_surface_pending = false;
return Ok(ContinueOutcome::TargetReloaded {
kernel_base: Some(self.target.guest.ntoskrnl.base_address.0),
coherent: self.kernel_coherent(),
});
}
let rip = self
.backend
.read_registers()
.ok()
.and_then(|regs| self.register_map.read_u64("rip", ®s).ok())
.unwrap_or(0);
return Ok(ContinueOutcome::Halted { rip });
}
continue;
}
};
set_current_thread_from_stop(self.backend.as_mut(), &event, &mut self.current_thread);
if event.is_bugcheck && !event.target_reloaded {
self.target.registers = None;
return Ok(ContinueOutcome::Bugcheck {
rip: event.program_counter,
info: event.bugcheck.clone(),
});
}
match self.classify_reload_stop(&mut event) {
disposition @ (ReloadDisposition::Reloaded { .. }
| ReloadDisposition::ReloadCompleted) => {
let coherent =
!matches!(disposition, ReloadDisposition::Reloaded { coherent: false });
reload_trace!(
"continue: SURFACE target_reloaded base={:#x} coherent={}",
self.target.guest.ntoskrnl.base_address.0,
coherent,
);
self.refresh_context_for_current_thread();
return Ok(ContinueOutcome::TargetReloaded {
kernel_base: Some(self.target.guest.ntoskrnl.base_address.0),
coherent,
});
}
ReloadDisposition::PendingRediscovery | ReloadDisposition::ResumePastAssist => {
reload_trace!("continue: absorb -> resume + keep waiting");
self.backend.continue_execution()?;
continue;
}
ReloadDisposition::Ordinary => {}
}
if stop_is_stray_single_step(&event, &self.breakpoints) {
let _ = clear_trap_flag(self.backend.as_mut(), &self.register_map);
self.backend.continue_execution()?;
continue;
}
if self.breakpoints.has_enabled_breakpoints() {
rewind_threads_off_breakpoints(
self.backend.as_mut(),
&self.register_map,
&self.breakpoints,
&self.current_thread,
);
}
let regs = self.backend.read_registers()?;
self.target.registers = Some(self.register_map.to_hashmap(®s));
let rip = self.register_map.read_u64("rip", ®s).unwrap_or(0);
let cr3 = self.register_map.read_u64("cr3", ®s).unwrap_or(0);
if cr3 != 0 {
self.target.set_context_dtb_override(cr3);
}
match self.resolve_breakpoint_stop(rip, cr3)? {
BreakpointStopAction::Hit {
id,
address,
symbol,
temporary,
} => {
return Ok(ContinueOutcome::Breakpoint {
id,
address,
symbol,
temporary,
rip,
});
}
BreakpointStopAction::Resumed => continue,
BreakpointStopAction::NotBreakpoint => {
return Ok(ContinueOutcome::Stopped {
rip,
exception_code: event.exception_code,
});
}
}
}
}
pub fn wait_for_stop(&mut self) -> Result<StopEvent> {
let event = self.backend.wait_for_stop()?;
if event.target_reloaded {
let _ = self.reload_with_hint(event.target_kernel_base_hint);
}
set_current_thread_from_stop(self.backend.as_mut(), &event, &mut self.current_thread);
self.refresh_context_for_current_thread();
Ok(event)
}
pub fn reload_with_hint(&mut self, hint: Option<VirtAddr>) -> Result<()> {
let outcome = perform_target_reload(
self.backend.as_mut(),
&mut self.target,
&mut self.breakpoints,
hint,
);
self.reload_module_list_pending = !outcome
.report
.as_ref()
.map(reload_report_has_loaded_module_list)
.unwrap_or(false);
outcome.report.map(|_| ())
}
pub fn reload(&mut self) -> Result<()> {
self.reload_with_hint(None)
}
pub fn try_complete_pending_reload(&mut self) -> bool {
if !self.reload_module_list_pending {
return false;
}
let startup = match self.target.startup_message_data() {
Ok(startup) => startup,
Err(e) => {
reload_trace!("try_complete: startup read failed: {e}");
return false;
}
};
reload_trace!("try_complete: psmods={:#x}", startup.loaded_module_list.0);
if startup.loaded_module_list.is_zero() {
return false;
}
let _ = self.target.refresh_kernel_module_symbols();
self.backend.note_target_rediscovery_complete();
self.reload_module_list_pending = false;
true
}
pub fn try_finish_rediscovery_from_memory(&mut self) {
if !self.reload_surface_pending {
let _ = self.try_complete_pending_reload();
}
}
pub fn clear_deferred_reload_surface(&mut self) {
if self.target.current_kernel_mapping_is_valid() {
self.reload_surface_pending = false;
}
}
pub fn classify_reload_stop(&mut self, event: &mut StopEvent) -> ReloadDisposition {
reload_trace!(
"classify: pc={} exc={} assisted={} reloaded={} bugcheck={} pending={}",
event
.program_counter
.map_or_else(|| "none".to_string(), |p| format!("{p:#x}")),
event
.exception_code
.map_or_else(|| "none".to_string(), |c| format!("{c:#x}")),
event.assisted_breakin,
event.target_reloaded,
event.is_bugcheck,
self.reload_module_list_pending,
);
if stop_event_requires_target_reload(&self.target, event) {
event.target_reloaded = true;
let outcome = perform_target_reload(
self.backend.as_mut(),
&mut self.target,
&mut self.breakpoints,
event.target_kernel_base_hint,
);
return match outcome.report {
Ok(report) => {
let coherent = reload_report_has_loaded_module_list(&report);
self.reload_module_list_pending = !coherent;
self.reload_surface_pending = false;
reload_trace!(
"classify: reload ok hint={} new_base={:#x} psmods={} coherent={}",
outcome
.hint
.map_or_else(|| "none".to_string(), |h| format!("{:#x}", h.0)),
self.target.guest.ntoskrnl.base_address.0,
report.startup.as_ref().map_or_else(
|| "none".to_string(),
|s| format!("{:#x}", s.loaded_module_list.0),
),
coherent,
);
ReloadDisposition::Reloaded { coherent }
}
Err(ref e) => {
self.reload_module_list_pending = true;
self.reload_surface_pending = true;
reload_trace!("classify: reload err={e} -> pending_rediscovery");
ReloadDisposition::PendingRediscovery
}
};
}
if self.try_complete_pending_reload() {
if self.reload_surface_pending {
self.reload_surface_pending = false;
reload_trace!(
"classify: pending reload COMPLETED (unsurfaced) -> reload_completed"
);
return ReloadDisposition::ReloadCompleted;
}
if stop_is_assisted_refresh_breakin(&self.target, &self.breakpoints, event) {
reload_trace!("classify: pending reload COMPLETED silently -> resume_past_assist");
return ReloadDisposition::ResumePastAssist;
}
reload_trace!("classify: pending reload COMPLETED silently at a real stop");
return ReloadDisposition::Ordinary;
}
if stop_is_assisted_refresh_breakin(&self.target, &self.breakpoints, event) {
reload_trace!("classify: assisted refresh break-in -> resume_past_assist");
return ReloadDisposition::ResumePastAssist;
}
reload_trace!("classify: ordinary");
ReloadDisposition::Ordinary
}
}
struct InstanceGuard(#[allow(dead_code)] SingleInstance);
fn acquire_instance_guard(target: &str) -> Result<InstanceGuard> {
let canonical = canonicalize_target(target);
let key = format!("ntoseye-{:016x}", fnv1a_64(canonical.as_bytes()));
let instance = SingleInstance::new(&key).map_err(|err| {
Error::DebugInfo(format!("failed to create single-instance guard: {err:?}"))
})?;
if !instance.is_single() {
return Err(Error::AlreadyRunning(canonical));
}
Ok(InstanceGuard(instance))
}
fn canonicalize_target(target: &str) -> String {
if let Some(normalized) = normalize_host_port(target) {
return normalized;
}
let path = std::path::Path::new(target);
if let Ok(canon) = std::fs::canonicalize(path) {
return canon.to_string_lossy().into_owned();
}
let full = if path.is_relative() {
std::env::current_dir().unwrap_or_default().join(path)
} else {
path.to_path_buf()
};
let mut out = std::path::PathBuf::new();
for component in full.components() {
match component {
std::path::Component::RootDir => out.push("/"),
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
out.pop();
}
std::path::Component::Normal(s) => out.push(s),
_ => {}
}
}
out.to_string_lossy().into_owned()
}
fn normalize_host_port(target: &str) -> Option<String> {
let (host, port_str) = target.rsplit_once(':')?;
if host.contains('/') {
return None;
}
if host.contains(':') && !host.starts_with('[') {
return None;
}
let _port: u16 = port_str.parse().ok()?;
let host = host
.trim_start_matches('[')
.trim_end_matches(']')
.to_ascii_lowercase();
let host = match host.as_str() {
"localhost" | "ip6-localhost" | "::1" => "127.0.0.1",
other => other,
};
Some(format!("{host}:{port_str}"))
}
fn fnv1a_64(data: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf29ce484222325;
for &byte in data {
hash ^= byte as u64;
hash = hash.wrapping_mul(0x100000001b3);
}
hash
}
pub fn processor_index_from_backend_thread_id(thread_id: &str) -> Option<u16> {
let stripped = thread_id.strip_prefix("p1.")?;
let one_based = u16::from_str_radix(stripped, 16).ok()?;
one_based.checked_sub(1)
}
pub fn reload_report_has_loaded_module_list(report: &ReloadReport) -> bool {
report
.startup
.as_ref()
.is_some_and(|startup| !startup.loaded_module_list.is_zero())
}
pub struct TargetReloadOutcome {
pub report: Result<ReloadReport>,
pub hint: Option<VirtAddr>,
}
pub fn perform_target_reload(
backend: &mut dyn DebugBackend,
target: &mut Target,
breakpoints: &mut BreakpointManager,
event_hint: Option<VirtAddr>,
) -> TargetReloadOutcome {
*breakpoints = BreakpointManager::new();
let hint = event_hint.or_else(|| backend.target_kernel_base_hint().ok().flatten());
let report = target.reload_guest_with_kernel_base_hint(hint);
match &report {
Ok(_) => backend.note_target_rediscovery_complete(),
Err(_) => backend.note_target_rediscovery_pending(),
}
TargetReloadOutcome { report, hint }
}
pub fn stop_event_requires_target_reload(debugger: &Target, event: &StopEvent) -> bool {
if event.target_reloaded {
return true;
}
let Some(pc) = event.program_counter else {
return false;
};
if !looks_like_kernel_pointer(pc) {
return false;
}
if !debugger.current_kernel_mapping_is_valid() {
return true;
}
let current_dtb = debugger.guest.ntoskrnl.dtb();
if debugger
.symbols
.find_module_for_address(current_dtb, VirtAddr(pc))
.is_some()
{
return false;
}
if !event.is_bugcheck
&& pc.abs_diff(debugger.guest.ntoskrnl.base_address.0) < CURRENT_KERNEL_RELOAD_WINDOW
{
return false;
}
debugger
.rediscovered_kernel_identity_changed()
.unwrap_or(false)
}
pub fn stop_is_assisted_refresh_breakin(
debugger: &Target,
breakpoints: &BreakpointManager,
event: &StopEvent,
) -> bool {
if event.bugcheck.is_some() || event.exception_code != Some(STATUS_BREAKPOINT) {
return false;
}
if event
.program_counter
.is_some_and(|pc| breakpoints.breakpoint_id_at_address(pc).is_some())
{
return false;
}
if event.assisted_breakin {
return true;
}
let Some(pc) = event.program_counter else {
return false;
};
debugger
.closest_symbol_current_context(VirtAddr(pc))
.as_deref()
.is_some_and(|symbol| {
symbol == "nt!DbgBreakPointWithStatus"
|| symbol.starts_with("nt!DbgBreakPointWithStatus+")
})
}
pub fn stop_is_stray_single_step(event: &StopEvent, breakpoints: &BreakpointManager) -> bool {
event.exception_code == Some(STATUS_SINGLE_STEP)
&& !event.is_bugcheck
&& event
.program_counter
.is_none_or(|pc| breakpoints.breakpoint_id_at_address(pc).is_none())
}
pub fn rewind_threads_off_breakpoints(
backend: &mut dyn DebugBackend,
register_map: &RegisterMap,
breakpoints: &BreakpointManager,
restore_thread: &str,
) {
let threads = match backend.thread_list() {
Ok(t) => t,
Err(_) => return,
};
for tid in &threads {
if backend.set_current_thread(tid).is_err() {
continue;
}
let Ok(regs) = backend.read_registers() else {
continue;
};
let rip = register_map.read_u64("rip", ®s).unwrap_or(0);
let cr3 = register_map.read_u64("cr3", ®s).unwrap_or(0);
let Some(prev) = rip.checked_sub(1) else {
continue;
};
if !matches!(
breakpoints.check_breakpoint_hit(prev, cr3),
BreakpointHitResult::Hit(_)
) {
continue;
}
let mut adjusted = regs.clone();
if register_map.write_u64("rip", &mut adjusted, prev).is_err() {
continue;
}
let _ = backend.write_registers(&adjusted);
}
let _ = backend.set_current_thread(restore_thread);
}
pub fn split_condition_operator(condition: &str) -> Option<(&str, &str, &str)> {
const OPS: [&str; 6] = ["==", "!=", "<=", ">=", "<", ">"];
for op in OPS {
if let Some((left, right)) = condition.split_once(op) {
return Some((left.trim(), op, right.trim()));
}
}
None
}
pub fn eval_breakpoint_condition(condition: &str, debugger: &Target) -> Result<bool> {
if let Some((left, op, right)) = split_condition_operator(condition) {
let left = Expr::eval(left, debugger)?.0;
let right = Expr::eval(right, debugger)?.0;
return Ok(match op {
"==" => left == right,
"!=" => left != right,
"<=" => left <= right,
">=" => left >= right,
"<" => left < right,
">" => left > right,
_ => false,
});
}
Ok(Expr::eval(condition, debugger)?.0 != 0)
}
pub fn set_current_thread_from_stop(
backend: &mut dyn DebugBackend,
event: &StopEvent,
current: &mut String,
) {
let stopped_tid = event
.thread_id
.clone()
.or_else(|| backend.stopped_thread_id().ok());
if let Some(tid) = stopped_tid {
*current = tid;
let _ = backend.set_current_thread(current);
}
}
pub fn step_one_and_clear_tf(
backend: &mut dyn DebugBackend,
register_map: &RegisterMap,
) -> Result<()> {
backend.step()?;
backend.wait_for_stop()?;
clear_trap_flag(backend, register_map)
}
pub fn clear_trap_flag(backend: &mut dyn DebugBackend, register_map: &RegisterMap) -> Result<()> {
if let Ok(mut regs) = backend.read_registers()
&& let Ok(eflags) = register_map.read_u64("eflags", ®s)
{
let cleared = eflags & !(1u64 << 8);
if cleared != eflags && register_map.write_u64("eflags", &mut regs, cleared).is_ok() {
backend.write_registers(®s)?;
}
}
Ok(())
}
pub fn step_over_current_breakpoint(
backend: &mut dyn DebugBackend,
register_map: &RegisterMap,
debugger: &Target,
breakpoints: &mut BreakpointManager,
) -> Result<bool> {
let regs = backend.read_registers()?;
let rip = register_map.read_u64("rip", ®s)?;
let cr3 = register_map.read_u64("cr3", ®s)?;
let Some(bp_id) = breakpoints.breakpoint_id_at_address(rip) else {
return Ok(false);
};
if let Err(err) = breakpoints.disable(backend, debugger, bp_id) {
if matches!(err, Error::BadVirtualAddress(_)) {
breakpoints
.disable_guest_memory_patch_in_address_space(backend, debugger, bp_id, cr3)?;
} else {
return Err(err);
}
}
step_one_and_clear_tf(backend, register_map)?;
if let Err(err) = breakpoints.enable(backend, debugger, bp_id) {
if matches!(err, Error::BadVirtualAddress(_)) {
breakpoints.discard(bp_id)?;
} else {
return Err(err);
}
}
Ok(true)
}