sys_rs/breakpoint.rs
1use libc::c_long;
2use nix::{errno::Errno, sys::ptrace, unistd::Pid};
3use std::{collections::HashMap, fmt, mem::size_of};
4
5use crate::{
6 diag::{Error, Result},
7 hwaccess::Registers,
8};
9
10const INT3: u8 = 0xcc;
11
12fn set_byte_in_word(word: c_long, offset: usize, byte: u8) -> c_long {
13 #[allow(clippy::cast_sign_loss)]
14 let mut w = word as u64;
15 let shift = offset * 8;
16
17 w &= !(0xffu64 << shift);
18 w |= u64::from(byte) << shift;
19
20 #[allow(clippy::cast_possible_wrap)]
21 let ret = w as c_long;
22
23 ret
24}
25
26struct Active {
27 id: Option<u64>,
28 byte: u8,
29 temporary: bool,
30}
31
32impl Active {
33 fn new(id: Option<u64>, byte: u8, temporary: bool) -> Self {
34 Self {
35 id,
36 byte,
37 temporary,
38 }
39 }
40
41 fn restore_byte(&self, pid: Pid, addr: u64) -> Result<()> {
42 let aligned = addr & !(size_of::<c_long>() as u64 - 1);
43 let word = ptrace::read(pid, aligned as ptrace::AddressType)? as c_long;
44
45 let offset = usize::try_from(addr - aligned)?;
46 let restored = set_byte_in_word(word, offset, self.byte);
47 ptrace::write(pid, aligned as ptrace::AddressType, restored)?;
48
49 Ok(())
50 }
51}
52
53/// Represents a breakpoint event that needs to be processed by the tracer.
54///
55/// When a permanent breakpoint is hit we return a `Pending` value containing
56/// the breakpoint id (if assigned) and the address where it was hit. This
57/// allows the tracer to re-install or re-register the breakpoint as needed.
58pub struct Pending {
59 id: Option<u64>,
60 address: u64,
61}
62
63impl Pending {
64 #[must_use]
65 /// Create a new `Pending` event.
66 ///
67 /// # Arguments
68 ///
69 /// * `id` - Optional breakpoint id assigned by the manager.
70 /// * `address` - Address where the breakpoint was hit.
71 ///
72 /// # Returns
73 ///
74 /// A newly-created `Pending` event.
75 pub fn new(id: Option<u64>, address: u64) -> Self {
76 Self { id, address }
77 }
78
79 #[must_use]
80 /// Return the optional breakpoint id associated with this pending event.
81 ///
82 /// # Returns
83 ///
84 /// The optional breakpoint id assigned by the manager, or `None` if the
85 /// breakpoint was not registered.
86 pub fn id(&self) -> Option<u64> {
87 self.id
88 }
89
90 #[must_use]
91 /// Return the address where the breakpoint was hit.
92 ///
93 /// # Returns
94 ///
95 /// The instruction address where the breakpoint occurred.
96 pub fn address(&self) -> u64 {
97 self.address
98 }
99}
100
101/// Breakpoint manager that owns breakpoint metadata for a traced process.
102///
103/// `Manager` keeps track of installed software breakpoints (INT3) and the
104/// original bytes they replaced. It provides helpers to install, remove and
105/// temporarily save/restore breakpoints. All ptrace operations are performed
106/// by this manager and therefore require the tracee to be stopped when called.
107pub struct Manager {
108 pid: Pid,
109 next_id: u64,
110 saved: Option<u64>,
111 breakpoints: HashMap<u64, Active>,
112}
113
114impl Manager {
115 #[must_use]
116 /// Create a new `Manager` for `pid`.
117 ///
118 /// # Arguments
119 ///
120 /// * `pid` - PID of the traced process this manager will operate on.
121 ///
122 /// # Returns
123 ///
124 /// A new `Manager` ready to install and manage breakpoints for `pid`.
125 pub fn new(pid: Pid) -> Self {
126 Self {
127 pid,
128 next_id: 1,
129 saved: None,
130 breakpoints: HashMap::new(),
131 }
132 }
133
134 fn install_breakpoint(&self, addr: u64) -> Result<u8> {
135 let aligned = addr & !(size_of::<c_long>() as u64 - 1);
136 let offset = usize::try_from(addr - aligned)?;
137
138 let word = ptrace::read(self.pid, aligned as ptrace::AddressType)? as c_long;
139
140 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
141 let byte = ((word as u64) >> (8 * offset)) as u8;
142
143 let patched = set_byte_in_word(word, offset, INT3);
144 ptrace::write(self.pid, aligned as ptrace::AddressType, patched)?;
145
146 Ok(byte)
147 }
148
149 /// Set a breakpoint at `addr`.
150 ///
151 /// # Arguments
152 ///
153 /// * `addr` - The address where the breakpoint should be set.
154 /// * `temporary` - If true the breakpoint is considered temporary and
155 /// won't be returned as a `Pending` event when hit.
156 /// * `registered` - If true allocate or use an id and persist the
157 /// breakpoint in the manager's table. If false the breakpoint is not
158 /// assigned an id.
159 /// * `id` - Optionally provide an explicit id to use when `registered` is
160 /// true.
161 ///
162 /// # Errors
163 ///
164 /// Returns an error if a ptrace read/write fails while patching memory.
165 ///
166 /// # Returns
167 ///
168 /// Returns `Ok(Some(id))` when the breakpoint is registered and has an id,
169 /// `Ok(None)` when it is not registered.
170 pub fn set_breakpoint(
171 &mut self,
172 addr: u64,
173 temporary: bool,
174 registered: bool,
175 id: Option<u64>,
176 ) -> Result<Option<u64>> {
177 if let Some(bp) = self.breakpoints.get(&addr) {
178 let new_id = bp.id;
179 if bp.temporary != temporary {
180 self.breakpoints
181 .insert(addr, Active::new(new_id, bp.byte, temporary));
182 }
183
184 Ok(new_id)
185 } else {
186 let byte = self.install_breakpoint(addr)?;
187
188 let new_id = if registered {
189 id.or_else(|| {
190 let ret = self.next_id;
191 self.next_id = self.next_id.wrapping_add(1);
192 Some(ret)
193 })
194 } else {
195 None
196 };
197
198 self.breakpoints
199 .insert(addr, Active::new(new_id, byte, temporary));
200 Ok(new_id)
201 }
202 }
203
204 /// Handle a breakpoint stop at RIP-1.
205 ///
206 /// When the tracee hits an INT3 the RIP points at the instruction after
207 /// the breakpoint; this method restores the original byte, rewrites RIP
208 /// to point at the original instruction and writes the registers back.
209 ///
210 /// # Arguments
211 ///
212 /// * `regs` - Mutable register snapshot for the stopped tracee.
213 ///
214 /// # Errors
215 ///
216 /// Returns an error if ptrace operations fail while restoring the
217 /// original instruction or writing registers.
218 ///
219 /// # Returns
220 ///
221 /// Returns `Ok(Some(Pending))` for non-temporary breakpoints that need
222 /// further processing (reinstallation), or `Ok(None)` when nothing needs
223 /// to be done.
224 pub fn handle_breakpoint(
225 &mut self,
226 regs: &mut Registers,
227 ) -> Result<Option<Pending>> {
228 let mut ret = None;
229
230 let addr = regs.rip() - 1;
231 if let Some(bp) = self.breakpoints.remove(&addr) {
232 bp.restore_byte(self.pid, addr)?;
233 regs.set_rip(addr);
234 regs.write()?;
235
236 if !bp.temporary {
237 ret = Some(Pending::new(bp.id, addr));
238 }
239 }
240
241 Ok(ret)
242 }
243
244 /// Delete a registered breakpoint by `id`.
245 ///
246 /// # Arguments
247 ///
248 /// * `id` - The identifier of the breakpoint to delete.
249 ///
250 /// # Errors
251 ///
252 /// Returns `ENODATA` when no breakpoint with the given id exists or when
253 /// the ptrace write to restore the original byte fails.
254 ///
255 /// # Returns
256 ///
257 /// Returns `Ok(())` on success; returns `Err` if no matching breakpoint
258 /// exists or if restoring the original byte fails.
259 pub fn delete_breakpoint(&mut self, id: u64) -> Result<()> {
260 let addr = self
261 .breakpoints
262 .iter()
263 .find_map(
264 |(addr, bp)| if bp.id == Some(id) { Some(*addr) } else { None },
265 )
266 .ok_or_else(|| Error::from(Errno::ENODATA))?;
267
268 if let Some(bp) = self.breakpoints.remove(&addr) {
269 bp.restore_byte(self.pid, addr)
270 } else {
271 Err(Error::from(Errno::ENODATA))
272 }
273 }
274
275 /// Temporarily remove (save) the breakpoint at `addr`.
276 ///
277 /// This is used when single-stepping over an instruction that previously
278 /// had an INT3 installed: we restore the original byte so the single
279 /// step executes the original instruction. The manager records `addr` in
280 /// its `saved` slot so `restore_breakpoint` can re-install it later.
281 ///
282 /// # Arguments
283 ///
284 /// * `addr` - Address of the breakpoint to temporarily remove (save).
285 ///
286 /// # Errors
287 ///
288 /// Returns `EBUSY` if there is already a saved breakpoint in progress.
289 /// Returns an error if the underlying ptrace restore fails.
290 pub fn save_breakpoint(&mut self, addr: u64) -> Result<()> {
291 self.saved
292 .is_none()
293 .then_some(())
294 .ok_or_else(|| Error::from(Errno::EBUSY))?;
295
296 if let Some(bp) = self.breakpoints.get(&addr) {
297 bp.restore_byte(self.pid, addr)?;
298 }
299
300 self.saved = Some(addr);
301 Ok(())
302 }
303
304 /// Reinstall a previously saved breakpoint (if any).
305 ///
306 /// # Errors
307 ///
308 /// Returns an error if reinstallation via ptrace write fails.
309 ///
310 /// # Returns
311 ///
312 /// Returns `Ok(())` on success. If there was no saved breakpoint this
313 /// method is a no-op and still returns `Ok(())`.
314 pub fn restore_breakpoint(&mut self) -> Result<()> {
315 if let Some(addr) = self.saved.take() {
316 if self.breakpoints.contains_key(&addr) {
317 self.install_breakpoint(addr)?;
318 }
319 }
320
321 Ok(())
322 }
323}
324
325impl fmt::Display for Manager {
326 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327 let mut items: Vec<(u64, bool, u64)> = self
328 .breakpoints
329 .iter()
330 .filter_map(|(addr, bp)| bp.id.map(|id| (id, bp.temporary, *addr)))
331 .collect();
332 items.sort_by_key(|(id, _, _)| *id);
333
334 if items.is_empty() {
335 write!(f, "No breakpoints")
336 } else {
337 let mut lines = Vec::with_capacity(items.len() + 1);
338 lines.push("Num Type Address".to_string());
339
340 for (id, temporary, addr) in items {
341 lines.push(format!(
342 "{:<6}{:<12}{:#018x}",
343 id,
344 if temporary { "Temporary" } else { "Permanent" },
345 addr
346 ));
347 }
348
349 write!(f, "{}", lines.join("\n"))
350 }
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 use nix::unistd::Pid;
359
360 #[test]
361 fn test_set_byte_in_word_basic() {
362 let word: c_long = 0x1122334455667788;
363 let out = set_byte_in_word(word, 0, 0xaa);
364 assert_eq!(out as u64 & 0xff, 0xaa);
365
366 let out = set_byte_in_word(word, 7, 0xbb);
367 assert_eq!(((out as u64) >> 56) & 0xff, 0xbb);
368 }
369
370 #[test]
371 fn test_pending_new_and_accessors() {
372 let p = Pending::new(Some(3), 0x1000);
373 assert_eq!(p.id(), Some(3));
374 assert_eq!(p.address(), 0x1000);
375 }
376
377 #[test]
378 fn test_manager_display_empty() {
379 let mgr = Manager::new(Pid::from_raw(1));
380 let s = format!("{}", mgr);
381 assert!(s.contains("No breakpoints"));
382 }
383
384 #[test]
385 fn test_manager_display_with_entries() {
386 let mut mgr = Manager::new(Pid::from_raw(1));
387 mgr.breakpoints
388 .insert(0x1000, Active::new(Some(2), 0x90, false));
389 let s = format!("{}", mgr);
390 assert!(s.contains("Num"));
391 assert!(s.contains("2"));
392 assert!(s.contains("0x0000000000001000"));
393 }
394}