cm4: hardware-validate eMMC A/B disk boot and FIPS on CM4 (arm64 header, 0x200000 load, mini-UART console)

pull/863/head
David Garske 2026-07-28 16:48:27 -07:00 committed by Daniele Lacamera
parent 71b855f7b5
commit 9a4f800546
13 changed files with 537 additions and 132 deletions

5
.gitignore vendored
View File

@ -509,3 +509,8 @@ wolfboot-sbom-srcs.txt
# Python cache files
__pycache__/
*.py[cod]
# CM4 disk-boot test artifacts (fetched RPi firmware, built/signed payloads)
tools/scripts/cm4/fw/
tools/scripts/cm4/*.elf
tools/scripts/cm4/*.bin

16
arch.mk
View File

@ -222,15 +222,19 @@ ifeq ($(ARCH),AARCH64)
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
# -mstrict-align: wolfBoot runs with the MMU off (simple startup), so data
# accesses are Device memory where unaligned access faults. Required for the
# FIPS in-core HMAC over the code region (NO_ARM_ASM drops the asm path that
# otherwise sets this).
# -mstrict-align: the plain RAM-boot config runs with the MMU off (simple
# startup), where all memory is Device-nGnRnE and unaligned access faults.
# The FIPS / disk configs bring up an identity MMU first (CM4_USE_MMU in
# hal/cm4.c). cm4 defaults to NO_ARM_ASM=1 (portable C, no NEON structure
# loads), so -mstrict-align keeps every config safe either way.
CFLAGS+=$(ARCH_FLAGS) -DCORTEX_A72 -mstrict-align
endif
# Default ARM ASM setting for unrecognized AARCH64 targets
ifeq ($(filter zynq versal nxp_ls1028a cm4,$(TARGET)),)
# Default ARM ASM setting for unrecognized AARCH64 targets. cm4 is excluded
# from the asm path (defaults NO_ARM_ASM=1): the plain config runs MMU-off
# where NEON multi-register loads would fault, and the FIPS path mandates
# portable-C crypto anyway.
ifeq ($(filter zynq versal nxp_ls1028a,$(TARGET)),)
NO_ARM_ASM?=1
endif

View File

@ -0,0 +1,54 @@
# Raspberry Pi CM4 (BCM2711) - onboard eMMC A/B disk boot.
#
# wolfBoot drives the BCM2711 EMMC2 controller (Arasan SDHCI v3.0 @ 0xFE340000)
# to read GPT A/B image partitions from the onboard eMMC via update_disk.c, and
# boots the highest valid version with rollback.
#
# Transfer mode: SDHCI_SDMA_DISABLED forces PIO. The BCM2711 EMMC2 SDMA
# boundary-restart and cache coherency are not yet validated (same Arasan
# family as the Versal quirk); DISK_EMMC compiles in the PIO BRR-race
# workaround in src/sdhci.c. SDHCI_FORCE_CARD_DETECT: the onboard eMMC has no
# routed card-detect line.
#
# Bring-up: uncomment DEBUG_SDHCI (+ DEBUG_DISK / DEBUG_GPT) for verbose
# controller/partition tracing over the mini-UART console.
#
# For wolfCrypt FIPS 140-3, build with FIPS=1 pointing at a FIPS / FIPS-ready
# wolfSSL tree (NO_ARM_ASM required; entropy from the RNG200 TRNG). See
# docs/FIPS.md. e.g.:
# make FIPS=1 WOLFBOOT_LIB_WOLFSSL=../wolfssl-5.9.2-gplv3-fips-ready NO_ARM_ASM=1
#FIPS?=1
#WOLFBOOT_LIB_WOLFSSL?=../wolfssl-5.9.2-gplv3-fips-ready
#NO_ARM_ASM?=1
ARCH?=AARCH64
TARGET?=cm4
SIGN?=ECC384
HASH?=SHA384
IMAGE_HEADER_SIZE?=1024
DEBUG?=0
DEBUG_UART?=1
DISK_SDCARD?=0
DISK_EMMC?=1
CFLAGS_EXTRA+=-DSDHCI_FORCE_CARD_DETECT
CFLAGS_EXTRA+=-DSDHCI_SDMA_DISABLED
#CFLAGS_EXTRA+=-DDEBUG_SDHCI
#CFLAGS_EXTRA+=-DDEBUG_DISK
#CFLAGS_EXTRA+=-DDEBUG_GPT
EXT_FLASH?=0
NO_XIP=1
NO_QNX?=1
ELF?=1
VTOR?=1
SPMATH?=1
PKA?=0
WOLFTPM?=0
WOLFBOOT_NO_PARTITIONS=1
CFLAGS_EXTRA+=-DBOOT_PART_A=1
CFLAGS_EXTRA+=-DBOOT_PART_B=2
CFLAGS_EXTRA+=-DDISK_BLOCK_SIZE=0x80000
WOLFBOOT_LOAD_ADDRESS?=0x10000000
WOLFBOOT_RAMBOOT_MAX_SIZE=0x2BC00000
WOLFBOOT_LOAD_DTS_ADDRESS?=0x08000000
WOLFBOOT_PARTITION_BOOT_ADDRESS=0x80200000
WOLFBOOT_PARTITION_SIZE=0x4000000
WOLFBOOT_SECTOR_SIZE=0x1000

View File

@ -5,10 +5,10 @@
# reads GPT A/B image partitions via update_disk.c and boots the highest valid
# version with rollback.
#
# NOTE: the EMMC2 platform glue in hal/cm4.c is bring-up scaffolding pending
# on-hardware validation (controller clock/pinmux state left by the GPU
# firmware, card-detect wiring on the carrier). Use SDHCI_FORCE_CARD_DETECT
# for the embedded eMMC path.
# The EMMC2 glue in hal/cm4.c is hardware-validated on the onboard eMMC
# (see cm4_emmc.config). The microSD path uses the same driver but needs a CM4
# Lite - an eMMC module disables the carrier's microSD slot. Use
# SDHCI_FORCE_CARD_DETECT for media with no routed card-detect line.
#
# For wolfCrypt FIPS 140-3, build with FIPS=1 pointing at a FIPS / FIPS-ready
# wolfSSL tree. The FIPS module uses portable-C crypto, so NO_ARM_ASM is
@ -28,6 +28,10 @@ DEBUG_UART?=1
DISK_SDCARD?=1
DISK_EMMC?=0
CFLAGS_EXTRA+=-DSDHCI_FORCE_CARD_DETECT
# Force PIO: the BCM2711 EMMC2 SDMA path is unvalidated and hal/cm4.c provides no
# DMA cache-maintenance hooks, while the disk configs run with the D-cache on
# (CM4_USE_MMU), so SDMA could hash/verify stale DDR. Matches cm4_emmc.config.
CFLAGS_EXTRA+=-DSDHCI_SDMA_DISABLED
EXT_FLASH?=0
NO_XIP=1
NO_QNX?=1
@ -42,7 +46,7 @@ CFLAGS_EXTRA+=-DBOOT_PART_B=2
CFLAGS_EXTRA+=-DDISK_BLOCK_SIZE=0x80000
WOLFBOOT_LOAD_ADDRESS?=0x10000000
WOLFBOOT_RAMBOOT_MAX_SIZE=0x2BC00000
WOLFBOOT_LOAD_DTS_ADDRESS?=0x1000
WOLFBOOT_LOAD_DTS_ADDRESS?=0x08000000
WOLFBOOT_PARTITION_BOOT_ADDRESS=0x80200000
WOLFBOOT_PARTITION_SIZE=0x4000000
WOLFBOOT_SECTOR_SIZE=0x1000

View File

@ -45,7 +45,7 @@ make FIPS=1 WOLFBOOT_LIB_WOLFSSL=/path/to/wolfssl-5.9.2-gplv3-fips-ready \
SIGN=ECC384 HASH=SHA384 SPMATH=1
```
`-DHAVE_FIPS` is added by the `FIPS=1` block. `FIPS=1` also defaults `FIPS_READY=1`, which defines `WOLFSSL_FIPS_READY` (`include/user_settings.h`); that macro forces the evaluation bundle's `HAVE_FIPS_VERSION` to 7 and selects the FIPS-186-4 gating in `settings.h`. A production build with the licensed **validated** bundle (not a FIPS-ready drop) must pass `FIPS_READY=0` so the bundle keeps its own declared module version. The `HAVE_FIPS` block in `include/user_settings.h` also enables the module's algorithm set, keeps the RNG/DRBG enabled, and wires the entropy seed (below).
`-DHAVE_FIPS` is added by the `FIPS=1` block. `FIPS=1` also defaults `FIPS_READY=1`, which defines `WOLFSSL_FIPS_READY` (`include/user_settings.h`); that macro forces the evaluation bundle's `HAVE_FIPS_VERSION` to 7 and selects the FIPS-186-4 gating in `settings.h`. `FIPS_READY=1` (the default) builds the evaluation FIPS-ready bundle. A production build with the licensed **validated** bundle (not a FIPS-ready drop) must pass both `FIPS_READY=0` and `FIPS_VERSION=<n>` (for example `FIPS_VERSION=7` for a 140-3 module), which `options.mk` turns into `-DHAVE_FIPS_VERSION=<n>` to pin the module version. This is required because under wolfBoot's `-DWOLFSSL_USER_SETTINGS` build `settings.h` includes only `user_settings.h` and never the configure-generated `wolfssl/options.h`, so the validated bundle does not self-declare `HAVE_FIPS_VERSION`; without it the module would silently build as FIPS v1 (140-2). `options.mk` therefore errors if `FIPS_READY=0` is passed without `FIPS_VERSION`. The `HAVE_FIPS` block in `include/user_settings.h` also enables the module's algorithm set, keeps the RNG/DRBG enabled, and wires the entropy seed (below).
## Entropy source (required)
@ -55,12 +55,17 @@ The FIPS DRBG needs a seed. wolfBoot's lean configuration compiles out the OS se
The module verifies an in-core integrity hash (HMAC-SHA-256 over the module's code and read-only data) at startup. A fresh build ships with a placeholder, so the first run reports a mismatch; capture the runtime hash and seal it:
1. Build and run with a FIPS callback registered (wolfBoot does this in `src/loader.c`). On a mismatch the module reports the runtime hash; wolfBoot prints it (`FIPS in-core hash = ...`, from `wolfCrypt_GetCoreHash_fips()`), and on the CM4 the test app (`test-app/app_cm4.c`) prints it over UART.
1. Build and run with a FIPS callback registered (wolfBoot does this in `src/loader.c`). On a mismatch the module reports the runtime hash; wolfBoot prints it (`FIPS in-core hash = ...`, from `wolfCrypt_GetCoreHash_fips()`) before halting.
2. Copy the reported 64-hex-character hash into `verifyCore[]` in `wolfcrypt/src/fips_test.c`.
3. Rebuild and re-run. `wolfCrypt_GetStatus_fips()` now returns 0 (operational).
The seal is **specific to the exact binary layout**: any code change that shifts the FIPS module's link addresses changes the in-core hash and requires a re-seal. Re-sealing `verifyCore[]` itself does not shift addresses (same-size rewrite), so once the rest of the build is fixed the seal converges in one pass.
Two practical traps when re-sealing (both cost time on the CM4 bring-up):
- `verifyCore[]` can be sealed via a build define instead of editing the FIPS tree: `CFLAGS_EXTRA="-DWOLFCRYPT_FIPS_CORE_HASH_VALUE=<hash>"` (unquoted; `fips_test.c` stringifies it). But apply it by recompiling **only** `fips_test.o` - `rm "$WOLFBOOT_LIB_WOLFSSL/wolfcrypt/src/fips_test.o"` then rebuild. `verifyCore[]` lives *after* `wolfCrypt_FIPS_last`, so this leaves the hashed region byte-identical and converges in one pass. Passing the define through a **full** rebuild (`make clean` + build) recompiles the whole module and shifts its link addresses, so the hash never stabilizes.
- `make clean` removes `$(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/*.o`. If you build with `WOLFBOOT_LIB_WOLFSSL=<fips-tree>` but run `make clean` **without** that variable, it cleans the default `lib/wolfssl` instead, leaving the stale FIPS `fips_test.o` in place - the new seal silently never lands. Pass `WOLFBOOT_LIB_WOLFSSL` to `clean` too, or `rm` the object directly.
## Bare-metal targets
The FIPS module targets a hosted environment; a few things must be provided on bare-metal (the CM4 does all of these):

View File

@ -3815,14 +3815,14 @@ On CM4 modules with onboard eMMC the boot files live on the eMMC FAT boot partit
```
cp config/examples/cm4.config .config
make CROSS_COMPILE=aarch64-none-elf-
make CROSS_COMPILE=aarch64-none-elf- DEBUG_UART=1
```
The example uses `SIGN=ECC384 HASH=SHA384` (both FIPS-approved). wolfBoot is entered by the firmware at `0x80000` at EL2, matching `hal/cm4.ld`. The image is loaded from RAM: wolfBoot reads the signed application at `kernel_addr` (`0x140000`), verifies it, copies it to `WOLFBOOT_LOAD_ADDRESS`, and boots.
`cm4.config` defaults `DEBUG?=0` and `DEBUG_UART?=0` for a silent, optimized release build; the `DEBUG_UART=1` above enables the boot log shown under "Boot output". The example uses `SIGN=ECC384 HASH=SHA384` (both FIPS-approved). wolfBoot is built as an AArch64 Linux kernel image (`kernel8.img`): `src/boot_aarch64_start.S` prepends the 64-byte ARM64 image header (`"ARM\x64"` magic), and `hal/cm4.ld` links at `0x200000`. The VideoCore firmware only transfers control to a 64-bit kernel that carries this header, and it runs the image in place at the 2 MB-aligned load address `0x200000` (it does not relocate a header image down to `0x80000`) at EL2. The image is loaded from RAM: wolfBoot reads the signed application at `kernel_addr` (`0x2C0000`), verifies it, copies it to `WOLFBOOT_LOAD_ADDRESS`, and boots.
### Signing and assembling the boot image
Sign the application, then concatenate wolfBoot and the signed image so the signed image lands at `kernel_addr` (`0x140000` = `0x80000` load + `0xC0000`):
Sign the application, then concatenate wolfBoot and the signed image so the signed image lands at `kernel_addr` (`0x2C0000` = `0x200000` load + `0xC0000`):
```
make keytools tools/bin-assemble/bin-assemble
@ -3835,11 +3835,13 @@ tools/bin-assemble/bin-assemble kernel8.img \
### config.txt
The CM4 UART on GPIO14/15 defaults to the mini-UART because the PL011 is used by Bluetooth. wolfBoot drives the PL011, so route it to the header and fix a known UART clock:
The debug console on GPIO14/15 is the BCM2711 mini-UART (AUX, Linux `ttyS0`); the PL011 is used by Bluetooth. wolfBoot drives the **mini-UART** by default (it inherits the firmware's stable baud, which `enable_uart=1` fixes by pinning `core_freq`), so no baud reprogramming is needed. Boards where `dtoverlay=disable-bt` actually routes the PL011 onto GPIO14/15 can build with `CFLAGS_EXTRA=-DCM4_UART_PL011` to use the PL011 instead.
```
arm_64bit=1
kernel=kernel8.img
# wolfBoot is linked absolutely at 0x200000; pin the load address to match.
kernel_address=0x200000
enable_uart=1
uart_2ndstage=1
dtoverlay=disable-bt
@ -3847,6 +3849,8 @@ init_uart_clock=48000000
init_uart_baud=115200
```
`kernel_address=0x200000` is required: wolfBoot is linked absolutely at `0x200000`, and its startup code self-checks the runtime base against that link-time base. If the firmware loads the image at any other address the check fails and wolfBoot halts silently, before any UART output, so a missing or mismatched `kernel_address` looks like a dead board with no console log. All the `prepare_emmc*.sh` scripts emit this line.
### Flashing
- CM4 Lite: write `kernel8.img` + the RPi firmware (`start4.elf`, `fixup4.dat`) + `config.txt` to the microSD FAT boot partition.
@ -3858,20 +3862,43 @@ With `DEBUG_UART=1`, a successful authenticated boot prints (115200 8N1):
```
wolfBoot CM4 (BCM2711 Cortex-A72) hal_init, EL2
Trying partition 0 at 0x140000
Trying partition 0 at 0x2C0000
Checking integrity...done
Verifying signature...done
Firmware Valid
Booting at 0x3080000
```
### Optional: eMMC/SD A/B updates
### Optional: eMMC/SD A/B disk boot
`config/examples/cm4_sdcard.config` enables the disk updater (`DISK_SDCARD`/`DISK_EMMC`), driving the BCM2711 EMMC2 controller through the generic SDHCI driver (`src/sdhci.c`) for A/B image partitions on the boot medium. This path is provided as bring-up scaffolding and is not yet hardware-validated.
`config/examples/cm4_emmc.config` (onboard eMMC) and `config/examples/cm4_sdcard.config` (microSD) enable the disk updater (`DISK_EMMC`/`DISK_SDCARD`), driving the BCM2711 EMMC2 controller through the generic SDHCI driver (`src/sdhci.c` + the `hal/cm4.c` register glue) to read A/B signed images from GPT partitions. wolfBoot reads the GPT, selects the higher-version image, verifies it, ELF-loads it (`ELF=1`) to `WOLFBOOT_LOAD_ADDRESS`, and boots.
The **eMMC** path (`cm4_emmc.config`) has been validated end to end on CM4 hardware: SDHCI/eMMC card init -> GPT parse -> A/B version select -> SHA-384 integrity -> ECDSA-P384 signature verify -> ELF64 load -> boot of a signed payload. `tools/scripts/cm4/prepare_emmc.sh` builds the GPT layout (FAT boot partition with `kernel8.img` + firmware, plus raw A/B image partitions), signs a minimal test payload (`tools/scripts/cm4/disk_app.S`), and writes it to the eMMC over `rpiboot`. Uncomment `DEBUG_SDHCI` / `DEBUG_DISK` / `DEBUG_GPT` in the config for verbose bring-up tracing. The **microSD** path shares the same driver but is validated only on modules whose SD lines reach the microSD slot (a CM4 with onboard eMMC disables that slot).
### FIPS 140-3
The CM4 target uses `SIGN=ECC384 HASH=SHA384` (FIPS-approved) and can perform its signature verification with the wolfCrypt FIPS 140-3 module (build `config/examples/cm4.config` with `FIPS=1`, pointing `WOLFBOOT_LIB_WOLFSSL` at a FIPS wolfSSL tree). At boot the module runs its power-on self-test and in-core integrity check, and wolfBoot refuses to boot unless the module is operational. Entropy for the FIPS DRBG comes from the BCM2711 RNG200 hardware TRNG. The full flow has been brought up and exercised end to end on CM4 hardware using the FIPS-ready bundle: FIPS module operational -> SHA-384 integrity -> ECDSA-P384 signature verification -> boot handoff. A production, CMVP-validated deployment additionally requires the licensed validated wolfCrypt FIPS bundle at the validated revision (see [FIPS.md](FIPS.md)). The on-target harness in `test-app/app_cm4.c` prints the runtime in-core hash over the UART for the `verifyCore[]` seal. See [FIPS.md](FIPS.md) for the full build, entropy, and hash-sealing procedure.
The CM4 target uses `SIGN=ECC384 HASH=SHA384` (FIPS-approved) and can perform its signature verification with the wolfCrypt FIPS 140-3 module (build `config/examples/cm4.config` with `FIPS=1`, pointing `WOLFBOOT_LIB_WOLFSSL` at a FIPS wolfSSL tree). At boot the module runs its power-on self-test and in-core integrity check, and wolfBoot refuses to boot unless the module is operational. Entropy for the FIPS DRBG comes from the BCM2711 RNG200 hardware TRNG. The FIPS configuration builds with the CM4 hardware-boot support (ARM64 image header, `0x200000` load address, mini-UART console) and has been validated end to end on CM4 hardware with the FIPS-ready bundle: after sealing the in-core integrity hash, wolfBoot reports `FIPS 140-3 module operational` and the module gates the boot with SHA-384 integrity and ECDSA-P384 signature verification of the eMMC A/B image (`cm4_emmc.config` with `FIPS=1`; wolfBoot's `src/loader.c` runs the power-on self-test and in-core check before booting). A production, CMVP-validated deployment additionally requires the licensed validated wolfCrypt FIPS bundle at the validated revision (see [FIPS.md](FIPS.md)). On an in-core hash mismatch, wolfBoot prints the runtime hash (`FIPS in-core hash = ...`, from `src/loader.c`) to seal into `verifyCore[]`. Re-seal by recompiling only `fips_test.o` with `-DWOLFCRYPT_FIPS_CORE_HASH_VALUE=<hash>` (a full rebuild shifts the module boundary and the hash); see [FIPS.md](FIPS.md) for the full build, entropy, and hash-sealing procedure.
### Optional: Linux kernel FIT boot
`config/examples/cm4_emmc_linux.config` boots a real Linux kernel instead of the `disk_app` prove-out stub. wolfBoot loads a wolfBoot-signed FIT (kernel-only, gzip-compressed) from an eMMC GPT partition, verifies the outer ECDSA-P384/SHA-384 signature, decompresses the kernel to `0x10000000`, relocates the RPi-firmware-provided DTB to `WOLFBOOT_LOAD_DTS_ADDRESS` (`0x08000000`) and injects the kernel command line (`root=`, `console=`) into `/chosen/bootargs`, then boots Linux at EL2.
Key config points: `GZIP=1` (the FIT kernel subimage is `Image.gz`), `ELF=1`, `DISK_EMMC=1`, and `WOLFBOOT_LOAD_ADDRESS=0x18000000` - the FIT is staged above the decompressed kernel so gunzip does not overwrite its own compressed input mid-stream. `CFLAGS_EXTRA+=-DCM4_FIRMWARE_DTB` captures and reuses the firmware DTB (which already carries the RAM size and mini-UART clock), `CFLAGS_EXTRA+=-DCM4_UART_PL011` puts the Linux console on the PL011 (`ttyAMA0`, via `dtoverlay=disable-bt`), and `CFLAGS_EXTRA+=-DLINUX_BOOTARGS_ROOT=...` sets `root=`. The FIT is built from `hal/cm4.its` with `mkimage` and signed with the wolfBoot key; `tools/scripts/cm4/prepare_emmc_linux.sh` stages it on the eMMC. This path was hardware-validated booting a Yocto (Scarthgap, kernel 6.6) rootfs.
### Optional: RAUC A/B redundant boot
`config/examples/cm4_emmc_rauc.config` makes wolfBoot replace U-Boot as the RAUC slot arbiter. wolfBoot reads a raw U-Boot-environment partition (`mkenvimage`/`fw_setenv` compatible), runs the RAUC `BOOT_ORDER` / `BOOT_<slot>_LEFT` try-counter state machine, decrements the selected slot's counter and writes it back (so a hung slot fails over to the other on the next boot), then boots the shared signed kernel FIT with `root=` pointing at the active slot's rootfs and `rauc.slot=<name>` on the command line.
The eMMC uses a 6-partition layout:
- `p1` boot FAT: RPi firmware + `kernel8.img` + `config.txt`
- `p2` uboot-env raw: RAUC `fw_env.config` target; its size must match wolfBoot's `UBOOT_ENV_SIZE` (`0x4000`)
- `p3` fitImage raw: shared wolfBoot-signed kernel FIT
- `p4` rootfs_A ext4: slot A
- `p5` rootfs_B ext4: slot B
- `p6` data ext4: persistent data
Key config: `CM4_RAUC_AB=1` (a make var that pulls in `src/ubootenv.o` and the RAUC branch of `hal/cm4.c`), `CFLAGS_EXTRA+=-DCM4_UBOOT_ENV_PART=<n>` (0-based GPT index of `p2`), `CFLAGS_EXTRA+=-DCM4_ROOT_A=...` / `-DCM4_ROOT_B=...` (slot rootfs devices), and optionally `-DCM4_SLOT_A_NAME=...` / `-DCM4_SLOT_B_NAME=...` (RAUC bootnames, default `"A"` / `"B"`). `tools/scripts/cm4/prepare_emmc_rauc.sh` lays out the disk and writes an initial env (`BOOT_ORDER "A B"`, tries `3`). Both slot-switch and hung-slot failover were hardware-validated. On the Yocto side, RAUC's `fw_env.config` must point at the raw `p2` partition (offset `0`, size `0x4000`) and the `system.conf` slot devices must match `p4`/`p5`, so userspace (`rauc mark-good` / `fw_setenv`) and wolfBoot agree on the env layout.
## Xilinx Zynq UltraScale

147
hal/cm4.c
View File

@ -3,10 +3,11 @@
* HAL for the Raspberry Pi Compute Module 4 (CM4): Broadcom BCM2711,
* quad-core Cortex-A72 (ARMv8-A).
*
* The VideoCore GPU firmware loads wolfBoot (as kernel8.img) to 0x80000 and
* releases the A72 cores; wolfBoot verifies the appended signed payload and
* boots it from RAM. hal_flash_* are no-ops (no in-place flash in this mode).
* Optional eMMC/SD A/B via the generic SDHCI driver is at the end of the file.
* The VideoCore GPU firmware loads wolfBoot (an ARM64 kernel8.img carrying the
* Linux image header) to 0x200000 and enters it at EL2; wolfBoot verifies the
* signed payload and boots it from RAM, or from eMMC/SD A/B via the generic
* SDHCI driver (at the end of this file). hal_flash_* are no-ops (no in-place
* flash in this mode).
*
* Copyright (C) 2026 wolfSSL Inc.
*
@ -42,32 +43,37 @@
/* Fixed addresses (provided by the linker script) */
extern void *kernel_addr, *update_addr, *dts_addr;
#if defined(HAVE_FIPS)
/* Enable the identity MMU + caches when the build does more than the trivial
* RAM-boot: FIPS (unaligned/SIMD in the module), or the disk path (optimized
* code + SDHCI block-buffer memcpy fault on MMU-off Device memory). Normal
* cacheable memory permits those accesses and speeds up crypto/disk reads. */
#if (defined(HAVE_FIPS) || defined(DISK_SDCARD) || defined(DISK_EMMC)) \
&& defined(__aarch64__)
#define CM4_USE_MMU
#endif
#if defined(CM4_USE_MMU)
void cm4_mmu_enable(void); /* defined below; called from hal_init */
void cm4_mmu_disable(void); /* defined below; called from hal_prepare_boot */
#endif
#if defined(DEBUG_UART)
/* Console UART select. On this bench CM4 the debug cable on GPIO14/15 is the
* BCM2711 mini-UART (AUX, Linux ttyS0), so that is the default. Boards where
* dtoverlay=disable-bt actually routes the PL011 onto GPIO14/15 can build with
* CM4_UART_PL011 to use the PL011 (0xFE201000) instead. */
#if defined(CM4_UART_PL011)
static void uart_tx(char c)
{
while (*UART0_FR & 0x20) /* TXFF: wait while FIFO full */
;
*UART0_DR = c;
}
void uart_write(const char* buf, uint32_t sz)
{
while (sz-- > 0 && *buf)
uart_tx(*buf++);
*UART0_DR = (unsigned int)(unsigned char)c;
}
void uart_init(void)
{
/* The VideoCore firmware has already routed the PL011 to GPIO14/15
* (dtoverlay=disable-bt) and set init_uart_clock=48MHz. Program the PL011
* for 115200 8N1 directly, without the VideoCore mailbox (a mailbox poll
* that never returns post-handoff would hang before any output).
* 48MHz UARTCLK: BAUDDIV = 48e6/(16*115200) = 26.04 -> IBRD 26, FBRD 3. */
/* Program the PL011 for 115200 8N1 assuming a 48MHz UARTCLK
* (init_uart_clock=48000000): BAUDDIV = 48e6/(16*115200) -> IBRD 26 FBRD 3. */
*UART0_CR = 0;
*UART0_ICR = 0x7FF;
*UART0_IBRD = 26;
@ -75,6 +81,29 @@ void uart_init(void)
*UART0_LCRH = (1 << 4) | (1 << 5) | (1 << 6); /* FIFO, 8-bit */
*UART0_CR = (1 << 0) | (1 << 8) | (1 << 9); /* enable UART, TX, RX */
}
#else /* mini-UART (default) */
static void uart_tx(char c)
{
while ((*MU_LSR & MU_LSR_TXFF_EMPTY) == 0) /* wait until TX can accept */
;
*MU_IO = (unsigned int)(unsigned char)c;
}
void uart_init(void)
{
/* The firmware has already enabled the mini-UART at a stable baud
* (enable_uart=1 fixes core_freq), so - like the Linux 8250 console with
* "skip-init" - wolfBoot inherits that setup and just writes AUX_MU_IO.
* Reprogramming the baud here is unnecessary (and error-prone: the mini-UART
* clock is core_freq-derived, not a fixed rate). */
}
#endif /* CM4_UART_PL011 */
void uart_write(const char* buf, uint32_t sz)
{
while (sz-- > 0 && *buf)
uart_tx(*buf++);
}
#endif /* DEBUG_UART */
void* hal_get_primary_address(void)
@ -129,23 +158,29 @@ void hal_init(void)
{
#if defined(DEBUG_UART)
unsigned long el;
/* The banner is emitted before cm4_mmu_enable() on purpose - it is the
* earliest bring-up signal. This is safe because wolfBoot's uart_printf
* (src/string.c) is built -mstrict-align and performs no unaligned / SIMD
* access; do NOT route the banner through a libc printf, which would fault
* on the MMU-off Device memory this runs on. */
uart_init();
__asm__ volatile("mrs %0, CurrentEL" : "=r"(el));
wolfBoot_printf("wolfBoot CM4 (BCM2711 Cortex-A72) hal_init, EL%d\n",
(int)((el >> 2) & 0x3));
#endif
#if defined(HAVE_FIPS)
/* Bring up Normal cacheable memory before the FIPS POST, which uses
* unaligned / SIMD accesses that the MMU-off Device memory rejects. */
#if defined(CM4_USE_MMU)
/* Bring up Normal cacheable memory before any code that uses unaligned /
* SIMD accesses (FIPS module, optimized disk path) which the MMU-off
* Device memory rejects. */
cm4_mmu_enable();
#endif
}
void hal_prepare_boot(void)
{
#if defined(HAVE_FIPS)
/* Undo cm4_mmu_enable() before handoff: flush the app out of the D-cache
* and return to the MMU-off state the application expects. */
#if defined(CM4_USE_MMU)
/* Undo cm4_mmu_enable() before handoff: flush the loaded image out of the
* D-cache and return to the MMU-off state the application expects. */
cm4_mmu_disable();
#endif
}
@ -153,7 +188,7 @@ void hal_prepare_boot(void)
#if defined(HAVE_FIPS)
/* Bounded heap for the FIPS module's malloc. wolfBoot builds the FIPS target
* with --specs=nosys.specs, whose newlib _sbrk grows unbounded from the linker
* 'end' symbol - toward the unverified image staged at kernel_addr (0x140000).
* 'end' symbol - toward the unverified image staged at kernel_addr (0x2C0000).
* Provide our own _sbrk over a fixed static buffer (in .bss, well below the
* image) so heap growth is bounded and can never reach kernel_addr. */
#ifndef CM4_FIPS_HEAP_SIZE
@ -166,20 +201,30 @@ void* _sbrk(int incr)
static unsigned char* brk = cm4_fips_heap;
unsigned char* prev = brk;
if (incr < 0)
return (void*)-1;
if (incr < 0) {
/* Heap trim: newlib's malloc returns memory on free() with a negative
* increment. Clamp to the heap base so brk cannot underflow. */
if ((size_t)(-incr) > (size_t)(brk - cm4_fips_heap))
brk = cm4_fips_heap;
else
brk += incr;
return (void*)prev;
}
if ((size_t)(brk - cm4_fips_heap) + (size_t)incr > sizeof(cm4_fips_heap))
return (void*)-1; /* out of heap */
brk += incr;
return (void*)prev;
}
#endif /* HAVE_FIPS */
#if defined(CM4_USE_MMU)
/* Minimal identity-mapped MMU + caches for the CM4. wolfBoot's simple startup
* runs with the MMU off, so all memory is Device-nGnRnE, which faults on the
* unaligned / 128-bit SIMD accesses the FIPS module and newlib printf perform.
* Mapping DDR as Normal (cacheable) permits those accesses and speeds up the
* crypto; the peripheral region (incl. 0xFE000000) stays Device.
* Four 1GB block descriptors cover the 32-bit VA space at translation level 1. */
* unaligned / 128-bit SIMD accesses that the FIPS module, newlib printf, and
* the optimized disk/SDHCI code paths perform. Mapping DDR as Normal
* (cacheable) permits those accesses and speeds up crypto/disk reads; the
* peripheral region (incl. 0xFE000000) stays Device. Four 1GB block
* descriptors cover the 32-bit VA space at translation level 1. */
#define MMU_BLOCK_NORMAL 0x0000000000000701ULL /* block, AttrIdx0, AF, SH inner */
#define MMU_BLOCK_DEVICE 0x0000000000000405ULL /* block, AttrIdx1, AF, SH none */
@ -224,11 +269,29 @@ static void cm4_dcache_maint(int clean)
__asm__ volatile("isb");
}
/* MMU/cache setup uses EL2 system registers; wolfBoot enters at EL2 on the CM4.
* Guard against an EL1 entry (a custom armstub) so the msr *_el2 below do not
* trap silently before anything can be reported. */
static void cm4_require_el2(void)
{
unsigned long el;
__asm__ volatile("mrs %0, CurrentEL" : "=r"(el));
if (((el >> 2) & 0x3) != 2) {
#if defined(DEBUG_UART)
wolfBoot_printf("cm4: MMU setup requires EL2 (running at EL%d); halting\n",
(int)((el >> 2) & 0x3));
#endif
while (1)
__asm__ volatile("wfi");
}
}
void cm4_mmu_enable(void)
{
unsigned long sctlr;
int i;
cm4_require_el2();
/* 0-3GB DDR -> Normal; 3-4GB peripherals (0xFE000000) -> Device. */
for (i = 0; i < 4; i++) {
uint64_t base = (uint64_t)i << 30;
@ -239,8 +302,10 @@ void cm4_mmu_enable(void)
__asm__ volatile("msr ttbr0_el2, %0"
:: "r"((uint64_t)(uintptr_t)cm4_l1_table));
/* TCR_EL2: T0SZ=32 (32-bit VA), 4KB granule, WB cacheable inner-shareable
* table walks, 36-bit PA. */
__asm__ volatile("msr tcr_el2, %0" :: "r"(0x0000000000013520UL));
* table walks, 36-bit PA. Bits 31 and 23 are RES1 for TCR_EL2 (E2H==0) and
* must be written as 1. */
__asm__ volatile("msr tcr_el2, %0"
:: "r"(0x0000000000013520UL | (1UL << 31) | (1UL << 23)));
__asm__ volatile("isb");
__asm__ volatile("tlbi alle2");
__asm__ volatile("dsb sy");
@ -265,6 +330,16 @@ void cm4_mmu_disable(void)
{
unsigned long sctlr;
cm4_require_el2();
/* Flush the loaded app to DRAM WHILE the D-cache is still enabled, then
* disable M/C/I together. The "textbook" order (clear SCTLR.C first, then
* flush) is UNSAFE here: cm4_dcache_maint() and this function use the stack,
* and once C is cleared, stack reads bypass the cache and return stale DRAM
* (the dirty lines - including this function's spilled return address - are
* not yet written back), so the function would return to garbage. That
* order is only safe in a pure-asm flush with no stack use (U-Boot). What
* must be coherent for the application is the loaded image, and it is fully
* flushed here with caches on. */
cm4_dcache_maint(1); /* clean+invalidate: flush the loaded app to memory */
__asm__ volatile("mrs %0, sctlr_el2" : "=r"(sctlr));
sctlr &= ~((1UL << 0) | (1UL << 2) | (1UL << 12)); /* clear M, C, I */
@ -275,7 +350,7 @@ void cm4_mmu_disable(void)
__asm__ volatile("dsb sy");
__asm__ volatile("isb");
}
#endif /* HAVE_FIPS */
#endif /* CM4_USE_MMU */
#if defined(DEBUG) && defined(DEBUG_UART)
/* CM4 bring-up diagnostic: exception handler invoked from cm4_vectors in
@ -371,9 +446,9 @@ int RAMFUNCTION hal_flash_erase(uintptr_t address, int len)
/* BCM2711 EMMC2 platform glue for the generic SDHCI driver (src/sdhci.c).
* EMMC2 is a standard SDHCI v3.0 Arasan block at 0xFE340000. The driver uses
* Cadence-style SRS offsets (0x200 + std); translate them to the standard
* Arasan layout, mirroring the ZynqMP path in hal/zynq.c. NOTE: not yet
* hardware-validated; clock/caps/card-detect quirks may be required once
* validated on hardware. */
* Arasan layout, mirroring the ZynqMP path in hal/zynq.c. The GPU firmware has
* already configured the EMMC2 clock/pinmux, so only a controller soft reset is
* needed here (hardware-validated on CM4 eMMC). */
#include "sdhci.h"
uint32_t sdhci_reg_read(uint32_t offset)

View File

@ -36,6 +36,10 @@
#define BCM2711_MMIO_BASE 0xFE000000
#define BCM2711_GPIO_BASE (BCM2711_MMIO_BASE + 0x200000)
#define BCM2711_UART0_BASE (BCM2711_GPIO_BASE + 0x1000) /* PL011 */
/* The debug cable on GPIO14/15 is the BCM2711 mini-UART (AUX / 8250-style,
* Linux ttyS0), NOT the PL011. The firmware leaves it enabled with a stable
* baud (enable_uart=1 fixes core_freq), so wolfBoot inherits it. */
#define BCM2711_AUX_BASE (BCM2711_GPIO_BASE + 0x15000) /* AUX (mini-UART) */
/* Overridable so host unit tests can retarget the SDHCI glue at a mock buffer */
#ifndef BCM2711_EMMC2_BASE
#define BCM2711_EMMC2_BASE (BCM2711_MMIO_BASE + 0x340000) /* Arasan SDHCI */
@ -70,7 +74,7 @@
#define STD_SDHCI_SRA 0x01 /* Software Reset for All */
#ifndef __ASSEMBLER__
/* PL011 UART0 register accessors */
/* PL011 UART0 register accessors (not the debug console on this board) */
#define UART0_DR ((volatile unsigned int*)(BCM2711_UART0_BASE+0x00))
#define UART0_FR ((volatile unsigned int*)(BCM2711_UART0_BASE+0x18))
#define UART0_IBRD ((volatile unsigned int*)(BCM2711_UART0_BASE+0x24))
@ -79,6 +83,12 @@
#define UART0_CR ((volatile unsigned int*)(BCM2711_UART0_BASE+0x30))
#define UART0_ICR ((volatile unsigned int*)(BCM2711_UART0_BASE+0x44))
/* Mini-UART (AUX) register accessors - the actual debug console (Linux ttyS0).
* 8250/16550-style: IO at +0x40 (data), LSR at +0x54 (bit5 = TX can accept). */
#define MU_IO ((volatile unsigned int*)(BCM2711_AUX_BASE+0x40))
#define MU_LSR ((volatile unsigned int*)(BCM2711_AUX_BASE+0x54))
#define MU_LSR_TXFF_EMPTY 0x20 /* transmit FIFO can accept a byte */
/* RNG200 hardware TRNG register accessors */
#define RNG_CTRL ((volatile unsigned int*)(BCM2711_RNG_BASE+RNG200_CTRL))
#define RNG_SOFT_RESET ((volatile unsigned int*)(BCM2711_RNG_BASE+RNG200_SOFT_RESET))

View File

@ -2,14 +2,17 @@
*
* Linker script for the Raspberry Pi Compute Module 4 (BCM2711, Cortex-A72).
*
* The VideoCore GPU firmware loads kernel8.img to 0x80000 and releases the
* A72 cores there. DDR_MEM length covers the 1GB low-peripheral view common
* to all CM4 RAM variants (1/2/4/8 GB); the peripheral block sits above at
* 0xFE000000 and is not mapped here.
* The VideoCore GPU firmware loads kernel8.img (an ARM64 image with the Linux
* kernel header) to the 2MB-aligned address 0x200000 and enters it in place at
* EL2 - it does NOT relocate a header image down to 0x80000. So wolfBoot links
* at 0x200000. DDR_MEM length covers the 1GB low-peripheral view common to all
* CM4 RAM variants (1/2/4/8 GB); the peripheral block sits above at 0xFE000000
* and is not mapped here. The initial stack grows down from 0x200000 into the
* ~2MB of free low DDR below the image.
*/
MEMORY
{
DDR_MEM(rwx): ORIGIN = 0x00080000, LENGTH = 0x3c000000
DDR_MEM(rwx): ORIGIN = 0x00200000, LENGTH = 0x3be00000
}
ENTRY(_vector_table);
@ -21,6 +24,7 @@ SECTIONS
KEEP(*(.boot*))
*(.text*)
*(.rodata*)
KEEP(*(.keystore))
*(.note.*)
. = ALIGN(4);
_end_text = .;
@ -53,19 +57,35 @@ SECTIONS
_end = .;
} > DDR_MEM
. = ALIGN(8);
/* Heap start for the newlib sbrk (FIPS module malloc). Grows up into free
* DDR; the stack lives at END_STACK (0x80000) and grows down. */
/* These symbols exist only to satisfy newlib's link-time references (the
* default sbrk/heap symbols). hal/cm4.c defines its own _sbrk over a bounded
* static buffer (cm4_fips_heap[] in .bss), so _heap_start is effectively
* dead - the real FIPS heap is that fixed static buffer, not this address.
* The stack lives at END_STACK (0x200000) and grows down. */
PROVIDE(end = .);
PROVIDE(__end__ = .);
PROVIDE(_heap_start = .);
}
END_STACK = _start_text;
kernel_addr = 0x0140000;
update_addr = 0x1140000;
dts_addr = 0x00a0000;
/* RAM-boot staging addresses for the non-disk configs: the firmware loads
* kernel8.img (wolfBoot + concatenated signed app) to 0x200000, so the app
* lands at 0x200000 + its bin-assemble offset. These sit above the wolfBoot
* image/heap; the initial stack grows down from 0x200000. (The DISK_EMMC /
* DISK_SDCARD configs ignore these - they read A/B images from the eMMC/SD to
* WOLFBOOT_LOAD_ADDRESS instead.) */
kernel_addr = 0x02C0000; /* 0x200000 load + 0xC0000 bin-assemble offset */
update_addr = 0x12C0000;
dts_addr = 0x0280000;
kernel_load_addr = 0x20000000;
dts_load_addr = 0x21000000;
_wolfboot_partition_boot_address = kernel_addr;
_wolfboot_partition_update_address = update_addr;
/* Guard the RAM-boot staging addresses: the image (.text/.data/.bss, including
* the 128KB FIPS heap) must stay below dts_addr (the lower of dts_addr and
* kernel_addr). An overflow would not fail the link - it would silently corrupt
* the staged DTB or the unverified application at runtime. */
ASSERT(_end <= dts_addr,
"cm4: image + heap overruns dts_addr - raise dts_addr/kernel_addr in hal/cm4.ld")

