imx: FIT verified boot, dm-verity, AHAB, encrypted-var posture — wrynose i.MX collection - #316
Merged
Merged
Conversation
mobileoverlord
force-pushed
the
imx-fit-verity-capability
branch
3 times, most recently
from
August 26, 2026 17:36
329761f to
f24759e
Compare
…mp-evk from a signed FIT Capability in the feed, policy in the project: every i.MX kernel and U-Boot can do dm-verity and verified FIT boot, and a project decides whether to use them. Hardware-verified on imx8mp-evk: an unsigned FIT boots when U-Boot carries no key node; with kas/feature/verified-boot.yml the signed FIT boots (sha256,rsa2048:FIT+ OK), a tampered kernel payload is refused with Bad Data Hash, and an unsigned FIT with valid hashes is refused with "Failed to verify required signature 'key-FIT'". Kernels: linux-imx, linux-fslc-imx, linux-compulab, linux-variscite-imx require avocado-security-kernel.inc (DM_VERITY, DM_CRYPT, BLK_DEV_DM, TRUSTED_KEYS built in). dm-verity.cfg pins hardware SHA-256 for both symbol generations (CRYPTO_SHA2_ARM64_CE <=6.15, CRYPTO_LIB_SHA256_ARCH 6.16+). imx93-frdm's private dm-crypt.cfg becomes caam.cfg, CAAM drivers only. U-Boot: fit.cfg (CONFIG_FIT) on every u-boot-imx; fit-verify.cfg and the pubkey embed gated on the verified-boot DISTRO_FEATURE alone, no MACHINE check. CONFIG_SYS_BOOTM_LEN=0x4000000: NXP's imx8m defconfigs pin 32 MiB and the 6.18 Image is 33.8 MiB uncompressed, so bootm failed with inflate() -5. FIT boot generalized: conf/machine/include/avocado-imx-fit.inc holds the UBOOT_SIGN_* / FIT_KERNEL_SIGN_* / KERNEL_CLASSES block; the fitimage recipe becomes avocado-imx-fitimage.bb keyed on kernel-fit-extra-artifacts in KERNEL_CLASSES, and avocado-stone.bbappend keys the dependency the same way. imx8mp-evk boots one fitImage via bootm; FIT_CONF_DEFAULT_DTB pins the dtb its env used to load. Feed/SDK: nativesdk-cryptsetup and nativesdk-u-boot-tools in avocado-sdk-toolchain; mmc-utils in packagegroup-avocado-imx-rootfs. avocado-img-bootfiles gains a dependency edge on virtual/bootloader and imx-boot: it scrapes DEPLOY_DIR_IMAGE, so without the edge a U-Boot rebuild left the package restored from sstate with the previous imx-boot, and provisioning kept flashing the old bootloader.
… a prebuilt imx-boot Lets a project make a prebuilt imx-boot enforce the project's own FIT signing key, with no U-Boot source build. Hardware-verified on imx8mp-evk: the project-keyed bootloader refuses the distro-signed FIT and boots the project-signed one. - u-boot-tools_%.bbappend installs fdt_add_pubkey and fit_info. The tools-only build already produces both (hostprogs-$(CONFIG_TOOLS_LIBCRYPTO)); oe-core's do_install never picks them up. Lands in nativesdk-u-boot-tools-mkimage. - imx-mkimage-tools_git.bb: mkimage_imx8 and mkimage_fit_atf.sh as a nativesdk package. meta-freescale's imx-mkimage is `inherit native` and cannot be BBCLASSEXTENDed, and its deployed binary has an RPATH into the build tree. Same source pin as imx-boot; only mkimage_imx8.c is compiled. Pulled by packagegroup-avocado-imx-sdk-extra on mx8m-generic-bsp, with nativesdk-lz4 (soc.mak lz4-compresses tee.bin). - avocado-img-bootfiles ships imx-boot-tools/ (DDR firmware, SPL, u-boot-nodtb, control DTB, bl31/tee, soc.mak, mkimage_fit_atf.sh) minus the native binary. Flow: fdt_add_pubkey -a sha256,rsa2048 -k <keydir> -n FIT -r conf <dtb>, then soc.mak's u-boot-spl-ddr.bin / u-boot.itb / flash_evk steps with mkimage and mkimage_imx8, then mkimage -F -k <keydir> to re-sign fitImage.
u-boot-imx do_unpack now fails with 'Recipes that set S = "${WORKDIR}/git"
... should remove that assignment, as S set by bitbake.conf in oe-core now
works' (S defaults to ${UNPACKDIR}/${BP}, BB_GIT_DEFAULT_DESTSUFFIX = ${BP}).
Upstream meta-imx's u-boot-imx-common_2026.04.inc carries no S line; match it
in our 2025.04 copy. rubikpi-bt-staticdev had the same pattern via an
explicit destsuffix=git; drop both so the default layout applies.
…stream keys
mali-imx.inc still sets RPROVIDES/RREPLACES/RCONFLICTS with literal
"${PN}-lib*" keys from its anonymous python; the repinned BitBake expands
those at finalise, so the upstream keys now work and collide with our
re-declared mali-imx-lib* ones:
WARNING: Variable key RPROVIDES:${PN}-libegl ( libegl libegl1) replaces
original key RPROVIDES:mali-imx-libegl ( libegl libegl1).
Five warnings per parse, same values on both sides; the workaround is dead.
imx-base-extend.inc points PREFERRED_VERSION_libcamera at 0.7.1.imx for mx95/mx8mm/mx8ulp, but that recipe is in meta-imx-sdk, which our layer set omits. libcamera is not in any Avocado image; clear the preference.
Nothing provisioned, updated or booted it: every stone manifest carried a 128 MiB "recovery" partition with no image, and every U-Boot env had a slot "r" branch that pointed bootpart at it and left rootdev unset. A/B is the whole model, so remove both. rootfs-a/rootfs-b/var move up one partition number (p5/p6/p7); the envs follow, and everything else addresses them by GPT label. An unset or invalid avocado_boot_slot now boots A, as before. Partition table change: devices need a fresh provision.
…he FIT Every i.MX manifest gains rootfs-a-hash / rootfs-b-hash (8 MiB, plain data) after the rootfs slots, filled from a new `rootfs_hash` image (avocado-image-rootfs-<machine>.verity) and carried as an OTA artifact next to boot and rootfs; the shared fwup template writes it in the complete and upgrade tasks. systemd's veritysetup generator takes the hash tree as a block device and has no hash-offset option, which is why it is a partition rather than a tail on the rootfs image. On the FIT machines (imx8mp-evk, imx93-frdm) the env reads `avocado,roothash` from the default configuration node of the loaded FIT - after `bootm` will have verified it - and boots root=/dev/mapper/root with roothash= and the slot's data/hash devices. A FIT without the property boots the rootfs directly. The hash image is always present: the runtime build writes a zero-filled placeholder when rootfs verity is off, so the manifest can name it unconditionally.
… set SPL never printed on the A55 UART (SD and uuu/SDPS alike) while the SM console looped 'Reset LM 2, reason=fccu, errId=19' -- DEV_SM_FAULT_WDOG3, the AP LM's boot watchdog, reaction=lm_reset. The SPL's first act is SCMI traffic to the System Manager before init_wdog() disables WDOG3, so a SM/U-Boot generation mismatch shows up exactly like this. meta-imx wrynose-6.18.20-2.0.0 pairs SM 2026q2 (44f76dcb, Build 896) with U-Boot lf_v2026.04; avocado-imx.inc pins u-boot-imx to 2025.04 for every i.MX board. The FRDM's factory image runs the *other* consistent pair -- U-Boot 2025.04 @ 99518e6b (our exact SRCREV) with SM 2026q1 (c450f539, Build 819) -- so the U-Boot commit itself is fine; only the pairing is not. Override the pin to 2026.04 on imx95-frdm. The EFI capsule / DFU options behind the original pin are already disabled for every version by disable-unused-vendor-features.cfg.
The i.MX stone manifests name avocado-image-rootfs-<machine>.verity for the per-slot rootfs hash partitions and avocado-stone.bbappend deploys a 4096-byte zero placeholder, but the bootfiles collector skips every deploy artifact matching 'rootfs', so the file never reached the runtime input dir. With a CLI older than rootfs.image.verity (1.0.0-rc.2 is), the bundle dies: Image file 'avocado-image-rootfs-imx95-frdm.verity' for artifact 'rootfs_hash' not found in any input directory Install it explicitly from DEPLOY_DIR_IMAGE; a CLI that emits the real hash tree writes to the same path and replaces it.
With U-Boot 2026.04 the i.MX 95 SPL boots via USB SDP ('Trying to boot from
USB SDP ... SDP: handle requests'), re-enumerates as the SDPV device and
waits for the host to push the second stage. Our script went straight from
'SDPS: boot' to the FB block, so uuu waited for fastboot while the SPL waited
for SDPV. Add the SDPV delay/write -skipspl/jump block from uuu's built-in
emmc_all; uuu only runs it when an SDPV device appears, so ROM-API streaming
SPLs (i.MX 93) are unaffected.
…er take /run/avocado down with it root=/dev/mapper/root carries no mmc device; the partition the verity root is built on is systemd.verity_root_data=, so read that too. And the service's RuntimeDirectory=avocado is the same directory avocadoctl.socket listens in - a failure of this one-shot had systemd delete it, socket included, so every avocadoctl call afterwards failed with Daemon Not Running and no extension merged. RuntimeDirectoryPreserve=yes keeps the directory across this unit's lifecycle.
OE-core's 90-systemd.preset enables the getty@.service template with no instance; the offline enabler writes a bare getty@.service symlink, systemd instantiates getty@getty.service, agetty fails on /dev/getty and the board boots 'degraded' forever. Seen on imx95-frdm and imx8mp-evk; the login prompt is serial-getty@ttyLP0/ttymxc1 from systemd-getty-generator regardless. Add one generic allarch package in meta-avocado-distro, systemd-serial-console-preset (89-avocado-serial-console.preset: 'disable getty@.service'), and pull it from packagegroup-avocado-imx-rootfs. Not put in the global systemd bbappend on purpose: the x86 machines have a VT and want getty@tty1. nvidia (systemd-jetson-masks) and qcom (systemd-rubikpi3-masks) carry the same line per vendor and can move to this package later.
…encrypted-var on imx8mp-evk caam.cfg (was imx93-frdm-only) now applies to linux-imx, linux-fslc-imx, linux-compulab and linux-variscite-imx: CAAM built in (it was =m), plus CRYPTO_DEV_FSL_CAAM_BLOB_GEN and TRUSTED_KEYS_CAAM, which needs CAAM_JR >= TRUSTED_KEYS. A CAAM trusted key seals a volume key to the SoC's OTPMK with the plaintext never leaving the kernel - the backend the /var encryption on i.MX8M will use. The i.MX var-key backend (SoC-UID-derived Argon2id key) applies to every i.MX machine instead of imx93-frdm alone; soc-imx8m publishes the same OCOTP UID. avocado-imx8mp-evk declares encrypted-var and points /var at /dev/mapper/var when the feature is on, like every other encrypted-var machine.
…rompt avocado_verity_init reads avocado,roothash from the FIT's default configuration and is documented to fall through when the property is absent. It did not: U-Boot's hush returns the status of a failed if-condition when there is no else, so the nested 'fdt get value roothash' failure became the variable's exit status, 'run' aborted before avocado_boot, and imx93-frdm sat at the u-boot=> prompt after 'libfdt fdt_getprop(): FDT_ERR_NOTFOUND'. imx8mp-evk only worked because its FIT carried the property. End the block in 'true' on both FIT machines.
mobileoverlord
force-pushed
the
imx-fit-verity-capability
branch
from
August 26, 2026 22:07
f24759e to
27120ed
Compare
jetm
self-requested a review
August 26, 2026 23:15
Both the TPM2 enroll and the TPM2 open in cryptsetup-var.sh fail open on purpose, because a legitimate PCR 7 change must not lock a device out of its own /var. The cost of that choice is silence: a device whose PCR 7 moved keeps booting on the Argon2id recovery slot indefinitely, and from outside it is indistinguishable from a correctly sealed one. At fleet scale that is a security control that can lapse across a whole deployment with no signal anywhere. Record which keyslot actually opened the volume, because that is the one fact nothing can reconstruct later - a luksDump afterwards shows that a TPM2 token exists, not that it was what worked. Pair it with the token's presence and whether /var really came up through the mapper, the latter re-checked in userspace rather than trusted from the initrd because opening the container says nothing about what fstab then mounted; that exact mismatch was a real defect on this board when AVOCADO_VAR_PART_DEV was unset. The initramfs cannot write the KV store directly - neither fw_setenv nor the fw_env.config it needs exists there, and the generator for that config runs in the real root. So the initrd drops the facts in /run, the tmpfs the key file already depends on and which systemd carries across the switch-root, and a rootfs-only unit publishes from there into the U-Boot environment peridiod already reads. Publishing on change only respects that store's documented expectation of infrequent writes, and makes the write itself the interesting event rather than steady per-boot flash wear. Verified by stubbing fw_printenv/fw_setenv/findmnt over six cases: first boot publishes all three keys, an identical second boot writes nothing, a tpm2 -> argon2id regression flips only that key and emits a warning naming PCR 7, a /var mounted off the raw partition reports unencrypted, and an absent posture file or absent libubootenv both exit 0 with a diagnostic. Separately confirmed write_posture cannot abort the unlock: it survives a failing token probe under set -e (the reason it uses `if` rather than `&&`) and an unwritable target. Not yet built or run on hardware - the build hosts were in use. Signed-off-by: Javier Tia <javier@peridio.com>
cryptsetup-var-posture was defined, packaged and set to auto-enable its unit, but nothing in the tree ever pulled it into an image. The RPM built and stopped there: a qemuarm64 encrypted-var rootfs came out with cryptsetup-var-udev in its manifest and no posture package, so avocado-posture-publish.service never reached a device and the posture this branch exists to report was never published anywhere. Add it beside cryptsetup-var-udev in the same encrypted-var branch, which is the condition that already decides whether any of this content is wanted - the posture reporter has nothing to describe on a machine whose /var is not a dm-crypt mapping. The auto-enable was what hid this. A package with SYSTEMD_AUTO_ENABLE looks wired up on inspection, and the unit is genuinely correct; the gap was one level up, in whether anything installed it. Nothing failed, nothing warned, and the built RPM in deploy/rpm made it look present. Signed-off-by: Javier Tia <javier@peridio.com>
The posture keys are written from Linux with fw_setenv and read back with fw_printenv, so nothing in this script's own path suggests U-Boot has an opinion about them. On a machine built with CONFIG_ENV_WRITEABLE_LIST it does, and the failure is silent: env_flags_validate rejects an H_EXTERNAL variable without the 'w' flag so it never reaches env_htab, env_export serialises the hashtable, and the A/B flow calls saveenv on every slot switch. A key absent from CFG_ENV_FLAGS_LIST_STATIC therefore survives until the first OTA and is then gone, with nothing logged and a fleet view that keeps reporting whatever it last saw. Neither this change nor the one adding that permit list is wrong on its own, which is why no review of either would surface it. Recording it against the keys themselves puts the constraint where the next person adds a fourth key, rather than in a commit message nobody reads at that moment. The debt marker rather than the fix because the permit list has not merged yet, so there is no list here to add the entries to. It names the trigger so this cannot leave draft without them. Also note the consequence for consumers: a permitted key is writable from the saved environment by construction, so posture is forgeable by anyone who can write the boot medium. That is acceptable for an observation, and is exactly why it must never be presented as tamper-evident. Signed-off-by: Javier Tia <javier@peridio.com>
Testing a change meant pulling the microSD, carrying it to a reader, flashing, and carrying it back - eight times in one session. U-Boot can export the card as USB mass storage instead, so the reader moves and the card stays in the slot. `ums` reuses the existing fwup path once the device appears, and the write is the same one the sd verb performs. Device discovery is the part worth care. It diffs /sys/block across the export rather than taking the newest or highest /dev/sd*, because this path ends in a write from sector 0 and a development host carries multi-terabyte disks in the same namespace. Zero or several new devices both refuse. The discovered node then goes through the same safety guard the sd path uses, after unmounting whatever udisks grabbed - it auto-mounts within a second of the export, which is precisely the live mount that guard exists to catch. An export left running by a previous invocation is released first. Without that the stale device is already in the before-set, the diff finds nothing new, and the tool reports that no device appeared while the device is sitting right there. That failure happened during bring-up, and the message blamed the cable - so the timeout now distinguishes "no gadget enumerated" from "gadget enumerated but nothing exported" and only names the port when nothing enumerated at all. An error that asserts an unverified cause sends the reader to the wrong place. Verified on an FRDM-IMX93: the board exported mmc 1 as /dev/sdd, fwup wrote the full image in 40s, and after a reset over the same serial link the board booted the signed chain to Linux with LUKS2 /var mounted - start to finish without touching the hardware. Signed-off-by: Javier Tia <javier@peridio.com>
ums and fuse-srk both need the board at a U-Boot prompt, and getting one meant catching a three-second autoboot window by hand. That window lands about ten seconds into a boot, so waiting and then knocking races it - the pattern that works is holding CR from the moment the reboot is issued, which was hand-rolled in five throwaway probe scripts before it earned a place in the tool. --interrupt classifies what is on the console before touching anything: a U-Boot prompt needs no work, a login prompt or a shell gets rebooted, and anything unrecognised refuses rather than resetting a board whose state is unknown. The reboot goes through the console rather than the network because a freshly flashed card has no address yet, which is exactly when this is wanted. Two bugs found by running it. The prompt was tested against each individual read, so a prompt split across reads - the common case at 115200 - was never matched and the search timed out with the board sitting at the very prompt it wanted; it now accumulates. And the console-state probe had to be inert, since a nudge that changes the state it measures is useless: a bare CR qualifies, because U-Boot reprints its prompt, a getty reprints its banner and a shell gives an empty line. Verified on an FRDM-IMX93 from a running Linux: rebooted through the console, stopped at the prompt, exported mmc 1, wrote the image in 42s, and after a reset the board booted the signed chain with LUKS2 /var mounted. The same run exercised the stale-export release added earlier, which had not been hit before. Signed-off-by: Javier Tia <javier@peridio.com>
fuse-srk unpacked the fuse file big-endian and burned the SRK hash byte-swapped into an FRDM-IMX93 on 2026-08-13. NXP programs the words as `od -t x4` prints them - native order - per doc/imx/ahab/guides/mx8ulp_9x_secure_boot.txt section 1.6. The --endian=big form comes from introduction_ahab.txt, where it exists only to show the file contains the SHA, under a line saying those commands cannot be used as a reference for programming the fuses. Variscite's mx8-fuse-commands-helper reads them with hexdump -e '/4 "%X"', also native, which is a second vote for the ordering. Every guard in the tool passed on that burn: the words were written correctly, read back correctly, the fuses were blank and the part open. They all compare the burn against what this tool intended, and a byte-swapped hash is intended-and-wrong, so none of them could fail. The only check that distinguishes the two is what the ELE says afterwards, and that was left to the operator as a printed suggestion. So the tool now reboots after a successful read-back and fails if ahab_status still reports events. The event log is per boot session and the entries present immediately after programming predate the fuses, so the reboot is what makes it mean anything. On failure it says plainly that the part is open and must not be closed, since closing a board whose hash the ELE rejects makes it refuse every image these keys can sign. That check cannot be exercised here - SRK_HASH is one-time programmable and the only board's is spent - so it is written from the failure it is meant to catch rather than from a passing run. Confirming it needs a second board. Signed-off-by: Javier Tia <javier@peridio.com>
The i.MX91 runs the same ELE-based flow as the i.MX93 - same fuse bank 16, same 256-bit SRK hash, same CONFIG_AHAB_BOOT under ARCH_IMX9, and SPSDK covers it as mimx9131 - so only the family and the device tree differ. That makes it the cheaper board to spend an irreversible SRK_HASH on, which matters now that the i.MX93's is gone. The machine-specific values move into the machine configuration rather than gaining a second hardcode beside the first. Both are declared with no default and the build refuses when either is missing: signing with the wrong SPSDK family succeeds and yields an image the ELE rejects, and the wrong dtb yields a container that authenticates and then boots a kernel with the wrong hardware description. Neither failure is visible until a board is in hand, so a defaulted value would just be a slower way to learn the same thing. Verified without an i.MX91 present. With MACHINE set to that board the recipes resolve mimx9131 and imx91-11x11-frdm.dtb, COMPATIBLE_MACHINE accepts it, and its u-boot picks up ahab.cfg; with MACHINE back on the i.MX93 the OS container still signs, so the parameterisation did not regress the board that works. What remains untestable here is the burn itself. Signed-off-by: Javier Tia <javier@peridio.com>
This branch was cut from a tree that carried no shell avocado-flash, so the port landed beside it as scripts/avocado-flash.py. Rebasing onto wrynose brought the original back, and the tree now ships two copies of one tool with nothing to tell a caller which is current. Keep the port and give it the plain name. Every resolve_* and flash_* function in the shell version has a counterpart in the Python one, which then adds the ums and fuse-srk verbs, so retiring the port instead would strand callers on the smaller tool. The plain name is the one the port already prints in its own usage and error output, and the one the flashing procedure tells people to run. Signed-off-by: Javier Tia <javier@peridio.com>
AHAB covered the kernel and device tree but stopped there. booti reads its
ramdisk from argv[1] and never authenticates it, so the initrd stayed outside
the chain - and on this image that initrd is what derives the LUKS key and
opens /var. Swapping it yields code running before the disk is decrypted, with
the key derivation under the attacker's control, which is most of what secure
boot plus encryption was bought to prevent.
Carrying the initramfs as a second --data payload does not close it, despite
being the obvious reading of the mkimage interface. container_get_image_dst()
latches on the first exec and first data image, so the payload authenticates
and is then never located; booti still uses argv[1]. Keeping the separate load
just for its size is worse than it looks: ${filesize} comes from the on-disk
copy, so a larger file leaves unverified bytes past the authenticated region,
and appending a cpio archive is the documented way to overwrite files in an
initrd.
Bundle it into the kernel Image instead. It then rides inside the exec payload
the container already signs, booti is handed no ramdisk at all, and the
property holds by construction rather than by a check something can drop. The
build refuses if the bundle is off or if the environment still loads a
ramdisk, because both fail open - the board boots and /var unlocks either way.
The loose initramfs also leaves the boot partition. Nothing loads it once the
container carries it, and at 44 MB it no longer fits beside an ~80 MB
container in 128 MB.
Signed-off-by: Javier Tia <javier@peridio.com>
Bundling the initramfs into the kernel put the signed container at ~170 MB, and stone died building the FAT image with "Failed to write to file 'os_cntr_signed.bin': No space left on device" against a 128 MiB partition. The message names the host filesystem's failure mode, not the partition's, which is why the first read of it went looking at df. The size is not an accident of bundling but of how it is done: oe-core's copy_initramfs unpacks the .cpio.zst and hands the kernel a plain archive, which the kernel stores as given. Compressing it back cannot be reached from a config fragment - the compression choice is `depends on INITRAMFS_SOURCE != ""` and that variable only ever reaches the kernel as a make argument during do_bundle_initramfs, so the symbol is inactive when .config is written. Correcting it means overriding an oe-core class function and re-checking that override on every uprev; 128 MiB of disk on a board with room is the cheaper side of that trade. recovery grows alongside boot-a and boot-b because it is bootpart 5 in the U-Boot environment and holds a boot filesystem too. A recovery slot too small for the image it recovers from is worse than none. Scoped to AHAB, like the file list above it: an unsigned build carries Image, dtb and initramfs at about 67 MB and still fits 128 MiB. The new check asserts the container fits before stone runs, so the next time this boundary moves it says so in those terms rather than through a FAT writer. Its arithmetic goes through awk because bitbake's shell parser raises NotImplementedError on $(( while collecting task dependencies, which fails the recipe at parse rather than at run.
The board stopped in U-Boot with "FDT image overlaps OS image (OS=80200000..8aa90000)". The addresses were picked when the kernel was about 33 MB; bundling the initramfs made it 200 MB, so its unpack range now covers fdt_addr at 0x83000000 and the container staged at 0x88000000. The device tree collision is what U-Boot reported, and it is the milder half. The container overlap would not have been reported at all: authentication memcpys each payload to its destination and booti then calls container_get_image_dst() on the container to find out where they went, so a kernel unpacking across the staging address destroys the header that is about to be read. That failure surfaces as a parse error about addresses rather than as anything naming an overlap, which cost several probes the first time this shape appeared. fdt_addr goes near the top of the first DRAM bank. A kernel that outgrew that would not fit the bank at all, so the address stops being the thing to adjust first. The container cannot share that bank. `bdinfo` reports DRAM as 0x80000000+0x16000000 and 0x98000000+0x68000000, a 32 MiB hole between them, leaving 352 MiB in the first bank against a kernel taking 200 MB of it. A load crossing the hole is refused with "Reading file would overwrite reserved memory", which describes neither the hole nor the size. So the container stages at 0xa8000000, in the second bank and above the no-map carve-out at 0xa4000000 that Linux later claims as ele-reserved. The bootm window moves with it. NXP's board env caps it at 256 MiB from 0x80000000, which suited a 33 MB kernel and cannot hold this kernel plus the container staging it. Assert the ranges rather than the addresses. The old check compared cntr_addr against image_addr for equality, which is true of any layout that fails this way and false of every layout that failed here. The new check measures the staged kernel and refuses when it reaches the device tree or the container - it caught the second collision at do_compile, when enabling encrypted-var grew the initramfs and took the kernel past a device tree that had already been moved once. Signed-off-by: Javier Tia <javier@peridio.com>
fdt_addr and bootm_size were moved in the environment itself, so an unsigned build of these boards got them too. It still boots - a ~33 MB kernel does not reach a device tree at 0x94000000, and the wider window only permits more - but neither change is one the unsigned path asked for, and nothing marked that it was being changed. They are also coupled: 0x94000000 sits outside NXP's 256 MiB window, so the device tree move cannot land without the window widening. Carrying both in the shared file made an AHAB-only requirement look like a property of the board. Rewrite them in the AHAB block instead, beside the boot-flow lines already rewritten there, and restore NXP's values in the environment. The unsigned boot goes back to what it was before this branch; the signed boot is unchanged, since it reads the rewritten file. The environment keeps stating the stock values explicitly rather than inheriting them, so the rewrite has a line to match on and a reader can see both values in one place. Signed-off-by: Javier Tia <javier@peridio.com>
…chine
SRC_URI:append:imx93-frdm and :imx91-frdm applied ahab.cfg (which sets
CONFIG_AHAB_BOOT=y) unconditionally, so every u-boot built for these
machines compiled in AHAB container authentication regardless of
whether the ahab feature was ever enabled. With CONFIG_AHAB_BOOT=y,
booti always treats argv[1] as a signed container; a build that never
enabled ahab never constructs one, so booti hands the ELE a bare
kernel Image and gets "Error: Wrong container header" /
"Authenticate OS container is failed" - confirmed on an FRDM-IMX93
running a plain fTPM build with no ahab feature at all.
Wrap both lines in the same bb.utils.contains('DISTRO_FEATURES', 'ahab', ...)
check meta-avocado/recipes-kernel/linux/linux-yocto_%.bbappend already
uses for its own feature-gated fragments (secureboot, encrypted-var),
so a non-ahab build for these machines stops compiling in AHAB
authentication it has no way to satisfy.
Signed-off-by: Javier Tia <javier@peridio.com>
…tate hash avocado_ahab_sign() (avocado-ahab-sign.bbclass) reads the AHAB SRK certs and private keys straight off AVOCADO_AHAB_KEYS_DIR inside do_sign_ahab, a do_compile postfunc, with no [file-checksums] hashing the actual cert/key content. Excluding the variable's value from BB_BASEHASH_IGNORE_VARS meant two builds pointing at different SRK sets computed the same sstate signature for the signed boot image - the identical bug 6c471b4 just fixed for AVOCADO_SB_KEYS_DIR, except here it can serve a device an image signed with someone else's AHAB root-of-trust keys from a shared sstate mirror, silently. AVOCADO_AHAB_SPSDK_BINDIR has the same exposure for a different reason: nothing pins the SPSDK version, so a different bindir can mean different signing tool behavior, and the path was the only signal available that the input changed. Drop both from BB_BASEHASH_IGNORE_VARS:append and rewrite the comment, which still argued "same reasoning" for keeping them excluded even after the SB fix reversed that reasoning one line above it. The cost is the same cache miss 6c471b4 already accepted for SB_KEYS_DIR, which is the safe failure mode for key material. Signed-off-by: Javier Tia <javier@peridio.com>
…ing converted Turning rootfs var.encrypt on for a device that already ran plaintext failed closed: grow-var had resized the btrfs to fill the partition, so encrypt_in_place found no 32 MiB tail for the LUKS2 header and exited with 'leaves no 32 MiB for a LUKS header' - /var never mounted and the board sat in the emergency shell after the OTA. Only a fresh flash (fs far smaller than its expand-to-fill partition) ever worked. btrfs shrinks online, so when the headroom is short, mount the filesystem on /run/cryptsetup-var-shrink, 'btrfs filesystem resize' it by exactly the deficit, unmount, and continue with the same --reduce-device-size 32M reencryption. A shrink refusal (tail chunk in use, no free space) still fails closed with a message naming that. A seeded /var can be tens of GiB and the conversion then runs for many minutes with nothing on the console: print the MiB being converted, that it is a one-time resumable migration, and pass --progress-frequency 30 when the installed cryptsetup (>= 2.4) has it. tests: grown-fs case (shrink by the deficit, mount->resize->umount before the reencrypt, device-size covers the result), the small flashed case is not shrunk, and the console line is present.
FIT signature verification, landed in the previous change, is enforced only on the FIT path. The environment lives in the unsigned, writable uboot-env partition (CONFIG_ENV_IS_IN_MMC=y in the vendor defconfig), and neither CONFIG_CMD_BOOTI nor CONFIG_LEGACY_IMAGE_FORMAT was disabled, so anyone with offline write access to the card could rewrite bootcmd to boot a raw unsigned kernel and skip verification without touching the FIT at all. That is the threat this work's own threat model names, so the capability was left undeclared until it was closed. The permit list ships as a U-Boot patch rather than a config fragment because it has to. CONFIG_ENV_WRITEABLE_LIST is a Kconfig symbol, but the list it consults is CFG_ENV_FLAGS_LIST_STATIC, a C define in a board config header that no fragment can set. Enabling the symbol alone would leave the list at its empty default, which rejects every variable arriving from the saved environment - avocado_boot_slot included - and would silently stop A/B updates taking effect. The two ride the same recipe gate for that reason. The nine permitted variables are the union of what actually writes this board's environment: rootdisk.conf's uboot_setenv calls across the complete and upgrade tasks, and the activate map in the stone manifest that avocadoctl's SlotAction::UbootEnv iterates. The per-slot version and uuid stamps are deliberately absent - nothing sets them, the upgrade tasks only unset them, and the boot script never reads them. Keyed autoboot rather than CONFIG_BOOTDELAY=-1, which would close the serial path outright. Every documented recovery for this board goes through the U-Boot prompt, ums being the flashing path, and the bench board's SRK fuse is already permanently burned, so removing recovery would leave it one failure from unrecoverable. Keying also fixes a real availability bug: this board's serial line intermittently injects stray bytes that interrupt autoboot with no keypress. The residual exposure is recorded in a debt marker rather than left implied, and the capability declaration that depends on this is gated on hardware verification. Signed-off-by: Javier Tia <javier@peridio.com>
093f504 pinned PREFERRED_VERSION_u-boot-imx to 2025.04 in avocado-imx.inc for every i.MX machine, because 2026.04's stock defconfigs enable EFI capsule authentication and USB DFU and do_compile fails on boards carrying neither fix. That is the right default. It is the wrong answer for this board, which already turns both off at source via no-efi-capsule-auth.cfg and disable-unused-vendor-features.cfg, so override it back here. 2026.04 is a requirement on this machine rather than a preference. The compiled-in environment selects its source with CONFIG_ENV_SOURCE_FILE, resolved against board/$(SYS_VENDOR)/$(SYS_BOARD): board/nxp/imx93_frdm on 2026.04, board/freescale/imx93_frdm on 2025.04. u-boot-imx_%.bbappend refuses to build on the older layout rather than emit a bootloader with no Avocado boot flow compiled into it, so under the layer pin this machine does not build at all. The downgrade is independently destructive: 2025.04 carries CONFIG_SYS_BOOTM_LEN=0x4000000 against 2026.04's 0x8000000, halving the bootm budget for a FIT that currently uses about 60% of it. A bootloader flash under the old pin produced a board that ran SPL and never reached U-Boot, recoverable only over uuu serial download. Placed after the requires rather than beside the other machine variables, since avocado-imx.inc is pulled in at line 10 and an assignment above it would be overwritten by the layer-wide value instead of overriding it. Signed-off-by: Javier Tia <javier@peridio.com>
The patch was generated against the 2025.04 header, which the pin removal in the previous commit no longer selects. On 2026.04 the same hunk still applies, but at an 11-line offset with fuzz 1, and Yocto's patch-fuzz QA fails the task rather than accepting a patch that no longer matches its context. Regenerated against the pristine 2026.04 source. The permitted set is unchanged - the nine variables that actually write this board's environment - and only the surrounding context moved. Note 2026.04 spells the MMC env device CONFIG_ENV_MMC_DEVICE_INDEX where 2025.04 used CONFIG_SYS_MMC_ENV_DEV, which is what shifted the anchor.
The env permit list added alongside FIT verification does not merely harden this board, it stops it booting. The whole Avocado boot flow - bootcmd, avocado_boot_init, load_image, avocado_boot and the variables they read - reached U-Boot only as the saved environment: u-boot-env.inc runs mkenvimage over env/avocado-imx93-frdm.txt and fwup writes the result to the uboot-env partition. env/mmc.c imports that copy with H_EXTERNAL, and env/flags.c drops every H_EXTERNAL variable missing the 'w' access flag, so all thirteen were rejected while the compiled-in default supplied only console and a stock bootcmd. The board would have come up running U-Boot's own boot command with no Avocado boot path at all. Move the flow to board/nxp/imx93_frdm/avocado.env, selected by CONFIG_ENV_SOURCE_FILE. Compiled-in variables arrive with H_DEFAULT, which env_flags_validate returns early for, so the flow is immune to the permit list by construction rather than by being listed on it - listing bootcmd would have reopened the offline bypass the list exists to close. Delivered unconditionally for this machine rather than under the verified-boot gate, matching fit.cfg. A build without the feature still imports the saved environment and it still wins, so those builds are unaffected; what the unconditional form buys is that both build modes run one authored copy of the flow instead of diverging. env/ avocado-imx93-frdm.txt therefore drops to provisioning state only, because a second copy would be inert under verified-boot and authoritative without it. devnum and mmcblk join the permit list. They are the one pair that cannot come from the compiled-in default: stone-provision-uuu-emmc.sh rewrites them with fw_setenv directly in the built image, after mkenvimage has already run, so an eMMC-provisioned device would otherwise fall back to the SD defaults and look for its boot partition on the wrong medium. They select the boot device, not what runs from it, and the FIT signature check still gates execution. avocado.env includes the vendor board .env rather than replacing it. That file was already in effect through the Makefile ENV_FILE_BOARD wildcard and supplies initrd_high plus the fastboot and manufacturing helpers uuu recovery depends on; dropping it would have broken the recovery path this board has already needed once. CONFIG_ENV_SOURCE_FILE is set explicitly instead of relying on the board-name default, because the default resolves through a $(wildcard): a path that stops resolving yields an empty environment silently, while the explicit form fails the build. For the file carrying the boot command, the loud failure is the one worth having. Signed-off-by: Javier Tia <javier@peridio.com>
Drop CONFIG_AUTOBOOT_KEYED and its stop string on this machine. This is an ergonomics decision, not a defect report, and the distinction matters because the first version of this change argued the opposite. The stop string was never shown to be broken. A day went into "it does not work" - sent through a confirmed 2.001-second window at 137 chars/sec and again at 44 chars/sec, plus hand-typed pastes, all failing - and the cause was the sender, not the board. The host script paced itself on a read timeout, and tio's read returns the instant bytes are waiting rather than sleeping out its timeout, so during a boot flood it spun and pushed thousands of bytes per second. That overran the LPUART FIFO, so passwd_abort_key(), which drains one character per 10ms udelay, never saw a clean sequence. Throttled to ~8 chars/sec an unkeyed keypress broke in first try, with nothing about the board or the Kconfig changed. What the keying actually cost was a day of working around it, and the reason to drop it now is that every documented recovery for this hardware goes through the U-Boot prompt - "ums 0 mmc 1" is the flashing path - and reaching it should not depend on getting a host-side send rate right. The line-noise exposure is accepted knowingly rather than dismissed: unkeyed, the stray bytes this board's serial line injects can halt an unattended boot, which is why the debt marker names unattended operation as the upgrade trigger. Re-enabling the keying when the board joins a HIL runner needs no new investigation - the string works, and the only thing that ever defeated it is now understood.
The offline-card bypass this capability names is now closed and proven on hardware, so the declaration deferred out of imx93-fit-verified-boot task 7.1 can finally be made. Until now kas/feature/verified-boot.yml appended the token to DISTRO_FEATURES and every consumer gated on it independently, so asking for it on a machine with no FIT signing key wired produced an ordinary unsigned build and no diagnostic - the silent-success failure this class exists to remove. Declared unconditionally rather than derived from DISTRO_FEATURES. The token states what the MACHINE can deliver: the signing wiring lives in this conf and in u-boot-imx_%.bbappend, so it is a property of the board's configuration and true whether or not a given build asks for it. Deriving it from the request would make the refusal tautological, for the same reason already spelled out for tpm2 one paragraph above it. The declaration names its residuals in the conf rather than implying they do not exist, because an over-claim here is exactly what 7.1 was deferred to avoid. Serial console access is one, and it is WIDER than the plan assumed: the design called for a keyed autoboot stop phrase, so the residual was to have been "anyone who reads the phrase from the source". That keying was removed for ergonomics, so it is now plain physical serial access. booti is the other - CONFIG_CMD_BOOTI cannot be disabled, since boot/Kconfig:446 selects it from a promptless BOOT_DEFAULTS_CMDS the bootstd stack needs, so the command exists and is merely unreachable from the saved environment. Both directions were checked rather than assumed. Requesting verified-boot on avocado-imx93-frdm parses clean; requesting it on avocado-qemux86-64, which declares "encrypted-var tpm2", now fails with a message naming the machine, the feature and the fix. Without the AVOCADO_SECURITY_FEATURES entry that second build would have succeeded and shipped an unsigned image that had been asked to be signed. Signed-off-by: Javier Tia <javier@peridio.com>
…heckable This change's own bypass-closed check was verified by hand: a person power- cycled the board, typed at the U-Boot prompt, and read the result. That is real evidence and it is also unreadable by anything downstream, so the task carried a prose verify: line, landed at assertion class against a test-class floor, and blocked the change it was verifying. Nothing was wrong with the work; the evidence just could not be re-run. The board became scriptable once power control and a console profile were both available, so the check is now a program. It asserts the two properties that matter and that pull against each other: a saved bootcmd must NOT survive an import, or the offline environment bypass is open, and a saved avocado_boot_slot MUST survive it, or the permit list closed the hole by breaking OTA instead. Recording the verdict rather than driving the board from verify: is deliberate. A run power-cycles hardware and takes minutes, well past the 120s per-command timeout a verification pass allows, so the harness records and check-result.sh reads. The record names the commit it ran against and is rejected unless that commit is an ancestor of HEAD, so a stale pass cannot vouch for a tree it never saw. The unimplemented modes exit non-zero instead of doing nothing. A stub that succeeds is worse than a missing one - it turns an absent check into a passing verify: line, and the task it gates then reads as verified. Two failure modes are commented in the script because both were paid for on this board. Sends are paced with msleep and never on a read timeout: tio's read returns as soon as bytes are waiting, so a loop paced on it floods a console that drains far slower, which produced three separate and wrong conclusions that the hardware was broken. And boot markers are latched as they stream rather than matched in the tail at the end, because a boot is far larger than any bounded buffer and an early marker is long gone by the time a late one arrives - that one was written, run, and observed failing here before it was fixed. Signed-off-by: Javier Tia <javier@peridio.com>
… permit list CONFIG_ENV_WRITEABLE_LIST rode fit-verify.cfg, which every u-boot-imx board gets under verified-boot, while the permit list it depends on (CFG_ENV_FLAGS_LIST_STATIC, env-writeable-list.patch) and the compiled-in boot flow (env-compiled-in.cfg) exist for imx93-frdm only. On any other verified-boot board the symbol alone rejects every saved variable - avocado_boot_slot and the whole Avocado bootcmd included - and the board comes up on the stock bootcmd without booting Avocado. Move the symbol into its own fragment and add it, with the patch, under the same gate and the same machine scope. A board adopts the lockdown together with its own compiled-in env and permit list.
The capability check gates the verified-boot DISTRO_FEATURE on the machine's declaration; the FIT boot path this machine carries is exactly that capability.
/etc/fstab carried "/dev/root / auto defaults 1 1", so systemd-remount-fs
remounted / read-write on every boot. On a plain erofs that was a silent no-op;
on a dm-verity root the mapping is write-protected and the unit fails
("cannot remount /dev/root read-write"). The image is read-only by
construction, so say so, and drop the fsck pass.
complete.yml is built for every machine, so it cannot append the encrypted-var DISTRO_FEATURE outright: avocado-security-capabilities bb.fatals on a request the machine has not declared. Request it through the machine's own AVOCADO_SECURITY_CAPABILITIES instead - a board that declares encrypted-var ships LUKS2 /var (cryptsetup-var in the initramfs, /var from the mapping); a board that has not been migrated onto the declaration builds exactly as before. verified-boot is deliberately not here: it embeds the distro FIT key into U-Boot and enforces it, so a project's own FIT would not boot without replacing the bootloader. That stays a per-product choice via kas/feature/verified-boot.yml.
…hon tool The shell tool refused a fwup archive older than the deploy tree (an ordinary build stops at do_stone_bundle, so the archive silently carries the previous bootloader and kernel). The Python rewrite dropped that check. Restore it for the sd and ums paths, following symlinks on both sides, with --stale-ok as the explicit override.
This was referenced Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Collects the open i.MX work for wrynose into one branch, tested together on imx8mp-evk, so it lands in one rebase.
Included
verified-bootembeds and enforces the distro key); per-slot rootfs hash partitions with the root hash read from the FIT; recovery partition removed from every i.MX machine (stone json + fwuprootdisk.conf); SDK tools for signing a FIT and re-keying a prebuiltimx-boot;avocado-uboot-envfinds the boot device under verity and no longer removes/run/avocadoon failure;/mountedro./varencryption posture in the KV store.reencryptfor a grown/var.avocado-flash(ums, fuse-srk). The payload freshness guard from avocado-flash: refuse a payload older than the build it should carry #318 is ported into the Python tool (--stale-ok).ENV_WRITEABLE_LISTscoped to imx93 so imx8mp-evk keeps a writable env.kas/feature/complete.ymlrequestsencrypted-varonly where the machine declares it (AVOCADO_SECURITY_CAPABILITIES); undeclared boards build as before.Tested
imx8mp-evk, full stack on U-Boot 2026.04: unsigned and distro-signed FIT boot; tampered / unsigned FIT refused by a keyed U-Boot; project-keyed
imx-bootfrom eMMC boot1 refuses the distro FIT and boots the project FIT; extension dm-verity and rootfs dm-verity (/dev/mapper/root, corrupted block → EIO).bitbake -p/-g avocado-completeclean for imx8mp-evk and imx93-frdm.Not exercised here:
encrypted-varon imx8mp-evk (so #295/#321 are parse- and build-tested only), and the imx93/imx91 AHAB and env-lockdown paths (#314/#319, no imx93 board on this bench; hardware evidence is in those PRs). AHAB signing needs SPSDK and a PKI tree outside whatavocado-clidrives today.Not included
Other
u-boot-imxboards stay onbooti.avocado buildperforming theimx-bootre-keying itself. CAAM-sealed/varkeys.