1use std::{
3 borrow::Cow,
4 error::Error,
5 ffi::{CStr, OsStr, OsString},
6 fmt::{self, Write},
7 fs,
8 io::{self, BufRead as _, Cursor, Read as _},
9 num::NonZeroU32,
10 os::unix::ffi::{OsStrExt as _, OsStringExt as _},
11 path::{Path, PathBuf},
12 sync::LazyLock,
13};
14
15use aya_obj::generated::{bpf_link_type, bpf_prog_type::BPF_PROG_TYPE_KPROBE};
16use object::{Object as _, ObjectSection as _, ObjectSymbol as _, Symbol};
17use thiserror::Error;
18
19use crate::{
20 VerifierLogLevel,
21 programs::{
22 ProgramData, ProgramError, ProgramType, define_link_wrapper, impl_try_from_fdlink,
23 impl_try_into_fdlink, load_program_without_attach_type,
24 perf_attach::{PerfLinkIdInner, PerfLinkInner},
25 probe::{OsStringExt as _, Probe, ProbeKind, attach},
26 },
27 util::MMap,
28};
29
30const LD_SO_CACHE_FILE: &str = "/etc/ld.so.cache";
31
32static LD_SO_CACHE: LazyLock<Result<LdSoCache, io::Error>> =
33 LazyLock::new(|| LdSoCache::load(LD_SO_CACHE_FILE));
34const LD_SO_CACHE_HEADER_OLD: &str = "ld.so-1.7.0\0";
35const LD_SO_CACHE_HEADER_NEW: &str = "glibc-ld.so.cache1.1";
36
37#[derive(Debug)]
45#[doc(alias = "BPF_PROG_TYPE_KPROBE")]
46pub struct UProbe {
47 pub(crate) data: ProgramData<UProbeLink>,
48 pub(crate) kind: ProbeKind,
49}
50
51#[derive(Debug)]
54pub enum UProbeAttachLocation<'a> {
55 Symbol(&'a str),
57 SymbolOffset(&'a str, u64),
60 AbsoluteOffset(u64),
62}
63
64impl<'a> From<&'a str> for UProbeAttachLocation<'a> {
65 fn from(s: &'a str) -> Self {
66 Self::Symbol(s)
67 }
68}
69
70impl From<u64> for UProbeAttachLocation<'static> {
71 fn from(offset: u64) -> Self {
72 Self::AbsoluteOffset(offset)
73 }
74}
75
76#[derive(Debug, Error)]
78pub enum UProbeAttachLocationError {
79 #[error(
81 "instruction address {instruction_address:#x} is not in section: \
82 before section address {section_address:#x}"
83 )]
84 AddressNotInSection {
85 instruction_address: u64,
87 section_address: u64,
89 },
90
91 #[error(
93 "computed file offset overflows u64: \
94 {instruction_address:#x} - {section_address:#x} + {section_offset:#x}"
95 )]
96 FileOffsetOverflow {
97 instruction_address: u64,
99 section_address: u64,
101 section_offset: u64,
103 },
104}
105
106impl UProbeAttachLocation<'static> {
107 pub fn from_virtual_address(
141 instruction_address: u64,
142 section_address: u64,
143 section_offset: u64,
144 ) -> Result<Self, UProbeAttachLocationError> {
145 let section_relative_offset = instruction_address.checked_sub(section_address).ok_or(
146 UProbeAttachLocationError::AddressNotInSection {
147 instruction_address,
148 section_address,
149 },
150 )?;
151 let file_offset = section_relative_offset.checked_add(section_offset).ok_or(
152 UProbeAttachLocationError::FileOffsetOverflow {
153 instruction_address,
154 section_address,
155 section_offset,
156 },
157 )?;
158
159 Ok(Self::AbsoluteOffset(file_offset))
160 }
161}
162
163pub struct UProbeAttachPoint<'a> {
165 pub location: UProbeAttachLocation<'a>,
167 pub cookie: Option<u64>,
169}
170
171impl<'a, L: Into<UProbeAttachLocation<'a>>> From<L> for UProbeAttachPoint<'a> {
172 fn from(location: L) -> Self {
173 Self {
174 location: location.into(),
175 cookie: None,
176 }
177 }
178}
179
180#[derive(Debug, Clone, Copy)]
182pub enum UProbeScope {
183 AllProcesses,
185 CallingProcess,
187 OneProcess(NonZeroU32),
189}
190
191impl UProbe {
192 pub const PROGRAM_TYPE: ProgramType = ProgramType::KProbe;
194
195 pub fn load(&mut self) -> Result<(), ProgramError> {
197 let Self { data, kind: _ } = self;
198 load_program_without_attach_type(BPF_PROG_TYPE_KPROBE, data)
199 }
200
201 pub const fn kind(&self) -> ProbeKind {
204 self.kind
205 }
206
207 pub fn attach<'a, T: AsRef<Path>, Point: Into<UProbeAttachPoint<'a>>>(
230 &mut self,
231 point: Point,
232 target: T,
233 scope: UProbeScope,
234 ) -> Result<UProbeLinkId, ProgramError> {
235 let UProbeAttachPoint { location, cookie } = point.into();
236 let target = target.as_ref();
237 let (proc_map_pid, perf_event_pid) = match scope {
238 UProbeScope::AllProcesses => (None, None),
239 UProbeScope::CallingProcess => (Some(std::process::id()), Some(0)),
242 UProbeScope::OneProcess(pid) => {
243 let pid = pid.get();
244 (Some(pid), Some(pid))
245 }
246 };
247
248 let proc_map;
252 let path = if is_basename_only(target) {
256 proc_map = proc_map_pid.map(ProcMap::new).transpose()?;
257 resolve_attach_target_basename(target, proc_map.as_ref())?
258 } else {
259 target
260 };
261
262 let (symbol, offset) = match location {
263 UProbeAttachLocation::Symbol(s) => (Some(s), 0),
264 UProbeAttachLocation::SymbolOffset(s, offset) => (Some(s), offset),
265 UProbeAttachLocation::AbsoluteOffset(offset) => (None, offset),
266 };
267 let offset = if let Some(symbol) = symbol {
268 let symbol_offset =
269 resolve_symbol(path, symbol).map_err(|error| UProbeError::SymbolError {
270 symbol: symbol.to_string(),
271 error: Box::new(error),
272 })?;
273 symbol_offset + offset
274 } else {
275 offset
276 };
277
278 let Self { data, kind } = self;
279 let path = path.as_os_str();
280 attach::<Self, _>(data, *kind, path, offset, perf_event_pid, cookie)
281 }
282
283 pub fn from_pin<P: AsRef<Path>>(path: P, kind: ProbeKind) -> Result<Self, ProgramError> {
290 let data = ProgramData::from_pinned_path(path, VerifierLogLevel::default())?;
291 Ok(Self { data, kind })
292 }
293}
294
295impl Probe for UProbe {
296 const PMU: &'static str = "uprobe";
297
298 type Error = UProbeError;
299
300 fn file_error(filename: PathBuf, io_error: io::Error) -> Self::Error {
301 UProbeError::FileError { filename, io_error }
302 }
303
304 fn write_offset<W: Write>(w: &mut W, _: ProbeKind, offset: u64) -> fmt::Result {
305 write!(w, ":{offset:#x}")
306 }
307}
308
309fn is_basename_only(target: &Path) -> bool {
310 target.file_name() == Some(target.as_os_str())
311}
312
313fn resolve_attach_target_basename<'a, 'b, 'c, T>(
316 target: &'a Path,
317 proc_map: Option<&'b ProcMap<T>>,
318) -> Result<&'c Path, UProbeError>
319where
320 'a: 'c,
321 'b: 'c,
322 T: AsRef<[u8]>,
323{
324 proc_map
325 .and_then(|proc_map| {
326 proc_map
327 .find_library_path_by_name(target)
328 .map_err(|source| {
329 let ProcMap { pid, data: _ } = proc_map;
330 let pid = *pid;
331 UProbeError::ProcMap { pid, source }
332 })
333 .transpose()
334 })
335 .or_else(|| {
336 LD_SO_CACHE
337 .as_ref()
338 .map_err(|io_error| UProbeError::InvalidLdSoCache { io_error })
339 .map(|cache| cache.resolve(target))
340 .transpose()
341 })
342 .unwrap_or_else(|| {
343 Err(UProbeError::InvalidTarget {
344 path: target.to_owned(),
345 })
346 })
347}
348
349define_link_wrapper!(
350 UProbeLink,
351 UProbeLinkId,
352 PerfLinkInner,
353 PerfLinkIdInner,
354 UProbe,
355);
356
357impl_try_into_fdlink!(UProbeLink, PerfLinkInner);
358impl_try_from_fdlink!(
359 UProbeLink,
360 PerfLinkInner,
361 bpf_link_type::BPF_LINK_TYPE_PERF_EVENT
362);
363
364#[derive(Debug, Error)]
366pub enum UProbeError {
367 #[error("error reading `{}` file", LD_SO_CACHE_FILE)]
369 InvalidLdSoCache {
370 #[source]
372 io_error: &'static io::Error,
373 },
374
375 #[error("could not resolve uprobe target `{path}`")]
377 InvalidTarget {
378 path: PathBuf,
380 },
381
382 #[error("error resolving symbol")]
384 SymbolError {
385 symbol: String,
387 #[source]
389 error: Box<dyn Error + Send + Sync>,
390 },
391
392 #[error("`{filename}`")]
394 FileError {
395 filename: PathBuf,
397 #[source]
399 io_error: io::Error,
400 },
401
402 #[error("error fetching libs for {pid}")]
404 ProcMap {
405 pid: u32,
407 #[source]
409 source: ProcMapError,
410 },
411}
412
413#[derive(Debug, Error)]
415pub enum ProcMapError {
416 #[error(transparent)]
418 ReadFile(#[from] io::Error),
419
420 #[error("could not parse {}", line.display())]
422 ParseLine {
423 line: OsString,
425 },
426}
427
428#[cfg_attr(test, derive(Debug, PartialEq))]
433struct ProcMapEntry<'a> {
434 #[cfg_attr(not(test), expect(dead_code, reason = "parsed but not exposed"))]
435 address: u64,
436 #[cfg_attr(not(test), expect(dead_code, reason = "parsed but not exposed"))]
437 address_end: u64,
438 #[cfg_attr(not(test), expect(dead_code, reason = "parsed but not exposed"))]
439 perms: &'a OsStr,
440 #[cfg_attr(not(test), expect(dead_code, reason = "parsed but not exposed"))]
441 offset: u64,
442 #[cfg_attr(not(test), expect(dead_code, reason = "parsed but not exposed"))]
443 dev: &'a OsStr,
444 #[cfg_attr(not(test), expect(dead_code, reason = "parsed but not exposed"))]
445 inode: u32,
446 path: Option<&'a OsStr>,
447}
448
449fn split_ascii_whitespace_n(s: &[u8], mut n: usize) -> impl Iterator<Item = &[u8]> {
454 let mut s = s.trim_ascii_end();
455
456 std::iter::from_fn(move || {
457 if n == 0 {
458 None
459 } else {
460 s = s.trim_ascii_start();
461
462 n -= 1;
463 Some(if n == 0 {
464 s
465 } else if let Some(i) = s.iter().position(u8::is_ascii_whitespace) {
466 let (next, rest) = s.split_at(i);
467 s = rest;
468 next
469 } else {
470 n = 0;
471 s
472 })
473 }
474 })
475}
476
477impl<'a> ProcMapEntry<'a> {
478 fn parse(line: &'a [u8]) -> Result<Self, ProcMapError> {
479 use std::os::unix::ffi::OsStrExt as _;
480
481 let err = || ProcMapError::ParseLine {
482 line: OsString::from_vec(line.to_vec()),
483 };
484
485 let mut parts =
486 split_ascii_whitespace_n(line, 6)
488 .filter(|part| !part.is_empty());
489
490 let mut next = || parts.next().ok_or_else(err);
491
492 let (start, end) = {
493 let addr = next()?;
494 let mut addr_parts = addr.split(|b| *b == b'-');
495 let mut next = || {
496 addr_parts
497 .next()
498 .ok_or(())
499 .and_then(|part| {
500 let s =
501 std::str::from_utf8(part).map_err(|std::str::Utf8Error { .. }| ())?;
502 let n = u64::from_str_radix(s, 16)
503 .map_err(|std::num::ParseIntError { .. }| ())?;
504 Ok(n)
505 })
506 .map_err(|()| err())
507 };
508 let start = next()?;
509 let end = next()?;
510 if let Some(_part) = addr_parts.next() {
511 return Err(err());
512 }
513 (start, end)
514 };
515
516 let perms = next()?;
517 let perms = OsStr::from_bytes(perms);
518 let offset = next()?;
519 let offset = std::str::from_utf8(offset).map_err(|std::str::Utf8Error { .. }| err())?;
520 let offset =
521 u64::from_str_radix(offset, 16).map_err(|std::num::ParseIntError { .. }| err())?;
522 let dev = next()?;
523 let dev = OsStr::from_bytes(dev);
524 let inode = next()?;
525 let inode = std::str::from_utf8(inode).map_err(|std::str::Utf8Error { .. }| err())?;
526 let inode = inode
527 .parse()
528 .map_err(|std::num::ParseIntError { .. }| err())?;
529
530 let path = parts.next().map(OsStr::from_bytes);
531
532 if let Some(_part) = parts.next() {
533 return Err(err());
534 }
535
536 Ok(Self {
537 address: start,
538 address_end: end,
539 perms,
540 offset,
541 dev,
542 inode,
543 path,
544 })
545 }
546}
547
548struct ProcMap<T> {
554 pid: u32,
555 data: T,
556}
557
558impl ProcMap<Vec<u8>> {
559 fn new(pid: u32) -> Result<Self, UProbeError> {
560 let filename = PathBuf::from(format!("/proc/{pid}/maps"));
561 let data = fs::read(&filename)
562 .map_err(|io_error| UProbeError::FileError { filename, io_error })?;
563 Ok(Self { pid, data })
564 }
565}
566
567impl<T: AsRef<[u8]>> ProcMap<T> {
568 fn libs(&self) -> impl Iterator<Item = Result<ProcMapEntry<'_>, ProcMapError>> {
569 let Self { pid: _, data } = self;
570
571 data.as_ref()
573 .trim_ascii()
574 .split(|&b| b == b'\n')
575 .map(ProcMapEntry::parse)
576 }
577
578 fn find_library_path_by_name(&self, lib: &Path) -> Result<Option<&Path>, ProcMapError> {
583 let lib = lib.as_os_str();
584 let lib = lib.strip_suffix(OsStr::new(".so")).unwrap_or(lib);
585
586 for entry in self.libs() {
587 let ProcMapEntry {
588 address: _,
589 address_end: _,
590 perms: _,
591 offset: _,
592 dev: _,
593 inode: _,
594 path,
595 } = entry?;
596 if let Some(path) = path {
597 let path = Path::new(path);
598 if let Some(filename) = path.file_name() {
599 if let Some(suffix) = filename.strip_prefix(lib) {
600 if suffix.is_empty()
601 || suffix.starts_with(OsStr::new(".so"))
602 || suffix.starts_with(OsStr::new("-"))
603 {
604 return Ok(Some(path));
605 }
606 }
607 }
608 }
609 }
610 Ok(None)
611 }
612}
613
614#[derive(Debug)]
615pub(crate) struct CacheEntry {
616 key: OsString,
617 value: OsString,
618 _flags: i32,
619}
620
621#[derive(Debug)]
622pub(crate) struct LdSoCache {
623 entries: Vec<CacheEntry>,
624}
625
626impl LdSoCache {
627 fn load<T: AsRef<Path>>(path: T) -> Result<Self, io::Error> {
628 let data = fs::read(path)?;
629 Self::parse(&data)
630 }
631
632 fn parse(data: &[u8]) -> Result<Self, io::Error> {
633 let mut cursor = Cursor::new(data);
634
635 let read_u32 = |cursor: &mut Cursor<_>| -> Result<u32, io::Error> {
636 let mut buf = [0u8; size_of::<u32>()];
637 cursor.read_exact(&mut buf)?;
638
639 Ok(u32::from_ne_bytes(buf))
640 };
641
642 let read_i32 = |cursor: &mut Cursor<_>| -> Result<i32, io::Error> {
643 let mut buf = [0u8; size_of::<i32>()];
644 cursor.read_exact(&mut buf)?;
645
646 Ok(i32::from_ne_bytes(buf))
647 };
648
649 let mut buf = [0u8; LD_SO_CACHE_HEADER_NEW.len()];
651 cursor.read_exact(&mut buf)?;
652 let header = std::str::from_utf8(&buf).map_err(|std::str::Utf8Error { .. }| {
653 io::Error::new(io::ErrorKind::InvalidData, "invalid ld.so.cache header")
654 })?;
655
656 let new_format = header == LD_SO_CACHE_HEADER_NEW;
657
658 if !new_format {
660 cursor.set_position(0);
661 let mut buf = [0u8; LD_SO_CACHE_HEADER_OLD.len()];
662 cursor.read_exact(&mut buf)?;
663 let header = std::str::from_utf8(&buf).map_err(|std::str::Utf8Error { .. }| {
664 io::Error::new(io::ErrorKind::InvalidData, "invalid ld.so.cache header")
665 })?;
666
667 if header != LD_SO_CACHE_HEADER_OLD {
668 return Err(io::Error::new(
669 io::ErrorKind::InvalidData,
670 "invalid ld.so.cache header",
671 ));
672 }
673 }
674
675 let num_entries = read_u32(&mut cursor)?;
676
677 if new_format {
678 cursor.consume(6 * size_of::<u32>());
679 }
680
681 let offset = if new_format {
682 0
683 } else {
684 cursor.position() as usize + num_entries as usize * 12
685 };
686
687 let entries = std::iter::repeat_with(|| {
688 let flags = read_i32(&mut cursor)?;
689 let k_pos = read_u32(&mut cursor)? as usize;
690 let v_pos = read_u32(&mut cursor)? as usize;
691
692 if new_format {
693 cursor.consume(12);
694 }
695
696 let read_str = |pos| {
697 use std::os::unix::ffi::OsStrExt as _;
698 OsStr::from_bytes(
699 unsafe { CStr::from_ptr(cursor.get_ref()[offset + pos..].as_ptr().cast()) }
700 .to_bytes(),
701 )
702 .to_owned()
703 };
704
705 let key = read_str(k_pos);
706 let value = read_str(v_pos);
707
708 Ok::<_, io::Error>(CacheEntry {
709 key,
710 value,
711 _flags: flags,
712 })
713 })
714 .take(num_entries as usize)
715 .collect::<Result<_, _>>()?;
716
717 Ok(Self { entries })
718 }
719
720 fn resolve(&self, lib: &Path) -> Option<&Path> {
721 let Self { entries } = self;
722
723 let lib = lib.as_os_str();
724 let lib = lib.strip_suffix(OsStr::new(".so")).unwrap_or(lib);
725
726 entries
727 .iter()
728 .find_map(|CacheEntry { key, value, _flags }| {
729 let suffix = key.strip_prefix(lib)?;
730 suffix
731 .starts_with(OsStr::new(".so"))
732 .then_some(Path::new(value.as_os_str()))
733 })
734 }
735}
736
737#[derive(Error, Debug)]
738enum ResolveSymbolError {
739 #[error(transparent)]
740 Io(#[from] io::Error),
741
742 #[error("error parsing ELF")]
743 Object(#[from] object::Error),
744
745 #[error("unknown symbol `{0}`")]
746 Unknown(String),
747
748 #[error("symbol `{0}` does not appear in section")]
749 NotInSection(String),
750
751 #[error("symbol `{0}` in section `{1:?}` which has no offset")]
752 SectionFileRangeNone(String, Result<String, object::Error>),
753
754 #[error("failed to access debuglink file `{0}`: `{1}`")]
755 DebuglinkAccessError(PathBuf, io::Error),
756
757 #[error("symbol `{0}` not found, mismatched build IDs in main and debug files")]
758 BuildIdMismatch(String),
759}
760
761fn construct_debuglink_path<'a>(filename: &'a [u8], main_path: &Path) -> Cow<'a, Path> {
762 let filename_str = OsStr::from_bytes(filename);
763 let debuglink_path = Path::new(filename_str);
764
765 if debuglink_path.is_relative() {
766 main_path.parent().map_or_else(
768 || debuglink_path.into(), |parent| parent.join(debuglink_path).into(),
770 )
771 } else {
772 debuglink_path.into()
774 }
775}
776
777fn verify_build_ids<'a>(
778 main_obj: &'a object::File<'a>,
779 debug_obj: &'a object::File<'a>,
780 symbol_name: &str,
781) -> Result<(), ResolveSymbolError> {
782 let main_build_id = main_obj.build_id().ok().flatten();
783 let debug_build_id = debug_obj.build_id().ok().flatten();
784
785 match (debug_build_id, main_build_id) {
786 (Some(debug_build_id), Some(main_build_id)) => {
787 if debug_build_id != main_build_id {
789 return Err(ResolveSymbolError::BuildIdMismatch(symbol_name.to_owned()));
790 }
791 Ok(())
792 }
793 _ => Ok(()),
794 }
795}
796
797fn find_debug_path_in_object<'a>(
798 obj: &object::File<'a>,
799 main_path: &Path,
800 symbol: &str,
801) -> Result<Cow<'a, Path>, ResolveSymbolError> {
802 match obj.gnu_debuglink() {
803 Ok(Some((filename, _))) => Ok(construct_debuglink_path(filename, main_path)),
804 Ok(None) => Err(ResolveSymbolError::Unknown(symbol.to_string())),
805 Err(err) => Err(ResolveSymbolError::Object(err)),
806 }
807}
808
809fn find_symbol_in_object<'a>(obj: &'a object::File<'a>, symbol: &str) -> Option<Symbol<'a, 'a>> {
810 obj.dynamic_symbols()
811 .chain(obj.symbols())
812 .find(|sym| sym.name().is_ok_and(|name| name == symbol))
813}
814
815fn resolve_symbol(path: &Path, symbol: &str) -> Result<u64, ResolveSymbolError> {
816 let data = MMap::map_copy_read_only(path)?;
817 let obj = object::read::File::parse(data.as_ref())?;
818
819 if let Some(sym) = find_symbol_in_object(&obj, symbol) {
820 symbol_translated_address(&obj, sym, symbol)
821 } else {
822 let debug_path = find_debug_path_in_object(&obj, path, symbol)?;
824 let debug_data = MMap::map_copy_read_only(&debug_path)
825 .map_err(|e| ResolveSymbolError::DebuglinkAccessError(debug_path.into_owned(), e))?;
826 let debug_obj = object::read::File::parse(debug_data.as_ref())?;
827
828 verify_build_ids(&obj, &debug_obj, symbol)?;
829
830 let sym = find_symbol_in_object(&debug_obj, symbol)
831 .ok_or_else(|| ResolveSymbolError::Unknown(symbol.to_string()))?;
832
833 symbol_translated_address(&debug_obj, sym, symbol)
834 }
835}
836
837fn symbol_translated_address(
838 obj: &object::File<'_>,
839 sym: Symbol<'_, '_>,
840 symbol_name: &str,
841) -> Result<u64, ResolveSymbolError> {
842 let needs_addr_translation = matches!(
843 obj.kind(),
844 object::ObjectKind::Dynamic | object::ObjectKind::Executable
845 );
846 if needs_addr_translation {
847 let index = sym
848 .section_index()
849 .ok_or_else(|| ResolveSymbolError::NotInSection(symbol_name.to_string()))?;
850 let section = obj.section_by_index(index)?;
851 let (offset, _size) = section.file_range().ok_or_else(|| {
852 ResolveSymbolError::SectionFileRangeNone(
853 symbol_name.to_string(),
854 section.name().map(str::to_owned),
855 )
856 })?;
857 Ok(sym.address() - section.address() + offset)
858 } else {
859 Ok(sym.address())
860 }
861}
862
863#[cfg(test)]
864mod tests {
865 use assert_matches::assert_matches;
866 use object::{Architecture, BinaryFormat, Endianness, write::SectionKind};
867 use rstest::rstest;
868
869 use super::*;
870
871 #[test]
874 #[cfg_attr(
875 any(miri, not(target_os = "linux"), target_feature = "crt-static"),
876 ignore = "requires dynamic linkage of libc"
877 )]
878 fn test_resolve_attach_target_basename() {
879 let pid = std::process::id();
881 let proc_map = ProcMap::new(pid).expect("failed to get proc map");
882
883 assert_matches!(
886 resolve_attach_target_basename("libc".as_ref(), Some(&proc_map)),
887 Ok(path) => {
888 assert_matches!(
890 path.to_str(),
891 Some(path) if path.contains("libc"), "path: {}", path.display()
892 );
893 }
894 );
895 }
896
897 #[rstest]
898 #[case::shared_object_basename("libssl.so", true)]
899 #[case::binary_basename("bash", true)]
900 #[case::versioned_shared_object_basename("foo.so.1", true)]
901 #[case::absolute_path("/usr/bin/bash", false)]
902 #[case::root_absolute_path("/aa", false)]
903 #[case::current_dir_relative_path("./bin/foo", false)]
904 #[case::current_dir_relative_file("./aa", false)]
905 #[case::subdir_relative_path("subdir/lib.so", false)]
906 #[case::parent_dir_relative_path("../lib/foo", false)]
907 #[case::root_dir("/", false)]
908 #[case::current_dir(".", false)]
909 #[case::parent_dir("..", false)]
910 #[case::trailing_separator("foo/", false)]
911 fn test_is_basename_only(#[case] input: &str, #[case] expected: bool) {
912 assert_eq!(is_basename_only(Path::new(input)), expected);
913 }
914
915 #[test]
916 fn test_uprobe_attach_location_from_virtual_address() {
917 assert_matches!(
918 UProbeAttachLocation::from_virtual_address(0x601300, 0x35ff00, 0x15ff00),
919 Ok(UProbeAttachLocation::AbsoluteOffset(0x401300))
920 );
921 }
922
923 #[test]
924 fn test_uprobe_attach_location_from_virtual_address_errors_on_underflow() {
925 assert_matches!(
926 UProbeAttachLocation::from_virtual_address(1, 2, 0),
927 Err(UProbeAttachLocationError::AddressNotInSection {
928 instruction_address: 1,
929 section_address: 2,
930 })
931 );
932 }
933
934 #[test]
935 fn test_uprobe_attach_location_from_virtual_address_errors_on_overflow() {
936 assert_matches!(
937 UProbeAttachLocation::from_virtual_address(u64::MAX, 0, 1),
938 Err(UProbeAttachLocationError::FileOffsetOverflow {
939 instruction_address: u64::MAX,
940 section_address: 0,
941 section_offset: 1,
942 })
943 );
944 }
945
946 #[test]
947 fn test_relative_path_with_parent() {
948 let filename = b"debug_info";
949 let main_path = Path::new("/usr/lib/main_binary");
950 let expected = Path::new("/usr/lib/debug_info");
951
952 let result = construct_debuglink_path(filename, main_path);
953 assert_eq!(
954 result, expected,
955 "The debug path should resolve relative to the main path's parent"
956 );
957 }
958
959 #[test]
960 fn test_relative_path_without_parent() {
961 let filename = b"debug_info";
962 let main_path = Path::new("main_binary");
963 let expected = Path::new("debug_info");
964
965 let result = construct_debuglink_path(filename, main_path);
966 assert_eq!(
967 result, expected,
968 "The debug path should be the original path as there is no parent"
969 );
970 }
971
972 #[test]
973 fn test_absolute_path() {
974 let filename = b"/absolute/path/to/debug_info";
975 let main_path = Path::new("/usr/lib/main_binary");
976 let expected = Path::new("/absolute/path/to/debug_info");
977
978 let result = construct_debuglink_path(filename, main_path);
979 assert_eq!(
980 result, expected,
981 "The debug path should be the same as the input absolute path"
982 );
983 }
984
985 #[expect(
986 clippy::little_endian_bytes,
987 reason = "ELF debuglink fields are encoded as little-endian"
988 )]
989 fn create_elf_with_debuglink(
990 debug_filename: &[u8],
991 crc: u32,
992 ) -> Result<Vec<u8>, object::write::Error> {
993 let mut obj =
994 object::write::Object::new(BinaryFormat::Elf, Architecture::X86_64, Endianness::Little);
995
996 let section_name = b".gnu_debuglink";
997
998 let section_id = obj.add_section(vec![], section_name.to_vec(), SectionKind::Note);
999
1000 let mut debuglink_data = Vec::new();
1001
1002 debuglink_data.extend_from_slice(debug_filename);
1003 debuglink_data.push(0); while debuglink_data.len() % 4 != 0 {
1006 debuglink_data.push(0);
1007 }
1008
1009 debuglink_data.extend(&crc.to_le_bytes());
1010
1011 obj.append_section_data(section_id, &debuglink_data, 4 );
1012
1013 obj.write()
1014 }
1015
1016 #[expect(
1017 clippy::little_endian_bytes,
1018 reason = "ELF note headers are encoded as little-endian"
1019 )]
1020 fn create_elf_with_build_id(build_id: &[u8]) -> Result<Vec<u8>, object::write::Error> {
1021 let mut obj =
1022 object::write::Object::new(BinaryFormat::Elf, Architecture::X86_64, Endianness::Little);
1023
1024 let section_name = b".note.gnu.build-id";
1025
1026 let section_id = obj.add_section(vec![], section_name.to_vec(), SectionKind::Note);
1027
1028 let mut note_data = Vec::new();
1029 let build_id_name = b"GNU";
1030
1031 note_data.extend(&(build_id_name.len() as u32 + 1).to_le_bytes());
1032 note_data.extend(&(build_id.len() as u32).to_le_bytes());
1033 note_data.extend(&3u32.to_le_bytes());
1034
1035 note_data.extend_from_slice(build_id_name);
1036 note_data.push(0); note_data.extend_from_slice(build_id);
1038
1039 obj.append_section_data(section_id, ¬e_data, 4 );
1040
1041 obj.write()
1042 }
1043
1044 fn aligned_slice(vec: &mut Vec<u8>) -> &mut [u8] {
1045 let alignment = 8;
1046
1047 let original_size = vec.len();
1048 let total_size = original_size + alignment - 1;
1049
1050 if vec.capacity() < total_size {
1051 vec.reserve(total_size - vec.capacity());
1052 }
1053
1054 if vec.len() < total_size {
1055 vec.resize(total_size, 0);
1056 }
1057
1058 let ptr = vec.as_ptr() as usize;
1059
1060 let aligned_ptr = ptr.next_multiple_of(alignment);
1061
1062 let offset = aligned_ptr - ptr;
1063
1064 if offset > 0 {
1065 let tmp = vec.len();
1066 vec.copy_within(0..tmp - offset, offset);
1067 }
1068
1069 &mut vec[offset..offset + original_size]
1070 }
1071
1072 #[test]
1073 fn test_find_debug_path_success() {
1074 let debug_filepath = b"main.debug";
1075 let mut main_bytes = create_elf_with_debuglink(debug_filepath, 0x123 )
1076 .expect("got main_bytes");
1077 let align_bytes = aligned_slice(&mut main_bytes);
1078 let main_obj = object::File::parse(&*align_bytes).expect("got main obj");
1079
1080 let main_path = Path::new("/path/to/main");
1081
1082 assert_matches!(
1083 find_debug_path_in_object(&main_obj, main_path, "symbol"),
1084 Ok(path) => {
1085 assert_eq!(&*path, "/path/to/main.debug", "path: {}", path.display());
1086 }
1087 );
1088 }
1089
1090 #[test]
1091 fn test_verify_build_ids_same() {
1092 let build_id = b"test_build_id";
1093 let mut main_bytes = create_elf_with_build_id(build_id).expect("got main_bytes");
1094 let align_bytes = aligned_slice(&mut main_bytes);
1095 let main_obj = object::File::parse(&*align_bytes).expect("got main obj");
1096 let debug_build_id = b"test_build_id";
1097 let mut debug_bytes = create_elf_with_build_id(debug_build_id).expect("got debug bytes");
1098 let align_bytes = aligned_slice(&mut debug_bytes);
1099 let debug_obj = object::File::parse(&*align_bytes).expect("got debug obj");
1100
1101 assert_matches!(
1102 verify_build_ids(&main_obj, &debug_obj, "symbol_name"),
1103 Ok(())
1104 );
1105 }
1106
1107 #[test]
1108 fn test_verify_build_ids_different() {
1109 let build_id = b"main_build_id";
1110 let mut main_bytes = create_elf_with_build_id(build_id).expect("got main_bytes");
1111 let align_bytes = aligned_slice(&mut main_bytes);
1112 let main_obj = object::File::parse(&*align_bytes).expect("got main obj");
1113 let debug_build_id = b"debug_build_id";
1114 let mut debug_bytes = create_elf_with_build_id(debug_build_id).expect("got debug bytes");
1115 let align_bytes = aligned_slice(&mut debug_bytes);
1116 let debug_obj = object::File::parse(&*align_bytes).expect("got debug obj");
1117
1118 assert_matches!(
1119 verify_build_ids(&main_obj, &debug_obj, "symbol_name"),
1120 Err(ResolveSymbolError::BuildIdMismatch(symbol_name)) if symbol_name == "symbol_name"
1121 );
1122 }
1123
1124 #[derive(Debug, Clone, Copy)]
1125 struct ExpectedProcMapEntry {
1126 address: u64,
1127 address_end: u64,
1128 perms: &'static str,
1129 offset: u64,
1130 dev: &'static str,
1131 inode: u32,
1132 path: Option<&'static str>,
1133 }
1134
1135 #[rstest]
1136 #[case::bracketed_name(
1137 b"7ffd6fbea000-7ffd6fbec000 r-xp 00000000 00:00 0 [vdso]",
1138 ExpectedProcMapEntry {
1139 address: 0x7ffd6fbea000,
1140 address_end: 0x7ffd6fbec000,
1141 perms: "r-xp",
1142 offset: 0,
1143 dev: "00:00",
1144 inode: 0,
1145 path: Some("[vdso]"),
1146 })]
1147 #[case::absolute_path(
1148 b"7f1bca83a000-7f1bca83c000 rw-p 00036000 fd:01 2895508 /usr/lib64/ld-linux-x86-64.so.2",
1149 ExpectedProcMapEntry {
1150 address: 0x7f1bca83a000,
1151 address_end: 0x7f1bca83c000,
1152 perms: "rw-p",
1153 offset: 0x00036000,
1154 dev: "fd:01",
1155 inode: 2895508,
1156 path: Some("/usr/lib64/ld-linux-x86-64.so.2"),
1157 })]
1158 #[case::no_path(
1159 b"7f1bca5f9000-7f1bca601000 rw-p 00000000 00:00 0",
1160 ExpectedProcMapEntry {
1161 address: 0x7f1bca5f9000,
1162 address_end: 0x7f1bca601000,
1163 perms: "rw-p",
1164 offset: 0,
1165 dev: "00:00",
1166 inode: 0,
1167 path: None,
1168 })]
1169 #[case::relative_path_token(
1170 b"7f1bca5f9000-7f1bca601000 rw-p 00000000 00:00 0 deadbeef",
1171 ExpectedProcMapEntry {
1172 address: 0x7f1bca5f9000,
1173 address_end: 0x7f1bca601000,
1174 perms: "rw-p",
1175 offset: 0,
1176 dev: "00:00",
1177 inode: 0,
1178 path: Some("deadbeef"),
1179 })]
1180 #[case::deleted_suffix_in_path(
1181 b"7f1bca83a000-7f1bca83c000 rw-p 00036000 fd:01 2895508 /usr/lib/libc.so.6 (deleted)",
1182 ExpectedProcMapEntry {
1183 address: 0x7f1bca83a000,
1184 address_end: 0x7f1bca83c000,
1185 perms: "rw-p",
1186 offset: 0x00036000,
1187 dev: "fd:01",
1188 inode: 2895508,
1189 path: Some("/usr/lib/libc.so.6 (deleted)"),
1190 })]
1191 #[case::path_remainder_with_spaces(
1193 b"71064dc000-71064df000 ---p 00000000 00:00 0 [page size compat] extra",
1194 ExpectedProcMapEntry {
1195 address: 0x71064dc000,
1196 address_end: 0x71064df000,
1197 perms: "---p",
1198 offset: 0,
1199 dev: "00:00",
1200 inode: 0,
1201 path: Some("[page size compat] extra"),
1202 })]
1203 #[case::bracketed_name_with_spaces(
1204 b"724a0000-72aab000 rw-p 00000000 00:00 0 [anon:dalvik-zygote space] (deleted) extra",
1205 ExpectedProcMapEntry {
1206 address: 0x724a0000,
1207 address_end: 0x72aab000,
1208 perms: "rw-p",
1209 offset: 0,
1210 dev: "00:00",
1211 inode: 0,
1212 path: Some("[anon:dalvik-zygote space] (deleted) extra"),
1213 })]
1214 #[case::memfd_deleted(
1215 b"5ba3b000-5da3b000 r--s 00000000 00:01 1033 /memfd:jit-zygote-cache (deleted)",
1216 ExpectedProcMapEntry {
1217 address: 0x5ba3b000,
1218 address_end: 0x5da3b000,
1219 perms: "r--s",
1220 offset: 0,
1221 dev: "00:01",
1222 inode: 1033,
1223 path: Some("/memfd:jit-zygote-cache (deleted)"),
1224 })]
1225 #[case::ashmem_with_spaces(
1226 b"6cd539c000-6cd559c000 rw-s 00000000 00:01 7215 /dev/ashmem/CursorWindow: /data/user/0/package/databases/kitefly.db (deleted)",
1227 ExpectedProcMapEntry {
1228 address: 0x6cd539c000,
1229 address_end: 0x6cd559c000,
1230 perms: "rw-s",
1231 offset: 0,
1232 dev: "00:01",
1233 inode: 7215,
1234 path: Some("/dev/ashmem/CursorWindow: /data/user/0/package/databases/kitefly.db (deleted)"),
1235 })]
1236 fn test_parse_proc_map_entry_ok(
1237 #[case] line: &'static [u8],
1238 #[case] expected: ExpectedProcMapEntry,
1239 ) {
1240 use std::ffi::OsStr;
1241
1242 let ExpectedProcMapEntry {
1243 address,
1244 address_end,
1245 perms,
1246 offset,
1247 dev,
1248 inode,
1249 path,
1250 } = expected;
1251
1252 assert_matches!(ProcMapEntry::parse(line), Ok(entry) if entry == ProcMapEntry {
1253 address,
1254 address_end,
1255 perms: OsStr::new(perms),
1256 offset,
1257 dev: OsStr::new(dev),
1258 inode,
1259 path: path.map(OsStr::new),
1260 });
1261 }
1262
1263 #[rstest]
1264 #[case::bad_address(b"zzzz-7ffd6fbea000 r-xp 00000000 00:00 0 [vdso]")]
1265 #[case::bad_offset(b"7f1bca5f9000-7f1bca601000 r-xp zzzz 00:00 0 [vdso]")]
1266 #[case::bad_inode(b"7f1bca5f9000-7f1bca601000 r-xp 00000000 00:00 zzzz [vdso]")]
1267 #[case::bad_address_range(b"7f1bca5f90007ffd6fbea000 r-xp 00000000 00:00 0 [vdso]")]
1268 #[case::missing_fields(b"7f1bca5f9000-7f1bca601000 r-xp 00000000")]
1269 #[case::bad_address_delimiter(b"7f1bca5f9000-7f1bca601000-deadbeef rw-p 00000000 00:00 0")]
1270 fn test_parse_proc_map_entry_err(#[case] line: &'static [u8]) {
1271 assert_matches!(
1272 ProcMapEntry::parse(line),
1273 Err(ProcMapError::ParseLine { line: _ })
1274 );
1275 }
1276
1277 #[test]
1278 fn test_proc_map_find_lib_by_name() {
1279 let proc_map_libs = ProcMap {
1280 pid: 0xdead,
1281 data: b"
12827fc4a9800000-7fc4a98ad000 r--p 00000000 00:24 18147308 /usr/lib64/libcrypto.so.3.0.9
1283",
1284 };
1285
1286 assert_matches!(
1287 proc_map_libs.find_library_path_by_name(Path::new("libcrypto.so.3.0.9")),
1288 Ok(Some(path)) => {
1289 assert_eq!(path, "/usr/lib64/libcrypto.so.3.0.9", "path: {}", path.display());
1290 }
1291 );
1292 }
1293
1294 #[test]
1295 fn test_proc_map_find_lib_by_partial_name() {
1296 let proc_map_libs = ProcMap {
1297 pid: 0xdead,
1298 data: b"
12997fc4a9800000-7fc4a98ad000 r--p 00000000 00:24 18147308 /usr/lib64/libcrypto.so.3.0.9
1300",
1301 };
1302
1303 assert_matches!(
1304 proc_map_libs.find_library_path_by_name(Path::new("libcrypto")),
1305 Ok(Some(path)) => {
1306 assert_eq!(path, "/usr/lib64/libcrypto.so.3.0.9", "path: {}", path.display());
1307 }
1308 );
1309 }
1310
1311 #[test]
1312 fn test_proc_map_with_multiple_lib_entries() {
1313 let proc_map_libs = ProcMap {
1314 pid: 0xdead,
1315 data: b"
13167f372868000-7f3722869000 r--p 00000000 00:24 18097875 /usr/lib64/ld-linux-x86-64.so.2
13177f3722869000-7f372288f000 r-xp 00001000 00:24 18097875 /usr/lib64/ld-linux-x86-64.so.2
13187f372288f000-7f3722899000 r--p 00027000 00:24 18097875 /usr/lib64/ld-linux-x86-64.so.2
13197f3722899000-7f372289b000 r--p 00030000 00:24 18097875 /usr/lib64/ld-linux-x86-64.so.2
13207f372289b000-7f372289d000 rw-p 00032000 00:24 18097875 /usr/lib64/ld-linux-x86-64.so.2
1321",
1322 };
1323
1324 assert_matches!(
1325 proc_map_libs.find_library_path_by_name(Path::new("ld-linux-x86-64.so.2")),
1326 Ok(Some(path)) => {
1327 assert_eq!(path, "/usr/lib64/ld-linux-x86-64.so.2", "path: {}", path.display());
1328 }
1329 );
1330 }
1331}