rust-apk 1.0.0

Build, align, sign, verify, and inspect Android APKs in pure Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
//! Native libraries: ELF inspection and Android's loading requirements.
//!
//! [`ElfInfo::parse`] reads the facts that matter for packaging: the ABI,
//! `PT_LOAD` alignment (16 KiB page support), exported entry points, and
//! `DT_NEEDED` dependencies. [`NativePolicy`] decides how libraries are stored
//! and which requirements are enforced.

use std::collections::BTreeSet;

use goblin::elf::{Elf, header, program_header, section_header, sym};

use crate::types::{Abi, ApiLevel, LibraryName};

/// The entry point `android.app.NativeActivity` calls.
pub const NATIVE_ACTIVITY_ENTRY: &str = "ANativeActivity_onCreate";
/// The entry point `GameActivity` calls.
pub const GAME_ACTIVITY_ENTRY: &str = "GameActivity_onCreate";

/// Why a native library cannot be packaged.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum NativeError {
    #[error("not a valid ELF file: {0}")]
    Elf(#[from] goblin::error::Error),
    #[error("ELF type {e_type} is not a shared library (ET_DYN)")]
    NotSharedLibrary { e_type: u16 },
    #[error("big-endian ELF files are not supported by Android")]
    BigEndian,
    #[error("ELF machine {machine} ({bits}-bit) is not an Android ABI")]
    UnsupportedMachine { machine: u16, bits: u8 },
    #[error("PT_LOAD segment {segment} has file range {offset:#x}+{size:#x} outside its ELF file")]
    SegmentFileRange { segment: usize, offset: u64, size: u64 },
    #[error("PT_LOAD segment {segment} has {file_size} file bytes but only {memory_size} memory bytes")]
    SegmentSize { segment: usize, file_size: u64, memory_size: u64 },
    #[error("{library} is built for {found}, but was added for {expected}")]
    AbiMismatch { library: LibraryName, expected: Abi, found: Abi },
    #[error("{library} is not an ARM EABI5 library")]
    NotArmEabi5 { library: LibraryName },
    #[error("{library} uses the hard-float ABI; armeabi-v7a requires softfp")]
    HardFloat { library: LibraryName },
    #[error("{library} has no PT_LOAD segments")]
    NoLoadSegments { library: LibraryName },
    #[error("{library} segment {segment} is aligned to {alignment} bytes; {abi} requires {required}")]
    SegmentAlignment { library: LibraryName, abi: Abi, segment: usize, alignment: u64, required: u64 },
    #[error(
        "{library} segment {segment} has offset {offset:#x} and address {address:#x}, which disagree modulo {alignment}"
    )]
    SegmentIncongruent { library: LibraryName, segment: usize, offset: u64, address: u64, alignment: u64 },
    #[error("{library} does not export {symbol}")]
    MissingEntryPoint { library: LibraryName, symbol: &'static str },
    #[error("{library} ({abi}) needs {needed}, which is neither packaged nor an Android system library")]
    MissingDependency { library: LibraryName, abi: Abi, needed: String },
    #[error("{library} needs {needed}, which Android only provides from {since}, but minSdkVersion is {min_sdk}")]
    SystemLibraryTooNew { library: LibraryName, needed: String, since: ApiLevel, min_sdk: ApiLevel },
    #[error("{library} was added twice for {abi}")]
    DuplicateLibrary { library: LibraryName, abi: Abi },
    #[error("{abi} has native libraries but not the entry library {library}")]
    MissingEntryLibrary { library: LibraryName, abi: Abi },
    #[error(
        "4 KiB pages cannot be used for 64-bit libraries when targeting {target_sdk}; Android 15 devices use 16 KiB pages"
    )]
    FourKibPages { target_sdk: ApiLevel },
}

/// A memory page size Android devices use.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PageSize {
    FourKiB,
    #[default]
    SixteenKiB,
}

impl PageSize {
    pub const fn bytes(self) -> u64 {
        match self {
            Self::FourKiB => 4 * 1024,
            Self::SixteenKiB => 16 * 1024,
        }
    }
}

/// How native libraries are stored in the APK.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum LibraryStorage {
    /// Stored uncompressed and page-aligned, so Android maps them straight
    /// from the APK (`android:extractNativeLibs="false"`). Smaller installs.
    #[default]
    Mapped,
    /// Compressed, and extracted to disk at install time
    /// (`android:extractNativeLibs="true"`). Smaller downloads.
    Extracted,
}

/// Requirements applied to packaged native libraries.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NativePolicy {
    /// The page size 64-bit libraries must support. 32-bit ABIs always use 4 KiB pages.
    pub page_size: PageSize,
    pub storage: LibraryStorage,
    /// Whether every `DT_NEEDED` library must be packaged or provided by Android.
    pub check_dependencies: bool,
    /// Extra libraries (beyond the NDK's stable libraries) assumed to exist on devices.
    pub system_libraries: BTreeSet<String>,
}

