Skip to content

Commit d881495

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 5107f26 commit d881495

File tree

7 files changed

+109
-23
lines changed

7 files changed

+109
-23
lines changed

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: Option<i64>,
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

+39
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,45 @@ 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+
/// `zip_offset` should be page-aligned; the dynamic linker
51+
/// requires this when it loads libraries.
52+
#[cfg(target_os = "android")]
53+
pub fn new_android(path: &Path, zip_offset: Option<i64>) -> Option<Mapping> {
54+
fn map_embedded_library(path: &Path, zip_offset: i64) -> 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
58+
.windows(2)
59+
.enumerate()
60+
.find(|(_, chunk)| chunk == b"!/")
61+
.map(|(index, _)| Path::new(OsStr::from_bytes(raw_path.split_at(index).0)))?;
62+
63+
let file = fs::File::open(zip_path).ok()?;
64+
let len: usize = file.metadata().ok()?.len().try_into().ok()?;
65+
66+
// NOTE: we map the remainder of the entire archive instead of just the library so we don't have to determine its length
67+
// NOTE: mmap will fail if `zip_offset` is not page-aligned
68+
let map = unsafe {
69+
super::mmap::Mmap::map(&file, len - usize::try_from(zip_offset).ok()?, zip_offset)
70+
}?;
71+
72+
Mapping::mk(map, |map, stash| {
73+
Context::new(stash, Object::parse(&map)?, None, None)
74+
})
75+
}
76+
77+
// if ZIP offset is given, try mapping as a ZIP-embedded library
78+
if let Some(zip_offset) = zip_offset {
79+
map_embedded_library(path, zip_offset).or_else(|| Self::new(path))
80+
} else {
81+
Self::new(path)
82+
}
83+
}
84+
4685
/// Load debuginfo from an external debug file.
4786
fn new_debug(original_path: &Path, path: PathBuf, crc: Option<u32>) -> Option<Mapping> {
4887
let map = super::mmap(&path)?;

src/symbolize/gimli/libs_dl_iterate_phdr.rs

+37-6
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 {
@@ -37,20 +46,24 @@ fn infer_current_exe(base_addr: usize) -> OsString {
3746

3847
/// # Safety
3948
/// `info` must be a valid pointer.
40-
/// `vec` must be a valid pointer to `Vec<Library>`
49+
/// `data` must be a valid pointer to `CallbackData`.
4150
#[forbid(unsafe_op_in_unsafe_fn)]
4251
unsafe extern "C" fn callback(
4352
info: *mut libc::dl_phdr_info,
4453
_size: libc::size_t,
45-
vec: *mut libc::c_void,
54+
data: *mut libc::c_void,
4655
) -> libc::c_int {
4756
// SAFETY: We are guaranteed these fields:
4857
let dlpi_addr = unsafe { (*info).dlpi_addr };
4958
let dlpi_name = unsafe { (*info).dlpi_name };
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 *data.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,22 @@ 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: Option<i64> = {
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()
95+
.windows(2)
96+
.find(|&chunk| chunk == b"!/")
97+
.and_then(|_| {
98+
// find MapsEntry matching library's base address
99+
maps.iter()
100+
.find(|m| m.ip_matches(dlpi_addr as usize))
101+
.and_then(|m| m.offset().try_into().ok())
102+
})
103+
})
104+
};
76105
let headers = if dlpi_phdr.is_null() || dlpi_phnum == 0 {
77106
&[]
78107
} else {
@@ -81,6 +110,8 @@ unsafe extern "C" fn callback(
81110
};
82111
libs.push(Library {
83112
name,
113+
#[cfg(target_os = "android")]
114+
zip_offset,
84115
segments: headers
85116
.iter()
86117
.map(|header| LibrarySegment {

src/symbolize/gimli/mmap_fake.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::{mystd::io::Read, File};
1+
use super::{mystd::io::Read, mystd::io::Seek, mystd::io::SeekFrom, File};
22
use alloc::vec::Vec;
33
use core::ops::Deref;
44

@@ -7,10 +7,11 @@ pub struct Mmap {
77
}
88

99
impl Mmap {
10-
pub unsafe fn map(mut file: &File, len: usize) -> Option<Mmap> {
10+
pub unsafe fn map(mut file: &File, len: usize, offset: i64) -> Option<Mmap> {
1111
let mut mmap = Mmap {
1212
vec: Vec::with_capacity(len),
1313
};
14+
file.seek(SeekFrom::Start(offset.try_into().ok()?));
1415
file.read_to_end(&mut mmap.vec).ok()?;
1516
Some(mmap)
1617
}

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: i64) -> 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.try_into().ok()?,
2626
);
2727
if ptr == libc::MAP_FAILED {
2828
return None;

src/symbolize/gimli/mmap_windows.rs

+11-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ pub struct Mmap {
1616
}
1717

1818
impl Mmap {
19-
pub unsafe fn map(file: &File, len: usize) -> Option<Mmap> {
19+
pub unsafe fn map(file: &File, len: usize, offset: i64) -> Option<Mmap> {
20+
if offset.is_negative() {
21+
return None;
22+
}
2023
let file = file.try_clone().ok()?;
2124
let mapping = CreateFileMappingA(
2225
file.as_raw_handle(),
@@ -29,7 +32,13 @@ impl Mmap {
2932
if mapping.is_null() {
3033
return None;
3134
}
32-
let ptr = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, len);
35+
let ptr = MapViewOfFile(
36+
mapping,
37+
FILE_MAP_READ,
38+
(offset >> 32) as u32,
39+
offset as u32,
40+
len,
41+
);
3342
CloseHandle(mapping);
3443
if ptr.Value.is_null() {
3544
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)