mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 11:18:24 +00:00
fix(build): close two gaps in the Arti ABI guard
Audit of the previous commit turned up two ways the new checks could still pass while the APK ships a dead Tor. verify-reproducible.sh kept its own copy of the ABI list, a fourth one next to splits.abi, rust-toolchain.toml and build-arti.sh's TARGETS. Add an ABI that copy misses and the script rebuilds it twice, hashes neither, and still prints REPRODUCIBLE — the same false pass its own comment says was fixed for --release. It now asks `build-arti.sh --print-abis` with the flags it is about to pass on, so the set it hashes is by construction the set that was just built. verifyArtiAbis only tested File.exists(). A zero-byte placeholder, a truncated file, or arm64's library copied into x86/ all satisfied that and all load as nothing on device — and unlike a missing file, they look fine in git. It now reads the ELF header and checks the class and e_machine for the ABI, which is what separates arm64's .so from armv7's when both are the right size and both exist. Verified: the guard still passes on the four committed libraries, and rejects an empty file, a 64-bit .so in a 32-bit dir, and armv7's .so in x86/ (same bitness, wrong machine) with the right rebuild command each time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MX1p385SiQMd2Lhkbg2YKc
This commit is contained in:
+52
-13
@@ -339,29 +339,68 @@ android {
|
||||
val verifyArtiAbis =
|
||||
tasks.register("verifyArtiAbis") {
|
||||
group = "verification"
|
||||
description = "Checks that every ABI in the APK splits has a libarti_android.so."
|
||||
description = "Checks that every ABI in the APK splits has a libarti_android.so for that architecture."
|
||||
|
||||
val jniLibs = file("src/main/jniLibs")
|
||||
val abis = shippedAbis
|
||||
// Rust target triple per ABI, so the failure says exactly what to build.
|
||||
val triples =
|
||||
// Per ABI: the Rust target triple (so a failure names the exact build
|
||||
// command) and the ELF identity the library must have — 32/64-bit class
|
||||
// (header byte 4) and e_machine (bytes 18-19, little-endian on every
|
||||
// Android ABI we ship). Existence alone is not enough: a truncated file,
|
||||
// an empty placeholder, or arm64's .so copied into x86/ all load as
|
||||
// nothing on device, which is the same silent dead Tor this task exists
|
||||
// to prevent — and unlike a missing file, those look fine in git.
|
||||
val expected =
|
||||
mapOf(
|
||||
"arm64-v8a" to "aarch64-linux-android",
|
||||
"x86_64" to "x86_64-linux-android",
|
||||
"armeabi-v7a" to "armv7-linux-androideabi",
|
||||
"x86" to "i686-linux-android",
|
||||
"arm64-v8a" to Triple("aarch64-linux-android", 2, 0xB7),
|
||||
"x86_64" to Triple("x86_64-linux-android", 2, 0x3E),
|
||||
"armeabi-v7a" to Triple("armv7-linux-androideabi", 1, 0x28),
|
||||
"x86" to Triple("i686-linux-android", 1, 0x03),
|
||||
)
|
||||
|
||||
doLast {
|
||||
val missing = abis.filter { !File(jniLibs, "$it/libarti_android.so").exists() }
|
||||
if (missing.isNotEmpty()) {
|
||||
val bitness = mapOf(1 to "32-bit", 2 to "64-bit")
|
||||
val problems = mutableListOf<Pair<String, String>>()
|
||||
|
||||
abis.forEach { abi ->
|
||||
val lib = File(jniLibs, "$abi/libarti_android.so")
|
||||
val want = expected[abi]
|
||||
val header = ByteArray(20)
|
||||
val read = if (lib.isFile) lib.inputStream().use { it.read(header) } else -1
|
||||
|
||||
val problem =
|
||||
when {
|
||||
!lib.isFile -> "no libarti_android.so"
|
||||
want == null -> "no expected ELF identity recorded for this ABI"
|
||||
read < header.size ||
|
||||
header[0] != 0x7F.toByte() ||
|
||||
header[1] != 'E'.code.toByte() ||
|
||||
header[2] != 'L'.code.toByte() ||
|
||||
header[3] != 'F'.code.toByte() -> "not an ELF file (truncated or corrupt)"
|
||||
header[4].toInt() != want.second ->
|
||||
"${bitness[header[4].toInt()] ?: "unknown-class"} ELF, expected ${bitness[want.second]}"
|
||||
else -> {
|
||||
val machine = (header[18].toInt() and 0xFF) or ((header[19].toInt() and 0xFF) shl 8)
|
||||
if (machine != want.third) {
|
||||
"built for ELF machine 0x%02x, expected 0x%02x".format(machine, want.third)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (problem != null) problems += abi to problem
|
||||
}
|
||||
|
||||
if (problems.isNotEmpty()) {
|
||||
throw GradleException(
|
||||
buildString {
|
||||
appendLine("No libarti_android.so for ABI(s): ${missing.joinToString()}.")
|
||||
appendLine("libarti_android.so is missing or wrong for ${problems.size} ABI split(s):")
|
||||
problems.forEach { (abi, problem) -> appendLine(" $abi: $problem") }
|
||||
appendLine("Those APK splits would install with Tor permanently unavailable.")
|
||||
appendLine("Build them (tools/arti-build/README.md):")
|
||||
missing.forEach { abi ->
|
||||
val triple = triples[abi] ?: "<add the Rust target for $abi>"
|
||||
appendLine("Rebuild them (tools/arti-build/README.md):")
|
||||
problems.forEach { (abi, _) ->
|
||||
val triple = expected[abi]?.first ?: "<add the Rust target for $abi>"
|
||||
appendLine(" ./tools/arti-build/build-arti.sh --target=$triple")
|
||||
}
|
||||
append("…or drop the ABI from `shippedAbis` in amethyst/build.gradle.kts.")
|
||||
|
||||
@@ -135,6 +135,9 @@ cd tools/arti-build
|
||||
# Build a single ABI without touching the other committed .so files
|
||||
./build-arti.sh --target=armv7-linux-androideabi
|
||||
|
||||
# Print the jniLibs ABI dirs a given invocation would write, then exit
|
||||
./build-arti.sh --print-abis --release # -> arm64-v8a
|
||||
|
||||
# Clean rebuild from scratch
|
||||
./build-arti.sh --clean
|
||||
```
|
||||
@@ -173,9 +176,13 @@ never connect — so the ABI loses its DMs too, not just Tor.
|
||||
|
||||
The ABI list therefore lives in three places that must agree: `splits.abi` in
|
||||
`amethyst/build.gradle.kts`, `targets` in `rust-toolchain.toml`, and `TARGETS`
|
||||
in `build-arti.sh`. The `verifyArtiAbis` Gradle task (wired into `preBuild`)
|
||||
fails the build when an ABI split has no `libarti_android.so`, so the drift is
|
||||
caught here rather than on a user's phone.
|
||||
in `build-arti.sh`. (`verify-reproducible.sh` has no copy of its own — it asks
|
||||
`build-arti.sh --print-abis`, so it can never hash a different set than the one
|
||||
it just rebuilt.) The `verifyArtiAbis` Gradle task, wired into `preBuild`, fails
|
||||
the build when an ABI split has no `libarti_android.so` **or** has one that is
|
||||
not an ELF of that architecture — a truncated file or arm64's library copied
|
||||
into `x86/` loads as nothing on device, exactly like a missing one, and unlike a
|
||||
missing one it looks fine in `git status`.
|
||||
|
||||
## Verifying 16KB page alignment
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
# # add one ABI without rewriting the other .so
|
||||
# # files already committed under jniLibs/.
|
||||
# ./build-arti.sh --clean # Clean and rebuild
|
||||
# ./build-arti.sh --print-abis # print the jniLibs ABI dirs this invocation
|
||||
# # would write (honours --release/--target=),
|
||||
# # then exit. This is how verify-reproducible.sh
|
||||
# # learns the ABI list instead of keeping its
|
||||
# # own copy of it.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
@@ -75,19 +80,21 @@ REGEN_LOCK=false
|
||||
# `${#empty_array[@]}` under `set -u` is an unbound-variable error. Target
|
||||
# triples never contain spaces, so word splitting is exact here.
|
||||
SELECTED_TARGETS=""
|
||||
PRINT_ABIS=false
|
||||
|
||||
# Parse arguments
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--release) TARGETS=("aarch64-linux-android") ;;
|
||||
--target=*) SELECTED_TARGETS="$SELECTED_TARGETS ${arg#--target=}" ;;
|
||||
--print-abis) PRINT_ABIS=true ;;
|
||||
--clean) CLEAN=true ;;
|
||||
# Refresh the committed Cargo.lock from the pinned Arti tag, then exit
|
||||
# (no compile — needs only git + cargo, not the NDK). Use after bumping
|
||||
# ARTI_VERSION / Cargo.toml; the normal build is --locked and will fail
|
||||
# until the lock is regenerated and committed.
|
||||
--regen-lock) REGEN_LOCK=true; CLEAN=true ;;
|
||||
--help) echo "Usage: $0 [--release] [--target=<triple>]... [--clean] [--regen-lock] [--help]"; exit 0 ;;
|
||||
--help) echo "Usage: $0 [--release] [--target=<triple>]... [--clean] [--regen-lock] [--print-abis] [--help]"; exit 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
@@ -419,6 +426,16 @@ verify_ndk_stamp() {
|
||||
# ============================================================================
|
||||
|
||||
main() {
|
||||
# Answer --print-abis before any other output, so the caller gets exactly the
|
||||
# ABI directory names on stdout and nothing else. Runs here rather than in the
|
||||
# argument loop because abi_dir_for is not defined yet at that point.
|
||||
if [ "$PRINT_ABIS" = true ]; then
|
||||
for target in "${TARGETS[@]}"; do
|
||||
abi_dir_for "$target"
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}Arti Android Build — version $ARTI_VERSION${NC}"
|
||||
|
||||
# --regen-lock only needs git + cargo, not the NDK/cargo-ndk toolchain.
|
||||
|
||||
@@ -30,33 +30,19 @@ sha256() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@"; else shasum -a 256 "$@"; fi
|
||||
}
|
||||
|
||||
# Mirrors abi_dir_for() in build-arti.sh — kept in step with it, since a triple
|
||||
# that maps to the wrong directory here would hash a library this run never
|
||||
# rebuilt and call it reproducible.
|
||||
abi_dir_for() {
|
||||
case "$1" in
|
||||
aarch64-linux-android) echo "arm64-v8a" ;;
|
||||
x86_64-linux-android) echo "x86_64" ;;
|
||||
armv7-linux-androideabi) echo "armeabi-v7a" ;;
|
||||
i686-linux-android) echo "x86" ;;
|
||||
*) echo "Unknown Rust target '$1'" >&2; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Only the ABIs this run actually rebuilds. Hashing everything under jniLibs/
|
||||
# (what `find` used to do) made `--release` look like it had verified the
|
||||
# x86_64 library: that build never touches it, so the untouched file hashed
|
||||
# identically in both runs and the script reported the whole tree reproducible
|
||||
# and matching the commit.
|
||||
ABIS="arm64-v8a x86_64 armeabi-v7a x86"
|
||||
SELECTED=""
|
||||
for arg in ${PASSTHRU[@]+"${PASSTHRU[@]}"}; do
|
||||
case "$arg" in
|
||||
--release) ABIS="arm64-v8a" ;;
|
||||
--target=*) SELECTED="$SELECTED $(abi_dir_for "${arg#--target=}")" ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$SELECTED" ] && ABIS="${SELECTED# }"
|
||||
#
|
||||
# Asked of build-arti.sh with the very flags it is about to receive, rather than
|
||||
# kept as a second copy of the ABI list here. A local copy would drift the moment
|
||||
# an ABI is added: this script would rebuild it twice, hash neither, and still
|
||||
# print ✅ REPRODUCIBLE — the same false pass the paragraph above describes.
|
||||
# Under `set -e` a failing --print-abis (e.g. an unknown triple) aborts here.
|
||||
ABIS="$("$SCRIPT_DIR/build-arti.sh" --print-abis ${PASSTHRU[@]+"${PASSTHRU[@]}"} | tr '\n' ' ')"
|
||||
ABIS="${ABIS% }"
|
||||
|
||||
# sha256 of each built .so, keyed by ABI dir (relative paths → stable keys).
|
||||
hashes() {
|
||||
|
||||
Reference in New Issue
Block a user