DEV Community

Ai2th
Ai2th

Posted on

Linxr | Part 2 — Shipping QEMU in an APK

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Problem 3: Wrong ELF Interpreter

A self-built QEMU from a Linux desktop uses glibc:

[Requesting program interpreter: /lib/ld-linux-aarch64.so.1]
Enter fullscreen mode Exit fullscreen mode

Android's PackageManager only extracts .so files using Android's Bionic linker:

[Requesting program interpreter: /system/bin/linker64]
Enter fullscreen mode Exit fullscreen mode

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:

  1. Termux embeds a hardcoded RUNPATH pointing to /data/data/com.termux/files/usr/lib/ — Android's linker tries it first and fails
  2. patchelf --remove-rpath corrupts 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")
Enter fullscreen mode Exit fullscreen mode

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)