Skip to main content

sys_rs/
process.rs

1use goblin::elf;
2use nix::{errno::Errno, sys::wait::wait, unistd::Pid};
3use procfs::process::{MMPermissions, MMapPath, Process};
4use std::{collections::HashMap, fs::File, io::Read, path::Path};
5
6use crate::diag::{Error, Result};
7
8const AT_ENTRY: u64 = 9;
9const AT_PHDR: u64 = 3;
10const EI_DATA: usize = 5;
11const MAX_OPCODE_SIZE: u64 = 16;
12
13/// Process metadata and parsed ELF image used by the tracer.
14///
15/// `Info` holds the auxiliary vector, an in-memory copy of the ELF file
16/// contents, parsed headers/sections, and the offsets needed to translate
17/// runtime addresses to file offsets. Construct using `Info::build`.
18pub struct Info {
19    pid: Pid,
20    auxv: HashMap<u64, u64>,
21    buffer: Vec<u8>,
22    header: elf::Header,
23    sections: HashMap<String, elf::SectionHeader>,
24    load_vaddr: u64,
25    load_offset: u64,
26    mem_offset: u64,
27}
28
29impl Info {
30    /// Builds an `Info` struct by collecting process data.
31    ///
32    /// # Arguments
33    ///
34    /// * `path` - The path to the file containing process data.
35    /// * `pid` - The process ID.
36    ///
37    /// # Errors
38    ///
39    /// Returns an `Err` upon any failure while collecting process data.
40    ///
41    /// # Returns
42    ///
43    /// Returns a `Result` containing the `Info` struct upon success, or an `Err` upon failure.
44    pub fn build(path: &str, pid: Pid) -> Result<Self> {
45        // First, wait for the process to start so we can collect its data.
46        wait()?;
47
48        let auxv = Self::get_auxv(pid)?;
49
50        let mut file = File::open(Path::new(path))?;
51        let mut buffer = Vec::new();
52        file.read_to_end(&mut buffer)?;
53
54        let elf = elf::Elf::parse(&buffer)?;
55        let header = elf.header;
56
57        let sections: HashMap<String, elf::SectionHeader> = elf
58            .section_headers
59            .iter()
60            .filter_map(|header| {
61                elf.shdr_strtab
62                    .get_at(header.sh_name)
63                    .map(|name| (name.to_string(), header.clone()))
64            })
65            .collect();
66        sections
67            .get(".text")
68            .ok_or_else(|| Error::from(Errno::ENODATA))?;
69
70        let dynamic = match elf.header.e_type {
71            elf::header::ET_DYN => Ok(true),
72            elf::header::ET_EXEC => Ok(false),
73            _ => Err(Error::from(Errno::ENOEXEC)),
74        }?;
75
76        let (load_vaddr, load_offset) = if dynamic {
77            elf.program_headers
78                .iter()
79                .find(|ph| {
80                    ph.p_type == elf::program_header::PT_LOAD && ph.is_executable()
81                })
82                .map(|ph| (ph.p_vaddr, ph.p_offset))
83                .ok_or_else(|| Error::from(Errno::ENODATA))?
84        } else {
85            (0, 0)
86        };
87
88        let mem_offset = if dynamic {
89            Self::get_mem_offset(path, pid)?
90        } else {
91            0
92        };
93
94        Ok(Self {
95            pid,
96            auxv,
97            buffer,
98            header,
99            sections,
100            load_vaddr,
101            load_offset,
102            mem_offset,
103        })
104    }
105
106    fn get_auxv(pid: Pid) -> Result<HashMap<u64, u64>> {
107        let process = Process::new(pid.into())?;
108        let auxv = process.auxv()?;
109        Ok(auxv.into_iter().collect())
110    }
111
112    fn get_mem_offset(path: &str, pid: Pid) -> Result<u64> {
113        let absolute_path = std::fs::canonicalize(path)?;
114
115        let process = Process::new(pid.into())?;
116        let maps = process.maps()?;
117
118        maps.into_iter()
119            .find_map(|map| match &map.pathname {
120                MMapPath::Path(buf)
121                    if buf == &absolute_path
122                        && map.perms.contains(MMPermissions::READ)
123                        && map.perms.contains(MMPermissions::EXECUTE) =>
124                {
125                    Some(map.address.0)
126                }
127                _ => None,
128            })
129            .ok_or_else(|| Error::from(Errno::ENODATA))
130    }
131
132    fn get_buffer_data(&self, offset: u64, len: u64) -> Result<Option<&[u8]>> {
133        let offset = usize::try_from(offset)?;
134        let len = usize::try_from(len)?;
135        Ok(self.buffer.get(offset..offset + len))
136    }
137
138    #[must_use]
139    /// Return the PID associated with this `Info`.
140    ///
141    /// # Returns
142    ///
143    /// The `Pid` belonging to the traced process.
144    pub fn pid(&self) -> Pid {
145        self.pid
146    }
147
148    #[must_use]
149    /// Return the ELF data encoding (endianness) byte (`EI_DATA`).
150    ///
151    /// This value matches the ELF header `e_ident[EI_DATA]` and can be
152    /// compared against `ELFDATA2LSB`/`ELFDATA2MSB` constants.
153    ///
154    /// # Returns
155    ///
156    /// The ELF `e_ident[EI_DATA]` byte as a `u8`.
157    pub fn endianness(&self) -> u8 {
158        self.header.e_ident[EI_DATA]
159    }
160
161    #[must_use]
162    /// Return the offset used to translate file addresses to runtime
163    /// addresses (i.e., `mem_offset - load_vaddr`).
164    ///
165    /// # Returns
166    ///
167    /// The computed offset which should be added to file addresses to
168    /// obtain runtime addresses (equal to `mem_offset - load_vaddr`).
169    pub fn offset(&self) -> u64 {
170        self.mem_offset - self.load_vaddr
171    }
172
173    #[must_use]
174    /// Return true if `addr` lies within the named ELF section (e.g.
175    /// ".text"). Addresses are compared against the section's runtime
176    /// address (`section.sh_addr` + `mem_offset`).
177    ///
178    /// # Arguments
179    ///
180    /// * `addr` - Runtime address to test.
181    /// * `name` - The section name to check (for example `".text"`).
182    ///
183    /// # Returns
184    ///
185    /// `true` if `addr` falls within the runtime range of the named section
186    /// (computed as `section.sh_addr + mem_offset` .. `+ sh_size`), otherwise
187    /// `false`.
188    pub fn is_addr_in_section(&self, addr: u64, name: &str) -> bool {
189        self.sections.get(name).is_some_and(|section| {
190            let start = section.sh_addr + self.mem_offset;
191            let end = start + section.sh_size;
192            (start..end).contains(&addr)
193        })
194    }
195
196    /// Retrieves the runtime entry point address of the process from the auxiliary vector.
197    ///
198    /// # Errors
199    ///
200    /// Returns an `Err` if the entry point is not available in the auxiliary vector.
201    ///
202    /// # Returns
203    ///
204    /// Returns a `Result` containing a reference to the entry point address (`u64`) from the auxiliary vector (`AT_ENTRY`).
205    pub fn entry(&self) -> Result<&u64> {
206        self.auxv
207            .get(&AT_ENTRY)
208            .ok_or_else(|| Error::from(Errno::ENODATA))
209    }
210
211    /// Retrieves the data from the specified section.
212    ///
213    /// # Arguments
214    ///
215    /// * `name` - The name of the section.
216    ///
217    /// # Errors
218    ///
219    /// Returns an `Err` if the conversion from `u64` to `usize` fails when getting buffer data.
220    ///
221    /// # Returns
222    ///
223    /// Returns an `Ok` containing the data from the specified section as a slice of bytes, or `Err` if the section does not exist or if the conversion from `u64` to `usize` fails.
224    pub fn get_section_data(&self, name: &str) -> Result<Option<&[u8]>> {
225        self.sections.get(name).map_or(Ok(None), |section| {
226            self.get_buffer_data(section.sh_offset, section.sh_size)
227        })
228    }
229
230    /// Retrieves opcode data from the loaded binary at a given runtime address.
231    ///
232    /// # Arguments
233    ///
234    /// * `addr` - The runtime address for which to fetch opcode bytes.
235    ///
236    /// # Errors
237    ///
238    /// Returns an `Err` if the conversion from `u64` to `usize` fails when accessing the buffer, or if required auxiliary vector data is missing.
239    ///
240    /// # Returns
241    ///
242    /// Returns `Ok(Some(&[u8]))` containing the opcode bytes at the given address, `Ok(None)` if the address is invalid, or `Err` on conversion failure or missing data.
243    pub fn get_opcode_from_addr(&self, addr: u64) -> Result<Option<&[u8]>> {
244        let phdr = self
245            .auxv
246            .get(&AT_PHDR)
247            .ok_or_else(|| Error::from(Errno::ENODATA))?;
248        let offset =
249            (addr - phdr + self.header.e_phoff) + self.load_offset - self.load_vaddr;
250        self.get_buffer_data(offset, MAX_OPCODE_SIZE)
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    use std::io::Write;
259
260    fn create_temp_elf_file() -> std::io::Result<String> {
261        let path = "/tmp/test.elf";
262        let mut file = File::create(path)?;
263        file.write_all(b"\x7fELF")?;
264        Ok(path.to_string())
265    }
266
267    #[test]
268    fn test_get_section_data_invalid() {
269        let pid = Pid::from_raw(1234);
270        let path =
271            create_temp_elf_file().expect("Failed to create temporary ELF file");
272        let info = Info::build(&path, pid);
273        if let Ok(info) = info {
274            let data = info.get_section_data(".invalid");
275            assert!(matches!(data, Ok(None)));
276        }
277    }
278
279    #[test]
280    fn test_get_opcode_from_addr_invalid_auxv() {
281        let pid = Pid::from_raw(1234);
282        let path =
283            create_temp_elf_file().expect("Failed to create temporary ELF file");
284        let info = Info::build(&path, pid);
285        if let Ok(info) = info {
286            let result = info.get_opcode_from_addr(0xdeadbeef);
287            assert!(result.is_err());
288        }
289    }
290
291    #[test]
292    fn test_is_addr_in_section_false() {
293        let pid = Pid::from_raw(1234);
294        let path =
295            create_temp_elf_file().expect("Failed to create temporary ELF file");
296        let info = Info::build(&path, pid);
297        if let Ok(info) = info {
298            let found = info.is_addr_in_section(0xdeadbeef, ".text");
299            assert!(!found);
300        }
301    }
302
303    #[test]
304    fn test_entry_missing() {
305        let pid = Pid::from_raw(1234);
306        let path =
307            create_temp_elf_file().expect("Failed to create temporary ELF file");
308        let info = Info::build(&path, pid);
309        if let Ok(info) = info {
310            let entry = info.entry();
311            assert!(entry.is_err());
312        }
313    }
314
315    #[test]
316    fn test_build_invalid_path() {
317        let pid = Pid::from_raw(1234);
318        let result = Info::build("/invalid/path", pid);
319        assert!(result.is_err());
320    }
321
322    #[test]
323    fn test_build_invalid_elf() {
324        let pid = Pid::from_raw(1234);
325        let path =
326            create_temp_elf_file().expect("Failed to create temporary ELF file");
327        let result = Info::build(&path, pid);
328        assert!(result.is_err());
329    }
330
331    #[test]
332    fn test_get_mem_offset_invalid_path() {
333        let pid = Pid::from_raw(1234);
334        let result = Info::get_mem_offset("/invalid/path", pid);
335        assert!(result.is_err());
336    }
337
338    #[test]
339    fn test_get_buffer_and_section_helpers() {
340        let pid = Pid::from_raw(1);
341        let buffer = vec![0u8; 64];
342        let header = elf::Header {
343            e_ident: [0; 16],
344            e_type: 0,
345            e_machine: 0,
346            e_version: 0,
347            e_entry: 0,
348            e_phoff: 0,
349            e_shoff: 0,
350            e_flags: 0,
351            e_ehsize: 0,
352            e_phentsize: 0,
353            e_phnum: 0,
354            e_shentsize: 0,
355            e_shnum: 0,
356            e_shstrndx: 0,
357        };
358        let sections: HashMap<String, elf::SectionHeader> = HashMap::new();
359
360        let info = Info {
361            pid,
362            auxv: HashMap::new(),
363            buffer,
364            header,
365            sections,
366            load_vaddr: 0,
367            load_offset: 0,
368            mem_offset: 0,
369        };
370
371        let data = info.get_buffer_data(0, 16).expect("get_buffer_data failed");
372        assert!(data.is_some());
373
374        let sec = info.get_section_data(".text");
375        assert!(sec.expect("get_section_data returned Err").is_none());
376    }
377
378    #[test]
379    fn test_info_accessors_and_opcode() {
380        let pid = Pid::from_raw(42);
381
382        let mut buffer = vec![0u8; 256];
383        for i in 100..116 {
384            buffer[i] = (i - 100) as u8;
385        }
386
387        let header = elf::Header {
388            e_ident: [0; 16],
389            e_type: 0,
390            e_machine: 0,
391            e_version: 0,
392            e_entry: 0,
393            e_phoff: 20,
394            e_shoff: 0,
395            e_flags: 0,
396            e_ehsize: 0,
397            e_phentsize: 0,
398            e_phnum: 0,
399            e_shentsize: 0,
400            e_shnum: 0,
401            e_shstrndx: 0,
402        };
403
404        let mut sections: HashMap<String, elf::SectionHeader> = HashMap::new();
405        let sh = elf::SectionHeader {
406            sh_name: 0,
407            sh_type: 0,
408            sh_flags: 0,
409            sh_addr: 0x200,
410            sh_offset: 50,
411            sh_size: 10,
412            sh_link: 0,
413            sh_info: 0,
414            sh_addralign: 0,
415            sh_entsize: 0,
416        };
417        sections.insert(".text".to_string(), sh);
418
419        let mut auxv = HashMap::new();
420        auxv.insert(AT_PHDR, 150u64);
421
422        let info = Info {
423            pid,
424            auxv,
425            buffer,
426            header,
427            sections,
428            load_vaddr: 0,
429            load_offset: 0,
430            mem_offset: 0,
431        };
432
433        assert_eq!(info.pid(), pid);
434        assert_eq!(info.endianness(), 0);
435        assert_eq!(info.offset(), 0);
436
437        let addr = 0x200 + 5;
438        assert!(info.is_addr_in_section(addr, ".text"));
439
440        let sec = info
441            .get_section_data(".text")
442            .expect("get_section_data failed");
443        assert!(sec.is_some());
444        let sec = sec.unwrap();
445        assert_eq!(sec.len(), 10);
446
447        let addr_for_opcode = 150u64 - 20u64 + 100u64;
448        let opc = info
449            .get_opcode_from_addr(addr_for_opcode)
450            .expect("get_opcode_from_addr failed")
451            .expect("opcode not found");
452
453        assert_eq!(opc.len(), usize::try_from(MAX_OPCODE_SIZE).unwrap());
454        assert_eq!(opc[0], 0u8);
455        assert_eq!(opc[15], 15u8);
456    }
457}