use std::env;
use std::ffi::CString;
use std::fs::copy;
use std::io;
#[cfg(linux)]
use std::os::unix::ffi::OsStringExt as _;
use std::path::Path;
use std::str;
use blazesym::helper::read_elf_build_id;
use blazesym::normalize;
use blazesym::normalize::NormalizeOpts;
use blazesym::normalize::Normalizer;
use blazesym::symbolize;
use blazesym::Addr;
use blazesym::Mmap;
use blazesym::Pid;
#[cfg(linux)]
use blazesym::__private::find_gettimeofday_in_process;
use blazesym::__private::find_the_answer_fn;
use blazesym::__private::zip;
use scopeguard::defer;
use tempfile::tempdir;
use test_fork::fork;
use test_log::test;
use crate::suite::common::run_unprivileged_process_test;
#[cfg(linux)]
use crate::suite::common::RemoteProcess;
#[test]
fn normalize_unsorted_err() {
let mut addrs = [
libc::atexit as *const () as Addr,
libc::chdir as *const () as Addr,
libc::fopen as *const () 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}");
}
#[test]
fn normalize_unknown_addrs() {
let addrs = [0x500, 0x600];
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);
}
#[cfg(linux)]
#[cfg(not(any(target_arch = "arm", target_arch = "aarch64")))]
#[test]
fn normalization_self() {
fn test(normalizer: &Normalizer) {
let addrs = [
libc::__errno_location as *const () as Addr,
libc::dlopen as *const () as Addr,
libc::fopen as *const () as Addr,
normalize_unknown_addrs as *const () as Addr,
normalization_self as *const () as Addr,
normalize::Normalizer::new as *const () as Addr,
];
let (errno_idx, _) = addrs
.iter()
.enumerate()
.find(|(_idx, addr)| **addr == libc::__errno_location as *const () 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);
}
#[cfg(linux)]
#[test]
fn normalize_elf_addr() {
fn test(so: &str, map_files: bool) {
let test_so = Path::new(&env!("CARGO_MANIFEST_DIR")).join("data").join(so);
let so_cstr = CString::new(test_so.clone().into_os_string().into_vec()).unwrap();
let handle = unsafe { libc::dlopen(so_cstr.as_ptr(), libc::RTLD_NOW) };
assert!(!handle.is_null());
defer!({
let rc = unsafe { libc::dlclose(handle) };
assert_eq!(rc, 0, "{}", io::Error::last_os_error());
});
let the_answer_addr = unsafe { libc::dlsym(handle, "the_answer\0".as_ptr().cast()) };
assert!(!the_answer_addr.is_null());
let opts = NormalizeOpts {
sorted_addrs: true,
map_files,
..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 output = normalized.outputs[0];
let meta = &normalized.meta[output.1];
let path = &meta.as_elf().unwrap().path;
assert_eq!(
path.to_str().unwrap().contains("/map_files/"),
map_files,
"{path:?}"
);
assert_eq!(path.canonicalize().unwrap(), test_so);
let elf = symbolize::source::Elf::new(test_so);
let src = symbolize::source::Source::Elf(elf);
let symbolizer = symbolize::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");
}
for map_files in [false, true] {
test("libtest-so.so", map_files);
test("libtest-so-no-separate-code.so", map_files);
}
}
#[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_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));
}
#[test]
fn normalize_custom_so_in_zip() {
#[track_caller]
fn test(so_name: &str, apk_to_elf: bool, build_ids: bool, cache_build_ids: bool) {
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,
apk_to_elf,
..Default::default()
};
let normalizer = Normalizer::builder()
.enable_build_ids(build_ids)
.enable_build_id_caching(cache_build_ids)
.build();
let normalized = normalizer
.normalize_user_addrs_opts(Pid::Slf, [the_answer_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];
if apk_to_elf {
let elf = meta.as_elf().unwrap();
assert!(elf.path.ends_with(so_name), "{elf:?}");
assert_eq!(elf.build_id.is_some(), build_ids);
} else {
let expected_offset = so.data_offset + sym.file_offset.unwrap();
assert_eq!(output.0, expected_offset);
let expected = normalize::Apk {
path: test_zip.clone(),
_non_exhaustive: (),
};
assert_eq!(meta, &normalize::UserMeta::Apk(expected));
}
let src = if apk_to_elf {
let so_path = Path::new(&env!("CARGO_MANIFEST_DIR"))
.join("data")
.join(so_name);
let elf = symbolize::source::Elf::new(so_path);
let src = symbolize::source::Source::Elf(elf);
src
} else {
let apk = symbolize::source::Apk::new(test_zip);
let src = symbolize::source::Source::Apk(apk);
src
};
let symbolizer = symbolize::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");
}
for (apk_to_elf, build_ids, cache_build_ids) in [
(false, false, false),
(true, false, false),
(true, true, false),
(true, true, true),
] {
test("libtest-so.so", apk_to_elf, build_ids, cache_build_ids);
test(
"libtest-so-no-separate-code.so",
apk_to_elf,
build_ids,
cache_build_ids,
);
}
}
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);
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_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,
);
}
}
}
}
#[test]
fn normalize_deleted_so_proc_maps() {
test_normalize_deleted_so(false)
}
#[test]
#[ignore = "test requires PROCMAP_QUERY ioctl kernel support"]
fn normalize_deleted_so_ioctl() {
test_normalize_deleted_so(true)
}
#[cfg(linux)]
#[test]
fn normalize_build_id_reading() {
fn test(read_build_ids: bool) {
let test_so = Path::new(&env!("CARGO_MANIFEST_DIR"))
.join("data")
.join("libtest-so.so");
let so_cstr = CString::new(test_so.clone().into_os_string().into_vec()).unwrap();
let handle = unsafe { libc::dlopen(so_cstr.as_ptr(), libc::RTLD_NOW) };
assert!(!handle.is_null());
let the_answer_addr = unsafe { libc::dlsym(handle, "the_answer\0".as_ptr().cast()) };
assert!(!the_answer_addr.is_null());
let opts = NormalizeOpts {
sorted_addrs: true,
..Default::default()
};
let normalizer = Normalizer::builder()
.enable_build_ids(read_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 rc = unsafe { libc::dlclose(handle) };
assert_eq!(rc, 0, "{}", io::Error::last_os_error());
let output = normalized.outputs[0];
let meta = &normalized.meta[output.1];
let elf = meta.as_elf().unwrap();
assert_eq!(elf.path, test_so);
if read_build_ids {
let expected = read_elf_build_id(&test_so).unwrap().unwrap();
assert_eq!(elf.build_id.as_ref().unwrap(), &expected);
} else {
assert_eq!(elf.build_id, None);
}
}
test(true);
test(false);
}
#[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 *const () 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:?}");
}
fn normalize_permissionless_impl(pid: Pid, addr: Addr, test_lib: &Path) {
let normalizer = Normalizer::builder().enable_build_ids(true).build();
let opts = NormalizeOpts::default();
let normalized = normalizer
.normalize_user_addrs_opts(pid, &[addr], &opts)
.unwrap();
let output = normalized.outputs[0];
let meta = &normalized.meta[output.1].as_elf().unwrap();
assert_eq!(
meta.build_id,
Some(read_elf_build_id(&test_lib).unwrap().unwrap())
);
}
#[cfg(linux)]
#[fork]
#[test]
fn normalize_process_symbolic_paths() {
run_unprivileged_process_test(normalize_permissionless_impl)
}
#[cfg(linux)]
#[test]
fn normalize_local_vdso_address() {
use libc::gettimeofday;
let addrs = [gettimeofday as *const () as Addr];
let normalizer = Normalizer::new();
let normalized = normalizer.normalize_user_addrs(Pid::Slf, &addrs).unwrap();
assert_eq!(normalized.outputs.len(), 1);
assert_eq!(normalized.meta.len(), 1);
let output = normalized.outputs[0];
let sym = &normalized.meta[output.1].as_sym().unwrap();
assert!(sym.name.ends_with("gettimeofday"), "{sym:?}");
}
#[cfg(linux)]
#[test]
fn normalize_remote_vdso_address() {
let test_block = Path::new(&env!("CARGO_MANIFEST_DIR"))
.join("data")
.join("test-block.bin");
let () = RemoteProcess::default().exec(&test_block, |pid, _addr| {
let addr = find_gettimeofday_in_process(pid);
let normalizer = Normalizer::new();
let normalized = normalizer
.normalize_user_addrs(pid, [addr].as_slice())
.unwrap();
assert_eq!(normalized.outputs.len(), 1);
assert_eq!(normalized.meta.len(), 1);
let output = normalized.outputs[0];
let sym = &normalized.meta[output.1].as_sym().unwrap();
assert!(sym.name.ends_with("gettimeofday"), "{sym:?}");
});
}