vmi_utils/
interceptor.rs

1//! Simple software breakpoint management.
2//!
3//! Provides the fundamental mechanisms for inserting and removing breakpoint
4//! instructions in guest memory using shadow pages. It serves as a building
5//! block for higher-level breakpoint management systems like the
6//! [`BreakpointController`].
7//!
8//! When a breakpoint is inserted, the `Interceptor`:
9//! - Creates a shadow copy of the target page
10//! - Replaces the target instruction with a breakpoint instruction
11//! - Remaps the guest's view to the shadow page
12//!
13//! The original page content is preserved, allowing the `Interceptor` to
14//! restore the original state when breakpoints are removed.
15//!
16//! [`BreakpointController`]: crate::bpm::BreakpointController
17
18use std::collections::{HashMap, hash_map::Entry};
19
20use vmi_core::{
21    Gfn, Pa, Va, View, VmiCore, VmiDriver, VmiError, VmiEvent,
22    arch::{Architecture, EventInterrupt, EventReason, Registers as _},
23};
24
25/// A single breakpoint within a page.
26///
27/// Stores the original content that was replaced by the breakpoint instruction
28/// and tracks the number of references to this breakpoint location.
29struct Breakpoint {
30    #[expect(unused)]
31    offset: u16,
32    original_content: Vec<u8>, // until [u8; Arch::BREAKPOINT.len()] is allowed
33    references: u32,
34}
35
36/// A memory page containing one or more breakpoints.
37///
38/// Maintains the mapping between original and shadow pages, along with all
39/// breakpoint locations within the page.
40struct Page {
41    original_gfn: Gfn,
42    shadow_gfn: Gfn,
43    view: View,
44    breakpoints: HashMap<u16, Breakpoint>,
45}
46
47/// Core implementation of software breakpoint handling.
48#[derive(Default)]
49pub struct Interceptor<Driver>
50where
51    Driver: VmiDriver,
52    <Driver::Architecture as Architecture>::EventReason:
53        EventReason<Architecture = Driver::Architecture>,
54{
55    pages: HashMap<(View, Gfn), Page>,
56    _marker: std::marker::PhantomData<Driver>,
57}
58
59impl<Driver> Interceptor<Driver>
60where
61    Driver: VmiDriver,
62    <Driver::Architecture as Architecture>::EventReason:
63        EventReason<Architecture = Driver::Architecture>,
64{
65    /// Creates a new `Interceptor`.
66    pub fn new() -> Self {
67        Self {
68            pages: HashMap::new(),
69            _marker: std::marker::PhantomData,
70        }
71    }
72
73    /// Inserts a breakpoint at the given address.
74    pub fn insert_breakpoint(
75        &mut self,
76        vmi: &VmiCore<Driver>,
77        address: Pa,
78        view: View,
79    ) -> Result<Gfn, VmiError> {
80        let original_gfn = Driver::Architecture::gfn_from_pa(address);
81        let offset = Driver::Architecture::pa_offset(address) as usize;
82
83        debug_assert!(offset < Driver::Architecture::PAGE_SIZE as usize);
84
85        // Check if the breakpoint doesn't cross a page boundary.
86        if offset + Driver::Architecture::BREAKPOINT.len()
87            > Driver::Architecture::PAGE_SIZE as usize
88        {
89            return Err(VmiError::OutOfBounds);
90        }
91
92        // Check if the page already has a breakpoint.
93        let page = match self.pages.entry((view, original_gfn)) {
94            Entry::Occupied(entry) => {
95                let page = entry.into_mut();
96
97                if let Some(breakpoint) = page.breakpoints.get_mut(&(offset as u16)) {
98                    breakpoint.references += 1;
99
100                    tracing::debug!(
101                        %address,
102                        current_count = breakpoint.references,
103                        "breakpoint already exists"
104                    );
105
106                    return Ok(page.shadow_gfn);
107                }
108
109                page
110            }
111            Entry::Vacant(entry) => {
112                // Create a shadow page for the original page.
113                let page = Page {
114                    original_gfn,
115                    shadow_gfn: vmi.allocate_next_available_gfn()?,
116                    view,
117                    breakpoints: HashMap::new(),
118                };
119
120                // Copy the content of the original page to the shadow page.
121                let mut content = vec![0u8; Driver::Architecture::PAGE_SIZE as usize];
122                vmi.read(
123                    Driver::Architecture::pa_from_gfn(original_gfn),
124                    &mut content,
125                )?;
126                vmi.write(Driver::Architecture::pa_from_gfn(page.shadow_gfn), &content)?;
127
128                // Change the view of the original page to the shadow page.
129                vmi.change_view_gfn(view, original_gfn, page.shadow_gfn)?;
130
131                tracing::debug!(
132                    %address,
133                    %original_gfn,
134                    shadow_gfn = %page.shadow_gfn,
135                    %view,
136                    "created shadow page"
137                );
138
139                entry.insert(page)
140            }
141        };
142
143        let shadow_address = Driver::Architecture::pa_from_gfn(page.shadow_gfn) + offset as u64;
144
145        // Replace the original content with a breakpoint instruction.
146        let mut original_content = vec![0u8; Driver::Architecture::BREAKPOINT.len()];
147        vmi.read(shadow_address, &mut original_content)?;
148        vmi.write(shadow_address, Driver::Architecture::BREAKPOINT)?;
149
150        // Save the original content of the breakpoint.
151        let offset = offset as u16;
152        page.breakpoints.insert(
153            offset,
154            Breakpoint {
155                offset,
156                original_content,
157                references: 1,
158            },
159        );
160
161        Ok(page.shadow_gfn)
162    }
163
164    /// Removes a breakpoint at the given address.
165    pub fn remove_breakpoint(
166        &mut self,
167        vmi: &VmiCore<Driver>,
168        address: Pa,
169        view: View,
170    ) -> Result<Option<bool>, VmiError> {
171        self.remove_breakpoint_internal(vmi, address, view, false)
172    }
173
174    /// Removes a breakpoint at the given address by force.
175    pub fn remove_breakpoint_by_force(
176        &mut self,
177        vmi: &VmiCore<Driver>,
178        address: Pa,
179        view: View,
180    ) -> Result<Option<bool>, VmiError> {
181        self.remove_breakpoint_internal(vmi, address, view, true)
182    }
183
184    fn remove_breakpoint_internal(
185        &mut self,
186        vmi: &VmiCore<Driver>,
187        address: Pa,
188        view: View,
189        force: bool,
190    ) -> Result<Option<bool>, VmiError> {
191        let gfn = Driver::Architecture::gfn_from_pa(address);
192        let offset = Driver::Architecture::pa_offset(address) as u16;
193
194        // Check if the page has any breakpoints.
195        let page = match self.pages.get_mut(&(view, gfn)) {
196            Some(page) => page,
197            None => return Ok(None),
198        };
199
200        // Check if the breakpoint at the given offset exists.
201        let breakpoint = match page.breakpoints.get_mut(&offset) {
202            Some(breakpoint) => breakpoint,
203            None => return Ok(None),
204        };
205
206        if !force && breakpoint.references > 1 {
207            breakpoint.references -= 1;
208
209            tracing::debug!(
210                %address,
211                current_count = breakpoint.references,
212                "breakpoint still in use"
213            );
214
215            return Ok(Some(false));
216        }
217
218        // Restore the original content of the shadow page at the given offset.
219        let shadow_address = Driver::Architecture::pa_from_gfn(page.shadow_gfn) + offset as u64;
220        vmi.write(shadow_address, &breakpoint.original_content)?;
221
222        // Remove the breakpoint from the page.
223        page.breakpoints.remove(&offset);
224
225        // If the page has no more breakpoints, reset the view of the page to
226        // the original page.
227        if page.breakpoints.is_empty() {
228            vmi.reset_view_gfn(view, page.original_gfn)?;
229
230            // Free the shadow page.
231            // TODO: figure out why it's not working
232            //self.vmi.free_gfn(page.new_gfn)?;
233            //self.pages.remove(&(view, gfn));
234        }
235
236        Ok(Some(true))
237    }
238
239    /// Checks if the given event was caused by a breakpoint managed by the
240    /// [`Interceptor`].
241    pub fn contains_breakpoint(&self, event: &VmiEvent<Driver::Architecture>) -> bool {
242        let interrupt = match event.reason().as_software_breakpoint() {
243            Some(interrupt) => interrupt,
244            _ => return false,
245        };
246
247        let ip = Va(event.registers().instruction_pointer());
248
249        let gfn = interrupt.gfn();
250        let offset = Driver::Architecture::va_offset(ip) as u16;
251
252        let view = match event.view() {
253            Some(view) => view,
254            None => return false,
255        };
256
257        let page = match self.pages.get(&(view, gfn)) {
258            Some(page) => page,
259            None => return false,
260        };
261
262        if view != page.view {
263            return false;
264        }
265
266        page.breakpoints.contains_key(&offset)
267    }
268}