impl Default for NativePolicy {
    fn default() -> Self {
        Self {
            page_size: PageSize::SixteenKiB,
            storage: LibraryStorage::Mapped,
            check_dependencies: true,
            system_libraries: BTreeSet::new(),
        }
    }
}

impl NativePolicy {
    /// The page size required of libraries for `abi`.
    pub fn page_size_for(&self, abi: Abi) -> PageSize {
        if abi.is_64_bit() { self.page_size } else { PageSize::FourKiB }
    }

    /// Checks that `needed`, a dependency of `library` that is not packaged,
    /// is provided by every Android release from `min_sdk` on.
    pub fn check_system_library(
        &self,
        library: &LibraryName,
        abi: Abi,
        needed: &str,
        min_sdk: ApiLevel,
    ) -> Result<(), NativeError> {
        if self.system_libraries.contains(needed) {
            return Ok(());
        }
        match NDK_SYSTEM_LIBRARIES.iter().find(|(name, _)| *name == needed) {
            Some(&(_, since)) if since > min_sdk => Err(NativeError::SystemLibraryTooNew {
                library: library.clone(),
                needed: needed.to_owned(),
                since,
                min_sdk,
            }),
            Some(_) => Ok(()),
            None => Err(NativeError::MissingDependency { library: library.clone(), abi, needed: needed.to_owned() }),
        }
    }
}

const fn api(level: u32) -> ApiLevel {
    match ApiLevel::try_new(level) {
        Ok(level) => level,
        Err(_) => panic!("invalid API level"),
    }
}

/// The NDK's stable system libraries, which apps may link against without
/// packaging them, and the API level each first shipped in.
pub const NDK_SYSTEM_LIBRARIES: &[(&str, ApiLevel)] = &[
    ("libaaudio.so", api(26)),
    ("libamidi.so", api(29)),
    ("libandroid.so", api(9)),
    ("libbinder_ndk.so", api(29)),
    ("libc.so", api(3)),
    ("libcamera2ndk.so", api(24)),
    ("libdl.so", api(3)),
    ("libEGL.so", api(9)),
    ("libGLESv1_CM.so", api(4)),
    ("libGLESv2.so", api(5)),
    ("libGLESv3.so", api(18)),
    ("libicu.so", api(31)),
    ("libjnigraphics.so", api(8)),
    ("liblog.so", api(3)),
    ("libm.so", api(3)),
    ("libmediandk.so", api(21)),
    ("libnativehelper.so", api(31)),
    ("libnativewindow.so", api(26)),
    ("libneuralnetworks.so", api(27)),
    ("libOpenMAXAL.so", api(14)),
    ("libOpenSLES.so", api(9)),
    ("libstdc++.so", api(3)),
    ("libsync.so", api(26)),
    ("libvulkan.so", api(24)),
    ("libz.so", api(3)),
];

/// One `PT_LOAD` segment.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct LoadSegment {
    pub offset: u64,
    pub virtual_address: u64,
    pub alignment: u64,
}

/// Packaging-relevant facts read from a shared library.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ElfInfo {
    pub abi: Abi,
    pub soname: Option<String>,
    /// `DT_NEEDED` entries: the libraries this one links against.
    pub needed: Vec<String>,
    pub load_segments: Vec<LoadSegment>,
    /// Exported (defined, global or weak, default or protected visibility) function names
    /// that matter to Android: activity entry points and `JNI_OnLoad`.
    pub entry_points: BTreeSet<String>,
    /// ARM `e_flags`; `0` for other machines.
    arm_flags: u32,
}

const INTERESTING_EXPORTS: [&str; 3] = [NATIVE_ACTIVITY_ENTRY, GAME_ACTIVITY_ENTRY, "JNI_OnLoad"];
const EF_ARM_EABIMASK: u32 = 0xff00_0000;
const EF_ARM_EABI_VER5: u32 = 0x0500_0000;
const EF_ARM_ABI_FLOAT_HARD: u32 = 0x400;

