Skip to content

Commit 80f5df4

Browse files
committed
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
1 parent 38d49aa commit 80f5df4

File tree

5 files changed

+100
-14
lines changed

5 files changed

+100
-14
lines changed

src/symbolize/gimli.rs

+11-10
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@ struct Cache {
269269

270270
struct Library {
271271
name: OsString,
272+
#[cfg(target_os = "android")]
273+
zip_offset: usize,
272274
#[cfg(target_os = "aix")]
273275
/// On AIX, the library mmapped can be a member of a big-archive file.
274276
/// For example, with a big-archive named libfoo.a containing libbar.so,
@@ -295,17 +297,16 @@ struct LibrarySegment {
295297
len: usize,
296298
}
297299

298-
#[cfg(target_os = "aix")]
299300
fn create_mapping(lib: &Library) -> Option<Mapping> {
300-
let name = &lib.name;
301-
let member_name = &lib.member_name;
302-
Mapping::new(name.as_ref(), member_name)
303-
}
304-
305-
#[cfg(not(target_os = "aix"))]
306-
fn create_mapping(lib: &Library) -> Option<Mapping> {
307-
let name = &lib.name;
308-
Mapping::new(name.as_ref())
301+
cfg_if::cfg_if! {
302+
if #[cfg(target_os = "aix")] {
303+
Mapping::new(lib.name.as_ref(), &lib.member_name)
304+
} else if #[cfg(target_os = "android")] {
305+
Mapping::new_android(lib.name.as_ref(), lib.zip_offset)
306+
} else {
307+
Mapping::new(lib.name.as_ref())
308+
}
309+
}
309310
}
310311

311312
// unsafe because this is required to be externally synchronized

src/symbolize/gimli/elf.rs

+36
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,42 @@ impl Mapping {
4343
})
4444
}
4545

