diff --git a/Cargo.toml b/Cargo.toml index 4822ff23..d9339110 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,11 +87,18 @@ zstd = ["dep:zstd"] # Below here are dev-mostly features that should not be needed by # regular users. +# Expose test-only helpers for convenient use in end-to-end tests from +# crate. +test = [] # Enable code paths requiring a nightly toolchain. This feature is only meant to # be used for testing and benchmarking purposes, not for the core library, which # is expected to work on stable. nightly = [] +[[test]] +name = "blazesym" +required-features = ["test"] + [[example]] name = "addr2ln" @@ -111,6 +118,7 @@ required-features = ["demangle", "blazesym-dev/generate-unit-test-files"] [[bench]] name = "main" harness = false +required-features = ["test"] [profile.release] debug = true @@ -140,10 +148,6 @@ zstd = {version = "0.13.1", default-features = false, optional = true} # APIs. addr2line = "=0.24.2" anyhow = "1.0.71" -# TODO: Enable `zstd` feature once toolchain support for it is more -# widespread (enabled by default in `ld`). Remove conditionals in -# test code alongside. -blazesym = {path = ".", features = ["apk", "breakpad", "gsym", "tracing"]} blazesym-dev = {path = "dev", features = ["generate-unit-test-files"]} # TODO: Use 0.5.2 once released. criterion = {git = "https://github.com/bheisler/criterion.rs.git", rev = "b913e232edd98780961ecfbae836ec77ede49259", default-features = false, features = ["rayon", "cargo_bench_support"]} diff --git a/dev/Cargo.toml b/dev/Cargo.toml index f1069f82..130ab639 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -40,7 +40,10 @@ zip = {version = "2.0.0", optional = true, default-features = false} zstd = {version = "0.13.1", default-features = false, optional = true} [dependencies] -blazesym = {version = "=0.2.0-rc.2", path = "../"} +# TODO: Enable `zstd` feature once toolchain support for it is more +# widespread (enabled by default in `ld`). Remove conditionals in +# test code alongside. +blazesym = {path = "../", features = ["apk", "breakpad", "gsym", "tracing", "test"]} libc = "0.2.137" [target.'cfg(target_os = "linux")'.dependencies] diff --git a/src/lib.rs b/src/lib.rs index 95d7349c..0ebe26c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,7 +74,7 @@ pub mod normalize; mod once; mod pid; pub mod symbolize; -#[cfg(test)] +#[cfg(any(test, feature = "test"))] mod test_helper; mod util; #[cfg(feature = "apk")] @@ -170,6 +170,16 @@ pub mod __private { pub use crate::util::bytes_to_path; pub use crate::util::stat; pub use crate::util::ReadRaw; + + #[cfg(feature = "apk")] + pub mod zip { + pub use crate::zip::Archive; + } + + #[cfg(feature = "test")] + pub use crate::test_helper::find_the_answer_fn; + #[cfg(feature = "test")] + pub use crate::test_helper::find_the_answer_fn_in_zip; } diff --git a/src/mmap.rs b/src/mmap.rs index 1d87c081..69d650af 100644 --- a/src/mmap.rs +++ b/src/mmap.rs @@ -12,8 +12,9 @@ use crate::ErrorExt as _; use crate::Result; +#[doc(hidden)] #[derive(Debug)] -pub(crate) struct Builder { +pub struct Builder { exec: bool, } @@ -23,14 +24,15 @@ impl Builder { } /// Configure the mapping to be executable. - #[cfg(test)] - pub(crate) fn exec(mut self) -> Self { + #[doc(hidden)] + pub fn exec(mut self) -> Self { self.exec = true; self } /// Memory map the file at the provided `path`. - pub(crate) fn open

(self, path: P) -> Result + #[doc(hidden)] + pub fn open

