diff options
Diffstat (limited to 'README')
| -rw-r--r-- | README | 99 |
1 files changed, 99 insertions, 0 deletions
@@ -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://<guest-ip>: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. |