Skip to content

imx93-frdm: close the offline U-Boot environment bypass - #319

Closed
jetm wants to merge 7 commits into
wrynosefrom
uboot-env-lockdown-imx93
Closed

jetm wants to merge 7 commits into
wrynosefrom
uboot-env-lockdown-imx93

Conversation

@jetm

@jetm jetm commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Closes the offline environment bypass on avocado-imx93-frdm: with FIT
signature verification on, a saved U-Boot environment could still redirect the
boot, so the signature guarded the payload while the thing choosing the payload
stayed writable by anyone who could reach the card.

The bypass, and why the obvious fix was not enough

CONFIG_ENV_WRITEABLE_LIST plus a permit list is the mechanism. But turning it
on alone would have bricked the board, and that is worth stating because it was
found by reading rather than by flashing.

This machine's entire boot flow reached U-Boot only as the saved
environment - u-boot-env.inc runs mkenvimage over env/${MACHINE}.txt and fwup
writes the result. CONFIG_ENV_WRITEABLE_LIST rejects every H_EXTERNAL
variable missing from the list (env/flags.c:561-563), and the saved copy is
imported H_EXTERNAL (env/mmc.c:547). All 13 flow variables would have been
dropped while the compiled-in default supplied 2, leaving the board on NXP's
stock bootcmd.

So the flow is now compiled in via CONFIG_ENV_SOURCE_FILE. Compiled-in
variables arrive H_DEFAULT, which env_flags_validate returns early for, so
the boot flow is immune by construction rather than by being listed - listing
bootcmd would have reopened the hole it closes.

env/${MACHINE}.txt deliberately keeps its own full copy. env_load() seeds the
built-in default before the storage driver only under
CONFIG_ENV_WRITEABLE_LIST; with the feature off the table is built from the
saved environment alone, so a reduced .txt would leave those builds with no
bootcmd. There is a debt marker on the duplication.

The permit list

avocado_boot_slot, avocado_device_id, avocado_device_cert, avocado_device_key,
peridio_vsn_current, a.avocado_platform, a.avocado_architecture,
b.avocado_platform, b.avocado_architecture   (all :sw)
devnum, mmcblk                               (:dw)

devnum and mmcblk are :dw, not :sw, and that distinction is the security
content. _env_flags_validate_type's string case is a bare break, so :sw
accepts anything: mmcblk="1p6 init=/bin/sh x" flows into rootdev, then into
bootargs, which the FIT signature does not cover - a root shell on a verified
kernel. devnum the same way, into load argument injection. Decimal rejects
trailing non-digits and still admits 0 and 1.

u-boot-imx stays on 2026.04 here

093f504 pinned PREFERRED_VERSION_u-boot-imx = "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. Right default; wrong answer for this board, which turns both off at source
already. Overridden back per-machine, after the require so it actually wins.

2026.04 is a requirement rather than a preference here:
CONFIG_ENV_SOURCE_FILE resolves against board/$(SYS_VENDOR)/$(SYS_BOARD) -
board/nxp/imx93_frdm on 2026.04, board/freescale/imx93_frdm on 2025.04. The
bbappend refuses to build on the older layout rather than emit a bootloader with
no Avocado boot flow in it. The downgrade is independently destructive:
CONFIG_SYS_BOOTM_LEN halves from 0x8000000 to 0x4000000, and one flash
under the old pin produced a board that ran SPL and never reached U-Boot.

Autoboot is interruptible by any key

The series originally added CONFIG_AUTOBOOT_KEYED with a stop string. It is
removed - as an ergonomics decision, not a defect report. An earlier
revision of this PR argued the stop string was broken; that was wrong and is
corrected here.

A day went into "the stop string 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. 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, and
passwd_abort_key() - which drains one character per udelay(10000) - never
saw a clean sequence. Throttled to ~8 chars/sec, an unkeyed keypress broke in on
the first try with nothing about the board or the Kconfig changed.

