Files
hp-chell/CHROMEOS-FLEX.md
T
2026-09-27 12:43:40 -04:00

12 KiB

ChromeOS Flex on hp-chell's internal eMMC

hp-chell (HP Chromebook 13 G1, MrChromebox UEFI firmware) dual-boots:

OS Disk Notes
KDE neon sda, external USB/SD card (117GB) primary OS, owns GRUB
ChromeOS Flex mmcblk0, internal eMMC (29GB) installed from neon, see below

Because neon lives entirely on external media, the eMMC can be wiped and reinstalled from the running neon system with no risk to neon.

Installing / reinstalling

sudo ./install-flex-emmc.sh ~/Downloads/chromeos_<ver>_reven_recovery_stable-channel_<keys>.bin.zip
  • Accepts the .bin.zip straight from Google's Flex download or an already-extracted .bin (zips are extracted to /var/tmp/flex-install).
  • Destination defaults to /dev/mmcblk0; pass a second argument to override. The destination is wiped entirely.
  • Refuses to run if the destination holds the running root filesystem, shows the target's layout, and asks you to type yes.
  • Takes about 2.5 minutes. Full installer log: /var/log/flex-install.log. UEFI boot entries from before the run are saved to /var/log/flex-install-efiboot-before.txt and the current ones are printed at the end.
  • Updates the GRUB chainload entry (below) with the new EFI partition's UUID and runs update-grub.

Last installed: 2026-09-27, Flex 16640.57.0 (stable, reven).

Why not just dd the image

The Flex recovery .bin is a full GPT disk image, but it's the installer layout, not an installed system:

Partition Installer image (raw dd) Proper install
ROOT-A 2.4GB 4GB
ROOT-B 2MB placeholder 4GB (A/B updates)
STATE fixed 6GB rest of the disk (~20.6GB)
unallocated ~20.7GB none

Flex decides whether it's running from installer media with /usr/sbin/is_running_from_installer, which compares the ROOT-A and ROOT-B sizes. A raw dd therefore boots as live media ("Install ChromeOS Flex / Try it first"), can't auto-update, and never grows STATE. There's no flag to set, and resizing in place isn't practical (ROOT-B sits in a 2MB gap between the ESP and ROOT-A). This was the state from 2026-09-04 (raw dd) until the 2026-09-27 reinstall.

The fix is to run the image's own chromeos-install, which is what the "Install ChromeOS Flex" button runs, against the eMMC.

How the script works

  1. Adds a temporary udev rule (/run/udev/rules.d/99-flex-install-ignore.rules, UDISKS_IGNORE=1) so the Plasma desktop doesn't auto-mount eMMC partitions mid-install, then unmounts anything already mounted there.
  2. Attaches the .bin read-only as a partitioned loop device.
  3. In a private mount namespace (unshare -m; every mount is discarded on exit), mounts the image's ROOT-A read-only and builds a chroot: tmpfs /tmp /run /var /mnt/stateful_partition, bind /dev /sys, fresh /proc.
  4. Masks every disk except the destination: each /dev/sd*, /dev/nvme*, other /dev/mmcblk*, and /dev/vd* node gets /dev/null bind-mounted over it, and /dev/disk and /dev/block are hidden. Inside the chroot, reading /dev/sda returns 0 bytes, so neon is unreachable.
  5. Runs:
    chromeos-install --dst /dev/mmcblk0 --payload_image /tmp/payload.bin \
        --skip_dst_removable --yes --target_bios efi --debug
    
  6. Checks the log for success, re-reads the partition table, and updates GRUB. Cleanup (loop device, udev rule) runs on exit, including on failure.

Gotchas when running chromeos-install from another Linux

