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.zipstraight 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.txtand 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
- 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. - Attaches the
.binread-only as a partitioned loop device. - 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. - Masks every disk except the destination: each
/dev/sd*,/dev/nvme*, other/dev/mmcblk*, and/dev/vd*node gets/dev/nullbind-mounted over it, and/dev/diskand/dev/blockare hidden. Inside the chroot, reading/dev/sdareturns 0 bytes, so neon is unreachable. - Runs:
chromeos-install --dst /dev/mmcblk0 --payload_image /tmp/payload.bin \ --skip_dst_removable --yes --target_bios efi --debug - 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_postinstreads/proc/cmdlineforcros_efi(orcros_legacyetc.), even though it's passed--bios efi. The script bind-mounts a copy of the command line withcros_efiappended over/proc/cmdline.chromeos-postinstthen re-chroots into the freshly written root with a non-recursivemount --bind /proc, which drops that overmount. So the image'schroot(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 realcoreutils --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-installexited 0 while its innerchromeos-install.shhad failed. It also logs harmlessERROR cros_installerlines 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 hasInstallation to '/dev/mmcblk0' complete.and no finalError: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 ACPIGetAcpiSysfsPatherrors (not stock ChromeOS firmware). nohup … &over SSH got killed when the session closed, so long runs go throughsystemd-run --unit=... -p StandardOutput=file:/var/log/.... The log must not be in/tmpif another user owns the file (root is denied byfs.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:
- From GRUB (neon's menu, shown for 5s): the entry "ChromeOS Flex
(internal eMMC)" in
/etc/grub.d/40_customchainloads Flex's own EFI loader (crdyboot) from the eMMC's ESP:Runmenuentry "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 }sudo update-grubafter editing40_custom. The install script rewrites the--fs-uuidvalue automatically. - From the firmware boot picker (MrChromebox UEFI,
Escduring POST):efibootmgr -vshowsneon(Boot0005, on the USB card) andInternal eMMC(Boot0001), withBootOrder0005,0001,0000,0003,0002. With the USB card unplugged, the firmware falls through toInternal eMMCand 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/sysnon-recursively, soefivarfsisn't visible and it logsEFI 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_engineruns post-install on the running Flex system with real EFI access. That's what happened here: the eMMC afterwards had ROOT-A16640.57.0(the install) and ROOT-B16640.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 (
Escat POST), thensudo efibootmgr -o 0005,0001,0000,0003,0002(adjust to taste). - A small neon systemd oneshot at boot that puts
neonback first whenever a ChromeOS entry is ahead of it. It still needs one manualEscboot into neon after each ChromeOS update, but then it's fixed for good until the next update. - Deleting
Boot0004doesn'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) andchromeos-install.sh,write_gpt.sh,cros_installer,is_running_from_installer, and coreutils/bin/chrootshim, so the chroot wrapper applies. cros_installerhas the sameNo recognized cros_XXX bios optioncheck, so the fakecros_efiis still needed. The success line is the sameInstallation to '${DST}' complete.- The target layout from
write_gpt.shis 2.66GB each for ROOT-A and ROOT-B, with STATE filling the rest of the disk. - FydeOS's
chromeos-postinstadds steps: it mounts the destination's ESP (${install_dev%[0-9]}12) to sync acros_debug(dev-mode) flag in itsgrub.cfg, then runs/usr/share/dualboot/update_initrd.shifrootdevreturns a loop device, which it does in our chroot. That's harmless:update_initrd.shexits unlessfydeos_dualbootis 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'scrdyboot. The40_customchainload 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 isF35E-F13C.
Caveats before trying it:
- It replaces Flex (same eMMC, full wipe).
- The script rewrites the UUID in the
40_customentry titled "ChromeOS Flex", so FydeOS would boot under that title. Rename the entry, or make the script take its title from the image'slsb-release. The script's messages and this doc also say "Flex" throughout. - Hardware support is unverified: it's unclear whether the
irisbuild supports this 6th-gen Core m7-6Y75 / HD 515 (Skylake). Check FydeOS's hardware compatibility list first.
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.