Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/test-configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
49 changes: 49 additions & 0 deletions docs/compile.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
25 changes: 22 additions & 3 deletions hal/filesystem.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ||
Expand All @@ -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
Expand Down
180 changes: 175 additions & 5 deletions hal/library_fs.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
#include "image.h"
#include "printf.h"
#include "wolfboot/wolfboot.h"
#include "hal.h"
#include "disk_trailer.h"
Comment on lines 26 to +30


/* Helper function to convert partition ID to string */
Expand Down Expand Up @@ -142,28 +144,168 @@ static int cmd_update_trigger(void)
return 0;
}

/* Set by "--dev <path>" 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 <path>.\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 <command> [options]\n\n", prog_name);
wolfBoot_printf("\nUsage: %s [--dev <path>] <command>\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 <path> - 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");
Expand Down Expand Up @@ -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 <path>": 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();
Expand Down
Loading
Loading