mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +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 =
|
val verifyArtiAbis =
|
||||||
tasks.register("verifyArtiAbis") {
|
tasks.register("verifyArtiAbis") {
|
||||||
group = "verification"
|
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 jniLibs = file("src/main/jniLibs")
|
||||||
val abis = shippedAbis
|
val abis = shippedAbis
|
||||||
// Rust target triple per ABI, so the failure says exactly what to build.
|
// Per ABI: the Rust target triple (so a failure names the exact build
|
||||||
val triples =
|
// 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(
|
mapOf(
|
||||||
"arm64-v8a" to "aarch64-linux-android",
|
"arm64-v8a" to Triple("aarch64-linux-android", 2, 0xB7),
|
||||||
"x86_64" to "x86_64-linux-android",
|
"x86_64" to Triple("x86_64-linux-android", 2, 0x3E),
|
||||||
"armeabi-v7a" to "armv7-linux-androideabi",
|
"armeabi-v7a" to Triple("armv7-linux-androideabi", 1, 0x28),
|
||||||
"x86" to "i686-linux-android",
|
"x86" to Triple("i686-linux-android", 1, 0x03),
|
||||||
)
|
)
|
||||||
|
|
||||||
doLast {
|
doLast {
|
||||||
val missing = abis.filter { !File(jniLibs, "$it/libarti_android.so").exists() }
|
val bitness = mapOf(1 to "32-bit", 2 to "64-bit")
|
||||||
if (missing.isNotEmpty()) {
|
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(
|
throw GradleException(
|
||||||
buildString {
|
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("Those APK splits would install with Tor permanently unavailable.")
|
||||||
appendLine("Build them (tools/arti-build/README.md):")
|
appendLine("Rebuild them (tools/arti-build/README.md):")
|
||||||
missing.forEach { abi ->
|
problems.forEach { (abi, _) ->
|
||||||
val triple = triples[abi] ?: "<add the Rust target for $abi>"
|
val triple = expected[abi]?.first ?: "<add the Rust target for $abi>"
|
||||||
appendLine(" ./tools/arti-build/build-arti.sh --target=$triple")
|
appendLine(" ./tools/arti-build/build-arti.sh --target=$triple")
|
||||||
}
|
}
|
||||||
append("…or drop the ABI from `shippedAbis` in amethyst/build.gradle.kts.")
|
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 a single ABI without touching the other committed .so files
|
||||||
./build-arti.sh --target=armv7-linux-androideabi
|
./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
|
# Clean rebuild from scratch
|
||||||
./build-arti.sh --clean
|
./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
|
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`
|
`amethyst/build.gradle.kts`, `targets` in `rust-toolchain.toml`, and `TARGETS`
|
||||||
in `build-arti.sh`. The `verifyArtiAbis` Gradle task (wired into `preBuild`)
|
in `build-arti.sh`. (`verify-reproducible.sh` has no copy of its own — it asks
|
||||||
fails the build when an ABI split has no `libarti_android.so`, so the drift is
|
`build-arti.sh --print-abis`, so it can never hash a different set than the one
|
||||||
caught here rather than on a user's phone.
|
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
|
## Verifying 16KB page alignment
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
# # add one ABI without rewriting the other .so
|
# # add one ABI without rewriting the other .so
|
||||||
# # files already committed under jniLibs/.
|
# # files already committed under jniLibs/.
|
||||||
# ./build-arti.sh --clean # Clean and rebuild
|
# ./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
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -75,19 +80,21 @@ REGEN_LOCK=false
|
|||||||
# `${#empty_array[@]}` under `set -u` is an unbound-variable error. Target
|
# `${#empty_array[@]}` under `set -u` is an unbound-variable error. Target
|
||||||
# triples never contain spaces, so word splitting is exact here.
|
# triples never contain spaces, so word splitting is exact here.
|
||||||
SELECTED_TARGETS=""
|
SELECTED_TARGETS=""
|
||||||
|
PRINT_ABIS=false
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case $arg in
|
case $arg in
|
||||||
--release) TARGETS=("aarch64-linux-android") ;;
|
--release) TARGETS=("aarch64-linux-android") ;;
|
||||||
--target=*) SELECTED_TARGETS="$SELECTED_TARGETS ${arg#--target=}" ;;
|
--target=*) SELECTED_TARGETS="$SELECTED_TARGETS ${arg#--target=}" ;;
|
||||||
|
--print-abis) PRINT_ABIS=true ;;
|
||||||
--clean) CLEAN=true ;;
|
--clean) CLEAN=true ;;
|
||||||
# Refresh the committed Cargo.lock from the pinned Arti tag, then exit
|
# Refresh the committed Cargo.lock from the pinned Arti tag, then exit
|
||||||
# (no compile — needs only git + cargo, not the NDK). Use after bumping
|
# (no compile — needs only git + cargo, not the NDK). Use after bumping
|
||||||
# ARTI_VERSION / Cargo.toml; the normal build is --locked and will fail
|
# ARTI_VERSION / Cargo.toml; the normal build is --locked and will fail
|
||||||
# until the lock is regenerated and committed.
|
# until the lock is regenerated and committed.
|
||||||
--regen-lock) REGEN_LOCK=true; CLEAN=true ;;
|
--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
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -419,6 +426,16 @@ verify_ndk_stamp() {
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
main() {
|
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}"
|
echo -e "${BLUE}Arti Android Build — version $ARTI_VERSION${NC}"
|
||||||
|
|
||||||
# --regen-lock only needs git + cargo, not the NDK/cargo-ndk toolchain.
|
# --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
|
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/
|
# Only the ABIs this run actually rebuilds. Hashing everything under jniLibs/
|
||||||
# (what `find` used to do) made `--release` look like it had verified the
|
# (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
|
# 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
|
# identically in both runs and the script reported the whole tree reproducible
|
||||||
# and matching the commit.
|
# and matching the commit.
|
||||||
ABIS="arm64-v8a x86_64 armeabi-v7a x86"
|
#
|
||||||
SELECTED=""
|
# Asked of build-arti.sh with the very flags it is about to receive, rather than
|
||||||
for arg in ${PASSTHRU[@]+"${PASSTHRU[@]}"}; do
|
# kept as a second copy of the ABI list here. A local copy would drift the moment
|
||||||
case "$arg" in
|
# an ABI is added: this script would rebuild it twice, hash neither, and still
|
||||||
--release) ABIS="arm64-v8a" ;;
|
# print ✅ REPRODUCIBLE — the same false pass the paragraph above describes.
|
||||||
--target=*) SELECTED="$SELECTED $(abi_dir_for "${arg#--target=}")" ;;
|
# Under `set -e` a failing --print-abis (e.g. an unknown triple) aborts here.
|
||||||
esac
|
ABIS="$("$SCRIPT_DIR/build-arti.sh" --print-abis ${PASSTHRU[@]+"${PASSTHRU[@]}"} | tr '\n' ' ')"
|
||||||
done
|
ABIS="${ABIS% }"
|
||||||
[ -n "$SELECTED" ] && ABIS="${SELECTED# }"
|
|
||||||
|
|
||||||
# sha256 of each built .so, keyed by ABI dir (relative paths → stable keys).
|
# sha256 of each built .so, keyed by ABI dir (relative paths → stable keys).
|
||||||
hashes() {
|
hashes() {
|
||||||
|
|||||||
Reference in New Issue
Block a user