Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/test-configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,33 @@ jobs:
config-file: ./config/examples/imx95-m7.config
make-args: wolfboot.bin test-app/image_v1_signed.bin

imx95_a55_test:
uses: ./.github/workflows/test-build.yml
with:
arch: aarch64
# i.MX95 Cortex-A55 as BL33. The payload is a Linux FIT on the boot
# medium, so there is no test app and no contiguous factory.bin.
config-file: ./config/examples/imx95-a55.config
make-args: wolfboot.bin CROSS_COMPILE=aarch64-linux-gnu-

imx95_a55_stage1_emmc_test:
uses: ./.github/workflows/test-build.yml
with:
arch: aarch64
# The stage 1 that replaces U-Boot SPL, reading the containers from the
# eMMC boot partition. Nothing else compiles the AHAB parser or the
# OCRAM link, so this keeps both from rotting.
config-file: ./config/examples/imx95-a55.config
make-args: -C stage1 DISK_EMMC=1 DISK_SDCARD=0 loader_stage1.bin CROSS_COMPILE=aarch64-linux-gnu-

imx95_a55_stage1_sd_test:
uses: ./.github/workflows/test-build.yml
with:
arch: aarch64
# Same stage 1 against the SD controller rather than the eMMC one.
config-file: ./config/examples/imx95-a55.config
make-args: -C stage1 DISK_SDCARD=1 DISK_EMMC=0 loader_stage1.bin CROSS_COMPILE=aarch64-linux-gnu-