impl ElfInfo {
    /// Reads `bytes` as an Android shared library.
    pub fn parse(bytes: &[u8]) -> Result<Self, NativeError> {
        let elf = Elf::parse(bytes)?;
        if !elf.little_endian {
            return Err(NativeError::BigEndian);
        }
        if elf.header.e_type != header::ET_DYN {
            return Err(NativeError::NotSharedLibrary { e_type: elf.header.e_type });
        }
        let abi = match (elf.header.e_machine, elf.is_64) {
            (header::EM_ARM, false) => Abi::ArmeabiV7a,
            (header::EM_AARCH64, true) => Abi::Arm64V8a,
            (header::EM_386, false) => Abi::X86,
            (header::EM_X86_64, true) => Abi::X86_64,
            (header::EM_RISCV, true) => Abi::Riscv64,
            (machine, is_64) => {
                return Err(NativeError::UnsupportedMachine { machine, bits: if is_64 { 64 } else { 32 } });
            }
        };
        let load_segments = elf
            .program_headers
            .iter()
            .enumerate()
            .filter(|(_, ph)| ph.p_type == program_header::PT_LOAD)
            .map(|(segment, ph)| {
                if ph.p_filesz > ph.p_memsz {
                    return Err(NativeError::SegmentSize { segment, file_size: ph.p_filesz, memory_size: ph.p_memsz });
                }
                if ph.p_filesz != 0 && ph.p_offset.checked_add(ph.p_filesz).is_none_or(|end| end > bytes.len() as u64) {
                    return Err(NativeError::SegmentFileRange { segment, offset: ph.p_offset, size: ph.p_filesz });
                }
                Ok(LoadSegment { offset: ph.p_offset, virtual_address: ph.p_vaddr, alignment: ph.p_align })
            })
            .collect::<Result<_, _>>()?;
        let entry_points = elf
            .dynsyms
            .iter()
            .filter(|s| {
                s.st_type() == sym::STT_FUNC
                    && s.st_shndx != section_header::SHN_UNDEF as usize
                    && matches!(s.st_bind(), sym::STB_GLOBAL | sym::STB_WEAK)
                    && matches!(s.st_visibility(), sym::STV_DEFAULT | sym::STV_PROTECTED)
            })
            .filter_map(|s| elf.dynstrtab.get_at(s.st_name))
            .filter(|name| INTERESTING_EXPORTS.contains(name))
            .map(str::to_owned)
            .collect();
        Ok(Self {
            abi,
            soname: elf.soname.map(str::to_owned),
            needed: elf.libraries.iter().map(|l| (*l).to_owned()).collect(),
            load_segments,
            entry_points,
            arm_flags: if abi == Abi::ArmeabiV7a { elf.header.e_flags } else { 0 },
        })
    }

    /// Whether this library exports the function `symbol`.
    pub fn exports(&self, symbol: &str) -> bool {
        self.entry_points.contains(symbol)
    }

    /// The largest page size every load segment supports.
    pub fn max_page_size(&self) -> u64 {
        self.load_segments.iter().map(|s| s.alignment).min().unwrap_or(0)
    }

