Skip to content

Releases: zalexdev/linux-um-arm64

arm64 UML prebuilt binaries (bionic/Android)

Choose a tag to compare

@zalexdev zalexdev released this 16 Aug 10:25

Prebuilt binaries for bionic (Android), so you can try this without building a kernel.

ARCH=um — User Mode Linux — has been x86-only for its whole existence. This is the arm64 subarch: a real Linux kernel running as an ordinary aarch64 userspace process. No root, no /dev/kvm, no kernel module, no capability. linux is a program; run it.

A shell, in one command:

chmod +x linux stub_exe
./linux mem=512M initrd=initramfs-alpine.cpio.gz rdinit=/bin/sh \
        stub_exe=$PWD/stub_exe panic=-1 con=null con0=fd:0,fd:1
~ # uname -srm
Linux 7.2.0-rc4-g8d984326036a aarch64

debian-docker.ext4.gz is Debian 12 with Docker installed, if you want the whole thing. Read RUN.md — it has the Docker commands, the console caveat, and an honest list of what was and was not tested.

Built from a528aeed4c28, and the kernel says so in uname, so a binary can always be traced to a commit.

Running the Debian command from RUN.md is what found the fourth bug: the kernel panicked in an exitcall on every clean shutdown when it had not been able to create its umid directory — which is what happens whenever $HOME is not writable, i.e. whenever anyone runs this straight from a shell. Both command lines in RUN.md are now tested exactly as written.

Notes

The kernel uses 16 KB guest pages, which serves both 4 KB and 16 KB hosts: a guest page is a host mmap(), so it may be larger than the host's but never smaller, and Android 15+ ships 16 KB hosts.

It picks the fastest interception method the host allows and prints it as Userspace mode:. Inside an app sandbox that is usually SECCOMP; from a plain adb shell on a recent device it is often ptrace. About 2.7 µs against 25 µs per guest syscall on this hardware — worth knowing, neither broken.

This branch fixes four kernel bugs, all found by running it

  • Pointer authentication was left enabled in the stub. One host process per guest mm means a guest fork() starts a new one via execve, which regenerates the PAC keys — so glibc's _Fork (paciasp / svc #0 / autiasp) authenticates a return address with a different key than signed it. On Armv8.6+ that traps and the child dies inside _Fork having printed nothing. NOPs on older CPUs. It broke every fork() in a branch-protected binary on Armv9 and was invisible on Armv8.2.
  • Guest FP/SIMD state never crossed a signal in seccomp mode: the guard compared against the uapi struct (528 bytes) instead of the payload copied (520). Go preempts goroutines with SIGURG at arbitrary instructions and its AES-GCM assembly holds cipher state across the register file, so TLS transfers past a few MB died with bad record MAC — which Docker reports as filesystem layer verification failed, pointing at a disk that is fine.
  • PTRS_PER_PTE was a literal 512 where it has to be PMD_SIZE >> PAGE_SHIFT: right at 4 KB, four times too large at 16 KB, letting generic mm write PTEs past the window a PMD owns.

Known limits: one CPU (UML's SMP support is x86-only; the arm64 subarch cannot select it yet), no 32-bit compat, and page faults about 7.6× native — that last one is being worked on.