View File

@ -1227,10 +1227,45 @@ FPUStatus:
.align 8
#elif defined(USE_SIMPLE_STARTUP)
.section ".boot"
.global _vector_table
_vector_table:
#if defined(TARGET_cm4)
/* ARM64 Linux kernel image header (Documentation/arm64/booting.rst). The RPi
* VideoCore firmware (start4.elf) only enters a 64-bit kernel that carries
* this header (branch at off 0, "ARM\x64" magic at off 56); it then runs the
* image in place at its 2MB-aligned load address (0x200000) rather than
* relocating to 0x80000. code0 branches to the real entry. */
b _cm4_entry
.long 0 /* code1 */
.quad 0 /* text_offset */
.quad _end - _vector_table /* image_size */
.quad 0xA /* flags: LE, 4KB pages, load anywhere */
.quad 0 /* res2 */
.quad 0 /* res3 */
.quad 0 /* res4 */
.ascii "ARM\x64" /* magic 0x644d5241 at offset 56 */
.long 0 /* res5 */
_cm4_entry:
#endif
mov x21, x0 // read ATAG/FDT address
#if defined(TARGET_cm4)
/* Load-address self-check: wolfBoot is linked absolutely at 0x200000 and is
* NOT position independent (the ldr =_vector_table below loads the absolute
* stack top). Compare the runtime address (PC-relative adr) with the linked
* address (from the literal pool); a mismatch means the firmware placed the
* image at a different 2MB base, so halt loudly instead of executing from the
* wrong VMA. The generated config.txt pins kernel_address=0x200000. */
adr x2, _vector_table
ldr x3, =_vector_table
cmp x2, x3
b.eq 5f
6: wfi
b 6b
5:
#endif
4: ldr x1, =_vector_table // get start of .text in x1
// Read current EL
mrs x0, CurrentEL
@ -1319,12 +1354,25 @@ cm4_vectors:
CM4_VEC cm4_v15 /* SError */
cm4_fault_common:
/* Switch to a dedicated fault stack before calling the C handler: the
* exception may have been taken with SP unset (the window before 'mov sp,x1'
* or on a secondary core that never sets SP) or with SP overflowed, and
* cm4_fault_handler needs a valid stack for its wolfBoot_printf calls. */
ldr x3, =cm4_fault_stack_top
mov sp, x3
mrs x0, esr_el2
mrs x1, elr_el2
mrs x2, far_el2
bl cm4_fault_handler /* void cm4_fault_handler(esr, elr, far) */
9: wfi
b 9b
.section ".bss"
.balign 16
cm4_fault_stack:
.skip 1024
cm4_fault_stack_top:
.section ".boot"
#endif /* TARGET_cm4 && DEBUG && DEBUG_UART */
#if 0
.section ".boot"