46+
/// On Android, shared objects can be loaded directly from a
47+
/// ZIP archive. For example, an app may load a library from
48+
/// `/data/app/com.example/base.apk!/lib/x86_64/mylib.so`
49+
///
50+
/// For one of these "ZIP-embedded" libraries, `zip_offset` will be
51+
/// non-zero (see [super::libs_dl_iterate_phdr]).
52+
#[cfg(target_os = "android")]
53+
pub fn new_android(path: &Path, zip_offset: usize) -> Option<Mapping> {
54+
fn map_embedded_library(path: &Path, zip_offset: usize) -> Option<Mapping> {
55+
// get path of ZIP archive (delimited by `!/`)
56+
let raw_path = path.as_os_str().as_bytes();
57+
let zip_path = raw_path.windows(2).enumerate().find(|(_, chunk)| chunk == b"!/").map(|(index, _)| {
58+
Path::new(OsStr::from_bytes(raw_path.split_at(index).0))
59+
})?;
60+
61+
let file = fs::File::open(zip_path).ok()?;
62+
let len: usize = file.metadata().ok()?.len().try_into().ok()?;
63+
64+
// NOTE: we map the remainder of the entire archive instead of just the library so we don't have to determine its length
65+
// NOTE: mmap will fail if `zip_offset` is not page-aligned
66+
let map =
67+
unsafe { super::mmap::Mmap::map_with_offset(&file, len - zip_offset, zip_offset) }?;
68+
69+
Mapping::mk(map, |map, stash| {
70+
Context::new(stash, Object::parse(&map)?, None, None)
71+
})
72+
}
73+
74+
// if ZIP offset is non-zero, try mapping as a ZIP-embedded library
75+
if zip_offset > 0 {
76+
map_embedded_library(path, zip_offset).or_else(|| Self::new(path))
77+
} else {
78+
Self::new(path)
79+
}
80+
}
81+
4682
/// Load debuginfo from an external debug file.
4783
fn new_debug(original_path: &Path, path: PathBuf, crc: Option<u32>) -> Option<Mapping> {
4884
let map = super::mmap(&path)?;

src/symbolize/gimli/libs_dl_iterate_phdr.rs

+32-4
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,21 @@ use super::mystd::os::unix::prelude::*;
99
use super::{Library, LibrarySegment, OsString, Vec};
1010
use core::slice;
1111

12+
struct CallbackData {
13+
ret: Vec<Library>,
14+
#[cfg(target_os = "android")]
15+
maps: Option<Vec<super::parse_running_mmaps::MapsEntry>>,
16+
}
1217
pub(super) fn native_libraries() -> Vec<Library> {
13-
let mut ret = Vec::new();
18+
let mut cb_data = CallbackData {
19+
ret: Vec::new(),
20+
#[cfg(target_os = "android")]
21+
maps: super::parse_running_mmaps::parse_maps().ok(),
22+
};
1423
unsafe {
15-
libc::dl_iterate_phdr(Some(callback), core::ptr::addr_of_mut!(ret).cast());
24+
libc::dl_iterate_phdr(Some(callback), core::ptr::addr_of_mut!(cb_data).cast());
1625
}
17-
return ret;
26+
cb_data.ret
1827
}
1928

2029
fn infer_current_exe(base_addr: usize) -> OsString {
@@ -50,7 +59,11 @@ unsafe extern "C" fn callback(
5059
let dlpi_phdr = unsafe { (*info).dlpi_phdr };
5160
let dlpi_phnum = unsafe { (*info).dlpi_phnum };
5261
// SAFETY: We assured this.
53-
let libs = unsafe { &mut *vec.cast::<Vec<Library>>() };
62+
let CallbackData {
63+
ret: libs,
64+
#[cfg(target_os = "android")]
65+
maps,
66+
} = unsafe { &mut *vec.cast::<CallbackData>() };
5467
// most implementations give us the main program first
5568
let is_main = libs.is_empty();
5669
// we may be statically linked, which means we are main and mostly one big blob of code
@@ -73,6 +86,19 @@ unsafe extern "C" fn callback(
7386
OsStr::from_bytes(unsafe { CStr::from_ptr(dlpi_name) }.to_bytes()).to_owned()
7487
}
7588
};
89+
#[cfg(target_os = "android")]
90+
let zip_offset = {
91+
// only check for ZIP-embedded file if we have data from /proc/self/maps
92+
maps.as_ref().and_then(|maps| {
93+
// check if file is embedded within a ZIP archive by searching for `!/`
94+
name.as_bytes().windows(2).find(|&chunk| chunk == b"!/").and_then(|_| {
95+
// find MapsEntry matching library's base address
96+
maps.iter()
97+
.find(|m| m.ip_matches(dlpi_addr as usize))
98+
.map(|m| m.offset())
99+
})
100+
})
101+
};
76102
let headers = if dlpi_phdr.is_null() || dlpi_phnum == 0 {
77103
&[]
78104
} else {
@@ -81,6 +107,8 @@ unsafe extern "C" fn callback(
81107
};
82108
libs.push(Library {
83109
name,
110+
#[cfg(target_os = "android")]
111+
zip_offset: zip_offset.unwrap_or(0),
84112
segments: headers
85113
.iter()
86114
.map(|header| LibrarySegment {

src/symbolize/gimli/mmap_unix.rs

+16
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@ impl Mmap {
2929
}
3030
Some(Mmap { ptr, len })
3131
}
32+
33+
#[cfg(target_os = "android")]
34+
pub unsafe fn map_with_offset(file: &File, len: usize, offset: usize) -> Option<Mmap> {
35+
let ptr = mmap64(
36+
ptr::null_mut(),
37+
len,
38+
libc::PROT_READ,
39+
libc::MAP_PRIVATE,
40+
file.as_raw_fd(),
41+
offset.try_into().ok()?,
42+
);
43+
if ptr == libc::MAP_FAILED {
44+
return None;
45+
}
46+
Some(Mmap { ptr, len })
47+
}
3248
}
3349

3450
impl Deref for Mmap {

src/symbolize/gimli/parse_running_mmaps_unix.rs

+5
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ impl MapsEntry {
7676
pub(super) fn ip_matches(&self, ip: usize) -> bool {
7777
self.address.0 <= ip && ip < self.address.1
7878
}
79+
80+
#[cfg(target_os = "android")]
81+
pub(super) fn offset(&self) -> usize {
82+
self.offset
83+
}
7984
}
8085

8186
impl FromStr for MapsEntry {

0 commit comments

Comments
 (0)