aboutsummaryrefslogtreecommitdiff
path: root/README
blob: 8c6f603d45bf2bb9d7d1adc219d590db519d2277 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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.