tegra234_test:
uses: ./.github/workflows/test-build.yml
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ tools/unit-tests/unit-va416x0-fram
tools/unit-tests/unit-wolfhsm_flash_hal
tools/unit-tests/unit-ubootenv
tools/unit-tests/unit-cm4-sdhci
tools/unit-tests/unit-imx95-ahab
tools/unit-tests/unit-cm4-rauc-slot
tools/unit-tests/__pycache__/*

Expand Down
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,12 @@ ifeq ($(TARGET),tegra234)
MAIN_TARGET:=wolfboot.bin test-app/image_v1_signed.bin
endif

# i.MX95 A55 is a RAM-resident BL33 loaded by SPL inside an AHAB container, so
# there is no contiguous flash image to assemble into a factory.bin.
ifeq ($(TARGET),imx95_a55)
MAIN_TARGET:=wolfboot.bin
endif

ifeq ($(TARGET),cm4)
MAIN_TARGET:=wolfboot.bin
endif
Expand Down
54 changes: 52 additions & 2 deletions arch.mk
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ ifeq ($(ARCH),AARCH64)
# Skip the bare-metal reset (boot_aarch64_start.S) and EL2/GIC glue.
OBJS+=src/boot_aarch64_efi.o
else
OBJS+=src/boot_aarch64.o src/boot_aarch64_start.o
OBJS+=src/boot_aarch64.o src/boot_aarch64_start.o src/boot_aarch64_cache.o
endif

ifeq ($(TARGET),zynq)
Expand Down Expand Up @@ -308,6 +308,56 @@ ifeq ($(ARCH),AARCH64)
endif
endif

ifeq ($(TARGET),imx95_a55)
# NXP i.MX95 Cortex-A55 cluster, running as BL33 in AHAB container 2.
# -mstrict-align because this stage runs with the MMU off, so every access
# is Device-nGnRnE and unaligned ones fault.
ARCH_FLAGS=-mcpu=cortex-a55+crypto -march=armv8.2-a+crypto -mstrict-align
CFLAGS+=$(ARCH_FLAGS) -DCORTEX_A55
LDFLAGS+=$(ARCH_FLAGS)
# BL31 loads and enters this image here; must match ORIGIN in
# hal/imx95_a55.ld and IMX95_BL33_BASE in hal/imx95_a55.h.
WOLFBOOT_ORIGIN=0x90200000
# Console, SCMI client and uSDHC driver all live in hal/imx95_a55.c, so none
# needs its own object; the switches below pick which paths compile in.
# Bring-up aid: print the entry EL, SCTLR and handoff x0 that BL31 passed.
ifeq ($(IMX95_HANDOFF_DUMP),1)
CFLAGS+=-DIMX95_HANDOFF_DUMP
endif
# i.MX95 is GICv3 and BL31 already configured it. EL2_HYPERVISOR is not a
# generic options.mk variable, so it is emitted here as tegra234 does.
ifeq ($(EL2_HYPERVISOR),1)
CFLAGS+=-DEL2_HYPERVISOR=1
endif
# i.MX uSDHC is not SDHCI-compatible: skip src/sdhci.c, use our driver.
ifneq ($(filter 1,$(DISK_SDCARD) $(DISK_EMMC)),)
DISK_DRIVER=usdhc
# SCMI cold-init of uSDHC2 (clock+pinmux) when no prior stage did it.
ifeq ($(IMX95_SCMI_COLD_INIT),1)
CFLAGS+=-DIMX95_SCMI_COLD_INIT
endif
endif
# SCMI M7 power-on + TCM ECC scrub (U-Boot's power_on_m7 equivalent) for a
# Cortex-M7 that Linux launches later. Independent of the disk driver.
ifeq ($(IMX95_INIT_M7),1)
CFLAGS+=-DIMX95_INIT_M7
endif
# One-shot diagnostic: read the AHAB container out of the eMMC boot
# partitions and report it, from inside an image that boots from SD.
ifeq ($(IMX95_EMMC_PROBE),1)
CFLAGS+=-DIMX95_EMMC_PROBE
OBJS+=hal/imx95_ahab.o
endif
# Mirror the console into a DDR ring that survives the handoff to Linux.
ifeq ($(IMX95_LOG_RING),1)
CFLAGS+=-DIMX95_LOG_RING
endif
# Verbose per-step trace of the SCMI cold-init and SD bring-up.
ifeq ($(IMX95_SCMI_DEBUG),1)
CFLAGS+=-DIMX95_SCMI_DEBUG
endif
endif

ifeq ($(TARGET),cm4)
# Raspberry Pi Compute Module 4 - Broadcom BCM2711, Cortex-A72
ARCH_FLAGS=-mcpu=cortex-a72+crypto -march=armv8-a+crypto -mtune=cortex-a72
Expand All @@ -334,7 +384,7 @@ ifeq ($(ARCH),AARCH64)
# asm (sp_arm64.c / WOLFSSL_SP_ARM64_ASM); that is enabled independently on
# __aarch64__ in include/user_settings.h, so a non-FIPS cm4 build still links
# sp_arm64 asm.
ifeq ($(filter zynq versal nxp_ls1028a,$(TARGET)),)
ifeq ($(filter zynq versal nxp_ls1028a imx95_a55,$(TARGET)),)
NO_ARM_ASM?=1
endif

Expand Down
100 changes: 100 additions & 0 deletions config/examples/imx95-a55.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# NXP i.MX95 Cortex-A55 - wolfBoot as BL33 (replaces U-Boot proper):
# ROM -> ELE -> SM -> OEI -> SPL [c1] -> BL31 -> OP-TEE -> wolfBoot -> Linux
# wolfBoot takes U-Boot's slot in AHAB container 2 at 0x90200000; the other
# container images are the vendor's, reused verbatim.
#
# `make stage1` from this same config also replaces SPL, and then the chain is
# ROM -> ELE -> SM -> OEI -> wolfBoot [c1] -> BL31 -> OP-TEE -> wolfBoot
# with stage 1 making the ELE call that authenticates container 2 that SPL used
# to make (IMX95_AHAB_AUTH, on by default). See docs/Targets.md.

ARCH?=AARCH64
TARGET?=imx95_a55

# ML-DSA-87, matching the imx95_m7 port. Header fits sig (4627) + pubkey.
SIGN?=ML_DSA
ML_DSA_LEVEL?=5
HASH?=SHA256
# Required: unset, ML_DSA_IMAGE_SIGNATURE_SIZE expands empty -> image.c errors.
IMAGE_SIGNATURE_SIZE?=4627
IMAGE_HEADER_SIZE?=12288

DEBUG?=0
DEBUG_SYMBOLS=1
DEBUG_UART?=1
# Print entry EL, SCTLR, MPIDR, CNTFRQ and the handoff x0 pointer.
IMX95_HANDOFF_DUMP?=1
# Report how long each boot step takes, in ms, from the generic timer.
BOOT_BENCHMARK?=1
# Also mirror the console into a DDR ring, so the verified-boot log can be read
# from Linux (and shown on a display) long after the UART output has scrolled.
IMX95_LOG_RING?=1

V?=0
SPMATH?=1
NO_ASM?=0

# RAM-resident: BL31 loads and enters this image, so there is nothing to copy
# from a stored .data and no XIP flash behind it.
NO_XIP?=1

# GICv3, configured by BL31; gicv2_init_secure() would abort on an unmapped
# ZynqMP address.
SKIP_GIC_INIT?=1

# Stay at EL2 (Linux wants it for KVM). Do NOT set BOOT_EL1: el2_to_el1_boot
# skips cache/MMU cleanup and would hand Linux stale DRAM.
EL2_HYPERVISOR=1

# No flash: boot/update addresses come from the HAL.
WOLFBOOT_NO_PARTITIONS=1

# Must match ORIGIN in hal/imx95_a55.ld and IMX95_BL33_BASE in hal/imx95_a55.h.
WOLFBOOT_ORIGIN=0x90200000

# RAM staging at 0xA1000000: clear of the M7/BL31/OP-TEE/ELE/vpu carveouts;
# hal_init() asserts this via imx95_check_load_ranges().
WOLFBOOT_LOAD_ADDRESS?=0xA1000000
WOLFBOOT_LOAD_DTS_ADDRESS?=0xA0800000

# Bounds the image before authentication; required without fixed partitions.
WOLFBOOT_RAMBOOT_MAX_SIZE=0x10000000

# Required for the test-app link even with WOLFBOOT_NO_PARTITIONS=1.
WOLFBOOT_SECTOR_SIZE?=0x4000
WOLFBOOT_PARTITION_SIZE?=0x4000000
WOLFBOOT_PARTITION_BOOT_ADDRESS?=0xA1000000

# Toolchain
USE_GCC=1
CROSS_COMPILE?=aarch64-none-elf-

# Storage: carrier SD on uSDHC2 (uSDHC driver in hal/imx95_a55.c; i.MX uSDHC, not SDHCI).
# Boot containers stay on eMMC.
DISK_SDCARD?=1
# Cold start: with no U-Boot ahead of wolfBoot the uSDHC2 clock and pinmux are
# still owned by the System Manager and must be requested over SCMI.
IMX95_SCMI_COLD_INIT?=1
# Power the Cortex-M7 mix and scrub its TCM, as U-Boot's board init does, for
# boards that launch an M7 image from Linux.
IMX95_INIT_M7?=1
# A/B slots by 0-based index into the parsed partition table.
CFLAGS_EXTRA+=-DBOOT_PART_A=1 -DBOOT_PART_B=2
CFLAGS_EXTRA+=-DDISK_BLOCK_SIZE=0x10000
# This module's ethernet MAC (OUI 00:14:2d, low bytes are the module serial),
# from its Toradex config block. The ENETC ports are PCIe-enumerated and carry
# no local-mac-address in the OS DTB, so without this Linux picks a random MAC
# each boot and the DHCP address floats. Replace it for a different module.
CFLAGS_EXTRA+=-DIMX95_ETH0_MAC=0x00142d888d20

# Linux FIT boot: kernel at 0xB2000000, ramdisk at 0xBA000000 - both above
# the staging window so the copies cannot overlap the staged FIT.
FIT_RAMDISK?=1
WOLFBOOT_LOAD_RAMDISK_ADDRESS?=0xBA000000
# OS cmdline plus serial console; ostree= pins the deployment. audit=0 because
# the audit subsystem interleaves records with anything else on this console.
CFLAGS_EXTRA+=-DLINUX_BOOTARGS='"earlycon console=ttyLP1,115200 audit=0 root=LABEL=otaroot rootfstype=ext4 logo.nologo ostree=/ostree/boot.1/torizon/d976a72bdc954405cfbe989bae393ad9da0b14b5dc0c1328c9b19e88201745a6/0"'

# --- Later phases (not yet implemented) -------------------------------------
#ELF?=1
#GZIP?=1
68 changes: 68 additions & 0 deletions docs/Targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ This README describes configuration of supported targets.
* [Nordic nRF54L15](#nordic-nrf54l15)
* [NXP i.MX 8QuadMax](#nxp-imx-8quadmax)
* [NXP i.MX95 Cortex-M7](#nxp-imx95-cortex-m7)
* [NXP i.MX95 Cortex-A55 (BL33)](#nxp-imx95-cortex-a55-bl33)
* [NXP iMX-RT](#nxp-imx-rt)
* [NXP i.MX RT700](#nxp-imx-rt700)
* [NXP Kinetis](#nxp-kinetis)
Expand Down Expand Up @@ -9902,6 +9903,73 @@ The difference between the two timestamps is the cost of everything wolfBoot doe

Both caches are enabled by `hal_init()`, which matters because verifying an image means hashing megabytes resident in DDR. The ARMv7-M default memory map marks `0x80000000-0x9FFFFFFF` as Normal write-through, so no MPU region is needed and M7 stores to the shared window still reach DDR; the HAL nevertheless cleans the affected lines explicitly so that behaviour is not left depending on an inherited attribute.

## NXP i.MX95 Cortex-A55 (BL33)

wolfBoot replaces U-Boot proper on the i.MX95's Cortex-A55 cluster: it is the third image in AHAB container 2 (after BL31 and OP-TEE), entered by BL31 at `0x90200000` in NS-EL2, where it verifies a Linux FIT (kernel + DTB + initramfs) with ML-DSA-87 and boots it at EL2. NXP also documents an OEM PQC SRK hybrid (ML-DSA) AHAB flow, so the container half of the chain can be post-quantum as well.

Which component authenticates the containers depends on which slot wolfBoot occupies, and the two cases are not the same:

- **wolfBoot as BL33 only** (this section). U-Boot SPL is still in container 0 and still calls the ELE to authenticate container 2 before BL31 runs, so the chain is ROM -> ELE -> SPL -> BL31 -> wolfBoot -> Linux once SRK fuses are programmed.
- **wolfBoot as stage 1 as well** (next section). SPL is gone, so wolfBoot performs that ELE call itself. `IMX95_AHAB_AUTH` is what does it, and it is on by default; the chain becomes ROM -> ELE -> wolfBoot stage 1 -> BL31 -> wolfBoot BL33 -> Linux.

In both cases the ROM and the ELE authenticate container 0 before any A55 code runs, so whichever image sits in the SPL slot is itself verified.

Validated on a Toradex SMARC iMX95 with `TARGET=imx95_a55` (`config/examples/imx95-a55.config`): full boot to Linux userspace with the FIT on the carrier SD (uSDHC2, the uSDHC driver in `hal/imx95_a55.c` - i.MX uSDHC, not SDHCI-compatible).

Notes:
- The FIT DTB gets `/chosen` bootargs, initrd properties and a `/memory` node from `hal_dts_fixup()`; deployment DTBs commonly ship without `/memory` (the bootloader is expected to add it) and the kernel hangs silently without one.
- The EL2 exit is a fused asm routine (flush, DAIF mask, TLB invalidate, jump) that touches no memory after the D-cache goes off; set/way cleaning does not reach the A55 cluster's DSU system cache, so payload ranges are also cleaned by VA.
- Use plain `earlycon` (DTB-derived); an explicit `earlycon=lpuart32,mmio32,<addr>` uses the wrong register layout on i.MX and silences all console output.
- uSDHC2's clock, pinmux and card power are owned by the M33 System Manager, and its pad registers data-abort on direct access from BL33. `IMX95_SCMI_COLD_INIT=1` brings them up over SCMI (the SCMI client in `hal/imx95_a55.c`), which is what lets wolfBoot boot from SD on a cold power-on rather than only after a stage that already initialized the controller.
- `IMX95_INIT_M7=1` powers the Cortex-M7 mix and scrubs its TCM for ECC, matching what U-Boot's board init does, for boards that launch an M7 image from Linux later.

### Stage 1: replacing U-Boot SPL

The boot device holds a set of AHAB containers. The first one is what the boot ROM reads: it carries the ELE firmware, the M33 System Manager, the OEI that trains DDR, and one A55 image loaded into OCRAM at `0x20480000`. That last image is U-Boot SPL, and its whole job is to find the next container and load BL31, OP-TEE and BL33 out of it. wolfBoot can take that slot instead:

```
cp config/examples/imx95-a55.config .config
make # BL33, loaded into DRAM by BL31
make stage1 DISK_EMMC=1 # stage1/loader_stage1.bin, the SPL slot
```

DDR is up before stage 1 runs, because the OEI did it, so stage 1 only needs the boot device and the container walk. Nothing records where the next container starts: it is derived by taking the end of the first container - the furthest of its header, its images and its signature block - and rounding up to 1 KiB. `hal/imx95_ahab.c` is that walk, and `hal/imx95_a55_stage1.c` is the rest: the watchdog, GPIO and SMMU state a warm reset out of Linux leaves behind, the SCMI calls for the console clock and the A55 performance level, and the ELE call that starts its random generator.

Two things differ from the BL33 build. The console is programmed rather than inherited, since nothing has configured LPUART1 yet. And on eMMC the containers live in a boot partition, not the user area: `imx95_emmc_boot_partition()` reads back the same `PARTITION_CONFIG` field that told the ROM which one to load from, and reads follow it.

#### What stage 1 authenticates

Taking SPL's slot means taking over the step SPL performed. The boot ROM and the ELE authenticate container 0, so stage 1 is verified code, but the container it goes on to load BL31, OP-TEE and BL33 out of is a separate one that nothing has checked yet. `IMX95_AHAB_AUTH=1` (the default) makes stage 1 ask the ELE to check it, the same three calls U-Boot's SPL makes:

1. The container's header, image table and signature block are staged into DDR at `0x90000000` - NXP's own `IMG_CONTAINER_BASE` for this SoC - because the ELE reads the address it is given over its own master port and requires DDR. Every value stage 1 then acts on comes from those staged bytes; the medium is not consulted for the table again.
2. `ELE_OEM_CNTN_AUTH_REQ` authenticates the header.
3. Each image is loaded to its destination and then checked in place with `ELE_VERIFY_IMAGE_REQ`, one at a time so a failure names the image. `ELE_RELEASE_CONTAINER_REQ` follows, before the jump.

Any failure stops the boot. `IMX95_AHAB_AUTH=0` builds without it, for bring-up on a part whose containers are not signed; it cannot be combined with `IMX95_STAGE1_PASSTHROUGH` or `IMX95_STAGE1_ALLOW_SELF_OVERLAP`, both of which would carry on past a refusal, and the build fails if you try.

Separately, and regardless of that flag, stage 1 bounds every destination *before* writing any of them. An image is streamed straight from the boot device into the address the container names, so a check made afterwards would come too late to matter: by then the bytes have landed. Destinations must lie inside DDR, must not overlap the staging window, this loader, the M7 carveout, the ELE shared-memory region or the VPU boot area, must not overlap each other once rounded up to the block size, and image 0 must be an A55 executable whose entry point lies inside its own bytes.

Two limits are worth stating plainly:

- **On an open part AHAB reports success without enforcing a signature.** That is why an unsigned container boots at all. What authentication buys there is an ELE-checked hash of each image against a table nobody signed - integrity, not authenticity. Build with `IMX95_AHAB_LIFECYCLE=1` to have stage 1 print the lifecycle, so the difference is visible rather than implied; it is off by default because nothing else on this platform reads the fuse shadow block from the slot SPL occupies. Enforcement begins when the SRK hash is fused; see the i.MX8QuadMax AHAB section above for what that costs and why it is irreversible.
- **Authenticating the container does not prevent rollback.** The offset of the next container is derived by walking container 0, which stage 1 reads off the medium and nothing re-authenticates, so a different but validly signed older container set would authenticate happily. That needs `sw_version`/`fuse_version` policy or the AHAB monotonic counter, neither of which is implemented here.

#### Stage 1 build options

Stage 1 is built by `make -C stage1` and takes its own options, separately from the BL33 build. Pass them on the command line.

| Option | Default | Effect |
|---|---|---|
| `IMX95_AHAB_AUTH` | `1` | Ask the ELE to authenticate the container set stage 1 loads BL31, OP-TEE and BL33 from, as described above. Setting it to `0` builds without those calls, for bring-up on a part whose containers are not signed. |
| `IMX95_AHAB_LIFECYCLE` | `0` | Read and print the part's lifecycle from the fuse shadow block. Off by default: U-Boot reads that block only from U-Boot proper and only under `CONFIG_AHAB_BOOT`, so whether it answers the A55 this early is untested per part, and a diagnostic must not be able to cost a boot. |
| `IMX95_STAGE1_PASSTHROUGH` | unset | Bring-up only. Enter BL31 even when the container load failed, for a board where some other loader has already staged the whole set and stage 1 only needs to be invisible when it cannot help. Refused at compile time together with `IMX95_AHAB_AUTH`, because returning an error is then indistinguishable from the jump. |
| `IMX95_STAGE1_ALLOW_SELF_OVERLAP` | unset | Bring-up only. Skip an image whose destination lands on the running loader instead of refusing the boot, which a stage 1 linked into DRAM needs in order to skip its own entry. Refused at compile time together with `IMX95_AHAB_AUTH`. |
| `IMX95_LOG_RING` | `0` | Mirror the console into a DDR ring that survives the handoff, so stage 1's output can be read back from Linux with `memtool con`. Stage 1 starts the ring and BL33 appends to it, so one address holds both. |
| `IMX95_LOG_RING_BASE` | `0x80F20000` | Where that ring lives. Change it only to move the ring out of the way of something else; stage 1 and BL33 must agree on it. |
| `IMX95_EMMC_PROBE` | `0` | One-shot diagnostic that walks the eMMC boot partitions and reports the AHAB containers it finds, from an image booting off SD. |

The two bring-up options exist to get a new board talking; neither belongs in a shipped configuration, and the build refuses to combine either with authentication rather than letting a boot continue past a refusal.

## TI C2000 C28x (LAUNCHXL-F28P55X)

wolfBoot runs on the Texas Instruments C2000 C28x DSP (TMS320F28P550SJ, 150 MHz) as a secure execute-in-place (XIP) bootloader. The C28x is word-addressed with `CHAR_BIT == 16` (no 8-bit type -- each octet occupies one 16-bit cell), built with the TI `cl2000` toolchain against wolfSSL's wide-byte (`CHAR_BIT != 8`) support.
Expand Down
Loading
Loading