1use 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
25struct Breakpoint {
30 #[expect(unused)]
31 offset: u16,
32 original_content: Vec<u8>, references: u32,
34}
35
36struct Page {
41 original_gfn: Gfn,
42 shadow_gfn: Gfn,
43 view: View,
44 breakpoints: HashMap<u16, Breakpoint>,
45}
46
47#[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 pub fn new() -> Self {
67 Self {
68 pages: HashMap::new(),
69 _marker: std::marker::PhantomData,
70 }
71 }
72
73 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 if offset + Driver::Architecture::BREAKPOINT.len()
87 > Driver::Architecture::PAGE_SIZE as usize
88 {
89 return Err(VmiError::OutOfBounds);
90 }
91
92 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 let page = Page {
114 original_gfn,
115 shadow_gfn: vmi.allocate_next_available_gfn()?,
116 view,
117 breakpoints: HashMap::new(),
118 };
119
120 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 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 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 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 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 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 let page = match self.pages.get_mut(&(view, gfn)) {
196 Some(page) => page,
197 None => return Ok(None),
198 };
199
200 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 let shadow_address = Driver::Architecture::pa_from_gfn(page.shadow_gfn) + offset as u64;
220 vmi.write(shadow_address, &breakpoint.original_content)?;
221
222 page.breakpoints.remove(&offset);
224
225 if page.breakpoints.is_empty() {
228 vmi.reset_view_gfn(view, page.original_gfn)?;
229
230 }
235
236 Ok(Some(true))
237 }
238
239 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}