#!/bin/sh # AC Native OS — Chromebook boot stub. # # This tiny initramfs is embedded in vmlinuz (CONFIG_INITRAMFS_SOURCE) so the # kernel can be packed with vbutil_kernel into a ChromeOS kernel partition and # booted by stock Chromebook firmware (developer mode, Ctrl+U). Depthcharge # hands the kernel no initrd, so this script finds the stick's ACBOOT (or # ACEFI) FAT partition, unpacks the real initramfs.cpio.gz into a tmpfs and # switch_roots into it. From there boot is byte-identical to the UEFI path. # # On UEFI boots the firmware-supplied initrd is unpacked over this one, its # /init replaces this file, and this script never runs. export PATH=/bin:/sbin mount -t proc proc /proc 2>/dev/null mount -t sysfs sysfs /sys 2>/dev/null mount -t devtmpfs devtmpfs /dev 2>/dev/null say() { echo "[ac-stub] $*" > /dev/kmsg 2>/dev/null echo "[ac-stub] $*" > /dev/console 2>/dev/null } fail() { say "$*" say "dropping to a shell — the USB stick is probably missing initramfs.cpio.gz" exec sh } say "chromebook boot stub: waiting for the AC OS stick" mkdir -p /boot /newroot # USB enumeration takes a second or three; poll for up to 30 s. Any FAT # partition carrying initramfs.cpio.gz + config.json is ours (ACBOOT is # partition 1, ACEFI partition 2 — either works, ACBOOT is found first). found="" tries=0 while [ "$tries" -lt 60 ]; do for p in /dev/sd[a-h][1-9] /dev/mmcblk[0-9]p[1-9] /dev/nvme[0-9]n[0-9]p[1-9]; do [ -b "$p" ] || continue mount -t vfat -o ro "$p" /boot 2>/dev/null || continue if [ -f /boot/initramfs.cpio.gz ] && [ -f /boot/config.json ]; then found="$p" break fi umount /boot 2>/dev/null done [ -n "$found" ] && break tries=$((tries + 1)) sleep 0.5 done [ -n "$found" ] || fail "no partition with initramfs.cpio.gz appeared after 30 s" say "found AC OS files on $found" # The real initramfs is ~1 GB unpacked; the UEFI path holds the same bytes # in the kernel's rootfs, so the memory footprint is unchanged. mount -t tmpfs -o size=90%,mode=0755 tmpfs /newroot || fail "tmpfs mount failed" cd /newroot || fail "cannot enter /newroot" say "unpacking initramfs.cpio.gz" if ! gzip -dc /boot/initramfs.cpio.gz | cpio -idm 2>/dev/null; then fail "initramfs.cpio.gz failed to unpack" fi [ -x /newroot/init ] || fail "unpacked initramfs has no /init" cd / umount /boot 2>/dev/null # Built-in drivers probed while this stub was the whole root, so anything # that wanted firmware (Intel wifi, SOF audio DSP, Realtek wifi) failed at # ~0.7 s with "Direct firmware load failed" and gave up. Point the kernel's # firmware search path at the unpacked tree, re-probe those drivers, and # wait for the results so ac-native finds a sound card and a wlan the # moment it starts. The path entry is left in place: after switch_root it # is a dead directory the loader skips before its normal /lib/firmware. if [ -w /sys/module/firmware_class/parameters/path ]; then echo /newroot/lib/firmware > /sys/module/firmware_class/parameters/path want_wlan=0; want_snd=0 # 1. A driver whose probe failed outright (iwlwifi with no firmware) left # its device unbound, so there is nothing to unbind: ask the PCI bus to # probe every unbound wireless (class 0x0280xx) or audio (0x04xxxx) # device again now that the firmware tree is reachable. for dev in /sys/bus/pci/devices/*; do [ -e "$dev/driver" ] && continue class=$(cat "$dev/class" 2>/dev/null) case "$class" in 0x028*) want_wlan=1 ;; 0x04*) want_snd=1 ;; *) continue ;; esac addr=${dev##*/} echo "$addr" > /sys/bus/pci/drivers_probe 2>/dev/null say "asked the PCI bus to re-probe unbound $addr (class $class)" done # 2. SOF binds fine and only fails later in its firmware workqueue, so it # stays bound with a dead DSP: unbind and bind to rerun the probe. for drv in /sys/bus/pci/drivers/sof-audio-pci-intel-*; do [ -d "$drv" ] || continue for dev in "$drv"/0000:*; do [ -e "$dev" ] || continue addr=${dev##*/} want_snd=1 echo "$addr" > "$drv/unbind" 2>/dev/null echo "$addr" > "$drv/bind" 2>/dev/null say "re-bound $addr on ${drv##*/} with firmware from the stick" done done # Firmware loads finish asynchronously after bind; give them ~8 s. waited=0 while [ "$waited" -lt 40 ]; do ok=1 # Plain globs and tests: the stub's busybox has no reason to carry # more than it must, and a missing tool here would silently turn the # wait into a fixed 8 s (as it did on 2026-09-24 without grep). if [ "$want_wlan" = 1 ]; then have_wl=0; for n in /sys/class/net/wl*; do [ -e "$n" ] && have_wl=1; done [ "$have_wl" = 1 ] || ok=0 fi if [ "$want_snd" = 1 ]; then [ -d /proc/asound/card0 ] || ok=0; fi [ "$ok" = 1 ] && break waited=$((waited + 1)) sleep 0.2 done nets=""; for n in /sys/class/net/*; do [ -e "$n" ] && nets="$nets ${n##*/}"; done cards=""; for c in /proc/asound/card[0-9]*; do [ -d "$c" ] && cards="$cards ${c##*/}"; done say "firmware re-probe done after $((waited / 5)).$((waited % 5 * 2)) s (wlan wanted=$want_wlan, sound wanted=$want_snd): net=[$nets ] cards=[$cards ]" fi # Hand the live mounts to the new root, then pivot. The real /init mounts # proc/sys/dev itself and tolerates them already being there. mount -o move /dev /newroot/dev 2>/dev/null mount -o move /proc /newroot/proc 2>/dev/null mount -o move /sys /newroot/sys 2>/dev/null say "switching root" exec switch_root /newroot /init