use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use iced_x86::{Code, Decoder, DecoderOptions, 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, ContinueDisposition, DebugBackend, DebugCapability,
DebugOutputPage, HW_BREAKPOINT_SLOTS, HwBreakpointAccess, LastEvent, StopEvent,
WatchpointAccess,
};
use crate::disasm::{DisasmRow, decode_rows, decode_rows_arm64, disasm_formatter};
use crate::dmp::DmpBackend;
use crate::error::{Error, Result};
use crate::gdb::breakpoints::{Breakpoint, BreakpointConfig};
use crate::gdb::{
BreakpointHitDisposition, BreakpointHitResult, BreakpointManager, GdbClient, RegisterMap,
};
use crate::guest::ProcessInfo;
use crate::kd::{KdBackend, KdMemorySource, hwbp, trace_enabled};
use crate::memory::DTB_IDENTITY;
use crate::memory_backend::MemoryBackend;
use crate::phys::PhysMem;
use crate::target::{ReloadReport, Target, ThreadInfo};
use crate::types::{Arch, VirtAddr};
use crate::unwind::{
StackTrace, ThreadStackTrace, build_parked_thread_stack, build_stacktrace, preferred_code_dtb,
resolve_thread_trace_context,
};
use crate::{Backend, TargetSpec};
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,
action: Option<String>,
rip: u64,
condition_error: Option<String>,
},
Bugcheck {
rip: Option<u64>,
info: Option<BugcheckInfo>,
},
Stopped {
rip: u64,
exception_code: Option<u32>,
first_chance: Option<bool>,
exception_address: Option<u64>,
},
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<ProcessInfo>,
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, Copy, PartialEq, Eq)]
pub struct CurrentInstruction {
pub is_call: bool,
pub next_ip: u64,
}
#[derive(Debug, Clone)]
pub enum BreakpointStopAction {
Hit {
breakpoint: Breakpoint,
condition_error: Option<String>,
},
Resumed,
NotBreakpoint,
}
#[derive(Debug, Clone)]
pub enum WatchpointStopAction {
Hit {
breakpoint: Breakpoint,
condition_error: Option<String>,
},
Resumed,
NotBreakpoint,
}
#[derive(Debug, Clone)]
pub enum StopResolution {
Resumed,
Breakpoint {
breakpoint: Breakpoint,
event: StopEvent,
rip: u64,
condition_error: Option<String>,
},
Bugcheck { event: StopEvent },
TargetReloaded { event: StopEvent, coherent: bool },
Stopped { event: StopEvent, rip: u64 },
}
fn update_target_context_from_registers(
target: &mut Target,
register_map: &RegisterMap,
registers: Result<Vec<u8>>,
) {
target.selected_frame = None;
let Ok(registers) = registers else {
target.registers = None;
target.clear_context_dtb_override();
return;
};
target.registers = Some(register_map.to_hashmap(®isters));
match register_map.read_u64(target.arch().dtb_register(), ®isters) {
Ok(dtb) if dtb != 0 && target.guest.is_some() && target.kernel_dtb() != DTB_IDENTITY => {
target.set_context_dtb_override(dtb)
}
_ => target.clear_context_dtb_override(),
}
}
#[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 CONTINUE_POLL_INTERVAL: Duration = Duration::from_millis(200);
const INTERRUPT_MAX_RESUMES: usize = 8;
const EXIT_STOP_POLL: 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,
parked_windows_thread: Option<VirtAddr>,
pub reload_module_list_pending: bool,
reload_surface_pending: bool,
parked_stop: Option<ContinueOutcome>,
pub last_event: Option<LastEvent>,
_instance_guard: Option<InstanceGuard>,
}
fn prepare_backend_after_cleanup(
backend: &mut dyn DebugBackend,
cleanup: Result<()>,
) -> Result<()> {
match cleanup {
Ok(()) => backend.prepare_for_exit(true),
Err(cleanup_error) => match backend.prepare_for_exit(false) {
Ok(()) => Err(cleanup_error),
Err(teardown_error) => Err(Error::Rsp(format!(
"{cleanup_error}; backend teardown also failed: {teardown_error}"
))),
},
}
}
impl Session {
pub fn open(spec: &TargetSpec) -> Result<Self> {
spec.validate().map_err(Error::DebugInfo)?;
match spec {
TargetSpec::Dump(path) => {
let phys = Arc::new(PhysMem::dmp(path)?);
let info = phys
.dmp_info()
.expect("dmp_info must be Some for DMP backend")
.clone();
Self::connect(phys, None, || Ok(Box::new(DmpBackend::new(&info))))
}
TargetSpec::Live {
backend: backend @ (Backend::Kd | Backend::KdNet),
kdnet_key,
memory_source,
..
} => {
let endpoint = spec.endpoint().expect("KD/KDNET always have an endpoint");
Self::connect_kd(endpoint, *memory_source, || match backend {
Backend::Kd => KdBackend::connect(endpoint),
Backend::KdNet => {
let key = kdnet_key.as_deref().expect("validated above");
KdBackend::connect_net(endpoint, key)
}
Backend::Gdb | Backend::Memory => unreachable!("matched KD above"),
})
}
TargetSpec::Live { backend, .. } => {
let phys = Arc::new(PhysMem::live()?);
let endpoint = spec.endpoint();
Self::connect(phys, endpoint, || {
Ok(match backend {
Backend::Gdb => Box::new(GdbClient::connect(
endpoint.expect("gdb always has an endpoint"),
)?),
Backend::Memory => Box::new(MemoryBackend::new()),
Backend::Kd | Backend::KdNet => unreachable!("matched above"),
})
})
}
}
}
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 connect_kd<F>(
resource: &str,
memory_source: KdMemorySource,
make_backend: F,
) -> Result<Self>
where
F: FnOnce() -> Result<KdBackend>,
{
let guard = Some(acquire_instance_guard(resource)?);
let mut backend = make_backend()?;
let hints = backend.target_hints()?;
let host_phys = match memory_source {
KdMemorySource::Kd => None,
KdMemorySource::Host => {
let phys = Arc::new(PhysMem::live().map_err(|error| {
Error::Kd(format!("host memory source unavailable: {error}"))
})?);
backend.validate_host_memory(&*phys, hints)?;
Some(phys)
}
KdMemorySource::Auto => match PhysMem::live() {
Ok(phys) => {
let phys = Arc::new(phys);
match backend.validate_host_memory(&*phys, hints) {
Ok(()) => Some(phys),
Err(error) => {
eprintln!(
"{}: host memory rejected ({error}); falling back to KD memory",
backend.name()
);
None
}
}
}
Err(error) => {
eprintln!(
"{}: host memory unavailable ({error}); falling back to KD memory",
backend.name()
);
None
}
},
};
let (target, backend): (Target, Box<dyn DebugBackend>) = match host_phys {
Some(phys) => {
eprintln!(
"{}: memory source host (validated VM-process memory)",
backend.name()
);
(
Target::with_remote_phys(
phys,
hints.kernel_dtb,
hints.kernel_base,
hints.arch,
)?,
Box::new(backend),
)
}
None => {
let (backend, memory) = backend.into_remote_memory();
let phys = Arc::new(PhysMem::remote(memory));
(
Target::with_remote_phys(
phys,
hints.kernel_dtb,
hints.kernel_base,
hints.arch,
)?,
Box::new(backend),
)
}
};
let mut session = Self::new_with_target(target, backend)?;
session._instance_guard = guard;
Ok(session)
}
pub fn new(phys: Arc<PhysMem>, backend: Box<dyn DebugBackend>) -> Result<Self> {
let target = Target::with_phys(phys)?;
Self::new_with_target(target, backend)
}
fn new_with_target(mut target: Target, mut backend: Box<dyn DebugBackend>) -> Result<Self> {
let debugger_data_hint = backend.target_debugger_data_hint().ok().flatten();
target.refresh_debugger_data(debugger_data_hint);
backend.initialize_from_target(&target);
backend.set_kernel_dtb(target.kernel_dtb());
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);
let mut session = Self {
id: NEXT_SESSION_ID.fetch_add(1, Ordering::Relaxed),
target,
backend,
breakpoints: BreakpointManager::new(),
register_map,
current_thread,
parked_windows_thread: None,
reload_module_list_pending: false,
reload_surface_pending: false,
parked_stop: None,
last_event: None,
_instance_guard: None,
};
if has_register_context {
session.refresh_context_for_current_thread();
}
Ok(session)
}
pub fn step(&mut self) -> Result<()> {
self.require_live_register_context()?;
self.target.selected_frame = None;
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)?;
}
for id in self.breakpoints.one_shot_hit_ids() {
self.breakpoints
.remove(self.backend.as_mut(), &self.target, id)?;
}
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.target.selected_frame = None;
self.current_thread = id.to_string();
self.parked_windows_thread = None;
self.target.clear_current_windows_thread_context();
self.refresh_context_for_current_thread();
Ok(())
}
pub fn select_parked_windows_thread(&mut self, thread: &ThreadInfo) {
self.target.selected_frame = None;
self.parked_windows_thread = Some(thread.ethread);
self.target.set_parked_windows_thread(thread.clone());
}
pub fn parked_windows_thread(&self) -> Option<&ThreadInfo> {
let ethread = self.parked_windows_thread?;
self.target
.windows_thread_selection
.as_ref()
.filter(|thread| thread.ethread == ethread)
}
fn require_live_register_context(&self) -> Result<()> {
if self.parked_windows_thread().is_some() {
return Err(Error::DebugInfo(
"selected Windows thread is parked; registers and execution control require a live vCPU context (use `vcpu <id>`)".into(),
));
}
Ok(())
}
pub fn record_stop_event(&mut self, event: &StopEvent) {
self.last_event = Some(LastEvent::new(event.clone()));
}
pub fn record_continuation_disposition(&mut self, disposition: ContinueDisposition) {
if let Some(last_event) = &mut self.last_event {
last_event.disposition = Some(disposition);
}
}
pub fn interrupt(&mut self) -> Result<StopEvent> {
for _ in 0..INTERRUPT_MAX_RESUMES {
let event = self.backend.interrupt()?;
match self.classify_stop_event(event)? {
StopResolution::Resumed => continue,
StopResolution::Breakpoint { event, .. }
| StopResolution::Bugcheck { event }
| StopResolution::TargetReloaded { event, .. }
| StopResolution::Stopped { event, .. } => return Ok(event),
}
}
self.backend.interrupt()
}
pub fn halt_for_exit(&mut self) -> Result<()> {
if let Some(event) = self.backend.try_wait_for_stop(EXIT_STOP_POLL)?
&& !matches!(self.classify_stop_event(event)?, StopResolution::Resumed)
{
return Ok(());
}
self.interrupt().map(drop)
}
fn refresh_context_for_current_thread(&mut self) {
self.parked_windows_thread = None;
if self.backend.is_running() {
return;
}
let registers = self
.backend
.set_current_thread(&self.current_thread)
.and_then(|_| self.backend.read_registers());
update_target_context_from_registers(&mut self.target, &self.register_map, registers);
}
pub fn current_instruction(&mut self) -> Result<CurrentInstruction> {
self.require_live_register_context()?;
self.backend.set_current_thread(&self.current_thread)?;
let regs = self.backend.read_registers()?;
let pc = self.register_map.read_u64("rip", ®s)?;
let dtb = self
.register_map
.read_u64(self.target.arch().dtb_register(), ®s)
.unwrap_or(0);
let trace = resolve_thread_trace_context(&self.target, dtb);
let code_dtb = preferred_code_dtb(&trace, pc);
let memory = self.target.address_space(code_dtb);
let mut bytes = [0u8; 16];
memory.read_bytes(VirtAddr(pc), &mut bytes)?;
self.breakpoints
.mask_breakpoint_bytes(VirtAddr(pc), &mut bytes, trace.active_dtb);
if self.target.arch() == Arch::Arm64 {
let word = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let Ok(instruction) = bad64::decode(word, pc) else {
return Err(Error::DebugInfo(format!(
"failed to decode instruction at {pc:#x}"
)));
};
let mnem = instruction.op().mnem();
return Ok(CurrentInstruction {
is_call: mnem == "bl" || mnem == "blr",
next_ip: pc.wrapping_add(4),
});
}
let mut decoder = Decoder::with_ip(64, &bytes, pc, DecoderOptions::NONE);
let instruction = decoder.decode();
if instruction.code() == Code::INVALID {
return Err(Error::DebugInfo(format!(
"failed to decode instruction at {pc:#x}"
)));
}
Ok(CurrentInstruction {
is_call: instruction.mnemonic() == Mnemonic::Call,
next_ip: instruction.next_ip(),
})
}
pub fn step_over_target(&mut self) -> Result<StepKind> {
let instruction = self.current_instruction()?;
if instruction.is_call {
Ok(StepKind::RunTo(VirtAddr(instruction.next_ip)))
} else {
Ok(StepKind::Single)
}
}
pub fn step_out_target(&mut self) -> Result<VirtAddr> {
self.require_live_register_context()?;
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.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 read_registers(&mut self) -> Result<Vec<u8>> {
self.require_live_register_context()?;
if self.backend.is_running() {
return Err(Error::TargetRunning);
}
self.backend.set_current_thread(&self.current_thread)?;
self.backend.read_registers()
}
pub fn write_register(&mut self, name: &str, value: u64) -> Result<()> {
self.require_live_register_context()?;
if self.backend.is_running() {
return Err(Error::TargetRunning);
}
if !self
.backend
.capabilities()
.iter()
.any(|entry| entry.capability == DebugCapability::WriteRegisters && entry.supported)
{
return Err(Error::RegisterWriteUnsupported);
}
let mut regs = self.read_registers()?;
self.register_map.write_u64(name, &mut regs, value)?;
self.backend.write_registers(®s)?;
self.target.registers = Some(self.register_map.to_hashmap(®s));
Ok(())
}
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()?;
if matches!(
self.classify_stop_event(event)?,
StopResolution::TargetReloaded { .. }
) {
self.reload_surface_pending = true;
}
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 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)
};
RunStatus {
running,
current_thread: self.current_thread.clone(),
rip,
symbol,
process: self.target.current_process_info.clone(),
coherent: self.kernel_coherent(),
kernel_base: self.target.kernel_base().map(|a| a.0).unwrap_or(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 add_symbol_breakpoint(
&mut self,
symbol: String,
condition: Option<String>,
) -> Result<u32> {
self.breakpoints.add_symbolic(
self.backend.as_mut(),
&self.target,
symbol,
BreakpointConfig {
condition,
..BreakpointConfig::default()
},
)
}
pub fn add_source_breakpoint(
&mut self,
source: String,
condition: Option<String>,
) -> Result<Vec<u32>> {
self.breakpoints.add_source(
self.backend.as_mut(),
&self.target,
source,
BreakpointConfig {
condition,
..BreakpointConfig::default()
},
)
}
pub fn add_watchpoint(
&mut self,
addr: VirtAddr,
access: WatchpointAccess,
len: u8,
) -> Result<u32> {
self.add_watchpoint_with_condition(addr, access, len, None)
}
pub fn add_watchpoint_with_condition(
&mut self,
addr: VirtAddr,
access: WatchpointAccess,
len: u8,
condition: Option<String>,
) -> Result<u32> {
self.add_watchpoint_with_symbol_condition(addr, access, len, None, condition)
}
pub fn add_watchpoint_with_symbol_condition(
&mut self,
addr: VirtAddr,
access: WatchpointAccess,
len: u8,
symbol: Option<String>,
condition: Option<String>,
) -> Result<u32> {
self.breakpoints.add_hardware(
self.backend.as_mut(),
&self.target,
addr,
access.into(),
len,
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
.as_ref()
.and_then(|g| g.enumerate_processes().ok())
.unwrap_or_default();
let dtb_mask = self.target.arch().dtb_page_mask();
let kernel_dtb_masked = self
.target
.guest
.as_ref()
.map(|g| g.ntoskrnl.dtb() & dtb_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(dtb)) = (
self.register_map.read_u64("rip", ®s),
self.register_map
.read_u64(self.target.arch().dtb_register(), ®s),
) else {
out.push(VcpuInfo {
id: thread.clone(),
rip: None,
context: String::new(),
symbol: None,
error: None,
});
continue;
};
if rip == 0 {
out.push(VcpuInfo {
id: thread.clone(),
rip: Some(0),
context: "no context".to_string(),
symbol: None,
error: None,
});
continue;
}
let dtb_masked = dtb & dtb_mask;
let (context, symbol) = if kernel_dtb_masked.is_some_and(|k| dtb_masked == k) {
let sym = self
.target
.guest
.as_ref()
.and_then(|g| g.ntoskrnl.closest_symbol(VirtAddr(rip)).ok())
.map(|(s, o)| format!("{s}+{o:#x}"));
("kernel".to_string(), sym)
} else {
match processes.iter().find(|p| (p.dtb & dtb_mask) == dtb_masked) {
Some(proc) => {
let sym = self
.target
.symbols
.format_closest_symbol_for_address(proc.dtb, VirtAddr(rip));
(proc.name.clone(), sym)
}
None => {
let sym = self.target.closest_symbol_current_context(VirtAddr(rip));
let ctx = if sym.is_some() { "kernel" } else { "unknown" };
(ctx.to_string(), sym)
}
}
};
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 read_masked(&self, addr: VirtAddr, buf: &mut [u8]) -> Result<()> {
let process = self.target.current_process()?;
process.memory().read_bytes(addr, buf)?;
self.breakpoints
.mask_breakpoint_bytes(addr, buf, process.dtb());
Ok(())
}
pub fn disassemble(&self, addr: VirtAddr, count: usize) -> Result<Vec<DisasmRow>> {
let process = self.target.current_process()?;
let dtb = process.dtb();
let overread = match self.target.arch() {
Arch::Amd64 => count * 16,
Arch::Arm64 => count * 4,
};
let mut buf = vec![0u8; overread];
self.read_masked(addr, &mut buf)?;
let symbols = &self.target.symbols;
let resolve = |target: u64| {
symbols
.format_closest_symbol_for_address(dtb, VirtAddr(target))
.unwrap_or_default()
};
match self.target.arch() {
Arch::Amd64 => {
let mut formatter = disasm_formatter();
Ok(decode_rows(
&buf,
addr.0,
Some(count),
&mut formatter,
resolve,
))
}
Arch::Arm64 => Ok(decode_rows_arm64(&buf, addr.0, Some(count), resolve)),
}
}
pub fn backtrace(&mut self, limit: usize) -> Result<StackTrace> {
if let Some(thread) = self.parked_windows_thread() {
return Ok(build_parked_thread_stack(&self.target, thread, limit)?.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 backtrace_thread(&self, thread: &ThreadInfo, limit: usize) -> Result<ThreadStackTrace> {
build_parked_thread_stack(&self.target, thread, limit)
}
pub fn remove_all_breakpoints(&mut self) -> Result<()> {
self.breakpoints
.remove_all(self.backend.as_mut(), &self.target)
}
pub fn cleanup_for_exit(&mut self) -> Result<()> {
let halted = if self.backend.is_running() {
self.interrupt().map(|_| ())
} else {
Ok(())
};
if halted.is_err() {
return prepare_backend_after_cleanup(self.backend.as_mut(), halted);
}
let cleanup = self.remove_all_breakpoints();
prepare_backend_after_cleanup(self.backend.as_mut(), cleanup)
}
pub fn resume(&mut self) -> Result<()> {
self.resume_with_disposition(ContinueDisposition::Handled)
}
pub fn clear_resume_state(&mut self) {
self.target.selected_frame = None;
self.target.registers = None;
self.target.clear_context_dtb_override();
self.target.clear_current_windows_thread_context();
self.parked_windows_thread = None;
self.parked_stop = None;
}
pub fn resume_with_disposition(&mut self, disposition: ContinueDisposition) -> Result<()> {
self.target.selected_frame = None;
if self.parked_windows_thread().is_some() {
self.parked_windows_thread = None;
self.target.clear_current_windows_thread_context();
self.refresh_context_for_current_thread();
}
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,
)?;
}
for id in self.breakpoints.one_shot_hit_ids() {
self.breakpoints
.remove(self.backend.as_mut(), &self.target, id)?;
}
self.breakpoints
.refresh_enabled(self.backend.as_mut(), &self.target)?;
self.backend
.continue_execution_with_disposition(disposition)?;
self.record_continuation_disposition(disposition);
self.target.registers = None;
self.target.clear_context_dtb_override();
self.target.clear_current_windows_thread_context();
self.parked_windows_thread = None;
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 self.breakpoints.record_hit(bp.id)? == BreakpointHitDisposition::SkipPass {
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 condition_error = match bp.evaluate_condition(&self.target) {
Ok(false) => {
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(true) => None,
Err(error) => Some(error.to_string()),
};
let _ = self
.breakpoints
.refresh_enabled(self.backend.as_mut(), &self.target);
self.breakpoints.mark_one_shot_hit(bp.id)?;
Ok(BreakpointStopAction::Hit {
breakpoint: bp,
condition_error,
})
}
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 classify_stop_event(&mut self, mut event: StopEvent) -> Result<StopResolution> {
self.target.selected_frame = None;
self.record_stop_event(&event);
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(StopResolution::Bugcheck { event });
}
match self.classify_reload_stop(&mut event)? {
disposition @ (ReloadDisposition::Reloaded { .. }
| ReloadDisposition::ReloadCompleted) => {
let coherent =
!matches!(disposition, ReloadDisposition::Reloaded { coherent: false });
self.refresh_context_for_current_thread();
return Ok(StopResolution::TargetReloaded { event, coherent });
}
ReloadDisposition::PendingRediscovery | ReloadDisposition::ResumePastAssist => {
self.backend.continue_execution()?;
return Ok(StopResolution::Resumed);
}
ReloadDisposition::Ordinary => {}
}
match resolve_watchpoint_stop(
self.backend.as_mut(),
&self.register_map,
&mut self.breakpoints,
&mut self.target,
&mut self.current_thread,
&event,
)? {
WatchpointStopAction::Hit {
breakpoint,
condition_error,
} => {
let rip = self
.target
.registers
.as_ref()
.and_then(|registers| registers.get("rip").copied())
.unwrap_or(0);
return Ok(StopResolution::Breakpoint {
breakpoint,
event,
rip,
condition_error,
});
}
WatchpointStopAction::Resumed => return Ok(StopResolution::Resumed),
WatchpointStopAction::NotBreakpoint => {}
}
if stop_is_stray_single_step(&event, &self.breakpoints) {
let _ = clear_trap_flag(self.backend.as_mut(), &self.register_map);
self.backend.continue_execution()?;
return Ok(StopResolution::Resumed);
}
if self.breakpoints.has_enabled_breakpoints() {
rewind_threads_off_breakpoints(
self.backend.as_mut(),
&self.register_map,
&self.breakpoints,
&self.current_thread,
self.target.arch(),
);
}
let registers = self.backend.read_registers()?;
let rip = self.register_map.read_u64("rip", ®isters).unwrap_or(0);
let cr3 = self
.register_map
.read_u64(self.target.arch().dtb_register(), ®isters)
.unwrap_or(0);
update_target_context_from_registers(&mut self.target, &self.register_map, Ok(registers));
match self.resolve_breakpoint_stop(rip, cr3)? {
BreakpointStopAction::Hit {
breakpoint,
condition_error,
} => Ok(StopResolution::Breakpoint {
breakpoint,
event,
rip,
condition_error,
}),
BreakpointStopAction::Resumed => Ok(StopResolution::Resumed),
BreakpointStopAction::NotBreakpoint => Ok(StopResolution::Stopped { event, rip }),
}
}
pub fn continue_until_break(
&mut self,
timeout: Option<Duration>,
cancel: &AtomicBool,
) -> Result<ContinueOutcome> {
self.continue_until_break_with_disposition(timeout, cancel, ContinueDisposition::Handled)
}
pub fn continue_until_break_with_disposition(
&mut self,
timeout: Option<Duration>,
cancel: &AtomicBool,
disposition: ContinueDisposition,
) -> Result<ContinueOutcome> {
if !self.backend.is_running() {
self.resume_with_disposition(disposition)?;
}
self.wait_for_stop_bounded(timeout, cancel)
}
pub fn wait_for_stop_bounded(
&mut self,
timeout: Option<Duration>,
cancel: &AtomicBool,
) -> Result<ContinueOutcome> {
if let Some(parked) = self.parked_stop.take() {
return Ok(parked);
}
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 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: self.target.kernel_base().map(|a| a.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;
}
};
match self.classify_stop_event(event)? {
StopResolution::Resumed => continue,
StopResolution::Breakpoint {
breakpoint,
rip,
condition_error,
..
} => {
return Ok(ContinueOutcome::Breakpoint {
id: breakpoint.id,
address: breakpoint.address.0,
symbol: breakpoint.symbol,
temporary: breakpoint.temporary,
action: breakpoint.action,
rip,
condition_error,
});
}
StopResolution::Bugcheck { event } => {
return Ok(ContinueOutcome::Bugcheck {
rip: event.program_counter,
info: event.bugcheck,
});
}
StopResolution::TargetReloaded { coherent, .. } => {
reload_trace!(
"continue: SURFACE target_reloaded base={} coherent={}",
self.target.kernel_base().map_or_else(
|| "none".to_string(),
|address| format!("{:#x}", address.0)
),
coherent,
);
return Ok(ContinueOutcome::TargetReloaded {
kernel_base: self.target.kernel_base().map(|address| address.0),
coherent,
});
}
StopResolution::Stopped { event, rip } => {
return Ok(ContinueOutcome::Stopped {
rip,
exception_code: event.exception_code,
first_chance: event.first_chance,
exception_address: event.exception_address,
});
}
}
}
}
pub fn wait_for_stop(&mut self) -> Result<StopEvent> {
loop {
let event = self.backend.wait_for_stop()?;
match self.classify_stop_event(event)? {
StopResolution::Resumed => continue,
StopResolution::Breakpoint { event, .. }
| StopResolution::Bugcheck { event }
| StopResolution::TargetReloaded { event, .. }
| StopResolution::Stopped { event, .. } => return 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);
if let Some(error) = outcome.breakpoint_error {
return Err(error);
}
outcome.report.map(|_| ())
}
pub fn reload(&mut self) -> Result<()> {
self.reload_with_hint(None)
}
pub fn try_complete_pending_reload(&mut self) -> Result<bool> {
if !self.reload_module_list_pending {
return Ok(false);
}
let startup = match self.target.startup_message_data() {
Ok(startup) => startup,
Err(error) => {
reload_trace!("try_complete: startup read failed: {error}");
return Ok(false);
}
};
reload_trace!("try_complete: psmods={:#x}", startup.loaded_module_list.0);
if startup.loaded_module_list.is_zero() {
return Ok(false);
}
self.target.refresh_kernel_module_symbols()?;
self.breakpoints
.resolve_symbolic(self.backend.as_mut(), &self.target)?;
self.backend.note_target_rediscovery_complete();
self.reload_module_list_pending = false;
Ok(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) -> Result<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 TargetReloadOutcome {
report,
hint,
breakpoint_error,
} = perform_target_reload(
self.backend.as_mut(),
&mut self.target,
&mut self.breakpoints,
event.target_kernel_base_hint,
);
if let Some(error) = breakpoint_error {
return Err(error);
}
return Ok(match 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={} psmods={} coherent={}",
hint.map_or_else(|| "none".to_string(), |value| format!("{:#x}", value.0)),
self.target.kernel_base().map_or_else(
|| "none".to_string(),
|address| format!("{:#x}", address.0)
),
report.startup.as_ref().map_or_else(
|| "none".to_string(),
|startup| format!("{:#x}", startup.loaded_module_list.0),
),
coherent,
);
ReloadDisposition::Reloaded { coherent }
}
Err(error) => {
self.reload_module_list_pending = true;
self.reload_surface_pending = true;
reload_trace!("classify: reload err={error} -> 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 Ok(ReloadDisposition::ReloadCompleted);
}
if stop_is_assisted_refresh_breakin(&self.breakpoints, event) {
reload_trace!("classify: pending reload COMPLETED silently -> resume_past_assist");
return Ok(ReloadDisposition::ResumePastAssist);
}
reload_trace!("classify: pending reload COMPLETED silently at a real stop");
return Ok(ReloadDisposition::Ordinary);
}
if stop_is_assisted_refresh_breakin(&self.breakpoints, event) {
reload_trace!("classify: assisted refresh break-in -> resume_past_assist");
return Ok(ReloadDisposition::ResumePastAssist);
}
reload_trace!("classify: ordinary");
Ok(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()));
#[cfg(target_os = "macos")]
let key = std::env::temp_dir().join(&key).display().to_string();
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 breakpoint_error: Option<Error>,
}
pub fn perform_target_reload(
backend: &mut dyn DebugBackend,
target: &mut Target,
breakpoints: &mut BreakpointManager,
event_hint: Option<VirtAddr>,
) -> TargetReloadOutcome {
breakpoints.prepare_target_reload(backend);
let hint = event_hint.or_else(|| backend.target_kernel_base_hint().ok().flatten());
let report = target.reload_guest_with_kernel_base_hint(hint);
let breakpoint_error = if report.is_ok() {
let debugger_data_hint = backend.target_debugger_data_hint().ok().flatten();
target.refresh_debugger_data(debugger_data_hint);
breakpoints.resolve_symbolic(backend, target).err()
} else {
None
};
match &report {
Ok(_) => backend.note_target_rediscovery_complete(),
Err(_) => backend.note_target_rediscovery_pending(),
}
TargetReloadOutcome {
report,
hint,
breakpoint_error,
}
}
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.kernel_dtb();
if debugger
.symbols
.find_module_for_address(current_dtb, VirtAddr(pc))
.is_some()
{
return false;
}
if !event.is_bugcheck
&& debugger
.kernel_base()
.is_some_and(|base| pc.abs_diff(base.0) < CURRENT_KERNEL_RELOAD_WINDOW)
{
return false;
}
debugger
.rediscovered_kernel_identity_changed()
.unwrap_or(false)
}
pub fn stop_is_assisted_refresh_breakin(
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;
}
event.assisted_breakin
}
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 hardware_breakpoint_hit(
backend: &mut dyn DebugBackend,
register_map: &RegisterMap,
breakpoints: &BreakpointManager,
event: &StopEvent,
) -> Result<Option<Breakpoint>> {
if event.exception_code != Some(STATUS_SINGLE_STEP)
|| event.is_bugcheck
|| !breakpoints.has_enabled_hardware_breakpoints()
{
return Ok(None);
}
let mut regs = backend.read_registers()?;
let Ok(dr6) = register_map.read_u64("dr6", ®s) else {
return arm64_hardware_breakpoint_hit(register_map, breakpoints, event, ®s);
};
let hit = (0..HW_BREAKPOINT_SLOTS)
.filter(|slot| dr6 & (1u64 << slot) != 0)
.find_map(|slot| breakpoints.hardware_breakpoint_for_slot(slot));
let mut dirty = false;
let cleared = dr6 & !0b1111u64;
if cleared != dr6 {
register_map.write_u64("dr6", &mut regs, cleared)?;
dirty = true;
}
if hit
.as_ref()
.and_then(|bp| bp.hardware)
.is_some_and(|hw| hw.access == HwBreakpointAccess::Execute)
{
let eflags = register_map.read_u64("eflags", ®s)?;
const RF: u64 = 1 << 16;
if eflags & RF == 0 {
register_map.write_u64("eflags", &mut regs, eflags | RF)?;
dirty = true;
}
}
if dirty {
backend.write_registers(®s)?;
}
Ok(hit)
}
fn arm64_hardware_breakpoint_hit(
register_map: &RegisterMap,
breakpoints: &BreakpointManager,
event: &StopEvent,
regs: &[u8],
) -> Result<Option<Breakpoint>> {
let pc = register_map
.read_u64("pc", regs)
.or_else(|_| register_map.read_u64("rip", regs))
.unwrap_or_else(|_| event.program_counter.unwrap_or(0));
let far = register_map.read_u64("far", regs).unwrap_or(0);
let mut hit = None;
if far != 0 {
for slot in hwbp::ARM64_WATCHPOINT_SLOTS {
let Some(bp) = breakpoints.hardware_breakpoint_for_slot(slot) else {
continue;
};
let Some(hw) = bp.hardware else { continue };
if hw.access == HwBreakpointAccess::Execute {
continue;
}
let control = register_map
.read_u64(format!("wcr{slot}"), regs)
.unwrap_or(0);
let value = register_map
.read_u64(format!("wvr{slot}"), regs)
.unwrap_or(0);
if control & 1 == 0 || value != far & !7 {
continue;
}
let bas = ((control >> 5) & 0xff) as u8;
let far_bit = 1u8 << (far & 7);
let in_requested_range = bp
.address
.0
.checked_add(hw.len as u64)
.is_some_and(|end| far >= bp.address.0 && far < end);
if bas & far_bit != 0 && in_requested_range {
hit = Some(bp);
break;
}
}
}
if hit.is_none() {
for slot in hwbp::ARM64_BREAKPOINT_SLOTS {
let Some(bp) = breakpoints.hardware_breakpoint_for_slot(slot) else {
continue;
};
let Some(hw) = bp.hardware else { continue };
if hw.access != HwBreakpointAccess::Execute {
continue;
}
let index = slot - hwbp::ARM64_BREAKPOINT_SLOTS.start;
let control = register_map
.read_u64(format!("bcr{index}"), regs)
.unwrap_or(0);
let value = register_map
.read_u64(format!("bvr{index}"), regs)
.unwrap_or(0);
if control & 1 != 0 && value == pc & !3 && bp.address.0 == pc {
hit = Some(bp);
break;
}
}
}
Ok(hit)
}
pub fn resolve_watchpoint_stop(
backend: &mut dyn DebugBackend,
register_map: &RegisterMap,
breakpoints: &mut BreakpointManager,
target: &mut Target,
current_thread: &mut String,
event: &StopEvent,
) -> Result<WatchpointStopAction> {
let Some(breakpoint) = hardware_breakpoint_hit(backend, register_map, breakpoints, event)?
else {
return Ok(WatchpointStopAction::NotBreakpoint);
};
set_current_thread_from_stop(backend, event, current_thread);
let registers = backend.read_registers()?;
let scope_dtb = register_map
.read_u64(target.arch().dtb_register(), ®isters)
.unwrap_or(0);
update_target_context_from_registers(target, register_map, Ok(registers));
if !breakpoint.scope.matches_cr3(scope_dtb) {
backend.continue_execution()?;
return Ok(WatchpointStopAction::Resumed);
}
if breakpoints.record_hit(breakpoint.id)? == BreakpointHitDisposition::SkipPass {
backend.continue_execution()?;
return Ok(WatchpointStopAction::Resumed);
}
let condition_error = match breakpoint.evaluate_condition(target) {
Ok(false) => {
backend.continue_execution()?;
return Ok(WatchpointStopAction::Resumed);
}
Ok(true) => None,
Err(error) => Some(error.to_string()),
};
if breakpoint.one_shot {
breakpoints.remove(backend, target, breakpoint.id)?;
}
Ok(WatchpointStopAction::Hit {
breakpoint,
condition_error,
})
}
pub fn rewind_threads_off_breakpoints(
backend: &mut dyn DebugBackend,
register_map: &RegisterMap,
breakpoints: &BreakpointManager,
restore_thread: &str,
arch: Arch,
) {
if arch == Arch::Arm64 {
return;
}
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(arch.dtb_register(), ®s)
.unwrap_or(0);
let Some(prev) = rip.checked_sub(register_map.breakpoint_step_size() as u64) 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 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()?;
let event = backend.wait_for_stop()?;
clear_trap_flag(backend, register_map)?;
if event.is_bugcheck {
return Err(Error::DebugInfo(
"target bugchecked while single-stepping".into(),
));
}
if let Some(code) = event
.exception_code
.filter(|&code| code != STATUS_SINGLE_STEP && code != STATUS_BREAKPOINT)
{
return Err(Error::DebugInfo(format!(
"target raised exception {code:#x} while single-stepping"
)));
}
Ok(())
}
pub fn clear_trap_flag(backend: &mut dyn DebugBackend, register_map: &RegisterMap) -> Result<()> {
if let Ok(mut regs) = backend.read_registers() {
let mut dirty = false;
if 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() {
dirty = true;
}
}
if let Ok(dr6) = register_map.read_u64("dr6", ®s) {
let cleared = dr6 & !0b1111u64;
if cleared != dr6 && register_map.write_u64("dr6", &mut regs, cleared).is_ok() {
dirty = true;
}
}
if dirty {
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(debugger.arch().dtb_register(), ®s)
.ok();
let Some(bp_id) = breakpoints.breakpoint_id_at_address(rip) else {
return Ok(false);
};
match (breakpoints.disable(backend, debugger, bp_id), cr3) {
(Ok(()), _) => {}
(Err(Error::BadVirtualAddress(_) | Error::AddressNotInDump(_)), Some(cr3)) => {
breakpoints
.disable_guest_memory_patch_in_address_space(backend, debugger, bp_id, cr3)?;
}
(Err(err), _) => return Err(err),
}
let stepped = step_one_and_clear_tf(backend, register_map);
match breakpoints.enable(backend, debugger, bp_id) {
Ok(()) => {}
Err(Error::BadVirtualAddress(_) | Error::AddressNotInDump(_)) => {
breakpoints.discard(backend, bp_id)?;
}
Err(err) => return stepped.and(Err(err)),
}
stepped.map(|()| true)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gdb::breakpoints::HardwareBreakpoint;
use crate::kd::context::{REGISTER_BUFFER_SIZE, build_register_map};
const DR6_BS: u64 = 1 << 14;
const RF: u64 = 1 << 16;
const TF: u64 = 1 << 8;
const EFLAGS_BASE: u64 = 0x202;
struct MockBackend {
register_map: RegisterMap,
regs: Vec<u8>,
writes: usize,
fail_writes: bool,
exit_requests: Vec<bool>,
fail_exit: bool,
}
impl MockBackend {
fn new() -> Self {
Self {
register_map: build_register_map(),
regs: vec![0u8; REGISTER_BUFFER_SIZE],
writes: 0,
fail_writes: false,
exit_requests: Vec::new(),
fail_exit: false,
}
}
fn set(&mut self, name: &str, value: u64) {
self.register_map
.write_u64(name, &mut self.regs, value)
.unwrap();
}
fn get(&self, name: &str) -> u64 {
self.register_map.read_u64(name, &self.regs).unwrap()
}
}
impl DebugBackend for MockBackend {
fn register_map(&self) -> &RegisterMap {
&self.register_map
}
fn read_registers(&mut self) -> Result<Vec<u8>> {
Ok(self.regs.clone())
}
fn write_registers(&mut self, data: &[u8]) -> Result<()> {
self.writes += 1;
if self.fail_writes {
return Err(Error::Kd("injected register write failure".into()));
}
self.regs = data.to_vec();
Ok(())
}
fn set_breakpoint(&mut self, _addr: u64) -> Result<()> {
Err(Error::NotSupported)
}
fn remove_breakpoint(&mut self, _addr: u64) -> Result<()> {
Err(Error::NotSupported)
}
fn continue_execution(&mut self) -> Result<()> {
Err(Error::NotSupported)
}
fn step(&mut self) -> Result<()> {
Err(Error::NotSupported)
}
fn interrupt(&mut self) -> Result<StopEvent> {
Err(Error::NotSupported)
}
fn wait_for_stop(&mut self) -> Result<StopEvent> {
Err(Error::NotSupported)
}
fn try_wait_for_stop(&mut self, _timeout: Duration) -> Result<Option<StopEvent>> {
Ok(None)
}
fn thread_list(&mut self) -> Result<Vec<String>> {
Err(Error::NotSupported)
}
fn set_current_thread(&mut self, _thread_id: &str) -> Result<()> {
Err(Error::NotSupported)
}
fn stopped_thread_id(&mut self) -> Result<String> {
Err(Error::NotSupported)
}
fn is_running(&self) -> bool {
false
}
fn prepare_for_exit(&mut self, leave_running: bool) -> Result<()> {
self.exit_requests.push(leave_running);
if self.fail_exit {
Err(Error::Kd("injected backend teardown failure".into()))
} else {
Ok(())
}
}
}
fn single_step_event() -> StopEvent {
StopEvent {
thread_id: None,
exception_code: Some(STATUS_SINGLE_STEP),
first_chance: Some(true),
exception_address: None,
program_counter: None,
is_bugcheck: false,
bugcheck: None,
target_reloaded: false,
target_kernel_base_hint: None,
assisted_breakin: false,
}
}
fn manager_with_hw(slot: u8, access: HwBreakpointAccess, enabled: bool) -> BreakpointManager {
let mut manager = BreakpointManager::new();
let len = match access {
HwBreakpointAccess::Execute => 1,
_ => 4,
};
manager.insert_for_test(
7,
VirtAddr(0x1000),
enabled,
Some(HardwareBreakpoint { access, len, slot }),
);
manager
}
#[test]
fn backend_default_rejects_not_handled_continuation() {
let mut backend = MockBackend::new();
assert!(matches!(
backend.continue_execution_with_disposition(ContinueDisposition::NotHandled),
Err(Error::ExceptionDispositionUnsupported)
));
}
#[test]
fn successful_breakpoint_cleanup_requests_running_exit() {
let mut backend = MockBackend::new();
prepare_backend_after_cleanup(&mut backend, Ok(())).unwrap();
assert_eq!(backend.exit_requests, vec![true]);
}
#[test]
fn failed_breakpoint_cleanup_requests_halted_exit() {
let mut backend = MockBackend::new();
let error = prepare_backend_after_cleanup(
&mut backend,
Err(Error::Kd("injected breakpoint removal failure".into())),
)
.unwrap_err();
assert!(error.to_string().contains("breakpoint removal failure"));
assert_eq!(backend.exit_requests, vec![false]);
}
#[test]
fn cleanup_reports_breakpoint_and_backend_teardown_failures() {
let mut backend = MockBackend::new();
backend.fail_exit = true;
let error = prepare_backend_after_cleanup(
&mut backend,
Err(Error::Kd("injected breakpoint removal failure".into())),
)
.unwrap_err();
let message = error.to_string();
assert!(message.contains("breakpoint removal failure"));
assert!(message.contains("backend teardown failure"));
assert_eq!(backend.exit_requests, vec![false]);
}
#[test]
fn hardware_breakpoint_hit_claims_matching_dr6_bit_and_clears_status() {
let manager = manager_with_hw(2, HwBreakpointAccess::Write, true);
let mut backend = MockBackend::new();
backend.set("dr6", (1 << 2) | DR6_BS);
backend.set("eflags", EFLAGS_BASE);
let map = build_register_map();
let hit = hardware_breakpoint_hit(&mut backend, &map, &manager, &single_step_event())
.expect("register update must succeed")
.expect("slot 2 #DB must be claimed by the registered watch");
assert_eq!(hit.id, 7);
assert_eq!(hit.hardware.expect("hw params").slot, 2);
assert_eq!(backend.get("dr6"), DR6_BS);
assert_eq!(backend.writes, 1);
assert_eq!(backend.get("eflags"), EFLAGS_BASE);
}
#[test]
fn hardware_breakpoint_hit_sets_resume_flag_only_for_execute_watches() {
for (access, want_rf) in [
(HwBreakpointAccess::Execute, true),
(HwBreakpointAccess::Write, false),
(HwBreakpointAccess::ReadWrite, false),
] {
let manager = manager_with_hw(0, access, true);
let mut backend = MockBackend::new();
backend.set("dr6", 1);
backend.set("eflags", EFLAGS_BASE);
let map = build_register_map();
let hit = hardware_breakpoint_hit(&mut backend, &map, &manager, &single_step_event())
.unwrap();
assert!(hit.is_some(), "{access:?} hit must be claimed");
let eflags = backend.get("eflags");
assert_eq!(eflags & RF != 0, want_rf, "{access:?}: RF mismatch");
assert_eq!(eflags & !RF, EFLAGS_BASE, "{access:?}: eflags clobbered");
assert_eq!(backend.get("dr6"), 0, "{access:?}: B0 not cleared");
}
}
#[test]
fn hardware_breakpoint_hit_propagates_required_register_write_failure() {
let manager = manager_with_hw(0, HwBreakpointAccess::Execute, true);
let mut backend = MockBackend::new();
backend.set("dr6", 1);
backend.set("eflags", EFLAGS_BASE);
backend.fail_writes = true;
let map = build_register_map();
assert!(
hardware_breakpoint_hit(&mut backend, &map, &manager, &single_step_event()).is_err()
);
assert_eq!(backend.get("dr6"), 1);
assert_eq!(backend.get("eflags"), EFLAGS_BASE);
assert_eq!(backend.writes, 1);
}
#[test]
fn hardware_breakpoint_hit_ignores_non_single_step_stops() {
let manager = manager_with_hw(0, HwBreakpointAccess::Write, true);
let mut backend = MockBackend::new();
backend.set("dr6", 1); let before = backend.regs.clone();
let map = build_register_map();
let mut event = single_step_event();
event.exception_code = Some(0x8000_0003);
assert!(
hardware_breakpoint_hit(&mut backend, &map, &manager, &event)
.unwrap()
.is_none()
);
event.exception_code = None;
assert!(
hardware_breakpoint_hit(&mut backend, &map, &manager, &event)
.unwrap()
.is_none()
);
event.exception_code = Some(STATUS_SINGLE_STEP);
event.is_bugcheck = true;
assert!(
hardware_breakpoint_hit(&mut backend, &map, &manager, &event)
.unwrap()
.is_none()
);
assert_eq!(backend.writes, 0);
assert_eq!(backend.regs, before);
}
#[test]
fn hardware_breakpoint_hit_requires_an_enabled_hardware_breakpoint() {
let map = build_register_map();
let mut backend = MockBackend::new();
backend.set("dr6", 1);
let before = backend.regs.clone();
let empty = BreakpointManager::new();
assert!(
hardware_breakpoint_hit(&mut backend, &map, &empty, &single_step_event())
.unwrap()
.is_none()
);
assert_eq!(backend.writes, 0);
assert_eq!(backend.regs, before);
let manager = manager_with_hw(0, HwBreakpointAccess::Write, false);
assert!(
hardware_breakpoint_hit(&mut backend, &map, &manager, &single_step_event())
.unwrap()
.is_none()
);
assert_eq!(backend.writes, 0);
assert_eq!(backend.regs, before);
}
#[test]
fn hardware_breakpoint_hit_clears_stale_dr6_bits_for_unregistered_slots() {
let manager = manager_with_hw(1, HwBreakpointAccess::Write, true);
let mut backend = MockBackend::new();
backend.set("dr6", (1 << 3) | DR6_BS);
let map = build_register_map();
assert!(
hardware_breakpoint_hit(&mut backend, &map, &manager, &single_step_event())
.unwrap()
.is_none()
);
assert_eq!(backend.get("dr6"), DR6_BS);
assert_eq!(backend.writes, 1);
}
#[test]
fn clear_trap_flag_clears_tf_and_dr6_status_in_one_write() {
let mut backend = MockBackend::new();
backend.set("eflags", TF | EFLAGS_BASE);
backend.set("dr6", 0b1011 | DR6_BS);
let map = build_register_map();
clear_trap_flag(&mut backend, &map).unwrap();
assert_eq!(backend.get("eflags"), EFLAGS_BASE);
assert_eq!(backend.get("dr6"), DR6_BS);
assert_eq!(backend.writes, 1);
}
#[test]
fn clear_trap_flag_skips_the_write_when_nothing_is_set() {
let mut backend = MockBackend::new();
backend.set("eflags", EFLAGS_BASE);
backend.set("dr6", DR6_BS);
let before = backend.regs.clone();
let map = build_register_map();
clear_trap_flag(&mut backend, &map).unwrap();
assert_eq!(backend.writes, 0);
assert_eq!(backend.regs, before);
}
}