use std::collections::BTreeSet;
use goblin::elf::{Elf, header, program_header, section_header, sym};
use crate::types::{Abi, ApiLevel, LibraryName};
pub const NATIVE_ACTIVITY_ENTRY: &str = "ANativeActivity_onCreate";
pub const GAME_ACTIVITY_ENTRY: &str = "GameActivity_onCreate";
#[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 },
}
#[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,
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum LibraryStorage {
#[default]
Mapped,
Extracted,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NativePolicy {
pub page_size: PageSize,
pub storage: LibraryStorage,
pub check_dependencies: bool,
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 {
pub fn page_size_for(&self, abi: Abi) -> PageSize {
if abi.is_64_bit() { self.page_size } else { PageSize::FourKiB }
}
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"),
}
}
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)),
];
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct LoadSegment {
pub offset: u64,
pub virtual_address: u64,
pub alignment: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ElfInfo {
pub abi: Abi,
pub soname: Option<String>,
pub needed: Vec<String>,
pub load_segments: Vec<LoadSegment>,
pub entry_points: BTreeSet<String>,
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 {
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 },
})
}
pub fn exports(&self, symbol: &str) -> bool {
self.entry_points.contains(symbol)
}
pub fn max_page_size(&self) -> u64 {
self.load_segments.iter().map(|s| s.alignment).min().unwrap_or(0)
}
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}");
}
}
}