(self, path: P) -> Result where P: AsRef, { @@ -81,7 +83,8 @@ pub struct Mmap { impl Mmap { /// Create [`Builder`] for creating a customizable memory mapping. - pub(crate) fn builder() -> Builder { + #[doc(hidden)] + pub fn builder() -> Builder { Builder::new() } @@ -93,7 +96,8 @@ impl Mmap { /// Create a new `Mmap` object (sharing the same underlying memory mapping /// as the current one) that restricts its view to the provided `range`. /// Adjustment happens relative to the current view. - pub(crate) fn constrain(&self, range: Range) -> Option { + #[doc(hidden)] + pub fn constrain(&self, range: Range) -> Option { if self.view.start + range.end > self.view.end { return None } diff --git a/src/normalize/normalizer.rs b/src/normalize/normalizer.rs index a3d0a4dd..22939c0f 100644 --- a/src/normalize/normalizer.rs +++ b/src/normalize/normalizer.rs @@ -345,323 +345,3 @@ impl Normalizer { self.normalize_user_addrs_opts(pid, addrs, &NormalizeOpts::default()) } } - - -#[cfg(test)] -mod tests { - use super::*; - - use std::fs::copy; - use std::path::Path; - - use tempfile::tempdir; - - use test_log::test; - - use crate::mmap::Mmap; - use crate::normalize::buildid::read_elf_build_id; - use crate::normalize::Apk; - use crate::normalize::Elf; - use crate::normalize::Reason; - use crate::normalize::Unknown; - use crate::normalize::UserMeta; - use crate::symbolize; - use crate::symbolize::Symbolizer; - use crate::test_helper::find_the_answer_fn; - use crate::zip; - - - /// Check that we detect unsorted input addresses. - #[test] - fn user_address_normalization_unsorted() { - let mut addrs = [ - libc::atexit as Addr, - libc::chdir as Addr, - libc::fopen as Addr, - ]; - let () = addrs.sort(); - let () = addrs.swap(0, 1); - - let opts = NormalizeOpts { - sorted_addrs: true, - ..Default::default() - }; - let normalizer = Normalizer::new(); - let err = normalizer - .normalize_user_addrs_opts(Pid::Slf, addrs.as_slice(), &opts) - .unwrap_err(); - assert!(err.to_string().contains("are not sorted"), "{err}"); - } - - /// Check that we handle unknown addresses as expected. - #[test] - fn user_address_normalization_unknown() { - // The very first page of the address space should never be - // mapped, so use addresses from there. - let addrs = [0x500 as Addr, 0x600 as Addr]; - - let normalizer = Normalizer::new(); - let normalized = normalizer - .normalize_user_addrs(Pid::Slf, addrs.as_slice()) - .unwrap(); - assert_eq!(normalized.outputs.len(), 2); - assert_eq!(normalized.meta.len(), 1); - assert_eq!(normalized.meta[0], Unknown::new(Reason::Unmapped).into()); - assert_eq!(normalized.outputs[0].1, 0); - assert_eq!(normalized.outputs[1].1, 0); - } - - /// Check that we can normalize user addresses. - #[cfg(linux)] - // `libc` on Arm doesn't have `__errno_location`. - #[cfg(not(any(target_arch = "arm", target_arch = "aarch64")))] - #[test] - fn user_address_normalization() { - fn test(normalizer: &Normalizer) { - let addrs = [ - libc::__errno_location as Addr, - libc::dlopen as Addr, - libc::fopen as Addr, - user_address_normalization_unknown as Addr, - user_address_normalization as Addr, - Mmap::map as Addr, - ]; - - let (errno_idx, _) = addrs - .iter() - .enumerate() - .find(|(_idx, addr)| **addr == libc::__errno_location as Addr) - .unwrap(); - - let normalized = normalizer - .normalize_user_addrs(Pid::Slf, addrs.as_slice()) - .unwrap(); - assert_eq!(normalized.outputs.len(), 6); - - let outputs = &normalized.outputs; - let meta = &normalized.meta; - assert_eq!(meta.len(), 2); - - let errno_meta_idx = outputs[errno_idx].1; - assert!(meta[errno_meta_idx] - .as_elf() - .unwrap() - .path - .file_name() - .unwrap() - .to_string_lossy() - .contains("libc.so")); - } - - let normalizer = Normalizer::new(); - test(&normalizer); - - let normalizer = Normalizer::builder().enable_vma_caching(true).build(); - test(&normalizer); - test(&normalizer); - } - - /// Check that we can normalize user addresses in our own shared object. - #[test] - fn user_address_normalization_custom_so() { - let test_so = Path::new(&env!("CARGO_MANIFEST_DIR")) - .join("data") - .join("libtest-so.so"); - - let mmap = Mmap::builder().exec().open(&test_so).unwrap(); - let (sym, the_answer_addr) = find_the_answer_fn(&mmap); - - let normalizer = Normalizer::new(); - let normalized = normalizer - .normalize_user_addrs(Pid::Slf, [the_answer_addr as Addr].as_slice()) - .unwrap(); - assert_eq!(normalized.outputs.len(), 1); - assert_eq!(normalized.meta.len(), 1); - - let output = normalized.outputs[0]; - assert_eq!(output.0, sym.file_offset.unwrap()); - let meta = &normalized.meta[output.1]; - let expected_elf = Elf { - build_id: Some(read_elf_build_id(&test_so).unwrap().unwrap()), - path: test_so.clone(), - _non_exhaustive: (), - }; - assert_eq!(meta, &UserMeta::Elf(expected_elf)); - } - - fn test_user_address_normalization_deleted_so(use_procmap_query: bool) { - fn test( - use_procmap_query: bool, - cache_vmas: bool, - cache_build_ids: bool, - use_map_files: bool, - ) { - let test_so = Path::new(&env!("CARGO_MANIFEST_DIR")) - .join("data") - .join("libtest-so.so"); - let dir = tempdir().unwrap(); - let tmp_so = dir.path().join("libtest-so.so"); - let _count = copy(&test_so, &tmp_so).unwrap(); - - let mmap = Mmap::builder().exec().open(&tmp_so).unwrap(); - let (sym, the_answer_addr) = find_the_answer_fn(&mmap); - - // Remove the temporary directory and with it the mapped shared - // object. - let () = drop(dir); - - let opts = NormalizeOpts { - sorted_addrs: false, - map_files: use_map_files, - ..Default::default() - }; - let normalizer = Normalizer::builder() - .enable_procmap_query(use_procmap_query) - .enable_vma_caching(cache_vmas) - .enable_build_id_caching(cache_build_ids) - .build(); - let normalized = normalizer - .normalize_user_addrs_opts(Pid::Slf, [the_answer_addr as Addr].as_slice(), &opts) - .unwrap(); - assert_eq!(normalized.outputs.len(), 1); - assert_eq!(normalized.meta.len(), 1); - - let output = normalized.outputs[0]; - assert_eq!(output.0, sym.file_offset.unwrap()); - let meta = &normalized.meta[output.1].as_elf().unwrap(); - let expected_build_id = if use_map_files || use_procmap_query { - Some(read_elf_build_id(&test_so).unwrap().unwrap()) - } else { - None - }; - - assert_eq!(meta.build_id, expected_build_id); - } - - for cache_build_ids in [true, false] { - for cache_vmas in [true, false] { - for use_map_files in [true, false] { - let () = test( - use_procmap_query, - cache_build_ids, - cache_vmas, - use_map_files, - ); - } - } - } - } - - /// Check that we can normalize user addresses in a shared object - /// that has been deleted already (but is still mapped) without - /// errors. - #[test] - fn user_address_normalization_deleted_so_proc_maps() { - test_user_address_normalization_deleted_so(false) - } - - /// Check that we can normalize user addresses in a shared object - /// that has been deleted already (but is still mapped) without - /// errors. - #[test] - #[ignore = "test requires PROCMAP_QUERY ioctl kernel support"] - fn user_address_normalization_deleted_so_ioctl() { - test_user_address_normalization_deleted_so(true) - } - - /// Check that we can normalize addresses in our own shared object inside a - /// zip archive. - #[test] - fn normalize_custom_so_in_zip() { - fn test(so_name: &str) { - let test_zip = Path::new(&env!("CARGO_MANIFEST_DIR")) - .join("data") - .join("test.zip"); - - let mmap = Mmap::builder().exec().open(&test_zip).unwrap(); - let archive = zip::Archive::with_mmap(mmap.clone()).unwrap(); - let so = archive - .entries() - .find_map(|entry| { - let entry = entry.unwrap(); - (entry.path == Path::new(so_name)).then_some(entry) - }) - .unwrap(); - - let elf_mmap = mmap - .constrain(so.data_offset..so.data_offset + so.data.len() as u64) - .unwrap(); - let (sym, the_answer_addr) = find_the_answer_fn(&elf_mmap); - - let opts = NormalizeOpts { - sorted_addrs: true, - ..Default::default() - }; - let normalizer = Normalizer::new(); - let normalized = normalizer - .normalize_user_addrs_opts(Pid::Slf, [the_answer_addr as Addr].as_slice(), &opts) - .unwrap(); - assert_eq!(normalized.outputs.len(), 1); - assert_eq!(normalized.meta.len(), 1); - - let expected_offset = so.data_offset + sym.file_offset.unwrap(); - let output = normalized.outputs[0]; - assert_eq!(output.0, expected_offset); - let meta = &normalized.meta[output.1]; - let expected = Apk { - path: test_zip.clone(), - _non_exhaustive: (), - }; - assert_eq!(meta, &UserMeta::Apk(expected)); - - // Also symbolize the normalization output. - let apk = symbolize::Apk::new(test_zip); - let src = symbolize::Source::Apk(apk); - let symbolizer = Symbolizer::new(); - let result = symbolizer - .symbolize_single(&src, symbolize::Input::FileOffset(output.0)) - .unwrap() - .into_sym() - .unwrap(); - - assert_eq!(result.name, "the_answer"); - - let results = symbolizer - .symbolize(&src, symbolize::Input::FileOffset(&[output.0])) - .unwrap(); - assert_eq!(results.len(), 1); - - let sym = results[0].as_sym().unwrap(); - assert_eq!(sym.name, "the_answer"); - } - - test("libtest-so.so"); - test("libtest-so-no-separate-code.so"); - } - - /// Make sure that when using the `map_files` normalization option, - /// we never end up reporting a path referencing "self". - #[test] - fn normalize_no_self_vma_path_reporting() { - let opts = NormalizeOpts { - sorted_addrs: true, - map_files: true, - ..Default::default() - }; - let normalizer = Normalizer::new(); - let normalized = normalizer - .normalize_user_addrs_opts( - Pid::Slf, - [normalize_no_self_vma_path_reporting as Addr].as_slice(), - &opts, - ) - .unwrap(); - - assert_eq!(normalized.outputs.len(), 1); - assert_eq!(normalized.meta.len(), 1); - let output = normalized.outputs[0]; - let meta = &normalized.meta[output.1]; - let elf = meta.as_elf().unwrap(); - assert!(!elf.path.to_string_lossy().contains("self"), "{elf:?}"); - } -} diff --git a/src/test_helper.rs b/src/test_helper.rs index 9a8266e7..0eefa1ef 100644 --- a/src/test_helper.rs +++ b/src/test_helper.rs @@ -16,7 +16,7 @@ use crate::SymType; /// This function returns the symbol information of the function along /// with it's absolute address in the memory mapped region. #[allow(clippy::missing_transmute_annotations)] -pub(crate) fn find_the_answer_fn(mmap: &Mmap) -> (inspect::SymInfo<'static>, Addr) { +pub fn find_the_answer_fn(mmap: &Mmap) -> (inspect::SymInfo<'static>, Addr) { // Look up the address of the `the_answer` function inside of the shared // object. let elf_parser = ElfParser::from_mmap(mmap.clone(), Some(PathBuf::from("libtest-so.so"))); @@ -39,7 +39,9 @@ pub(crate) fn find_the_answer_fn(mmap: &Mmap) -> (inspect::SymInfo<'static>, Add (sym.to_owned(), the_answer_addr as Addr) } -pub(crate) fn find_the_answer_fn_in_zip(mmap: &Mmap) -> (inspect::SymInfo<'static>, Addr) { +/// Find the `the_answer` function inside the provided `mmap`, which is +/// expected to be the memory mapped zip archive. +pub fn find_the_answer_fn_in_zip(mmap: &Mmap) -> (inspect::SymInfo<'static>, Addr) { let archive = zip::Archive::with_mmap(mmap.clone()).unwrap(); let so = archive .entries() diff --git a/src/zip.rs b/src/zip.rs index f92d5c26..f82a25b4 100644 --- a/src/zip.rs +++ b/src/zip.rs @@ -116,6 +116,7 @@ unsafe impl Pod for LocalFileHeader {} /// Carries information on path, compression method, and data corresponding to a /// file in a zip archive. +#[doc(hidden)] pub struct Entry<'archive> { /// Compression method as defined in pkzip spec. 0 means data is /// uncompressed. @@ -256,8 +257,9 @@ impl<'archive> Iterator for EntryIter<'archive> { /// - streaming /// - multi-part ZIP files /// - ZIP64 +#[doc(hidden)] #[derive(Debug)] -pub(crate) struct Archive { +pub struct Archive { mmap: Mmap, cd_offset: u32, cd_records: u16, @@ -275,7 +277,8 @@ impl Archive { } /// Create an `Archive` instance using the provided `Mmap`. - pub(crate) fn with_mmap(mmap: Mmap) -> Result { + #[doc(hidden)] + pub fn with_mmap(mmap: Mmap) -> Result { // Check that a central directory is present as at least some form // of validation that we are in fact dealing with a valid zip file. let (cd_offset, cd_records) = Archive::find_cd(&mmap)?; @@ -346,7 +349,8 @@ impl Archive { } /// Create an iterator over the entries of the archive. - pub(crate) fn entries(&self) -> EntryIter<'_> { + #[doc(hidden)] + pub fn entries(&self) -> impl Iterator>> { let archive_data = &self.mmap; // SANITY: The offset has been validated during construction. let cd_record_data = self.mmap.get(self.cd_offset as usize..).unwrap(); diff --git a/tests/blazesym.rs b/tests/blazesym.rs index 2ace67ea..6ae1f16a 100644 --- a/tests/blazesym.rs +++ b/tests/blazesym.rs @@ -27,6 +27,7 @@ use blazesym::helper::read_elf_build_id; use blazesym::helper::ElfResolver; use blazesym::inspect; use blazesym::inspect::Inspector; +use blazesym::normalize; use blazesym::normalize::NormalizeOpts; use blazesym::normalize::Normalizer; use blazesym::symbolize; @@ -39,9 +40,12 @@ use blazesym::symbolize::Symbolized; use blazesym::symbolize::Symbolizer; use blazesym::Addr; use blazesym::ErrorKind; +use blazesym::Mmap; use blazesym::Pid; use blazesym::Result; use blazesym::SymType; +use blazesym::__private::find_the_answer_fn; +use blazesym::__private::zip; #[cfg(linux)] use blazesym_dev::with_bpf_symbolization_target_addrs; @@ -909,6 +913,103 @@ fn symbolize_normalized_large_memsize() { let _status = child.wait().unwrap(); } +/// Check that we detect unsorted input addresses. +#[test] +fn normalize_unsorted_err() { + let mut addrs = [ + libc::atexit as Addr, + libc::chdir as Addr, + libc::fopen as Addr, + ]; + let () = addrs.sort(); + let () = addrs.swap(0, 1); + + let opts = NormalizeOpts { + sorted_addrs: true, + ..Default::default() + }; + let normalizer = Normalizer::new(); + let err = normalizer + .normalize_user_addrs_opts(Pid::Slf, addrs.as_slice(), &opts) + .unwrap_err(); + assert!(err.to_string().contains("are not sorted"), "{err}"); +} + +/// Check that we handle unknown addresses as expected. +#[test] +fn normalize_unknown_addrs() { + // The very first page of the address space should never be + // mapped, so use addresses from there. + let addrs = [0x500 as Addr, 0x600 as Addr]; + + let normalizer = Normalizer::new(); + let normalized = normalizer + .normalize_user_addrs(Pid::Slf, addrs.as_slice()) + .unwrap(); + assert_eq!(normalized.outputs.len(), 2); + assert_eq!(normalized.meta.len(), 1); + assert_eq!( + normalized.meta[0], + normalize::Unknown { + reason: normalize::Reason::Unmapped, + _non_exhaustive: () + } + .into() + ); + assert_eq!(normalized.outputs[0].1, 0); + assert_eq!(normalized.outputs[1].1, 0); +} + +/// Check that we can normalize user addresses in our own process. +#[cfg(linux)] +// `libc` on Arm doesn't have `__errno_location`. +#[cfg(not(any(target_arch = "arm", target_arch = "aarch64")))] +#[test] +fn normalization_self() { + fn test(normalizer: &Normalizer) { + let addrs = [ + libc::__errno_location as Addr, + libc::dlopen as Addr, + libc::fopen as Addr, + normalize_unknown_addrs as Addr, + normalization_self as Addr, + normalize::Normalizer::new as Addr, + ]; + + let (errno_idx, _) = addrs + .iter() + .enumerate() + .find(|(_idx, addr)| **addr == libc::__errno_location as Addr) + .unwrap(); + + let normalized = normalizer + .normalize_user_addrs(Pid::Slf, addrs.as_slice()) + .unwrap(); + assert_eq!(normalized.outputs.len(), 6); + + let outputs = &normalized.outputs; + let meta = &normalized.meta; + assert_eq!(meta.len(), 2); + + let errno_meta_idx = outputs[errno_idx].1; + assert!(meta[errno_meta_idx] + .as_elf() + .unwrap() + .path + .file_name() + .unwrap() + .to_string_lossy() + .contains("libc.so")); + } + + let normalizer = Normalizer::new(); + test(&normalizer); + + let normalizer = Normalizer::builder().enable_vma_caching(true).build(); + test(&normalizer); + test(&normalizer); +} + /// Check that we can normalize addresses in an ELF shared object. #[cfg(linux)] #[test] @@ -974,6 +1075,178 @@ fn normalize_elf_addr() { } } +/// Check that we can normalize user addresses in our own shared object. +#[test] +fn normalize_custom_so() { + let test_so = Path::new(&env!("CARGO_MANIFEST_DIR")) + .join("data") + .join("libtest-so.so"); + + let mmap = Mmap::builder().exec().open(&test_so).unwrap(); + let (sym, the_answer_addr) = find_the_answer_fn(&mmap); + + let normalizer = Normalizer::new(); + let normalized = normalizer + .normalize_user_addrs(Pid::Slf, [the_answer_addr as Addr].as_slice()) + .unwrap(); + assert_eq!(normalized.outputs.len(), 1); + assert_eq!(normalized.meta.len(), 1); + + let output = normalized.outputs[0]; + assert_eq!(output.0, sym.file_offset.unwrap()); + let meta = &normalized.meta[output.1]; + let expected_elf = normalize::Elf { + build_id: Some(read_elf_build_id(&test_so).unwrap().unwrap()), + path: test_so.clone(), + _non_exhaustive: (), + }; + assert_eq!(meta, &normalize::UserMeta::Elf(expected_elf)); +} + +/// Check that we can normalize addresses in our own shared object inside a +/// zip archive. +#[test] +fn normalize_custom_so_in_zip() { + fn test(so_name: &str) { + let test_zip = Path::new(&env!("CARGO_MANIFEST_DIR")) + .join("data") + .join("test.zip"); + + let mmap = Mmap::builder().exec().open(&test_zip).unwrap(); + let archive = zip::Archive::with_mmap(mmap.clone()).unwrap(); + let so = archive + .entries() + .find_map(|entry| { + let entry = entry.unwrap(); + (entry.path == Path::new(so_name)).then_some(entry) + }) + .unwrap(); + + let elf_mmap = mmap + .constrain(so.data_offset..so.data_offset + so.data.len() as u64) + .unwrap(); + let (sym, the_answer_addr) = find_the_answer_fn(&elf_mmap); + + let opts = NormalizeOpts { + sorted_addrs: true, + ..Default::default() + }; + let normalizer = Normalizer::new(); + let normalized = normalizer + .normalize_user_addrs_opts(Pid::Slf, [the_answer_addr as Addr].as_slice(), &opts) + .unwrap(); + assert_eq!(normalized.outputs.len(), 1); + assert_eq!(normalized.meta.len(), 1); + + let expected_offset = so.data_offset + sym.file_offset.unwrap(); + let output = normalized.outputs[0]; + assert_eq!(output.0, expected_offset); + let meta = &normalized.meta[output.1]; + let expected = normalize::Apk { + path: test_zip.clone(), + _non_exhaustive: (), + }; + assert_eq!(meta, &normalize::UserMeta::Apk(expected)); + + // Also symbolize the normalization output. + let apk = symbolize::Apk::new(test_zip); + let src = symbolize::Source::Apk(apk); + let symbolizer = Symbolizer::new(); + let result = symbolizer + .symbolize_single(&src, symbolize::Input::FileOffset(output.0)) + .unwrap() + .into_sym() + .unwrap(); + + assert_eq!(result.name, "the_answer"); + + let results = symbolizer + .symbolize(&src, symbolize::Input::FileOffset(&[output.0])) + .unwrap(); + assert_eq!(results.len(), 1); + + let sym = results[0].as_sym().unwrap(); + assert_eq!(sym.name, "the_answer"); + } + + test("libtest-so.so"); + test("libtest-so-no-separate-code.so"); +} + +fn test_normalize_deleted_so(use_procmap_query: bool) { + fn test(use_procmap_query: bool, cache_vmas: bool, cache_build_ids: bool, use_map_files: bool) { + let test_so = Path::new(&env!("CARGO_MANIFEST_DIR")) + .join("data") + .join("libtest-so.so"); + let dir = tempdir().unwrap(); + let tmp_so = dir.path().join("libtest-so.so"); + let _count = copy(&test_so, &tmp_so).unwrap(); + + let mmap = Mmap::builder().exec().open(&tmp_so).unwrap(); + let (sym, the_answer_addr) = find_the_answer_fn(&mmap); + + // Remove the temporary directory and with it the mapped shared + // object. + let () = drop(dir); + + let opts = NormalizeOpts { + sorted_addrs: false, + map_files: use_map_files, + ..Default::default() + }; + let normalizer = Normalizer::builder() + .enable_procmap_query(use_procmap_query) + .enable_vma_caching(cache_vmas) + .enable_build_id_caching(cache_build_ids) + .build(); + let normalized = normalizer + .normalize_user_addrs_opts(Pid::Slf, [the_answer_addr as Addr].as_slice(), &opts) + .unwrap(); + assert_eq!(normalized.outputs.len(), 1); + assert_eq!(normalized.meta.len(), 1); + + let output = normalized.outputs[0]; + assert_eq!(output.0, sym.file_offset.unwrap()); + let meta = &normalized.meta[output.1].as_elf().unwrap(); + let expected_build_id = if use_map_files || use_procmap_query { + Some(read_elf_build_id(&test_so).unwrap().unwrap()) + } else { + None + }; + + assert_eq!(meta.build_id, expected_build_id); + } + + for cache_build_ids in [true, false] { + for cache_vmas in [true, false] { + for use_map_files in [true, false] { + let () = test( + use_procmap_query, + cache_build_ids, + cache_vmas, + use_map_files, + ); + } + } + } +} + +/// Check that we can normalize user addresses in a shared object +/// that has been deleted already (but is still mapped) without +/// errors. +#[test] +fn normalize_deleted_so_proc_maps() { + test_normalize_deleted_so(false) +} + +/// Check that we can normalize user addresses in a shared object +/// that has been deleted already (but is still mapped) without +/// errors. +#[test] +#[ignore = "test requires PROCMAP_QUERY ioctl kernel support"] +fn normalize_deleted_so_ioctl() { + test_normalize_deleted_so(true) +} /// Check that we can enable/disable the reading of build IDs. #[cfg(linux)] @@ -1022,6 +1295,31 @@ fn normalize_build_id_reading() { test(false); } +/// Make sure that when using the `map_files` normalization option, +/// we never end up reporting a path referencing "self". +#[test] +fn normalize_no_self_vma_path_reporting() { + let opts = NormalizeOpts { + sorted_addrs: true, + map_files: true, + ..Default::default() + }; + let normalizer = Normalizer::new(); + let normalized = normalizer + .normalize_user_addrs_opts( + Pid::Slf, + [normalize_no_self_vma_path_reporting as Addr].as_slice(), + &opts, + ) + .unwrap(); + + assert_eq!(normalized.outputs.len(), 1); + assert_eq!(normalized.meta.len(), 1); + let output = normalized.outputs[0]; + let meta = &normalized.meta[output.1]; + let elf = meta.as_elf().unwrap(); + assert!(!elf.path.to_string_lossy().contains("self"), "{elf:?}"); +} /// Check that we can look up an address. #[test]