1use std::collections::BTreeSet;
9
10use goblin::elf::{Elf, header, program_header, section_header, sym};
11
12use crate::types::{Abi, ApiLevel, LibraryName};
13
14pub const NATIVE_ACTIVITY_ENTRY: &str = "ANativeActivity_onCreate";
16pub const GAME_ACTIVITY_ENTRY: &str = "GameActivity_onCreate";
18
19#[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#[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#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
84pub enum LibraryStorage {
85 #[default]
88 Mapped,
89 Extracted,
92}
93
94#[derive(Clone, Debug, Eq, PartialEq)]
96pub struct NativePolicy {
97 pub page_size: PageSize,
99 pub storage: LibraryStorage,
100 pub check_dependencies: bool,
102 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 pub fn page_size_for(&self, abi: Abi) -> PageSize {
120 if abi.is_64_bit() { self.page_size } else { PageSize::FourKiB }
121 }
122
123 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
155pub 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#[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#[derive(Clone, Debug, Eq, PartialEq)]
195pub struct ElfInfo {
196 pub abi: Abi,
197 pub soname: Option<String>,
198 pub needed: Vec<String>,
200 pub load_segments: Vec<LoadSegment>,
201 pub entry_points: BTreeSet<String>,
204 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 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 pub fn exports(&self, symbol: &str) -> bool {
273 self.entry_points.contains(symbol)
274 }
275
276 pub fn max_page_size(&self) -> u64 {
278 self.load_segments.iter().map(|s| s.alignment).min().unwrap_or(0)
279 }
280
281 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}