A self-built, trustworthy QEMU VM launcher for Android — a replacement for untrusted third-party APKs. Not an Android port of Virt-Forge (which stays desktop-only); a separate project. Fully standalone — the app bundles its own QEMU binary and runs an ARM64 Debian VM entirely on-device, no Termux dependency.
KvmDetector.kt— checks whether/dev/kvmis accessible and tells the user plainly whether the VM will run in KVM (fast) or TCG (software emulation, slow) modeNativeVmLauncher.kt— launches QEMU straight fromapplicationInfo.nativeLibraryDir(where Android extracted the bundled binary/libs at install time) withLD_LIBRARY_PATHset thereVmService.kt— foreground service that runs one QEMU process per architecture simultaneously (ARM64 and x86_64 at the same time is fine); each arch is stopped/started independently, andTerminalActivityattaches to whichever arch's console it was opened forMainActivity.kt— home screen with three tabs:- ARM64 VM / x86_64 VM — each an independent, self-contained slot:
its own KVM/TCG status, SSH/VNC/SPICE ports, headless toggle, Start/Stop,
Open Terminal, "View last launch command", and
Import rootfs.qcow2/Import seed.isobuttons. ARM64 and x86_64 use different default SSH ports (2222 vs 2322) and separatevm/arm64/vm/x86_64directories, so both VMs can run and import images at the same time without colliding. Each slot also has a "Clear … disk images" button to wipe its own images/firmware/log without touching the other arch - Proot — the "Open Proot Forge Terminal" entrance to the integrated proot-forge core, a live Boffin rootfs status readout, and a "Clear Boffin rootfs" button (deletes the downloaded archive and its extracted rootfs so the next session re-installs from scratch)
- ARM64 VM / x86_64 VM — each an independent, self-contained slot:
its own KVM/TCG status, SSH/VNC/SPICE ports, headless toggle, Start/Stop,
Open Terminal, "View last launch command", and
TerminalActivity.kt— a minimal interactive console for the VM's serial output (ANSI codes stripped for readability; not a full VT100 emulator — good for shell use, not for full-screen apps liketoporvim; the Proot Forge terminal is the full-featured one)core/main,core/proot,core/components,core/resources— the complete proot-forge core: Compose terminal, SessionService, init scripts, and the native PRoot runtime (libproot.so/libloader.sobuilt from C source by the:core:prootCMake project), including the Boffin rootfs-URL download installer
.github/workflows/build.yml automatically builds debug and release APKs
on every push/PR to main (via the checked-in Gradle wrapper — the AGP
9.2.1 / Kotlin 2.3.20 toolchain used by the proot core, so no local
Android Studio/PC setup is required). Once the build
finishes, go to the GitHub Actions tab and download
vm-forge-release-apk (installs directly, no extra setup) from that
run's "Artifacts" section.
The app needs three files in place before it can boot a VM: the Debian disk image, the UEFI firmware (bundled with the app already), and a cloud-init seed for the first-boot password. The disk image + seed are prepared once (on a PC or via Termux, since they need tools not worth bundling into the app itself) and then imported into the app:
- Get a Debian cloud image and build a seed ISO — either:
- For the ARM64 tab:
scripts/make-seed.shproduces~/vm-test/debian-13-genericcloud-arm64.qcow2and~/vm-test/seed.iso - For the x86_64 tab: do the same with an amd64 cloud image
(e.g.
debian-13-genericcloud-amd64.qcow2) - Or prepare equivalent files any other way
- For the ARM64 tab:
- Copy each file to somewhere the app's file picker can reach, e.g.
/sdcard/Download/(cp ~/vm-test/*.qcow2 ~/vm-test/seed.iso /sdcard/Download/in Termux) - In the app, on the relevant VM tab, tap "Import rootfs.qcow2" and
"Import seed.iso", picking each file from Downloads — this copies
them into that architecture's own slot (
vm/arm64/orvm/x86_64/) under the exact names QEMU expects. ARM64 and x86_64 keep separate images, so you can have both installed at once - Tap "Start … VM", then "Open Terminal …" to watch it boot and log
in. SSH is forwarded to 127.0.0.1:2222 for the ARM64 VM and
127.0.0.1:2322 for the x86_64 VM by default (both editable), and
both VMs can run at the same time (first-boot password is shown by
make-seed.sh— save it, it's not shown again) - Want a clean slate? Each VM tab has a "Clear … disk images" button, and the Proot tab has "Clear Boffin rootfs" — both ask for confirmation before deleting anything.
The qemu-system-aarch64 binary installed via pkg install in Termux is
hard-linked to Termux's own prefix (/data/data/com.termux/files/usr)
for shared libraries (glib, pixman, etc.), so it can't be copied straight
into another app. The process used to get a working standalone build:
scripts/test-in-termux.sh— boot-tested the VM using Termux's own QEMU first, to confirm the image/UEFI/cloud-init setup all work before touching the appscripts/collect-native-deps.sh— collectedqemu-system-aarch64and all its transitive.sodependencies from Termuxscripts/patch-for-jnilibs.sh— renamed the versioned.sofiles (e.g.libfoo.so.1→libfoo.so) and usedpatchelfto fix up SONAME/NEEDED references so they still resolve, then placed everything inapp/src/main/jniLibs/arm64-v8a/(notassets/— Android 10+ blocks executing, or even dlopen-mapping-as-executable, files copied to an app's writable private storage at runtime, regardless ofchmod; onlyjniLibs, extracted by PackageManager at install time, is exempt from this)app/build.gradle.ktssetspackaging.jniLibs.useLegacyPackaging = true— otherwise AGP keeps native libs uncompressed inside the APK instead of extracting them to disk, and the binary can't be exec'd from there
None of this needs to be repeated unless the bundled QEMU itself needs updating.
The app supports selecting ARM64 or x86_64 (AMD64) as the guest
architecture from the UI. x86_64 always runs in full software
emulation (TCG) on this ARM64 device — KVM only accelerates
same-architecture virtualization, never cross-architecture, regardless
of /dev/kvm access. Expect it to be significantly slower than ARM64
guests.
To add the x86_64 binary (this only needs to be done once):
pkg install qemu-system-x86_64-headlessin Termuxscripts/collect-native-deps.sh qemu-system-x86_64— merges into the same~/vm-forge-nativefolder used for ARM64 (most.sodependencies are shared between the two, since both are ARM64 host binaries that just emulate different guest architectures)scripts/patch-for-jnilibs.sh— now renames everyqemu-system-*binary it finds (not just aarch64) to thelibqemu_system_<arch>.soconvention- Copy the result into
app/src/main/jniLibs/arm64-v8a/as before — you should end up with bothlibqemu_system_aarch64.soandlibqemu_system_x86_64.soside by side, sharing the same dependency.sofiles - Also copy
edk2-x86_64-code.fdandedk2-x86_64-vars.fd(both from$PREFIX/share/qemu/) intoapp/src/main/assets/qemu-libs/, alongside the existingedk2-aarch64-code.fd. TheVARSfile is mandatory — see below. - Get an x86_64 Debian cloud image (same idea as
debian-13-genericcloud-arm64.qcow2but the-amd64.qcow2variant from the samecloud.debian.orgpath) and import it the same way
x86_64 = q35 + OVMF pflash (fixed & verified): unlike the ARM64
virt machine (which loads its full EDK2 image via -bios), the q35
machine refuses an OVMF CODE image via -bios — it hard-exits
with could not load PC BIOS because q35's -bios slot expects a 2MB
image and OVMF CODE is 3.6MB. The launcher therefore gives q35 two
pflash drives instead:
-drive if=pflash,format=raw,unit=0,file=edk2-x86_64-code.fd,readonly=on-drive if=pflash,format=raw,unit=1,file=edk2-x86_64-vars.fd(writable, so UEFI NVRAM now persists across boots)
plus -vga none (q35 creates a default std VGA whose vgabios-stdvga.bin
lives in QEMU's Termux data dir — absent on-device without Termux) and a
virtio-net-pci NIC with romfile= (q35 has no virtio-bus, and
virtio-net-device dies with No 'virtio-bus' bus found; the empty
romfile= also skips the efi-virtio.rom that would otherwise be looked
up in that same missing data dir). Boot-tested: OVMF → GRUB → Debian
cloud amd64 kernel comes all the way up.
Alongside the QEMU VM path, the app ships the full proot-forge core
(from the proot-forge project, as :core:main + :core:proot +
:core:components + :core:resources Gradle modules) as its PRoot
container subsystem. This replaces the app's original hand-rolled
PRoot container (PRootLauncher/ProotService) entirely:
- Compose terminal (
TerminalView/TerminalEmulatorfrom Termux) — a real ANSI/color/screen-redraw terminal, unlike the VM's line-basedTerminalActivity - Session service —
SessionServicekeeps each container session running in the foreground; multiple sessions, custom sessions, rename, sort, session switching - Init scripts —
init-host.sh/init.sh/rm-wrapper.sh, auto-extracted from assets into<filesDir-parent>/local/binon every app start (UpdateManager) - PRoot runtime with libloader — built from C source by
:core:proot(libproot.so+libloader.so, custom ARM64 build,--link2symlink, seccomp, etc.), replaced the previous bundledlibproot.so - Boffin rootfs URL install — tap Add Session → Boffin inside the
terminal to enter a direct
.tar.gzdownload URL (manifest-free, no file picker); the archive streams tofilesDir/boffin.tar.gzand is extracted byinit-host.sh - Android (host shell) session mode and user-defined custom sessions also work, plus a Settings/Customization drawer
All container state lives in <filesDir-parent>/local/. With this app's
applicationId = io.boffin.vmforge, that resolves to
/data/user/0/io.boffin.vmforge/local/ (init scripts use $PREFIX =
filesDir.parentFile and $PKG = package name, so there are no
hardcoded /data/data/io.boffin.proot/... paths left anywhere).
Build-wise, the proot core requires the newer AGP/Kotlin toolchain that
proot-forge already used (AGP 9.2.1, Kotlin 2.3.20, Gradle 9.4.1 via
the checked-in wrapper and gradle/libs.versions.toml version catalog).
- Open "Open Proot Forge Terminal" on the main screen
- Tap the menu → Add Session → Boffin, paste the rootfs
.tar.gzURL, and Download - The download streams to
filesDir/boffin.tar.gz, extracts tolocal/boffin/root, and drops you into the container shell — no storage permissions or external tools needed
- In-app VM setup: the disk image + seed still need to be prepared externally (Termux) and imported by hand; a fully in-app download/provisioning flow would remove that step
- Boffin rootfs URL: the direct-URL installer is in (from proot-forge) — previously the app also shipped an official Debian (bookworm) rootfs downloader, which has been removed
- KVM devices: untested on a device that actually has
/dev/kvmaccess (e.g. Pixel with pKVM) — should be significantly faster there for ARM64 guests (never applies to x86_64 guests, see above)