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]