Establish native Linux ARM64 Android CI and verification gates (#94)
This commit is contained in:
1 parent
1abd75282e
commit
66b1d4a4c4
29 files changed
+1882
-56
No files matched your search
@@ -5,7 +5,7 @@ use std::{collections::BTreeMap, error::Error, fs::File, io::Read, path::Path};
|
||||
use object::{Architecture, BinaryFormat, Object, ObjectSection, ObjectSegment, ObjectSymbol};
|
||||
|
||||
const PAGE_SIZE: u64 = 16_384;
|
||||
const ABIS: [&str; 3] = ["arm64-v8a", "armeabi-v7a", "x86_64"];
|
||||
const ABIS: [&str; 2] = ["arm64-v8a", "armeabi-v7a"];
|
||||
|
||||
pub fn package_directory(input: &Path, output: &Path) -> Result<(), Box<dyn Error>> {
|
||||
audit_directory(input)?;
|
||||
@@ -71,7 +71,7 @@ pub fn audit_directory(directory: &Path) -> Result<(), Box<dyn Error>> {
|
||||
expected = Some(metadata);
|
||||
}
|
||||
}
|
||||
println!("Verified three native ABIs, 16 KB ELF alignment and identical UniFFI metadata");
|
||||
println!("Verified both ARM native ABIs, 16 KB ELF alignment and identical UniFFI metadata");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -79,7 +79,6 @@ pub fn audit_elf(bytes: &[u8], abi: &str) -> Result<(), Box<dyn Error>> {
|
||||
let expected = match abi {
|
||||
"arm64-v8a" => Architecture::Aarch64,
|
||||
"armeabi-v7a" => Architecture::Arm,
|
||||
"x86_64" => Architecture::X86_64,
|
||||
_ => return Err("unapproved native ABI".into()),
|
||||
};
|
||||
let library = object::File::parse(bytes)?;
|
||||
@@ -111,6 +110,61 @@ pub fn audit_split(path: &Path, abi: &str) -> Result<(), Box<dyn Error>> {
|
||||
audit_package_abis(path, &[abi])
|
||||
}
|
||||
|
||||
pub fn audit_release(package: &Path, symbols: &Path) -> Result<(), Box<dyn Error>> {
|
||||
audit_package(package)?;
|
||||
let mut package = zip::ZipArchive::new(File::open(package)?)?;
|
||||
let mut symbols = zip::ZipArchive::new(File::open(symbols)?)?;
|
||||
for abi in ABIS {
|
||||
for library in ["libironstorage_android.so", "libjnidispatch.so"] {
|
||||
let mut bytes = Vec::new();
|
||||
package
|
||||
.by_name(&format!("lib/{abi}/{library}"))?
|
||||
.read_to_end(&mut bytes)?;
|
||||
let mut debug = Vec::new();
|
||||
let mut entry = symbols.by_name(&format!("{abi}/{library}.dbg"))?;
|
||||
if entry.size() > 512 * 1024 * 1024 {
|
||||
return Err("native debug symbols exceed the audit bound".into());
|
||||
}
|
||||
entry.read_to_end(&mut debug)?;
|
||||
audit_symbol_pair(&bytes, &debug, abi, library == "libironstorage_android.so")?;
|
||||
}
|
||||
}
|
||||
println!("Release libraries are stripped; FULL symbols match both ARM build IDs");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn audit_symbol_pair(
|
||||
bytes: &[u8],
|
||||
debug: &[u8],
|
||||
abi: &str,
|
||||
require_debug_info: bool,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
audit_elf(bytes, abi)?;
|
||||
let release = object::File::parse(bytes)?;
|
||||
let symbols = object::File::parse(debug)?;
|
||||
if symbols.format() != BinaryFormat::Elf || symbols.architecture() != release.architecture() {
|
||||
return Err("debug archive architecture does not match release".into());
|
||||
}
|
||||
if release.sections().any(|s| {
|
||||
s.name()
|
||||
.is_ok_and(|n| n.starts_with(".debug_") || n == ".symtab")
|
||||
}) {
|
||||
return Err("release library contains unstripped debug symbols".into());
|
||||
}
|
||||
if (require_debug_info && symbols.section_by_name(".debug_info").is_none())
|
||||
|| symbols.symbols().next().is_none()
|
||||
{
|
||||
return Err("native symbol archive lacks required symbols/debug information".into());
|
||||
}
|
||||
let id = release
|
||||
.build_id()?
|
||||
.ok_or("release library lacks a build ID")?;
|
||||
if id.is_empty() || Some(id) != symbols.build_id()? {
|
||||
return Err("native symbol archive does not match release build ID".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn audit_package_abis(path: &Path, abis: &[&str]) -> Result<(), Box<dyn Error>> {
|
||||
let apk = path.extension().is_some_and(|ext| ext == "apk");
|
||||
if !apk && !path.extension().is_some_and(|ext| ext == "aab") {
|
||||
@@ -134,6 +188,18 @@ fn audit_package_abis(path: &Path, abis: &[&str]) -> Result<(), Box<dyn Error>>
|
||||
if !abis.contains(&abi) || library.contains('/') {
|
||||
return Err("package contains an unapproved native library path".into());
|
||||
}
|
||||
if ![
|
||||
"libironstorage_android.so",
|
||||
"libjnidispatch.so",
|
||||
"libandroidx.graphics.path.so",
|
||||
]
|
||||
.contains(&library)
|
||||
|| libraries
|
||||
.get(abi)
|
||||
.is_some_and(|names| names.iter().any(|name| name == library))
|
||||
{
|
||||
return Err("package contains an unreviewed or duplicate native library".into());
|
||||
}
|
||||
if apk
|
||||
&& (file.compression() != zip::CompressionMethod::Stored
|
||||
|| file.data_start() % PAGE_SIZE != 0)
|
||||
@@ -205,4 +271,14 @@ mod tests {
|
||||
assert!(audit_elf(b"\x7fELF", "x86_64").is_err());
|
||||
assert!(audit_elf(&[], "mips").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbol_gate_rejects_missing_full_symbols_and_build_identity() {
|
||||
assert!(
|
||||
audit_symbol_pair(&elf(PAGE_SIZE, 0), &elf(PAGE_SIZE, 0), "arm64-v8a", true).is_err()
|
||||
);
|
||||
assert!(
|
||||
audit_symbol_pair(&elf(PAGE_SIZE, 0), b"wrong archive", "arm64-v8a", false).is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user