From 67c819bc0b9446492667c8dd0d5b53ed10f53380 Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 6 Aug 2026 22:09:51 -0700 Subject: [PATCH] cm4: add RAUC A/B slot selection via U-Boot env (wolfBoot replaces U-Boot) --- .gitignore | 3 + arch.mk | 23 +- config/examples/cm4_emmc_rauc.config | 66 +++++ hal/cm4.c | 146 ++++++++++- include/ubootenv.h | 91 +++++++ src/sdhci.c | 22 +- src/ubootenv.c | 334 +++++++++++++++++++++++++ tools/scripts/cm4/prepare_emmc_rauc.sh | 180 +++++++++++++ tools/unit-tests/Makefile | 4 + tools/unit-tests/unit-ubootenv.c | 247 ++++++++++++++++++ 10 files changed, 1099 insertions(+), 17 deletions(-) create mode 100644 config/examples/cm4_emmc_rauc.config create mode 100644 include/ubootenv.h create mode 100644 src/ubootenv.c create mode 100755 tools/scripts/cm4/prepare_emmc_rauc.sh create mode 100644 tools/unit-tests/unit-ubootenv.c diff --git a/.gitignore b/.gitignore index 05618e8f..909dede2 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/arch.mk b/arch.mk index 8dc481ca..3ad87647 100644 --- a/arch.mk +++ b/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 diff --git a/config/examples/cm4_emmc_rauc.config b/config/examples/cm4_emmc_rauc.config new file mode 100644 index 00000000..262f14c2 --- /dev/null +++ b/config/examples/cm4_emmc_rauc.config @@ -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__LEFT state machine, decrements the try counter +# (so a hung slot fails over), and injects "root= rauc.slot=". +# +# 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 diff --git a/hal/cm4.c b/hal/cm4.c index c928e0cc..edaae30a 100644 --- a/hal/cm4.c +++ b/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 " root= rauc.slot= 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) diff --git a/include/ubootenv.h b/include/ubootenv.h new file mode 100644 index 00000000..ed1b6955 --- /dev/null +++ b/include/ubootenv.h @@ -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__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= rauc.slot=". 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 +#include + +/* 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 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) = 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__LEFT = UBOOT_ENV_DEFAULT_TRIES); + * - pick the first name in BOOT_ORDER whose BOOT__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 */ diff --git a/src/sdhci.c b/src/sdhci.c index 39247df2..cbca0b37 100644 --- a/src/sdhci.c +++ b/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; + } } } diff --git a/src/ubootenv.c b/src/ubootenv.c new file mode 100644 index 00000000..409cfde6 --- /dev/null +++ b/src/ubootenv.c @@ -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 +#include +#include + +#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__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; +} diff --git a/tools/scripts/cm4/prepare_emmc_rauc.sh b/tools/scripts/cm4/prepare_emmc_rauc.sh new file mode 100755 index 00000000..03ce359e --- /dev/null +++ b/tools/scripts/cm4/prepare_emmc_rauc.sh @@ -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 diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 670e0fab..c65f5582 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -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) diff --git a/tools/unit-tests/unit-ubootenv.c b/tools/unit-tests/unit-ubootenv.c new file mode 100644 index 00000000..4abc884e --- /dev/null +++ b/tools/unit-tests/unit-ubootenv.c @@ -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__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 +#include +#include + +#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 "=" 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__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; +}