    /// Checks this library against `expected` ABI and the page size `policy`
    /// requires of it.
    pub fn check(&self, library: &LibraryName, expected: Abi, policy: &NativePolicy) -> Result<(), NativeError> {
        let library = || library.clone();
        if self.abi != expected {
            return Err(NativeError::AbiMismatch { library: library(), expected, found: self.abi });
        }
        if self.abi == Abi::ArmeabiV7a {
            if self.arm_flags & EF_ARM_EABIMASK != EF_ARM_EABI_VER5 {
                return Err(NativeError::NotArmEabi5 { library: library() });
            }
            if self.arm_flags & EF_ARM_ABI_FLOAT_HARD != 0 {
                return Err(NativeError::HardFloat { library: library() });
            }
        }
        if self.load_segments.is_empty() {
            return Err(NativeError::NoLoadSegments { library: library() });
        }
        let required = policy.page_size_for(self.abi).bytes();
        for (segment, load) in self.load_segments.iter().enumerate() {
            if load.alignment < required || !load.alignment.is_power_of_two() {
                return Err(NativeError::SegmentAlignment {
                    library: library(),
                    abi: self.abi,
                    segment,
                    alignment: load.alignment,
                    required,
                });
            }
            if load.offset % load.alignment != load.virtual_address % load.alignment {
                return Err(NativeError::SegmentIncongruent {
                    library: library(),
                    segment,
                    offset: load.offset,
                    address: load.virtual_address,
                    alignment: load.alignment,
                });
            }
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/native/libpostcard.so");

    fn library() -> LibraryName {
        LibraryName::try_new("postcard").unwrap()
    }

    #[test]
    fn reads_the_fixture() {
        let info = ElfInfo::parse(FIXTURE).unwrap();
        assert_eq!(info.abi, Abi::Arm64V8a);
        assert!(info.exports(NATIVE_ACTIVITY_ENTRY));
        assert!(info.max_page_size() >= PageSize::SixteenKiB.bytes());
        info.check(&library(), Abi::Arm64V8a, &NativePolicy::default()).unwrap();
    }

    #[test]
    fn rejects_wrong_abi_and_non_elf_input() {
        let info = ElfInfo::parse(FIXTURE).unwrap();
        assert!(matches!(
            info.check(&library(), Abi::X86_64, &NativePolicy::default()),
            Err(NativeError::AbiMismatch { expected: Abi::X86_64, found: Abi::Arm64V8a, .. })
        ));
        assert!(matches!(ElfInfo::parse(b"not an elf"), Err(NativeError::Elf(_))));
    }

    #[test]
    fn page_size_requirement_depends_on_the_abi() {
        let policy = NativePolicy::default();
        assert_eq!(policy.page_size_for(Abi::Arm64V8a), PageSize::SixteenKiB);
        assert_eq!(policy.page_size_for(Abi::ArmeabiV7a), PageSize::FourKiB);
    }

    #[test]
    fn system_libraries_must_exist_on_the_oldest_supported_release() {
        let policy = NativePolicy::default();
        let check = |needed, min_sdk| policy.check_system_library(&library(), Abi::Arm64V8a, needed, min_sdk);
        check("liblog.so", ApiLevel::LOLLIPOP).unwrap();
        check("libaaudio.so", ApiLevel::O).unwrap();
        assert!(matches!(
            check("libaaudio.so", ApiLevel::N),
            Err(NativeError::SystemLibraryTooNew { since, .. }) if since == ApiLevel::O
        ));
        assert!(matches!(check("libc++_shared.so", ApiLevel::N), Err(NativeError::MissingDependency { .. })));
        let vendor = NativePolicy { system_libraries: ["libOpenCL.so".to_owned()].into(), ..NativePolicy::default() };
        vendor.check_system_library(&library(), Abi::Arm64V8a, "libOpenCL.so", ApiLevel::N).unwrap();
    }

    #[test]
    fn underaligned_segments_are_rejected() {
        let mut info = ElfInfo::parse(FIXTURE).unwrap();
        info.load_segments[0].alignment = 4096;
        assert!(matches!(
            info.check(&library(), Abi::Arm64V8a, &NativePolicy::default()),
            Err(NativeError::SegmentAlignment { segment: 0, alignment: 4096, required: 16384, .. })
        ));
        let four = NativePolicy { page_size: PageSize::FourKiB, ..NativePolicy::default() };
        info.check(&library(), Abi::Arm64V8a, &four).unwrap();
    }

    #[test]
    fn load_segments_must_fit_the_file_and_their_memory_mapping() {
        let elf = Elf::parse(FIXTURE).unwrap();
        let index = elf
            .program_headers
            .iter()
            .position(|ph| ph.p_type == program_header::PT_LOAD && ph.is_executable())
            .unwrap();
        let at = elf.header.e_phoff as usize + index * elf.header.e_phentsize as usize;
        let mut bytes = FIXTURE.to_vec();
        bytes[at + 8..at + 16].copy_from_slice(&u64::MAX.to_le_bytes());
        assert!(matches!(ElfInfo::parse(&bytes), Err(NativeError::SegmentFileRange { .. })));

        let mut bytes = FIXTURE.to_vec();
        bytes[at + 32..at + 40].copy_from_slice(&(elf.program_headers[index].p_memsz + 1).to_le_bytes());
        assert!(matches!(ElfInfo::parse(&bytes), Err(NativeError::SegmentSize { .. })));
    }

    #[test]
    fn congruence_uses_the_declared_alignment() {
        let mut info = ElfInfo::parse(FIXTURE).unwrap();
        info.load_segments[0].alignment = 65536;
        info.load_segments[0].virtual_address = info.load_segments[0].offset + 16384;
        assert!(matches!(
            info.check(&library(), Abi::Arm64V8a, &NativePolicy::default()),
            Err(NativeError::SegmentIncongruent { alignment: 65536, .. })
        ));
    }

    #[test]
    fn activity_entry_must_be_a_visible_defined_function() {
        let elf = Elf::parse(FIXTURE).unwrap();
        let index =
            elf.dynsyms.iter().position(|s| elf.dynstrtab.get_at(s.st_name) == Some(NATIVE_ACTIVITY_ENTRY)).unwrap();
        let table = elf.section_headers.iter().find(|s| s.sh_type == section_header::SHT_DYNSYM).unwrap();
        let at = table.sh_offset as usize + index * table.sh_entsize as usize;
        for (field, replacement) in [(4, 0x02), (4, 0x11), (5, sym::STV_HIDDEN), (6, 0)] {
            let mut bytes = FIXTURE.to_vec();
            bytes[at + field] = replacement;
            assert!(!ElfInfo::parse(&bytes).unwrap().exports(NATIVE_ACTIVITY_ENTRY), "field {field}: {replacement}");
        }
    }
}