mirror of https://github.com/wolfSSL/wolfBoot.git
cm4: add RAUC A/B slot selection via U-Boot env (wolfBoot replaces U-Boot)
parent
56651bde95
commit
67c819bc0b
|
|
@ -236,6 +236,8 @@ tools/unit-tests/unit-update-ram-enc
|
|||
tools/unit-tests/unit-update-ram-enc-nopart
|
||||
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/__pycache__/*
|
||||
|
||||
|
||||
|
|
@ -515,3 +517,4 @@ tools/scripts/cm4/fw/
|
|||
tools/scripts/cm4/*.elf
|
||||
tools/scripts/cm4/*.bin
|
||||
tools/scripts/cm4/linux/
|
||||
tools/scripts/cm4/rauc/
|
||||
|
|
|
|||
23
arch.mk
23
arch.mk
|
|
@ -225,15 +225,26 @@ ifeq ($(ARCH),AARCH64)
|
|||
# -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.
|
||||
# hal/cm4.c). cm4 defaults to NO_ARM_ASM=1, which keeps it off the wolfcrypt
|
||||
# ARM port asm (NEON structure loads); it does NOT disable the SP ECC
|
||||
# AArch64 asm, which is gated on __aarch64__ in include/user_settings.h. So
|
||||
# -mstrict-align keeps every config safe either way.
|
||||
CFLAGS+=$(ARCH_FLAGS) -DCORTEX_A72 -mstrict-align
|
||||
# RAUC A/B slot selection via a raw U-Boot env partition (wolfBoot replaces
|
||||
# U-Boot's boot script). Adds the env state-machine module + define.
|
||||
ifeq ($(CM4_RAUC_AB),1)
|
||||
OBJS+=src/ubootenv.o
|
||||
CFLAGS+=-DCM4_RAUC_AB
|
||||
endif
|
||||
endif
|
||||
|
||||
# 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.
|
||||
# Default ARM ASM setting for unrecognized AARCH64 targets. cm4 defaults to
|
||||
# NO_ARM_ASM=1, which keeps it off the wolfcrypt ARM port asm (WOLFSSL_ARMASM /
|
||||
# wolfcrypt/src/port/arm/*, the NEON multi-register loads that would fault
|
||||
# MMU-off in the plain config). NO_ARM_ASM does NOT disable the SP ECC 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)),)
|
||||
NO_ARM_ASM?=1
|
||||
endif
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
# Raspberry Pi CM4 (BCM2711) - production RAUC A/B boot: wolfBoot replaces GCX's
|
||||
# U-Boot as the RAUC slot arbiter. Same Linux boot as cm4_emmc_linux.config, plus
|
||||
# wolfBoot reads a raw U-Boot-env partition (RAUC bootloader=uboot, fw_setenv),
|
||||
# runs the BOOT_ORDER / BOOT_<name>_LEFT state machine, decrements the try counter
|
||||
# (so a hung slot fails over), and injects "root=<slot> rauc.slot=<name>".
|
||||
#
|
||||
# eMMC GPT layout (0-based indices in [brackets]):
|
||||
# p1 boot FAT [0] : RPi fw + wolfBoot kernel8.img + config.txt (disable-bt)
|
||||
# p2 uboot-env raw [1] : U-Boot env (RAUC fw_env.config -> this partition)
|
||||
# p3 fitImage raw [2] : wolfBoot-signed kernel FIT (shared across A/B)
|
||||
# p4 rootfs_A ext4 [3] : RAUC slot A
|
||||
# p5 rootfs_B ext4 [4] : RAUC slot B
|
||||
# p6 data ext4 [5] : persistent /data
|
||||
#
|
||||
# Target: GCX "iron-butterfly" Yocto Scarthgap, kernel 6.6.63, raspberrypi4-64.
|
||||
ARCH?=AARCH64
|
||||
TARGET?=cm4
|
||||
SIGN?=ECC384
|
||||
HASH?=SHA384
|
||||
IMAGE_HEADER_SIZE?=1024
|
||||
DEBUG?=0
|
||||
DEBUG_UART?=1
|
||||
|
||||
# --- eMMC disk driver --------------------------------------------------------
|
||||
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
|
||||
EXT_FLASH?=0
|
||||
NO_XIP=1
|
||||
NO_QNX?=1
|
||||
ELF?=1
|
||||
VTOR?=1
|
||||
SPMATH?=1
|
||||
PKA?=0
|
||||
WOLFTPM?=0
|
||||
WOLFBOOT_NO_PARTITIONS=1
|
||||
# Shared kernel FIT lives on p3 (index 2); no per-slot kernel, so A==B.
|
||||
CFLAGS_EXTRA+=-DBOOT_PART_A=2
|
||||
CFLAGS_EXTRA+=-DBOOT_PART_B=2
|
||||
CFLAGS_EXTRA+=-DDISK_BLOCK_SIZE=0x80000
|
||||
|
||||
# --- Linux boot (see cm4_emmc_linux.config for the rationale) -----------------
|
||||
GZIP?=1
|
||||
CFLAGS_EXTRA+=-DCM4_UART_PL011
|
||||
CFLAGS_EXTRA+=-DCM4_FIRMWARE_DTB
|
||||
# Fallback root if the RAUC env read fails (normally root= comes from the slot).
|
||||
CFLAGS_EXTRA+=-DLINUX_BOOTARGS_ROOT=\"/dev/mmcblk0p4\"
|
||||
|
||||
# --- RAUC A/B slot selection (wolfBoot replaces U-Boot's boot.scr) ------------
|
||||
# CM4_RAUC_AB is a make var: arch.mk links src/ubootenv.o and defines -DCM4_RAUC_AB.
|
||||
CM4_RAUC_AB?=1
|
||||
# 0-based GPT index of the raw U-Boot-env partition (p2). Match RAUC fw_env.config.
|
||||
CFLAGS_EXTRA+=-DCM4_UBOOT_ENV_PART=1
|
||||
# RAUC bootname -> rootfs device. Match RAUC system.conf slot devices.
|
||||
CFLAGS_EXTRA+=-DCM4_ROOT_A=\"/dev/mmcblk0p4\"
|
||||
CFLAGS_EXTRA+=-DCM4_ROOT_B=\"/dev/mmcblk0p5\"
|
||||
|
||||
WOLFBOOT_LOAD_ADDRESS?=0x18000000
|
||||
WOLFBOOT_RAMBOOT_MAX_SIZE=0x23C00000
|
||||
WOLFBOOT_LOAD_DTS_ADDRESS?=0x08000000
|
||||
WOLFBOOT_PARTITION_BOOT_ADDRESS=0x80200000
|
||||
WOLFBOOT_PARTITION_SIZE=0x4000000
|
||||
WOLFBOOT_SECTOR_SIZE=0x1000
|
||||
146
hal/cm4.c
146
hal/cm4.c
|
|
@ -164,23 +164,136 @@ extern void* cm4_fw_dtb;
|
|||
* PL011 registers cleanly in Linux; the mini-UART (bcm2835-aux) does not on
|
||||
* this DTB ("unable to register 8250 port").
|
||||
* default (mini-UART) -> serial0/ttyS0. */
|
||||
#ifndef LINUX_BOOTARGS
|
||||
#ifndef LINUX_BOOTARGS_ROOT
|
||||
#define LINUX_BOOTARGS_ROOT "/dev/mmcblk0p3"
|
||||
#endif
|
||||
/* Console/earlycon base (no root=), shared by the static and RAUC A/B cmdlines. */
|
||||
#if defined(CM4_UART_PL011)
|
||||
#define LINUX_BOOTARGS \
|
||||
"earlycon=pl011,mmio32,0xfe201000 console=ttyAMA0,115200 root=" \
|
||||
LINUX_BOOTARGS_ROOT " rootfstype=ext4 rootwait"
|
||||
#define LINUX_BOOTARGS_BASE \
|
||||
"earlycon=pl011,mmio32,0xfe201000 console=ttyAMA0,115200"
|
||||
#else
|
||||
/* earlycon=uart8250,mmio32,0xfe215040: the mini-UART is 8250-driven with a 4-byte
|
||||
* register stride, so LSR lands at 0xfe215054 - prints from MMIO before the dtb
|
||||
* console driver is up. serial0 aliases the mini-UART (ttyS0) at runtime. */
|
||||
#define LINUX_BOOTARGS_BASE \
|
||||
"earlycon=uart8250,mmio32,0xfe215040 console=serial0,115200"
|
||||
#endif
|
||||
#ifndef LINUX_BOOTARGS
|
||||
#ifndef LINUX_BOOTARGS_ROOT
|
||||
#define LINUX_BOOTARGS_ROOT "/dev/mmcblk0p3"
|
||||
#endif
|
||||
#define LINUX_BOOTARGS \
|
||||
"earlycon=uart8250,mmio32,0xfe215040 console=serial0,115200 root=" \
|
||||
LINUX_BOOTARGS_ROOT " rootfstype=ext4 rootwait"
|
||||
LINUX_BOOTARGS_BASE " root=" LINUX_BOOTARGS_ROOT " rootfstype=ext4 rootwait"
|
||||
#endif
|
||||
|
||||
#if defined(CM4_RAUC_AB)
|
||||
#include "disk.h"
|
||||
#include "ubootenv.h"
|
||||
|
||||
/* 0-based GPT index of the raw U-Boot-env partition (RAUC fw_env.config target). */
|
||||
#ifndef CM4_UBOOT_ENV_PART
|
||||
#define CM4_UBOOT_ENV_PART 1
|
||||
#endif
|
||||
#ifndef BOOT_DISK
|
||||
#define BOOT_DISK 0
|
||||
#endif
|
||||
/* RAUC bootname -> rootfs device. Override in the .config for your layout. */
|
||||
#ifndef CM4_ROOT_A
|
||||
#define CM4_ROOT_A "/dev/mmcblk0p4"
|
||||
#endif
|
||||
#ifndef CM4_ROOT_B
|
||||
#define CM4_ROOT_B "/dev/mmcblk0p5"
|
||||
#endif
|
||||
/* RAUC bootnames (BOOT_ORDER tokens) that map to each rootfs. RAUC's own
|
||||
* default is A/B; override to match a system.conf that uses other names
|
||||
* (e.g. system0/system1). An unrecognised name fails loudly (static cmdline). */
|
||||
#ifndef CM4_SLOT_A_NAME
|
||||
#define CM4_SLOT_A_NAME "A"
|
||||
#endif
|
||||
#ifndef CM4_SLOT_B_NAME
|
||||
#define CM4_SLOT_B_NAME "B"
|
||||
#endif
|
||||
|
||||
static uint8_t cm4_uboot_env[UBOOT_ENV_SIZE];
|
||||
|
||||
/* Bounded append of src to dst starting at index at; returns the new index. */
|
||||
static size_t cm4_strcat(char *dst, size_t dstsz, size_t at, const char *src)
|
||||
{
|
||||
while (*src != '\0' && at + 1 < dstsz)
|
||||
dst[at++] = *src++;
|
||||
dst[at] = '\0';
|
||||
return at;
|
||||
}
|
||||
|
||||
/* Read the raw U-Boot env, run the RAUC A/B state machine, persist the
|
||||
* decremented try counter (so a hung slot fails over next boot), and build the
|
||||
* kernel command line "<base> root=<slot dev> rauc.slot=<name> rootfstype=ext4
|
||||
* rootwait" into out. Returns 0 on success, -1 on failure (caller falls back to
|
||||
* the static LINUX_BOOTARGS). Runs with the MMU on and the disk already open
|
||||
* (called from hal_get_boot_dts, before hal_prepare_boot). */
|
||||
static int cm4_rauc_build_bootargs(char *out, size_t outsz)
|
||||
{
|
||||
struct uboot_slot slot;
|
||||
const char *root;
|
||||
size_t at;
|
||||
int r;
|
||||
int env_ok;
|
||||
|
||||
/* Require the FULL env: disk_part_read() clamps sz to the partition length,
|
||||
* so a short read (partition smaller than UBOOT_ENV_SIZE) returns a positive
|
||||
* count with the tail left zero - which would fail CRC and reset to defaults
|
||||
* every boot. Treat anything but a complete read as "no env". */
|
||||
r = disk_part_read(BOOT_DISK, CM4_UBOOT_ENV_PART, 0, UBOOT_ENV_SIZE,
|
||||
cm4_uboot_env);
|
||||
env_ok = (r == (int)UBOOT_ENV_SIZE);
|
||||
if (!env_ok) {
|
||||
/* Boot a default slot but do NOT persist a guess over RAUC state we did
|
||||
* not successfully read (a transient read error must not roll back a
|
||||
* "slot B good" system to "boot A"). */
|
||||
wolfBoot_printf("cm4: uboot-env read failed (%d); default slot, no writeback\n",
|
||||
r);
|
||||
memset(cm4_uboot_env, 0, sizeof(cm4_uboot_env));
|
||||
}
|
||||
if (uboot_env_select_slot(cm4_uboot_env, UBOOT_ENV_SIZE, &slot) != 0)
|
||||
return -1;
|
||||
if (!slot.selected) {
|
||||
/* All slots were exhausted; counters were re-armed - pick slot A now. */
|
||||
wolfBoot_printf("cm4: all RAUC slots exhausted; re-armed, retrying\n");
|
||||
if (uboot_env_select_slot(cm4_uboot_env, UBOOT_ENV_SIZE, &slot) != 0)
|
||||
return -1;
|
||||
if (!slot.selected)
|
||||
return -1; /* malformed BOOT_ORDER -> static cmdline */
|
||||
}
|
||||
if (env_ok) {
|
||||
/* Persist the decremented try counter. A short/failed write leaves the
|
||||
* counter un-decremented, so booting the selected slot would loop into
|
||||
* it forever - fall back to the static cmdline instead. */
|
||||
r = disk_part_write(BOOT_DISK, CM4_UBOOT_ENV_PART, 0, UBOOT_ENV_SIZE,
|
||||
cm4_uboot_env);
|
||||
if (r != (int)UBOOT_ENV_SIZE) {
|
||||
wolfBoot_printf("cm4: uboot-env write failed (%d); static cmdline\n",
|
||||
r);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (strcmp(slot.name, CM4_SLOT_B_NAME) == 0)
|
||||
root = CM4_ROOT_B;
|
||||
else if (strcmp(slot.name, CM4_SLOT_A_NAME) == 0)
|
||||
root = CM4_ROOT_A;
|
||||
else {
|
||||
wolfBoot_printf("cm4: unknown RAUC slot '%s'; static cmdline\n",
|
||||
slot.name);
|
||||
return -1;
|
||||
}
|
||||
at = 0;
|
||||
at = cm4_strcat(out, outsz, at, LINUX_BOOTARGS_BASE);
|
||||
at = cm4_strcat(out, outsz, at, " root=");
|
||||
at = cm4_strcat(out, outsz, at, root);
|
||||
at = cm4_strcat(out, outsz, at, " rauc.slot=");
|
||||
at = cm4_strcat(out, outsz, at, slot.name);
|
||||
(void)cm4_strcat(out, outsz, at, " rootfstype=ext4 rootwait");
|
||||
wolfBoot_printf("cm4: RAUC slot %s -> root %s\n", slot.name, root);
|
||||
return 0;
|
||||
}
|
||||
#endif /* CM4_RAUC_AB */
|
||||
|
||||
/* Supply Linux a bootable DTB when the FIT is kernel-only: relocate the
|
||||
* firmware dtb to WOLFBOOT_LOAD_DTS_ADDRESS (headroom to grow /chosen without
|
||||
|
|
@ -231,9 +344,20 @@ void* hal_get_boot_dts(void)
|
|||
wolfBoot_printf("cm4: DTB /chosen error (%d)\n", off);
|
||||
return fdt; /* still bootable; kernel falls back to built-in cmdline */
|
||||
}
|
||||
#if defined(CM4_RAUC_AB)
|
||||
{
|
||||
static char cm4_bootargs[256];
|
||||
const char *args = LINUX_BOOTARGS;
|
||||
if (cm4_rauc_build_bootargs(cm4_bootargs, sizeof(cm4_bootargs)) == 0)
|
||||
args = cm4_bootargs;
|
||||
if (fdt_fixup_str(fdt, off, "chosen", "bootargs", args) != 0)
|
||||
wolfBoot_printf("cm4: DTB bootargs fixup failed\n");
|
||||
}
|
||||
#else
|
||||
if (fdt_fixup_str(fdt, off, "chosen", "bootargs", LINUX_BOOTARGS) != 0) {
|
||||
wolfBoot_printf("cm4: DTB bootargs fixup failed\n");
|
||||
}
|
||||
#endif
|
||||
wolfBoot_printf("cm4: DTB relocated to %p, bootargs set\n", fdt);
|
||||
return fdt;
|
||||
#endif /* CM4_FIRMWARE_DTB */
|
||||
|
|
@ -632,6 +756,10 @@ void sdhci_platform_init(void)
|
|||
if ((*((volatile uint8_t *)(base + STD_SDHCI_SW_RESET)) & STD_SDHCI_SRA) == 0)
|
||||
break;
|
||||
}
|
||||
if ((*((volatile uint8_t *)(base + STD_SDHCI_SW_RESET)) & STD_SDHCI_SRA) != 0) {
|
||||
wolfBoot_printf("sdhci_platform_init: SRA soft reset did not clear; "
|
||||
"controller may be unresponsive\n");
|
||||
}
|
||||
}
|
||||
|
||||
void sdhci_platform_irq_init(void)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
/* ubootenv.h
|
||||
*
|
||||
* U-Boot environment based A/B slot selection, RAUC-compatible.
|
||||
*
|
||||
* wolfBoot replicates the slot arbitration a RAUC "bootloader=uboot" setup
|
||||
* normally performs in a U-Boot boot script: read BOOT_ORDER and the per-slot
|
||||
* BOOT_<name>_LEFT try counters from a shared U-Boot environment, pick the first
|
||||
* slot in BOOT_ORDER with tries remaining, decrement its counter, and hand the
|
||||
* kernel "root=<dev> rauc.slot=<name>". RAUC (userspace, via fw_setenv) writes
|
||||
* the same environment. The environment lives in a raw region wolfBoot can read
|
||||
* with disk_part_read()/disk_part_write() (wolfBoot has no filesystem support).
|
||||
*
|
||||
* Environment layout (U-Boot "simple", non-redundant):
|
||||
* [ crc32 : 4 bytes little-endian ][ data : env_len - 4 bytes ]
|
||||
* data is a NUL-separated list of "key=value" entries terminated by an empty
|
||||
* key (an extra NUL); trailing space is zero padding, and the crc32 covers the
|
||||
* whole data region including the padding.
|
||||
*
|
||||
* Copyright (C) 2026 wolfSSL Inc.
|
||||
*
|
||||
* This file is part of wolfBoot.
|
||||
*
|
||||
* wolfBoot is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* wolfBoot is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
|
||||
*/
|
||||
#ifndef UBOOTENV_H
|
||||
#define UBOOTENV_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* Default environment size (matches GCX fw_env.config: 0x4000 = 16 KiB). */
|
||||
#ifndef UBOOT_ENV_SIZE
|
||||
#define UBOOT_ENV_SIZE 0x4000
|
||||
#endif
|
||||
|
||||
/* Default number of boot attempts a freshly (re)armed slot gets. */
|
||||
#ifndef UBOOT_ENV_DEFAULT_TRIES
|
||||
#define UBOOT_ENV_DEFAULT_TRIES 3
|
||||
#endif
|
||||
|
||||
/* Longest slot value we handle in bootargs building. */
|
||||
#define UBOOT_ENV_VAL_MAX 64
|
||||
|
||||
/* Selected slot returned by uboot_env_select_slot(). */
|
||||
struct uboot_slot {
|
||||
char name[UBOOT_ENV_VAL_MAX]; /* RAUC bootname, e.g. "A" or "B" */
|
||||
int selected; /* 1 if a slot with tries left was chosen */
|
||||
int rearmed; /* 1 if all counters were exhausted+reset */
|
||||
};
|
||||
|
||||
/* Verify the leading CRC32 over the data region. Returns 1 if valid, else 0. */
|
||||
int uboot_env_verify(const uint8_t *env, size_t env_len);
|
||||
|
||||
/* Recompute and store the leading CRC32 over the data region. */
|
||||
void uboot_env_reseal(uint8_t *env, size_t env_len);
|
||||
|
||||
/* Copy the value of <key> into val_out (NUL-terminated, bounded by val_max).
|
||||
* Returns the value length on success, or -1 if the key is absent. */
|
||||
int uboot_env_get(const uint8_t *env, size_t env_len,
|
||||
const char *key, char *val_out, size_t val_max);
|
||||
|
||||
/* Set (overwrite or append) <key>=<val> in the data region. Does NOT reseal.
|
||||
* Returns 0 on success, -1 if it would not fit. */
|
||||
int uboot_env_set(uint8_t *env, size_t env_len, const char *key,
|
||||
const char *val);
|
||||
|
||||
/* Run the A/B boot-slot state machine on an in-memory environment image:
|
||||
* - if the CRC32 is invalid, reinitialize defaults (BOOT_ORDER "A B",
|
||||
* BOOT_<name>_LEFT = UBOOT_ENV_DEFAULT_TRIES);
|
||||
* - pick the first name in BOOT_ORDER whose BOOT_<name>_LEFT > 0, decrement
|
||||
* that counter, and report it in *out (out->selected = 1);
|
||||
* - if none have tries left, re-arm every counter to the default, set
|
||||
* out->rearmed = 1 and out->selected = 0 (the caller should reboot);
|
||||
* - reseal the CRC32.
|
||||
* The env buffer is modified in place; the caller writes it back to storage.
|
||||
* Returns 0 on success, -1 on a malformed BOOT_ORDER / buffer error. */
|
||||
int uboot_env_select_slot(uint8_t *env, size_t env_len, struct uboot_slot *out);
|
||||
|
||||
#endif /* UBOOTENV_H */
|
||||
22
src/sdhci.c
22
src/sdhci.c
|
|
@ -1342,6 +1342,13 @@ static int emmc_card_full_init(void)
|
|||
#define SDHCI_DIR_READ 1
|
||||
#define SDHCI_DIR_WRITE 0
|
||||
|
||||
/* Bounded spin for the multi-block write settle-wait (see sdhci_transfer): TC
|
||||
* may not arrive until CMD12 on some controllers, so the pre-CMD12 wait is
|
||||
* capped instead of spinning forever. Override per platform if needed. */
|
||||
#ifndef SDHCI_WRITE_SETTLE_SPINS
|
||||
#define SDHCI_WRITE_SETTLE_SPINS 1000000U
|
||||
#endif
|
||||
|
||||
/* Unified internal transfer function for read and write operations
|
||||
* dir: SDHCI_DIR_READ or SDHCI_DIR_WRITE
|
||||
* cmd_index: command to send (e.g., MMC_CMD17_READ_SINGLE, MMC_CMD25_WRITE_MULTIPLE)
|
||||
|
|
@ -1568,10 +1575,21 @@ static int sdhci_transfer(int dir, uint32_t cmd_index, uint32_t block_addr,
|
|||
SDHCI_REG_SET(SDHCI_SRS12, SDHCI_SRS12_BRR);
|
||||
}
|
||||
|
||||
/* For write: wait for transfer complete before checking status */
|
||||
/* Write completion: settle the data phase so the SRS12 error sample
|
||||
* below is valid and a latched error (EINT) is caught. A single-block
|
||||
* write (CMD24) sets TC after the data phase - wait for it. An open-ended
|
||||
* multi-block write (CMD25, no Auto-CMD12) may not set TC until the CMD12
|
||||
* stop issued below (observed on the CM4 EMMC2: SRS12 stuck at 0x51, TC
|
||||
* never set), so its wait is BOUNDED: it still captures an EINT without
|
||||
* deadlocking, and the CMD12 + wait-busy sequence below completes the
|
||||
* transfer. */
|
||||
if (dir == SDHCI_DIR_WRITE) {
|
||||
uint32_t spins = SDHCI_WRITE_SETTLE_SPINS;
|
||||
while (((reg = SDHCI_REG(SDHCI_SRS12)) &
|
||||
(SDHCI_SRS12_TC | SDHCI_SRS12_EINT)) == 0);
|
||||
(SDHCI_SRS12_TC | SDHCI_SRS12_EINT)) == 0) {
|
||||
if (is_multi_block && spins-- == 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,334 @@
|
|||
/* ubootenv.c
|
||||
*
|
||||
* U-Boot environment based A/B slot selection, RAUC-compatible.
|
||||
* See include/ubootenv.h for the format and the flow it replicates.
|
||||
*
|
||||
* Copyright (C) 2026 wolfSSL Inc.
|
||||
*
|
||||
* This file is part of wolfBoot.
|
||||
*
|
||||
* wolfBoot is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* wolfBoot is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ubootenv.h"
|
||||
|
||||
/* Standard (zlib) CRC32 - self-contained so this module has no dependencies and
|
||||
* is trivially unit-testable. Matches the CRC32 U-Boot uses for its env. */
|
||||
static uint32_t ubootenv_crc32(const uint8_t *p, size_t len)
|
||||
{
|
||||
uint32_t crc;
|
||||
size_t i;
|
||||
int b;
|
||||
|
||||
crc = 0xFFFFFFFFUL;
|
||||
for (i = 0; i < len; i++) {
|
||||
crc ^= (uint32_t)p[i];
|
||||
for (b = 0; b < 8; b++) {
|
||||
if ((crc & 1U) != 0U)
|
||||
crc = (crc >> 1) ^ 0xEDB88320UL;
|
||||
else
|
||||
crc >>= 1;
|
||||
}
|
||||
}
|
||||
return crc ^ 0xFFFFFFFFUL;
|
||||
}
|
||||
|
||||
static uint32_t rd_le32(const uint8_t *p)
|
||||
{
|
||||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
|
||||
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
static void wr_le32(uint8_t *p, uint32_t v)
|
||||
{
|
||||
p[0] = (uint8_t)(v);
|
||||
p[1] = (uint8_t)(v >> 8);
|
||||
p[2] = (uint8_t)(v >> 16);
|
||||
p[3] = (uint8_t)(v >> 24);
|
||||
}
|
||||
|
||||
/* Parse a small non-negative decimal; -1 on a non-numeric value. */
|
||||
static long env_atol(const char *s)
|
||||
{
|
||||
long v;
|
||||
|
||||
if (s == NULL)
|
||||
return -1;
|
||||
while (*s == ' ')
|
||||
s++;
|
||||
if (*s < '0' || *s > '9')
|
||||
return -1;
|
||||
v = 0;
|
||||
while (*s >= '0' && *s <= '9') {
|
||||
v = (v * 10) + (long)(*s - '0');
|
||||
s++;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/* Format a non-negative decimal into out (caller supplies >= 16 bytes). */
|
||||
static void env_ltoa(long v, char *out)
|
||||
{
|
||||
char tmp[16];
|
||||
int i;
|
||||
int j;
|
||||
|
||||
i = 0;
|
||||
j = 0;
|
||||
if (v <= 0) {
|
||||
out[0] = '0';
|
||||
out[1] = '\0';
|
||||
return;
|
||||
}
|
||||
while (v > 0 && i < (int)sizeof(tmp)) {
|
||||
tmp[i++] = (char)('0' + (v % 10));
|
||||
v /= 10;
|
||||
}
|
||||
while (i > 0)
|
||||
out[j++] = tmp[--i];
|
||||
out[j] = '\0';
|
||||
}
|
||||
|
||||
/* Build "BOOT_<name>_LEFT" into out. */
|
||||
static void env_leftkey(const char *name, char *out, size_t out_max)
|
||||
{
|
||||
const char *pre = "BOOT_";
|
||||
const char *suf = "_LEFT";
|
||||
size_t n = 0;
|
||||
|
||||
while (*pre != '\0' && n < out_max - 1)
|
||||
out[n++] = *pre++;
|
||||
while (*name != '\0' && n < out_max - 1)
|
||||
out[n++] = *name++;
|
||||
while (*suf != '\0' && n < out_max - 1)
|
||||
out[n++] = *suf++;
|
||||
out[n] = '\0';
|
||||
}
|
||||
|
||||
int uboot_env_verify(const uint8_t *env, size_t env_len)
|
||||
{
|
||||
if (env == NULL || env_len < 5)
|
||||
return 0;
|
||||
return (rd_le32(env) == ubootenv_crc32(env + 4, env_len - 4)) ? 1 : 0;
|
||||
}
|
||||
|
||||
void uboot_env_reseal(uint8_t *env, size_t env_len)
|
||||
{
|
||||
if (env == NULL || env_len < 5)
|
||||
return;
|
||||
wr_le32(env, ubootenv_crc32(env + 4, env_len - 4));
|
||||
}
|
||||
|
||||
int uboot_env_get(const uint8_t *env, size_t env_len, const char *key,
|
||||
char *val_out, size_t val_max)
|
||||
{
|
||||
const char *data;
|
||||
const char *end;
|
||||
const char *p;
|
||||
size_t keylen;
|
||||
|
||||
if (env == NULL || key == NULL || val_out == NULL || env_len < 5 ||
|
||||
val_max == 0)
|
||||
return -1;
|
||||
data = (const char *)(env + 4);
|
||||
end = data + (env_len - 4);
|
||||
keylen = strlen(key);
|
||||
p = data;
|
||||
while (p < end && *p != '\0') {
|
||||
const char *entry = p;
|
||||
size_t elen;
|
||||
while (p < end && *p != '\0')
|
||||
p++;
|
||||
elen = (size_t)(p - entry);
|
||||
if (p < end)
|
||||
p++; /* skip the entry NUL */
|
||||
if (elen > keylen && entry[keylen] == '=' &&
|
||||
memcmp(entry, key, keylen) == 0) {
|
||||
const char *v = entry + keylen + 1;
|
||||
size_t vlen = elen - keylen - 1;
|
||||
if (vlen >= val_max)
|
||||
vlen = val_max - 1;
|
||||
memcpy(val_out, v, vlen);
|
||||
val_out[vlen] = '\0';
|
||||
return (int)vlen;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int uboot_env_set(uint8_t *env, size_t env_len, const char *key,
|
||||
const char *val)
|
||||
{
|
||||
uint8_t *data;
|
||||
uint8_t *end;
|
||||
uint8_t *p;
|
||||
uint8_t *list_end;
|
||||
size_t keylen;
|
||||
size_t vlen;
|
||||
size_t entrylen;
|
||||
size_t old_total;
|
||||
size_t avail;
|
||||
|
||||
if (env == NULL || key == NULL || val == NULL || env_len < 5)
|
||||
return -1;
|
||||
data = env + 4;
|
||||
end = env + env_len;
|
||||
keylen = strlen(key);
|
||||
vlen = strlen(val);
|
||||
|
||||
/* 1) Measure the space the existing entries for key occupy plus the free
|
||||
* space, WITHOUT modifying anything, so a would-not-fit result below is
|
||||
* non-destructive (never deletes the old value on failure). */
|
||||
old_total = 0;
|
||||
p = data;
|
||||
while (p < end && *p != '\0') {
|
||||
uint8_t *entry = p;
|
||||
size_t elen;
|
||||
while (p < end && *p != '\0')
|
||||
p++;
|
||||
elen = (size_t)(p - entry);
|
||||
if (p < end)
|
||||
p++; /* include the NUL */
|
||||
if (elen > keylen && entry[keylen] == '=' &&
|
||||
memcmp(entry, key, keylen) == 0)
|
||||
old_total += (size_t)(p - entry);
|
||||
}
|
||||
list_end = p; /* start of the terminating NUL run */
|
||||
entrylen = keylen + 1 + vlen + 1;
|
||||
avail = (size_t)(end - list_end) + old_total; /* freed by removal + tail */
|
||||
if (entrylen + 1 > avail) /* +1 keeps the terminator */
|
||||
return -1;
|
||||
|
||||
/* 2) Remove any existing entry for key (shift the remainder down). */
|
||||
p = data;
|
||||
while (p < end && *p != '\0') {
|
||||
uint8_t *entry = p;
|
||||
size_t elen;
|
||||
while (p < end && *p != '\0')
|
||||
p++;
|
||||
elen = (size_t)(p - entry);
|
||||
if (p < end)
|
||||
p++; /* include the NUL */
|
||||
if (elen > keylen && entry[keylen] == '=' &&
|
||||
memcmp(entry, key, keylen) == 0) {
|
||||
size_t tail = (size_t)(end - p);
|
||||
memmove(entry, p, tail);
|
||||
memset(entry + tail, 0, (size_t)(end - (entry + tail)));
|
||||
p = entry; /* continue scanning from the compacted position */
|
||||
}
|
||||
}
|
||||
|
||||
/* 3) Append "key=val\0"; the following byte stays NUL (list terminator). */
|
||||
list_end = data;
|
||||
while (list_end < end && *list_end != '\0') {
|
||||
uint8_t *e = list_end;
|
||||
while (e < end && *e != '\0')
|
||||
e++;
|
||||
if (e < end)
|
||||
e++;
|
||||
list_end = e;
|
||||
}
|
||||
memcpy(list_end, key, keylen);
|
||||
list_end[keylen] = '=';
|
||||
memcpy(list_end + keylen + 1, val, vlen);
|
||||
list_end[keylen + 1 + vlen] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Copy the next space-separated token of BOOT_ORDER into name; advance *po.
|
||||
* Returns the token length (0 when the list is exhausted). */
|
||||
static int env_next_name(const char **po, char *name, size_t name_max)
|
||||
{
|
||||
const char *o = *po;
|
||||
int n = 0;
|
||||
|
||||
while (*o == ' ')
|
||||
o++;
|
||||
while (*o != '\0' && *o != ' ' && n < (int)name_max - 1)
|
||||
name[n++] = *o++;
|
||||
name[n] = '\0';
|
||||
*po = o;
|
||||
return n;
|
||||
}
|
||||
|
||||
int uboot_env_select_slot(uint8_t *env, size_t env_len, struct uboot_slot *out)
|
||||
{
|
||||
char order[UBOOT_ENV_VAL_MAX];
|
||||
char leftkey[UBOOT_ENV_VAL_MAX];
|
||||
char leftval[16];
|
||||
char name[UBOOT_ENV_VAL_MAX];
|
||||
const char *o;
|
||||
long left;
|
||||
int n;
|
||||
|
||||
if (env == NULL || out == NULL || env_len < 5)
|
||||
return -1;
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
/* Corrupt / blank env: reinitialize with defaults. */
|
||||
if (!uboot_env_verify(env, env_len)) {
|
||||
env_ltoa(UBOOT_ENV_DEFAULT_TRIES, leftval);
|
||||
memset(env, 0, env_len);
|
||||
(void)uboot_env_set(env, env_len, "BOOT_ORDER", "A B");
|
||||
(void)uboot_env_set(env, env_len, "BOOT_A_LEFT", leftval);
|
||||
(void)uboot_env_set(env, env_len, "BOOT_B_LEFT", leftval);
|
||||
uboot_env_reseal(env, env_len);
|
||||
}
|
||||
|
||||
if (uboot_env_get(env, env_len, "BOOT_ORDER", order, sizeof(order)) < 0) {
|
||||
memcpy(order, "A B", 4);
|
||||
(void)uboot_env_set(env, env_len, "BOOT_ORDER", order);
|
||||
uboot_env_reseal(env, env_len);
|
||||
}
|
||||
|
||||
/* Malformed (empty / whitespace-only) BOOT_ORDER yields no tokens: report
|
||||
* failure per the header contract rather than "success, no slot". */
|
||||
o = order;
|
||||
if (env_next_name(&o, name, sizeof(name)) == 0)
|
||||
return -1;
|
||||
|
||||
/* Pass 1: first slot with tries left -> select + decrement. */
|
||||
o = order;
|
||||
while ((n = env_next_name(&o, name, sizeof(name))) > 0) {
|
||||
env_leftkey(name, leftkey, sizeof(leftkey));
|
||||
left = UBOOT_ENV_DEFAULT_TRIES;
|
||||
if (uboot_env_get(env, env_len, leftkey, leftval, sizeof(leftval)) >= 0)
|
||||
left = env_atol(leftval);
|
||||
if (left > 0) {
|
||||
env_ltoa(left - 1, leftval);
|
||||
(void)uboot_env_set(env, env_len, leftkey, leftval);
|
||||
uboot_env_reseal(env, env_len);
|
||||
memcpy(out->name, name, (size_t)(n + 1));
|
||||
out->selected = 1;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pass 2: none left -> re-arm every counter, ask the caller to reboot. */
|
||||
env_ltoa(UBOOT_ENV_DEFAULT_TRIES, leftval);
|
||||
o = order;
|
||||
while (env_next_name(&o, name, sizeof(name)) > 0) {
|
||||
env_leftkey(name, leftkey, sizeof(leftkey));
|
||||
(void)uboot_env_set(env, env_len, leftkey, leftval);
|
||||
}
|
||||
uboot_env_reseal(env, env_len);
|
||||
out->rearmed = 1;
|
||||
out->selected = 0;
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
#!/bin/bash
|
||||
# prepare_emmc_rauc.sh - lay out the CM4 eMMC for production RAUC A/B boot under
|
||||
# wolfBoot (cm4_emmc_rauc.config). wolfBoot replaces GCX's U-Boot as the RAUC
|
||||
# slot arbiter: it reads a raw U-Boot-env partition, runs the BOOT_ORDER state
|
||||
# machine, and boots the shared kernel FIT with root= pointing at the active slot.
|
||||
#
|
||||
# GPT (0-based indices in [brackets]):
|
||||
# p1 boot FAT [0] : RPi fw + wolfBoot kernel8.img + config.txt (disable-bt)
|
||||
# p2 uboot-env raw [1] : U-Boot env (mkenvimage; RAUC fw_env.config -> here)
|
||||
# p3 fitImage raw [2] : wolfBoot-signed kernel FIT (shared)
|
||||
# p4 rootfs_A ext4 [3] : RAUC slot A (initial rootfs)
|
||||
# p5 rootfs_B ext4 [4] : RAUC slot B (initial rootfs copy)
|
||||
# p6 data ext4 [5] : persistent /data
|
||||
#
|
||||
# Usage:
|
||||
# cp config/examples/cm4_emmc_rauc.config .config
|
||||
# make wolfboot.bin CROSS_COMPILE=aarch64-none-elf- DEBUG=1
|
||||
# bash tools/scripts/cm4/prepare_emmc_rauc.sh (stage only, no root)
|
||||
# sudo bash tools/scripts/cm4/prepare_emmc_rauc.sh /dev/sdN (write, root)
|
||||
#
|
||||
# Copyright (C) 2026 wolfSSL Inc. GPLv3 (see wolfBoot COPYING).
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
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}"
|
||||
GCX_DEPLOY="${GCX_DEPLOY:-$HOME/Projects/GCX/iron-butterfly-os/build/tmp/deploy/images/raspberrypi4-64}"
|
||||
DEV="${1:-}"
|
||||
OUT="$HERE/rauc"
|
||||
|
||||
KERNEL_GZ="$GCX_DEPLOY/fitImage-linux.bin-raspberrypi4-64"
|
||||
ROOTFS="$GCX_DEPLOY/core-image-ironbutterfly-raspberrypi4-64.rootfs.ext4"
|
||||
DTB="$GCX_DEPLOY/bcm2711-rpi-cm4.dtb"
|
||||
FW_START="$GCX_DEPLOY/bootfiles/start4.elf"
|
||||
FW_FIXUP="$GCX_DEPLOY/bootfiles/fixup4.dat"
|
||||
|
||||
prep_artifacts() {
|
||||
local f
|
||||
if [ "${FORCE_PREP:-0}" != "1" ] && \
|
||||
[ -f "$OUT/cm4-fitImage_v1_signed.bin" ] && [ -f "$OUT/kernel8.img" ] && \
|
||||
[ -f "$OUT/config.txt" ] && [ -f "$OUT/uboot.env" ] && \
|
||||
[ -f "$OUT/start4.elf" ] && [ -f "$OUT/fixup4.dat" ] && \
|
||||
[ -f "$OUT/bcm2711-rpi-cm4.dtb" ]; then
|
||||
echo "== artifacts already staged in $OUT (skip prep; FORCE_PREP=1 to rebuild) =="
|
||||
return 0
|
||||
fi
|
||||
[ -f "$ROOT/wolfboot.bin" ] || { echo "!! build wolfboot.bin first (cm4_emmc_rauc.config)"; exit 1; }
|
||||
[ -f "$KEY" ] || { echo "!! signing key not found: $KEY"; exit 1; }
|
||||
for f in "$KERNEL_GZ" "$DTB" "$FW_START" "$FW_FIXUP"; do
|
||||
[ -f "$f" ] || { echo "!! missing GCX artifact: $f (set GCX_DEPLOY?)"; exit 1; }
|
||||
done
|
||||
command -v mkimage >/dev/null || { echo "!! mkimage not found (u-boot-tools)"; exit 1; }
|
||||
command -v mkenvimage >/dev/null || { echo "!! mkenvimage not found (u-boot-tools)"; exit 1; }
|
||||
|
||||
mkdir -p "$OUT"
|
||||
echo "== building + signing the shared kernel FIT =="
|
||||
cp -L "$KERNEL_GZ" "$OUT/linux.bin.gz"
|
||||
( cd "$OUT" && sed 's#\.\./Image\.gz#linux.bin.gz#' "$ROOT/hal/cm4.its" > cm4.its \
|
||||
&& mkimage -f cm4.its cm4-fitImage >/dev/null )
|
||||
( cd "$ROOT" && IMAGE_HEADER_SIZE="$IMG_HDR" ./tools/keytools/sign --ecc384 --sha384 \
|
||||
"$OUT/cm4-fitImage" "$KEY" 1 >/dev/null )
|
||||
[ -f "$OUT/cm4-fitImage_v1_signed.bin" ] || { echo "!! signing failed"; exit 1; }
|
||||
|
||||
echo "== generating initial U-Boot env (BOOT_ORDER 'A B', tries=3) =="
|
||||
printf 'BOOT_ORDER=A B\nBOOT_A_LEFT=3\nBOOT_B_LEFT=3\n' > "$OUT/env.txt"
|
||||
# 0x4000 must match wolfBoot UBOOT_ENV_SIZE and RAUC fw_env.config size.
|
||||
mkenvimage -s 0x4000 -o "$OUT/uboot.env" "$OUT/env.txt"
|
||||
|
||||
echo "== assembling boot partition files =="
|
||||
cp "$FW_START" "$OUT/start4.elf"
|
||||
cp "$FW_FIXUP" "$OUT/fixup4.dat"
|
||||
cp "$DTB" "$OUT/bcm2711-rpi-cm4.dtb"
|
||||
cp "$ROOT/wolfboot.bin" "$OUT/kernel8.img"
|
||||
cat > "$OUT/config.txt" <<'EOF'
|
||||
arm_64bit=1
|
||||
enable_uart=1
|
||||
dtoverlay=disable-bt
|
||||
init_uart_clock=48000000
|
||||
kernel=kernel8.img
|
||||
kernel_address=0x200000
|
||||
device_tree=bcm2711-rpi-cm4.dtb
|
||||
EOF
|
||||
echo "== staged in $OUT: $(ls "$OUT" | tr '\n' ' ')"
|
||||
}
|
||||
|
||||
write_device() {
|
||||
local t root_src root_pk P1 P2 P3 P4 P5 P6 MP p ok
|
||||
[ "$(id -u)" -eq 0 ] || { echo "!! writing $DEV requires root"; exit 1; }
|
||||
for t in sgdisk partprobe mkfs.vfat mkfs.ext4 lsblk udevadm dd tune2fs; do
|
||||
command -v "$t" >/dev/null || { echo "!! missing required tool: $t"; exit 1; }
|
||||
done
|
||||
[ -b "$DEV" ] || { echo "!! not a block device: $DEV"; exit 1; }
|
||||
[ -f "$ROOTFS" ] || { echo "!! rootfs not found: $ROOTFS (set GCX_DEPLOY)"; exit 1; }
|
||||
[ -f "$OUT/uboot.env" ] || { echo "!! env not staged; run prep first"; exit 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; }
|
||||
|
||||
echo "== GPT: boot 128M / uboot-env 8M / fitImage 64M / rootfs_A 4G / rootfs_B 4G / data (rest) =="
|
||||
sgdisk --zap-all "$DEV" >/dev/null
|
||||
sgdisk -n 1:2048:+128M -t 1:0700 -c 1:boot "$DEV" >/dev/null
|
||||
sgdisk -n 2:0:+8M -t 2:8300 -c 2:uboot-env "$DEV" >/dev/null
|
||||
sgdisk -n 3:0:+64M -t 3:8300 -c 3:fitImage "$DEV" >/dev/null
|
||||
sgdisk -n 4:0:+4G -t 4:8300 -c 4:rootfs_A "$DEV" >/dev/null
|
||||
sgdisk -n 5:0:+4G -t 5:8300 -c 5:rootfs_B "$DEV" >/dev/null
|
||||
sgdisk -n 6:0:0 -t 6:8300 -c 6:data "$DEV" >/dev/null
|
||||
partprobe "$DEV"; udevadm settle
|
||||
P1="${DEV}1"; P2="${DEV}2"; P3="${DEV}3"; P4="${DEV}4"; P5="${DEV}5"; P6="${DEV}6"
|
||||
[ -b "$P1" ] || { P1="${DEV}p1"; P2="${DEV}p2"; P3="${DEV}p3"; P4="${DEV}p4"; P5="${DEV}p5"; P6="${DEV}p6"; }
|
||||
# Assert every node exists before any dd/mkfs: a missing node makes dd/mkfs
|
||||
# silently create a regular file instead of writing the partition.
|
||||
for p in "$P1" "$P2" "$P3" "$P4" "$P5" "$P6"; 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 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 "$OUT"/start4.elf "$OUT"/fixup4.dat "$OUT"/bcm2711-rpi-cm4.dtb \
|
||||
"$OUT"/config.txt "$OUT"/kernel8.img "$MP"/
|
||||
sync; umount "$MP"
|
||||
|
||||
echo "== U-Boot env -> uboot-env ($P2) =="
|
||||
dd if="$OUT/uboot.env" of="$P2" bs=4k conv=fsync status=none
|
||||
echo "== signed kernel FIT -> fitImage ($P3) =="
|
||||
dd if="$OUT/cm4-fitImage_v1_signed.bin" of="$P3" bs=4k conv=fsync status=none
|
||||
|
||||
echo "== rootfs -> slot A ($P4) and slot B ($P5), then grow both =="
|
||||
dd if="$ROOTFS" of="$P4" bs=4M conv=fsync status=none
|
||||
dd if="$ROOTFS" of="$P5" bs=4M conv=fsync status=none
|
||||
for p in "$P4" "$P5"; do
|
||||
# e2fsck returns 1/2 when it fixed errors (not a failure), so || true is fine.
|
||||
command -v e2fsck >/dev/null && e2fsck -pf "$p" >/dev/null 2>&1 || true
|
||||
# Distinguish "tool missing" from "tool ran but failed" - a swallowed
|
||||
# resize2fs failure hides a real error behind a silent no-op.
|
||||
if command -v resize2fs >/dev/null; then
|
||||
resize2fs "$p" >/dev/null 2>&1 || echo " !! resize2fs failed on $p (rootfs left at image size)"
|
||||
else
|
||||
echo " (resize2fs missing; rootfs stays at image size)"
|
||||
fi
|
||||
done
|
||||
# Slots A and B were dd'd from the same image, so they share ext4 UUID+label.
|
||||
# Re-stamp a distinct random UUID + label on each so nothing resolves root by
|
||||
# a duplicate UUID/LABEL once RAUC updates a slot.
|
||||
if command -v tune2fs >/dev/null; then
|
||||
tune2fs -U random -L rootfs_A "$P4" >/dev/null 2>&1 || echo " !! tune2fs restamp failed on $P4 (slot A)"
|
||||
tune2fs -U random -L rootfs_B "$P5" >/dev/null 2>&1 || echo " !! tune2fs restamp failed on $P5 (slot B)"
|
||||
else
|
||||
echo " (tune2fs missing; A/B slots keep duplicate ext4 UUID/label)"
|
||||
fi
|
||||
echo "== data partition ($P6) =="
|
||||
mkfs.ext4 -q -L data "$P6" >/dev/null 2>&1 || true
|
||||
sync
|
||||
echo "== done. BOOT switch OFF, power-cycle, watch the CM4 PL011 console (ttyAMA0). =="
|
||||
echo " wolfBoot picks the RAUC slot from p2 and boots root=/dev/mmcblk0p4|p5."
|
||||
}
|
||||
|
||||
prep_artifacts
|
||||
if [ -n "$DEV" ]; then
|
||||
write_device
|
||||
else
|
||||
echo "(no device given; artifacts only. Re-run with: sudo bash $0 /dev/sdN)"
|
||||
fi
|
||||
|
|
@ -76,6 +76,7 @@ TESTS+=unit-tpm-nsc-cert
|
|||
TESTS+=unit-tpm-advio-zeroize
|
||||
TESTS+=unit-tpm-mfgid-eh-zeroize
|
||||
TESTS+=unit-pkcs11-nsc-zeroize
|
||||
TESTS+=unit-ubootenv
|
||||
TESTS+=unit-diagnostics
|
||||
TESTS+=unit-diagnostics-256
|
||||
TESTS+=unit-fit-gzip unit-fit-nogzip
|
||||
|
|
@ -553,6 +554,9 @@ unit-cm4-sdhci: ../../include/target.h unit-cm4-sdhci.c
|
|||
gcc -o $@ $^ $(CFLAGS) -I../.. -ffunction-sections -fdata-sections $(LDFLAGS) \
|
||||
-Wl,--gc-sections
|
||||
|
||||
unit-ubootenv: unit-ubootenv.c
|
||||
gcc -o $@ $< $(CFLAGS) -I../.. $(LDFLAGS)
|
||||
|
||||
unit-aes128: ../../include/target.h unit-extflash.c
|
||||
gcc -o $@ $^ $(CFLAGS) $(LDFLAGS)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,247 @@
|
|||
/* unit-ubootenv.c
|
||||
*
|
||||
* Unit tests for the U-Boot environment A/B slot selection (src/ubootenv.c):
|
||||
* the RAUC-compatible BOOT_ORDER / BOOT_<name>_LEFT state machine wolfBoot uses
|
||||
* to replace a U-Boot boot script.
|
||||
*
|
||||
* Copyright (C) 2026 wolfSSL Inc.
|
||||
*
|
||||
* This file is part of wolfBoot.
|
||||
*
|
||||
* wolfBoot is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* wolfBoot is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
|
||||
*/
|
||||
|
||||
#include <check.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../../src/ubootenv.c"
|
||||
|
||||
#define ENVLEN 512
|
||||
static uint8_t env[ENVLEN];
|
||||
|
||||
static void init_env(const char *order, int a, int b)
|
||||
{
|
||||
char v[16];
|
||||
memset(env, 0, ENVLEN);
|
||||
uboot_env_set(env, ENVLEN, "BOOT_ORDER", order);
|
||||
env_ltoa(a, v);
|
||||
uboot_env_set(env, ENVLEN, "BOOT_A_LEFT", v);
|
||||
env_ltoa(b, v);
|
||||
uboot_env_set(env, ENVLEN, "BOOT_B_LEFT", v);
|
||||
uboot_env_reseal(env, ENVLEN);
|
||||
}
|
||||
|
||||
/* Count how many entries start with "<key>=" in the data region. */
|
||||
static int count_key(const char *key)
|
||||
{
|
||||
const char *p = (const char *)(env + 4);
|
||||
const char *end = (const char *)(env + ENVLEN);
|
||||
size_t klen = strlen(key);
|
||||
int c = 0;
|
||||
while (p < end && *p != '\0') {
|
||||
const char *entry = p;
|
||||
size_t elen;
|
||||
while (p < end && *p != '\0')
|
||||
p++;
|
||||
elen = (size_t)(p - entry);
|
||||
if (p < end)
|
||||
p++;
|
||||
if (elen > klen && entry[klen] == '=' && memcmp(entry, key, klen) == 0)
|
||||
c++;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
START_TEST(test_crc_roundtrip)
|
||||
{
|
||||
init_env("A B", 3, 3);
|
||||
ck_assert_int_eq(uboot_env_verify(env, ENVLEN), 1);
|
||||
env[10] ^= 0xFF; /* corrupt a data byte */
|
||||
ck_assert_int_eq(uboot_env_verify(env, ENVLEN), 0);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_get_set_roundtrip)
|
||||
{
|
||||
char v[32];
|
||||
init_env("A B", 3, 3);
|
||||
ck_assert_int_ge(uboot_env_get(env, ENVLEN, "BOOT_ORDER", v, sizeof(v)), 0);
|
||||
ck_assert_str_eq(v, "A B");
|
||||
ck_assert_int_eq(uboot_env_get(env, ENVLEN, "NOPE", v, sizeof(v)), -1);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_set_overwrite_no_dup)
|
||||
{
|
||||
char v[16];
|
||||
init_env("A B", 3, 3);
|
||||
uboot_env_set(env, ENVLEN, "BOOT_A_LEFT", "2");
|
||||
ck_assert_int_eq(count_key("BOOT_A_LEFT"), 1);
|
||||
uboot_env_get(env, ENVLEN, "BOOT_A_LEFT", v, sizeof(v));
|
||||
ck_assert_str_eq(v, "2");
|
||||
/* the neighbouring key must survive the overwrite */
|
||||
uboot_env_get(env, ENVLEN, "BOOT_B_LEFT", v, sizeof(v));
|
||||
ck_assert_str_eq(v, "3");
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_select_blank_defaults)
|
||||
{
|
||||
struct uboot_slot s;
|
||||
char v[16];
|
||||
memset(env, 0xFF, ENVLEN); /* invalid CRC -> defaults */
|
||||
ck_assert_int_eq(uboot_env_select_slot(env, ENVLEN, &s), 0);
|
||||
ck_assert_int_eq(s.selected, 1);
|
||||
ck_assert_str_eq(s.name, "A");
|
||||
uboot_env_get(env, ENVLEN, "BOOT_A_LEFT", v, sizeof(v));
|
||||
ck_assert_str_eq(v, "2"); /* default 3, minus this boot */
|
||||
ck_assert_int_eq(uboot_env_verify(env, ENVLEN), 1); /* resealed */
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_select_decrement_and_failover)
|
||||
{
|
||||
struct uboot_slot s;
|
||||
char v[16];
|
||||
init_env("A B", 3, 3);
|
||||
uboot_env_select_slot(env, ENVLEN, &s);
|
||||
ck_assert_str_eq(s.name, "A");
|
||||
uboot_env_select_slot(env, ENVLEN, &s);
|
||||
ck_assert_str_eq(s.name, "A");
|
||||
uboot_env_select_slot(env, ENVLEN, &s);
|
||||
ck_assert_str_eq(s.name, "A");
|
||||
uboot_env_get(env, ENVLEN, "BOOT_A_LEFT", v, sizeof(v));
|
||||
ck_assert_str_eq(v, "0");
|
||||
/* A exhausted -> fail over to B, decrement B */
|
||||
uboot_env_select_slot(env, ENVLEN, &s);
|
||||
ck_assert_int_eq(s.selected, 1);
|
||||
ck_assert_str_eq(s.name, "B");
|
||||
uboot_env_get(env, ENVLEN, "BOOT_B_LEFT", v, sizeof(v));
|
||||
ck_assert_str_eq(v, "2");
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_select_rearm_when_exhausted)
|
||||
{
|
||||
struct uboot_slot s;
|
||||
char v[16];
|
||||
init_env("A B", 0, 0);
|
||||
ck_assert_int_eq(uboot_env_select_slot(env, ENVLEN, &s), 0);
|
||||
ck_assert_int_eq(s.selected, 0);
|
||||
ck_assert_int_eq(s.rearmed, 1);
|
||||
uboot_env_get(env, ENVLEN, "BOOT_A_LEFT", v, sizeof(v));
|
||||
ck_assert_str_eq(v, "3");
|
||||
uboot_env_get(env, ENVLEN, "BOOT_B_LEFT", v, sizeof(v));
|
||||
ck_assert_str_eq(v, "3");
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_select_order_b_first)
|
||||
{
|
||||
struct uboot_slot s;
|
||||
init_env("B A", 3, 3);
|
||||
uboot_env_select_slot(env, ENVLEN, &s);
|
||||
ck_assert_str_eq(s.name, "B");
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* A replacement that does not fit must fail WITHOUT destroying the old value. */
|
||||
START_TEST(test_set_nofit_nondestructive)
|
||||
{
|
||||
uint8_t small[40];
|
||||
char v[16];
|
||||
memset(small, 0, sizeof(small));
|
||||
ck_assert_int_eq(uboot_env_set(small, sizeof(small), "K", "9"), 0);
|
||||
ck_assert_int_eq(uboot_env_set(small, sizeof(small), "K",
|
||||
"12345678901234567890123456789012345678901234567890"), -1);
|
||||
ck_assert_int_eq(uboot_env_get(small, sizeof(small), "K", v, sizeof(v)), 1);
|
||||
ck_assert_str_eq(v, "9"); /* old value survives the failed set */
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* uboot_env_get truncates a value longer than val_max-1 and NUL-terminates. */
|
||||
START_TEST(test_get_truncation)
|
||||
{
|
||||
char v[4];
|
||||
init_env("A B", 3, 3);
|
||||
uboot_env_set(env, ENVLEN, "LONG", "abcdefgh");
|
||||
uboot_env_reseal(env, ENVLEN);
|
||||
ck_assert_int_eq(uboot_env_get(env, ENVLEN, "LONG", v, sizeof(v)), 3);
|
||||
ck_assert_str_eq(v, "abc");
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* An empty or whitespace-only BOOT_ORDER is malformed -> select returns -1. */
|
||||
START_TEST(test_select_empty_order)
|
||||
{
|
||||
struct uboot_slot s;
|
||||
memset(env, 0, ENVLEN);
|
||||
uboot_env_set(env, ENVLEN, "BOOT_ORDER", "");
|
||||
uboot_env_reseal(env, ENVLEN);
|
||||
ck_assert_int_eq(uboot_env_select_slot(env, ENVLEN, &s), -1);
|
||||
memset(env, 0, ENVLEN);
|
||||
uboot_env_set(env, ENVLEN, "BOOT_ORDER", " ");
|
||||
uboot_env_reseal(env, ENVLEN);
|
||||
ck_assert_int_eq(uboot_env_select_slot(env, ENVLEN, &s), -1);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* A non-numeric BOOT_<name>_LEFT counts as no tries -> that slot is skipped. */
|
||||
START_TEST(test_select_nonnumeric_left)
|
||||
{
|
||||
struct uboot_slot s;
|
||||
init_env("A B", 3, 3);
|
||||
uboot_env_set(env, ENVLEN, "BOOT_A_LEFT", "junk");
|
||||
uboot_env_reseal(env, ENVLEN);
|
||||
uboot_env_select_slot(env, ENVLEN, &s);
|
||||
ck_assert_int_eq(s.selected, 1);
|
||||
ck_assert_str_eq(s.name, "B");
|
||||
}
|
||||
END_TEST
|
||||
|
||||
Suite *ubootenv_suite(void)
|
||||
{
|
||||
Suite *s = suite_create("ubootenv");
|
||||
TCase *tc = tcase_create("ab_state_machine");
|
||||
|
||||
tcase_add_test(tc, test_crc_roundtrip);
|
||||
tcase_add_test(tc, test_get_set_roundtrip);
|
||||
tcase_add_test(tc, test_set_overwrite_no_dup);
|
||||
tcase_add_test(tc, test_select_blank_defaults);
|
||||
tcase_add_test(tc, test_select_decrement_and_failover);
|
||||
tcase_add_test(tc, test_select_rearm_when_exhausted);
|
||||
tcase_add_test(tc, test_select_order_b_first);
|
||||
tcase_add_test(tc, test_set_nofit_nondestructive);
|
||||
tcase_add_test(tc, test_get_truncation);
|
||||
tcase_add_test(tc, test_select_empty_order);
|
||||
tcase_add_test(tc, test_select_nonnumeric_left);
|
||||
suite_add_tcase(s, tc);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int fails;
|
||||
Suite *s = ubootenv_suite();
|
||||
SRunner *sr = srunner_create(s);
|
||||
|
||||
srunner_run_all(sr, CK_NORMAL);
|
||||
fails = srunner_ntests_failed(sr);
|
||||
srunner_free(sr);
|
||||
|
||||
return fails;
|
||||
}
|
||||
Loading…
Reference in New Issue