Each of these broke an attempt during the 2026-09-27 install:

  • No recognized cros_XXX bios option on kernel command line: chromeos_postinst reads /proc/cmdline for cros_efi (or cros_legacy etc.), even though it's passed --bios efi. The script bind-mounts a copy of the command line with cros_efi appended over /proc/cmdline. chromeos-postinst then re-chroots into the freshly written root with a non-recursive mount --bind /proc, which drops that overmount. So the image's chroot (a coreutils shim at /bin/chroot) is also overmounted with a wrapper that re-applies the fake cmdline inside the target root before exec'ing the real coreutils --coreutils-prog=chroot.
  • sfdisk: This disk is currently in use: udisks auto-mounted the new STATE partition (/media/matt/H-STATE) from a previous attempt. The udev rule in step 1 above handles this.
  • Exit status can't be trusted: chromeos-install exited 0 while its inner chromeos-install.sh had failed. It also logs harmless ERROR cros_installer lines on success (metrics /var/lib/metrics/uma-events, "Couldn't check the current permission"). The script treats the run as successful only if the log has Installation to '/dev/mmcblk0' complete. and no final Error: line.
  • Touch(/mnt/stateful_partition/.install_completed) failed: postinst marks the installer's STATE as done. In the chroot that's the read-only image, so the script gives it a throwaway tmpfs.
  • Harmless noise: initctl (no Upstart), Failed to find physical volume, BLKRRPART: Device or resource busy, and ChromeOS ACPI GetAcpiSysfsPath errors (not stock ChromeOS firmware).
  • nohup … & over SSH got killed when the session closed, so long runs go through systemd-run --unit=... -p StandardOutput=file:/var/log/.... The log must not be in /tmp if another user owns the file (root is denied by fs.protected_regular).

Resulting layout (2026-09-27)

mmcblk0      29.1G
├─mmcblk0p1  20.6G ext4   STATE
├─mmcblk0p2    64M        KERN-A    (priority=1 successful=1)
├─mmcblk0p3     4G ext4   ROOT-A
├─mmcblk0p4    64M        KERN-B
├─mmcblk0p5     4G ext4   ROOT-B
├─mmcblk0p6-7 512B        KERN-C / ROOT-C
├─mmcblk0p8    16M ext4   OEM
├─mmcblk0p9-11 512B       reserved / reserved / RWFW
└─mmcblk0p12  256M vfat   EFI-SYSTEM (UUID CBE4-703E)

The ESP is copied from the image, so its FAT UUID (CBE4-703E) survived the reinstall unchanged. The install itself left the UEFI boot entries and BootOrder untouched, but Flex's first auto-update changed them. See "Flex takes over the UEFI boot order on updates" below.

Booting Flex

