From 61db7c877733db98255dae0c0388fd651a3e6354 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 24 Sep 2026 15:15:26 -0700 Subject: [PATCH] =?UTF-8?q?ac-os:=20boot=20sticks=20on=20stock=20Chromeboo?= =?UTF-8?q?ks=20=E2=80=94=20vboot=20kernel=20partition=20+=20embedded=20st?= =?UTF-8?q?ub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stock Chromebook firmware (developer mode, Ctrl+U) only boots a kernel packed by vbutil_kernel from a ChromeOS-kernel GPT partition and hands it no initrd. Every stick now carries that partition next to the UEFI layouts: - initramfs-stub/init: tiny embedded initramfs (static busybox + one script) that finds the stick, unpacks the real initramfs.cpio.gz into tmpfs and switch_roots into it. On UEFI boots the external initrd overlays it. - docker-build.sh: Step 3b builds the stub, sets CONFIG_INITRAMFS_SOURCE, and packs + verifies vmlinuz.kpart with the public devkeys after bzImage. Missing tooling fails the build; AC_SKIP_KPART=1 opts out loudly. - Dockerfile.builder: vboot-utils. - oven/native-builder.mjs + upload-release.sh: extract and publish native-notepat-latest.vmlinuz.kpart (required unless opted out). - ac-os pull: fetch the kpart when the release has one. - flash-mac.sh: third partition KERN-A (type 7f00, priority 10 / tries 5 / successful) between ACBOOT and ACEFI, dd the blob, read it back to verify. Releases without a kpart still flash two-partition UEFI sticks. - docs/chromebook-boot.md: layout, boot path, Chromebook-side steps. First target: Lenovo 500e Chromebook 2nd Gen (phaser360 / Octopus). --- fedac/native/Dockerfile.builder | 12 ++++ fedac/native/ac-os | 11 ++++ fedac/native/docker-build.sh | 80 ++++++++++++++++++++++++ fedac/native/docs/chromebook-boot.md | 73 ++++++++++++++++++++++ fedac/native/initramfs-stub/init | 74 +++++++++++++++++++++++ fedac/native/scripts/flash-mac.sh | 84 ++++++++++++++++++++++---- fedac/native/scripts/upload-release.sh | 10 +++ oven/native-builder.mjs | 11 +++- 8 files changed, 342 insertions(+), 13 deletions(-) create mode 100644 fedac/native/docs/chromebook-boot.md create mode 100755 fedac/native/initramfs-stub/init diff --git a/fedac/native/Dockerfile.builder b/fedac/native/Dockerfile.builder index b52cbaeadc..a205ca2770 100644 --- a/fedac/native/Dockerfile.builder +++ b/fedac/native/Dockerfile.builder @@ -99,6 +99,18 @@ RUN curl -fsSL https://claude.ai/install.sh | bash 2>/dev/null \ || echo "Claude Code install skipped (non-fatal)" # ── Verify tools ── +# ── vboot-utils: vbutil_kernel + public devkeys for the Chromebook kernel +# partition (docker-build.sh packs vmlinuz.kpart; stock Chromebook firmware +# boots it in developer mode via Ctrl+U). ── +RUN dnf install -y --setopt=install_weak_deps=False vboot-utils \ + && dnf clean all && rm -rf /var/cache/dnf \ + && command -v vbutil_kernel \ + && { find /usr/share -path '*devkeys/kernel.keyblock' | grep -q . \ + || { mkdir -p /usr/share/vboot/devkeys \ + && curl -fsSL "https://chromium.googlesource.com/chromiumos/platform/vboot_reference/+archive/HEAD/tests/devkeys.tar.gz" \ + | tar -xz -C /usr/share/vboot/devkeys \ + && test -f /usr/share/vboot/devkeys/kernel.keyblock; }; } + RUN gcc --version | head -1 && busybox --help >/dev/null 2>&1 && esbuild --version && echo "OK" # ── Copy source into image ── diff --git a/fedac/native/ac-os b/fedac/native/ac-os index e93406f46e..a05cab86fa 100755 --- a/fedac/native/ac-os +++ b/fedac/native/ac-os @@ -1768,6 +1768,17 @@ pull_ota() { local INIT_REMOTE_SIZE=$(curl -sfI "${INITRAMFS_URL}" | awk -F'[: ]+' '/^[Cc]ontent-[Ll]ength:/ {print $2+0}' | tr -d '\r') fetch_if_stale "${SLIM_URL}" "${PULLED_SLIM}" "slim kernel" fetch_if_stale "${INITRAMFS_URL}" "${PULLED_INITRAMFS}" "initramfs" + # Chromebook kernel partition image (vboot-packed). Older releases + # never published one; flash-mac.sh then writes a two-partition stick + # that boots UEFI machines only. + local KPART_URL="${CDN_BASE}/native-notepat-latest.vmlinuz.kpart" + local PULLED_KPART="${PULL_DIR}/vmlinuz.kpart" + if curl -sf --head "${KPART_URL}" >/dev/null 2>&1; then + fetch_if_stale "${KPART_URL}" "${PULLED_KPART}" "Chromebook kpart" + else + rm -f "${PULLED_KPART}" "${PULLED_KPART}.etag" + log "No Chromebook kpart published for this release — stick will boot UEFI machines only" + fi # A dropped transfer above leaves a truncated file (the || true # swallows curl's error) — flashing it makes an unbootable stick. # Compare against the remote Content-Length before proceeding. diff --git a/fedac/native/docker-build.sh b/fedac/native/docker-build.sh index 0ea7d3373b..6c11555c97 100644 --- a/fedac/native/docker-build.sh +++ b/fedac/native/docker-build.sh @@ -808,6 +808,40 @@ find . -print0 | cpio --null -o --format=newc 2>/dev/null \ INITRAMFS_SIZE=$(stat -c%s "$BUILD/initramfs.cpio.gz") log " Initramfs: $((INITRAMFS_SIZE / 1048576))MB compressed (${GZIP_CMD%% *})" +# ══════════════════════════════════════════════ +# Step 3b: Chromebook boot stub (embedded initramfs) +# ══════════════════════════════════════════════ +# Stock Chromebook firmware (developer mode, Ctrl+U) boots a vboot-packed +# kernel from a ChromeOS kernel partition and passes it NO initrd. So the +# kernel carries a tiny embedded initramfs — static busybox + one script — +# that finds the stick's ACBOOT partition, unpacks the real initramfs.cpio.gz +# into a tmpfs and switch_roots into it. On UEFI boots the external initrd +# overlays this one and its /init is never reached. See docs/chromebook-boot.md. +log "Step 3b: Building Chromebook boot stub..." +STUB_ROOT="$BUILD/stub-root" +STUB_CPIO="$BUILD/stub-initramfs.cpio" +rm -rf "$STUB_ROOT" +mkdir -p "$STUB_ROOT"/{bin,sbin,dev,proc,sys,boot,newroot} +if ! file "$BUSYBOX" | grep -q "statically linked"; then + err "busybox at $BUSYBOX is not statically linked — the Chromebook stub cannot use it" + exit 1 +fi +for applet in sh mount umount gzip cpio switch_root sleep; do + "$BUSYBOX" --list 2>/dev/null | grep -qx "$applet" \ + || { err "busybox lacks the '$applet' applet the Chromebook stub needs"; exit 1; } +done +cp "$BUSYBOX" "$STUB_ROOT/bin/busybox" +for cmd in sh mount umount gzip cpio sleep mkdir cat echo ls; do + ln -sf busybox "$STUB_ROOT/bin/$cmd" +done +ln -sf ../bin/busybox "$STUB_ROOT/sbin/switch_root" +cp "$NATIVE/initramfs-stub/init" "$STUB_ROOT/init" +chmod 755 "$STUB_ROOT/init" +sh -n "$STUB_ROOT/init" || { err "initramfs-stub/init has a syntax error"; exit 1; } +(cd "$STUB_ROOT" && find . -print0 | cpio --null -o -H newc --quiet > "$STUB_CPIO") +[ -s "$STUB_CPIO" ] || { err "stub initramfs cpio is empty"; exit 1; } +log " Stub: $(($(stat -c%s "$STUB_CPIO") / 1024))KB (busybox + init), embedded via CONFIG_INITRAMFS_SOURCE" + # ══════════════════════════════════════════════ # Step 4: Build kernel with embedded initramfs # ══════════════════════════════════════════════ @@ -975,7 +1009,13 @@ for sym in \ ; do scripts/config --enable "CONFIG_$sym" 2>/dev/null || true done +# Embed the Chromebook boot stub (Step 3b). Path is build-specific, so it is +# set here rather than in config-minimal. kbuild re-packs usr/initramfs_data +# whenever the cpio changes. +scripts/config --set-str INITRAMFS_SOURCE "$STUB_CPIO" make olddefconfig >>"$KCFG_LOG" 2>&1 || { err "Kernel olddefconfig (post-config) failed"; tail -120 "$KCFG_LOG" >&2; exit 1; } +grep -q "^CONFIG_INITRAMFS_SOURCE=\"$STUB_CPIO\"" .config \ + || { err "CONFIG_INITRAMFS_SOURCE did not stick (stub would be missing from vmlinuz)"; exit 1; } tail -1 "$KCFG_LOG" || true # Verify the critical audio/GPIO symbols actually stuck after olddefconfig @@ -1070,6 +1110,44 @@ cp arch/x86/boot/bzImage "$OUT/vmlinuz" 2>/dev/null || true cp arch/x86/boot/bzImage "$BUILD/vmlinuz-slim" cp arch/x86/boot/bzImage "$OUT/vmlinuz-slim" 2>/dev/null || true +# ── Chromebook kernel partition image ── +# vbutil_kernel wraps the bzImage in a vboot keyblock + preamble signed with +# the public developer keys. Stock Chromebook firmware accepts those in +# developer mode (Ctrl+U), which is how the same stick boots a Chromebook +# without touching its firmware. flash-mac.sh dd's this into a ChromeOS +# kernel-type GPT partition. The kernel's built-in CONFIG_CMDLINE supplies +# the real command line (x86 appends the bootloader's to it); the config +# passed here is just a marker so /proc/cmdline shows which path booted. +if [ "${AC_SKIP_KPART:-0}" = "1" ]; then + log " Chromebook kpart: SKIPPED (AC_SKIP_KPART=1) — this release will not boot stock Chromebooks" +else + VBUTIL=$(command -v vbutil_kernel || true) + DEVKEYS=$(dirname "$(find /usr/share /usr/local/share -path '*devkeys/kernel.keyblock' 2>/dev/null | head -1)" 2>/dev/null || true) + if [ -z "$VBUTIL" ] || [ ! -f "$DEVKEYS/kernel.keyblock" ] || [ ! -f "$DEVKEYS/kernel_data_key.vbprivk" ]; then + err "vbutil_kernel or the vboot devkeys are missing (Dockerfile.builder installs vboot-utils)." + err " Set AC_SKIP_KPART=1 to build without Chromebook support." + exit 1 + fi + KPART="$BUILD/vmlinuz.kpart" + printf 'ac.boot=chromeos' > "$BUILD/kpart-cmdline.txt" + # x86 depthcharge ignores the bootloader blob but vbutil_kernel insists on one. + dd if=/dev/zero of="$BUILD/kpart-bootstub.bin" bs=512 count=1 status=none + "$VBUTIL" --pack "$KPART" \ + --keyblock "$DEVKEYS/kernel.keyblock" \ + --signprivate "$DEVKEYS/kernel_data_key.vbprivk" \ + --version 1 \ + --vmlinuz arch/x86/boot/bzImage \ + --config "$BUILD/kpart-cmdline.txt" \ + --bootloader "$BUILD/kpart-bootstub.bin" \ + --arch x86 || { err "vbutil_kernel --pack failed"; exit 1; } + "$VBUTIL" --verify "$KPART" --signpubkey "$DEVKEYS/kernel_subkey.vbpubk" >/dev/null 2>&1 \ + || { err "vbutil_kernel --verify rejected the packed kernel"; exit 1; } + cp "$KPART" "$OUT/vmlinuz.kpart" 2>/dev/null || true + KPART_SIZE=$(stat -c%s "$KPART") + KPART_SHA=$(sha256sum "$KPART" | awk '{print $1}') + log " vmlinuz.kpart: $((KPART_SIZE / 1048576))MB (vboot devkeys, sha256: ${KPART_SHA:0:16}...)" +fi + # Post-build verification: confirm critical driver objects actually got # compiled. The .config saying =y isn't enough — kbuild's persistent # build state could skip re-compiling a driver whose toggle changed. @@ -1163,6 +1241,8 @@ cat > "$OUT/build.json" << EOF "sha256": "$SHA", "iso_size": ${ISO_SIZE:-0}, "iso_sha256": "${ISO_SHA:-}", + "kpart_size": ${KPART_SIZE:-0}, + "kpart_sha256": "${KPART_SHA:-}", "handle": "$HANDLE" } EOF diff --git a/fedac/native/docs/chromebook-boot.md b/fedac/native/docs/chromebook-boot.md new file mode 100644 index 0000000000..e085a98d14 --- /dev/null +++ b/fedac/native/docs/chromebook-boot.md @@ -0,0 +1,73 @@ +# Booting AC OS sticks on stock Chromebooks + +Stock Chromebook firmware never boots a UEFI stick. In developer mode, Ctrl+U +looks for a GPT partition of ChromeOS-kernel type holding a kernel packed and +signed by `vbutil_kernel`; the public developer keys are accepted, so no +firmware change (MrChromebox RW_LEGACY or full ROM) is needed. Every AC OS +stick now carries that partition alongside the UEFI layouts, and the same +`vmlinuz` serves all three. + +## Layout + +| partition | type | contents | +|-----------|-------------------|--------------------------------------------| +| 1 ACBOOT | Basic data (FAT32)| BOOTX64.EFI (kernel-direct), initramfs, config.json, wifi_creds.json | +| 3 KERN-A | ChromeOS kernel | `vmlinuz.kpart` — vboot-packed bzImage, 64 MB partition | +| 2 ACEFI | EFI System | splash → systemd-boot → KERNEL.EFI + initramfs | + +UEFI firmware picks ACEFI. Depthcharge (Chromebook firmware) picks KERN-A. +Partition 3 sits between the two on disk; the number is what macOS and vboot +care about. GPT attribute bits follow cgpt semantics: priority 10, tries 5, +successful 1 — the flags recovery media use, so the firmware never counts +the stick down to unbootable. + +## How the Chromebook path reaches the real initramfs + +Depthcharge passes the kernel no initrd. The kernel therefore embeds a stub +initramfs (`initramfs-stub/init` + static busybox, wired in by +`docker-build.sh` Step 3b through `CONFIG_INITRAMFS_SOURCE`). The stub waits +for the stick, mounts the first FAT partition carrying `initramfs.cpio.gz` +and `config.json`, unpacks the real initramfs into a tmpfs and +`switch_root`s into it. From there boot is identical to the UEFI path, +including the inscription baked into `initramfs.cpio.gz` at flash time. + +On UEFI boots the firmware-supplied initrd is unpacked over the embedded +one, so its `/init` replaces the stub and the stub never runs. +`/proc/cmdline` contains `ac.boot=chromeos` only on the depthcharge path; +the kernel's built-in `CONFIG_CMDLINE` supplies everything else (x86 appends +the bootloader's command line to the built-in one). + +## Pipeline + +- `Dockerfile.builder` installs `vboot-utils` (Fedora) for `vbutil_kernel` + and `/usr/share/vboot/devkeys`. +- `docker-build.sh` builds the stub cpio, sets `CONFIG_INITRAMFS_SOURCE`, + and after `bzImage` packs + verifies `vmlinuz.kpart`. A missing tool fails + the build; `AC_SKIP_KPART=1` opts out loudly. +- `oven/native-builder.mjs` extracts `vmlinuz.kpart` (required unless + `AC_SKIP_KPART=1`) and `scripts/upload-release.sh` publishes it as + `native-notepat-latest.vmlinuz.kpart`. +- `ac-os pull` fetches it when the release has one; `scripts/flash-mac.sh` + adds KERN-A when `vmlinuz.kpart` is in the source dir, dd's the blob, and + verifies it by reading the partition back. Releases without a kpart still + flash as two-partition UEFI sticks. + +Not yet covered: the Linux `ac-os flash` path and the ISO (`media-layout.sh`). + +## On the Chromebook + +1. Developer mode (Esc+Refresh+Power, then Ctrl+D at the recovery screen; + this wipes local ChromeOS data once). +2. Sign in, open a terminal (Ctrl+Alt+T → `shell`, or VT2) and run + `sudo crossystem dev_boot_usb=1`. +3. Reboot with the stick in. At the "OS verification is OFF" screen press + Ctrl+U. + +If the stick is ignored, the firmware is not seeing a kernel partition it +accepts: check `cgpt show /dev/sdX` from the ChromeOS shell for a KERN-A +entry with type `ChromeOS kernel` and the priority/tries/successful bits. +The stub logs to the console and kmsg as `[ac-stub]`; if it cannot find the +stick it drops to a shell instead of hanging. + +First verified target: Lenovo 500e Chromebook 2nd Gen (board `phaser360`, +Octopus / Gemini Lake), 2026-09-24 lane. diff --git a/fedac/native/initramfs-stub/init b/fedac/native/initramfs-stub/init new file mode 100755 index 0000000000..1e78dac0c4 --- /dev/null +++ b/fedac/native/initramfs-stub/init @@ -0,0 +1,74 @@ +#!/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 + +# 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 diff --git a/fedac/native/scripts/flash-mac.sh b/fedac/native/scripts/flash-mac.sh index 2d31ece438..5d39e6552b 100755 --- a/fedac/native/scripts/flash-mac.sh +++ b/fedac/native/scripts/flash-mac.sh @@ -85,6 +85,11 @@ fi KERNEL="${SRC_DIR}/vmlinuz" INITRAMFS="${SRC_DIR}/initramfs.cpio.gz" +# Optional: vboot-packed kernel for stock Chromebook firmware (dev mode, +# Ctrl+U). When present a third GPT partition of ChromeOS-kernel type is +# added; UEFI firmware ignores it. See docs/chromebook-boot.md. +KPART="${SRC_DIR}/vmlinuz.kpart" +KPART_MB=64 SPLASH_EFI="${NATIVE_DIR}/bootloader/splash.efi" SDBOOT_EFI="${NATIVE_DIR}/boot/systemd-bootx64.efi" @@ -379,7 +384,15 @@ STAGE_MB=$(( $(mb_round_up "${KERNEL_BYTES}") + $(mb_round_up "${INITRD_BYTES}") # space". 2× the staged size + 256 MB gives the OTA room to double-buffer. # ACBOOT (MAIN) absorbs the difference out of its ~29 GB — negligible. EFI_MB=$(( STAGE_MB * 2 + 256 )) -MAIN_MB=$(( DISK_MB - EFI_MB - 64 )) # 64 MB GPT + alignment headroom +if [ -f "${KPART}" ]; then + KPART_BYTES=$(stat -f%z "${KPART}") + KPART_SHA=$(shasum -a 256 "${KPART}" | awk '{print $1}') + [ "${KPART_BYTES}" -le $(( KPART_MB * 1048576 )) ] \ + || die "vmlinuz.kpart (${KPART_BYTES} bytes) exceeds the ${KPART_MB} MB kernel partition." +else + KPART_MB=0 +fi +MAIN_MB=$(( DISK_MB - EFI_MB - KPART_MB - 64 )) # 64 MB GPT + alignment headroom [ "${MAIN_MB}" -ge $(( STAGE_MB + 64 )) ] \ || die "USB too small (${DISK_MB} MB) for hybrid layout." @@ -388,8 +401,13 @@ echo log "Target: ${USB_DEV} — ${DEV_NAME} — ${DEV_SIZE}" log "Kernel: ${KERNEL_BYTES} bytes ${KERNEL_SHA:0:16}…" log "Initramfs: ${INITRD_BYTES} bytes ${INITRD_SHA:0:16}…" -FREE_MB=$(( DISK_MB - MAIN_MB - EFI_MB - 64 )) -log "Layout: ACBOOT=${MAIN_MB}MB ACEFI=${EFI_MB}MB free=${FREE_MB}MB" +FREE_MB=$(( DISK_MB - MAIN_MB - KPART_MB - EFI_MB - 64 )) +if [ "${KPART_MB}" -gt 0 ]; then + log "Chromebook: ${KPART_BYTES} bytes ${KPART_SHA:0:16}… (KERN-A partition, ${KPART_MB}MB)" + log "Layout: ACBOOT=${MAIN_MB}MB KERN-A=${KPART_MB}MB ACEFI=${EFI_MB}MB free=${FREE_MB}MB" +else + log "Layout: ACBOOT=${MAIN_MB}MB ACEFI=${EFI_MB}MB free=${FREE_MB}MB (no Chromebook kpart in ${SRC_DIR})" +fi echo if [ -n "${AC_FLASH_YES:-}" ] || [ ! -t 0 ]; then log "Auto-confirming wipe (AC_FLASH_YES set or stdin not a TTY)" @@ -407,11 +425,29 @@ log "Zapping GPT + clearing first 16 MiB…" sgdisk --zap-all "${USB_DEV}" >/dev/null dd if=/dev/zero of="${USB_DEV}" bs=1m count=16 status=none -log "Creating GPT layout (ACBOOT + ACEFI)…" -sgdisk \ - --new=1:0:+${MAIN_MB}M --typecode=1:0700 --change-name=1:ACBOOT \ - --new=2:0:0 --typecode=2:ef00 --change-name=2:ACEFI \ - "${USB_DEV}" >/dev/null +if [ "${KPART_MB}" -gt 0 ]; then + # Partition 3 sits between ACBOOT and ACEFI on disk; numbering is what + # matters to macOS (disk4s3) and vboot scans every kernel-type partition. + # Type 7f00 = ChromeOS kernel. Attribute bits (cgpt semantics): + # 48-51 priority = 10 (bits 49,51), 52-55 tries = 5 (bits 52,54), + # 56 successful = 1 — the same flags chrx/ChromeOS recovery media use, so + # the firmware never counts the stick down to unbootable. + log "Creating GPT layout (ACBOOT + KERN-A + ACEFI)…" + sgdisk \ + --new=1:0:+${MAIN_MB}M --typecode=1:0700 --change-name=1:ACBOOT \ + --new=3:0:+${KPART_MB}M --typecode=3:7f00 --change-name=3:KERN-A \ + --attributes=3:set:49 --attributes=3:set:51 \ + --attributes=3:set:52 --attributes=3:set:54 \ + --attributes=3:set:56 \ + --new=2:0:0 --typecode=2:ef00 --change-name=2:ACEFI \ + "${USB_DEV}" >/dev/null +else + log "Creating GPT layout (ACBOOT + ACEFI)…" + sgdisk \ + --new=1:0:+${MAIN_MB}M --typecode=1:0700 --change-name=1:ACBOOT \ + --new=2:0:0 --typecode=2:ef00 --change-name=2:ACEFI \ + "${USB_DEV}" >/dev/null +fi # Force macOS to re-read the partition table after sgdisk wrote it. The # kernel caches the old layout until we explicitly notify it; without this, @@ -422,17 +458,29 @@ diskutil list "${USB_DEV}" >/dev/null 2>&1 || true P1="${USB_DEV}s1" P2="${USB_DEV}s2" +P3="${USB_DEV}s3" RAW1="/dev/r$(basename "${P1}")" RAW2="/dev/r$(basename "${P2}")" +RAW3="/dev/r$(basename "${P3}")" -# Wait up to 10s for both partition nodes to materialize. +# Wait up to 10s for the partition nodes to materialize. for i in $(seq 1 20); do - [ -e "${P1}" ] && [ -e "${P2}" ] && break + if [ -e "${P1}" ] && [ -e "${P2}" ] && { [ "${KPART_MB}" -eq 0 ] || [ -e "${P3}" ]; }; then break; fi sleep 0.5 diskutil list "${USB_DEV}" >/dev/null 2>&1 || true done [ -e "${P1}" ] || die "Partition ${P1} did not appear after sgdisk + reread." [ -e "${P2}" ] || die "Partition ${P2} did not appear after sgdisk + reread." +if [ "${KPART_MB}" -gt 0 ]; then + [ -e "${P3}" ] || die "Partition ${P3} did not appear after sgdisk + reread." + log "Writing KERN-A (vboot-packed kernel for Chromebook Ctrl+U boot)…" + diskutil unmount "${P3}" >/dev/null 2>&1 || true + # conv=sync pads the final block to bs so the raw device accepts it; + # the padding lands inside the 64 MB partition, past the kernel blob. + dd if="${KPART}" of="${RAW3}" bs=1m conv=sync status=none \ + || die "dd of vmlinuz.kpart to ${RAW3} failed" + sync +fi log "Formatting FAT32 partitions…" newfs_msdos -F 32 -v ACBOOT "${RAW1}" >/dev/null @@ -573,6 +621,13 @@ verify "ACBOOT/EFI/BOOT/BOOTX64.EFI" "${M1}/EFI/BOOT/BOOTX64.EFI" "${KERNEL_SHA verify "ACBOOT/initramfs.cpio.gz" "${M1}/initramfs.cpio.gz" "${INITRD_SHA}" verify "ACEFI/EFI/BOOT/KERNEL.EFI" "${M2}/EFI/BOOT/KERNEL.EFI" "${KERNEL_SHA}" verify "ACEFI/initramfs.cpio.gz" "${M2}/initramfs.cpio.gz" "${INITRD_SHA}" +if [ "${KPART_MB}" -gt 0 ]; then + # Read the blob's exact length back through the buffered node (the raw + # node only allows sector-multiple reads) and compare with the source. + got=$(head -c "${KPART_BYTES}" "${P3}" | shasum -a 256 | awk '{print $1}') + [ "${got}" = "${KPART_SHA}" ] || die "KERN-A sha mismatch (${got} != ${KPART_SHA})" + log " ✓ KERN-A (ChromeOS kernel partition) ${got:0:16}…" +fi # --- finalize --- sync @@ -582,5 +637,10 @@ diskutil eject "${USB_DEV}" >/dev/null trap - EXIT rmdir "${M1}" "${M2}" 2>/dev/null || true -log "Done. USB has both kernel-direct (ACBOOT) + systemd-boot (ACEFI) layouts." -log "Plug into target hardware and boot — UEFI firmware should pick ACEFI (real ESP)." +if [ "${KPART_MB}" -gt 0 ]; then + log "Done. USB has kernel-direct (ACBOOT) + systemd-boot (ACEFI) + ChromeOS kernel (KERN-A) layouts." + log "UEFI firmware picks ACEFI. Stock Chromebooks: developer mode, 'crossystem dev_boot_usb=1', then Ctrl+U at the boot screen." +else + log "Done. USB has both kernel-direct (ACBOOT) + systemd-boot (ACEFI) layouts." + log "Plug into target hardware and boot — UEFI firmware should pick ACEFI (real ESP)." +fi diff --git a/fedac/native/scripts/upload-release.sh b/fedac/native/scripts/upload-release.sh index a6b1389b88..4532ceddbb 100755 --- a/fedac/native/scripts/upload-release.sh +++ b/fedac/native/scripts/upload-release.sh @@ -241,6 +241,13 @@ if [ -f "$INITRAMFS_SIBLING" ]; then do_upload "$INITRAMFS_SIBLING" "os/${CHANNEL_PREFIX}native-notepat-latest.initramfs.cpio.gz" "application/octet-stream" fi +# Chromebook kernel partition image (vboot-packed vmlinuz; see docs/chromebook-boot.md) +KPART_SIBLING="$(dirname "$VMLINUZ")/vmlinuz.kpart" +if [ -f "$KPART_SIBLING" ]; then + echo " Uploading Chromebook kpart ($(du -sh "$KPART_SIBLING" | cut -f1))..." + do_upload "$KPART_SIBLING" "os/${CHANNEL_PREFIX}native-notepat-latest.vmlinuz.kpart" "application/octet-stream" +fi + # Also upload a template disk image if it exists (non-fatal) IMAGE_SIBLING="$(dirname "$VMLINUZ")/ac-os.img" if [ -f "$IMAGE_SIBLING" ]; then @@ -255,6 +262,9 @@ if [ -f "$SLIM_SIBLING" ]; then echo " ${BASE_URL}/os/${CHANNEL_PREFIX}native-notepat-latest.vmlinuz-slim" echo " ${BASE_URL}/os/${CHANNEL_PREFIX}native-notepat-latest.initramfs.cpio.gz" fi +if [ -f "$KPART_SIBLING" ]; then + echo " ${BASE_URL}/os/${CHANNEL_PREFIX}native-notepat-latest.vmlinuz.kpart" +fi echo " ${BASE_URL}/os/${CHANNEL_PREFIX}releases.json" if [ -f "$IMAGE_SIBLING" ]; then echo " ${BASE_URL}/os/${CHANNEL_PREFIX}native-notepat-latest.img" diff --git a/oven/native-builder.mjs b/oven/native-builder.mjs index 68aae4ac3c..3db19ba09e 100644 --- a/oven/native-builder.mjs +++ b/oven/native-builder.mjs @@ -509,6 +509,7 @@ async function runBuildJob(job) { const isoOut = `/tmp/oven-iso-${job.id}`; const slimOut = `/tmp/oven-vmlinuz-slim-${job.id}`; const initramfsOut = `/tmp/oven-initramfs-${job.id}`; + const kpartOut = `/tmp/oven-kpart-${job.id}`; // The initramfs and slim kernel are NOT optional, and their extraction // must not be allowed to fail quietly. On 2026-08-11 a build compiled // the right commit, logged the right stamps, exited 0 — and published @@ -524,7 +525,11 @@ async function runBuildJob(job) { `docker cp ${cid}:/tmp/ac-build/ac-os.iso ${isoOut} 2>/dev/null || docker cp ${cid}:/out/ac-os.iso ${isoOut} 2>/dev/null || true`, `docker cp ${cid}:/tmp/ac-build/vmlinuz-slim ${slimOut} 2>/dev/null || docker cp ${cid}:/out/vmlinuz-slim ${slimOut}`, `docker cp ${cid}:/tmp/ac-build/initramfs.cpio.gz ${initramfsOut} 2>/dev/null || docker cp ${cid}:/out/initramfs.cpio.gz ${initramfsOut}`, - `ls -lh ${slimOut} ${initramfsOut}`, + // Chromebook kernel partition image. Required unless the build opted + // out with AC_SKIP_KPART=1, for the same reason as the initramfs: a + // missing artifact must fail here, not publish a stale sibling. + `docker cp ${cid}:/tmp/ac-build/vmlinuz.kpart ${kpartOut} 2>/dev/null || docker cp ${cid}:/out/vmlinuz.kpart ${kpartOut} || [ "${process.env.AC_SKIP_KPART || ""}" = "1" ]`, + `ls -lh ${slimOut} ${initramfsOut} ${kpartOut} 2>/dev/null || ls -lh ${slimOut} ${initramfsOut}`, `docker rm ${cid} >/dev/null`, ].join("\n")], repoDir); @@ -556,6 +561,7 @@ async function runBuildJob(job) { const isoUpload = `${uploadDir}/ac-os.iso`; const slimUpload = `${uploadDir}/vmlinuz-slim`; const initramfsUpload = `${uploadDir}/initramfs.cpio.gz`; + const kpartUpload = `${uploadDir}/vmlinuz.kpart`; await fs.mkdir(uploadDir, { recursive: true }); await fs.rename(vmlinuzOut, vmlinuzUpload); try { await fs.rename(isoOut, isoUpload); } catch {} @@ -564,6 +570,9 @@ async function runBuildJob(job) { // to be published in this build's place. Fail the job instead. await fs.rename(slimOut, slimUpload); await fs.rename(initramfsOut, initramfsUpload); + try { await fs.rename(kpartOut, kpartUpload); } catch (e) { + if (process.env.AC_SKIP_KPART !== "1") throw e; + } // upload-release.sh auto-detects sibling files (vmlinuz-slim, initramfs.cpio.gz, ac-os.iso) await runPhase(job, "upload", "bash", [uploadScript, vmlinuzUpload], NATIVE_DIR, uploadEnv); -- 2.51.2