Shipping QEMU in an APK
Part 2 of 4 — building Linxr, a single APK that runs Alpine Linux on non-rooted Android.
← Part 1: The Idea and Architecture
The Same Wall as Pockr
Getting QEMU to run on non-rooted Android is the same challenge Pockr solved first. If you've read Pockr Part 2 and Part 3, you already know the full story. This article gives the overview as it applies to Linxr.
Problem 1: SELinux Blocks execve()
On Android 10+, files in app storage have the SELinux label app_data_file. That label does not allow execve().
Cannot run program ".../files/qemu/qemu-system-aarch64":
error=13, Permission denied
chmod +x doesn't help — it's a mandatory access control policy, not a Unix permission issue.
Fix: Put QEMU in jniLibs/arm64-v8a/ as a .so file.
qemu-system-aarch64 → jniLibs/arm64-v8a/libqemu.so
qemu-img → jniLibs/arm64-v8a/libqemu_img.so
Android's PackageManager installs these to nativeLibraryDir which has exec_type SELinux label — execve() works.
Problem 2: Compressed jniLibs
AGP 3.6+ compresses native libraries inside the APK by default — they're loaded directly from the zip, not extracted to disk. QEMU needs to be on disk to execute.
Fix in build.gradle:
android {
packagingOptions {
jniLibs {
useLegacyPackaging true
}
}
}
Problem 3: Wrong ELF Interpreter
A self-built QEMU from a Linux desktop uses glibc:
[Requesting program interpreter: /lib/ld-linux-aarch64.so.1]
Android's PackageManager only extracts .so files using Android's Bionic linker:
[Requesting program interpreter: /system/bin/linker64]
Fix: Use Termux's pre-built QEMU packages — compiled against Bionic.
Problem 4: 50 Shared Library Dependencies
Termux QEMU dynamically links ~50 shared libraries that live at Termux's prefix — a path that doesn't exist on non-Termux devices. All 50 must be bundled in jniLibs/.
Two sub-problems:
-
Termux embeds a hardcoded
RUNPATHpointing to/data/data/com.termux/files/usr/lib/— Android's linker tries it first and fails -
patchelf --remove-rpathcorrupts ELF — it restructures LOAD segments, breaking Android 11's strict segment count limits
Fix: An in-place Python script that zeroes only the d_val field of DT_RPATH/DT_RUNPATH ELF entries, leaving the structure intact. See Pockr Part 3 for the full implementation.
Linxr vs Pockr: Same Libraries
Linxr uses the same 50-library bundle as Pockr. The only difference in the QEMU launch command is the forwarded port:
// Linxr: SSH only
cmd += listOf("-netdev", "user,id=net0,hostfwd=tcp::2222-:22")
// Pockr: HTTP API
cmd += listOf("-netdev", "user,id=net0,hostfwd=tcp::7080-:7080")
Next: Part 3 — SSH Terminal in Flutter
GitHub: github.com/AI2TH/Linxr
Linxr Series — Alpine Linux on Android
Linxr = Linux + r. A single Android APK that runs a full Alpine Linux shell on any Android phone — no root, no Termux, no PC required.
| # | Post | Topic |
|---|---|---|
| 📖 | Intro | What is Linxr? Start here |
| 1 | Part 1 | The Idea and Architecture |
| 2 | Part 2 | Shipping QEMU in an APK |
| 3 | Part 3 | SSH Terminal in Flutter |
| 4 | Part 4 | Test Results |
GitHub: github.com/AI2TH/Linxr
Website: ai2th.github.io
Top comments (0)