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 os::{
10 fd::AsFd as _,
11 unix::ffi::{OsStrExt as _, OsStringExt as _},
12 },
13 path::{Path, PathBuf},
14 sync::LazyLock,
15};
16
17use aya_obj::generated::{bpf_link_type, bpf_prog_type::BPF_PROG_TYPE_KPROBE};
18use object::{Object as _, ObjectSection as _, ObjectSymbol as _, Symbol};
19use thiserror::Error;
20
21use crate::{
22 VerifierLogLevel,
23 programs::{
24 FdLink, LinkError, ProgramData, ProgramError, ProgramType, define_link_wrapper,
25 impl_try_into_fdlink, load_program,
26 perf_attach::{PerfLinkIdInner, PerfLinkInner},
27 probe::{OsStringExt as _, Probe, ProbeKind, attach},
28 },
29 sys::bpf_link_get_info_by_fd,
30 util::MMap,
31};
32
33const LD_SO_CACHE_FILE: &str = "/etc/ld.so.cache";
34
35static LD_SO_CACHE: LazyLock<Result<LdSoCache, io::Error>> =
36 LazyLock::new(|| LdSoCache::load(LD_SO_CACHE_FILE));
37const LD_SO_CACHE_HEADER_OLD: &str = "ld.so-1.7.0\0";
38const LD_SO_CACHE_HEADER_NEW: &str = "glibc-ld.so.cache1.1";
39
40#[derive(Debug)]
48#[doc(alias = "BPF_PROG_TYPE_KPROBE")]
49pub struct UProbe {
50 pub(crate) data: ProgramData<UProbeLink>,
51 pub(crate) kind: ProbeKind,
52}
53
54pub enum UProbeAttachLocation<'a> {
57 Symbol(&'a str),
59 SymbolOffset(&'a str, u64),
62 AbsoluteOffset(u64),
64}
65
66impl<'a> From<&'a str> for UProbeAttachLocation<'a> {
67 fn from(s: &'a str) -> Self {
68 Self::Symbol(s)
69 }
70}
71
72impl From<u64> for UProbeAttachLocation<'static> {
73 fn from(offset: u64) -> Self {
74 Self::AbsoluteOffset(offset)
75 }
76}
77
78pub struct UProbeAttachPoint<'a> {
80 pub location: UProbeAttachLocation<'a>,
82 pub cookie: Option<u64>,
84}
85
86impl<'a, L: Into<UProbeAttachLocation<'a>>> From<L> for UProbeAttachPoint<'a> {
87 fn from(location: L) -> Self {
88 Self {
89 location: location.into(),
90 cookie: None,
91 }
92 }
93}
94
95impl UProbe {
96 pub const PROGRAM_TYPE: ProgramType = ProgramType::KProbe;
98
99 pub fn load(&mut self) -> Result<(), ProgramError> {
101 load_program(BPF_PROG_TYPE_KPROBE, &mut self.data)
102 }
103
104 pub const fn kind(&self) -> ProbeKind {
107 self.kind
108 }
109
110 pub fn attach<'a, T: AsRef<Path>, Point: Into<UProbeAttachPoint<'a>>>(
131 &mut self,
132 point: Point,
133 target: T,
134 pid: Option<u32>,
135 ) -> Result<UProbeLinkId, ProgramError> {
136 let UProbeAttachPoint { location, cookie } = point.into();
137 let proc_map = pid.map(ProcMap::new).transpose()?;
138 let path = resolve_attach_path(target.as_ref(), proc_map.as_ref())?;
139 let (symbol, offset) = match location {
140 UProbeAttachLocation::Symbol(s) => (Some(s), 0),
141 UProbeAttachLocation::SymbolOffset(s, offset) => (Some(s), offset),
142 UProbeAttachLocation::AbsoluteOffset(offset) => (None, offset),
143 };
144 let offset = if let Some(symbol) = symbol {
145 let symbol_offset =
146 resolve_symbol(path, symbol).map_err(|error| UProbeError::SymbolError {
147 symbol: symbol.to_string(),
148 error: Box::new(error),
149 })?;
150 symbol_offset + offset
151 } else {
152 offset
153 };
154
155 let Self { data, kind } = self;
156 let path = path.as_os_str();
157 attach::<Self, _>(data, *kind, path, offset, pid, cookie)
158 }
159
160 pub fn from_pin<P: AsRef<Path>>(path: P, kind: ProbeKind) -> Result<Self, ProgramError> {
167 let data = ProgramData::from_pinned_path(path, VerifierLogLevel::default())?;
168 Ok(Self { data, kind })
169 }
170}
171
172impl Probe for UProbe {
173 const PMU: &'static str = "uprobe";
174
175 type Error = UProbeError;
176
177 fn file_error(filename: PathBuf, io_error: io::Error) -> Self::Error {
178 UProbeError::FileError { filename, io_error }
179 }
180
181 fn write_offset<W: Write>(w: &mut W, _: ProbeKind, offset: u64) -> fmt::Result {
182 write!(w, ":{offset:#x}")
183 }
184}
185
186fn resolve_attach_path<'a, 'b, 'c, T>(
187 target: &'a Path,
188 proc_map: Option<&'b ProcMap<T>>,
189) -> Result<&'c Path, UProbeError>
190where
191 'a: 'c,
192 'b: 'c,
193 T: AsRef<[u8]>,
194{
195 proc_map
196 .and_then(|proc_map| {
197 proc_map
198 .find_library_path_by_name(target)
199 .map_err(|source| {
200 let ProcMap { pid, data: _ } = proc_map;
201 let pid = *pid;
202 UProbeError::ProcMap { pid, source }
203 })
204 .transpose()
205 })
206 .or_else(|| target.is_absolute().then(|| Ok(target)))
207 .or_else(|| {
208 LD_SO_CACHE
209 .as_ref()
210 .map_err(|io_error| UProbeError::InvalidLdSoCache { io_error })
211 .map(|cache| cache.resolve(target))
212 .transpose()
213 })
214 .unwrap_or_else(|| {
215 Err(UProbeError::InvalidTarget {
216 path: target.to_owned(),
217 })
218 })
219}
220
221define_link_wrapper!(
222 UProbeLink,
223 UProbeLinkId,
224 PerfLinkInner,
225 PerfLinkIdInner,
226 UProbe,
227);
228
229impl_try_into_fdlink!(UProbeLink, PerfLinkInner);
230
231impl TryFrom<FdLink> for UProbeLink {
232 type Error = LinkError;
233
234 fn try_from(fd_link: FdLink) -> Result<Self, Self::Error> {
235 let info = bpf_link_get_info_by_fd(fd_link.fd.as_fd())?;
236 if info.type_ == (bpf_link_type::BPF_LINK_TYPE_TRACING as u32) {
237 return Ok(Self::new(PerfLinkInner::Fd(fd_link)));
238 }
239 Err(LinkError::InvalidLink)
240 }
241}
242
243#[derive(Debug, Error)]
245pub enum UProbeError {
246 #[error("error reading `{}` file", LD_SO_CACHE_FILE)]
248 InvalidLdSoCache {
249 #[source]
251 io_error: &'static io::Error,
252 },
253
254 #[error("could not resolve uprobe target `{path}`")]
256 InvalidTarget {
257 path: PathBuf,
259 },
260
261 #[error("error resolving symbol")]
263 SymbolError {
264 symbol: String,
266 #[source]
268 error: Box<dyn Error + Send + Sync>,
269 },
270
271 #[error("`{filename}`")]
273 FileError {
274 filename: PathBuf,
276 #[source]
278 io_error: io::Error,
279 },
280
281 #[error("error fetching libs for {pid}")]
283 ProcMap {
284 pid: u32,
286 #[source]
288 source: ProcMapError,
289 },
290}
291
292#[derive(Debug, Error)]
294pub enum ProcMapError {
295 #[error(transparent)]
297 ReadFile(#[from] io::Error),
298
299 #[error("could not parse {}", line.display())]
301 ParseLine {
302 line: OsString,
304 },
305}
306
307#[cfg_attr(test, derive(Debug, PartialEq))]
312struct ProcMapEntry<'a> {
313 #[cfg_attr(not(test), expect(dead_code, reason = "parsed but not exposed"))]
314 address: u64,
315 #[cfg_attr(not(test), expect(dead_code, reason = "parsed but not exposed"))]
316 address_end: u64,
317 #[cfg_attr(not(test), expect(dead_code, reason = "parsed but not exposed"))]
318 perms: &'a OsStr,
319 #[cfg_attr(not(test), expect(dead_code, reason = "parsed but not exposed"))]
320 offset: u64,
321 #[cfg_attr(not(test), expect(dead_code, reason = "parsed but not exposed"))]
322 dev: &'a OsStr,
323 #[cfg_attr(not(test), expect(dead_code, reason = "parsed but not exposed"))]
324 inode: u32,
325 path: Option<&'a OsStr>,
326}
327
328fn split_ascii_whitespace_n(s: &[u8], mut n: usize) -> impl Iterator<Item = &[u8]> {
333 let mut s = s.trim_ascii_end();
334
335 std::iter::from_fn(move || {
336 if n == 0 {
337 None
338 } else {
339 s = s.trim_ascii_start();
340
341 n -= 1;
342 Some(if n == 0 {
343 s
344 } else if let Some(i) = s.iter().position(u8::is_ascii_whitespace) {
345 let (next, rest) = s.split_at(i);
346 s = rest;
347 next
348 } else {
349 n = 0;
350 s
351 })
352 }
353 })
354}
355
356impl<'a> ProcMapEntry<'a> {
357 fn parse(line: &'a [u8]) -> Result<Self, ProcMapError> {
358 use std::os::unix::ffi::OsStrExt as _;
359
360 let err = || ProcMapError::ParseLine {
361 line: OsString::from_vec(line.to_vec()),
362 };
363
364 let mut parts =
365 split_ascii_whitespace_n(line, 6)
367 .filter(|part| !part.is_empty());
368
369 let mut next = || parts.next().ok_or_else(err);
370
371 let (start, end) = {
372 let addr = next()?;
373 let mut addr_parts = addr.split(|b| *b == b'-');
374 let mut next = || {
375 addr_parts
376 .next()
377 .ok_or(())
378 .and_then(|part| {
379 let s =
380 std::str::from_utf8(part).map_err(|std::str::Utf8Error { .. }| ())?;
381 let n = u64::from_str_radix(s, 16)
382 .map_err(|std::num::ParseIntError { .. }| ())?;
383 Ok(n)
384 })
385 .map_err(|()| err())
386 };
387 let start = next()?;
388 let end = next()?;
389 if let Some(_part) = addr_parts.next() {
390 return Err(err());
391 }
392 (start, end)
393 };
394
395 let perms = next()?;
396 let perms = OsStr::from_bytes(perms);
397 let offset = next()?;
398 let offset = std::str::from_utf8(offset).map_err(|std::str::Utf8Error { .. }| err())?;
399 let offset =
400 u64::from_str_radix(offset, 16).map_err(|std::num::ParseIntError { .. }| err())?;
401 let dev = next()?;
402 let dev = OsStr::from_bytes(dev);
403 let inode = next()?;
404 let inode = std::str::from_utf8(inode).map_err(|std::str::Utf8Error { .. }| err())?;
405 let inode = inode
406 .parse()
407 .map_err(|std::num::ParseIntError { .. }| err())?;
408
409 let path = parts.next().map(OsStr::from_bytes);
410
411 if let Some(_part) = parts.next() {
412 return Err(err());
413 }
414
415 Ok(Self {
416 address: start,
417 address_end: end,
418 perms,
419 offset,
420 dev,
421 inode,
422 path,
423 })
424 }
425}
426
427struct ProcMap<T> {
433 pid: u32,
434 data: T,
435}
436
437impl ProcMap<Vec<u8>> {
438 fn new(pid: u32) -> Result<Self, UProbeError> {
439 let filename = PathBuf::from(format!("/proc/{pid}/maps"));
440 let data = fs::read(&filename)
441 .map_err(|io_error| UProbeError::FileError { filename, io_error })?;
442 Ok(Self { pid, data })
443 }
444}
445
446impl<T: AsRef<[u8]>> ProcMap<T> {
447 fn libs(&self) -> impl Iterator<Item = Result<ProcMapEntry<'_>, ProcMapError>> {
448 let Self { pid: _, data } = self;
449
450 data.as_ref()
452 .trim_ascii()
453 .split(|&b| b == b'\n')
454 .map(ProcMapEntry::parse)
455 }
456
457 fn find_library_path_by_name(&self, lib: &Path) -> Result<Option<&Path>, ProcMapError> {
462 let lib = lib.as_os_str();
463 let lib = lib.strip_suffix(OsStr::new(".so")).unwrap_or(lib);
464
465 for entry in self.libs() {
466 let ProcMapEntry {
467 address: _,
468 address_end: _,
469 perms: _,
470 offset: _,
471 dev: _,
472 inode: _,
473 path,
474 } = entry?;
475 if let Some(path) = path {
476 let path = Path::new(path);
477 if let Some(filename) = path.file_name() {
478 if let Some(suffix) = filename.strip_prefix(lib) {
479 if suffix.is_empty()
480 || suffix.starts_with(OsStr::new(".so"))
481 || suffix.starts_with(OsStr::new("-"))
482 {
483 return Ok(Some(path));
484 }
485 }
486 }
487 }
488 }
489 Ok(None)
490 }
491}
492
493#[derive(Debug)]
494pub(crate) struct CacheEntry {
495 key: OsString,
496 value: OsString,
497 _flags: i32,
498}
499
500#[derive(Debug)]
501pub(crate) struct LdSoCache {
502 entries: Vec<CacheEntry>,
503}
504
505impl LdSoCache {
506 fn load<T: AsRef<Path>>(path: T) -> Result<Self, io::Error> {
507 let data = fs::read(path)?;
508 Self::parse(&data)
509 }
510
511 fn parse(data: &[u8]) -> Result<Self, io::Error> {
512 let mut cursor = Cursor::new(data);
513
514 let read_u32 = |cursor: &mut Cursor<_>| -> Result<u32, io::Error> {
515 let mut buf = [0u8; size_of::<u32>()];
516 cursor.read_exact(&mut buf)?;
517
518 Ok(u32::from_ne_bytes(buf))
519 };
520
521 let read_i32 = |cursor: &mut Cursor<_>| -> Result<i32, io::Error> {
522 let mut buf = [0u8; size_of::<i32>()];
523 cursor.read_exact(&mut buf)?;
524
525 Ok(i32::from_ne_bytes(buf))
526 };
527
528 let mut buf = [0u8; LD_SO_CACHE_HEADER_NEW.len()];
530 cursor.read_exact(&mut buf)?;
531 let header = std::str::from_utf8(&buf).map_err(|std::str::Utf8Error { .. }| {
532 io::Error::new(io::ErrorKind::InvalidData, "invalid ld.so.cache header")
533 })?;
534
535 let new_format = header == LD_SO_CACHE_HEADER_NEW;
536
537 if !new_format {
539 cursor.set_position(0);
540 let mut buf = [0u8; LD_SO_CACHE_HEADER_OLD.len()];
541 cursor.read_exact(&mut buf)?;
542 let header = std::str::from_utf8(&buf).map_err(|std::str::Utf8Error { .. }| {
543 io::Error::new(io::ErrorKind::InvalidData, "invalid ld.so.cache header")
544 })?;
545
546 if header != LD_SO_CACHE_HEADER_OLD {
547 return Err(io::Error::new(
548 io::ErrorKind::InvalidData,
549 "invalid ld.so.cache header",
550 ));
551 }
552 }
553
554 let num_entries = read_u32(&mut cursor)?;
555
556 if new_format {
557 cursor.consume(6 * size_of::<u32>());
558 }
559
560 let offset = if new_format {
561 0
562 } else {
563 cursor.position() as usize + num_entries as usize * 12
564 };
565
566 let entries = std::iter::repeat_with(|| {
567 let flags = read_i32(&mut cursor)?;
568 let k_pos = read_u32(&mut cursor)? as usize;
569 let v_pos = read_u32(&mut cursor)? as usize;
570
571 if new_format {
572 cursor.consume(12);
573 }
574
575 let read_str = |pos| {
576 use std::os::unix::ffi::OsStrExt as _;
577 OsStr::from_bytes(
578 unsafe { CStr::from_ptr(cursor.get_ref()[offset + pos..].as_ptr().cast()) }
579 .to_bytes(),
580 )
581 .to_owned()
582 };
583
584 let key = read_str(k_pos);
585 let value = read_str(v_pos);
586
587 Ok::<_, io::Error>(CacheEntry {
588 key,
589 value,
590 _flags: flags,
591 })
592 })
593 .take(num_entries as usize)
594 .collect::<Result<_, _>>()?;
595
596 Ok(Self { entries })
597 }
598
599 fn resolve(&self, lib: &Path) -> Option<&Path> {
600 let Self { entries } = self;
601
602 let lib = lib.as_os_str();
603 let lib = lib.strip_suffix(OsStr::new(".so")).unwrap_or(lib);
604
605 entries
606 .iter()
607 .find_map(|CacheEntry { key, value, _flags }| {
608 let suffix = key.strip_prefix(lib)?;
609 suffix
610 .starts_with(OsStr::new(".so"))
611 .then_some(Path::new(value.as_os_str()))
612 })
613 }
614}
615
616#[derive(Error, Debug)]
617enum ResolveSymbolError {
618 #[error(transparent)]
619 Io(#[from] io::Error),
620
621 #[error("error parsing ELF")]
622 Object(#[from] object::Error),
623
624 #[error("unknown symbol `{0}`")]
625 Unknown(String),
626
627 #[error("symbol `{0}` does not appear in section")]
628 NotInSection(String),
629
630 #[error("symbol `{0}` in section `{1:?}` which has no offset")]
631 SectionFileRangeNone(String, Result<String, object::Error>),
632
633 #[error("failed to access debuglink file `{0}`: `{1}`")]
634 DebuglinkAccessError(PathBuf, io::Error),
635
636 #[error("symbol `{0}` not found, mismatched build IDs in main and debug files")]
637 BuildIdMismatch(String),
638}
639
640fn construct_debuglink_path<'a>(filename: &'a [u8], main_path: &Path) -> Cow<'a, Path> {
641 let filename_str = OsStr::from_bytes(filename);
642 let debuglink_path = Path::new(filename_str);
643
644 if debuglink_path.is_relative() {
645 main_path.parent().map_or_else(
647 || debuglink_path.into(), |parent| parent.join(debuglink_path).into(),
649 )
650 } else {
651 debuglink_path.into()
653 }
654}
655
656fn verify_build_ids<'a>(
657 main_obj: &'a object::File<'a>,
658 debug_obj: &'a object::File<'a>,
659 symbol_name: &str,
660) -> Result<(), ResolveSymbolError> {
661 let main_build_id = main_obj.build_id().ok().flatten();
662 let debug_build_id = debug_obj.build_id().ok().flatten();
663
664 match (debug_build_id, main_build_id) {
665 (Some(debug_build_id), Some(main_build_id)) => {
666 if debug_build_id != main_build_id {
668 return Err(ResolveSymbolError::BuildIdMismatch(symbol_name.to_owned()));
669 }
670 Ok(())
671 }
672 _ => Ok(()),
673 }
674}
675
676fn find_debug_path_in_object<'a>(
677 obj: &object::File<'a>,
678 main_path: &Path,
679 symbol: &str,
680) -> Result<Cow<'a, Path>, ResolveSymbolError> {
681 match obj.gnu_debuglink() {
682 Ok(Some((filename, _))) => Ok(construct_debuglink_path(filename, main_path)),
683 Ok(None) => Err(ResolveSymbolError::Unknown(symbol.to_string())),
684 Err(err) => Err(ResolveSymbolError::Object(err)),
685 }
686}
687
688fn find_symbol_in_object<'a>(obj: &'a object::File<'a>, symbol: &str) -> Option<Symbol<'a, 'a>> {
689 obj.dynamic_symbols()
690 .chain(obj.symbols())
691 .find(|sym| sym.name().is_ok_and(|name| name == symbol))
692}
693
694fn resolve_symbol(path: &Path, symbol: &str) -> Result<u64, ResolveSymbolError> {
695 let data = MMap::map_copy_read_only(path)?;
696 let obj = object::read::File::parse(data.as_ref())?;
697
698 if let Some(sym) = find_symbol_in_object(&obj, symbol) {
699 symbol_translated_address(&obj, sym, symbol)
700 } else {
701 let debug_path = find_debug_path_in_object(&obj, path, symbol)?;
703 let debug_data = MMap::map_copy_read_only(&debug_path)
704 .map_err(|e| ResolveSymbolError::DebuglinkAccessError(debug_path.into_owned(), e))?;
705 let debug_obj = object::read::File::parse(debug_data.as_ref())?;
706
707 verify_build_ids(&obj, &debug_obj, symbol)?;
708
709 let sym = find_symbol_in_object(&debug_obj, symbol)
710 .ok_or_else(|| ResolveSymbolError::Unknown(symbol.to_string()))?;
711
712 symbol_translated_address(&debug_obj, sym, symbol)
713 }
714}
715
716fn symbol_translated_address(
717 obj: &object::File<'_>,
718 sym: Symbol<'_, '_>,
719 symbol_name: &str,
720) -> Result<u64, ResolveSymbolError> {
721 let needs_addr_translation = matches!(
722 obj.kind(),
723 object::ObjectKind::Dynamic | object::ObjectKind::Executable
724 );
725 if needs_addr_translation {
726 let index = sym
727 .section_index()
728 .ok_or_else(|| ResolveSymbolError::NotInSection(symbol_name.to_string()))?;
729 let section = obj.section_by_index(index)?;
730 let (offset, _size) = section.file_range().ok_or_else(|| {
731 ResolveSymbolError::SectionFileRangeNone(
732 symbol_name.to_string(),
733 section.name().map(str::to_owned),
734 )
735 })?;
736 Ok(sym.address() - section.address() + offset)
737 } else {
738 Ok(sym.address())
739 }
740}
741
742#[cfg(test)]
743mod tests {
744 use assert_matches::assert_matches;
745 use object::{Architecture, BinaryFormat, Endianness, write::SectionKind};
746 use test_case::test_case;
747
748 use super::*;
749
750 #[test]
753 #[cfg_attr(
754 any(miri, not(target_os = "linux"), target_feature = "crt-static"),
755 ignore = "requires dynamic linkage of libc"
756 )]
757 fn test_resolve_attach_path() {
758 let pid = std::process::id();
760 let proc_map = ProcMap::new(pid).expect("failed to get proc map");
761
762 assert_matches!(
765 resolve_attach_path("libc".as_ref(), Some(&proc_map)),
766 Ok(path) => {
767 assert_matches!(
769 path.to_str(),
770 Some(path) if path.contains("libc"), "path: {}", path.display()
771 );
772 }
773 );
774
775 let synthetic_absolute = Path::new("/tmp/.aya-test-resolve-attach-absolute");
779 assert_matches!(
780 resolve_attach_path(synthetic_absolute, Some(&proc_map)),
781 Ok(path) => {
782 assert_eq!(path, synthetic_absolute, "path: {}", path.display());
783 }
784 );
785 }
786
787 #[test]
788 fn test_relative_path_with_parent() {
789 let filename = b"debug_info";
790 let main_path = Path::new("/usr/lib/main_binary");
791 let expected = Path::new("/usr/lib/debug_info");
792
793 let result = construct_debuglink_path(filename, main_path);
794 assert_eq!(
795 result, expected,
796 "The debug path should resolve relative to the main path's parent"
797 );
798 }
799
800 #[test]
801 fn test_relative_path_without_parent() {
802 let filename = b"debug_info";
803 let main_path = Path::new("main_binary");
804 let expected = Path::new("debug_info");
805
806 let result = construct_debuglink_path(filename, main_path);
807 assert_eq!(
808 result, expected,
809 "The debug path should be the original path as there is no parent"
810 );
811 }
812
813 #[test]
814 fn test_absolute_path() {
815 let filename = b"/absolute/path/to/debug_info";
816 let main_path = Path::new("/usr/lib/main_binary");
817 let expected = Path::new("/absolute/path/to/debug_info");
818
819 let result = construct_debuglink_path(filename, main_path);
820 assert_eq!(
821 result, expected,
822 "The debug path should be the same as the input absolute path"
823 );
824 }
825
826 #[expect(
827 clippy::little_endian_bytes,
828 reason = "ELF debuglink fields are encoded as little-endian"
829 )]
830 fn create_elf_with_debuglink(
831 debug_filename: &[u8],
832 crc: u32,
833 ) -> Result<Vec<u8>, object::write::Error> {
834 let mut obj =
835 object::write::Object::new(BinaryFormat::Elf, Architecture::X86_64, Endianness::Little);
836
837 let section_name = b".gnu_debuglink";
838
839 let section_id = obj.add_section(vec![], section_name.to_vec(), SectionKind::Note);
840
841 let mut debuglink_data = Vec::new();
842
843 debuglink_data.extend_from_slice(debug_filename);
844 debuglink_data.push(0); while debuglink_data.len() % 4 != 0 {
847 debuglink_data.push(0);
848 }
849
850 debuglink_data.extend(&crc.to_le_bytes());
851
852 obj.append_section_data(section_id, &debuglink_data, 4 );
853
854 obj.write()
855 }
856
857 #[expect(
858 clippy::little_endian_bytes,
859 reason = "ELF note headers are encoded as little-endian"
860 )]
861 fn create_elf_with_build_id(build_id: &[u8]) -> Result<Vec<u8>, object::write::Error> {
862 let mut obj =
863 object::write::Object::new(BinaryFormat::Elf, Architecture::X86_64, Endianness::Little);
864
865 let section_name = b".note.gnu.build-id";
866
867 let section_id = obj.add_section(vec![], section_name.to_vec(), SectionKind::Note);
868
869 let mut note_data = Vec::new();
870 let build_id_name = b"GNU";
871
872 note_data.extend(&(build_id_name.len() as u32 + 1).to_le_bytes());
873 note_data.extend(&(build_id.len() as u32).to_le_bytes());
874 note_data.extend(&3u32.to_le_bytes());
875
876 note_data.extend_from_slice(build_id_name);
877 note_data.push(0); note_data.extend_from_slice(build_id);
879
880 obj.append_section_data(section_id, ¬e_data, 4 );
881
882 obj.write()
883 }
884
885 fn aligned_slice(vec: &mut Vec<u8>) -> &mut [u8] {
886 let alignment = 8;
887
888 let original_size = vec.len();
889 let total_size = original_size + alignment - 1;
890
891 if vec.capacity() < total_size {
892 vec.reserve(total_size - vec.capacity());
893 }
894
895 if vec.len() < total_size {
896 vec.resize(total_size, 0);
897 }
898
899 let ptr = vec.as_ptr() as usize;
900
901 let aligned_ptr = ptr.next_multiple_of(alignment);
902
903 let offset = aligned_ptr - ptr;
904
905 if offset > 0 {
906 let tmp = vec.len();
907 vec.copy_within(0..tmp - offset, offset);
908 }
909
910 &mut vec[offset..offset + original_size]
911 }
912
913 #[test]
914 fn test_find_debug_path_success() {
915 let debug_filepath = b"main.debug";
916 let mut main_bytes = create_elf_with_debuglink(debug_filepath, 0x123 )
917 .expect("got main_bytes");
918 let align_bytes = aligned_slice(&mut main_bytes);
919 let main_obj = object::File::parse(&*align_bytes).expect("got main obj");
920
921 let main_path = Path::new("/path/to/main");
922
923 assert_matches!(
924 find_debug_path_in_object(&main_obj, main_path, "symbol"),
925 Ok(path) => {
926 assert_eq!(&*path, "/path/to/main.debug", "path: {}", path.display());
927 }
928 );
929 }
930
931 #[test]
932 fn test_verify_build_ids_same() {
933 let build_id = b"test_build_id";
934 let mut main_bytes = create_elf_with_build_id(build_id).expect("got main_bytes");
935 let align_bytes = aligned_slice(&mut main_bytes);
936 let main_obj = object::File::parse(&*align_bytes).expect("got main obj");
937 let debug_build_id = b"test_build_id";
938 let mut debug_bytes = create_elf_with_build_id(debug_build_id).expect("got debug bytes");
939 let align_bytes = aligned_slice(&mut debug_bytes);
940 let debug_obj = object::File::parse(&*align_bytes).expect("got debug obj");
941
942 assert_matches!(
943 verify_build_ids(&main_obj, &debug_obj, "symbol_name"),
944 Ok(())
945 );
946 }
947
948 #[test]
949 fn test_verify_build_ids_different() {
950 let build_id = b"main_build_id";
951 let mut main_bytes = create_elf_with_build_id(build_id).expect("got main_bytes");
952 let align_bytes = aligned_slice(&mut main_bytes);
953 let main_obj = object::File::parse(&*align_bytes).expect("got main obj");
954 let debug_build_id = b"debug_build_id";
955 let mut debug_bytes = create_elf_with_build_id(debug_build_id).expect("got debug bytes");
956 let align_bytes = aligned_slice(&mut debug_bytes);
957 let debug_obj = object::File::parse(&*align_bytes).expect("got debug obj");
958
959 assert_matches!(
960 verify_build_ids(&main_obj, &debug_obj, "symbol_name"),
961 Err(ResolveSymbolError::BuildIdMismatch(symbol_name)) if symbol_name == "symbol_name"
962 );
963 }
964
965 #[derive(Debug, Clone, Copy)]
966 struct ExpectedProcMapEntry {
967 address: u64,
968 address_end: u64,
969 perms: &'static str,
970 offset: u64,
971 dev: &'static str,
972 inode: u32,
973 path: Option<&'static str>,
974 }
975
976 #[test_case(
977 b"7ffd6fbea000-7ffd6fbec000 r-xp 00000000 00:00 0 [vdso]",
978 ExpectedProcMapEntry {
979 address: 0x7ffd6fbea000,
980 address_end: 0x7ffd6fbec000,
981 perms: "r-xp",
982 offset: 0,
983 dev: "00:00",
984 inode: 0,
985 path: Some("[vdso]"),
986 };
987 "bracketed_name"
988 )]
989 #[test_case(
990 b"7f1bca83a000-7f1bca83c000 rw-p 00036000 fd:01 2895508 /usr/lib64/ld-linux-x86-64.so.2",
991 ExpectedProcMapEntry {
992 address: 0x7f1bca83a000,
993 address_end: 0x7f1bca83c000,
994 perms: "rw-p",
995 offset: 0x00036000,
996 dev: "fd:01",
997 inode: 2895508,
998 path: Some("/usr/lib64/ld-linux-x86-64.so.2"),
999 };
1000 "absolute_path"
1001 )]
1002 #[test_case(
1003 b"7f1bca5f9000-7f1bca601000 rw-p 00000000 00:00 0",
1004 ExpectedProcMapEntry {
1005 address: 0x7f1bca5f9000,
1006 address_end: 0x7f1bca601000,
1007 perms: "rw-p",
1008 offset: 0,
1009 dev: "00:00",
1010 inode: 0,
1011 path: None,
1012 };
1013 "no_path"
1014 )]
1015 #[test_case(
1016 b"7f1bca5f9000-7f1bca601000 rw-p 00000000 00:00 0 deadbeef",
1017 ExpectedProcMapEntry {
1018 address: 0x7f1bca5f9000,
1019 address_end: 0x7f1bca601000,
1020 perms: "rw-p",
1021 offset: 0,
1022 dev: "00:00",
1023 inode: 0,
1024 path: Some("deadbeef"),
1025 };
1026 "relative_path_token"
1027 )]
1028 #[test_case(
1029 b"7f1bca83a000-7f1bca83c000 rw-p 00036000 fd:01 2895508 /usr/lib/libc.so.6 (deleted)",
1030 ExpectedProcMapEntry {
1031 address: 0x7f1bca83a000,
1032 address_end: 0x7f1bca83c000,
1033 perms: "rw-p",
1034 offset: 0x00036000,
1035 dev: "fd:01",
1036 inode: 2895508,
1037 path: Some("/usr/lib/libc.so.6 (deleted)"),
1038 };
1039 "deleted_suffix_in_path"
1040 )]
1041 #[test_case(
1043 b"71064dc000-71064df000 ---p 00000000 00:00 0 [page size compat] extra",
1044 ExpectedProcMapEntry {
1045 address: 0x71064dc000,
1046 address_end: 0x71064df000,
1047 perms: "---p",
1048 offset: 0,
1049 dev: "00:00",
1050 inode: 0,
1051 path: Some("[page size compat] extra"),
1052 };
1053 "path_remainder_with_spaces"
1054 )]
1055 #[test_case(
1056 b"724a0000-72aab000 rw-p 00000000 00:00 0 [anon:dalvik-zygote space] (deleted) extra",
1057 ExpectedProcMapEntry {
1058 address: 0x724a0000,
1059 address_end: 0x72aab000,
1060 perms: "rw-p",
1061 offset: 0,
1062 dev: "00:00",
1063 inode: 0,
1064 path: Some("[anon:dalvik-zygote space] (deleted) extra"),
1065 };
1066 "bracketed_name_with_spaces"
1067 )]
1068 #[test_case(
1069 b"5ba3b000-5da3b000 r--s 00000000 00:01 1033 /memfd:jit-zygote-cache (deleted)",
1070 ExpectedProcMapEntry {
1071 address: 0x5ba3b000,
1072 address_end: 0x5da3b000,
1073 perms: "r--s",
1074 offset: 0,
1075 dev: "00:01",
1076 inode: 1033,
1077 path: Some("/memfd:jit-zygote-cache (deleted)"),
1078 };
1079 "memfd_deleted"
1080 )]
1081 #[test_case(
1082 b"6cd539c000-6cd559c000 rw-s 00000000 00:01 7215 /dev/ashmem/CursorWindow: /data/user/0/package/databases/kitefly.db (deleted)",
1083 ExpectedProcMapEntry {
1084 address: 0x6cd539c000,
1085 address_end: 0x6cd559c000,
1086 perms: "rw-s",
1087 offset: 0,
1088 dev: "00:01",
1089 inode: 7215,
1090 path: Some("/dev/ashmem/CursorWindow: /data/user/0/package/databases/kitefly.db (deleted)"),
1091 };
1092 "ashmem_with_spaces"
1093 )]
1094 fn test_parse_proc_map_entry_ok(line: &'static [u8], expected: ExpectedProcMapEntry) {
1095 use std::ffi::OsStr;
1096
1097 let ExpectedProcMapEntry {
1098 address,
1099 address_end,
1100 perms,
1101 offset,
1102 dev,
1103 inode,
1104 path,
1105 } = expected;
1106
1107 assert_matches!(ProcMapEntry::parse(line), Ok(entry) if entry == ProcMapEntry {
1108 address,
1109 address_end,
1110 perms: OsStr::new(perms),
1111 offset,
1112 dev: OsStr::new(dev),
1113 inode,
1114 path: path.map(OsStr::new),
1115 });
1116 }
1117
1118 #[test_case(b"zzzz-7ffd6fbea000 r-xp 00000000 00:00 0 [vdso]"; "bad_address")]
1119 #[test_case(b"7f1bca5f9000-7f1bca601000 r-xp zzzz 00:00 0 [vdso]"; "bad_offset")]
1120 #[test_case(b"7f1bca5f9000-7f1bca601000 r-xp 00000000 00:00 zzzz [vdso]"; "bad_inode")]
1121 #[test_case(b"7f1bca5f90007ffd6fbea000 r-xp 00000000 00:00 0 [vdso]"; "bad_address_range")]
1122 #[test_case(b"7f1bca5f9000-7f1bca601000 r-xp 00000000"; "missing_fields")]
1123 #[test_case(b"7f1bca5f9000-7f1bca601000-deadbeef rw-p 00000000 00:00 0"; "bad_address_delimiter")]
1124 fn test_parse_proc_map_entry_err(line: &'static [u8]) {
1125 assert_matches!(
1126 ProcMapEntry::parse(line),
1127 Err(ProcMapError::ParseLine { line: _ })
1128 );
1129 }
1130
1131 #[test]
1132 fn test_proc_map_find_lib_by_name() {
1133 let proc_map_libs = ProcMap {
1134 pid: 0xdead,
1135 data: b"
11367fc4a9800000-7fc4a98ad000 r--p 00000000 00:24 18147308 /usr/lib64/libcrypto.so.3.0.9
1137",
1138 };
1139
1140 assert_matches!(
1141 proc_map_libs.find_library_path_by_name(Path::new("libcrypto.so.3.0.9")),
1142 Ok(Some(path)) => {
1143 assert_eq!(path, "/usr/lib64/libcrypto.so.3.0.9", "path: {}", path.display());
1144 }
1145 );
1146 }
1147
1148 #[test]
1149 fn test_proc_map_find_lib_by_partial_name() {
1150 let proc_map_libs = ProcMap {
1151 pid: 0xdead,
1152 data: b"
11537fc4a9800000-7fc4a98ad000 r--p 00000000 00:24 18147308 /usr/lib64/libcrypto.so.3.0.9
1154",
1155 };
1156
1157 assert_matches!(
1158 proc_map_libs.find_library_path_by_name(Path::new("libcrypto")),
1159 Ok(Some(path)) => {
1160 assert_eq!(path, "/usr/lib64/libcrypto.so.3.0.9", "path: {}", path.display());
1161 }
1162 );
1163 }
1164
1165 #[test]
1166 fn test_proc_map_with_multiple_lib_entries() {
1167 let proc_map_libs = ProcMap {
1168 pid: 0xdead,
1169 data: b"
11707f372868000-7f3722869000 r--p 00000000 00:24 18097875 /usr/lib64/ld-linux-x86-64.so.2
11717f3722869000-7f372288f000 r-xp 00001000 00:24 18097875 /usr/lib64/ld-linux-x86-64.so.2
11727f372288f000-7f3722899000 r--p 00027000 00:24 18097875 /usr/lib64/ld-linux-x86-64.so.2
11737f3722899000-7f372289b000 r--p 00030000 00:24 18097875 /usr/lib64/ld-linux-x86-64.so.2
11747f372289b000-7f372289d000 rw-p 00032000 00:24 18097875 /usr/lib64/ld-linux-x86-64.so.2
1175",
1176 };
1177
1178 assert_matches!(
1179 proc_map_libs.find_library_path_by_name(Path::new("ld-linux-x86-64.so.2")),
1180 Ok(Some(path)) => {
1181 assert_eq!(path, "/usr/lib64/ld-linux-x86-64.so.2", "path: {}", path.display());
1182 }
1183 );
1184 }
1185}