Executing Binaries on Android — The SELinux Problem
Part 2 of 6 — building Pockr, a single APK that runs Docker on non-rooted Android.
← Part 1: The Idea and Architecture
The First Wall: Permission Denied
The most obvious approach is to bundle the QEMU binary inside the APK and extract it to app storage on first launch, then execute it with ProcessBuilder.
On Android 10+, this silently fails:
Cannot run program ".../files/qemu/qemu-system-aarch64":
error=13, Permission denied
This isn't a file permission issue. chmod +x won't fix it.
Why: SELinux W^X Policy
Android 10 enforces a W^X (Write XOR Execute) policy via SELinux. Any file in getFilesDir() — the app's private data directory — is labelled app_data_file. That label does not allow execve().
| Directory | SELinux Label | Executable? |
|---|---|---|
getFilesDir() |
app_data_file |
❌ |
getCacheDir() |
app_data_file |
❌ |
nativeLibraryDir |
exec_type |
✅ |
The native library directory is the exception — it's specifically labelled to allow execution. This is where Android puts .so files from your APK's jniLibs/.
The Fix: Ship QEMU as a .so File
Android's PackageManager extracts files from jniLibs/<abi>/ to nativeLibraryDir during installation. Those files must end in .so, but they don't have to be shared libraries.
We renamed the QEMU binary:
qemu-system-aarch64 → libqemu.so
qemu-img → libqemu_img.so
Put them in android/app/src/main/jniLibs/arm64-v8a/ and Android extracts them to:
/data/app/~~.../com.example.dockerapp-.../lib/arm64/libqemu.so
This path has exec_type. execve() works.
The Second Problem: Wrong ELF Interpreter
Our first attempt used a self-built QEMU binary compiled on Debian. Android installed it to nativeLibraryDir but silently refused to extract it.
The reason: Android's PackageManager only extracts .so files with the Android linker as interpreter:
# Debian-built QEMU (rejected)
readelf -l qemu | grep interpreter
[Requesting program interpreter: /lib/ld-linux-aarch64.so.1] ← glibc
# Termux-built QEMU (accepted)
readelf -l qemu | grep interpreter
[Requesting program interpreter: /system/bin/linker64] ← Bionic
Fix: Switch to pre-built QEMU from Termux packages. Termux builds everything against Android's Bionic libc and uses /system/bin/linker64.
One More Catch: useLegacyPackaging
AGP 3.6+ compresses native libraries inside the APK by default (to reduce download size). Compressed .so files are NOT extracted to disk — they're loaded directly from the APK zip.
QEMU is not a shared library. We need it on disk to execute it. Fix in build.gradle:
android {
packagingOptions {
jniLibs {
useLegacyPackaging true // force extraction to disk
}
}
}
Summary
| Problem | Root Cause | Fix |
|---|---|---|
Permission denied on exec |
SELinux W^X on filesDir
|
Ship as .so in jniLibs/
|
.so not extracted |
APK compression | useLegacyPackaging true |
| Wrong ELF interpreter | glibc binary | Use Termux pre-built QEMU |
Next: Part 3 — Bundling 50 Termux Libraries Without Breaking the ELF Linker
GitHub: github.com/AI2TH/Pockr
Pockr Series — Docker in Your Pocket
Pockr = Pocket + Docker. A single Android APK that runs real Docker containers in your pocket — no root, no Termux, no PC required.
| # | Post | Topic |
|---|---|---|
| 📖 | Intro | What is Pockr? Start here |
| 1 | Part 1 | The Idea and Architecture |
| 2 | Part 2 | Executing Binaries — The SELinux Problem |
| 3 | Part 3 | Bundling 50 Native Libraries |
| 4 | Part 4 | Docker Without Kernel Modules |
| 5 | Part 5 | Debugging the VM Restart Loop |
| 6 | Part 6 | Test Results and What's Next |
GitHub: github.com/AI2TH/Pockr
Systems Engineer: Kalvin Nathan
skalvinnathan@gmail.com · LinkedIn
Top comments (0)