use std::collections::{HashMap, hash_map::Entry};
use vmi_core::{
Gfn, Pa, Va, View, VmiCore, VmiError, VmiEvent,
arch::{Architecture, EventInterrupt, EventReason, Registers as _},
driver::{VmiRead, VmiViewControl, VmiVmControl, VmiWrite},
};
struct Breakpoint {
#[expect(unused)]
offset: u16,
original_content: Vec<u8>, references: u32,
}
struct Page {
original_gfn: Gfn,
shadow_gfn: Gfn,
view: View,
breakpoints: HashMap<u16, Breakpoint>,
}
#[derive(Default)]
pub struct Interceptor<Driver>
where
Driver: VmiRead + VmiWrite + VmiViewControl + VmiVmControl,
<Driver::Architecture as Architecture>::EventReason:
EventReason<Architecture = Driver::Architecture>,
{
pages: HashMap<(View, Gfn), Page>,
_marker: std::marker::PhantomData<Driver>,
}
impl<Driver> Interceptor<Driver>
where
Driver: VmiRead + VmiWrite + VmiViewControl + VmiVmControl,
<Driver::Architecture as Architecture>::EventReason:
EventReason<Architecture = Driver::Architecture>,
{
pub fn new() -> Self {
Self {
pages: HashMap::new(),
_marker: std::marker::PhantomData,
}
}
pub fn insert_breakpoint(
&mut self,
vmi: &VmiCore<Driver>,
address: Pa,
view: View,
) -> Result<Gfn, VmiError> {
let original_gfn = Driver::Architecture::gfn_from_pa(address);
let offset = Driver::Architecture::pa_offset(address) as usize;
debug_assert!(offset < Driver::Architecture::PAGE_SIZE as usize);
if offset + Driver::Architecture::BREAKPOINT.len()
> Driver::Architecture::PAGE_SIZE as usize
{
return Err(VmiError::OutOfBounds);
}
let page = match self.pages.entry((view, original_gfn)) {
Entry::Occupied(entry) => {
let page = entry.into_mut();
if let Some(breakpoint) = page.breakpoints.get_mut(&(offset as u16)) {
breakpoint.references += 1;
tracing::debug!(
%address,
current_count = breakpoint.references,
"breakpoint already exists"
);
return Ok(page.shadow_gfn);
}
page
}
Entry::Vacant(entry) => {
let page = Page {
original_gfn,
shadow_gfn: vmi.allocate_gfn()?,
view,
breakpoints: HashMap::new(),
};
let mut content = vec![0u8; Driver::Architecture::PAGE_SIZE as usize];
vmi.read(
Driver::Architecture::pa_from_gfn(original_gfn),
&mut content,
)?;
vmi.write(Driver::Architecture::pa_from_gfn(page.shadow_gfn), &content)?;
vmi.change_view_gfn(view, original_gfn, page.shadow_gfn)?;
tracing::debug!(
%address,
%original_gfn,
shadow_gfn = %page.shadow_gfn,
%view,
"created shadow page"
);
entry.insert(page)
}
};
let shadow_address = Driver::Architecture::pa_from_gfn(page.shadow_gfn) + offset as u64;
let mut original_content = vec![0u8; Driver::Architecture::BREAKPOINT.len()];
vmi.read(shadow_address, &mut original_content)?;
vmi.write(shadow_address, Driver::Architecture::BREAKPOINT)?;
let offset = offset as u16;
page.breakpoints.insert(
offset,
Breakpoint {
offset,
original_content,
references: 1,
},
);
Ok(page.shadow_gfn)
}
pub fn remove_breakpoint(
&mut self,
vmi: &VmiCore<Driver>,
address: Pa,
view: View,
) -> Result<Option<bool>, VmiError> {
self.remove_breakpoint_internal(vmi, address, view, false)
}
pub fn remove_breakpoint_by_force(
&mut self,
vmi: &VmiCore<Driver>,
address: Pa,
view: View,
) -> Result<Option<bool>, VmiError> {
self.remove_breakpoint_internal(vmi, address, view, true)
}
fn remove_breakpoint_internal(
&mut self,
vmi: &VmiCore<Driver>,
address: Pa,
view: View,
force: bool,
) -> Result<Option<bool>, VmiError> {
let gfn = Driver::Architecture::gfn_from_pa(address);
let offset = Driver::Architecture::pa_offset(address) as u16;
let page = match self.pages.get_mut(&(view, gfn)) {
Some(page) => page,
None => return Ok(None),
};
let breakpoint = match page.breakpoints.get_mut(&offset) {
Some(breakpoint) => breakpoint,
None => return Ok(None),
};
if !force && breakpoint.references > 1 {
breakpoint.references -= 1;
tracing::debug!(
%address,
current_count = breakpoint.references,
"breakpoint still in use"
);
return Ok(Some(false));
}
let shadow_address = Driver::Architecture::pa_from_gfn(page.shadow_gfn) + offset as u64;
vmi.write(shadow_address, &breakpoint.original_content)?;
page.breakpoints.remove(&offset);
if page.breakpoints.is_empty() {
vmi.reset_view_gfn(view, page.original_gfn)?;
}
Ok(Some(true))
}
pub fn contains_breakpoint(&self, event: &VmiEvent<Driver::Architecture>) -> bool {
let interrupt = match event.reason().as_software_breakpoint() {
Some(interrupt) => interrupt,
_ => return false,
};
let ip = Va(event.registers().instruction_pointer());
let gfn = interrupt.gfn();
let offset = Driver::Architecture::va_offset(ip) as u16;
let view = match event.view() {
Some(view) => view,
None => return false,
};
let page = match self.pages.get(&(view, gfn)) {
Some(page) => page,
None => return false,
};
if view != page.view {
return false;
}
page.breakpoints.contains_key(&offset)
}
}