So the trade is convenience against unattended safety. Convenience wins for now
because 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 cost is accepted knowingly and carries a debt marker: unkeyed, this board's
serial line can halt an unattended boot on stray bytes, which is fine for a
bench unit with a human watching and is not fine for a HIL runner. Re-enabling
the keying then needs no new investigation - the string works, and what defeated
it is now understood.

Verified on hardware

Flashed and booted on FRDM-IMX93. Confirmed on the board:

  • Saved bootcmd is refused: env set bootcmd ... + saveenv accepted and
    written, and after reset the compiled-in command still runs.
  • Saved avocado_boot_slot is still honoured, so A/B switching survives.
  • FIT signature enforcement intact: Verifying Hash Integrity ... sha256,rsa2048:FIT+ OK.
  • booti is present but unreachable from the saved environment.
  • Autoboot now prints Hit any key to stop autoboot: 2 and any key reaches the
    prompt.

do_configure selects 2026.04 under the layer-wide 2025.04 pin, confirmed by
which work directory it touched.

A/B upgrade verified

The -t upgrade cycle has now been run on hardware, over UMS from the U-Boot
prompt:

  • fwup: Upgrading boot slot B - correctly selected upgrade.b from the
    running slot A.
  • Slot stamps landed, read back from both halves of the redundant
    environment (CONFIG_ENV_OFFSET=0x400000, CONFIG_ENV_OFFSET_REDUND=0x440000),
    with the live copy identified by the higher flags byte (primary 15 >
    redundant 14).
primary (live) redundant (stale)
avocado_boot_slot b b
b.avocado_platform present present
b.avocado_architecture present absent

Both b. stamps were undefined before the upgrade. The asymmetry is why both
halves have to be read: the stale copy alone reads as a partial write.

  • Rebooted into Booting B, Verifying Hash Integrity ... sha256,rsa2048:FIT+ OK
    three times, and reached userspace - so the slot switch, the signature
    enforcement and the upgraded slot all hold together.

@jetm
jetm force-pushed the uboot-env-lockdown-imx93 branch from 9a7271f to 6d736e6 Compare August 26, 2026 17:42
jetm added 7 commits August 26, 2026 14:56
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>
@jetm
jetm force-pushed the uboot-env-lockdown-imx93 branch from bcbafde to 86d5a7b Compare August 26, 2026 20:56
@mobileoverlord

Copy link
Copy Markdown
Contributor

Stacked this locally with #316 (FIT boot on imx8mp-evk) to test the i.MX branches together; both parse and imx8mp-evk boots the combined tree. One interaction to fix here before merge:

CONFIG_ENV_WRITEABLE_LIST=y sits in fit-verify.cfg, which every u-boot-imx board gets under verified-boot, while the permit list (env-writeable-list.patch → imx93_frdm.h) and the compiled-in boot flow (env-compiled-in.cfg) exist for imx93-frdm only. On imx8mp-evk with verified-boot the symbol alone rejects every saved variable — avocado_boot_slot and the Avocado bootcmd included — and the board falls back to the stock bootcmd. Your own comment in fit-verify.cfg describes exactly this failure for the no-list case.

What I applied locally: move the symbol into env-writeable-list.cfg and add it with the patch under the same gate and the same machine scope:

        d.appendVar('SRC_URI', ' file://fit-verify.cfg')
        if d.getVar('MACHINE') == 'avocado-imx93-frdm':
            d.appendVar('SRC_URI', ' file://env-writeable-list.cfg file://env-writeable-list.patch')

A board then adopts the lockdown together with its compiled-in env and permit list. Also 02bc0310e2 (keep u-boot-imx on 2026.04 against the layer pin) is redundant now that #320 removed the pin.

@mobileoverlord

Copy link
Copy Markdown
Contributor

Merged into wrynose via #316 (rebased, commits preserved). Closing as included.

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