Skip to main content

aya_friday/programs/
uprobe.rs

1//! User space probes.
2use 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/// An user space probe.
38///
39/// User probes are eBPF programs that can be attached to any userspace
40/// function. They can be of two kinds:
41///
42/// - `uprobe`: get attached to the *start* of the target functions
43/// - `uretprobe`: get attached to the *return address* of the target functions
44#[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/// The location in the target object file to which the uprobe is to be
52/// attached.
53#[derive(Debug)]
54pub enum UProbeAttachLocation<'a> {
55    /// The location of the target function in the target object file.
56    Symbol(&'a str),
57    /// The location of the target function in the target object file, offset by
58    /// the given number of bytes.
59    SymbolOffset(&'a str, u64),
60    /// The offset in the target object file, in bytes.
61    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/// The type returned when constructing a [`UProbeAttachLocation`] fails.
77#[derive(Debug, Error)]
78pub enum UProbeAttachLocationError {
79    /// The instruction address is not in the containing section.
80    #[error(
81        "instruction address {instruction_address:#x} is not in section: \
82        before section address {section_address:#x}"
83    )]
84    AddressNotInSection {
85        /// The instruction's virtual address.
86        instruction_address: u64,
87        /// The containing section's virtual address.
88        section_address: u64,
89    },
90
91    /// The computed file offset does not fit in [`u64`].
92    #[error(
93        "computed file offset overflows u64: \
94        {instruction_address:#x} - {section_address:#x} + {section_offset:#x}"
95    )]
96    FileOffsetOverflow {
97        /// The instruction's virtual address.
98        instruction_address: u64,
99        /// The containing section's virtual address.
100        section_address: u64,
101        /// The containing section's file offset.
102        section_offset: u64,
103    },
104}
105
106impl UProbeAttachLocation<'static> {
107    /// Returns an attach location from an ELF virtual address.
108    ///
109    /// The kernel expects uprobes and uretprobes to be attached by object file
110    /// offset. If you have already resolved the instruction to an ELF virtual
111    /// address, pass that address together with the virtual address and file
112    /// offset of the section that contains it.
113    ///
114    /// This returns [`UProbeAttachLocation::AbsoluteOffset`] using the same
115    /// address translation used for uprobe attachment:
116    ///
117    /// ```text
118    /// file_offset = instruction_address - section_address + section_offset
119    /// ```
120    ///
121    /// The arguments correspond to ELF values as follows:
122    ///
123    /// - `instruction_address`: the instruction virtual address, typically a
124    ///   symbol's `st_value`.
125    /// - `section_address`: the containing section's virtual address
126    ///   (`sh_addr`).
127    /// - `section_offset`: the containing section's file offset (`sh_offset`).
128    ///
129    /// `instruction_address` must be greater than or equal to
130    /// `section_address`. The caller must pass the `section_address` and
131    /// `section_offset` for the section containing `instruction_address`.
132    ///
133    /// # Errors
134    ///
135    /// Returns [`UProbeAttachLocationError::AddressNotInSection`] if
136    /// `instruction_address` is less than `section_address`.
137    ///
138    /// Returns [`UProbeAttachLocationError::FileOffsetOverflow`] if the
139    /// computed file offset does not fit in [`u64`].
140    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
163/// Describes a single attachment point along with its optional cookie.
164pub struct UProbeAttachPoint<'a> {
165    /// The actual target location.
166    pub location: UProbeAttachLocation<'a>,
167    /// Optional cookie available via `bpf_get_attach_cookie()`.
168    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/// Specifies which processes a uprobe should fire for.
181#[derive(Debug, Clone, Copy)]
182pub enum UProbeScope {
183    /// Fire for any process that hits the attach point.
184    AllProcesses,
185    /// Fire only when the calling process/thread hits the attach point.
186    CallingProcess,
187    /// Fire only when the given process hits the attach point.
188    OneProcess(NonZeroU32),
189}
190
191impl UProbe {
192    /// The type of the program according to the kernel.
193    pub const PROGRAM_TYPE: ProgramType = ProgramType::KProbe;
194
195    /// Loads the program inside the kernel.
196    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    /// Returns [`ProbeKind::Entry`] if the program is a `uprobe`, or
202    /// [`ProbeKind::Return`] if the program is a `uretprobe`.
203    pub const fn kind(&self) -> ProbeKind {
204        self.kind
205    }
206
207    /// Attaches the program.
208    ///
209    /// Attaches the uprobe to `point` in the `target`. If the attach point is a
210    /// symbol offset, the offset is added to the address of the target function.
211    /// Absolute offsets must already be target object file offsets; use
212    /// [`UProbeAttachLocation::from_virtual_address()`] to construct one from an
213    /// ELF virtual address. `scope` specifies which processes should trigger
214    /// the uprobe.
215    ///
216    /// The `target` argument can be an absolute or relative path to a binary or
217    /// shared library, or a library name (eg: `"libc"`).
218    ///
219    /// If the program is an `uprobe`, it is attached to the *start* address of
220    /// the target function.  Instead if the program is a `uretprobe`, it is
221    /// attached to the return address of the target function.
222    ///
223    /// The returned value can be used to detach, see [`UProbe::detach`].
224    ///
225    /// The cookie is supported since kernel 5.15, and it is made available to
226    /// the eBPF program via the `bpf_get_attach_cookie()` helper. The `point`
227    /// argument may be just a location (no cookie) or a [`UProbeAttachPoint`],
228    /// only the latter sets the cookie explicitly.
229    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            // /proc/0/maps does not exist, so use the real pid for ProcMap
240            // resolution while keeping the kernel's pid=0 sentinel for attach.
241            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        // Keep ProcMap in this scope so resolve_attach_target_basename can return
249        // a path borrowed from the maps buffer without cloning the matched path.
250        // This keeps the borrow alive until attach uses it.
251        let proc_map;
252        // /proc/<pid>/maps entries are matched by basename, so only bare-basename
253        // targets can benefit from the lookup; paths with a directory separator
254        // are passed through unchanged.
255        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    /// Creates a program from a pinned entry on a bpffs.
284    ///
285    /// Existing links will not be populated. To work with existing links you should use [`crate::programs::links::PinnedLink`].
286    ///
287    /// On drop, any managed links are detached and the program is unloaded. This will not result in
288    /// the program being unloaded from the kernel if it is still pinned.
289    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
313// Resolves a bare basename (a single normal path component) to a concrete
314// path via /proc/<pid>/maps and ld.so.cache.
315fn 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/// The type returned when attaching an [`UProbe`] fails.
365#[derive(Debug, Error)]
366pub enum UProbeError {
367    /// There was an error parsing `/etc/ld.so.cache`.
368    #[error("error reading `{}` file", LD_SO_CACHE_FILE)]
369    InvalidLdSoCache {
370        /// the original [`io::Error`].
371        #[source]
372        io_error: &'static io::Error,
373    },
374
375    /// The target program could not be found.
376    #[error("could not resolve uprobe target `{path}`")]
377    InvalidTarget {
378        /// path to target.
379        path: PathBuf,
380    },
381
382    /// There was an error resolving the target symbol.
383    #[error("error resolving symbol")]
384    SymbolError {
385        /// symbol name.
386        symbol: String,
387        /// the original error.
388        #[source]
389        error: Box<dyn Error + Send + Sync>,
390    },
391
392    /// There was an error accessing `filename`.
393    #[error("`{filename}`")]
394    FileError {
395        /// The file name
396        filename: PathBuf,
397        /// The [`io::Error`] returned from the file operation
398        #[source]
399        io_error: io::Error,
400    },
401
402    /// There was en error fetching the memory map for `pid`.
403    #[error("error fetching libs for {pid}")]
404    ProcMap {
405        /// The pid.
406        pid: u32,
407        /// The [`ProcMapError`] that caused the error.
408        #[source]
409        source: ProcMapError,
410    },
411}
412
413/// Error reading from /proc/pid/maps.
414#[derive(Debug, Error)]
415pub enum ProcMapError {
416    /// Unable to read /proc/pid/maps.
417    #[error(transparent)]
418    ReadFile(#[from] io::Error),
419
420    /// Error parsing a line of /proc/pid/maps.
421    #[error("could not parse {}", line.display())]
422    ParseLine {
423        /// The line that could not be parsed.
424        line: OsString,
425    },
426}
427
428/// A entry that has been parsed from /proc/`pid`/maps.
429///
430/// This contains information about a mapped portion of memory
431/// for the process, ranging from address to `address_end`.
432#[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
449/// Split a byte slice on ASCII whitespace up to `n` times.
450///
451/// The last item yielded contains the remainder of the slice and may itself
452/// contain whitespace.
453fn 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            // address, perms, offset, dev, inode, path = 6.
487            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
548/// The memory maps of a process.
549///
550/// This is read from /proc/`pid`/maps.
551///
552/// The information here may be used to resolve addresses to paths.
553struct 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        // /proc/<pid>/maps ends with '\n', so split() yields a trailing empty slice without this.
572        data.as_ref()
573            .trim_ascii()
574            .split(|&b| b == b'\n')
575            .map(ProcMapEntry::parse)
576    }
577
578    // Find the full path of a library by its name.
579    //
580    // This isn't part of the public API since it's really only useful for
581    // attaching uprobes.
582    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        // Check for new format
650        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        // Check for old format
659        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        // If the debug path is relative, resolve it against the parent of the main path
767        main_path.parent().map_or_else(
768            || debuglink_path.into(), // Use original if no parent
769            |parent| parent.join(debuglink_path).into(),
770        )
771    } else {
772        // If the path is not relative, just use original
773        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            // Only perform a comparison if both build IDs are present
788            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        // Only search in the debug object if the symbol was not found in the main object
823        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    // Only run this test on with libc dynamically linked so that it can
872    // exercise resolving the path to libc via the current process's memory map.
873    #[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        // Look up the current process's pid.
880        let pid = std::process::id();
881        let proc_map = ProcMap::new(pid).expect("failed to get proc map");
882
883        // Now let's resolve the path to libc. It should exist in the current process's memory map and
884        // then in the ld.so.cache.
885        assert_matches!(
886            resolve_attach_target_basename("libc".as_ref(), Some(&proc_map)),
887            Ok(path) => {
888                // Make sure we got a path that contains libc.
889                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); // Null terminator
1004
1005        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 /* align */);
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); // Null terminator
1037        note_data.extend_from_slice(build_id);
1038
1039        obj.append_section_data(section_id, &note_data, 4 /* align */);
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 /* fake CRC */)
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    // The path field is the remainder of the line. It may contain whitespace and arbitrary tokens.
1192    #[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}