Skip to main content

apk/
native.rs

1//! Native libraries: ELF inspection and Android's loading requirements.
2//!
3//! [`ElfInfo::parse`] reads the facts that matter for packaging: the ABI,
4//! `PT_LOAD` alignment (16 KiB page support), exported entry points, and
5//! `DT_NEEDED` dependencies. [`NativePolicy`] decides how libraries are stored
6//! and which requirements are enforced.
7
8use std::collections::BTreeSet;
9
10use goblin::elf::{Elf, header, program_header, section_header, sym};
11
12use crate::types::{Abi, ApiLevel, LibraryName};
13
14/// The entry point `android.app.NativeActivity` calls.
15pub const NATIVE_ACTIVITY_ENTRY: &str = "ANativeActivity_onCreate";
16/// The entry point `GameActivity` calls.
17pub const GAME_ACTIVITY_ENTRY: &str = "GameActivity_onCreate";
18
19/// Why a native library cannot be packaged.
20#[derive(Debug, thiserror::Error)]
21#[non_exhaustive]
22pub enum NativeError {
23    #[error("not a valid ELF file: {0}")]
24    Elf(#[from] goblin::error::Error),
25    #[error("ELF type {e_type} is not a shared library (ET_DYN)")]
26    NotSharedLibrary { e_type: u16 },
27    #[error("big-endian ELF files are not supported by Android")]
28    BigEndian,
29    #[error("ELF machine {machine} ({bits}-bit) is not an Android ABI")]
30    UnsupportedMachine { machine: u16, bits: u8 },
31    #[error("PT_LOAD segment {segment} has file range {offset:#x}+{size:#x} outside its ELF file")]
32    SegmentFileRange { segment: usize, offset: u64, size: u64 },
33    #[error("PT_LOAD segment {segment} has {file_size} file bytes but only {memory_size} memory bytes")]
34    SegmentSize { segment: usize, file_size: u64, memory_size: u64 },
35    #[error("{library} is built for {found}, but was added for {expected}")]
36    AbiMismatch { library: LibraryName, expected: Abi, found: Abi },
37    #[error("{library} is not an ARM EABI5 library")]
38    NotArmEabi5 { library: LibraryName },
39    #[error("{library} uses the hard-float ABI; armeabi-v7a requires softfp")]
40    HardFloat { library: LibraryName },
41    #[error("{library} has no PT_LOAD segments")]
42    NoLoadSegments { library: LibraryName },
43    #[error("{library} segment {segment} is aligned to {alignment} bytes; {abi} requires {required}")]
44    SegmentAlignment { library: LibraryName, abi: Abi, segment: usize, alignment: u64, required: u64 },
45    #[error(
46        "{library} segment {segment} has offset {offset:#x} and address {address:#x}, which disagree modulo {alignment}"
47    )]
48    SegmentIncongruent { library: LibraryName, segment: usize, offset: u64, address: u64, alignment: u64 },
49    #[error("{library} does not export {symbol}")]
50    MissingEntryPoint { library: LibraryName, symbol: &'static str },
51    #[error("{library} ({abi}) needs {needed}, which is neither packaged nor an Android system library")]
52    MissingDependency { library: LibraryName, abi: Abi, needed: String },
53    #[error("{library} needs {needed}, which Android only provides from {since}, but minSdkVersion is {min_sdk}")]
54    SystemLibraryTooNew { library: LibraryName, needed: String, since: ApiLevel, min_sdk: ApiLevel },
55    #[error("{library} was added twice for {abi}")]
56    DuplicateLibrary { library: LibraryName, abi: Abi },
57    #[error("{abi} has native libraries but not the entry library {library}")]
58    MissingEntryLibrary { library: LibraryName, abi: Abi },
59    #[error(
60        "4 KiB pages cannot be used for 64-bit libraries when targeting {target_sdk}; Android 15 devices use 16 KiB pages"
61    )]
62    FourKibPages { target_sdk: ApiLevel },
63}
64
65/// A memory page size Android devices use.
66#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
67pub enum PageSize {
68    FourKiB,
69    #[default]
70    SixteenKiB,
71}
72
73impl PageSize {
74    pub const fn bytes(self) -> u64 {
75        match self {
76            Self::FourKiB => 4 * 1024,
77            Self::SixteenKiB => 16 * 1024,
78        }
79    }
80}
81
82/// How native libraries are stored in the APK.
83#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
84pub enum LibraryStorage {
85    /// Stored uncompressed and page-aligned, so Android maps them straight
86    /// from the APK (`android:extractNativeLibs="false"`). Smaller installs.
87    #[default]
88    Mapped,
89    /// Compressed, and extracted to disk at install time
90    /// (`android:extractNativeLibs="true"`). Smaller downloads.
91    Extracted,
92}
93
94/// Requirements applied to packaged native libraries.
95#[derive(Clone, Debug, Eq, PartialEq)]
96pub struct NativePolicy {
97    /// The page size 64-bit libraries must support. 32-bit ABIs always use 4 KiB pages.
98    pub page_size: PageSize,
99    pub storage: LibraryStorage,
100    /// Whether every `DT_NEEDED` library must be packaged or provided by Android.
101    pub check_dependencies: bool,
102    /// Extra libraries (beyond the NDK's stable libraries) assumed to exist on devices.
103    pub system_libraries: BTreeSet<String>,
104}
105
106impl Default for NativePolicy {
107    fn default() -> Self {
108        Self {
109            page_size: PageSize::SixteenKiB,
110            storage: LibraryStorage::Mapped,
111            check_dependencies: true,
112            system_libraries: BTreeSet::new(),
113        }
114    }
115}
116
117impl NativePolicy {
118    /// The page size required of libraries for `abi`.
119    pub fn page_size_for(&self, abi: Abi) -> PageSize {
120        if abi.is_64_bit() { self.page_size } else { PageSize::FourKiB }
121    }
122
123    /// Checks that `needed`, a dependency of `library` that is not packaged,
124    /// is provided by every Android release from `min_sdk` on.
125    pub fn check_system_library(
126        &self,
127        library: &LibraryName,
128        abi: Abi,
129        needed: &str,
130        min_sdk: ApiLevel,
131    ) -> Result<(), NativeError> {
132        if self.system_libraries.contains(needed) {
133            return Ok(());
134        }
135        match NDK_SYSTEM_LIBRARIES.iter().find(|(name, _)| *name == needed) {
136            Some(&(_, since)) if since > min_sdk => Err(NativeError::SystemLibraryTooNew {
137                library: library.clone(),
138                needed: needed.to_owned(),
139                since,
140                min_sdk,
141            }),
142            Some(_) => Ok(()),
143            None => Err(NativeError::MissingDependency { library: library.clone(), abi, needed: needed.to_owned() }),
144        }
145    }
146}
147
148const fn api(level: u32) -> ApiLevel {
149    match ApiLevel::try_new(level) {
150        Ok(level) => level,
151        Err(_) => panic!("invalid API level"),
152    }
153}
154
155/// The NDK's stable system libraries, which apps may link against without
156/// packaging them, and the API level each first shipped in.
157pub const NDK_SYSTEM_LIBRARIES: &[(&str, ApiLevel)] = &[
158    ("libaaudio.so", api(26)),
159    ("libamidi.so", api(29)),
160    ("libandroid.so", api(9)),
161    ("libbinder_ndk.so", api(29)),
162    ("libc.so", api(3)),
163    ("libcamera2ndk.so", api(24)),
164    ("libdl.so", api(3)),
165    ("libEGL.so", api(9)),
166    ("libGLESv1_CM.so", api(4)),
167    ("libGLESv2.so", api(5)),
168    ("libGLESv3.so", api(18)),
169    ("libicu.so", api(31)),
170    ("libjnigraphics.so", api(8)),
171    ("liblog.so", api(3)),
172    ("libm.so", api(3)),
173    ("libmediandk.so", api(21)),
174    ("libnativehelper.so", api(31)),
175    ("libnativewindow.so", api(26)),
176    ("libneuralnetworks.so", api(27)),
177    ("libOpenMAXAL.so", api(14)),
178    ("libOpenSLES.so", api(9)),
179    ("libstdc++.so", api(3)),
180    ("libsync.so", api(26)),
181    ("libvulkan.so", api(24)),
182    ("libz.so", api(3)),
183];
184
185/// One `PT_LOAD` segment.
186#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
187pub struct LoadSegment {
188    pub offset: u64,
189    pub virtual_address: u64,
190    pub alignment: u64,
191}
192
193/// Packaging-relevant facts read from a shared library.
194#[derive(Clone, Debug, Eq, PartialEq)]
195pub struct ElfInfo {
196    pub abi: Abi,
197    pub soname: Option<String>,
198    /// `DT_NEEDED` entries: the libraries this one links against.
199    pub needed: Vec<String>,
200    pub load_segments: Vec<LoadSegment>,
201    /// Exported (defined, global or weak, default or protected visibility) function names
202    /// that matter to Android: activity entry points and `JNI_OnLoad`.
203    pub entry_points: BTreeSet<String>,
204    /// ARM `e_flags`; `0` for other machines.
205    arm_flags: u32,
206}
207
208const INTERESTING_EXPORTS: [&str; 3] = [NATIVE_ACTIVITY_ENTRY, GAME_ACTIVITY_ENTRY, "JNI_OnLoad"];
209const EF_ARM_EABIMASK: u32 = 0xff00_0000;
210const EF_ARM_EABI_VER5: u32 = 0x0500_0000;
211const EF_ARM_ABI_FLOAT_HARD: u32 = 0x400;
212
213impl ElfInfo {
214    /// Reads `bytes` as an Android shared library.
215    pub fn parse(bytes: &[u8]) -> Result<Self, NativeError> {
216        let elf = Elf::parse(bytes)?;
217        if !elf.little_endian {
218            return Err(NativeError::BigEndian);
219        }
220        if elf.header.e_type != header::ET_DYN {
221            return Err(NativeError::NotSharedLibrary { e_type: elf.header.e_type });
222        }
223        let abi = match (elf.header.e_machine, elf.is_64) {
224            (header::EM_ARM, false) => Abi::ArmeabiV7a,
225            (header::EM_AARCH64, true) => Abi::Arm64V8a,
226            (header::EM_386, false) => Abi::X86,
227            (header::EM_X86_64, true) => Abi::X86_64,
228            (header::EM_RISCV, true) => Abi::Riscv64,
229            (machine, is_64) => {
230                return Err(NativeError::UnsupportedMachine { machine, bits: if is_64 { 64 } else { 32 } });
231            }
232        };
233        let load_segments = elf
234            .program_headers
235            .iter()
236            .enumerate()
237            .filter(|(_, ph)| ph.p_type == program_header::PT_LOAD)
238            .map(|(segment, ph)| {
239                if ph.p_filesz > ph.p_memsz {
240                    return Err(NativeError::SegmentSize { segment, file_size: ph.p_filesz, memory_size: ph.p_memsz });
241                }
242                if ph.p_filesz != 0 && ph.p_offset.checked_add(ph.p_filesz).is_none_or(|end| end > bytes.len() as u64) {
243                    return Err(NativeError::SegmentFileRange { segment, offset: ph.p_offset, size: ph.p_filesz });
244                }
245                Ok(LoadSegment { offset: ph.p_offset, virtual_address: ph.p_vaddr, alignment: ph.p_align })
246            })
247            .collect::<Result<_, _>>()?;
248        let entry_points = elf
249            .dynsyms
250            .iter()
251            .filter(|s| {
252                s.st_type() == sym::STT_FUNC
253                    && s.st_shndx != section_header::SHN_UNDEF as usize
254                    && matches!(s.st_bind(), sym::STB_GLOBAL | sym::STB_WEAK)
255                    && matches!(s.st_visibility(), sym::STV_DEFAULT | sym::STV_PROTECTED)
256            })
257            .filter_map(|s| elf.dynstrtab.get_at(s.st_name))
258            .filter(|name| INTERESTING_EXPORTS.contains(name))
259            .map(str::to_owned)
260            .collect();
261        Ok(Self {
262            abi,
263            soname: elf.soname.map(str::to_owned),
264            needed: elf.libraries.iter().map(|l| (*l).to_owned()).collect(),
265            load_segments,
266            entry_points,
267            arm_flags: if abi == Abi::ArmeabiV7a { elf.header.e_flags } else { 0 },
268        })
269    }
270
271    /// Whether this library exports the function `symbol`.
272    pub fn exports(&self, symbol: &str) -> bool {
273        self.entry_points.contains(symbol)
274    }
275
276    /// The largest page size every load segment supports.
277    pub fn max_page_size(&self) -> u64 {
278        self.load_segments.iter().map(|s| s.alignment).min().unwrap_or(0)
279    }
280
281    /// Checks this library against `expected` ABI and the page size `policy`
282    /// requires of it.
283    pub fn check(&self, library: &LibraryName, expected: Abi, policy: &NativePolicy) -> Result<(), NativeError> {
284        let library = || library.clone();
285        if self.abi != expected {
286            return Err(NativeError::AbiMismatch { library: library(), expected, found: self.abi });
287        }
288        if self.abi == Abi::ArmeabiV7a {
289            if self.arm_flags & EF_ARM_EABIMASK != EF_ARM_EABI_VER5 {
290                return Err(NativeError::NotArmEabi5 { library: library() });
291            }
292            if self.arm_flags & EF_ARM_ABI_FLOAT_HARD != 0 {
293                return Err(NativeError::HardFloat { library: library() });
294            }
295        }
296        if self.load_segments.is_empty() {
297            return Err(NativeError::NoLoadSegments { library: library() });
298        }
299        let required = policy.page_size_for(self.abi).bytes();
300        for (segment, load) in self.load_segments.iter().enumerate() {
301            if load.alignment < required || !load.alignment.is_power_of_two() {
302                return Err(NativeError::SegmentAlignment {
303                    library: library(),
304                    abi: self.abi,
305                    segment,
306                    alignment: load.alignment,
307                    required,
308                });
309            }
310            if load.offset % load.alignment != load.virtual_address % load.alignment {
311                return Err(NativeError::SegmentIncongruent {
312                    library: library(),
313                    segment,
314                    offset: load.offset,
315                    address: load.virtual_address,
316                    alignment: load.alignment,
317                });
318            }
319        }
320        Ok(())
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/native/libpostcard.so");
329
330    fn library() -> LibraryName {
331        LibraryName::try_new("postcard").unwrap()
332    }
333
334    #[test]
335    fn reads_the_fixture() {
336        let info = ElfInfo::parse(FIXTURE).unwrap();
337        assert_eq!(info.abi, Abi::Arm64V8a);
338        assert!(info.exports(NATIVE_ACTIVITY_ENTRY));
339        assert!(info.max_page_size() >= PageSize::SixteenKiB.bytes());
340        info.check(&library(), Abi::Arm64V8a, &NativePolicy::default()).unwrap();
341    }
342
343    #[test]
344    fn rejects_wrong_abi_and_non_elf_input() {
345        let info = ElfInfo::parse(FIXTURE).unwrap();
346        assert!(matches!(
347            info.check(&library(), Abi::X86_64, &NativePolicy::default()),
348            Err(NativeError::AbiMismatch { expected: Abi::X86_64, found: Abi::Arm64V8a, .. })
349        ));
350        assert!(matches!(ElfInfo::parse(b"not an elf"), Err(NativeError::Elf(_))));
351    }
352
353    #[test]
354    fn page_size_requirement_depends_on_the_abi() {
355        let policy = NativePolicy::default();
356        assert_eq!(policy.page_size_for(Abi::Arm64V8a), PageSize::SixteenKiB);
357        assert_eq!(policy.page_size_for(Abi::ArmeabiV7a), PageSize::FourKiB);
358    }
359
360    #[test]
361    fn system_libraries_must_exist_on_the_oldest_supported_release() {
362        let policy = NativePolicy::default();
363        let check = |needed, min_sdk| policy.check_system_library(&library(), Abi::Arm64V8a, needed, min_sdk);
364        check("liblog.so", ApiLevel::LOLLIPOP).unwrap();
365        check("libaaudio.so", ApiLevel::O).unwrap();
366        assert!(matches!(
367            check("libaaudio.so", ApiLevel::N),
368            Err(NativeError::SystemLibraryTooNew { since, .. }) if since == ApiLevel::O
369        ));
370        assert!(matches!(check("libc++_shared.so", ApiLevel::N), Err(NativeError::MissingDependency { .. })));
371        let vendor = NativePolicy { system_libraries: ["libOpenCL.so".to_owned()].into(), ..NativePolicy::default() };
372        vendor.check_system_library(&library(), Abi::Arm64V8a, "libOpenCL.so", ApiLevel::N).unwrap();
373    }
374
375    #[test]
376    fn underaligned_segments_are_rejected() {
377        let mut info = ElfInfo::parse(FIXTURE).unwrap();
378        info.load_segments[0].alignment = 4096;
379        assert!(matches!(
380            info.check(&library(), Abi::Arm64V8a, &NativePolicy::default()),
381            Err(NativeError::SegmentAlignment { segment: 0, alignment: 4096, required: 16384, .. })
382        ));
383        let four = NativePolicy { page_size: PageSize::FourKiB, ..NativePolicy::default() };
384        info.check(&library(), Abi::Arm64V8a, &four).unwrap();
385    }
386
387    #[test]
388    fn load_segments_must_fit_the_file_and_their_memory_mapping() {
389        let elf = Elf::parse(FIXTURE).unwrap();
390        let index = elf
391            .program_headers
392            .iter()
393            .position(|ph| ph.p_type == program_header::PT_LOAD && ph.is_executable())
394            .unwrap();
395        let at = elf.header.e_phoff as usize + index * elf.header.e_phentsize as usize;
396        let mut bytes = FIXTURE.to_vec();
397        bytes[at + 8..at + 16].copy_from_slice(&u64::MAX.to_le_bytes());
398        assert!(matches!(ElfInfo::parse(&bytes), Err(NativeError::SegmentFileRange { .. })));
399
400        let mut bytes = FIXTURE.to_vec();
401        bytes[at + 32..at + 40].copy_from_slice(&(elf.program_headers[index].p_memsz + 1).to_le_bytes());
402        assert!(matches!(ElfInfo::parse(&bytes), Err(NativeError::SegmentSize { .. })));
403    }
404
405    #[test]
406    fn congruence_uses_the_declared_alignment() {
407        let mut info = ElfInfo::parse(FIXTURE).unwrap();
408        info.load_segments[0].alignment = 65536;
409        info.load_segments[0].virtual_address = info.load_segments[0].offset + 16384;
410        assert!(matches!(
411            info.check(&library(), Abi::Arm64V8a, &NativePolicy::default()),
412            Err(NativeError::SegmentIncongruent { alignment: 65536, .. })
413        ));
414    }
415
416    #[test]
417    fn activity_entry_must_be_a_visible_defined_function() {
418        let elf = Elf::parse(FIXTURE).unwrap();
419        let index =
420            elf.dynsyms.iter().position(|s| elf.dynstrtab.get_at(s.st_name) == Some(NATIVE_ACTIVITY_ENTRY)).unwrap();
421        let table = elf.section_headers.iter().find(|s| s.sh_type == section_header::SHT_DYNSYM).unwrap();
422        let at = table.sh_offset as usize + index * table.sh_entsize as usize;
423        for (field, replacement) in [(4, 0x02), (4, 0x11), (5, sym::STV_HIDDEN), (6, 0)] {
424            let mut bytes = FIXTURE.to_vec();
425            bytes[at + field] = replacement;
426            assert!(!ElfInfo::parse(&bytes).unwrap().exports(NATIVE_ACTIVITY_ENTRY), "field {field}: {replacement}");
427        }
428    }
429}