Skip to content

Commit

Permalink
Add support for symbolicating APK/ZIP-embedded libraries on Android
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sudoBash418 committed Sep 3, 2024
1 parent 38d49aa commit 80f5df4
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 14 deletions.
21 changes: 11 additions & 10 deletions src/symbolize/gimli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -295,17 +297,16 @@ struct LibrarySegment {
len: usize,
}

#[cfg(target_os = "aix")]
fn create_mapping(lib: &Library) -> Option<Mapping> {
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<Mapping> {
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
Expand Down
36 changes: 36 additions & 0 deletions src/symbolize/gimli/elf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,42 @@ 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<Mapping> {
fn map_embedded_library(path: &Path, zip_offset: usize) -> Option<Mapping> {
// 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<u32>) -> Option<Mapping> {
let map = super::mmap(&path)?;
Expand Down
36 changes: 32 additions & 4 deletions src/symbolize/gimli/libs_dl_iterate_phdr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,21 @@ use super::mystd::os::unix::prelude::*;
use super::{Library, LibrarySegment, OsString, Vec};
use core::slice;

struct CallbackData {
ret: Vec<Library>,
#[cfg(target_os = "android")]
maps: Option<Vec<super::parse_running_mmaps::MapsEntry>>,
}
pub(super) fn native_libraries() -> Vec<Library> {
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 {
Expand Down Expand Up @@ -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::<Vec<Library>>() };
let CallbackData {
ret: libs,
#[cfg(target_os = "android")]
maps,
} = unsafe { &mut *vec.cast::<CallbackData>() };
// 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
Expand All @@ -73,6 +86,19 @@ 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 {
Expand All @@ -81,6 +107,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 {
Expand Down
16 changes: 16 additions & 0 deletions src/symbolize/gimli/mmap_unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Mmap> {
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 {
Expand Down
5 changes: 5 additions & 0 deletions src/symbolize/gimli/parse_running_mmaps_unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 80f5df4

Please sign in to comment.