#![doc = include_str!("./controller/breakpoint.md")]
#![doc = include_str!("./controller/memory.md")]
mod breakpoint;
use self::breakpoint::{ActiveBreakpoints, PendingBreakpoints};
pub use self::breakpoint::{
Breakpoint, BreakpointBuilder, BreakpointBuilderWithKey, BreakpointBuilderWithKeyTag,
BreakpointBuilderWithTag, KeyType, TagType,
};
mod controller;
use std::collections::{HashMap, HashSet, hash_map::Entry};
use vmi_core::{
AddressContext, Architecture as _, Gfn, Pa, Registers as _, Va, View, VmiCore, VmiDriver,
VmiError, VmiEvent, driver::VmiRead,
};
pub use self::controller::{BreakpointController, MemoryController, TapController};
use crate::ptm::{PageEntryUpdate, PageTableMonitorEvent};
pub struct BreakpointManager<Controller, Key = (), Tag = &'static str>
where
Controller: TapController,
Controller::Driver: VmiRead,
Key: KeyType,
Tag: TagType,
{
active_breakpoints: HashMap<(View, Gfn), ActiveBreakpoints<Key, Tag>>,
active_global_breakpoints: HashMap<(View, Va), GlobalBreakpoint>,
active_locations: HashMap<(Key, AddressContext), HashSet<(View, Gfn)>>,
active_gfns_by_view: HashMap<View, HashSet<Gfn>>,
pending_breakpoints: HashMap<(View, AddressContext), PendingBreakpoints<Key, Tag>>,
pending_ctx_by_view: HashMap<View, HashSet<AddressContext>>,
controller: Controller,
}
#[derive(Debug)]
struct GlobalBreakpoint {
root: Pa,
gfns: HashSet<Gfn>,
}
impl<Interface, Key, Tag> BreakpointManager<Interface, Key, Tag>
where
Interface: TapController,
Interface::Driver: VmiRead,
Key: KeyType,
Tag: TagType,
{
#[expect(clippy::new_without_default)]
pub fn new() -> Self {
Self {
active_breakpoints: HashMap::new(),
active_global_breakpoints: HashMap::new(),
active_locations: HashMap::new(),
active_gfns_by_view: HashMap::new(),
pending_breakpoints: HashMap::new(),
pending_ctx_by_view: HashMap::new(),
controller: Interface::new(),
}
}
pub fn insert(
&mut self,
vmi: &VmiCore<Interface::Driver>,
breakpoint: impl Into<Breakpoint<Key, Tag>>,
) -> Result<bool, VmiError> {
let breakpoint = breakpoint.into();
match vmi.translate_address(breakpoint.ctx) {
Ok(pa) => self.insert_with_hint(vmi, breakpoint, Some(pa)),
Err(VmiError::Translation(_)) => self.insert_with_hint(vmi, breakpoint, None),
Err(err) => Err(err),
}
}
pub fn insert_with_hint(
&mut self,
vmi: &VmiCore<Interface::Driver>,
breakpoint: impl Into<Breakpoint<Key, Tag>>,
pa: Option<Pa>,
) -> Result<bool, VmiError> {
let breakpoint = breakpoint.into();
let pa = match pa {
Some(pa) => pa,
None => return Ok(self.insert_pending_breakpoint(breakpoint)),
};
self.insert_active_breakpoint(vmi, breakpoint, pa)
}
pub fn remove(
&mut self,
vmi: &VmiCore<Interface::Driver>,
breakpoint: impl Into<Breakpoint<Key, Tag>>,
) -> Result<bool, VmiError> {
let breakpoint = breakpoint.into();
match vmi.translate_address(breakpoint.ctx) {
Ok(pa) => self.remove_with_hint(vmi, breakpoint, Some(pa)),
Err(VmiError::Translation(_)) => self.remove_with_hint(vmi, breakpoint, None),
Err(err) => Err(err),
}
}
pub fn remove_with_hint(
&mut self,
vmi: &VmiCore<Interface::Driver>,
breakpoint: impl Into<Breakpoint<Key, Tag>>,
pa: Option<Pa>,
) -> Result<bool, VmiError> {
let breakpoint = breakpoint.into();
let Breakpoint { ctx, view, key, .. } = breakpoint;
if self
.remove_pending_breakpoints_by_address(ctx, view)
.is_some()
{
return Ok(true);
}
let pa = match pa {
Some(pa) => pa,
None => return Ok(false),
};
let breakpoint_was_removed = self.remove_active_breakpoint(vmi, ctx, pa, key, view)?;
Ok(breakpoint_was_removed.is_some())
}
pub fn remove_by_event(
&mut self,
vmi: &VmiCore<Interface::Driver>,
event: &VmiEvent<<Interface::Driver as VmiDriver>::Architecture>,
key: Key,
) -> Result<Option<bool>, VmiError> {
let (ctx, pa, view) = match self.address_for_event(event) {
Some((ctx, pa, view)) => (ctx, pa, view),
None => return Ok(None),
};
let result = self.remove_active_breakpoint(vmi, ctx, pa, key, view)?;
let views = match self.active_locations.get(&(key, ctx)) {
Some(views) => views.clone(),
None => return Ok(result),
};
for (view, gfn) in views {
let pa = self.pa_from_gfn_and_va(gfn, ctx.va);
self.remove_active_breakpoint(vmi, ctx, pa, key, view)?;
}
Ok(result)
}
pub fn remove_by_view(
&mut self,
vmi: &VmiCore<Interface::Driver>,
view: View,
) -> Result<bool, VmiError> {
if let Some(pending_ctxs) = self.pending_ctx_by_view.remove(&view) {
for ctx in pending_ctxs {
self.remove_pending_breakpoints_by_address(ctx, view);
}
};
let gfns = match self.active_gfns_by_view.remove(&view) {
Some(gfns) => gfns,
None => return Ok(false),
};
debug_assert!(!gfns.is_empty(), "active_gfns_by_view is empty");
for gfn in gfns {
self.remove_active_breakpoints_by_location(vmi, gfn, view)?;
}
Ok(true)
}
pub fn get_by_event(
&mut self,
event: &VmiEvent<<Interface::Driver as VmiDriver>::Architecture>,
key: Key,
) -> Option<impl Iterator<Item = Breakpoint<Key, Tag>> + '_> {
let (ctx, pa, view) = self.address_for_event(event)?;
let gfn = <Interface::Driver as VmiDriver>::Architecture::gfn_from_pa(pa);
let breakpoints_by_ctx = self.active_breakpoints.get(&(view, gfn))?;
let breakpoints = breakpoints_by_ctx.get(&(key, ctx))?;
Some(breakpoints.iter().copied())
}
pub fn contains_by_event(
&self,
event: &VmiEvent<<Interface::Driver as VmiDriver>::Architecture>,
key: Key,
) -> bool {
let (ctx, pa, view) = match self.address_for_event(event) {
Some((ctx, pa, view)) => (ctx, pa, view),
None => return false,
};
let gfn = <Interface::Driver as VmiDriver>::Architecture::gfn_from_pa(pa);
let breakpoints = match self.active_breakpoints.get(&(view, gfn)) {
Some(breakpoints) => breakpoints,
None => return false,
};
breakpoints.contains_key(&(key, ctx))
}
pub fn contains_by_address(&self, ctx: impl Into<AddressContext>, key: Key) -> bool {
let ctx = ctx.into();
self.active_locations.contains_key(&(key, ctx))
}
pub fn clear(&mut self, vmi: &VmiCore<Interface::Driver>) -> Result<(), VmiError> {
let mut to_remove = Vec::new();
for (&(view, gfn), breakpoints) in &self.active_breakpoints {
for &(key, ctx) in breakpoints.keys() {
let pa = self.pa_from_gfn_and_va(gfn, ctx.va);
to_remove.push((key, view, pa, ctx));
}
}
self.pending_breakpoints.clear();
for (key, view, pa, ctx) in to_remove {
if let Err(err) = self.remove_active_breakpoint(vmi, ctx, pa, key, view) {
tracing::error!(
%err, %pa, %ctx, %view, ?key,
"failed to remove breakpoint"
);
}
}
debug_assert!(self.active_breakpoints.is_empty());
debug_assert!(self.active_global_breakpoints.is_empty());
debug_assert!(self.active_locations.is_empty());
debug_assert!(self.active_gfns_by_view.is_empty());
debug_assert!(self.pending_breakpoints.is_empty());
debug_assert!(self.pending_ctx_by_view.is_empty());
Ok(())
}
pub fn handle_ptm_event(
&mut self,
vmi: &VmiCore<Interface::Driver>,
event: &PageTableMonitorEvent,
) -> Result<bool, VmiError> {
match event {
PageTableMonitorEvent::PageIn(update) => self.handle_page_in(vmi, update),
PageTableMonitorEvent::PageOut(update) => self.handle_page_out(vmi, update),
}
}
pub fn handle_ptm_events(
&mut self,
vmi: &VmiCore<Interface::Driver>,
events: impl IntoIterator<Item = PageTableMonitorEvent>,
) -> Result<bool, VmiError> {
let mut updated = false;
for event in events {
updated |= self.handle_ptm_event(vmi, &event)?;
}
Ok(updated)
}
fn handle_page_in(
&mut self,
vmi: &VmiCore<Interface::Driver>,
update: &PageEntryUpdate,
) -> Result<bool, VmiError> {
tracing::trace!(?update, "page-in");
let ctx = update.ctx;
let view = update.view;
let pa = update.pa;
let breakpoints = match self.remove_pending_breakpoints_by_address(ctx, view) {
Some(breakpoints) => breakpoints,
None => return Ok(false),
};
for breakpoint in breakpoints {
self.insert_active_breakpoint(vmi, breakpoint, pa)?;
}
Ok(true)
}
fn handle_page_out(
&mut self,
vmi: &VmiCore<Interface::Driver>,
update: &PageEntryUpdate,
) -> Result<bool, VmiError> {
tracing::trace!(?update, "page-out");
let gfn = <Interface::Driver as VmiDriver>::Architecture::gfn_from_pa(update.pa);
let view = update.view;
let breakpoints_by_ctx = match self.remove_active_breakpoints_by_location(vmi, gfn, view)? {
Some(breakpoints_by_ctx) => breakpoints_by_ctx,
None => return Ok(false),
};
for breakpoint in breakpoints_by_ctx.into_values().flatten() {
self.insert_pending_breakpoint(breakpoint);
}
Ok(true)
}
fn insert_active_breakpoint(
&mut self,
vmi: &VmiCore<Interface::Driver>,
breakpoint: Breakpoint<Key, Tag>,
pa: Pa,
) -> Result<bool, VmiError> {
let Breakpoint {
mut ctx,
view,
global,
key,
tag,
} = breakpoint;
let gfn = <Interface::Driver as VmiDriver>::Architecture::gfn_from_pa(pa);
if global {
self.register_global_breakpoint(gfn, view, &mut ctx);
}
let (breakpoint_was_inserted, page_was_inserted) =
match self.active_breakpoints.entry((view, gfn)) {
Entry::Occupied(mut entry) => {
let breakpoints = entry.get_mut();
match breakpoints.entry((key, ctx)) {
Entry::Occupied(mut entry) => {
let breakpoints = entry.get_mut();
breakpoints.insert(breakpoint);
(false, false)
}
Entry::Vacant(entry) => {
entry.insert(HashSet::from([breakpoint]));
(true, false)
}
}
}
Entry::Vacant(entry) => {
entry.insert(HashMap::from([((key, ctx), HashSet::from([breakpoint]))]));
(true, true)
}
};
if breakpoint_was_inserted {
tracing::debug!(
active = self.active_breakpoints.len(),
%gfn, %ctx, %view, %global, ?key, ?tag,
"active breakpoint inserted"
);
self.install_breakpoint(vmi, pa, view, key, ctx)?;
if page_was_inserted {
self.monitor_page_for_changes(vmi, gfn, view)?;
}
}
else {
debug_assert!(
self.active_locations.contains_key(&(key, ctx)),
"desynchronized active breakpoints and monitored gfns"
);
}
Ok(breakpoint_was_inserted)
}
fn remove_active_breakpoint(
&mut self,
vmi: &VmiCore<Interface::Driver>,
ctx: impl Into<AddressContext>,
pa: Pa,
key: Key,
view: View,
) -> Result<Option<bool>, VmiError> {
let gfn = <Interface::Driver as VmiDriver>::Architecture::gfn_from_pa(pa);
let mut gfn_entry = match self.active_breakpoints.entry((view, gfn)) {
Entry::Occupied(gfn_entry) => gfn_entry,
Entry::Vacant(_) => return Ok(None),
};
debug_assert!(
self.active_gfns_by_view.contains_key(&view)
&& self.active_gfns_by_view[&view].contains(&gfn),
"desynchronized active_breakpoints and active_gfns_by_view"
);
let ctx = ctx.into();
let breakpoints_by_ctx = gfn_entry.get_mut();
let breakpoints = match breakpoints_by_ctx.remove(&(key, ctx)) {
Some(breakpoints) => breakpoints,
None => {
if !self.active_locations.contains_key(&(key, ctx)) {
tracing::debug!(
%gfn, %ctx, %view, ?key,
"breakpoint not found for key"
);
}
else {
tracing::error!(
%gfn, %ctx, %view, ?key,
"breakpoint not found for key"
);
}
debug_assert!(
!self.active_locations.contains_key(&(key, ctx)),
"desynchronized active_breakpoints and active_locations"
);
return Ok(Some(false));
}
};
let last_breakpoint_removed = breakpoints_by_ctx.is_empty();
if last_breakpoint_removed {
tracing::debug!(
%gfn, %ctx, %view, ?key, ?breakpoints,
"breakpoint removed"
);
gfn_entry.remove();
}
else {
tracing::debug!(
%gfn, %ctx, %view, ?key,
remaining = breakpoints_by_ctx.len(),
"breakpoint still in use"
);
}
self.uninstall_breakpoint(vmi, pa, view, key, ctx)?;
if last_breakpoint_removed {
self.unmonitor_page_for_changes(vmi, gfn, view)?;
}
Ok(Some(last_breakpoint_removed))
}
fn remove_active_breakpoints_by_location(
&mut self,
vmi: &VmiCore<Interface::Driver>,
gfn: Gfn,
view: View,
) -> Result<Option<ActiveBreakpoints<Key, Tag>>, VmiError> {
let breakpoints = match self.active_breakpoints.remove(&(view, gfn)) {
Some(breakpoints) => breakpoints,
None => return Ok(None),
};
for &(key, ctx) in breakpoints.keys() {
let pa = self.pa_from_gfn_and_va(gfn, ctx.va);
self.uninstall_breakpoint(vmi, pa, view, key, ctx)?;
}
tracing::debug!(
active = self.active_breakpoints.len(),
%gfn,
%view,
?breakpoints,
"active breakpoints removed"
);
self.unmonitor_page_for_changes(vmi, gfn, view)?;
Ok(Some(breakpoints))
}
fn insert_pending_breakpoint(&mut self, breakpoint: Breakpoint<Key, Tag>) -> bool {
let Breakpoint {
ctx,
view,
global,
key,
tag,
..
} = breakpoint;
let result = self
.pending_breakpoints
.entry((view, ctx))
.or_default()
.insert(breakpoint);
self.pending_ctx_by_view
.entry(view)
.or_default()
.insert(ctx);
tracing::debug!(
pending = self.pending_breakpoints.len(),
%ctx,
%view,
%global,
?key,
?tag,
"pending breakpoint inserted"
);
result
}
fn remove_pending_breakpoints_by_address(
&mut self,
ctx: AddressContext,
view: View,
) -> Option<PendingBreakpoints<Key, Tag>> {
let breakpoints = self.pending_breakpoints.remove(&(view, ctx))?;
match self.pending_ctx_by_view.entry(view) {
Entry::Occupied(mut entry) => {
let addresses = entry.get_mut();
let address_was_removed = addresses.remove(&ctx);
debug_assert!(
address_was_removed,
"desynchronized pending_breakpoints and pending_ctx_by_view"
);
if addresses.is_empty() {
entry.remove();
}
}
Entry::Vacant(_) => {
}
}
tracing::debug!(
pending = self.pending_breakpoints.len(),
%ctx,
?breakpoints,
"pending breakpoints removed"
);
Some(breakpoints)
}
fn register_global_breakpoint(&mut self, gfn: Gfn, view: View, ctx: &mut AddressContext) {
match self.active_global_breakpoints.entry((view, ctx.va)) {
Entry::Occupied(mut entry) => {
let global_breakpoint = entry.get_mut();
let gfn_was_inserted = global_breakpoint.gfns.insert(gfn);
debug_assert!(
gfn_was_inserted,
"trying to register a global breakpoint that is already registered"
);
ctx.root = global_breakpoint.root;
}
Entry::Vacant(entry) => {
entry.insert(GlobalBreakpoint {
root: ctx.root,
gfns: HashSet::from([gfn]),
});
}
}
}
fn unregister_global_breakpoint(
&mut self,
gfn: Gfn,
view: View,
ctx: AddressContext,
) -> Option<bool> {
match self.active_global_breakpoints.entry((view, ctx.va)) {
Entry::Occupied(mut entry) => {
let global_breakpoint = entry.get_mut();
let page_was_removed = global_breakpoint.gfns.remove(&gfn);
debug_assert!(
page_was_removed,
"trying to unregister a global breakpoint that is not registered"
);
if !global_breakpoint.gfns.is_empty() {
return Some(false);
}
entry.remove();
Some(true)
}
Entry::Vacant(_) => None,
}
}
fn insert_monitored_location(&mut self, gfn: Gfn, view: View) {
debug_assert!(
self.active_breakpoints.contains_key(&(view, gfn)),
"breakpoint must be in active_breakpoints before monitoring"
);
let gfn_was_inserted = self
.active_gfns_by_view
.entry(view)
.or_default()
.insert(gfn);
debug_assert!(
gfn_was_inserted,
"trying to monitor an already monitored GFN"
);
}
fn remove_monitored_location(&mut self, gfn: Gfn, view: View) {
debug_assert!(
!self.active_breakpoints.contains_key(&(view, gfn)),
"breakpoint must be removed from active_breakpoints before unmonitoring"
);
match self.active_gfns_by_view.entry(view) {
Entry::Occupied(mut entry) => {
let gfns = entry.get_mut();
let gfn_was_present = gfns.remove(&gfn);
debug_assert!(gfn_was_present, "trying to unmonitor a non-monitored gfn");
if gfns.is_empty() {
entry.remove();
}
}
Entry::Vacant(_) => {
}
}
}
fn monitor_page_for_changes(
&mut self,
vmi: &VmiCore<Interface::Driver>,
gfn: Gfn,
view: View,
) -> Result<(), VmiError> {
self.insert_monitored_location(gfn, view);
self.controller.monitor(vmi, gfn, view)
}
fn unmonitor_page_for_changes(
&mut self,
vmi: &VmiCore<Interface::Driver>,
gfn: Gfn,
view: View,
) -> Result<(), VmiError> {
self.remove_monitored_location(gfn, view);
match self.controller.unmonitor(vmi, gfn, view) {
Ok(()) => Ok(()),
Err(VmiError::ViewNotFound) => {
Ok(())
}
Err(err) => Err(err),
}
}
fn install_breakpoint(
&mut self,
vmi: &VmiCore<Interface::Driver>,
pa: Pa,
view: View,
key: Key,
ctx: AddressContext,
) -> Result<(), VmiError> {
let gfn = <Interface::Driver as VmiDriver>::Architecture::gfn_from_pa(pa);
let view_gfn_was_inserted = self
.active_locations
.entry((key, ctx))
.or_default()
.insert((view, gfn));
debug_assert!(
view_gfn_was_inserted,
"trying to install a breakpoint that is already installed"
);
self.controller.insert_breakpoint(vmi, pa, view)
}
fn uninstall_breakpoint(
&mut self,
vmi: &VmiCore<Interface::Driver>,
pa: Pa,
view: View,
key: Key,
ctx: AddressContext,
) -> Result<(), VmiError> {
let gfn = <Interface::Driver as VmiDriver>::Architecture::gfn_from_pa(pa);
self.unregister_global_breakpoint(gfn, view, ctx);
match self.active_locations.entry((key, ctx)) {
Entry::Occupied(mut entry) => {
let view_gfns = entry.get_mut();
let view_gfn_was_removed = view_gfns.remove(&(view, gfn));
debug_assert!(
view_gfn_was_removed,
"trying to uninstall a breakpoint that is not installed"
);
if view_gfns.is_empty() {
entry.remove();
}
}
Entry::Vacant(_) => {
panic!("trying to uninstall a breakpoint that is not installed");
}
}
match self.controller.remove_breakpoint(vmi, pa, view) {
Ok(()) => Ok(()),
Err(VmiError::ViewNotFound) => {
Ok(())
}
Err(err) => Err(err),
}
}
fn pa_from_gfn_and_va(&self, gfn: Gfn, va: Va) -> Pa {
<Interface::Driver as VmiDriver>::Architecture::pa_from_gfn(gfn)
+ <Interface::Driver as VmiDriver>::Architecture::va_offset(va)
}
fn address_for_event(
&self,
event: &VmiEvent<<Interface::Driver as VmiDriver>::Architecture>,
) -> Option<(AddressContext, Pa, View)> {
let (view, gfn) = match self.controller.check_event(event) {
Some((view, gfn)) => (view, gfn),
None => return None,
};
let ip = Va(event.registers().instruction_pointer());
let pa = self.pa_from_gfn_and_va(gfn, ip);
let root = match self.active_global_breakpoints.get(&(view, ip)) {
Some(global_breakpoint) => global_breakpoint.root,
None => event.registers().translation_root(ip),
};
let ctx = AddressContext::new(ip, root);
Some((ctx, pa, view))
}
}