From 29b53a8f56814499490c96169464103b89174365 Mon Sep 17 00:00:00 2001 From: Lena Date: Thu, 1 Jan 2026 00:00:00 +0000 Subject: alpine-avf: replace the Android Linux Terminal VM with Alpine The Terminal app ships Debian and offers no supported way to change the guest. Android root is not available, so the only writable surface is the payload directory the app exposes to the guest over virtiofs, and the app has to keep working unmodified against whatever replaces its image. Those constraints force the two-stage install. A live root cannot be overwritten in place, and crosvm only exposes the partitions listed in vm_config.json when the VM starts, so a second partition has to be staged and the VM rebooted before there is a block device to write the new root through. The app also inspects the payload in undocumented ways. The README records what it expects and why the image is built to match. --- README | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 README (limited to 'README') diff --git a/README b/README new file mode 100644 index 0000000..8c6f603 --- /dev/null +++ b/README @@ -0,0 +1,99 @@ +alpine-avf +========== +Replace the Debian VM behind Android's official "Linux Terminal" app (AVF) with +Alpine Linux, from inside the stock VM, with no Android root. The unmodified +Terminal app then boots Alpine and connects to a "user" shell. + + +How it works +------------ +The Terminal app boots a crosvm VM from a payload directory it exposes to the +guest over virtiofs at /mnt/internal/linux (root-writable). build-alpine-avf +produces that payload; install-alpine-avf swaps it in from inside the running VM. + +The app finds the terminal purely over mDNS: it resolves the Avahi service named +"ttyd" (_http._tcp), connects to https://:7681, presents a client cert +(which ttyd verifies against /mnt/internal/ca.crt), and shows ttyd's web UI. + + +Usage +----- +In the stock Debian VM, as root: + + sudo ./build-alpine-avf # writes ./alpine-avf + sudo ./install-alpine-avf # stage 1: stages the image, reboots + # reopen Terminal, then: + sudo ./install-alpine-avf # stage 2: installs it, reboots into Alpine + +Run both install stages from the same directory: stage 1 records the image path +in an alpine-avf-install marker file there, and stage 2 reads it back. + +The login is "user" (no password, passwordless doas); root is also +passwordless on the VM console, for debugging. build-alpine-avf takes +two optional env vars: ALPINE_BRANCH (default v3.23; "latest-stable" tracks the +newest) and ROOT_SIZE (default 4G). ROOT_SIZE is only the INITIAL filesystem +size: on first boot Alpine grows the root to fill whatever disk the Terminal app +provisions (see the "Disk size" note), matching what the stock Debian VM did. + + +Testing +------- +build-alpine-avf cross-builds an aarch64 image on an x86 host that has the +qemu-user-static binfmt handler for aarch64 registered. The result boots under +qemu-system-aarch64, which smoke-tests the boot chain (kernel extraction, +initramfs, root mount, OpenRC, DHCP) but not the takeover or ttyd: qemu has +no AVF virtiofs, so avf-mounts fails loudly and ttyd, which needs it and the +app's CA under /mnt/internal, stays down. + + sudo ./build-alpine-avf + qemu-system-aarch64 -M virt -cpu cortex-a57 -m 3072 -smp 2 \ + -kernel alpine-avf/vmlinuz -initrd alpine-avf/initrd.img \ + -append "root=/dev/vda rootfstype=ext4 rw console=ttyAMA0" \ + -drive file=alpine-avf/root_part,format=raw,if=virtio \ + -device virtio-gpu-pci -netdev user,id=n -device virtio-net-pci,netdev=n \ + -nographic + +A clean run reaches the default runlevel on the serial console. There is no +login prompt there: the image's getty is on ttyS0 (crosvm's serial), and the +qemu virt machine provides ttyAMA0. For a shell inside the image, add +init=/bin/sh to -append (bypasses OpenRC). Exit qemu with C-a x. Note +root=/dev/vda here (the raw ext4 image is the whole disk) versus /dev/vda1 +under crosvm (which presents a composite disk). + + +Notes +----- +- Kernel: Alpine's vmlinuz is an EFI zboot PE (gzip-wrapped); crosvm's direct + boot needs the raw arm64 image, so the build extracts it. +- Runlevels: a minirootfs ships them empty (no setup-alpine), so the build + seeds the standard services explicitly. +- DNS: the stock Debian VM's /etc/resolv.conf is a stale GCP build-env file, so + the build gives the chroot its own resolvers for apk and drops them before + mkfs; DHCP supplies the guest's. +- Mountpoints: /mnt/internal and /mnt/shared are pre-created in the image, + since OpenRC mounts them before the root fs is remounted read-write. +- Clock: The build installs an early avf-clock OpenRC service that sets the + clock from /mnt/internal/ca.crt's mtime before networking and ttyd start. +- Image writes: dd the root image through the new block device, not over + virtiofs (a file write over virtiofs corrupts); kernel/initrd/config go over + virtiofs but are verified, before the destructive root swap. +- build_id: the Terminal app parses the last space-separated token of + /mnt/internal/linux/build_id as a 4-digit year and, if it is below the app's + release year (InstalledImage.isOlderThanCurrentVersion), declares the image + outdated and initiates recovery of the stock image, so build_id must + therefore end in a current year. +- Disk size: the app sizes the root disk host-side, not the guest. With storage + ballooning on (the usual case) it grows the root_part backing file to ~95% of + total phone storage on every boot (VmLauncherService.calculateSparseDiskSize) + but leaves the guest filesystem alone, expecting the guest to grow it online; + an avf-resize OpenRC service runs resize2fs on the root device at boot to + fill it (idempotent; a no-op once filled). install-alpine-avf also sizes the + staged partition to the larger of the image and the outgoing root_part, so + the device is already large even without ballooning. ext4 online-grows + because mkfs keeps resize_inode. + + +Recovery +-------- +If a VM is left unbootable, reinstall stock Debian from Android Settings > +Developer options > Linux development environment > reset. -- cgit v1.2.3