Two ways:

  1. From GRUB (neon's menu, shown for 5s): the entry "ChromeOS Flex (internal eMMC)" in /etc/grub.d/40_custom chainloads Flex's own EFI loader (crdyboot) from the eMMC's ESP:
    menuentry "ChromeOS Flex (internal eMMC)" {
        insmod part_gpt
        insmod fat
        insmod chain
        search --no-floppy --fs-uuid --set=root CBE4-703E
        chainloader /EFI/BOOT/BOOTX64.EFI
    }
    
    Run sudo update-grub after editing 40_custom. The install script rewrites the --fs-uuid value automatically.
  2. From the firmware boot picker (MrChromebox UEFI, Esc during POST): efibootmgr -v shows neon (Boot0005, on the USB card) and Internal eMMC (Boot0001), with BootOrder 0005,0001,0000,0003,0002. With the USB card unplugged, the firmware falls through to Internal eMMC and boots Flex with no menu.

Flex takes over the UEFI boot order on updates

Seen 2026-09-27: after the install, Flex booted fine, found an update, installed it, and rebooted. The next boot went straight to Flex, and getting back to neon took the firmware setup. efibootmgr afterwards:

BootOrder: 0004,0005,0001,0000,0003,0002      (was 0005,0001,0000,0003,0002)
Boot0004* ChromeOS Flex  HD(12,GPT,2877e60b-...)/File(\efi\boot\bootx64.efi)   <- new
Boot0005* neon           HD(1,GPT,34facff5-...)/File(\EFI\neon\shimx64.efi)

What does it: cros_installer's post-install (platform2/installer/efi_boot_management.cc, strings "Adding EFI Boot entry." / "Creating new BootOrder."). It makes sure a "ChromeOS Flex" entry for the eMMC's ESP exists and puts it first in BootOrder. Nothing else in the rootfs writes BootOrder (only cros_installer, efibootmgr and fwupd's EFI binary mention it), and no boot-time upstart job runs cros_installer. It runs:

  • at install time, but not under install-flex-emmc.sh: the chroot binds /sys non-recursively, so efivarfs isn't visible and it logs EFI runtime services not [available] and skips EFI management. That's why the entry list printed straight after the install was unchanged.
  • after every auto-update, when update_engine runs post-install on the running Flex system with real EFI access. That's what happened here: the eMMC afterwards had ROOT-A 16640.57.0 (the install) and ROOT-B 16640.61.0 (the update), with KERN-B the higher priority.

So resetting the order once won't last: expect it again after each Flex update. No flag to turn it off was found. The binary does have an "EFI is buggy continue without EFI management" fallback, but nothing user-settable was found that triggers it.

FydeOS has the same code: its cros_installer includes the same efi_boot_management.cc and libefivar, so FydeOS updates will very likely do the same. The FydeOS fydeos_dualboot flag is not a fix: it's for FydeOS's dual-boot mode, where it runs from an image file on another OS's partition, and only gates its update_initrd.sh.

Ways to live with it (none implemented yet):

  • Fix it by hand when it happens: pick neon from the firmware boot picker (Esc at POST), then sudo efibootmgr -o 0005,0001,0000,0003,0002 (adjust to taste).
  • A small neon systemd oneshot at boot that puts neon back first whenever a ChromeOS entry is ahead of it. It still needs one manual Esc boot into neon after each ChromeOS update, but then it's fixed for good until the next update.
  • Deleting Boot0004 doesn't help: the next update recreates it. Neon's GRUB entry chainloads the ESP directly and doesn't need it.

Other ChromiumOS-based images: FydeOS

Checked 2026-09-27, by reading the image only (not installed): FydeOS_for_PC_iris_v23.0-SP1-io.bin.zip (board amd64-fydeos_iris-io, version 16700.56.23.51), in matt's ~/Downloads (extracted copy in ~/fydeos-check). install-flex-emmc.sh should work with it unchanged.

  • Same installer image layout: ROOT-A is p3 (2.7GB), ROOT-B is a 2MB placeholder, the ESP is p12. The same chromeos-install (Rust) and chromeos-install.sh, write_gpt.sh, cros_installer, is_running_from_installer, and coreutils /bin/chroot shim, so the chroot wrapper applies.
  • cros_installer has the same No recognized cros_XXX bios option check, so the fake cros_efi is still needed. The success line is the same Installation to '${DST}' complete.
  • The target layout from write_gpt.sh is 2.66GB each for ROOT-A and ROOT-B, with STATE filling the rest of the disk.
  • FydeOS's chromeos-postinst adds steps: it mounts the destination's ESP (${install_dev%[0-9]}12) to sync a cros_debug (dev-mode) flag in its grub.cfg, then runs /usr/share/dualboot/update_initrd.sh if rootdev returns a loop device, which it does in our chroot. That's harmless: update_initrd.sh exits unless fydeos_dualboot is on /proc/cmdline, which isn't in our faked cmdline, and other disks are masked anyway.
  • The ESP uses GRUB (/efi/boot/bootx64.efi + grub.cfg, kernels in /syslinux/vmlinuz.{A,B}) instead of Flex's crdyboot. The 40_custom chainload path still works. It's unsigned, which is fine because Secure Boot is disabled on hp-chell (mokutil --sb-state). The image's ESP UUID is F35E-F13C.

Caveats before trying it:

  • It replaces Flex (same eMMC, full wipe).
  • The script rewrites the UUID in the 40_custom entry titled "ChromeOS Flex", so FydeOS boots under that title. Decided 2026-09-27 to keep it, since FydeOS is effectively still ChromeOS.
  • Hardware support: FydeOS's site says the iris build supports Intel 6th to 14th gen, which covers this 6th-gen (Skylake) Core m7-6Y75 / HD 515 (checked 2026-09-27).

History

  • 2026-09-04: first install by streaming the image with unzip -p … | dd of=/dev/mmcblk0 bs=4M conv=fsync (~18 min; eMMC writes dropped from ~130MB/s to ~9MB/s as its SLC cache filled, which is normal for this class of eMMC). Booted as live/installer media, as explained above. The note at the time that Flex expands STATE on first boot was wrong.
  • 2026-09-27: reinstalled properly with install-flex-emmc.sh.