Skip to content

Commit 3534f66

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 3534f66

File tree

7 files changed

+90
-17
lines changed

7 files changed

+90
-17
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ miniz_oxide = { version = "0.8", default-features = false }
4646
addr2line = { version = "0.24.0", default-features = false }
4747
libc = { version = "0.2.156", default-features = false }
4848

49+
[target.'cfg(target_os = "android")'.dependencies]
50+
memchr = { version = "2.7", default-features = false }
51+
4952
[target.'cfg(not(all(windows, target_env = "msvc", not(target_vendor = "uwp"))))'.dependencies.object]
5053
version = "0.36.0"
5154
default-features = false

src/symbolize/gimli.rs

+12-11
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ impl<'data> Context<'data> {
187187
fn mmap(path: &Path) -> Option<Mmap> {
188188
let file = File::open(path).ok()?;
189189
let len = file.metadata().ok()?.len().try_into().ok()?;
190-
unsafe { Mmap::map(&file, len) }
190+
unsafe { Mmap::map(&file, len, 0) }
191191
}
192192

193193
cfg_if::cfg_if! {
@@ -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

+35
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,41 @@ 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 = memchr::memmem::find(raw_path, b"!/").map(|match_index| {
58+
Path::new(OsStr::from_bytes(raw_path.split_at(match_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 = unsafe { super::mmap::Mmap::map(&file, len - zip_offset, zip_offset) }?;
67+
68+
Mapping::mk(map, |map, stash| {
69+
Context::new(stash, Object::parse(&map)?, None, None)
70+
})
71+
}
72+
73+
// if ZIP offset is non-zero, try mapping as a ZIP-embedded library
74+
if zip_offset > 0 {
75+
map_embedded_library(path, zip_offset).or_else(|| Self::new(path))
76+
} else {
77+
Self::new(path)
78+
}
79+
}
80+
4681
/// Load debuginfo from an external debug file.
4782
fn new_debug(original_path: &Path, path: PathBuf, crc: Option<u32>) -> Option<Mapping> {
4883
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+
memchr::memmem::find(name.as_bytes(), 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

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ pub struct Mmap {
1515
}
1616

1717
impl Mmap {
18-
pub unsafe fn map(file: &File, len: usize) -> Option<Mmap> {
18+
pub unsafe fn map(file: &File, len: usize, offset: usize) -> Option<Mmap> {
1919
let ptr = mmap64(
2020
ptr::null_mut(),
2121
len,
2222
libc::PROT_READ,
2323
libc::MAP_PRIVATE,
2424
file.as_raw_fd(),
25-
0,
25+
offset as i64,
2626
);
2727
if ptr == libc::MAP_FAILED {
2828
return None;

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)