diff --git a/aya/Cargo.toml b/aya/Cargo.toml index 1fce9f645..09dbcf5bf 100644 --- a/aya/Cargo.toml +++ b/aya/Cargo.toml @@ -30,6 +30,7 @@ thiserror = { workspace = true } [dev-dependencies] tempfile = { workspace = true } +test-case = { workspace = true } [package.metadata.docs.rs] all-features = true diff --git a/aya/src/programs/uprobe.rs b/aya/src/programs/uprobe.rs index b9c47ca99..0e0eaa7a1 100644 --- a/aya/src/programs/uprobe.rs +++ b/aya/src/programs/uprobe.rs @@ -1,12 +1,16 @@ //! User space probes. use std::{ + borrow::Cow, error::Error, ffi::{CStr, OsStr, OsString}, fmt::{self, Write}, fs, io::{self, BufRead as _, Cursor, Read as _}, mem, - os::{fd::AsFd as _, unix::ffi::OsStrExt as _}, + os::{ + fd::AsFd as _, + unix::ffi::{OsStrExt as _, OsStringExt as _}, + }, path::{Path, PathBuf}, sync::LazyLock, }; @@ -215,49 +219,6 @@ where }) } -// Only run this test on linux with glibc because only in that configuration do we know that we'll -// be dynamically linked to libc and can exercise resolving the path to libc via the current -// process's memory map. -#[test] -#[cfg_attr( - any(miri, not(all(target_os = "linux", target_env = "gnu"))), - ignore = "requires glibc, doesn't work in miri" -)] -fn test_resolve_attach_path() { - // Look up the current process's pid. - let pid = std::process::id(); - let proc_map = ProcMap::new(pid).unwrap(); - - // Now let's resolve the path to libc. It should exist in the current process's memory map and - // then in the ld.so.cache. - let libc_path = resolve_attach_path("libc".as_ref(), Some(&proc_map)).unwrap_or_else(|err| { - match err.source() { - Some(source) => panic!("{err}: {source}"), - None => panic!("{err}"), - } - }); - - // Make sure we got a path that contains libc. - assert_matches::assert_matches!( - libc_path.to_str(), - Some(libc_path) if libc_path.contains("libc"), - "libc_path: {}", libc_path.display() - ); - - // If we pass an absolute path that doesn't match anything in /proc//maps, we should fall - // back to the provided path instead of erroring out. Using a synthetic absolute path keeps the - // test hermetic. - let synthetic_absolute = Path::new("/tmp/.aya-test-resolve-attach-absolute"); - let absolute_path = - resolve_attach_path(synthetic_absolute, Some(&proc_map)).unwrap_or_else(|err| { - match err.source() { - Some(source) => panic!("{err}: {source}"), - None => panic!("{err}"), - } - }); - assert_eq!(absolute_path, synthetic_absolute); -} - define_link_wrapper!( UProbeLink, UProbeLinkId, @@ -337,10 +298,10 @@ pub enum ProcMapError { ReadFile(#[from] io::Error), /// Error parsing a line of /proc/pid/maps. - #[error("could not parse {:?}", OsStr::from_bytes(line))] + #[error("could not parse {}", line.display())] ParseLine { /// The line that could not be parsed. - line: Vec, + line: OsString, }, } @@ -348,7 +309,7 @@ pub enum ProcMapError { /// /// This contains information about a mapped portion of memory /// for the process, ranging from address to address_end. -#[derive(Debug)] +#[cfg_attr(test, derive(Debug, PartialEq))] struct ProcMapEntry<'a> { #[cfg_attr(not(test), expect(dead_code))] address: u64, @@ -362,7 +323,35 @@ struct ProcMapEntry<'a> { dev: &'a OsStr, #[cfg_attr(not(test), expect(dead_code))] inode: u32, - path: Option<&'a Path>, + path: Option<&'a OsStr>, +} + +/// Split a byte slice on ASCII whitespace up to `n` times. +/// +/// The last item yielded contains the remainder of the slice and may itself +/// contain whitespace. +fn split_ascii_whitespace_n(s: &[u8], mut n: usize) -> impl Iterator { + let mut s = s.trim_ascii_end(); + + std::iter::from_fn(move || { + if n == 0 { + None + } else { + s = s.trim_ascii_start(); + + n -= 1; + Some(if n == 0 { + s + } else if let Some(i) = s.iter().position(|b| b.is_ascii_whitespace()) { + let (next, rest) = s.split_at(i); + s = rest; + next + } else { + n = 0; + s + }) + } + }) } impl<'a> ProcMapEntry<'a> { @@ -370,11 +359,12 @@ impl<'a> ProcMapEntry<'a> { use std::os::unix::ffi::OsStrExt as _; let err = || ProcMapError::ParseLine { - line: line.to_vec(), + line: OsString::from_vec(line.to_vec()), }; - let mut parts = line - .split(|b| b.is_ascii_whitespace()) + let mut parts = + // address, perms, offset, dev, inode, path = 6. + split_ascii_whitespace_n(line, 6) .filter(|part| !part.is_empty()); let mut next = || parts.next().ok_or_else(err); @@ -417,20 +407,7 @@ impl<'a> ProcMapEntry<'a> { .parse() .map_err(|std::num::ParseIntError { .. }| err())?; - let path = parts - .next() - .and_then(|path| match path { - [b'[', .., b']'] => None, - path => { - let path = Path::new(OsStr::from_bytes(path)); - if !path.is_absolute() { - Some(Err(err())) - } else { - Some(Ok(path)) - } - } - }) - .transpose()?; + let path = parts.next().map(OsStr::from_bytes); if let Some(_part) = parts.next() { return Err(err()); @@ -471,10 +448,10 @@ impl> ProcMap { fn libs(&self) -> impl Iterator, ProcMapError>> { let Self { pid: _, data } = self; + // /proc//maps ends with '\n', so split() yields a trailing empty slice without this. data.as_ref() + .trim_ascii() .split(|&b| b == b'\n') - // /proc//maps ends with '\n', so split() yields a trailing empty slice. - .filter(|line| !line.is_empty()) .map(ProcMapEntry::parse) } @@ -497,6 +474,7 @@ impl> ProcMap { path, } = entry?; if let Some(path) = path { + let path = Path::new(path); if let Some(filename) = path.file_name() { if let Some(suffix) = filename.strip_prefix(lib) { if suffix.is_empty() @@ -661,19 +639,19 @@ enum ResolveSymbolError { BuildIdMismatch(String), } -fn construct_debuglink_path(filename: &[u8], main_path: &Path) -> PathBuf { +fn construct_debuglink_path<'a>(filename: &'a [u8], main_path: &Path) -> Cow<'a, Path> { let filename_str = OsStr::from_bytes(filename); let debuglink_path = Path::new(filename_str); if debuglink_path.is_relative() { // If the debug path is relative, resolve it against the parent of the main path main_path.parent().map_or_else( - || PathBuf::from(debuglink_path), // Use original if no parent - |parent| parent.join(debuglink_path), + || debuglink_path.into(), // Use original if no parent + |parent| parent.join(debuglink_path).into(), ) } else { // If the path is not relative, just use original - PathBuf::from(debuglink_path) + debuglink_path.into() } } @@ -698,10 +676,10 @@ fn verify_build_ids<'a>( } fn find_debug_path_in_object<'a>( - obj: &'a object::File<'a>, + obj: &object::File<'a>, main_path: &Path, symbol: &str, -) -> Result { +) -> Result, ResolveSymbolError> { match obj.gnu_debuglink() { Ok(Some((filename, _))) => Ok(construct_debuglink_path(filename, main_path)), Ok(None) => Err(ResolveSymbolError::Unknown(symbol.to_string())), @@ -725,7 +703,7 @@ fn resolve_symbol(path: &Path, symbol: &str) -> Result // Only search in the debug object if the symbol was not found in the main object let debug_path = find_debug_path_in_object(&obj, path, symbol)?; let debug_data = MMap::map_copy_read_only(&debug_path) - .map_err(|e| ResolveSymbolError::DebuglinkAccessError(debug_path, e))?; + .map_err(|e| ResolveSymbolError::DebuglinkAccessError(debug_path.into_owned(), e))?; let debug_obj = object::read::File::parse(debug_data.as_ref())?; verify_build_ids(&obj, &debug_obj, symbol)?; @@ -767,9 +745,47 @@ fn symbol_translated_address( mod tests { use assert_matches::assert_matches; use object::{Architecture, BinaryFormat, Endianness, write::SectionKind}; + use test_case::test_case; use super::*; + // Only run this test on with libc dynamically linked so that it can + // exercise resolving the path to libc via the current process's memory map. + #[test] + #[cfg_attr( + any(miri, not(target_os = "linux"), target_feature = "crt-static"), + ignore = "requires dynamic linkage of libc" + )] + fn test_resolve_attach_path() { + // Look up the current process's pid. + let pid = std::process::id(); + let proc_map = ProcMap::new(pid).expect("failed to get proc map"); + + // Now let's resolve the path to libc. It should exist in the current process's memory map and + // then in the ld.so.cache. + assert_matches!( + resolve_attach_path("libc".as_ref(), Some(&proc_map)), + Ok(path) => { + // Make sure we got a path that contains libc. + assert_matches!( + path.to_str(), + Some(path) if path.contains("libc"), "path: {}", path.display() + ); + } + ); + + // If we pass an absolute path that doesn't match anything in /proc//maps, we should fall + // back to the provided path instead of erroring out. Using a synthetic absolute path keeps the + // test hermetic. + let synthetic_absolute = Path::new("/tmp/.aya-test-resolve-attach-absolute"); + assert_matches!( + resolve_attach_path(synthetic_absolute, Some(&proc_map)), + Ok(path) => { + assert_eq!(path, synthetic_absolute, "path: {}", path.display()); + } + ); + } + #[test] fn test_relative_path_with_parent() { let filename = b"debug_info"; @@ -897,9 +913,13 @@ mod tests { let main_obj = object::File::parse(&*align_bytes).expect("got main obj"); let main_path = Path::new("/path/to/main"); - let result = find_debug_path_in_object(&main_obj, main_path, "symbol"); - assert_eq!(result.unwrap(), Path::new("/path/to/main.debug")); + assert_matches!( + find_debug_path_in_object(&main_obj, main_path, "symbol"), + Ok(path) => { + assert_eq!(&*path, "/path/to/main.debug", "path: {}", path.display()); + } + ); } #[test] @@ -913,7 +933,10 @@ mod tests { let align_bytes = aligned_slice(&mut debug_bytes); let debug_obj = object::File::parse(&*align_bytes).expect("got debug obj"); - verify_build_ids(&main_obj, &debug_obj, "symbol_name").unwrap(); + assert_matches!( + verify_build_ids(&main_obj, &debug_obj, "symbol_name"), + Ok(()) + ); } #[test] @@ -927,99 +950,174 @@ mod tests { let align_bytes = aligned_slice(&mut debug_bytes); let debug_obj = object::File::parse(&*align_bytes).expect("got debug obj"); - assert!(matches!( - verify_build_ids(&main_obj, &debug_obj, "symbol_name"), - Err(ResolveSymbolError::BuildIdMismatch(_)) - )); - } - - #[test] - fn test_parse_proc_map_entry_shared_lib() { - assert_matches!( - ProcMapEntry::parse(b"7ffd6fbea000-7ffd6fbec000 r-xp 00000000 00:00 0 [vdso]"), - Ok(ProcMapEntry { - address: 0x7ffd6fbea000, - address_end: 0x7ffd6fbec000, - perms, - offset: 0, - dev, - inode: 0, - path: None, - }) if perms == "r-xp" && dev == "00:00" - ); - } - - #[test] - fn test_parse_proc_map_entry_absolute_path() { assert_matches!( - ProcMapEntry::parse(b"7f1bca83a000-7f1bca83c000 rw-p 00036000 fd:01 2895508 /usr/lib64/ld-linux-x86-64.so.2"), - Ok(ProcMapEntry { - address: 0x7f1bca83a000, - address_end: 0x7f1bca83c000, - perms, - offset: 0x00036000, - dev, - inode: 2895508, - path: Some(path), - }) if perms == "rw-p" && dev == "fd:01" && path == Path::new("/usr/lib64/ld-linux-x86-64.so.2") + verify_build_ids(&main_obj, &debug_obj, "symbol_name"), + Err(ResolveSymbolError::BuildIdMismatch(ref symbol_name)) if symbol_name == "symbol_name" ); } - #[test] - fn test_parse_proc_map_entry_all_zeros() { - assert_matches!( - ProcMapEntry::parse(b"7f1bca5f9000-7f1bca601000 rw-p 00000000 00:00 0"), - Ok(ProcMapEntry { - address: 0x7f1bca5f9000, - address_end: 0x7f1bca601000, - perms, - offset: 0, - dev, - inode: 0, - path: None, - }) if perms == "rw-p" && dev == "00:00" - ); + #[derive(Debug, Clone, Copy)] + struct ExpectedProcMapEntry { + address: u64, + address_end: u64, + perms: &'static str, + offset: u64, + dev: &'static str, + inode: u32, + path: Option<&'static str>, } - #[test] - fn test_parse_proc_map_entry_parse_errors() { - assert_matches!( - ProcMapEntry::parse(b"zzzz-7ffd6fbea000 r-xp 00000000 00:00 0 [vdso]"), - Err(ProcMapError::ParseLine { line: _ }) - ); - - assert_matches!( - ProcMapEntry::parse(b"zzzz-7ffd6fbea000 r-xp 00000000 00:00 0 [vdso]"), - Err(ProcMapError::ParseLine { line: _ }) - ); - - assert_matches!( - ProcMapEntry::parse(b"7f1bca5f9000-7f1bca601000 r-xp zzzz 00:00 0 [vdso]"), - Err(ProcMapError::ParseLine { line: _ }) - ); - - assert_matches!( - ProcMapEntry::parse(b"7f1bca5f9000-7f1bca601000 r-xp 00000000 00:00 zzzz [vdso]"), - Err(ProcMapError::ParseLine { line: _ }) - ); - - assert_matches!( - ProcMapEntry::parse(b"7f1bca5f90007ffd6fbea000 r-xp 00000000 00:00 0 [vdso]"), - Err(ProcMapError::ParseLine { line: _ }) - ); - - assert_matches!( - ProcMapEntry::parse(b"7f1bca5f9000-7f1bca601000 r-xp 00000000"), - Err(ProcMapError::ParseLine { line: _ }) - ); + #[test_case( + b"7ffd6fbea000-7ffd6fbec000 r-xp 00000000 00:00 0 [vdso]", + ExpectedProcMapEntry { + address: 0x7ffd6fbea000, + address_end: 0x7ffd6fbec000, + perms: "r-xp", + offset: 0, + dev: "00:00", + inode: 0, + path: Some("[vdso]"), + }; + "bracketed_name" + )] + #[test_case( + b"7f1bca83a000-7f1bca83c000 rw-p 00036000 fd:01 2895508 /usr/lib64/ld-linux-x86-64.so.2", + ExpectedProcMapEntry { + address: 0x7f1bca83a000, + address_end: 0x7f1bca83c000, + perms: "rw-p", + offset: 0x00036000, + dev: "fd:01", + inode: 2895508, + path: Some("/usr/lib64/ld-linux-x86-64.so.2"), + }; + "absolute_path" + )] + #[test_case( + b"7f1bca5f9000-7f1bca601000 rw-p 00000000 00:00 0", + ExpectedProcMapEntry { + address: 0x7f1bca5f9000, + address_end: 0x7f1bca601000, + perms: "rw-p", + offset: 0, + dev: "00:00", + inode: 0, + path: None, + }; + "no_path" + )] + #[test_case( + b"7f1bca5f9000-7f1bca601000 rw-p 00000000 00:00 0 deadbeef", + ExpectedProcMapEntry { + address: 0x7f1bca5f9000, + address_end: 0x7f1bca601000, + perms: "rw-p", + offset: 0, + dev: "00:00", + inode: 0, + path: Some("deadbeef"), + }; + "relative_path_token" + )] + #[test_case( + b"7f1bca83a000-7f1bca83c000 rw-p 00036000 fd:01 2895508 /usr/lib/libc.so.6 (deleted)", + ExpectedProcMapEntry { + address: 0x7f1bca83a000, + address_end: 0x7f1bca83c000, + perms: "rw-p", + offset: 0x00036000, + dev: "fd:01", + inode: 2895508, + path: Some("/usr/lib/libc.so.6 (deleted)"), + }; + "deleted_suffix_in_path" + )] + // The path field is the remainder of the line. It may contain whitespace and arbitrary tokens. + #[test_case( + b"71064dc000-71064df000 ---p 00000000 00:00 0 [page size compat] extra", + ExpectedProcMapEntry { + address: 0x71064dc000, + address_end: 0x71064df000, + perms: "---p", + offset: 0, + dev: "00:00", + inode: 0, + path: Some("[page size compat] extra"), + }; + "path_remainder_with_spaces" + )] + #[test_case( + b"724a0000-72aab000 rw-p 00000000 00:00 0 [anon:dalvik-zygote space] (deleted) extra", + ExpectedProcMapEntry { + address: 0x724a0000, + address_end: 0x72aab000, + perms: "rw-p", + offset: 0, + dev: "00:00", + inode: 0, + path: Some("[anon:dalvik-zygote space] (deleted) extra"), + }; + "bracketed_name_with_spaces" + )] + #[test_case( + b"5ba3b000-5da3b000 r--s 00000000 00:01 1033 /memfd:jit-zygote-cache (deleted)", + ExpectedProcMapEntry { + address: 0x5ba3b000, + address_end: 0x5da3b000, + perms: "r--s", + offset: 0, + dev: "00:01", + inode: 1033, + path: Some("/memfd:jit-zygote-cache (deleted)"), + }; + "memfd_deleted" + )] + #[test_case( + b"6cd539c000-6cd559c000 rw-s 00000000 00:01 7215 /dev/ashmem/CursorWindow: /data/user/0/package/databases/kitefly.db (deleted)", + ExpectedProcMapEntry { + address: 0x6cd539c000, + address_end: 0x6cd559c000, + perms: "rw-s", + offset: 0, + dev: "00:01", + inode: 7215, + path: Some("/dev/ashmem/CursorWindow: /data/user/0/package/databases/kitefly.db (deleted)"), + }; + "ashmem_with_spaces" + )] + fn test_parse_proc_map_entry_ok(line: &'static [u8], expected: ExpectedProcMapEntry) { + use std::ffi::OsStr; + + let ExpectedProcMapEntry { + address, + address_end, + perms, + offset, + dev, + inode, + path, + } = expected; - assert_matches!( - ProcMapEntry::parse(b"7f1bca5f9000-7f1bca601000-deadbeef rw-p 00000000 00:00 0"), - Err(ProcMapError::ParseLine { line: _ }) - ); + assert_matches!(ProcMapEntry::parse(line), Ok(entry) if entry == ProcMapEntry { + address, + address_end, + perms: OsStr::new(perms), + offset, + dev: OsStr::new(dev), + inode, + path: path.map(OsStr::new), + }); + } + #[test_case(b"zzzz-7ffd6fbea000 r-xp 00000000 00:00 0 [vdso]"; "bad_address")] + #[test_case(b"7f1bca5f9000-7f1bca601000 r-xp zzzz 00:00 0 [vdso]"; "bad_offset")] + #[test_case(b"7f1bca5f9000-7f1bca601000 r-xp 00000000 00:00 zzzz [vdso]"; "bad_inode")] + #[test_case(b"7f1bca5f90007ffd6fbea000 r-xp 00000000 00:00 0 [vdso]"; "bad_address_range")] + #[test_case(b"7f1bca5f9000-7f1bca601000 r-xp 00000000"; "missing_fields")] + #[test_case(b"7f1bca5f9000-7f1bca601000-deadbeef rw-p 00000000 00:00 0"; "bad_address_delimiter")] + fn test_parse_proc_map_entry_err(line: &'static [u8]) { assert_matches!( - ProcMapEntry::parse(b"7f1bca5f9000-7f1bca601000 rw-p 00000000 00:00 0 deadbeef"), + ProcMapEntry::parse(line), Err(ProcMapError::ParseLine { line: _ }) ); } @@ -1028,12 +1126,16 @@ mod tests { fn test_proc_map_find_lib_by_name() { let proc_map_libs = ProcMap { pid: 0xdead, - data: b"7fc4a9800000-7fc4a98ad000 r--p 00000000 00:24 18147308 /usr/lib64/libcrypto.so.3.0.9", + data: br#" +7fc4a9800000-7fc4a98ad000 r--p 00000000 00:24 18147308 /usr/lib64/libcrypto.so.3.0.9 +"#, }; assert_matches!( proc_map_libs.find_library_path_by_name(Path::new("libcrypto.so.3.0.9")), - Ok(Some(path)) if path == Path::new("/usr/lib64/libcrypto.so.3.0.9") + Ok(Some(path)) => { + assert_eq!(path, "/usr/lib64/libcrypto.so.3.0.9", "path: {}", path.display()); + } ); } @@ -1041,12 +1143,16 @@ mod tests { fn test_proc_map_find_lib_by_partial_name() { let proc_map_libs = ProcMap { pid: 0xdead, - data: b"7fc4a9800000-7fc4a98ad000 r--p 00000000 00:24 18147308 /usr/lib64/libcrypto.so.3.0.9", + data: br#" +7fc4a9800000-7fc4a98ad000 r--p 00000000 00:24 18147308 /usr/lib64/libcrypto.so.3.0.9 +"#, }; assert_matches!( proc_map_libs.find_library_path_by_name(Path::new("libcrypto")), - Ok(Some(path)) if path == Path::new("/usr/lib64/libcrypto.so.3.0.9") + Ok(Some(path)) => { + assert_eq!(path, "/usr/lib64/libcrypto.so.3.0.9", "path: {}", path.display()); + } ); } @@ -1065,7 +1171,9 @@ mod tests { assert_matches!( proc_map_libs.find_library_path_by_name(Path::new("ld-linux-x86-64.so.2")), - Ok(Some(path)) if path == Path::new("/usr/lib64/ld-linux-x86-64.so.2") + Ok(Some(path)) => { + assert_eq!(path, "/usr/lib64/ld-linux-x86-64.so.2", "path: {}", path.display()); + } ); } } diff --git a/xtask/public-api/aya.txt b/xtask/public-api/aya.txt index 6f5165912..339588812 100644 --- a/xtask/public-api/aya.txt +++ b/xtask/public-api/aya.txt @@ -7301,7 +7301,7 @@ pub fn aya::programs::trace_point::TracePointLinkId::from(t: T) -> T pub mod aya::programs::uprobe pub enum aya::programs::uprobe::ProcMapError pub aya::programs::uprobe::ProcMapError::ParseLine -pub aya::programs::uprobe::ProcMapError::ParseLine::line: alloc::vec::Vec +pub aya::programs::uprobe::ProcMapError::ParseLine::line: std::ffi::os_str::OsString pub aya::programs::uprobe::ProcMapError::ReadFile(std::io::error::Error) impl core::convert::From for aya::programs::uprobe::ProcMapError pub fn aya::programs::uprobe::ProcMapError::from(source: std::io::error::Error) -> Self