diff --git a/src/symbolize/gimli.rs b/src/symbolize/gimli.rs index 8c7051d4..bd7a9190 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 906a3005..39a862c0 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 e15750ec..f3fa6722 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 261ffc1d..4617cf9e 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 5d4b3467..ed3243c2 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 {