180 lines
8.1 KiB
Zig
180 lines
8.1 KiB
Zig
//! Checks that each Android `libtally.so` will load on a device (design 6.3).
|
|
//!
|
|
//! The library links against nothing: no libc, no NDK. So every symbol it references
|
|
//! has to be one it defines, it may not have a TLS segment (only Bionic's libc can
|
|
//! satisfy `__tls_get_addr`), and it may not name a library it needs. Each of those
|
|
//! links on a desktop and fails in `dlopen` on a phone, before any code in the library
|
|
//! runs. The first APK failed exactly that way, and a later instance reproduced on
|
|
//! arm64-v8a alone, so an x86_64 emulator would have passed while phones failed. The
|
|
//! code that pulls such a symbol in never has to run for it to matter: `BIND_NOW` is
|
|
//! set, so even an unreferenced PLT entry is resolved at load.
|
|
//!
|
|
//! Also checked: every `LOAD` segment is aligned for 16 KiB pages, which Android 15
|
|
//! introduced and Play requires (a 4 KiB alignment loads on a 4 KiB device and is
|
|
//! refused at submission), the library is for the architecture its directory names,
|
|
//! and the JNI entry points are exported.
|
|
//!
|
|
//! This replaced `android/audit-libs.sh`, a `readelf` pipeline that CI ran as a step of
|
|
//! its own after the build, and that needed binutils installed to do it. Here it is part
|
|
//! of `zig build android`, on every machine that builds the libraries, and a library
|
|
//! that fails it is never installed for Gradle to package.
|
|
//!
|
|
//! Usage: audit-android <abi> <path/to/libtally.so> [<abi> <path> ...]
|
|
//! Prints nothing when every library passes; otherwise every problem found, and
|
|
//! exits non-zero.
|
|
|
|
const std = @import("std");
|
|
const elf = std.elf;
|
|
|
|
/// Android's page size since version 15, and what Play holds libraries to.
|
|
const page_alignment = 16 * 1024;
|
|
|
|
/// Every JNI entry point is named for the Kotlin class it belongs to.
|
|
const jni_prefix = "Java_dev_lerch_tally_TallyEngine_";
|
|
|
|
pub fn main(init: std.process.Init) !u8 {
|
|
const allocator = init.arena.allocator();
|
|
const args = try init.minimal.args.toSlice(allocator);
|
|
if (args.len < 3 or (args.len - 1) % 2 != 0) {
|
|
std.log.err("usage: audit-android <abi> <libtally.so> [<abi> <libtally.so> ...]", .{});
|
|
return 2;
|
|
}
|
|
|
|
var stderr_buf: [4096]u8 = undefined;
|
|
var stderr = std.Io.File.stderr().writerStreaming(init.io, &stderr_buf);
|
|
const out = &stderr.interface;
|
|
|
|
var failed = false;
|
|
var i: usize = 1;
|
|
while (i < args.len) : (i += 2) {
|
|
const abi = args[i];
|
|
const path = args[i + 1];
|
|
const bytes = std.Io.Dir.cwd().readFileAlloc(init.io, path, allocator, .limited(64 * 1024 * 1024)) catch |err| {
|
|
try out.print("{s}: cannot read {s}: {t}\n", .{ abi, path, err });
|
|
failed = true;
|
|
continue;
|
|
};
|
|
var report: Report = .{ .abi = abi, .out = out };
|
|
audit(allocator, abi, bytes, &report) catch |err| {
|
|
try out.print("{s}: {s} is not an ELF file this can read: {t}\n", .{ abi, path, err });
|
|
failed = true;
|
|
continue;
|
|
};
|
|
if (report.problems > 0) failed = true;
|
|
}
|
|
try out.flush();
|
|
return if (failed) 1 else 0;
|
|
}
|
|
|
|
/// Problems found in one library, written as they are found.
|
|
const Report = struct {
|
|
abi: []const u8,
|
|
out: *std.Io.Writer,
|
|
problems: usize = 0,
|
|
|
|
fn problem(self: *Report, comptime fmt: []const u8, args: anytype) !void {
|
|
self.problems += 1;
|
|
try self.out.print("{s}: " ++ fmt ++ "\n", .{self.abi} ++ args);
|
|
}
|
|
};
|
|
|
|
/// The ELF machine each Android ABI directory holds.
|
|
fn expectedMachine(abi: []const u8) ?elf.EM {
|
|
if (std.mem.eql(u8, abi, "arm64-v8a")) return .AARCH64;
|
|
if (std.mem.eql(u8, abi, "x86_64")) return .X86_64;
|
|
if (std.mem.eql(u8, abi, "armeabi-v7a")) return .ARM;
|
|
return null;
|
|
}
|
|
|
|
fn audit(allocator: std.mem.Allocator, abi: []const u8, bytes: []const u8, report: *Report) !void {
|
|
var reader: std.Io.Reader = .fixed(bytes);
|
|
const header = try elf.Header.read(&reader);
|
|
|
|
if (expectedMachine(abi)) |machine| {
|
|
if (header.machine != machine) try report.problem("built for {t}, not {t}", .{ header.machine, machine });
|
|
} else {
|
|
try report.problem("not an Android ABI this knows", .{});
|
|
}
|
|
|
|
// Program headers: no TLS, every LOAD aligned for 16 KiB pages, and where the
|
|
// dynamic section is.
|
|
var dynamic: ?struct { offset: u64, size: u64 } = null;
|
|
var phdrs = header.iterateProgramHeadersBuffer(bytes);
|
|
while (try phdrs.next()) |phdr| switch (phdr.p_type) {
|
|
elf.PT_TLS => try report.problem("has a TLS segment, which needs __tls_get_addr from a libc", .{}),
|
|
elf.PT_LOAD => if (phdr.p_align < page_alignment) {
|
|
try report.problem("a LOAD segment is aligned to 0x{x}, not the 0x{x} 16 KiB pages need", .{ phdr.p_align, page_alignment });
|
|
},
|
|
elf.PT_DYNAMIC => dynamic = .{ .offset = phdr.p_offset, .size = phdr.p_filesz },
|
|
else => {},
|
|
};
|
|
|
|
// The dynamic symbol table and its strings, from the section headers.
|
|
var dynsym: ?elf.Elf64_Shdr = null;
|
|
var dynstr: ?elf.Elf64_Shdr = null;
|
|
var sections = header.iterateSectionHeadersBuffer(bytes);
|
|
var all_sections: std.ArrayList(elf.Elf64_Shdr) = .empty;
|
|
while (try sections.next()) |shdr| try all_sections.append(allocator, shdr);
|
|
for (all_sections.items) |shdr| {
|
|
if (shdr.sh_type == elf.SHT_DYNSYM) {
|
|
dynsym = shdr;
|
|
if (shdr.sh_link < all_sections.items.len) dynstr = all_sections.items[shdr.sh_link];
|
|
}
|
|
}
|
|
const symbols = dynsym orelse return report.problem("has no dynamic symbol table, so it exports nothing", .{});
|
|
const strings = dynstr orelse return report.problem("has no string table for its dynamic symbols", .{});
|
|
const names = try slice(bytes, strings.sh_offset, strings.sh_size);
|
|
|
|
// Dynamic entries: no DT_NEEDED.
|
|
if (dynamic) |d| {
|
|
var entries = header.iterateDynamicSectionBuffer(bytes, d.offset, d.size);
|
|
while (try entries.next()) |entry| {
|
|
if (entry.d_tag == elf.DT_NULL) break;
|
|
if (entry.d_tag == elf.DT_NEEDED) {
|
|
try report.problem("needs {s}, and it may need no library", .{name(names, entry.d_val)});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Symbols: none undefined, and the JNI entry points defined.
|
|
const entry_size: u64 = if (header.is_64) @sizeOf(elf.Elf64_Sym) else @sizeOf(elf.Elf32_Sym);
|
|
const table = try slice(bytes, symbols.sh_offset, symbols.sh_size);
|
|
var jni_exports: usize = 0;
|
|
var has_onload = false;
|
|
var at: usize = 0;
|
|
while (at + entry_size <= table.len) : (at += @intCast(entry_size)) {
|
|
var sym_reader: std.Io.Reader = .fixed(table[at..]);
|
|
const sym_name: u32, const section: u16 = if (header.is_64) blk: {
|
|
const sym = try sym_reader.takeStruct(elf.Elf64_Sym, header.endian);
|
|
break :blk .{ sym.st_name, sym.st_shndx };
|
|
} else blk: {
|
|
const sym = try sym_reader.takeStruct(elf.Elf32_Sym, header.endian);
|
|
break :blk .{ sym.st_name, sym.st_shndx };
|
|
};
|
|
const text = name(names, sym_name);
|
|
// The first entry is the null symbol, which has no name and is not a reference.
|
|
if (text.len == 0) continue;
|
|
if (section == elf.SHN_UNDEF) {
|
|
try report.problem("references {s} and does not define it", .{text});
|
|
} else if (std.mem.startsWith(u8, text, jni_prefix)) {
|
|
jni_exports += 1;
|
|
} else if (std.mem.eql(u8, text, "JNI_OnLoad")) {
|
|
has_onload = true;
|
|
}
|
|
}
|
|
if (jni_exports == 0) try report.problem("exports no {s}* entry points", .{jni_prefix});
|
|
if (!has_onload) try report.problem("does not export JNI_OnLoad", .{});
|
|
}
|
|
|
|
/// `len` bytes at `offset`, or an error if the file is shorter than its headers say.
|
|
fn slice(bytes: []const u8, offset: u64, len: u64) error{EndOfStream}![]const u8 {
|
|
if (offset > bytes.len or len > bytes.len - offset) return error.EndOfStream;
|
|
return bytes[@intCast(offset)..][0..@intCast(len)];
|
|
}
|
|
|
|
/// The NUL-terminated string at `offset` in a string table.
|
|
fn name(strings: []const u8, offset: u64) []const u8 {
|
|
if (offset >= strings.len) return "";
|
|
const rest = strings[@intCast(offset)..];
|
|
return rest[0 .. std.mem.indexOfScalar(u8, rest, 0) orelse rest.len];
|
|
}
|