Skip to content

imx: FIT verified boot, dm-verity, AHAB, encrypted-var posture — wrynose i.MX collection - #316

Merged
mobileoverlord merged 48 commits into
wrynosefrom
imx-fit-verity-capability
Aug 27, 2026
Merged

mobileoverlord merged 48 commits into
wrynosefrom
imx-fit-verity-capability

Conversation

@mobileoverlord

@mobileoverlord mobileoverlord commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Collects the open i.MX work for wrynose into one branch, tested together on imx8mp-evk, so it lands in one rebase.

Included

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-boot from 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-complete clean for imx8mp-evk and imx93-frdm.

Not exercised here: encrypted-var on 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 what avocado-cli drives today.

Not included

Other u-boot-imx boards stay on booti. avocado build performing the imx-boot re-keying itself. CAAM-sealed /var keys.

…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.
jetm added 3 commits August 26, 2026 20:31
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>
jetm and others added 24 commits August 26, 2026 20:31
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.
@mobileoverlord mobileoverlord changed the title imx: ship verity/FIT capability in every kernel and U-Boot, boot imx8mp-evk from a signed FIT imx: FIT verified boot, dm-verity, AHAB, encrypted-var posture — wrynose i.MX collection Aug 27, 2026
@mobileoverlord
mobileoverlord merged commit a5bb8a9 into wrynose Aug 27, 2026
2 checks passed
@mobileoverlord
mobileoverlord deleted the imx-fit-verity-capability branch August 27, 2026 00:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants