Kory Maincent 02673659e8 tools: Add support for fwumdata tool
Add a new fwumdata tool to allows users to read, display, and modify FWU
(Firmware Update) metadata from Linux userspace. It provides functionality
similar to fw_printenv/fw_setenv but for FWU metadata. Users can view
metadata, change active/previous bank indices, modify bank states, and set
image acceptance flags. Configuration is done via fwumdata.config file.

Signed-off-by: Kory Maincent <kory.maincent@bootlin.com>
Tested-by: Dario Binacchi <dario.binacchi@amarulasolutions.com>
Signed-off-by: Ilias Apalodimas <ilias.apalodimas@linaro.org>
2026-03-26 08:20:00 +02:00

855 lines
20 KiB
C

// SPDX-License-Identifier: GPL-2.0+
/*
* FWU Metadata Read/Write Tool
* Copyright (c) 2025, Kory Maincent <kory.maincent@bootlin.com>
*
* Tool to read, display, and modify FWU (Firmware Update) metadata
* from Linux userspace. Similar to fw_printenv/fw_setenv for U-Boot
* environment, but for FWU metadata.
*
* Usage:
* fwumdata - Print all metadata
* fwumdata -u - Print metadata and update it if CRC corrupted
* fwumdata -c <config> - Use custom config file
* fwumdata -a <bank> - Set active bank
* fwumdata -p <bank> - Set previous bank
* fwumdata -s <bank> <state> - Set bank state (V2 only)
* fwumdata -i <id> -b <bank> -A - Accept image
* fwumdata -i <id> -b <bank> -C - Clear image acceptance
* fwumdata -i <id> -b <bank>
* -B <num_banks>
* -I <num_images> -C - Clear image acceptance (V1 only)
* fwumdata -l - List detailed info with GUIDs
*/
#include <errno.h>
#include <getopt.h>
#include <stdio.h>
#include <unistd.h>
#include <mtd/mtd-user.h>
#include <sys/ioctl.h>
#include <u-boot/crc.h>
#include "fwumdata.h"
/* Device configuration */
struct fwumdata_device {
const char *devname;
long long devoff;
unsigned long mdata_size;
unsigned long erase_size;
int fd;
bool is_mtd;
};
/* Global state */
static struct fwumdata_device devices[2]; /* Primary and secondary */
static struct fwu_mdata *mdata;
static int have_redundant;
static struct fwu_mdata *valid_mdata;
static bool mdata_mod;
static const char *config_file;
static int nbanks, nimages; /* For V1 only */
static const char * const default_config_files[] = {
"./fwumdata.config",
"/etc/fwumdata.config",
NULL
};
/* GUID/UUID utilities */
static void guid_to_string(const struct efi_guid *guid, char *str)
{
sprintf(str, "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x",
guid->time_high, guid->time_low, guid->reserved,
guid->family, guid->node[0],
guid->node[1], guid->node[2], guid->node[3],
guid->node[4], guid->node[5], guid->node[6]);
}
/* Config file parsing */
static int parse_config(const char *fname)
{
size_t linesize = 0;
char *line = NULL;
char *devname;
int i = 0;
FILE *fp;
int rc;
fp = fopen(fname, "r");
if (!fp)
return -ENOENT;
while (i < 2 && getline(&line, &linesize, fp) != -1) {
/* Skip comments and empty lines */
if (line[0] == '#' || line[0] == '\n')
continue;
rc = sscanf(line, "%ms %lli %lx %lx",
&devname,
&devices[i].devoff,
&devices[i].mdata_size,
&devices[i].erase_size);
if (rc < 3) {
free(devname);
continue;
}
if (rc < 4)
devices[i].erase_size = devices[i].mdata_size;
devices[i].devname = devname;
i++;
}
free(line);
fclose(fp);
if (i == 2) {
have_redundant = true;
if (devices[0].mdata_size != devices[1].mdata_size) {
fprintf(stderr,
"Size mismatch between the two metadata\n");
return -EINVAL;
}
}
if (!i) {
fprintf(stderr,
"Can't read config %s content\n", fname);
return -EINVAL;
}
return 0;
}
static int find_parse_config(void)
{
int i;
if (config_file)
return parse_config(config_file);
for (i = 0; default_config_files[i]; i++) {
int ret;
ret = parse_config(default_config_files[i]);
if (ret == -ENOENT)
continue;
if (ret)
return ret;
config_file = default_config_files[i];
return 0;
}
fprintf(stderr, "Error: Cannot find config file\n");
return -ENOENT;
}
static int open_device(struct fwumdata_device *dev)
{
if (strstr(dev->devname, "/dev/mtd"))
dev->is_mtd = true;
dev->fd = open(dev->devname, O_RDWR | O_SYNC);
if (dev->fd < 0) {
fprintf(stderr, "Cannot open %s: %s\n", dev->devname,
strerror(errno));
return -ENODEV;
}
return 0;
}
static int mtd_erase(int fd, unsigned long offset, unsigned long size)
{
struct erase_info_user erase;
int ret;
erase.start = offset;
erase.length = size;
ret = ioctl(fd, MEMERASE, &erase);
if (ret < 0) {
fprintf(stderr, "MTD erase failed: %s\n", strerror(errno));
return -errno;
}
return 0;
}
static int read_device(struct fwumdata_device *dev, void *buf, size_t count)
{
if (lseek(dev->fd, dev->devoff, SEEK_SET) < 0) {
fprintf(stderr, "Seek failed: %s\n", strerror(errno));
return -errno;
}
if (read(dev->fd, buf, count) < 0) {
fprintf(stderr, "Read failed: %s\n", strerror(errno));
return -errno;
}
return 0;
}
static int write_device(struct fwumdata_device *dev, const void *buf,
size_t count)
{
int ret;
/* Erase if MTD device */
if (dev->is_mtd) {
ret = mtd_erase(dev->fd, dev->devoff, dev->erase_size);
if (ret)
return ret;
}
if (lseek(dev->fd, dev->devoff, SEEK_SET) < 0) {
fprintf(stderr, "Seek failed: %s\n", strerror(errno));
return -errno;
}
if (write(dev->fd, buf, count) < 0) {
fprintf(stderr, "Write failed: %s\n", strerror(errno));
return -errno;
}
return 0;
}
/* Metadata operations */
static int validate_crc(struct fwu_mdata *mdata, size_t size)
{
u32 calc_crc, stored_crc;
stored_crc = mdata->crc32;
calc_crc = crc32(0, (const u8 *)&mdata->version, size - sizeof(u32));
if (calc_crc != stored_crc) {
fprintf(stderr,
"CRC mismatch: calculated 0x%08x, stored 0x%08x\n",
calc_crc, stored_crc);
if (mdata->version == 1)
fprintf(stderr,
"Metadata is V1, this may be size description issue\n");
return -1;
}
return 0;
}
static void update_crc(struct fwu_mdata *mdata, size_t size)
{
mdata->crc32 = crc32(0, (const u8 *)&mdata->version, size - sizeof(u32));
}
static int read_one_metadata(int mdata_id, size_t size)
{
int ret;
ret = open_device(&devices[mdata_id]);
if (ret)
return ret;
ret = read_device(&devices[mdata_id], &mdata[mdata_id], size);
if (ret)
return ret;
if (mdata[mdata_id].version != 1 && mdata[mdata_id].version != 2) {
fprintf(stderr, "Invalid metadata %d version: %u\n",
mdata_id, mdata[mdata_id].version);
}
return 0;
}
static int read_metadata(bool update)
{
size_t alloc_size;
int ret;
/* Allocate initial buffer */
alloc_size = devices[0].mdata_size;
mdata = calloc(have_redundant ? 2 : 1, alloc_size);
if (!mdata) {
fprintf(stderr, "Memory allocation failed\n");
return -ENOMEM;
}
ret = read_one_metadata(0, alloc_size);
if (ret)
return ret;
if (validate_crc(&mdata[0], alloc_size) < 0) {
fprintf(stderr,
"Warning: Primary metadata CRC validation failed\n");
mdata_mod = update;
} else {
valid_mdata = &mdata[0];
}
if (have_redundant) {
ret = read_one_metadata(1, alloc_size);
if (ret)
return ret;
if (validate_crc(&mdata[1], alloc_size) < 0) {
fprintf(stderr,
"Warning: Secondary metadata CRC validation failed\n");
mdata_mod = update;
} else if (valid_mdata && mdata[0].crc32 != mdata[1].crc32) {
fprintf(stderr,
"Metadatas valid but not equal, use first one as default\n");
mdata_mod = update;
} else {
valid_mdata = &mdata[1];
}
}
if (!valid_mdata) {
fprintf(stderr,
"No metadata valid, use first one as default\n");
mdata_mod = update;
valid_mdata = &mdata[0];
}
if (valid_mdata->version == 2) {
struct fwu_mdata_ext *mdata_ext;
mdata_ext = fwu_get_fw_mdata_ext(valid_mdata);
if (mdata_ext->metadata_size != alloc_size) {
fprintf(stderr,
"Metadata real size 0x%x mismatch with the config 0x%zx\n",
mdata_ext->metadata_size, alloc_size);
return -EINVAL;
}
}
return 0;
}
static int write_metadata(void)
{
size_t write_size = devices[0].mdata_size;
int ret;
if (!mdata_mod)
return 0;
/* Update CRC */
update_crc(valid_mdata, write_size);
/* Write primary */
ret = write_device(&devices[0], valid_mdata, write_size);
if (ret < 0) {
fprintf(stderr, "Failed to write primary metadata\n");
return ret;
}
/* Write secondary if redundant */
if (have_redundant) {
ret = write_device(&devices[1], valid_mdata, write_size);
if (ret < 0) {
fprintf(stderr, "Failed to write secondary metadata\n");
return -1;
}
}
printf("FWU metadata updated successfully\n");
mdata_mod = 0;
return 0;
}
/* Display functions */
static const char *bank_state_to_string(u8 state)
{
switch (state) {
case FWU_BANK_ACCEPTED:
return "accepted";
case FWU_BANK_VALID:
return "valid";
case FWU_BANK_INVALID:
return "invalid";
default:
return "unknown";
}
}
static void print_metadata_summary(void)
{
int i;
printf("FWU Metadata:\n");
printf("\tVersion: %u\n", valid_mdata->version);
printf("\tActive Index: %u\n", valid_mdata->active_index);
printf("\tPrevious Index: %u\n", valid_mdata->previous_active_index);
printf("\tCRC32: 0x%08x\n", valid_mdata->crc32);
if (valid_mdata->version == 2) {
struct fwu_fw_store_desc *fw_desc;
struct fwu_mdata_ext *mdata_ext;
mdata_ext = fwu_get_fw_mdata_ext(valid_mdata);
printf("\tMetadata Size: %u bytes\n", mdata_ext->metadata_size);
printf("\tDescriptor Offset: %u\n", mdata_ext->desc_offset);
printf("\tBank States:\n");
fw_desc = fwu_get_fw_desc(valid_mdata);
for (i = 0; i < fw_desc->num_banks && i < MAX_BANKS_V2; i++) {
printf("\t\tBank %d: %s (0x%02x)\n", i,
bank_state_to_string(mdata_ext->bank_state[i]),
mdata_ext->bank_state[i]);
}
}
}
static void print_metadata_detailed(void)
{
struct fwu_fw_store_desc *fw_desc = NULL;
struct fwu_image_bank_info *bank_info;
struct fwu_image_entry *img_entry;
int num_images, num_banks;
char guid_str[64];
int i, j;
print_metadata_summary();
if (valid_mdata->version == 1) {
num_images = nimages;
num_banks = nbanks;
} else {
fw_desc = fwu_get_fw_desc(valid_mdata);
num_images = fw_desc->num_images;
num_banks = fw_desc->num_banks;
}
if (fw_desc) {
printf("\n\tFirmware Store Descriptor:\n");
printf("\t\tNumber of Banks: %u\n", num_banks);
printf("\t\tNumber of Images: %u\n", num_images);
printf("\t\tImage Entry Size: %u\n", fw_desc->img_entry_size);
printf("\t\tBank Info Entry Size: %u\n", fw_desc->bank_info_entry_size);
}
printf("\n\tImages:\n");
for (i = 0; i < num_images; i++) {
img_entry = fwu_get_image_entry(valid_mdata, valid_mdata->version,
num_banks, i);
printf("\t\tImage %d:\n", i);
guid_to_string(&img_entry->image_type_guid, guid_str);
printf("\t\t\tImage Type GUID: %s\n", guid_str);
guid_to_string(&img_entry->location_guid, guid_str);
printf("\t\t\tLocation GUID: %s\n", guid_str);
printf("\t\t\tBanks:\n");
for (j = 0; j < num_banks; j++) {
bank_info = fwu_get_bank_info(valid_mdata,
valid_mdata->version,
num_banks, i, j);
guid_to_string(&bank_info->image_guid, guid_str);
printf("\t\t\t\tBank %d:\n", j);
printf("\t\t\t\t\tImage GUID: %s\n", guid_str);
printf("\t\t\t\t\tAccepted: %s (%u)\n",
(bank_info->accepted & FWU_IMAGE_ACCEPTED) ? "yes" : "no",
bank_info->accepted);
}
}
}
/* Modification functions */
static int set_active_index(int bank)
{
struct fwu_fw_store_desc *fw_desc;
int num_banks;
if (valid_mdata->version == 2) {
fw_desc = fwu_get_fw_desc(valid_mdata);
num_banks = fw_desc->num_banks;
} else {
num_banks = nbanks;
}
if (bank < 0 || bank >= num_banks) {
fprintf(stderr, "Error: Invalid bank %d (must be 0-%d)\n",
bank, num_banks - 1);
return -EINVAL;
}
if (valid_mdata->active_index == bank)
return 0;
valid_mdata->active_index = bank;
mdata_mod = 1;
printf("Active bank set to %d\n", bank);
return 0;
}
static int set_previous_index(int bank)
{
struct fwu_fw_store_desc *fw_desc;
int num_banks;
if (valid_mdata->version == 2) {
fw_desc = fwu_get_fw_desc(valid_mdata);
num_banks = fw_desc->num_banks;
} else {
num_banks = nbanks;
}
if (bank < 0 || bank >= num_banks) {
fprintf(stderr, "Error: Invalid bank %d (must be 0-%d)\n",
bank, num_banks - 1);
return -EINVAL;
}
if (valid_mdata->previous_active_index == bank)
return 0;
valid_mdata->previous_active_index = bank;
mdata_mod = 1;
printf("Previous bank set to %d\n", bank);
return 0;
}
static int set_image_accepted(int image, int bank, int accept)
{
struct fwu_image_bank_info *bank_info;
int num_images, num_banks;
if (valid_mdata->version == 1) {
num_images = nimages;
num_banks = nbanks;
} else {
struct fwu_fw_store_desc *fw_desc;
fw_desc = fwu_get_fw_desc(valid_mdata);
num_images = fw_desc->num_images;
num_banks = fw_desc->num_banks;
}
if (bank < 0 || bank >= num_banks) {
fprintf(stderr, "Error: Invalid bank %d (must be 0-%d)\n",
bank, num_banks - 1);
return -EINVAL;
}
if (image < 0 || image >= num_images) {
fprintf(stderr, "Error: Invalid image %d (must be 0-%d)\n",
image, num_images - 1);
return -EINVAL;
}
bank_info = fwu_get_bank_info(valid_mdata, valid_mdata->version,
num_banks, image, bank);
if (accept == bank_info->accepted)
return 0;
if (accept) {
bank_info->accepted = FWU_IMAGE_ACCEPTED;
} else {
bank_info->accepted = 0;
/* According to the spec: bank_state[index] have to be set
* to invalid before any content in the img_bank_info[index]
* is overwritten.
*/
if (valid_mdata->version == 2) {
struct fwu_mdata_ext *mdata_ext;
mdata_ext = fwu_get_fw_mdata_ext(valid_mdata);
mdata_ext->bank_state[bank] = FWU_BANK_INVALID;
}
}
mdata_mod = 1;
printf("Image %d in bank %d: acceptance %s\n",
image, bank, accept ? "set" : "cleared");
return 0;
}
static int set_bank_state(int bank, const char *state_str)
{
struct fwu_fw_store_desc *fw_desc;
struct fwu_mdata_ext *mdata_ext;
u8 state;
int i;
if (valid_mdata->version != 2) {
fprintf(stderr,
"Error: Bank state is only supported in V2 metadata\n");
return -EINVAL;
}
fw_desc = fwu_get_fw_desc(valid_mdata);
mdata_ext = fwu_get_fw_mdata_ext(valid_mdata);
if (bank < 0 || bank >= fw_desc->num_banks || bank >= MAX_BANKS_V2) {
fprintf(stderr, "Error: Invalid bank %d (must be 0-%d)\n",
bank, fw_desc->num_banks - 1);
return -EINVAL;
}
/* Parse state string */
if (!strcmp(state_str, "accepted")) {
state = FWU_BANK_ACCEPTED;
} else if (!strcmp(state_str, "valid")) {
state = FWU_BANK_VALID;
} else if (!strcmp(state_str, "invalid")) {
state = FWU_BANK_INVALID;
} else {
fprintf(stderr,
"Error: Invalid state '%s' (must be accepted/valid/invalid)\n",
state_str);
return -EINVAL;
}
if (mdata_ext->bank_state[bank] == state)
return 0;
/* If a bank is set in a accepted state all firmware images in
* that bank must be marked as accepted as described in the spec.
*/
if (state == FWU_BANK_ACCEPTED) {
for (i = 0; i < fw_desc->num_images; i++) {
int ret;
ret = set_image_accepted(i, bank, true);
if (ret)
return ret;
}
}
mdata_ext->bank_state[bank] = state;
mdata_mod = 1;
printf("Bank %d state set to %s (0x%02x)\n", bank, state_str, state);
return 0;
}
static int metadata_v1_validate_size(void)
{
int calc_size;
calc_size = sizeof(struct fwu_mdata) +
(sizeof(struct fwu_image_entry) +
sizeof(struct fwu_image_bank_info) * nbanks) * nimages;
if (devices[0].mdata_size != calc_size) {
fprintf(stderr,
"Metadata calculate size (-B and -I options) 0x%x mismatch with the config 0x%zx\n",
calc_size, devices[0].mdata_size);
return -EINVAL;
}
return 0;
}
/* Command-line interface */
static void print_usage(void)
{
fprintf(stderr, "Usage: fwumdata [options]\n\n");
fprintf(stderr, "Options:\n"
"\t-c, --config <file> Use custom config file, defaults:\n"
"\t ./fwumdata.config or /etc/fwumdata.config\n"
"\t-l, --list List detailed metadata with GUIDs\n"
"\t-a, --active <bank> Set active bank index\n"
"\t-p, --previous <bank> Set previous bank index\n"
"\t-s, --state <bank> <state> Set bank state (V2 only)\n"
"\t state: accepted|valid|invalid\n"
"\t-i, --image <id> Image number (for -A/-C)\n"
"\t-b, --bank <bank> Bank number (for -A/-C)\n"
"\t-A, --accept Accept image (requires -i and -b)\n"
"\t-C, --clear Clear image acceptance (requires -i and -b)\n"
"\t-u, --update Update metadata if there is a checksum issue\n"
"\t-B, --nbanks <num_banks> Number of banks (required for V1 metadata)\n"
"\t-I, --nimages <num_images> Number of images (required for V1 metadata)\n"
"\t-h, --help Print this help\n\n");
fprintf(stderr, "Config file format (fwumdata.config):\n"
"\t# Device Name Device Offset Metadata Size Erase Size\n"
"\t/dev/mtd0 0x0 0x78 0x1000\n"
"\t/dev/mtd1 0x0 0x78 0x1000\n\n");
fprintf(stderr, "Examples:\n"
"\tfwumdata # Print metadata summary\n"
"\tfwumdata -l # Print detailed metadata\n"
"\tfwumdata -a 1 # Set active bank to 1\n"
"\tfwumdata -s 1 accepted # Set bank 1 to accepted state\n"
"\tfwumdata -i 0 -b 0 -A # Accept image in bank 0\n"
"\tfwumdata -B 2 -I 2 -i 1 -b 1 -A -l # Accept image 1 in bank 1 with metadata V1\n");
}
int main(int argc, char *argv[])
{
char *bank_state_str = NULL;
bool list_detailed = false;
int bank_state_num = -1;
int active_index = -1;
int bank_id = -1;
int prev_index = -1;
bool do_accept = 0;
bool do_clear = 0;
bool do_update = 0;
int image_id = -1;
int ret = 0;
int opt;
static struct option long_options[] = {
{"config", required_argument, 0, 'c'},
{"list", no_argument, 0, 'l'},
{"active", required_argument, 0, 'a'},
{"previous", required_argument, 0, 'p'},
{"state", required_argument, 0, 's'},
{"image", required_argument, 0, 'i'},
{"bank", required_argument, 0, 'b'},
{"accept", no_argument, 0, 'A'},
{"clear", no_argument, 0, 'C'},
{"update", no_argument, 0, 'u'},
{"nbanks", required_argument, 0, 'B'},
{"nimages", required_argument, 0, 'I'},
{"help", no_argument, 0, 'h'},
{0, 0, 0, 0}
};
/* Parse arguments */
while ((opt = getopt_long(argc, argv, "c:la:p:s:i:b:ACuB:I:h", long_options, NULL)) != -1) {
switch (opt) {
case 'c':
config_file = optarg;
break;
case 'l':
list_detailed = 1;
break;
case 'a':
active_index = atoi(optarg);
break;
case 'p':
prev_index = atoi(optarg);
break;
case 's':
bank_state_num = atoi(optarg);
if (optind < argc && argv[optind][0] != '-') {
bank_state_str = argv[optind++];
} else {
fprintf(stderr,
"Error: -s requires bank number and state\n");
return 1;
}
break;
case 'i':
image_id = atoi(optarg);
break;
case 'b':
bank_id = atoi(optarg);
break;
case 'A':
do_accept = 1;
break;
case 'C':
do_clear = 1;
break;
case 'u':
do_update = 1;
break;
case 'B':
nbanks = atoi(optarg);
break;
case 'I':
nimages = atoi(optarg);
break;
case 'h':
print_usage();
return 0;
default:
print_usage();
return 1;
}
}
ret = find_parse_config();
if (ret < 0) {
fprintf(stderr, "Error: Cannot read configuration\n");
return ret;
}
ret = read_metadata(do_update);
if (ret < 0) {
fprintf(stderr, "Error: Cannot read metadata\n");
goto cleanup;
}
if (valid_mdata->version == 1) {
ret = metadata_v1_validate_size();
if (ret)
goto cleanup;
}
/* Perform operations */
if (active_index >= 0) {
ret = set_active_index(active_index);
if (ret < 0)
goto cleanup;
}
if (prev_index >= 0) {
ret = set_previous_index(prev_index);
if (ret < 0)
goto cleanup;
}
if (do_accept || do_clear) {
if (image_id < 0 || bank_id < 0) {
fprintf(stderr,
"Error: -A/-C requires both -i <guid> and -b <bank>\n");
ret = -EINVAL;
goto cleanup;
}
ret = set_image_accepted(image_id, bank_id, do_accept);
if (ret < 0)
goto cleanup;
}
if (bank_state_num >= 0 && bank_state_str) {
ret = set_bank_state(bank_state_num, bank_state_str);
if (ret < 0)
goto cleanup;
}
/* Write back if modified */
if (mdata_mod) {
ret = write_metadata();
if (ret)
goto cleanup;
}
/* Display metadata if no modifications or list requested */
if (list_detailed)
print_metadata_detailed();
else
print_metadata_summary();
cleanup:
/* Close devices and free memory */
if (devices[0].fd)
close(devices[0].fd);
if (devices[1].fd)
close(devices[1].fd);
free(mdata);
for (int i = 0; i < 2; i++) {
if (devices[i].devname)
free((void *)devices[i].devname);
}
return ret;
}