From 1020bf4714d07a61d92e237a45fe948dc5c69a5c Mon Sep 17 00:00:00 2001 From: sudoBash418 Date: Sun, 1 Sep 2024 23:43:38 -0600 Subject: [PATCH] Add support for symbolicating APK/ZIP-embedded libraries on Android By default, modern Android build tools will store native libraries uncompressed, and the [loader][1] will map them directly from the APK (instead of the package manager extracting them on installation). This commit adds support for symbolicating these embedded libraries. To avoid parsing ZIP structures, the offset of the library within the archive is determined via /proc/self/maps. [1]: https://cs.android.com/search?q=open_library_in_zipfile&ss=android%2Fplatform%2Fsuperproject%2Fmain --- src/symbolize/gimli.rs | 21 +++++----- src/symbolize/gimli/elf.rs | 38 ++++++++++++++++++ src/symbolize/gimli/libs_dl_iterate_phdr.rs | 39 +++++++++++++++++-- src/symbolize/gimli/mmap_unix.rs | 16 ++++++++ .../gimli/parse_running_mmaps_unix.rs | 5 +++ 5 files changed, 105 insertions(+), 14 deletions(-) diff --git a/src/symbolize/gimli.rs b/src/symbolize/gimli.rs index 8c7051d4d..bd7a9190d 100644 --- a/src/symbolize/gimli.rs +++ b/src/symbolize/gimli.rs @@ -269,6 +269,8 @@ struct Cache { struct Library { name: OsString, + #[cfg(target_os = "android")] + zip_offset: usize, #[cfg(target_os = "aix")] /// On AIX, the library mmapped can be a member of a big-archive file. /// For example, with a big-archive named libfoo.a containing libbar.so, @@ -295,17 +297,16 @@ struct LibrarySegment { len: usize, } -#[cfg(target_os = "aix")] fn create_mapping(lib: &Library) -> Option { - let name = &lib.name; - let member_name = &lib.member_name; - Mapping::new(name.as_ref(), member_name) -} - -#[cfg(not(target_os = "aix"))] -fn create_mapping(lib: &Library) -> Option { - let name = &lib.name; - Mapping::new(name.as_ref()) + cfg_if::cfg_if! { + if #[cfg(target_os = "aix")] { + Mapping::new(lib.name.as_ref(), &lib.member_name) + } else if #[cfg(target_os = "android")] { + Mapping::new_android(lib.name.as_ref(), lib.zip_offset) + } else { + Mapping::new(lib.name.as_ref()) + } + } } // unsafe because this is required to be externally synchronized diff --git a/src/symbolize/gimli/elf.rs b/src/symbolize/gimli/elf.rs index 906a30054..39a862c0a 100644 --- a/src/symbolize/gimli/elf.rs +++ b/src/symbolize/gimli/elf.rs @@ -43,6 +43,44 @@ impl Mapping { }) } + /// On Android, shared objects can be loaded directly from a + /// ZIP archive. For example, an app may load a library from + /// `/data/app/com.example/base.apk!/lib/x86_64/mylib.so` + /// + /// For one of these "ZIP-embedded" libraries, `zip_offset` will be + /// non-zero (see [super::libs_dl_iterate_phdr]). + #[cfg(target_os = "android")] + pub fn new_android(path: &Path, zip_offset: usize) -> Option { + fn map_embedded_library(path: &Path, zip_offset: usize) -> Option { + // get path of ZIP archive (delimited by `!/`) + let raw_path = path.as_os_str().as_bytes(); + let zip_path = raw_path + .windows(2) + .enumerate() + .find(|(_, chunk)| chunk == b"!/") + .map(|(index, _)| Path::new(OsStr::from_bytes(raw_path.split_at(index).0)))?; + + let file = fs::File::open(zip_path).ok()?; + let len: usize = file.metadata().ok()?.len().try_into().ok()?; + + // NOTE: we map the remainder of the entire archive instead of just the library so we don't have to determine its length + // NOTE: mmap will fail if `zip_offset` is not page-aligned + let map = + unsafe { super::mmap::Mmap::map_with_offset(&file, len - zip_offset, zip_offset) }?; + + Mapping::mk(map, |map, stash| { + Context::new(stash, Object::parse(&map)?, None, None) + }) + } + + // if ZIP offset is non-zero, try mapping as a ZIP-embedded library + if zip_offset > 0 { + map_embedded_library(path, zip_offset).or_else(|| Self::new(path)) + } else { + Self::new(path) + } + } + /// Load debuginfo from an external debug file. fn new_debug(original_path: &Path, path: PathBuf, crc: Option) -> Option { let map = super::mmap(&path)?; diff --git a/src/symbolize/gimli/libs_dl_iterate_phdr.rs b/src/symbolize/gimli/libs_dl_iterate_phdr.rs index e15750ec4..f3fa6722d 100644 --- a/src/symbolize/gimli/libs_dl_iterate_phdr.rs +++ b/src/symbolize/gimli/libs_dl_iterate_phdr.rs @@ -9,12 +9,21 @@ use super::mystd::os::unix::prelude::*; use super::{Library, LibrarySegment, OsString, Vec}; use core::slice; +struct CallbackData { + ret: Vec, + #[cfg(target_os = "android")] + maps: Option>, +} pub(super) fn native_libraries() -> Vec { - let mut ret = Vec::new(); + let mut cb_data = CallbackData { + ret: Vec::new(), + #[cfg(target_os = "android")] + maps: super::parse_running_mmaps::parse_maps().ok(), + }; unsafe { - libc::dl_iterate_phdr(Some(callback), core::ptr::addr_of_mut!(ret).cast()); + libc::dl_iterate_phdr(Some(callback), core::ptr::addr_of_mut!(cb_data).cast()); } - return ret; + cb_data.ret } fn infer_current_exe(base_addr: usize) -> OsString { @@ -50,7 +59,11 @@ unsafe extern "C" fn callback( let dlpi_phdr = unsafe { (*info).dlpi_phdr }; let dlpi_phnum = unsafe { (*info).dlpi_phnum }; // SAFETY: We assured this. - let libs = unsafe { &mut *vec.cast::>() }; + let CallbackData { + ret: libs, + #[cfg(target_os = "android")] + maps, + } = unsafe { &mut *vec.cast::() }; // most implementations give us the main program first let is_main = libs.is_empty(); // we may be statically linked, which means we are main and mostly one big blob of code @@ -73,6 +86,22 @@ unsafe extern "C" fn callback( OsStr::from_bytes(unsafe { CStr::from_ptr(dlpi_name) }.to_bytes()).to_owned() } }; + #[cfg(target_os = "android")] + let zip_offset = { + // only check for ZIP-embedded file if we have data from /proc/self/maps + maps.as_ref().and_then(|maps| { + // check if file is embedded within a ZIP archive by searching for `!/` + name.as_bytes() + .windows(2) + .find(|&chunk| chunk == b"!/") + .and_then(|_| { + // find MapsEntry matching library's base address + maps.iter() + .find(|m| m.ip_matches(dlpi_addr as usize)) + .map(|m| m.offset()) + }) + }) + }; let headers = if dlpi_phdr.is_null() || dlpi_phnum == 0 { &[] } else { @@ -81,6 +110,8 @@ unsafe extern "C" fn callback( }; libs.push(Library { name, + #[cfg(target_os = "android")] + zip_offset: zip_offset.unwrap_or(0), segments: headers .iter() .map(|header| LibrarySegment { diff --git a/src/symbolize/gimli/mmap_unix.rs b/src/symbolize/gimli/mmap_unix.rs index 261ffc1d8..4617cf9eb 100644 --- a/src/symbolize/gimli/mmap_unix.rs +++ b/src/symbolize/gimli/mmap_unix.rs @@ -29,6 +29,22 @@ impl Mmap { } Some(Mmap { ptr, len }) } + + #[cfg(target_os = "android")] + pub unsafe fn map_with_offset(file: &File, len: usize, offset: usize) -> Option { + let ptr = mmap64( + ptr::null_mut(), + len, + libc::PROT_READ, + libc::MAP_PRIVATE, + file.as_raw_fd(), + offset.try_into().ok()?, + ); + if ptr == libc::MAP_FAILED { + return None; + } + Some(Mmap { ptr, len }) + } } impl Deref for Mmap { diff --git a/src/symbolize/gimli/parse_running_mmaps_unix.rs b/src/symbolize/gimli/parse_running_mmaps_unix.rs index 5d4b34675..ed3243c25 100644 --- a/src/symbolize/gimli/parse_running_mmaps_unix.rs +++ b/src/symbolize/gimli/parse_running_mmaps_unix.rs @@ -76,6 +76,11 @@ impl MapsEntry { pub(super) fn ip_matches(&self, ip: usize) -> bool { self.address.0 <= ip && ip < self.address.1 } + + #[cfg(target_os = "android")] + pub(super) fn offset(&self) -> usize { + self.offset + } } impl FromStr for MapsEntry {