View File

@ -1,9 +1,9 @@
/* app_cm4.c
*
* Test application for Raspberry Pi CM4 (BCM2711). Prints a banner over the
* PL011 UART and, when built against wolfCrypt FIPS (HAVE_FIPS), runs the
* power-on self-tests and registers a FIPS callback that reports the runtime
* in-core integrity hash for the verifyCore[] bootstrap.
* mini-UART (the console wired to GPIO14/15; build with CM4_UART_PL011 to use
* the PL011 instead) and halts. wolfBoot's FIPS power-on self-test and in-core
* integrity check run in the bootloader (src/loader.c), not in the app.
*
* Copyright (C) 2026 wolfSSL Inc.
*
@ -28,16 +28,24 @@
#include "wolfboot/wolfboot.h"
#include "hal/cm4.h" /* BCM2711 UART register map */
#ifdef HAVE_FIPS
#include <wolfssl/wolfcrypt/settings.h>
#include <wolfssl/wolfcrypt/fips_test.h>
#include <wolfssl/wolfcrypt/error-crypt.h>
#endif
#ifdef TARGET_cm4
#if defined(CM4_UART_PL011)
static void uart_init(void)
{
/* Route GPIO14 (TXD0) and GPIO15 (RXD0) to the PL011 via ALT0, so the app's
* console reaches the 40-pin debug header (matching hal/cm4.c uart_init()).
* Without this the PL011 stays wired to the Bluetooth pins and the app is
* silent. GPFSEL1 (GPIO_BASE+0x04) holds FSEL10-19: ALT0 = 0b100. */
volatile unsigned int *gpfsel1 =
(volatile unsigned int *)(BCM2711_GPIO_BASE + 0x04);
unsigned int fsel;
fsel = *gpfsel1;
fsel &= ~((7u << 12) | (7u << 15));
fsel |= ((4u << 12) | (4u << 15));
*gpfsel1 = fsel;
/* PL011 for 115200 8N1 from the 48 MHz UART clock (see hal/cm4.c) */
*UART0_CR = 0;
*UART0_ICR = 0x7FF;
@ -51,8 +59,22 @@ static void uart_putc(char c)
{
while (*UART0_FR & 0x20) /* wait while TX FIFO full */
;
*UART0_DR = (unsigned int)c;
*UART0_DR = (unsigned int)(unsigned char)c;
}
#else /* mini-UART (default) - inherit the firmware's enabled console */
static void uart_init(void)
{
/* The firmware leaves the mini-UART enabled at a stable baud; wolfBoot ran
* on it too. Nothing to program - just write AUX_MU_IO. */
}
static void uart_putc(char c)
{
while ((*MU_LSR & MU_LSR_TXFF_EMPTY) == 0) /* wait until TX can accept */
;
*MU_IO = (unsigned int)(unsigned char)c;
}
#endif /* CM4_UART_PL011 */
static void uart_puts(const char* s)
{
@ -63,64 +85,10 @@ static void uart_puts(const char* s)
}
}
static void uart_putdec(int v)
{
char buf[12];
unsigned int u;
int i = 0;
/* Negate in the unsigned domain: -(unsigned)INT_MIN is well defined,
* unlike negating the signed int (UB for INT_MIN). */
u = (unsigned int)v;
if (v < 0) {
uart_putc('-');
u = (unsigned int)(-u);
}
do {
buf[i++] = (char)('0' + (u % 10));
u /= 10;
} while (u != 0);
while (i > 0)
uart_putc(buf[--i]);
}
#ifdef HAVE_FIPS
/* wolfCrypt FIPS callback. On an in-core integrity mismatch (IN_CORE_FIPS_E)
* the module reports the runtime hash here; copy it into verifyCore[] in
* wolfcrypt/src/fips_test.c and rebuild to seal the module boundary. */
static void cm4_fipsCb(int ok, int err, const char* hash)
{
uart_puts("FIPS callback: ok=");
uart_putdec(ok);
uart_puts(" err=");
uart_putdec(err);
uart_puts("\nhash = ");
uart_puts(hash != NULL ? hash : "(null)");
uart_puts("\n");
if (err == IN_CORE_FIPS_E) {
uart_puts("In-core integrity mismatch: copy the hash above into\n");
uart_puts("verifyCore[] in wolfcrypt/src/fips_test.c and rebuild.\n");
}
}
#endif /* HAVE_FIPS */
void main(void)
{
uart_init();
uart_puts("\n=== wolfBoot CM4 test-app ===\n");
#ifdef HAVE_FIPS
uart_puts("wolfCrypt FIPS 140-3 power-on self-test\n");
wolfCrypt_SetCb_fips(cm4_fipsCb);
if (wc_RunAllCast_fips() == 0)
uart_puts("FIPS CASTs: PASS\n");
else
uart_puts("FIPS CASTs: FAIL (see callback output above)\n");
uart_puts("FIPS status: ");
uart_putdec(wolfCrypt_GetStatus_fips());
uart_puts("\n");
#endif /* HAVE_FIPS */
uart_puts("test-app done; halting.\n");
while (1)
;

View File

@ -0,0 +1,30 @@
/* disk_app.S - minimal CM4 disk-boot test payload.
*
* Prints a banner over the BCM2711 mini-UART (AUX @ 0xFE215040 - the console
* wired to GPIO14/15 on the CM4, i.e. Linux ttyS0; NOT the PL011) and spins.
* Linked at 0x10000000 (WOLFBOOT_LOAD_ADDRESS for cm4_emmc.config). Signed and
* written raw into the eMMC A/B image partitions to prove wolfBoot's disk-boot
* path (read from eMMC -> verify -> ELF load -> handoff).
*
* Build: aarch64-none-elf-gcc -nostdlib -nostartfiles \
* -Wl,-Ttext=0x10000000 -o disk_app.elf disk_app.S
*
* Copyright (C) 2026 wolfSSL Inc. GPLv3 (see wolfBoot COPYING).
*/
.section .text
.global _start
_start:
movz x1, #0xFE21, lsl #16
movk x1, #0x5000 /* x1 = 0xFE215000 (AUX mini-UART base) */
adr x0, msg
1: ldrb w2, [x0], #1
cbz w2, 2f
3: ldr w3, [x1, #0x54] /* AUX_MU_LSR: bit5 = TX can accept */
tbz w3, #5, 3b
str w2, [x1, #0x40] /* AUX_MU_IO (data) */
b 1b
2: wfi
b 2b
.align 3
msg:
.asciz "\r\n>>> CM4 DISK APP OK: wolfBoot read + verified + booted from eMMC <<<\r\n"

View File

@ -0,0 +1,155 @@
#!/bin/bash
# prepare_emmc.sh - build the eMMC/SD layout for the CM4 wolfBoot disk-boot test.
#
# Creates a GPT with:
# p1 = boot (FAT32) : RPi firmware (start4.elf, fixup4.dat), config.txt,
# kernel8.img (= wolfBoot, built from cm4_emmc.config)
# p2 = image_a (raw) : signed disk_app ELF, version 1 (BOOT_PART_A=1)
# p3 = image_b (raw) : signed disk_app ELF, version 2 (BOOT_PART_B=2)
#
# wolfBoot (loaded to RAM by the RPi firmware) then drives the BCM2711 EMMC2
# controller to read p2/p3, verifies ECC384/SHA384, and boots the higher
# version. Prove-out prints ">>> CM4 DISK APP OK <<<" over the mini-UART (AUX).
#
# Usage:
# # 1) build wolfBoot first:
# # cp config/examples/cm4_emmc.config .config
# # make wolfboot.bin CROSS_COMPILE=aarch64-none-elf- (add DEBUG_SDHCI for bring-up)
# # 2) prep artifacts only (no root):
# # bash tools/scripts/cm4/prepare_emmc.sh
# # 3) with the eMMC presented via rpiboot as /dev/sdN, write it (root):
# # sudo bash tools/scripts/cm4/prepare_emmc.sh /dev/sdN
#
# Copyright (C) 2026 wolfSSL Inc. GPLv3 (see wolfBoot COPYING).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" # wolfboot repo root
HERE="$ROOT/tools/scripts/cm4"
CROSS="${CROSS_COMPILE:-aarch64-none-elf-}"
KEY="${PRIVATE_KEY:-$ROOT/wolfboot_signing_private_key.der}"
IMG_HDR="${IMAGE_HEADER_SIZE:-1024}"
# Pin the RPi firmware to a specific ref for reproducibility - these blobs are
# the first link of the boot chain. Override with FWREF=<tag|sha> (a tag like
# 1.20240529, or a full commit sha).
FWREF="${FWREF:-1.20240529}"
FWBASE="https://raw.githubusercontent.com/raspberrypi/firmware/${FWREF}/boot"
DEV="${1:-}"
# ---- artifact prep (no root) ----------------------------------------------
prep_artifacts() {
# Idempotent: if everything is already staged (e.g. prepped without sudo,
# then re-run under sudo to write the device where the cross-toolchain is
# not in PATH), skip the rebuild. Set FORCE_PREP=1 to force.
if [ "${FORCE_PREP:-0}" != "1" ] && \
[ -f "$HERE/disk_app_v1_signed.bin" ] && \
[ -f "$HERE/disk_app_v2_signed.bin" ] && \
[ -f "$HERE/fw/kernel8.img" ] && [ -f "$HERE/fw/start4.elf" ] && \
[ -f "$HERE/fw/config.txt" ]; then
echo "== artifacts already staged in $HERE (skip prep; FORCE_PREP=1 to rebuild) =="
return 0
fi
[ -f "$ROOT/wolfboot.bin" ] || { echo "!! build wolfboot.bin first (cm4_emmc.config)"; exit 1; }
[ -f "$KEY" ] || { echo "!! signing key not found: $KEY"; exit 1; }
echo "== building + signing disk_app (v1, v2) =="
"${CROSS}gcc" -nostdlib -nostartfiles -Wl,-Ttext=0x10000000 \
-o "$HERE/disk_app.elf" "$HERE/disk_app.S"
( cd "$ROOT" && \
IMAGE_HEADER_SIZE="$IMG_HDR" ./tools/keytools/sign --ecc384 --sha384 \
"$HERE/disk_app.elf" "$KEY" 1 >/dev/null && \
IMAGE_HEADER_SIZE="$IMG_HDR" ./tools/keytools/sign --ecc384 --sha384 \
"$HERE/disk_app.elf" "$KEY" 2 >/dev/null )
ls -la "$HERE"/disk_app_v1_signed.bin "$HERE"/disk_app_v2_signed.bin
echo "== fetching RPi BCM2711 firmware =="
for f in start4.elf fixup4.dat bcm2711-rpi-cm4.dtb; do
[ -f "$HERE/fw/$f" ] || { mkdir -p "$HERE/fw"; curl -fsSL -o "$HERE/fw/$f" "$FWBASE/$f"; }
done
cat > "$HERE/fw/config.txt" <<'EOF'
arm_64bit=1
kernel=kernel8.img
# wolfBoot is linked absolutely at 0x200000; pin the load address to match.
kernel_address=0x200000
enable_uart=1
uart_2ndstage=1
dtoverlay=disable-bt
init_uart_clock=48000000
init_uart_baud=115200
EOF
cp "$ROOT/wolfboot.bin" "$HERE/fw/kernel8.img"
echo "== artifacts ready in $HERE (fw/ + disk_app_v{1,2}_signed.bin) =="
}
# ---- device write (root) --------------------------------------------------
write_device() {
local t root_src root_pk P1 P2 P3 MP ok p
[ "$(id -u)" -eq 0 ] || { echo "!! writing $DEV requires root"; exit 1; }
for t in sgdisk partprobe mkfs.vfat lsblk udevadm; do
command -v "$t" >/dev/null || { echo "!! missing required tool: $t"; exit 1; }
done
[ -b "$DEV" ] || { echo "!! not a block device: $DEV"; exit 1; }
# Refuse the disk currently backing '/' (a real safety check, not a
# bench-specific node name). Override with ALLOW_ANY_DISK=1.
root_src="$(findmnt -no SOURCE / 2>/dev/null || true)"
root_pk="$(lsblk -no PKNAME "$root_src" 2>/dev/null | head -1)"
if [ "${ALLOW_ANY_DISK:-0}" != "1" ]; then
# Fail closed: an empty PKNAME (LVM/btrfs/overlay root, empty root_src)
# would otherwise leave the guard comparing against a bare "/dev/".
[ -n "$root_pk" ] || { echo "!! cannot determine root disk (lsblk PKNAME empty); set ALLOW_ANY_DISK=1 to override"; exit 1; }
if [ "$DEV" = "/dev/$root_pk" ]; then
echo "!! refusing to erase the root disk $DEV (set ALLOW_ANY_DISK=1 to override)"; exit 1
fi
fi
echo "== target: $DEV =="; lsblk -o NAME,SIZE,FSTYPE,LABEL "$DEV"
read -rp "Repartition and ERASE $DEV? [type YES] " ok
[ "$ok" = "YES" ] || { echo "aborted"; exit 1; }
# A/B partitions are 32M - far larger than the signed disk_app test payload
# (~64KB). They intentionally need not match WOLFBOOT_PARTITION_SIZE (0x4000000
# = 64M) in cm4_emmc.config: the disk path reads only the image at the
# partition start, so the partition just has to be big enough to hold it.
echo "== GPT: p1 boot 128M / p2 image_a 32M / p3 image_b 32M =="
sgdisk --zap-all "$DEV" >/dev/null
sgdisk -n 1:2048:+128M -t 1:0700 -c 1:boot "$DEV" >/dev/null
sgdisk -n 2:0:+32M -t 2:8300 -c 2:image_a "$DEV" >/dev/null
sgdisk -n 3:0:+32M -t 3:8300 -c 3:image_b "$DEV" >/dev/null
partprobe "$DEV"; udevadm settle
P1="${DEV}1"; P2="${DEV}2"; P3="${DEV}3"
# some kernels use pN naming for nvme/mmc; handle 'p' suffix
[ -b "$P1" ] || { P1="${DEV}p1"; P2="${DEV}p2"; P3="${DEV}p3"; }
# Assert every node exists before any dd: a missing node makes dd silently
# create a regular file instead of writing the partition.
for p in "$P1" "$P2" "$P3"; do
[ -b "$p" ] || { echo "!! partition node $p missing (partprobe/udev race?)"; exit 1; }
done
echo "== FAT boot partition ($P1) =="
mkfs.vfat -n BOOT "$P1" >/dev/null
MP=$(mktemp -d)
# Clean up the mount point on exit even if mount/cp fails under set -e. A
# RETURN trap does NOT fire when set -e aborts inside a function; EXIT does.
# The mountpoint -q guard makes it idempotent with the explicit umount below.
trap 'mountpoint -q "$MP" && umount "$MP"; rmdir "$MP" 2>/dev/null || true' EXIT
mount "$P1" "$MP"
cp "$HERE"/fw/start4.elf "$HERE"/fw/fixup4.dat "$HERE"/fw/bcm2711-rpi-cm4.dtb \
"$HERE"/fw/config.txt "$HERE"/fw/kernel8.img "$MP"/
sync; umount "$MP"
echo "== raw signed images -> A ($P2, v1), B ($P3, v2) =="
dd if="$HERE/disk_app_v1_signed.bin" of="$P2" bs=4k conv=fsync status=none
dd if="$HERE/disk_app_v2_signed.bin" of="$P3" bs=4k conv=fsync status=none
sync
echo "== done. Set BOOT switch OFF, power-cycle, and watch the CM4 mini-UART console. =="
}
prep_artifacts
# if/else (not an AND-OR list): calling write_device as the left operand of
# '&&' would disable set -e for its whole body, so a failed sgdisk/dd would be
# silently ignored and a half-written eMMC reported as success.
if [ -n "$DEV" ]; then
write_device
else
echo "(no device given; artifacts only. Re-run with: sudo bash $0 /dev/sdN)"
fi