diff --git a/.github/workflows/test-configs.yml b/.github/workflows/test-configs.yml index 4b973482ae..c6d361c82e 100644 --- a/.github/workflows/test-configs.yml +++ b/.github/workflows/test-configs.yml @@ -461,6 +461,15 @@ jobs: config-file: ./config/examples/tegra234-sdcard.config make-args: wolfboot.bin test-app/image_v1_signed.bin CROSS_COMPILE=aarch64-linux-gnu- + disk_boot_confirm_test: + uses: ./.github/workflows/test-build-aarch64.yml + with: + arch: aarch64 + # Boot confirmation and rollback on the disk path: the only job that + # compiles the DISK_BOOT_CONFIRM state-record code in src/update_disk.c. + config-file: ./config/examples/cm4_sdcard.config + make-args: DISK_BOOT_CONFIRM=1 wolfboot.bin + cm4_test: uses: ./.github/workflows/test-build-aarch64.yml with: diff --git a/docs/compile.md b/docs/compile.md index 92ca052d90..d1ce59c6df 100644 --- a/docs/compile.md +++ b/docs/compile.md @@ -350,6 +350,55 @@ ChaCha20 symmetric key to access the content of the updates. For more details about this optional feature, please refer to the [Encrypted external partitions](encrypted_partitions.md) manual page. +### Disk boot confirmation and rollback + +Targets that boot from a disk (`DISK_SDCARD=1`, `DISK_EMMC=1`, or an x86 FSP/AHCI target) use `src/update_disk.c`, which by default is stateless: it selects the highest-versioned slot, verifies it, and falls over to the other slot when *verification* fails. It writes nothing back, so an image that verifies and then fails to **boot** is retried indefinitely. + +`DISK_BOOT_CONFIRM=1` closes that gap using the **same partition trailer** the sector-swap flow in `src/update_flash.c` already keeps, placed in the tail of the raw boot partition: + +| Offset from the end of the partition | Size | Meaning | +| --- | --- | --- | +| -4 | 4 | Magic, the ASCII `BOOT` (`WOLFBOOT_MAGIC_TRAIL`) | +| -5 | 1 | Partition state | + +The states are the `IMG_STATE_*` values: `0xFF` new, `0x70` updating, `0x10` testing, `0x00` success. They are written at their default polarity regardless of `WOLFBOOT_FLAGS_INVERT`, because a format an external tool reads must not change meaning with a build option. On a default-polarity build the bytes are identical to a flash trailer's. + +The magic is load-bearing rather than decorative. A freshly imaged partition tail is usually `0x00`, and `0x00` is `IMG_STATE_SUCCESS`, so without a magic to gate the read a blank slot would look already confirmed. + +#### The cycle + +1. Something writes a new image into the idle slot and marks that slot **`updating`**. +2. wolfBoot selects it on version as usual, verifies it, and promotes `updating` to **`testing`** before handing over. +3. Whatever comes up clears it to **`success`**. +4. If the next boot still finds **`testing`**, that slot did not come up. wolfBoot skips it and boots the other slot. + +**A slot is only ever put on probation if an update was staged into it.** A slot that is `new`, or already `success`, is booted without anything being written, so a device that never stages an update is never probated and a steady-state boot performs no writes at all. This mirrors `src/update_ram.c`, which also promotes only `updating`. The consequence is deliberate: enabling this cannot strand a system whose OS does not confirm, but an integrator who writes an image with plain `dd` and never marks the slot gets no protection either. + +Skipping an unconfirmed slot writes nothing. Its version is dropped from the election in memory, which also removes it from the anti-rollback ceiling, and that is what lets an older confirmed slot boot. **Anti-rollback itself is not relaxed**: a slot that merely fails verification keeps its version and still blocks an older slot, exactly as before. + +#### Staging and confirming + +The `library_fs` target builds `lib-fs`, a userspace tool that speaks this format. Point it at the slot's partition device with `--dev`: + +``` +lib-fs --dev /dev/mmcblk1p1 stage # after writing a new image to the slot +lib-fs --dev /dev/mmcblk1p1 success # once the system is known good +lib-fs --dev /dev/mmcblk1p1 status # read the current state +``` + +`update-trigger` is refused with `--dev`: it marks the separate UPDATE partition at a compile-time offset, which against a raw slot device is just somewhere in the middle of the slot. `stage` is the disk analogue. + +Order the `success` call after whatever the system treats as proof of a healthy boot; a systemd unit ordered after the services that matter is the usual place. + +The tool needs no configuration to match the loader's layout and no rebuild per slot. With `--dev` it locates the trailer from the size of the device it was handed, which is how wolfBoot locates it too, and it writes the pinned state values rather than the `IMG_STATE_*` of its own build. `include/disk_trailer.h` holds the offset, the magic and the four state values, and both the loader and the tool include it, so there is one definition to disagree with rather than two. + +#### Constraints + +- The trailer must not overlap the image, so the partition has to be larger than the signed image by at least the 8 bytes of the trailer. wolfBoot refuses the write and reports it rather than corrupting the image, and still boots: the consequence of no trailer is no confirmation, not a dead system. +- A partition smaller than one 512-byte sector cannot carry a trailer and is likewise never armed. +- **Raw partitions only.** A `DISK_FS` slot is a file inside a filesystem and has no partition tail to claim. +- A slot left in `testing` is refused on every path into it, including the failover after another slot fails verification, so it stays out of the boot even in an `ALLOW_DOWNGRADE` build where the version guard is compiled out. + ### Disk boot from a read-only filesystem (FAT32 / ext4) Targets that boot from a disk (`DISK_SDCARD=1`, `DISK_EMMC=1`, or an x86 FSP/AHCI target) use `src/update_disk.c`, which by default reads the signed image from **raw offset 0 of a partition**: the image has to be written there with `dd`, and the partition cannot hold anything else. diff --git a/hal/filesystem.c b/hal/filesystem.c index 464748486f..bb60c7db52 100644 --- a/hal/filesystem.c +++ b/hal/filesystem.c @@ -28,6 +28,12 @@ #error "WOLFBOOT_PARTITION_FILENAME needs to be defined for filesystem HAL" #endif +/* The backing store. Defaults to the compile-time image, and + * hal_filesystem_set_target() can point it at something else at runtime - + * a raw partition device, for instance, so one binary can address several + * boot slots in turn. Changing it closes any file already open. */ +static const char *fs_target = WOLFBOOT_PARTITION_FILENAME; + #ifndef MIN #define MIN(x,y) ((x)<(y)?(x):(y)) #endif @@ -88,7 +94,7 @@ static int setup_file(byte read_only) } } if (fp == XBADFILE) { - fp = XFOPEN(WOLFBOOT_PARTITION_FILENAME, read_only ? "rb" : "r+b"); + fp = XFOPEN(fs_target, read_only ? "rb" : "r+b"); if (fp != XBADFILE) { fp_write = !read_only; if (XFSEEK(fp, 0, XSEEK_END) < 0 || (fp_size = XFTELL(fp)) < 0 || @@ -99,13 +105,26 @@ static int setup_file(byte read_only) } } else { - wolfBoot_printf("Failed to open file %s\n", - WOLFBOOT_PARTITION_FILENAME); + wolfBoot_printf("Failed to open file %s\n", fs_target); } } return fp != XBADFILE ? 0 : -1; } +/* Point the HAL at a different backing store. Closes any open handle so the + * next access reopens against the new target. */ +void hal_filesystem_set_target(const char *path) +{ + if (path == NULL) { + return; + } + if (fp != XBADFILE) { + XFCLOSE(fp); + fp = XBADFILE; + } + fs_target = path; +} + int ext_flash_write(uintptr_t address, const uint8_t *data, int len) { #ifdef DEBUG_EXT_FLASH diff --git a/hal/library_fs.c b/hal/library_fs.c index 11ef428e7d..446e9db889 100644 --- a/hal/library_fs.c +++ b/hal/library_fs.c @@ -26,6 +26,8 @@ #include "image.h" #include "printf.h" #include "wolfboot/wolfboot.h" +#include "hal.h" +#include "disk_trailer.h" /* Helper function to convert partition ID to string */ @@ -142,28 +144,168 @@ static int cmd_update_trigger(void) return 0; } +/* Set by "--dev " when that path is a disk boot slot. NULL means the + * tool is addressing the flat image it was built against, and every command + * uses the ordinary trailer accessors. */ +static const char* disk_dev = NULL; + +/* Boot confirmation on a disk slot is not the compile-time trailer. + * + * wolfBoot_get/set_partition_state() locate the trailer from + * WOLFBOOT_PARTITION_BOOT_ADDRESS + WOLFBOOT_PARTITION_SIZE and write + * IMG_STATE_* values, which flip with WOLFBOOT_FLAGS_INVERT. Neither is + * right here: the loader locates a slot's trailer from the size of the media + * it actually found, and pins the values so the format does not depend on + * how either program was built. So for "--dev" the three state commands go + * straight at the tail of the device, through include/disk_trailer.h - the + * same definitions src/update_disk.c uses. + */ +static int disk_trailer_open(const char* mode, FILE** fp, uint64_t* off) +{ + long end; + + *fp = fopen(disk_dev, mode); + if (*fp == NULL) { + wolfBoot_printf("Failed to open %s\n", disk_dev); + return -1; + } + if (fseek(*fp, 0, SEEK_END) != 0 || (end = ftell(*fp)) < 0) { + wolfBoot_printf("Cannot determine the size of %s\n", disk_dev); + fclose(*fp); + return -1; + } + if (disk_trailer_offset((uint64_t)end, off) != 0) { + wolfBoot_printf("%s is too small to carry a boot state\n", disk_dev); + fclose(*fp); + return -1; + } + if (fseek(*fp, (long)*off, SEEK_SET) != 0) { + wolfBoot_printf("Cannot seek to the tail of %s\n", disk_dev); + fclose(*fp); + return -1; + } + return 0; +} + +static int disk_state_get(uint8_t* state) +{ + uint8_t buf[DISK_TRAILER_SZ]; + uint64_t off = 0; + FILE* fp = NULL; + + if (disk_trailer_open("rb", &fp, &off) != 0) { + return -1; + } + if (fread(buf, 1, sizeof(buf), fp) != sizeof(buf)) { + wolfBoot_printf("Failed to read the tail of %s\n", disk_dev); + fclose(fp); + return -1; + } + fclose(fp); + *state = disk_trailer_decode(buf); + return 0; +} + +static int disk_state_set(uint8_t state) +{ + uint8_t buf[DISK_TRAILER_SZ]; + uint64_t off = 0; + FILE* fp = NULL; + + if (disk_trailer_open("r+b", &fp, &off) != 0) { + return -1; + } + disk_trailer_encode(buf, state); + if (fwrite(buf, 1, sizeof(buf), fp) != sizeof(buf)) { + wolfBoot_printf("Failed to write the tail of %s\n", disk_dev); + fclose(fp); + return -1; + } + if (fflush(fp) != 0) { + wolfBoot_printf("Failed to flush %s\n", disk_dev); + fclose(fp); + return -1; + } + fclose(fp); + return 0; +} + +/* Print a disk slot's boot state. */ +static int cmd_disk_status(void) +{ + uint8_t state = DISK_STATE_NEW; + + if (disk_state_get(&state) != 0) { + return -1; + } + wolfBoot_printf("\n"); + wolfBoot_printf("Disk boot slot: %s\n", disk_dev); + wolfBoot_printf("====================================\n"); + wolfBoot_printf("Boot state : %s (0x%02x)\n", + disk_trailer_state_name(state), (unsigned int)state); + wolfBoot_printf("\n"); + return 0; +} + /* Mark current boot as successful */ static int cmd_success(void) { + if (disk_dev != NULL) { + wolfBoot_printf("Confirming this boot slot...\n"); + if (disk_state_set(DISK_STATE_SUCCESS) != 0) { + return -1; + } + wolfBoot_printf("Slot marked SUCCESS.\n"); + return 0; + } wolfBoot_printf("Marking BOOT partition as SUCCESS...\n"); wolfBoot_success(); wolfBoot_printf("BOOT partition marked as SUCCESS.\n"); return 0; } +/* Mark the current backing store as carrying a staged update. + * + * On a flash target the application calls wolfBoot_update_trigger(), which + * marks the separate UPDATE partition. A disk slot is self-describing: the + * state lives in its own tail, so staging means marking the slot that was + * just written. wolfBoot promotes that to TESTING when it boots it, and + * "success" below clears it. */ +static int cmd_stage(void) +{ + if (disk_dev == NULL) { + wolfBoot_printf("\"stage\" applies to a disk boot slot; pass " + "--dev .\n"); + return -1; + } + wolfBoot_printf("Marking this slot as carrying a staged update...\n"); + if (disk_state_set(DISK_STATE_UPDATING) != 0) { + return -1; + } + wolfBoot_printf("Slot marked UPDATING; it will be put on probation at " + "the next boot.\n"); + return 0; +} + /* Print usage information */ static void print_usage(const char* prog_name) { wolfBoot_printf("wolfBoot Partition Manager CLI\n"); - wolfBoot_printf("\nUsage: %s [options]\n\n", prog_name); + wolfBoot_printf("\nUsage: %s [--dev ] \n\n", prog_name); wolfBoot_printf("Commands:\n"); wolfBoot_printf(" status - Show state of all partitions\n"); wolfBoot_printf(" keystore - Show keystore information\n"); wolfBoot_printf(" update-trigger - Trigger an update (sets UPDATE partition to UPDATING)\n"); + wolfBoot_printf(" stage - Mark this slot as carrying a staged update\n"); wolfBoot_printf(" success - Mark BOOT partition as SUCCESS\n"); wolfBoot_printf(" verify-boot - Verify integrity and authenticity of BOOT partition\n"); wolfBoot_printf(" verify-update - Verify integrity and authenticity of UPDATE partition\n"); wolfBoot_printf(" help - Show this help message\n"); + wolfBoot_printf("\nOptions:\n"); + wolfBoot_printf(" --dev - Operate on this backing store instead of the\n"); + wolfBoot_printf(" image built in. With a disk boot slot's\n"); + wolfBoot_printf(" partition device, \"status\", \"stage\" and\n"); + wolfBoot_printf(" \"success\" act on that slot's own tail.\n"); wolfBoot_printf("\nPartitions:\n"); wolfBoot_printf(" BOOT - Currently running firmware partition\n"); wolfBoot_printf(" UPDATE - Staging partition for new firmware\n"); @@ -212,28 +354,56 @@ int main(int argc, const char* argv[]) int ret = 0; const char* prog_name = "lib-fs"; const char* command; + int argi = 1; if (argc >= 1) { prog_name = argv[0]; } + /* Optional "--dev ": operate on that backing store instead of the + * image the binary was built against. Pointed at a disk boot slot's + * partition device, the three state commands locate the trailer from the + * size of that device, so one binary addresses any slot in turn with no + * rebuild and nothing to keep in sync with the loader's layout. */ + if (argc >= 3 && strcmp(argv[1], "--dev") == 0) { + disk_dev = argv[2]; + hal_filesystem_set_target(disk_dev); + argi = 3; + } + /* Check for argument count */ - if (argc != 2) { + if (argc != argi + 1) { print_usage(prog_name); return 1; } - command = argv[1]; + command = argv[argi]; /* Process commands */ if (strcmp(command, "status") == 0) { - ret = cmd_get_all_states(); + ret = (disk_dev != NULL) ? cmd_disk_status() : cmd_get_all_states(); } else if (strcmp(command, "keystore") == 0) { ret = cmd_get_keystore(); } else if (strcmp(command, "update-trigger") == 0) { - ret = cmd_update_trigger(); + /* Refused with --dev, and not merely as a nicety. update-trigger + * marks the separate UPDATE partition, which it locates from the + * compile-time WOLFBOOT_PARTITION_UPDATE offset. Against a raw slot + * device that offset is just somewhere in the middle of the slot, so + * the write would land on whatever lives there. A disk slot is + * self-describing and "stage" is its analogue. */ + if (disk_dev != NULL) { + wolfBoot_printf("\"update-trigger\" does not apply to a disk " + "boot slot; use \"stage\" instead.\n"); + ret = -1; + } + else { + ret = cmd_update_trigger(); + } + } + else if (strcmp(command, "stage") == 0) { + ret = cmd_stage(); } else if (strcmp(command, "success") == 0) { ret = cmd_success(); diff --git a/include/disk_trailer.h b/include/disk_trailer.h new file mode 100644 index 0000000000..b0d956eab9 --- /dev/null +++ b/include/disk_trailer.h @@ -0,0 +1,116 @@ +/* disk_trailer.h + * + * On-media boot-confirmation trailer for the disk boot path. + * + * 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 DISK_TRAILER_H +#define DISK_TRAILER_H + +#include + +/* This is the partition trailer update_flash.c already keeps, placed in the + * tail of a raw boot partition: the 4-byte "BOOT" magic in the last four + * bytes of the partition, and the state byte immediately below it. The magic + * is what tells "never written" from a state, and on a disk that is + * load-bearing rather than decorative: a freshly imaged partition tail is + * usually 0x00, and 0x00 is IMG_STATE_SUCCESS, so without the magic a blank + * slot would read as already confirmed. + * + * The states are the default-polarity IMG_STATE_* values, written literally. + * IMG_STATE_* flips with WOLFBOOT_FLAGS_INVERT, and a format two programs + * must agree on must not change meaning with a build option. The bytes on + * media are therefore the same as a flash trailer's on a default-polarity + * build, whatever polarity either program was built with. + * + * Both sides of the contract include this header - the loader in + * src/update_disk.c and the userspace tool in hal/library_fs.c - so the + * offset, the magic and the four state values have exactly one definition. + */ + +#define DISK_TRAILER_SZ 8U /* state byte + magic, read together */ +#define DISK_TRAILER_MIN_PART 512U /* one LBA, the smallest tail usable */ + +#define DISK_TRAILER_STATE_IDX 3U /* state byte within the trailer */ +#define DISK_TRAILER_MAGIC_IDX 4U /* "BOOT", to the end of the tail */ + +#define DISK_STATE_NEW 0xFFU /* IMG_STATE_NEW */ +#define DISK_STATE_UPDATING 0x70U /* IMG_STATE_UPDATING */ +#define DISK_STATE_TESTING 0x10U /* IMG_STATE_TESTING */ +#define DISK_STATE_SUCCESS 0x00U /* IMG_STATE_SUCCESS */ + +/* Byte offset of the trailer in a partition of size sz, or a non-zero return + * when the partition is too small to carry one. Derived from the size the + * caller observed, so both programs locate it the same way: from the media, + * never from a compile-time partition size. */ +static inline int disk_trailer_offset(uint64_t sz, uint64_t *off) +{ + if ((off == NULL) || (sz < DISK_TRAILER_MIN_PART)) { + return -1; + } + *off = sz - (uint64_t)DISK_TRAILER_SZ; + return 0; +} + +/* Fill buf with a trailer carrying state. */ +static inline void disk_trailer_encode(uint8_t *buf, uint8_t state) +{ + unsigned int i; + + for (i = 0; i < DISK_TRAILER_SZ; i++) { + buf[i] = 0xFF; + } + buf[DISK_TRAILER_STATE_IDX] = state; + buf[DISK_TRAILER_MAGIC_IDX + 0] = 'B'; + buf[DISK_TRAILER_MAGIC_IDX + 1] = 'O'; + buf[DISK_TRAILER_MAGIC_IDX + 2] = 'O'; + buf[DISK_TRAILER_MAGIC_IDX + 3] = 'T'; +} + +/* State recorded in buf. A missing magic reads as NEW, so a partition + * written by a tool that knows nothing of this is never treated as failed. */ +static inline uint8_t disk_trailer_decode(const uint8_t *buf) +{ + if ((buf[DISK_TRAILER_MAGIC_IDX + 0] != 'B') || + (buf[DISK_TRAILER_MAGIC_IDX + 1] != 'O') || + (buf[DISK_TRAILER_MAGIC_IDX + 2] != 'O') || + (buf[DISK_TRAILER_MAGIC_IDX + 3] != 'T')) { + return DISK_STATE_NEW; + } + return buf[DISK_TRAILER_STATE_IDX]; +} + +static inline const char *disk_trailer_state_name(uint8_t state) +{ + switch (state) { + case DISK_STATE_NEW: + return "NEW"; + case DISK_STATE_UPDATING: + return "UPDATING"; + case DISK_STATE_TESTING: + return "TESTING"; + case DISK_STATE_SUCCESS: + return "SUCCESS"; + default: + return "UNKNOWN"; + } +} + +#endif /* DISK_TRAILER_H */ diff --git a/include/hal.h b/include/hal.h index f042023c66..56347bfb78 100644 --- a/include/hal.h +++ b/include/hal.h @@ -59,6 +59,12 @@ void hal_deinit(); void hal_init(void); +#ifdef WOLFBOOT_PARTITION_FILENAME +/* Repoint the filesystem HAL's backing store at runtime, so one binary can + * address several boot slots in turn. Implemented by hal/filesystem.c. */ +void hal_filesystem_set_target(const char *path); +#endif + /* Timer functions (platform-specific, used for benchmarking) */ #if defined(WOLFBOOT_UPDATE_DISK) || defined(BOOT_BENCHMARK) uint64_t hal_get_timer_us(void); diff --git a/options.mk b/options.mk index ce6915d098..10ea21b054 100644 --- a/options.mk +++ b/options.mk @@ -813,6 +813,22 @@ ifneq ($(filter 1,$(DISK_SDCARD) $(DISK_EMMC)),) endif endif +# Optional boot confirmation and rollback for disk boot (src/update_disk.c). +# wolfBoot marks the slot it boots as TESTING in the last sector of that raw +# partition; the OS clears it to SUCCESS once it is up. A slot still marked +# TESTING on the next boot did not come up and is failed over. Off by default, +# so every existing disk target keeps its stateless select-verify-boot +# behaviour. Raw partitions only: a DISK_FS slot has no partition tail to use. +DISK_BOOT_CONFIRM ?= 0 +ifeq ($(DISK_BOOT_CONFIRM),1) + ifeq ($(WOLFBOOT_TARGET_BUILD),1) + ifeq (,$(findstring WOLFBOOT_UPDATE_DISK,$(CFLAGS))) + $(error DISK_BOOT_CONFIRM requires a disk-boot target (DISK_SDCARD=1, DISK_EMMC=1, or an x86 FSP/AHCI target)) + endif + endif + CFLAGS+=-D"DISK_BOOT_CONFIRM=1" +endif + # Optional read-only filesystem support for disk boot (src/update_disk.c), # so a boot slot can name a file instead of requiring the signed image at # raw offset 0 of a partition. DISK_FS = fat32 | ext4 | both. Leaving it diff --git a/src/update_disk.c b/src/update_disk.c index 0d77858c6d..5290f881bf 100644 --- a/src/update_disk.c +++ b/src/update_disk.c @@ -44,6 +44,9 @@ #include "printf.h" #include "wolfboot/wolfboot.h" #include "disk.h" +#ifdef DISK_BOOT_CONFIRM +#include "disk_trailer.h" +#endif #ifdef WOLFBOOT_DISK_FS #include "disk_fs.h" #endif @@ -316,6 +319,119 @@ struct boot_slot { * WOLFBOOT_SMALL_STACK=1. */ static struct boot_slot boot_slots[2]; +#ifdef DISK_BOOT_CONFIRM +/* Boot confirmation for the disk path. include/disk_trailer.h owns the + * format: the offset, the magic and the four state values are defined there + * once and shared with the userspace tool that stages and confirms. */ + +/* Not on the stack: the disk targets that build WOLFBOOT_SMALL_STACK=1 keep + * boot_slots and the filesystem cache off it for the same reason. */ +static uint8_t disk_trailer[DISK_TRAILER_SZ]; + +/* Last known state of each slot, filled in by disk_boot_state_reap(). */ +static uint8_t slot_state[2]; + +/* Byte offset of the trailer within the slot's partition, or -1 when the slot + * cannot carry one. */ +static int slot_trailer_off(struct boot_slot *s, uint64_t *off) +{ + uint64_t sz = 0; + + if ((s == NULL) || (off == NULL) || (s->ready == 0)) { + return -1; + } +#ifdef WOLFBOOT_DISK_FS + /* A file-backed slot has no partition tail to claim: the filesystem owns + * it. Boot confirmation is raw-partition only. */ + if (s->vol.type != FS_TYPE_RAW) { + return -1; + } +#endif + if (disk_part_size(BOOT_DISK, s->part, &sz) != 0) { + return -1; + } + return disk_trailer_offset(sz, off); +} + +/* Current state of a slot. */ +static int slot_state_read(struct boot_slot *s, uint8_t *state) +{ + uint64_t off = 0; + + if (state == NULL) { + return -1; + } + *state = DISK_STATE_NEW; + if (slot_trailer_off(s, &off) != 0) { + return -1; + } + if (disk_part_read(BOOT_DISK, s->part, off, DISK_TRAILER_SZ, + disk_trailer) != (int)DISK_TRAILER_SZ) { + return -1; + } + *state = disk_trailer_decode(disk_trailer); + return 0; +} + +/* Record a slot's state. img_end is the first byte past the image, so a + * trailer that would land inside it is refused rather than corrupting it. */ +static int slot_state_write(struct boot_slot *s, uint8_t state, + uint64_t img_end) +{ + uint64_t off = 0; + + if (slot_trailer_off(s, &off) != 0) { + return -1; + } + if (off < img_end) { + wolfBoot_printf("Boot state would overlap the image on p%d\r\n", + s->part); + return -1; + } + disk_trailer_encode(disk_trailer, state); + if (disk_part_write(BOOT_DISK, s->part, off, DISK_TRAILER_SZ, + disk_trailer) != (int)DISK_TRAILER_SZ) { + return -1; + } + return 0; +} + +/* Read both slots' states, and drop any slot still marked TESTING out of the + * election. Such a slot was armed before the previous boot and nothing + * confirmed it, so whatever it started did not come up. + * + * Nothing is written here. Zeroing the version in memory removes the slot + * from the selection AND from max_ver, which is what lets an older surviving + * slot boot without relaxing the anti-rollback guard for anything still live. + * A slot that merely fails verification keeps its version and still blocks an + * older one, exactly as before. */ +static void disk_boot_state_reap(uint32_t *pA_ver, uint32_t *pB_ver) +{ + uint8_t st; + int i; + + for (i = 0; i < 2; i++) { + st = DISK_STATE_NEW; + slot_state[i] = DISK_STATE_NEW; + if (slot_state_read(&boot_slots[i], &st) != 0) { + continue; + } + slot_state[i] = st; + if (st != DISK_STATE_TESTING) { + continue; + } + wolfBoot_printf("Slot %c was not confirmed; skipping it\r\n", + 'A' + i); + if (i == 0) { + *pA_ver = 0; + } + else { + *pB_ver = 0; + } + } +} +#endif /* DISK_BOOT_CONFIRM */ + /** * @brief Read from a boot slot. * @@ -600,6 +716,10 @@ void RAMFUNCTION wolfBoot_start(void) #endif } +#ifdef DISK_BOOT_CONFIRM + disk_boot_state_reap(&pA_ver, &pB_ver); +#endif + if ((pB_ver == 0) && (pA_ver == 0)) { #ifdef DISK_ENCRYPT disk_decrypted_header_clear(dec_hdr); @@ -626,6 +746,20 @@ void RAMFUNCTION wolfBoot_start(void) failures++; slot = &boot_slots[selected]; cur_part = (uint32_t)slot->part; +#ifdef DISK_BOOT_CONFIRM + /* A slot still in TESTING did not confirm last boot. Zeroing its + * version in disk_boot_state_reap() takes it out of the election, + * but a failover below (selected ^= 1) can still land on it, and + * with ALLOW_DOWNGRADE defined the version guard that would other- + * wise refuse it is compiled out. Refuse it here so the exclusion + * holds on every path into the slot, not just the first choice. */ + if (slot_state[selected] == DISK_STATE_TESTING) { + wolfBoot_printf("Slot %c was not confirmed; not booting it\r\n", + 'A' + selected); + selected ^= 1; + continue; + } +#endif #ifndef ALLOW_DOWNGRADE { uint32_t cur_ver = selected ? pB_ver_u : pA_ver_u; @@ -814,6 +948,27 @@ void RAMFUNCTION wolfBoot_start(void) * disk_close(BOOT_DISK) is deferred to just before hal_prepare_boot(). */ wolfBoot_printf("Firmware Valid.\r\n"); +#ifdef DISK_BOOT_CONFIRM + /* Put the slot on probation, but only if an update was staged into it. + * This mirrors update_ram.c, which promotes UPDATING to TESTING and + * nothing else: a device that never stages an update is never probated, + * so enabling this cannot strand a system whose OS does not confirm. A + * slot already SUCCESS, or never written, is left untouched and a + * steady-state boot writes nothing at all. */ + if (slot_state[selected] == DISK_STATE_UPDATING) { + if (slot_state_write(&boot_slots[selected], DISK_STATE_TESTING, + (uint64_t)IMAGE_HEADER_SIZE + (uint64_t)os_image.fw_size) + != 0) { + /* Loud, because the consequence is silent: without the TESTING + * mark a failed boot of this image is never detected and the + * slot is retried for ever. */ + wolfBoot_printf("WARNING: could not arm boot confirmation on " + "p%d; a failed boot of this image will not be detected\r\n", + boot_slots[selected].part); + } + } +#endif + load_address = (uint32_t*)os_image.fw_base; #ifdef WOLFBOOT_FDT diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 9306819684..07dd8e0f6e 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -67,7 +67,7 @@ TESTS:=unit-parser unit-parser-large-header unit-fdt unit-extflash unit-string \ unit-update-flash-hook \ unit-update-flash-self-update \ unit-update-flash-enc unit-update-flash-enc-full unit-update-ram unit-update-ram-uboot unit-update-ram-enc unit-update-ram-enc-nopart unit-update-ram-nofixed unit-update-ram-nofixed-noramboot unit-update-ram-noramboot unit-update-ram-custom-trailer unit-custom-trailer-nopart unit-update-flash-hwswap unit-pkcs11_store unit-psa_store unit-wolfhsm_flash_hal unit-disk \ - unit-update-disk unit-update-disk-fsp unit-update-disk-oob unit-update-disk-fit unit-multiboot unit-boot-x86-fsp unit-loader-tpm-init unit-qspi-flash unit-fwtpm-stub unit-tpm-rsa-exp \ + unit-update-disk unit-update-disk-confirm unit-update-disk-fsp unit-update-disk-oob unit-update-disk-fit unit-multiboot unit-boot-x86-fsp unit-loader-tpm-init unit-qspi-flash unit-fwtpm-stub unit-tpm-rsa-exp \ unit-image-nopart unit-image-sha384 unit-image-sha3-384 unit-image-dts \ unit-image-dts-sha384 unit-image-dts-sha3-384 unit-store-sbrk \ unit-tpm-blob unit-policy-create unit-policy-sign unit-rot-auth unit-sdhci-response-bits \ @@ -375,6 +375,8 @@ unit-custom-trailer-nopart:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN \ # the cap update_disk.c now requires; all images here are exactly that size. unit-update-disk:CFLAGS+=-DMOCK_PARTITIONS -DPRINTF_ENABLED -DWOLFBOOT_RAMBOOT_MAX_SIZE=0x40 \ -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE +unit-update-disk-confirm:CFLAGS+=-DMOCK_PARTITIONS -DPRINTF_ENABLED -DWOLFBOOT_RAMBOOT_MAX_SIZE=0x400 \ + -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE # Non-FSP disk-boot OOB regression (CRIT-03). WOLFBOOT_RAMBOOT_MAX_SIZE is the # cap the loader applies to the unauthenticated header fw_size before loading to # RAM, the same bound update_disk.c and update_ram.c enforce. @@ -965,6 +967,9 @@ unit-update-flash-hwswap: ../../include/target.h unit-update-flash-hwswap.c unit-update-disk: ../../include/target.h unit-update-disk.c gcc -o $@ unit-update-disk.c $(CFLAGS) $(LDFLAGS) +unit-update-disk-confirm: ../../include/target.h unit-update-disk-confirm.c + gcc -o $@ unit-update-disk-confirm.c $(CFLAGS) $(LDFLAGS) + # WOLFBOOT_FSP (x86) boot path of update_disk.c: the low-memory (tolum) # size check must reject a slot and fall back to the other one, like every # other per-slot rejection in the retry loop. diff --git a/tools/unit-tests/unit-update-disk-confirm.c b/tools/unit-tests/unit-update-disk-confirm.c new file mode 100644 index 0000000000..436da27691 --- /dev/null +++ b/tools/unit-tests/unit-update-disk-confirm.c @@ -0,0 +1,597 @@ +#define WOLFBOOT_UPDATE_DISK +#define WOLFBOOT_SELF_UPDATE_MONOLITHIC +#define RAM_CODE +#define WOLFBOOT_SELF_HEADER +#define EXT_ENCRYPTED +#define ENCRYPT_WITH_CHACHA +#define HAVE_CHACHA +#define IMAGE_HEADER_SIZE 256 +#define BOOT_PART_A 0 +#define BOOT_PART_B 1 +#define MOCK_ADDRESS_BOOT 0xCD000000 +#define DISK_BOOT_CONFIRM + +#include +#include +#include +#include + +#include "hal.h" +#include "target.h" +#include "wolfboot/wolfboot.h" +#include "image.h" +#include "loader.h" +#include + +#define TEST_PAYLOAD_SIZE 64 + +/* Sized for the whole partition, not just TEST_PAYLOAD_SIZE: the + * trailer-overlap test below stages a deliberately large image. */ +static uint8_t load_buffer[IMAGE_HEADER_SIZE + TEST_PAYLOAD_SIZE + 1024]; +#define WOLFBOOT_LOAD_ADDRESS ((uintptr_t)load_buffer) + +/* A real partition is far larger than its image; the boot-state record lives + * in its last 512-byte sector. Size the mock so image and record cannot + * overlap, which is the layout the loader requires. */ +#define TEST_PART_SIZE (IMAGE_HEADER_SIZE + TEST_PAYLOAD_SIZE + 1024) +#define TEST_STATE_OFF (TEST_PART_SIZE - 512) + +static uint8_t part_a_image[TEST_PART_SIZE]; +static uint8_t part_b_image[TEST_PART_SIZE]; +/* What disk_part_size() reports. Settable, because the loader locates the + * trailer from the size the media reports: shrinking it is the only way to + * reach the "trailer would overlap the image" and "partition too small" + * refusals, and TEST_PART_SIZE always leaves room for the trailer. */ +static uint64_t mock_part_size; +static int mock_disk_init_ret; +static int mock_disk_close_called; +static int mock_do_boot_called; +static const uint32_t *mock_boot_address; +static int mock_fail_payload_part; +static int mock_verify_integrity_ret; +/* not_ext as the verify calls saw it: the staged image lives in RAM, so an + * EXT_FLASH build must not route these reads back through external flash. */ +static int mock_integrity_saw_not_ext; +static int mock_authenticity_saw_not_ext; +static int mock_verify_authenticity_ret; +static int mock_state_writes; + +ChaCha chacha; + +static void set_u16_le(uint8_t *dst, uint16_t value) +{ + dst[0] = (uint8_t)(value & 0xFF); + dst[1] = (uint8_t)(value >> 8); +} + +static void set_u32_le(uint8_t *dst, uint32_t value) +{ + dst[0] = (uint8_t)(value & 0xFF); + dst[1] = (uint8_t)((value >> 8) & 0xFF); + dst[2] = (uint8_t)((value >> 16) & 0xFF); + dst[3] = (uint8_t)(value >> 24); +} + +static void build_image(uint8_t *image, uint32_t version, uint8_t fill) +{ + memset(image, 0, IMAGE_HEADER_SIZE + TEST_PAYLOAD_SIZE); + set_u32_le(image, WOLFBOOT_MAGIC); + set_u32_le(image + sizeof(uint32_t), TEST_PAYLOAD_SIZE); + set_u16_le(image + IMAGE_HEADER_OFFSET, HDR_VERSION); + set_u16_le(image + IMAGE_HEADER_OFFSET + sizeof(uint16_t), 4); + set_u32_le(image + IMAGE_HEADER_OFFSET + 2 * sizeof(uint16_t), version); + memset(image + IMAGE_HEADER_SIZE, fill, TEST_PAYLOAD_SIZE); +} +/* Same, with an explicit payload size, for the test that needs an image + * large enough to reach the partition tail. */ +static void build_image_sized(uint8_t *image, uint32_t version, uint8_t fill, + uint32_t payload_sz) +{ + memset(image, 0, IMAGE_HEADER_SIZE + payload_sz); + set_u32_le(image, WOLFBOOT_MAGIC); + set_u32_le(image + sizeof(uint32_t), payload_sz); + set_u16_le(image + IMAGE_HEADER_OFFSET, HDR_VERSION); + set_u16_le(image + IMAGE_HEADER_OFFSET + sizeof(uint16_t), 4); + set_u32_le(image + IMAGE_HEADER_OFFSET + 2 * sizeof(uint16_t), version); + memset(image + IMAGE_HEADER_SIZE, fill, payload_sz); +} + + +static int mock_flash_protect_called; +static haladdr_t mock_flash_protect_addr; +static int mock_flash_protect_len; + +static void reset_mocks(void) +{ + mock_part_size = TEST_PART_SIZE; + memset(load_buffer, 0, sizeof(load_buffer)); + memset(part_a_image, 0, sizeof(part_a_image)); + memset(part_b_image, 0, sizeof(part_b_image)); + build_image(part_a_image, 1, 0xA1); + build_image(part_b_image, 2, 0xB2); + mock_disk_init_ret = 0; + mock_disk_close_called = 0; + mock_do_boot_called = 0; + mock_boot_address = NULL; + mock_fail_payload_part = -1; + mock_verify_integrity_ret = 0; + mock_verify_authenticity_ret = 0; + mock_state_writes = 0; + mock_integrity_saw_not_ext = -1; + mock_authenticity_saw_not_ext = -1; + mock_flash_protect_called = 0; + mock_flash_protect_addr = 0; + mock_flash_protect_len = 0; + wolfBoot_panicked = 0; +} + +int chacha_init(void) +{ + return 0; +} + +int wc_Chacha_SetIV(ChaCha* ctx, const byte* inIv, word32 counter) +{ + (void)ctx; + (void)inIv; + (void)counter; + return 0; +} + +int wc_Chacha_Process(ChaCha* ctx, byte* output, const byte* input, word32 msglen) +{ + (void)ctx; + memmove(output, input, msglen); + return 0; +} + +void wc_ForceZero(void* mem, size_t len) +{ + volatile uint8_t *p = (volatile uint8_t *)mem; + while (len-- > 0) { + *p++ = 0; + } +} + +int wolfBoot_initialize_encryption(void) +{ + return 0; +} + +int wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce) +{ + memset(key, 0x5A, ENCRYPT_KEY_SIZE); + memset(nonce, 0xC3, ENCRYPT_NONCE_SIZE); + return 0; +} + +int disk_init(int drv) +{ + (void)drv; + return mock_disk_init_ret; +} + +int disk_open(int drv) +{ + (void)drv; + return 0; +} + +void disk_close(int drv) +{ + (void)drv; + mock_disk_close_called++; +} + +int disk_part_read(int drv, int part, uint64_t off, uint64_t sz, uint8_t *buf) +{ + uint8_t *image; + + (void)drv; + image = (part == BOOT_PART_B) ? part_b_image : part_a_image; + if ((mock_fail_payload_part == part) && (off >= IMAGE_HEADER_SIZE) && + (off < TEST_STATE_OFF)) + return -1; + if ((off > TEST_PART_SIZE) || (sz > (TEST_PART_SIZE - off))) + return -1; + memcpy(buf, image + off, (size_t)sz); + return (int)sz; +} + +int disk_part_write(int drv, int part, uint64_t off, uint64_t sz, + const uint8_t *buf) +{ + uint8_t *image; + + (void)drv; + image = (part == BOOT_PART_B) ? part_b_image : part_a_image; + if ((off > TEST_PART_SIZE) || (sz > (TEST_PART_SIZE - off))) + return -1; + memcpy(image + off, buf, (size_t)sz); + mock_state_writes++; + return (int)sz; +} + +int disk_part_size(int drv, int part, uint64_t *size) +{ + (void)drv; + (void)part; + if (size == NULL) + return -1; + *size = mock_part_size; + return 0; +} + +int wolfBoot_open_image_address(struct wolfBoot_image* img, uint8_t* image) +{ + uint32_t magic; + uint32_t fw_size; + + memcpy(&magic, image, sizeof(magic)); + + if (magic != WOLFBOOT_MAGIC) + return -1; + memset(img, 0, sizeof(*img)); + img->hdr = image; + memcpy(&fw_size, image + sizeof(uint32_t), sizeof(fw_size)); + img->fw_size = fw_size; + img->fw_base = image + IMAGE_HEADER_SIZE; + img->hdr_ok = 1; + return 0; +} + +int wolfBoot_verify_integrity(struct wolfBoot_image* img) +{ + mock_integrity_saw_not_ext = img->not_ext; + if (mock_verify_integrity_ret == 0) + img->sha_ok = 1; + return mock_verify_integrity_ret; +} + +int wolfBoot_verify_authenticity(struct wolfBoot_image* img) +{ + mock_authenticity_saw_not_ext = img->not_ext; + if (mock_verify_authenticity_ret == 0) + img->signature_ok = 1; + return mock_verify_authenticity_ret; +} + +int wolfBoot_get_dts_size(void *dts_addr, uint32_t capacity) +{ + (void)capacity; + (void)dts_addr; + return -1; +} + +void hal_prepare_boot(void) +{ +} + +void do_boot(const uint32_t *address) +{ + mock_do_boot_called++; + mock_boot_address = address; +} + +int hal_flash_protect(haladdr_t address, int len) +{ + mock_flash_protect_called++; + mock_flash_protect_addr = address; + mock_flash_protect_len = len; + return 0; +} + +#include "update_disk.c" + + +/* Mirror of the on-disk trailer the loader writes: "BOOT" in the last four + * bytes of the partition, state immediately below it. Same layout as the + * flash partition trailer, with default-polarity IMG_STATE_* values. */ +#define ST_NEW 0xFFU +#define ST_UPDATING 0x70U +#define ST_TESTING 0x10U +#define ST_SUCCESS 0x00U + +static void set_state(uint8_t *part, uint8_t state) +{ + uint8_t *t = part + TEST_PART_SIZE - 8; + + memset(t, 0xFF, 8); + t[3] = state; + t[4] = 'B'; t[5] = 'O'; t[6] = 'O'; t[7] = 'T'; +} + +/* No trailer at all: the tail is left exactly as an imaging tool leaves it. */ +static void set_no_trailer(uint8_t *part, uint8_t fill) +{ + memset(part + TEST_PART_SIZE - 8, fill, 8); +} + +/* The same, at an arbitrary partition size, for the tests that shrink it. */ +static void set_state_at(uint8_t *part, size_t part_sz, uint8_t state) +{ + uint8_t *t = part + part_sz - 8; + + memset(t, 0xFF, 8); + t[3] = state; + t[4] = 'B'; t[5] = 'O'; t[6] = 'O'; t[7] = 'T'; +} + +static uint8_t get_state_at(const uint8_t *part, size_t part_sz) +{ + const uint8_t *t = part + part_sz - 8; + + if ((t[4] != 'B') || (t[5] != 'O') || (t[6] != 'O') || (t[7] != 'T')) + return ST_NEW; + return t[3]; +} + +static uint8_t get_state(const uint8_t *part) +{ + const uint8_t *t = part + TEST_PART_SIZE - 8; + + if ((t[4] != 'B') || (t[5] != 'O') || (t[6] != 'O') || (t[7] != 'T')) + return ST_NEW; + return t[3]; +} + +/* A slot with no update staged must boot and must NOT be armed. This is the + * property that stops the feature stranding a system whose OS never confirms: + * without it, boot 1 arms A, boot 2 retires A and arms B, boot 3 retires B, + * and nothing is left to boot. */ +START_TEST(test_no_update_staged_is_not_armed) +{ + reset_mocks(); + build_image(part_a_image, 7, 0xA1); + memset(part_b_image, 0, sizeof(part_b_image)); + set_state(part_a_image, ST_SUCCESS); + + wolfBoot_start(); + + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_do_boot_called, 1); + ck_assert_uint_eq(get_state(part_a_image), ST_SUCCESS); + ck_assert_int_eq(mock_state_writes, 0); +} +END_TEST + +/* A blank partition tail must read as NEW, not as SUCCESS. An imaged tail is + * usually 0x00, which IS IMG_STATE_SUCCESS, so the magic is what keeps a + * never-written slot from looking already-confirmed. */ +START_TEST(test_blank_tail_reads_as_new) +{ + reset_mocks(); + build_image(part_a_image, 7, 0xA1); + memset(part_b_image, 0, sizeof(part_b_image)); + /* 0x00 is what an imaging tool leaves, and 0x00 is also IMG_STATE_SUCCESS */ + set_no_trailer(part_a_image, 0x00); + + wolfBoot_start(); + + ck_assert_int_eq(mock_do_boot_called, 1); + ck_assert_int_eq(mock_state_writes, 0); + ck_assert_uint_eq(get_state(part_a_image), ST_NEW); +} +END_TEST + +/* The magic must actually gate the state read. A tail with no magic whose + * bytes happen to look like IMG_STATE_UPDATING must still read as NEW: if + * the magic check is dropped, this slot gets armed on the strength of + * uninitialised media. */ +START_TEST(test_no_magic_is_not_mistaken_for_updating) +{ + reset_mocks(); + build_image(part_a_image, 7, 0xA1); + memset(part_b_image, 0, sizeof(part_b_image)); + set_no_trailer(part_a_image, ST_UPDATING); + + wolfBoot_start(); + + ck_assert_int_eq(mock_do_boot_called, 1); + /* no magic, so NEW, so nothing armed and nothing written */ + ck_assert_int_eq(mock_state_writes, 0); +} +END_TEST + +/* A staged update is promoted to TESTING before handoff. */ +START_TEST(test_staged_update_is_armed) +{ + reset_mocks(); + build_image(part_a_image, 7, 0xA1); + memset(part_b_image, 0, sizeof(part_b_image)); + set_state(part_a_image, ST_UPDATING); + + wolfBoot_start(); + + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_do_boot_called, 1); + ck_assert_uint_eq(get_state(part_a_image), ST_TESTING); +} +END_TEST + +/* A slot left TESTING did not confirm: it is skipped, and the other slot + * boots. Nothing is written on this path. */ +START_TEST(test_unconfirmed_slot_is_skipped) +{ + reset_mocks(); + build_image(part_a_image, 7, 0xA1); + build_image(part_b_image, 7, 0xB2); + set_state(part_a_image, ST_TESTING); + set_state(part_b_image, ST_SUCCESS); + + wolfBoot_start(); + + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_do_boot_called, 1); + ck_assert_int_eq(memcmp(load_buffer, part_b_image + IMAGE_HEADER_SIZE, + TEST_PAYLOAD_SIZE), 0); + /* the retired slot is left as it was: the reap writes nothing */ + ck_assert_uint_eq(get_state(part_a_image), ST_TESTING); + ck_assert_int_eq(mock_state_writes, 0); +} +END_TEST + +/* Skipping a slot also drops its version from the ceiling, which is what + * lets an older confirmed slot boot. */ +START_TEST(test_skipped_slot_lets_older_slot_boot) +{ + reset_mocks(); + build_image(part_a_image, 4, 0xA1); + build_image(part_b_image, 5, 0xB2); + set_state(part_a_image, ST_SUCCESS); + set_state(part_b_image, ST_TESTING); + + wolfBoot_start(); + + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_do_boot_called, 1); + ck_assert_int_eq(memcmp(load_buffer, part_a_image + IMAGE_HEADER_SIZE, + TEST_PAYLOAD_SIZE), 0); +} +END_TEST + +/* Anti-rollback is NOT relaxed. A slot that merely fails verification keeps + * its version, so an older slot is still refused. Only a slot retired for + * failing to boot steps aside. */ +START_TEST(test_verification_failure_does_not_relax_antirollback) +{ + reset_mocks(); + build_image(part_a_image, 4, 0xA1); + build_image(part_b_image, 5, 0xB2); + set_state(part_a_image, ST_SUCCESS); + set_state(part_b_image, ST_SUCCESS); + mock_verify_authenticity_ret = -1; + + wolfBoot_start(); + + ck_assert_int_eq(wolfBoot_panicked, 1); + ck_assert_int_eq(mock_do_boot_called, 0); +} +END_TEST + +/* An image that fills its partition leaves no tail to claim, so the arming + * write must be refused rather than writing over the image it just verified. + * The boot still proceeds: the consequence of no trailer is no confirmation, + * not a dead system. + * + * The partition is kept at or above DISK_TRAILER_MIN_PART and the image made + * large enough to reach the tail, so the overlap check is the only thing + * that can refuse this - the minimum-size check cannot also fire and make + * the test pass for the wrong reason. */ +START_TEST(test_arming_is_refused_when_the_trailer_would_hit_the_image) +{ + const uint32_t big = 1024; + const uint64_t part_sz = IMAGE_HEADER_SIZE + big; /* image to the end */ + uint8_t *tail; + + reset_mocks(); + build_image_sized(part_a_image, 4, 0xA1, big); + build_image(part_b_image, 4, 0xB2); + mock_part_size = part_sz; + ck_assert(part_sz >= DISK_TRAILER_MIN_PART); + + /* A trailer at the offset that size implies, which is inside the image. */ + tail = part_a_image + part_sz - DISK_TRAILER_SZ; + memset(tail, 0xFF, DISK_TRAILER_SZ); + tail[3] = ST_UPDATING; + tail[4] = 'B'; tail[5] = 'O'; tail[6] = 'O'; tail[7] = 'T'; + + wolfBoot_start(); + + /* Booted anyway ... */ + ck_assert_int_eq(mock_do_boot_called, 1); + ck_assert_int_eq(wolfBoot_panicked, 0); + /* ... and the refusal left the image bytes alone: still UPDATING, never + * promoted to TESTING over the top of the payload. */ + ck_assert_uint_eq(tail[3], ST_UPDATING); +} +END_TEST + +/* A partition too small to hold a trailer at all must not be armed, and must + * still boot. */ +START_TEST(test_partition_too_small_for_a_trailer_still_boots) +{ + reset_mocks(); + build_image(part_a_image, 4, 0xA1); + build_image(part_b_image, 4, 0xB2); + + /* Below DISK_TRAILER_MIN_PART, but still clear of the image: the trailer + * would land at 392 and the image ends at 320, so the overlap check + * would NOT refuse this. The minimum-size check is the only guard, which + * is what makes this test specific to it. */ + mock_part_size = 400; + ck_assert(mock_part_size < DISK_TRAILER_MIN_PART); + ck_assert(mock_part_size - DISK_TRAILER_SZ >= + IMAGE_HEADER_SIZE + TEST_PAYLOAD_SIZE); + + /* A valid UPDATING trailer where that size puts it, so the only reason + * not to arm is the refusal under test. */ + set_state_at(part_a_image, (size_t)mock_part_size, ST_UPDATING); + + wolfBoot_start(); + + ck_assert_int_eq(mock_do_boot_called, 1); + ck_assert_int_eq(wolfBoot_panicked, 0); + /* Nothing was written: the tail still reads UPDATING. */ + ck_assert_uint_eq(get_state_at(part_a_image, (size_t)mock_part_size), + ST_UPDATING); +} +END_TEST + +/* Both slots unconfirmed: neither may boot. + * + * Reaping zeroes both versions, which makes max_ver zero and so leaves the + * anti-rollback guard inactive - it only fires when something newer exists. + * Nothing but the explicit TESTING refusal in the retry loop stops the first + * slot booting here, which is exactly the hole a failover (selected ^= 1) + * opens on the other paths into a slot. */ +START_TEST(test_two_unconfirmed_slots_boot_neither) +{ + reset_mocks(); + build_image(part_a_image, 4, 0xA1); + build_image(part_b_image, 4, 0xB2); + set_state(part_a_image, ST_TESTING); + set_state(part_b_image, ST_TESTING); + + wolfBoot_start(); + + ck_assert_int_eq(mock_do_boot_called, 0); + /* More than one panic: with both versions reaped to zero the "no valid + * OS image in either partition" guard fires first, and wolfBoot_panic() + * halts on a real target but returns in this harness. What matters is + * that nothing booted. */ + ck_assert_int_gt(wolfBoot_panicked, 0); +} +END_TEST + +Suite *wolfboot_suite(void) +{ + Suite *s = suite_create("wolfBoot"); + TCase *tc = tcase_create("update-disk-confirm"); + + tcase_add_test(tc, test_no_update_staged_is_not_armed); + tcase_add_test(tc, test_blank_tail_reads_as_new); + tcase_add_test(tc, test_no_magic_is_not_mistaken_for_updating); + tcase_add_test(tc, test_staged_update_is_armed); + tcase_add_test(tc, test_unconfirmed_slot_is_skipped); + tcase_add_test(tc, test_skipped_slot_lets_older_slot_boot); + tcase_add_test(tc, test_verification_failure_does_not_relax_antirollback); + tcase_add_test(tc, test_arming_is_refused_when_the_trailer_would_hit_the_image); + tcase_add_test(tc, test_partition_too_small_for_a_trailer_still_boots); + tcase_add_test(tc, test_two_unconfirmed_slots_boot_neither); + suite_add_tcase(s, tc); + + return s; +} + +int main(void) +{ + int fails; + Suite *s = wolfboot_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + return fails; +}