use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use pelite::{PeView, image::IMAGE_SCN_MEM_EXECUTE};
use crate::backend::MemoryOps;
use crate::bugchecks::looks_like_kernel_pointer;
use crate::dbg_backend::{
DebugBackend, DebugCapability, HwBreakpointAccess, WatchpointAccess, validate_hw_breakpoint,
};
use crate::error::{Error, Result};
use crate::expr::{Expr, NumberRadix};
use crate::guest::{ModuleInfo, ProcessInfo, read_pe_header_page};
use crate::target::Target;
use crate::types::{Arch, Dtb, VirtAddr};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HardwareBreakpoint {
pub access: HwBreakpointAccess,
pub len: u8,
pub slot: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BreakpointSpec {
Symbol {
name: String,
skip_prologue: bool,
},
Source {
raw: String,
file: String,
line: u32,
address_index: usize,
},
}
impl BreakpointSpec {
pub fn source(raw: &str, address_index: usize) -> Option<Self> {
let (file, line) = raw.rsplit_once(':')?;
let line = line.parse().ok()?;
(!file.is_empty()).then(|| Self::Source {
raw: raw.to_string(),
file: file.to_string(),
line,
address_index,
})
}
pub fn label(&self) -> &str {
match self {
Self::Symbol { name, .. } => name,
Self::Source { raw, .. } => raw,
}
}
fn resolve(&self, debugger: &Target, dtb: Dtb) -> Result<Option<VirtAddr>> {
match self {
Self::Symbol {
name,
skip_prologue,
} => {
let Some((address, offset)) = Self::resolve_symbol_offset(debugger, dtb, name)?
else {
return Ok(None);
};
if !skip_prologue || offset != 0 {
return Ok(Some(address));
}
Ok(Some(
debugger
.post_prologue_address(dtb, address)
.unwrap_or(address),
))
}
Self::Source {
file,
line,
address_index,
..
} => Ok(debugger
.symbols
.source_addresses(dtb, file, *line)
.get(*address_index)
.copied()),
}
}
fn resolve_symbol_offset(
debugger: &Target,
dtb: Dtb,
name: &str,
) -> Result<Option<(VirtAddr, i64)>> {
if let Some(address) = debugger.symbols.find_symbol_across_modules(dtb, name)? {
return Ok(Some((address, 0)));
}
let Some((symbol, offset)) = Self::split_symbol_offset(name) else {
return Ok(None);
};
let Some(base) = debugger
.symbols
.find_symbol_across_modules(dtb, symbol.trim())?
else {
return Ok(None);
};
Ok(Some((VirtAddr(base.0.wrapping_add(offset as u64)), offset)))
}
fn split_symbol_offset(name: &str) -> Option<(&str, i64)> {
let (separator, position) = ['+', '-']
.into_iter()
.filter_map(|separator| name.rfind(separator).map(|at| (separator, at)))
.max_by_key(|(_, at)| *at)?;
if position == 0 {
return None;
}
let (symbol, tail) = name.split_at(position);
let literal = &tail[separator.len_utf8()..];
let offset = match Expr::parse_with_radix(literal, NumberRadix::Hexadecimal).ok()? {
Expr::Literal(value) => i64::try_from(value.0).ok()?,
_ => return None,
};
Some((symbol, if separator == '-' { -offset } else { offset }))
}
}
#[derive(Debug, Clone)]
pub struct Breakpoint {
pub id: u32,
pub address: VirtAddr,
pub enabled: bool,
pub symbol: Option<String>,
pub spec: Option<BreakpointSpec>,
pub resolved: bool,
pub scope: BreakpointScope,
automatic_scope: bool,
pub condition: Option<String>,
pub condition_expr: Option<Arc<Expr>>,
pub pass_count: u64,
pub hit_count: u64,
pub remaining_pass_count: u64,
pub one_shot: bool,
pub action: Option<String>,
pub temporary: bool,
pub hardware: Option<HardwareBreakpoint>,
backend: BreakpointBackend,
}
impl Breakpoint {
pub fn resolved_address(&self) -> Option<VirtAddr> {
self.resolved.then_some(self.address)
}
pub fn deferred(&self) -> bool {
self.spec.is_some() && !self.resolved
}
pub fn awaiting_page_in(&self) -> bool {
matches!(self.backend, BreakpointBackend::Kernel { original: None })
}
pub fn specification(&self) -> Option<&str> {
self.spec.as_ref().map(BreakpointSpec::label)
}
fn should_evaluate_after_hit(&self) -> bool {
self.remaining_pass_count == 0
}
pub fn watchpoint(&self) -> Option<(WatchpointAccess, u8)> {
let hardware = self.hardware?;
let access = match hardware.access {
HwBreakpointAccess::Write => WatchpointAccess::Write,
HwBreakpointAccess::ReadWrite => WatchpointAccess::ReadWrite,
HwBreakpointAccess::Execute => return None,
};
Some((access, hardware.len))
}
pub fn watch_access_name(&self) -> Option<&'static str> {
self.watchpoint().map(|(access, _)| access.name())
}
pub fn watch_length(&self) -> Option<u8> {
self.watchpoint().map(|(_, length)| length)
}
pub fn evaluate_condition(&self, target: &Target) -> Result<bool> {
match &self.condition_expr {
Some(expr) => Ok(expr.resolve(target)?.0 != 0),
None => Ok(true),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BreakpointScope {
Kernel,
Process { pid: u64, dtb: Dtb, name: String },
}
impl BreakpointScope {
pub fn process(process: &ProcessInfo) -> Self {
Self::Process {
pid: process.pid,
dtb: process.dtb,
name: process.name.clone(),
}
}
pub fn matches_cr3(&self, cr3: u64) -> bool {
match self {
Self::Kernel => true,
Self::Process { dtb, .. } => {
let mask = Arch::Amd64.dtb_page_mask();
(cr3 & mask) == (*dtb & mask)
}
}
}
pub fn label(&self) -> String {
match self {
Self::Kernel => "global".to_string(),
Self::Process { pid, name, .. } => format!("{name} ({pid})"),
}
}
}
#[derive(Debug, Clone, Copy)]
struct BreakpointPatch {
bytes: [u8; 4],
len: u8,
}
impl BreakpointPatch {
fn new(len: usize) -> Self {
debug_assert!((1..=4).contains(&len));
Self {
bytes: [0; 4],
len: len as u8,
}
}
#[cfg(test)]
fn single(byte: u8) -> Self {
let mut patch = Self::new(1);
patch.bytes[0] = byte;
patch
}
fn as_slice(&self) -> &[u8] {
&self.bytes[..self.len as usize]
}
fn as_mut_slice(&mut self) -> &mut [u8] {
let len = self.len as usize;
&mut self.bytes[..len]
}
}
#[derive(Debug, Clone)]
enum BreakpointBackend {
Kernel {
original: Option<BreakpointPatch>,
},
GuestMemoryPatch {
original: BreakpointPatch,
},
Hardware,
Deferred,
}
const fn breakpoint_opcode(arch: Arch) -> &'static [u8] {
match arch {
Arch::Amd64 => &[0xcc],
Arch::Arm64 => &[0x00, 0x00, 0x3E, 0xD4],
}
}
impl BreakpointBackend {
fn original_bytes(&self) -> &[u8] {
match self {
Self::Kernel {
original: Some(original),
}
| Self::GuestMemoryPatch { original } => original.as_slice(),
Self::Kernel { original: None } | Self::Hardware | Self::Deferred => &[],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BreakpointHitDisposition {
SkipPass,
Evaluate,
}
#[derive(Debug, Clone, Default)]
pub struct BreakpointConfig {
pub condition: Option<String>,
pub condition_expr: Option<Arc<Expr>>,
pub pass_count: u64,
pub one_shot: bool,
pub action: Option<String>,
pub scope: Option<BreakpointScope>,
pub skip_prologue: bool,
}
#[derive(Default)]
pub struct BreakpointManager {
breakpoints: HashMap<u32, Breakpoint>,
one_shot_hits: HashSet<u32>,
next_id: u32,
}
impl BreakpointManager {
pub fn new() -> Self {
Self {
breakpoints: HashMap::new(),
one_shot_hits: HashSet::new(),
next_id: 0,
}
}
#[cfg(test)]
pub fn insert_for_test(
&mut self,
id: u32,
address: VirtAddr,
enabled: bool,
hardware: Option<HardwareBreakpoint>,
) {
let backend = match hardware {
Some(_) => BreakpointBackend::Hardware,
None => BreakpointBackend::Kernel {
original: Some(BreakpointPatch::single(0x90)),
},
};
self.breakpoints.insert(
id,
Breakpoint {
id,
address,
enabled,
symbol: None,
spec: None,
resolved: true,
scope: BreakpointScope::Kernel,
automatic_scope: false,
condition: None,
condition_expr: None,
pass_count: 0,
hit_count: 0,
remaining_pass_count: 0,
one_shot: false,
action: None,
temporary: false,
hardware,
backend,
},
);
}
pub fn add(
&mut self,
client: &mut dyn DebugBackend,
debugger: &Target,
address: VirtAddr,
symbol: Option<String>,
condition: Option<String>,
) -> Result<u32> {
self.add_code_configured(
client,
debugger,
Some(address),
symbol,
None,
false,
BreakpointConfig {
condition,
..BreakpointConfig::default()
},
)
}
pub fn add_configured(
&mut self,
client: &mut dyn DebugBackend,
debugger: &Target,
address: VirtAddr,
symbol: Option<String>,
config: BreakpointConfig,
) -> Result<u32> {
self.add_code_configured(client, debugger, Some(address), symbol, None, false, config)
}
pub fn add_symbolic(
&mut self,
client: &mut dyn DebugBackend,
debugger: &Target,
symbol: String,
config: BreakpointConfig,
) -> Result<u32> {
let spec = BreakpointSpec::Symbol {
name: symbol.clone(),
skip_prologue: config.skip_prologue,
};
let dtb = Self::resolution_dtb(debugger, config.scope.as_ref());
let address = spec.resolve(debugger, dtb)?;
self.add_code_configured(
client,
debugger,
address,
Some(symbol),
Some(spec),
false,
config,
)
}
pub fn add_source(
&mut self,
client: &mut dyn DebugBackend,
debugger: &Target,
source: String,
config: BreakpointConfig,
) -> Result<Vec<u32>> {
let Some(first_spec) = BreakpointSpec::source(&source, 0) else {
return Err(Error::InvalidArgument(format!(
"invalid source breakpoint: {source}"
)));
};
let dtb = Self::resolution_dtb(debugger, config.scope.as_ref());
let address_count = match &first_spec {
BreakpointSpec::Source { file, line, .. } => {
debugger.symbols.source_addresses(dtb, file, *line).len()
}
BreakpointSpec::Symbol { .. } => unreachable!(),
};
let count = address_count.max(1);
let mut ids = Vec::with_capacity(count);
for index in 0..count {
let result = (|| {
let spec = BreakpointSpec::source(&source, index).ok_or_else(|| {
Error::InvalidArgument(format!("invalid source breakpoint: {source}"))
})?;
let address = spec.resolve(debugger, dtb)?;
self.add_code_configured(
client,
debugger,
address,
Some(source.clone()),
Some(spec),
false,
config.clone(),
)
})();
match result {
Ok(id) => ids.push(id),
Err(error) => {
if let Err(rollback_error) =
self.remove_ids(client, debugger, ids.iter().rev().copied())
{
return Err(Error::Breakpoint(format!(
"failed to add source breakpoint '{source}': {error}; rollback incomplete: {rollback_error}"
)));
}
return Err(error);
}
}
}
Ok(ids)
}
fn resolution_dtb(debugger: &Target, scope: Option<&BreakpointScope>) -> Dtb {
match scope {
Some(BreakpointScope::Process { dtb, .. }) => *dtb,
Some(BreakpointScope::Kernel) => debugger.kernel_dtb(),
None => debugger.current_dtb(),
}
}
pub fn add_temporary_code(
&mut self,
client: &mut dyn DebugBackend,
debugger: &Target,
address: VirtAddr,
) -> Result<u32> {
self.add_code_configured(
client,
debugger,
Some(address),
None,
None,
true,
BreakpointConfig::default(),
)
}
fn add_code_configured(
&mut self,
client: &mut dyn DebugBackend,
debugger: &Target,
address: Option<VirtAddr>,
symbol: Option<String>,
spec: Option<BreakpointSpec>,
temporary: bool,
config: BreakpointConfig,
) -> Result<u32> {
let condition_expr = Self::configured_condition(&config)?;
let automatic_scope = config.scope.is_none();
let fallback_scope = config
.scope
.unwrap_or_else(|| Self::scope_for_current_context(debugger));
let scope = if automatic_scope {
address
.map(|address| Self::scope_for_address(debugger, address, &fallback_scope))
.unwrap_or(fallback_scope)
} else {
fallback_scope
};
Self::validate_scope_capability(client, debugger.arch(), address, &scope)?;
let (address, resolved, backend) = match address {
Some(address) => {
self.ensure_site_available(address, false, None)?;
Self::validate_breakpoint_target(debugger, address, &scope)?;
let backend = Self::install_breakpoint(client, debugger, address, &scope)?;
(address, true, backend)
}
None => (VirtAddr(0), false, BreakpointBackend::Deferred),
};
let pass_count = config.pass_count;
let id = self.next_id;
self.next_id += 1;
self.breakpoints.insert(
id,
Breakpoint {
id,
address,
enabled: true,
symbol,
spec,
resolved,
scope,
automatic_scope,
condition: config.condition,
condition_expr,
pass_count,
hit_count: 0,
remaining_pass_count: pass_count.saturating_sub(1),
one_shot: config.one_shot,
action: config.action,
temporary,
hardware: None,
backend,
},
);
Ok(id)
}
fn install_is_target_owned(
arch: Arch,
address: Option<VirtAddr>,
scope: &BreakpointScope,
) -> bool {
matches!(scope, BreakpointScope::Kernel)
|| address.is_some_and(|address| Self::is_kernel_space(arch, address))
}
fn validate_scope_capability(
client: &dyn DebugBackend,
arch: Arch,
address: Option<VirtAddr>,
scope: &BreakpointScope,
) -> Result<()> {
let capability = if Self::install_is_target_owned(arch, address, scope) {
DebugCapability::KernelBreakpoints
} else {
DebugCapability::UserModeBreakpoints
};
if client
.capabilities()
.iter()
.any(|c| c.capability == capability && c.supported)
{
Ok(())
} else {
Err(Error::NotSupported)
}
}
pub fn add_hardware(
&mut self,
client: &mut dyn DebugBackend,
debugger: &Target,
address: VirtAddr,
access: HwBreakpointAccess,
len: u8,
symbol: Option<String>,
condition: Option<String>,
) -> Result<u32> {
self.add_hardware_configured(
client,
debugger,
address,
access,
len,
symbol,
BreakpointConfig {
condition,
..BreakpointConfig::default()
},
)
}
pub fn add_hardware_configured(
&mut self,
client: &mut dyn DebugBackend,
debugger: &Target,
address: VirtAddr,
access: HwBreakpointAccess,
len: u8,
symbol: Option<String>,
config: BreakpointConfig,
) -> Result<u32> {
if !client.supports_watchpoints() {
return Err(Error::NotSupported);
}
let condition_expr = Self::configured_condition(&config)?;
validate_hw_breakpoint(access, len, address.0)?;
self.ensure_site_available(address, true, None)?;
let slot = self.free_hardware_slot(client, access)?;
let automatic_scope = config.scope.is_none();
let fallback_scope = config
.scope
.unwrap_or_else(|| Self::scope_for_current_context(debugger));
let scope = if automatic_scope {
Self::scope_for_address(debugger, address, &fallback_scope)
} else {
fallback_scope
};
client.set_hardware_breakpoint(slot, address.0, access, len)?;
let id = self.next_id;
self.next_id += 1;
let pass_count = config.pass_count;
self.breakpoints.insert(
id,
Breakpoint {
id,
address,
enabled: true,
symbol,
spec: None,
resolved: true,
scope,
automatic_scope,
condition: config.condition,
condition_expr,
pass_count,
hit_count: 0,
remaining_pass_count: pass_count.saturating_sub(1),
one_shot: config.one_shot,
action: config.action,
temporary: false,
hardware: Some(HardwareBreakpoint { access, len, slot }),
backend: BreakpointBackend::Hardware,
},
);
Ok(id)
}
fn configured_condition(config: &BreakpointConfig) -> Result<Option<Arc<Expr>>> {
if let Some(expr) = &config.condition_expr {
return Ok(Some(Arc::clone(expr)));
}
config
.condition
.as_deref()
.map(Expr::parse)
.transpose()
.map(|expr| expr.map(Arc::new))
}
fn free_hardware_slot(
&self,
client: &dyn DebugBackend,
access: HwBreakpointAccess,
) -> Result<u8> {
let mut slots = client.hardware_slot_range(access);
let kind = if matches!(access, HwBreakpointAccess::Execute) {
"execute"
} else {
"watchpoint"
};
slots
.find(|slot| {
!self
.breakpoints
.values()
.any(|bp| bp.hardware.is_some_and(|hw| hw.slot == *slot))
})
.ok_or_else(|| {
Error::Breakpoint(format!("all {kind} hardware breakpoint slots are in use"))
})
}
pub fn remove(
&mut self,
client: &mut dyn DebugBackend,
debugger: &Target,
id: u32,
) -> Result<()> {
self.remove_if_uninstalled(id, |bp| Self::uninstall_breakpoint(client, debugger, bp))
}
fn remove_ids(
&mut self,
client: &mut dyn DebugBackend,
debugger: &Target,
ids: impl IntoIterator<Item = u32>,
) -> Result<()> {
self.remove_ids_if_uninstalled(ids, |bp| Self::uninstall_breakpoint(client, debugger, bp))
}
pub fn remove_all(&mut self, client: &mut dyn DebugBackend, debugger: &Target) -> Result<()> {
let ids = self.managed_ids();
self.remove_ids(client, debugger, ids)
}
fn remove_if_uninstalled(
&mut self,
id: u32,
uninstall: impl FnOnce(&Breakpoint) -> Result<()>,
) -> Result<()> {
let bp = self
.breakpoints
.get(&id)
.cloned()
.ok_or(Error::BPNotFound(id))?;
if bp.enabled && bp.resolved {
uninstall(&bp)?;
}
self.breakpoints.remove(&id);
self.one_shot_hits.remove(&id);
if self.breakpoints.is_empty() {
self.next_id = 0;
}
Ok(())
}
fn remove_ids_if_uninstalled(
&mut self,
ids: impl IntoIterator<Item = u32>,
mut uninstall: impl FnMut(&Breakpoint) -> Result<()>,
) -> Result<()> {
let mut failures = Vec::new();
for id in ids {
if let Err(error) = self.remove_if_uninstalled(id, |bp| uninstall(bp)) {
failures.push(format!("#{id}: {error}"));
}
}
if failures.is_empty() {
Ok(())
} else {
Err(Error::Breakpoint(format!(
"failed to uninstall breakpoints: {}",
failures.join("; ")
)))
}
}
pub fn discard(&mut self, client: &mut dyn DebugBackend, id: u32) -> Result<Breakpoint> {
let bp = self.breakpoints.remove(&id).ok_or(Error::BPNotFound(id))?;
Self::forget_backend_site(client, &bp);
self.one_shot_hits.remove(&id);
if self.breakpoints.is_empty() {
self.next_id = 0;
}
Ok(bp)
}
pub fn renumber(&mut self, id: u32, new_id: u32) -> Result<()> {
if id == new_id {
if self.breakpoints.contains_key(&id) {
return Ok(());
}
return Err(Error::BPNotFound(id));
}
if self.breakpoints.contains_key(&new_id) {
return Err(Error::Breakpoint(format!(
"breakpoint ID {new_id} is already in use"
)));
}
let mut bp = self.breakpoints.remove(&id).ok_or(Error::BPNotFound(id))?;
bp.id = new_id;
self.breakpoints.insert(new_id, bp);
if self.one_shot_hits.remove(&id) {
self.one_shot_hits.insert(new_id);
}
self.next_id = self.next_id.max(new_id.saturating_add(1));
Ok(())
}
fn forget_backend_site(client: &mut dyn DebugBackend, bp: &Breakpoint) {
if matches!(bp.backend, BreakpointBackend::GuestMemoryPatch { .. }) {
client.note_breakpoint_uninstalled(bp.address.0);
}
}
pub fn enable(
&mut self,
client: &mut dyn DebugBackend,
debugger: &Target,
id: u32,
) -> Result<()> {
let snapshot = self
.breakpoints
.get(&id)
.cloned()
.ok_or(Error::BPNotFound(id))?;
if snapshot.enabled {
return Ok(());
}
if snapshot.resolved {
self.ensure_site_available(snapshot.address, snapshot.hardware.is_some(), Some(id))?;
let backend = if matches!(
&snapshot.backend,
BreakpointBackend::Deferred | BreakpointBackend::Kernel { original: None }
) {
Some(Self::install_breakpoint(
client,
debugger,
snapshot.address,
&snapshot.scope,
)?)
} else {
Self::install_existing_breakpoint(client, debugger, &snapshot)?;
None
};
if let Some(backend) = backend {
self.breakpoints
.get_mut(&id)
.ok_or(Error::BPNotFound(id))?
.backend = backend;
}
}
self.breakpoints
.get_mut(&id)
.ok_or(Error::BPNotFound(id))?
.enabled = true;
Ok(())
}
pub fn disable(
&mut self,
client: &mut dyn DebugBackend,
debugger: &Target,
id: u32,
) -> Result<()> {
let bp = self.breakpoints.get_mut(&id).ok_or(Error::BPNotFound(id))?;
if !bp.enabled {
return Ok(());
}
if bp.resolved {
Self::uninstall_breakpoint(client, debugger, bp)?;
}
bp.enabled = false;
Ok(())
}
pub fn disable_guest_memory_patch_in_address_space(
&mut self,
client: &mut dyn DebugBackend,
debugger: &Target,
id: u32,
dtb: Dtb,
) -> Result<()> {
let bp = self.breakpoints.get_mut(&id).ok_or(Error::BPNotFound(id))?;
if !bp.enabled {
return Ok(());
}
match &bp.backend {
BreakpointBackend::GuestMemoryPatch { original } => {
let memory = debugger.address_space(dtb);
memory.write_bytes(bp.address, original.as_slice())?;
client.note_breakpoint_uninstalled(bp.address.0);
bp.enabled = false;
Ok(())
}
BreakpointBackend::Kernel { .. } => Err(Error::Breakpoint(
"cannot address-space-disable a kernel breakpoint".into(),
)),
BreakpointBackend::Hardware => Err(Error::Breakpoint(
"cannot address-space-disable a hardware breakpoint".into(),
)),
BreakpointBackend::Deferred => {
bp.enabled = false;
Ok(())
}
}
}
pub fn managed_ids(&self) -> Vec<u32> {
let mut ids = self.breakpoints.keys().copied().collect::<Vec<_>>();
ids.sort_unstable();
ids
}
pub fn list(&self) -> Vec<&Breakpoint> {
let mut bps: Vec<_> = self
.breakpoints
.values()
.filter(|bp| !self.one_shot_hits.contains(&bp.id))
.collect();
bps.sort_by_key(|bp| bp.id);
bps
}
pub fn has_enabled_breakpoints(&self) -> bool {
self.breakpoints
.values()
.any(|bp| bp.enabled && bp.resolved)
}
pub fn has_enabled_hardware_breakpoints(&self) -> bool {
self.breakpoints
.values()
.any(|bp| bp.enabled && bp.hardware.is_some())
}
pub fn hardware_breakpoint_for_slot(&self, slot: u8) -> Option<Breakpoint> {
self.breakpoints
.values()
.find(|bp| bp.enabled && bp.hardware.is_some_and(|hw| hw.slot == slot))
.cloned()
}
pub fn clear_hardware_slots(&self, client: &mut dyn DebugBackend) {
for bp in self.breakpoints.values() {
if let Some(hw) = bp.hardware {
let _ = client.clear_hardware_breakpoint(hw.slot);
}
}
}
pub fn refresh_enabled(
&mut self,
client: &mut dyn DebugBackend,
debugger: &Target,
) -> Result<()> {
let mut enabled: Vec<_> = self
.breakpoints
.values()
.filter(|bp| bp.enabled && bp.resolved && bp.hardware.is_none())
.cloned()
.collect();
enabled.sort_by_key(|bp| bp.id);
let dropped = client.sites_dropped_by_stop();
for bp in enabled {
if client.target_manages_breakpoint_sites()
&& matches!(&bp.backend, BreakpointBackend::Kernel { original: Some(_) })
&& !dropped.contains(&bp.address.0)
{
continue;
}
let uninstalled = match Self::uninstall_breakpoint(client, debugger, &bp) {
Ok(()) => true,
Err(Error::BadVirtualAddress(_) | Error::AddressNotInDump(_)) => false,
Err(error) => return Err(error),
};
if uninstalled && matches!(&bp.backend, BreakpointBackend::Kernel { original: None }) {
let backend = Self::install_breakpoint(client, debugger, bp.address, &bp.scope)?;
self.breakpoints
.get_mut(&bp.id)
.ok_or(Error::BPNotFound(bp.id))?
.backend = backend;
} else {
Self::install_existing_breakpoint(client, debugger, &bp)?;
}
}
Ok(())
}
pub fn record_hit(&mut self, id: u32) -> Result<BreakpointHitDisposition> {
let bp = self.breakpoints.get_mut(&id).ok_or(Error::BPNotFound(id))?;
bp.hit_count = bp.hit_count.saturating_add(1);
if bp.remaining_pass_count > 0 {
bp.remaining_pass_count -= 1;
Ok(BreakpointHitDisposition::SkipPass)
} else {
debug_assert!(bp.should_evaluate_after_hit());
Ok(BreakpointHitDisposition::Evaluate)
}
}
pub fn set_pass_count(&mut self, id: u32, pass_count: u64) -> Result<()> {
let bp = self.breakpoints.get_mut(&id).ok_or(Error::BPNotFound(id))?;
bp.pass_count = pass_count;
bp.remaining_pass_count = pass_count.saturating_sub(1);
Ok(())
}
pub fn set_one_shot(&mut self, id: u32, one_shot: bool) -> Result<()> {
self.breakpoints
.get_mut(&id)
.ok_or(Error::BPNotFound(id))?
.one_shot = one_shot;
Ok(())
}
pub fn set_action(&mut self, id: u32, action: Option<String>) -> Result<()> {
self.breakpoints
.get_mut(&id)
.ok_or(Error::BPNotFound(id))?
.action = action;
Ok(())
}
pub fn mark_one_shot_hit(&mut self, id: u32) -> Result<()> {
let bp = self.breakpoints.get(&id).ok_or(Error::BPNotFound(id))?;
if bp.one_shot {
self.one_shot_hits.insert(id);
}
Ok(())
}
pub fn one_shot_hit_ids(&self) -> Vec<u32> {
self.one_shot_hits.iter().copied().collect()
}
pub fn set_condition(
&mut self,
id: u32,
condition: Option<String>,
condition_expr: Option<Arc<Expr>>,
) -> Result<()> {
let bp = self.breakpoints.get_mut(&id).ok_or(Error::BPNotFound(id))?;
bp.condition = condition;
bp.condition_expr = condition_expr;
Ok(())
}
pub fn prepare_target_reload(&mut self, client: &mut dyn DebugBackend) -> usize {
self.clear_hardware_slots(client);
let before = self.breakpoints.len();
let fired_one_shots = std::mem::take(&mut self.one_shot_hits);
self.breakpoints.retain(|id, bp| {
!fired_one_shots.contains(id) && bp.hardware.is_none() && bp.spec.is_some()
});
for bp in self.breakpoints.values_mut() {
bp.resolved = false;
bp.backend = BreakpointBackend::Deferred;
}
if self.breakpoints.is_empty() {
self.next_id = 0;
}
before - self.breakpoints.len()
}
fn expand_source_specs(&mut self, debugger: &Target) {
let roots: Vec<Breakpoint> = self
.breakpoints
.values()
.filter(|bp| {
matches!(
bp.spec,
Some(BreakpointSpec::Source {
address_index: 0,
..
})
)
})
.cloned()
.collect();
for root in roots {
let Some(BreakpointSpec::Source {
raw, file, line, ..
}) = root.spec.as_ref()
else {
continue;
};
let dtb = Self::resolution_dtb(debugger, Some(&root.scope));
let count = debugger.symbols.source_addresses(dtb, file, *line).len();
for address_index in 1..count {
let already_exists = self.breakpoints.values().any(|bp| {
matches!(
bp.spec.as_ref(),
Some(BreakpointSpec::Source {
raw: other,
address_index: other_index,
..
}) if other == raw && *other_index == address_index
)
});
if already_exists {
continue;
}
let id = self.next_id;
self.next_id += 1;
let mut bp = root.clone();
bp.id = id;
bp.address = VirtAddr(0);
bp.spec = BreakpointSpec::source(raw, address_index);
bp.resolved = false;
bp.backend = BreakpointBackend::Deferred;
self.breakpoints.insert(id, bp);
}
}
}
fn defer_symbolic_sites_if(
&mut self,
client: &mut dyn DebugBackend,
mut site_is_unloaded: impl FnMut(&Breakpoint) -> bool,
) -> usize {
let ids = self
.breakpoints
.values()
.filter(|bp| {
bp.resolved && bp.spec.is_some() && bp.hardware.is_none() && site_is_unloaded(bp)
})
.map(|bp| bp.id)
.collect::<Vec<_>>();
for id in &ids {
let bp = self
.breakpoints
.get_mut(id)
.expect("collected breakpoint exists");
Self::forget_backend_site(client, bp);
bp.resolved = false;
bp.backend = BreakpointBackend::Deferred;
}
ids.len()
}
pub fn reconcile_symbolic_after_module_refresh(
&mut self,
client: &mut dyn DebugBackend,
debugger: &Target,
) -> Result<usize> {
self.defer_symbolic_sites_if(client, |bp| {
let dtb = Self::resolution_dtb(debugger, Some(&bp.scope));
debugger
.symbols
.find_module_for_address(dtb, bp.address)
.is_none()
});
self.resolve_symbolic(client, debugger)
}
pub fn resolve_symbolic(
&mut self,
client: &mut dyn DebugBackend,
debugger: &Target,
) -> Result<usize> {
self.expand_source_specs(debugger);
let mut ids: Vec<u32> = self
.breakpoints
.values()
.filter(|bp| bp.spec.is_some() && bp.hardware.is_none())
.map(|bp| bp.id)
.collect();
ids.sort_unstable();
let mut resolved_count = 0;
let mut failures: Vec<String> = Vec::new();
for id in ids {
let snapshot = self
.breakpoints
.get(&id)
.cloned()
.ok_or(Error::BPNotFound(id))?;
let spec = snapshot.spec.as_ref().ok_or_else(|| {
Error::Breakpoint(format!("breakpoint {id} lost its specification"))
})?;
let dtb = Self::resolution_dtb(debugger, Some(&snapshot.scope));
let resolved = spec.resolve(debugger, dtb)?;
let scope = resolved
.filter(|_| snapshot.automatic_scope)
.map(|address| Self::scope_for_address(debugger, address, &snapshot.scope))
.unwrap_or_else(|| snapshot.scope.clone());
if snapshot.resolved && resolved == Some(snapshot.address) && scope == snapshot.scope {
resolved_count += 1;
continue;
}
if let Some(address) = resolved
&& let Err(error) =
Self::validate_scope_capability(client, debugger.arch(), resolved, &scope)
.and_then(|()| Self::validate_breakpoint_target(debugger, address, &scope))
.and_then(|()| self.ensure_site_available(address, false, Some(id)))
{
failures.push(format!("breakpoint {id}: {error}"));
continue;
}
if snapshot.resolved
&& snapshot.enabled
&& let Err(error) = Self::uninstall_breakpoint(client, debugger, &snapshot)
{
failures.push(format!("breakpoint {id}: {error}"));
continue;
}
let backend = match resolved {
Some(address) if snapshot.enabled => {
match Self::install_breakpoint(client, debugger, address, &scope) {
Ok(backend) => backend,
Err(install_error) => {
if snapshot.resolved
&& let Err(rollback_error) =
Self::install_existing_breakpoint(client, debugger, &snapshot)
{
failures.push(format!(
"failed to move breakpoint {id}: {install_error}; restoring its previous installation also failed: {rollback_error}"
));
continue;
}
failures.push(format!("breakpoint {id}: {install_error}"));
continue;
}
}
}
_ => BreakpointBackend::Deferred,
};
let bp = self.breakpoints.get_mut(&id).ok_or(Error::BPNotFound(id))?;
match resolved {
Some(address) => {
bp.address = address;
bp.resolved = true;
bp.scope = scope;
bp.backend = backend;
bp.symbol = Some(spec.label().to_string());
resolved_count += 1;
}
None => {
bp.resolved = false;
bp.backend = BreakpointBackend::Deferred;
}
}
}
if !failures.is_empty() {
return Err(Error::Breakpoint(failures.join("; ")));
}
Ok(resolved_count)
}
pub fn check_breakpoint_hit(&self, rip: u64, cr3: u64) -> BreakpointHitResult {
for bp in self.breakpoints.values() {
if !self.one_shot_hits.contains(&bp.id)
&& bp.resolved
&& bp.hardware.is_none()
&& bp.address.0 == rip
&& bp.enabled
&& bp.scope.matches_cr3(cr3)
{
return BreakpointHitResult::Hit(bp.clone());
}
}
BreakpointHitResult::NotBreakpoint
}
pub fn enabled_breakpoint_id_for_current_context(
&self,
debugger: &Target,
address: VirtAddr,
) -> Option<u32> {
let cr3 = debugger.current_dtb();
self.breakpoints
.values()
.filter(|bp| {
bp.resolved
&& bp.enabled
&& bp.hardware.is_none()
&& bp.address == address
&& bp.scope.matches_cr3(cr3)
})
.map(|bp| bp.id)
.min()
}
#[cfg(test)]
fn enabled_software_breakpoint_id(
&self,
scope: &BreakpointScope,
address: VirtAddr,
) -> Option<u32> {
self.breakpoints
.values()
.filter(|bp| {
bp.resolved
&& bp.enabled
&& bp.hardware.is_none()
&& bp.address == address
&& &bp.scope == scope
})
.map(|bp| bp.id)
.min()
}
pub fn mask_breakpoint_bytes(&self, start: VirtAddr, buf: &mut [u8], cr3: u64) {
let end = start.0.wrapping_add(buf.len() as u64);
for bp in self.breakpoints.values() {
if !bp.resolved || !bp.enabled || bp.hardware.is_some() || !bp.scope.matches_cr3(cr3) {
continue;
}
if bp.address.0 < start.0 || bp.address.0 >= end {
continue;
}
let offset = (bp.address.0 - start.0) as usize;
let bytes = bp.backend.original_bytes();
if offset + bytes.len() <= buf.len() {
buf[offset..offset + bytes.len()].copy_from_slice(bytes);
}
}
}
pub fn breakpoint_id_at_address(&self, rip: u64) -> Option<u32> {
self.breakpoints
.values()
.find(|bp| bp.resolved && bp.enabled && bp.hardware.is_none() && bp.address.0 == rip)
.map(|bp| bp.id)
}
fn ensure_site_available(
&self,
address: VirtAddr,
hardware: bool,
exclude_id: Option<u32>,
) -> Result<()> {
if let Some(existing) = self.breakpoints.values().find(|bp| {
Some(bp.id) != exclude_id
&& bp.resolved
&& bp.address == address
&& bp.hardware.is_some() == hardware
}) {
let kind = if hardware { "hardware" } else { "software" };
return Err(Error::Breakpoint(format!(
"{kind} breakpoint {} already owns address {:#x}",
existing.id, address.0
)));
}
Ok(())
}
fn scope_for_current_context(debugger: &Target) -> BreakpointScope {
match &debugger.current_process_info {
Some(ProcessInfo { pid, name, dtb, .. }) => BreakpointScope::Process {
pid: *pid,
dtb: *dtb,
name: name.clone(),
},
None => BreakpointScope::Kernel,
}
}
fn scope_for_address(
debugger: &Target,
address: VirtAddr,
fallback: &BreakpointScope,
) -> BreakpointScope {
const WINDOWS_X64_KERNEL_START: u64 = 0xffff_8000_0000_0000;
if address.0 >= WINDOWS_X64_KERNEL_START
|| Self::find_kernel_module_containing_address(debugger, address).is_some()
{
BreakpointScope::Kernel
} else {
fallback.clone()
}
}
fn install_breakpoint(
client: &mut dyn DebugBackend,
debugger: &Target,
address: VirtAddr,
scope: &BreakpointScope,
) -> Result<BreakpointBackend> {
if Self::install_is_target_owned(debugger.arch(), Some(address), scope) {
let memory = debugger.address_space(debugger.kernel_dtb());
let mut original = BreakpointPatch::new(breakpoint_opcode(debugger.arch()).len());
let original = match memory.read_bytes(address, original.as_mut_slice()) {
Ok(()) => Some(original),
Err(Error::BadVirtualAddress(_) | Error::AddressNotInDump(_)) => None,
Err(error) => return Err(error),
};
client.set_breakpoint(address.0)?;
return Ok(BreakpointBackend::Kernel { original });
}
let dtb = match scope {
BreakpointScope::Process { dtb, .. } => *dtb,
BreakpointScope::Kernel => debugger.kernel_dtb(),
};
let memory = debugger.address_space(dtb);
let opcode = breakpoint_opcode(debugger.arch());
let mut original = BreakpointPatch::new(opcode.len());
match memory.read_bytes(address, original.as_mut_slice()) {
Ok(()) => {}
Err(Error::BadVirtualAddress(_) | Error::AddressNotInDump(_)) => {
return Err(Self::non_resident_site_error(debugger, dtb, address, scope));
}
Err(error) => return Err(error),
}
memory.write_bytes(address, opcode)?;
client.note_breakpoint_installed(address.0);
Ok(BreakpointBackend::GuestMemoryPatch { original })
}
fn install_existing_breakpoint(
client: &mut dyn DebugBackend,
debugger: &Target,
bp: &Breakpoint,
) -> Result<()> {
match (&bp.scope, &bp.backend) {
(_, BreakpointBackend::Kernel { .. }) => client.set_breakpoint(bp.address.0),
(BreakpointScope::Process { dtb, .. }, BreakpointBackend::GuestMemoryPatch { .. }) => {
let memory = debugger.address_space(*dtb);
memory.write_bytes(bp.address, breakpoint_opcode(debugger.arch()))?;
client.note_breakpoint_installed(bp.address.0);
Ok(())
}
(_, BreakpointBackend::Hardware) => match bp.hardware {
Some(hw) => {
client.set_hardware_breakpoint(hw.slot, bp.address.0, hw.access, hw.len)
}
None => Err(Error::Breakpoint(
"hardware breakpoint missing parameters".into(),
)),
},
_ => Err(Error::Breakpoint(
"breakpoint backend/scope mismatch".into(),
)),
}
}
fn uninstall_breakpoint(
client: &mut dyn DebugBackend,
debugger: &Target,
bp: &Breakpoint,
) -> Result<()> {
match (&bp.scope, &bp.backend) {
(_, BreakpointBackend::Kernel { .. }) => client.remove_breakpoint(bp.address.0),
(
BreakpointScope::Process { dtb, .. },
BreakpointBackend::GuestMemoryPatch { original },
) => {
let memory = debugger.address_space(*dtb);
memory.write_bytes(bp.address, original.as_slice())?;
client.note_breakpoint_uninstalled(bp.address.0);
Ok(())
}
(_, BreakpointBackend::Hardware) => match bp.hardware {
Some(hw) => client.clear_hardware_breakpoint(hw.slot),
None => Err(Error::Breakpoint(
"hardware breakpoint missing parameters".into(),
)),
},
_ => Err(Error::Breakpoint(
"breakpoint backend/scope mismatch".into(),
)),
}
}
fn is_kernel_space(arch: Arch, address: VirtAddr) -> bool {
match arch {
Arch::Amd64 => looks_like_kernel_pointer(address.0),
Arch::Arm64 => address.0 & (1 << 55) != 0,
}
}
fn validate_breakpoint_target(
debugger: &Target,
address: VirtAddr,
scope: &BreakpointScope,
) -> Result<()> {
let module = Self::find_kernel_module_containing_address(debugger, address);
let dtb = match scope {
BreakpointScope::Kernel => debugger.kernel_dtb(),
BreakpointScope::Process { dtb, .. } => *dtb,
};
let memory = debugger.address_space(dtb);
let translation = match memory.virt_to_phys(address) {
Ok(Some(translation)) => Some(translation),
Ok(None) if Self::is_kernel_space(debugger.arch(), address) => None,
Ok(None) => return Err(Self::non_resident_site_error(debugger, dtb, address, scope)),
Err(error) => return Err(error),
};
let nx = match (debugger.arch(), address.0 & (1 << 55) != 0, &translation) {
(Arch::Arm64, true, _) => false,
(Arch::Arm64, false, Some(translation)) => translation.uxn,
(_, _, Some(translation)) => translation.nx,
(_, _, None) => false,
};
if nx {
let context = module
.as_ref()
.map(|module| module.short_name.as_str())
.unwrap_or("unknown");
return Err(Error::Breakpoint(format!(
"refusing breakpoint at {:#x}: target page is non-executable ({})",
address.0, context
)));
}
if let Some(module) = module {
let headers = read_pe_header_page(module.base_address, &memory)?;
let view = PeView::from_bytes(&headers)?;
let rva = address.0.saturating_sub(module.base_address.0) as u32;
let in_executable_section = view.section_headers().iter().any(|section| {
let size = section.VirtualSize.max(section.SizeOfRawData);
size != 0
&& section.Characteristics & IMAGE_SCN_MEM_EXECUTE != 0
&& rva >= section.VirtualAddress
&& rva < section.VirtualAddress.saturating_add(size)
});
if !in_executable_section {
return Err(Error::Breakpoint(format!(
"refusing breakpoint at {:#x}: address falls in non-executable section of {}",
address.0, module.short_name
)));
}
}
Ok(())
}
fn non_resident_site_error(
debugger: &Target,
dtb: Dtb,
address: VirtAddr,
scope: &BreakpointScope,
) -> Error {
match debugger.symbols.find_module_for_address(dtb, address) {
Some(module) => Error::Breakpoint(format!(
"{:#x} is in {}+{:#x} but that page is not paged in; consider using \
`ba e1 {:#x}` instead",
address.0,
ModuleInfo::derive_short_name(&module.name),
address.0.saturating_sub(module.base_address.0),
address.0,
)),
None => Error::Breakpoint(format!(
"{:#x} is not mapped in {} and no loaded image covers it",
address.0,
scope.label(),
)),
}
}
fn find_kernel_module_containing_address(
debugger: &Target,
address: VirtAddr,
) -> Option<ModuleInfo> {
debugger
.kernel_modules()
.ok()?
.into_iter()
.find(|module| module.contains_address(address))
}
}
#[derive(Debug)]
pub enum BreakpointHitResult {
Hit(Breakpoint),
NotBreakpoint,
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use crate::types::Arch;
use super::{
Breakpoint, BreakpointBackend, BreakpointConfig, BreakpointHitDisposition,
BreakpointHitResult, BreakpointManager, BreakpointPatch, BreakpointScope, BreakpointSpec,
HardwareBreakpoint,
};
use crate::dbg_backend::{DebugBackend, HwBreakpointAccess, StopEvent};
use crate::error::{Error, Result};
use crate::gdb::RegisterMap;
use crate::session::session_over_memory;
use crate::types::VirtAddr;
#[test]
fn failed_uninstall_keeps_breakpoint_managed_for_retry() {
let mut manager = BreakpointManager::new();
manager.insert_for_test(
7,
VirtAddr(0x1000),
true,
Some(HardwareBreakpoint {
access: HwBreakpointAccess::Execute,
len: 1,
slot: 0,
}),
);
let result = manager.remove_if_uninstalled(7, |_| {
Err(Error::Kd("injected hardware clear failure".into()))
});
assert!(result.is_err());
assert_eq!(manager.list().len(), 1);
assert_eq!(manager.list()[0].id, 7);
assert!(manager.has_enabled_hardware_breakpoints());
}
#[test]
fn renumber_moves_breakpoint_and_preserves_one_shot_state() {
let mut manager = BreakpointManager::new();
manager.insert_for_test(2, VirtAddr(0x2000), true, None);
manager.breakpoints.get_mut(&2).unwrap().one_shot = true;
manager.mark_one_shot_hit(2).unwrap();
manager.renumber(2, 7).unwrap();
assert!(!manager.breakpoints.contains_key(&2));
assert_eq!(manager.breakpoints.get(&7).unwrap().id, 7);
assert_eq!(manager.one_shot_hit_ids(), vec![7]);
assert!(manager.renumber(7, 7).is_ok());
manager.insert_for_test(8, VirtAddr(0x8000), true, None);
assert!(manager.renumber(7, 8).is_err());
assert!(manager.breakpoints.contains_key(&7));
}
#[test]
fn source_batch_rollback_removes_only_locations_added_by_the_batch() {
let mut manager = BreakpointManager::new();
manager.insert_for_test(1, VirtAddr(0x1000), true, None);
manager.insert_for_test(2, VirtAddr(0x2000), true, None);
manager.insert_for_test(9, VirtAddr(0x9000), true, None);
manager
.remove_ids_if_uninstalled([2, 1], |_| Ok(()))
.unwrap();
let ids: Vec<_> = manager.list().into_iter().map(|bp| bp.id).collect();
assert_eq!(ids, vec![9]);
}
#[test]
fn source_batch_rollback_reports_failed_uninstall_and_keeps_it_managed() {
let mut manager = BreakpointManager::new();
manager.insert_for_test(1, VirtAddr(0x1000), true, None);
manager.insert_for_test(2, VirtAddr(0x2000), true, None);
manager.insert_for_test(9, VirtAddr(0x9000), true, None);
let error = manager
.remove_ids_if_uninstalled([2, 1], |bp| {
if bp.id == 2 {
Err(Error::Kd("injected rollback failure".into()))
} else {
Ok(())
}
})
.unwrap_err();
assert!(error.to_string().contains("#2"));
assert!(error.to_string().contains("injected rollback failure"));
let ids: Vec<_> = manager.list().into_iter().map(|bp| bp.id).collect();
assert_eq!(ids, vec![2, 9]);
}
#[test]
fn detects_breakpoint_hit_at_exact_rip() {
let mut manager = BreakpointManager::new();
manager.breakpoints.insert(
0,
Breakpoint {
id: 0,
address: VirtAddr(0x1000),
enabled: true,
symbol: None,
spec: None,
resolved: true,
scope: BreakpointScope::Kernel,
automatic_scope: false,
condition: None,
condition_expr: None,
pass_count: 0,
hit_count: 0,
remaining_pass_count: 0,
one_shot: false,
action: None,
temporary: false,
hardware: None,
backend: BreakpointBackend::Kernel {
original: Some(BreakpointPatch::single(0x90)),
},
},
);
match manager.check_breakpoint_hit(0x1000, 0) {
BreakpointHitResult::Hit(bp) => assert_eq!(bp.id, 0),
other => panic!("unexpected result: {:?}", other),
}
}
#[test]
fn process_breakpoint_hit_requires_matching_cr3() {
let mut manager = BreakpointManager::new();
manager.breakpoints.insert(
0,
Breakpoint {
id: 0,
address: VirtAddr(0x7ff7_1234_1000),
enabled: true,
symbol: None,
spec: None,
resolved: true,
scope: BreakpointScope::Process {
pid: 42,
dtb: 0x1234_5000,
name: "user.exe".to_string(),
},
automatic_scope: false,
condition: None,
condition_expr: None,
pass_count: 0,
hit_count: 0,
remaining_pass_count: 0,
one_shot: false,
action: None,
temporary: false,
hardware: None,
backend: BreakpointBackend::GuestMemoryPatch {
original: BreakpointPatch::single(0x90),
},
},
);
assert!(matches!(
manager.check_breakpoint_hit(0x7ff7_1234_1000, 0x1234_5000),
BreakpointHitResult::Hit(_)
));
assert!(matches!(
manager.check_breakpoint_hit(0x7ff7_1234_1000, 0x1234_5fff),
BreakpointHitResult::Hit(_)
));
assert!(matches!(
manager.check_breakpoint_hit(0x7ff7_1234_1000, 0x9999_9000),
BreakpointHitResult::NotBreakpoint
));
assert!(matches!(
manager.check_breakpoint_hit(0x7ff7_1234_1000, 0x1234_4000),
BreakpointHitResult::NotBreakpoint
));
}
#[test]
fn hardware_breakpoint_is_ignored_by_int3_hit_predicates() {
let mut manager = BreakpointManager::new();
manager.insert_for_test(
0,
VirtAddr(0x2000),
true,
Some(HardwareBreakpoint {
access: HwBreakpointAccess::Write,
len: 4,
slot: 1,
}),
);
assert!(matches!(
manager.check_breakpoint_hit(0x2000, 0),
BreakpointHitResult::NotBreakpoint
));
assert_eq!(manager.breakpoint_id_at_address(0x2000), None);
}
#[test]
fn has_enabled_hardware_breakpoints_tracks_enabled_hw_bps() {
let mut manager = BreakpointManager::new();
manager.insert_for_test(0, VirtAddr(0x1000), true, None);
assert!(!manager.has_enabled_hardware_breakpoints());
manager.insert_for_test(
1,
VirtAddr(0x2000),
true,
Some(HardwareBreakpoint {
access: HwBreakpointAccess::Write,
len: 4,
slot: 1,
}),
);
assert!(manager.has_enabled_hardware_breakpoints());
manager.breakpoints.get_mut(&1).unwrap().enabled = false;
assert!(!manager.has_enabled_hardware_breakpoints());
}
#[test]
fn hardware_breakpoint_for_slot_resolves_enabled_slot_only() {
let mut manager = BreakpointManager::new();
manager.insert_for_test(
7,
VirtAddr(0x3000),
true,
Some(HardwareBreakpoint {
access: HwBreakpointAccess::ReadWrite,
len: 8,
slot: 1,
}),
);
let found = manager
.hardware_breakpoint_for_slot(1)
.expect("slot 1 hw bp");
assert_eq!(found.id, 7);
assert_eq!(found.hardware.expect("hw params").slot, 1);
assert!(manager.hardware_breakpoint_for_slot(0).is_none());
manager.insert_for_test(
8,
VirtAddr(0x4000),
false,
Some(HardwareBreakpoint {
access: HwBreakpointAccess::Write,
len: 2,
slot: 0,
}),
);
assert!(manager.hardware_breakpoint_for_slot(0).is_none());
}
#[test]
fn software_and_hardware_breakpoint_coexist_at_same_address() {
let mut manager = BreakpointManager::new();
let addr = 0x5000;
manager.insert_for_test(0, VirtAddr(addr), true, None);
manager.insert_for_test(
1,
VirtAddr(addr),
true,
Some(HardwareBreakpoint {
access: HwBreakpointAccess::Write,
len: 4,
slot: 1,
}),
);
match manager.check_breakpoint_hit(addr, 0) {
BreakpointHitResult::Hit(bp) => {
assert_eq!(bp.id, 0);
assert!(bp.hardware.is_none());
}
other => panic!("expected software hit, got {:?}", other),
}
assert_eq!(manager.breakpoint_id_at_address(addr), Some(0));
assert_eq!(
manager.enabled_software_breakpoint_id(&BreakpointScope::Kernel, VirtAddr(addr)),
Some(0)
);
manager.breakpoints.remove(&0);
assert_eq!(
manager.enabled_software_breakpoint_id(&BreakpointScope::Kernel, VirtAddr(addr)),
None
);
}
#[test]
fn pass_count_records_every_hit_and_surfaces_requested_hit() {
let mut manager = BreakpointManager::new();
manager.insert_for_test(3, VirtAddr(0x1000), true, None);
manager.set_pass_count(3, 3).unwrap();
assert_eq!(
manager.record_hit(3).unwrap(),
BreakpointHitDisposition::SkipPass
);
assert_eq!(
manager.record_hit(3).unwrap(),
BreakpointHitDisposition::SkipPass
);
assert_eq!(
manager.record_hit(3).unwrap(),
BreakpointHitDisposition::Evaluate
);
let bp = manager.list()[0];
assert_eq!(bp.hit_count, 3);
assert_eq!(bp.remaining_pass_count, 0);
}
#[test]
fn one_shot_is_hidden_after_surface_but_remains_available_for_safe_step_over() {
let mut manager = BreakpointManager::new();
manager.insert_for_test(4, VirtAddr(0x2000), true, None);
manager.set_one_shot(4, true).unwrap();
manager.mark_one_shot_hit(4).unwrap();
assert!(manager.list().is_empty());
assert_eq!(manager.breakpoint_id_at_address(0x2000), Some(4));
assert_eq!(manager.one_shot_hit_ids(), vec![4]);
manager.discard(&mut SlotRecorder::new(), 4).unwrap();
assert!(manager.one_shot_hit_ids().is_empty());
}
#[test]
fn one_unarmable_site_does_not_block_the_others() {
let session = session_over_memory(0x1000, &[0u8; 0x80]);
let dtb = session.target.current_dtb();
session.target.symbols.set_kernel(Some(1), dtb);
session.target.symbols.inject_module_for_test(
1,
Vec::new(),
&[("Refused", 0x10), ("Allowed", 0x20)],
);
session.target.symbols.inject_source_lines_for_test(
1,
dtb,
VirtAddr(0x1000),
0x1000,
"driver.c",
&[],
);
let mut manager = BreakpointManager::new();
for (id, name) in [(0u32, "driver!Refused"), (1, "driver!Allowed")] {
manager.insert_for_test(id, VirtAddr(0), true, None);
let bp = manager.breakpoints.get_mut(&id).unwrap();
bp.spec = Some(BreakpointSpec::Symbol {
name: name.to_string(),
skip_prologue: false,
});
bp.resolved = false;
bp.backend = BreakpointBackend::Deferred;
}
let mut client = SlotRecorder::refusing(0x1010);
let error = manager
.resolve_symbolic(&mut client, &session.target)
.expect_err("the refused site is reported");
assert!(
error.to_string().contains("breakpoint 0"),
"failure names the breakpoint: {error}"
);
assert!(!manager.breakpoints[&0].resolved);
let allowed = &manager.breakpoints[&1];
assert!(
allowed.resolved && allowed.address == VirtAddr(0x1020),
"the armable site still resolved: {allowed:?}"
);
}
#[test]
fn an_unreadable_kernel_site_installs_and_reports_the_owed_write() {
let session = session_over_memory(0x1000, &[0u8; 0x40]);
let address = VirtAddr(0xfffff80000001000);
let mut manager = BreakpointManager::new();
let mut client = SlotRecorder::accepting();
let id = manager
.add_configured(
&mut client,
&session.target,
address,
None,
BreakpointConfig::default(),
)
.expect("a non-resident kernel site is the target's decision");
assert_eq!(client.installed, vec![address.0]);
let breakpoint = &manager.breakpoints[&id];
assert!(
breakpoint.awaiting_page_in(),
"an unread site must report that the write is owed"
);
let mut buffer = [0xccu8; 4];
manager.mask_breakpoint_bytes(address, &mut buffer, session.target.current_dtb());
assert_eq!(buffer, [0xcc; 4], "masked bytes it never read");
}
#[test]
fn a_non_resident_user_site_is_refused_and_names_the_page() {
let session = session_over_memory(0x1000, &[0u8; 0x40]);
let dtb = session.target.current_dtb();
let mapped = VirtAddr(0x00007ff000001000);
session
.target
.symbols
.inject_source_lines_for_test(1, dtb, mapped, 0x1000, "user.c", &[]);
let scope = BreakpointScope::Process {
dtb,
pid: 4,
name: "mspaint.exe".to_string(),
};
let config = |scope: &BreakpointScope| BreakpointConfig {
scope: Some(scope.clone()),
..BreakpointConfig::default()
};
let mut manager = BreakpointManager::new();
let mut client = SlotRecorder::accepting();
let error = manager
.add_configured(&mut client, &session.target, mapped, None, config(&scope))
.expect_err("a page this debugger cannot write is not a site it can arm");
let message = error.to_string();
assert!(
message.contains("not paged in") && message.contains("ba e1"),
"the refusal must name the missing page and the mechanism that works: {message}"
);
assert!(
manager.list().is_empty(),
"a refused site must not be left registered"
);
assert!(
client.installed.is_empty(),
"a host-patched site must not enter the target's breakpoint table"
);
}
#[test]
fn kernel_space_is_classified_per_architecture() {
for (arch, kernel, user) in [
(
Arch::Amd64,
VirtAddr(0xfffff80000001000),
VirtAddr(0x00007ff000001000),
),
(
Arch::Arm64,
VirtAddr(0xffff800000001000),
VirtAddr(0x00007ff000001000),
),
] {
assert!(
BreakpointManager::is_kernel_space(arch, kernel),
"{arch:?} rejected its own kernel address {:#x}",
kernel.0
);
assert!(
!BreakpointManager::is_kernel_space(arch, user),
"{arch:?} accepted a user address {:#x} as kernel space",
user.0
);
}
assert!(!BreakpointManager::is_kernel_space(
Arch::Amd64,
VirtAddr(0x0080000000001000)
));
assert!(BreakpointManager::is_kernel_space(
Arch::Arm64,
VirtAddr(0x0080000000001000)
));
}
#[test]
fn conditions_supplied_as_text_are_enforced() {
let session = session_over_memory(0x1000, &[0u8; 0x80]);
let mut client = SlotRecorder::accepting();
for (condition, holds) in [("0", false), ("1", true)] {
let mut manager = BreakpointManager::new();
let id = manager
.add_configured(
&mut client,
&session.target,
VirtAddr(0x1000),
None,
BreakpointConfig {
condition: Some(condition.to_string()),
..BreakpointConfig::default()
},
)
.expect("configured breakpoint installs");
let bp = &manager.breakpoints[&id];
assert_eq!(
bp.evaluate_condition(&session.target).unwrap(),
holds,
"condition {condition:?} was stored but not evaluated"
);
}
}
#[test]
fn deferred_symbol_specs_apply_their_offset() {
let session = session_over_memory(0x1000, &[0u8; 0x80]);
let dtb = session.target.current_dtb();
session.target.symbols.set_kernel(Some(1), dtb);
session
.target
.symbols
.inject_module_for_test(1, Vec::new(), &[("DriverEntry", 0)]);
session.target.symbols.inject_source_lines_for_test(
1,
dtb,
VirtAddr(0x1000),
0x1000,
"driver.c",
&[],
);
let resolve = |name: &str| {
BreakpointSpec::Symbol {
name: name.to_string(),
skip_prologue: false,
}
.resolve(&session.target, dtb)
.unwrap()
};
assert_eq!(resolve("driver!DriverEntry"), Some(VirtAddr(0x1000)));
assert_eq!(resolve("driver!DriverEntry+0x29"), Some(VirtAddr(0x1029)));
assert_eq!(resolve("driver!DriverEntry+29"), Some(VirtAddr(0x1029)));
assert_eq!(resolve("driver!DriverEntry+0n16"), Some(VirtAddr(0x1010)));
assert_eq!(resolve("driver!DriverEntry-8"), Some(VirtAddr(0xff8)));
assert_eq!(resolve("driver!Missing+0x10"), None);
assert_eq!(resolve("absent!DriverEntry+0x10"), None);
}
#[test]
fn target_reload_keeps_symbolic_identity_deferred_and_drops_numeric_points() {
let mut manager = BreakpointManager::new();
manager.insert_for_test(1, VirtAddr(0x1000), true, None);
manager.insert_for_test(7, VirtAddr(0x2000), true, None);
{
let symbolic = manager.breakpoints.get_mut(&7).unwrap();
symbolic.symbol = Some("driver!Entry".into());
symbolic.spec = Some(BreakpointSpec::Symbol {
name: "driver!Entry".into(),
skip_prologue: false,
});
}
let mut backend = SlotRecorder::new();
assert_eq!(manager.prepare_target_reload(&mut backend), 1);
let bp = manager.list()[0];
assert_eq!(bp.id, 7);
assert!(bp.deferred());
assert_eq!(bp.address, VirtAddr(0x2000));
assert!(matches!(bp.backend, BreakpointBackend::Deferred));
}
#[test]
fn unloaded_symbolic_site_becomes_deferred_without_dropping_identity() {
let mut manager = BreakpointManager::new();
manager.insert_for_test(3, VirtAddr(0x3000), true, None);
manager.insert_for_test(4, VirtAddr(0x4000), true, None);
manager.breakpoints.get_mut(&3).unwrap().spec = Some(BreakpointSpec::Source {
raw: "probe.c:35".into(),
file: "probe.c".into(),
line: 35,
address_index: 0,
});
assert_eq!(
manager.defer_symbolic_sites_if(&mut SlotRecorder::new(), |bp| bp.id == 3),
1
);
let deferred = manager.breakpoints.get(&3).unwrap();
assert!(deferred.enabled);
assert!(deferred.deferred());
assert_eq!(deferred.address, VirtAddr(0x3000));
assert!(matches!(deferred.backend, BreakpointBackend::Deferred));
assert!(manager.breakpoints.get(&4).unwrap().resolved);
assert!(matches!(
manager.check_breakpoint_hit(0x3000, 0),
BreakpointHitResult::NotBreakpoint
));
}
#[test]
fn physical_breakpoint_sites_reject_same_kind_collisions() {
let mut manager = BreakpointManager::new();
let address = VirtAddr(0x4000);
manager.insert_for_test(2, address, false, None);
let error = manager
.ensure_site_available(address, false, None)
.expect_err("disabled breakpoints still own their physical site");
assert!(error.to_string().contains("breakpoint 2 already owns"));
assert!(manager.ensure_site_available(address, true, None).is_ok());
}
struct SlotRecorder {
register_map: RegisterMap,
cleared: Vec<u8>,
refused: Option<u64>,
installed: Vec<u64>,
}
impl SlotRecorder {
fn new() -> Self {
Self {
register_map: RegisterMap::default(),
cleared: Vec::new(),
refused: None,
installed: Vec::new(),
}
}
fn accepting() -> Self {
Self::refusing(u64::MAX)
}
fn refusing(address: u64) -> Self {
Self {
refused: Some(address),
..Self::new()
}
}
}
impl DebugBackend for SlotRecorder {
fn register_map(&self) -> &RegisterMap {
&self.register_map
}
fn supports_user_mode_breakpoints(&self) -> bool {
true
}
fn read_registers(&mut self) -> Result<Vec<u8>> {
Err(Error::NotSupported)
}
fn write_registers(&mut self, _data: &[u8]) -> Result<()> {
Err(Error::NotSupported)
}
fn set_breakpoint(&mut self, addr: u64) -> Result<()> {
if self.refused == Some(addr) {
return Err(Error::Kd("kernel returned NTSTATUS 0xc0000001".into()));
}
if self.refused.is_none() {
return Err(Error::NotSupported);
}
self.installed.push(addr);
Ok(())
}
fn remove_breakpoint(&mut self, _addr: u64) -> Result<()> {
Err(Error::NotSupported)
}
fn clear_hardware_breakpoint(&mut self, slot: u8) -> Result<()> {
self.cleared.push(slot);
Ok(())
}
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
}
}
#[test]
fn clear_hardware_slots_releases_every_hw_slot_and_skips_software() {
let mut manager = BreakpointManager::new();
manager.insert_for_test(
0,
VirtAddr(0x1000),
true,
Some(HardwareBreakpoint {
access: HwBreakpointAccess::Write,
len: 4,
slot: 2,
}),
);
manager.insert_for_test(
1,
VirtAddr(0x2000),
false,
Some(HardwareBreakpoint {
access: HwBreakpointAccess::Execute,
len: 1,
slot: 0,
}),
);
manager.insert_for_test(2, VirtAddr(0x3000), true, None);
let mut backend = SlotRecorder::new();
manager.clear_hardware_slots(&mut backend);
let mut cleared = backend.cleared.clone();
cleared.sort_unstable();
assert_eq!(cleared, vec![0, 2]);
}
}