cm4: add RAUC A/B slot selection via U-Boot env (wolfBoot replaces U-Boot)

pull/863/head
David Garske 2026-08-06 22:09:51 -07:00 committed by Daniele Lacamera
parent 56651bde95
commit 67c819bc0b
10 changed files with 1099 additions and 17 deletions

3
.gitignore vendored
View File

@ -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
View File

@ -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

View File

@ -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
View File

@ -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)

91
include/ubootenv.h 100644
View File

@ -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 */

View File

@ -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;
}
}
}

334
src/ubootenv.c 100644
View File

@ -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;
}

View File

@ -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

View File

@ -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)

View File

@ -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;
}