Skip to main content

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}