diff --git a/arch/arm/mach-snapdragon/capsule_update.c b/arch/arm/mach-snapdragon/capsule_update.c index 586682434b73..eb900d80b421 100644 --- a/arch/arm/mach-snapdragon/capsule_update.c +++ b/arch/arm/mach-snapdragon/capsule_update.c @@ -8,30 +8,55 @@ #define pr_fmt(fmt) "QCOM-FMP: " fmt -#include -#include +#include #include #include #include #include -#include #include +#include +#include +#include #include - #include "qcom-priv.h" /* - * To handle different variants like chainloaded U-Boot here we need to - * build the fw_images array dynamically at runtime. These are the possible - * implementations: - * - * - Devices with U-Boot on the uefi_a/b partition - * - Devices with U-Boot on the boot (a/b) partition - * - Devices with U-Boot on the xbl (a/b) partition - * - * Which partition actually has U-Boot on it is determined based on the - * qcom_boot_source variable and additional logic in find_target_partition(). + * Capsule update support with conditional FIT vs RAW implementation: + * - FIT capsules: Comprehensive partition discovery with dynamic fw_images + * - RAW capsules: Existing single-partition approach with static fw_images */ + +#ifdef CONFIG_EFI_CAPSULE_FIRMWARE_FIT +#define MAX_DFU_STRING_SIZE 2048 +#define MAX_PARTITION_GROUPS 64 +#define MAX_PARTITIONS_PER_LUN 64 +#define MAX_PARTITIONS_TO_SCAN 128 +#define MAX_LUN_GROUPS 16 + +struct qcom_partition_info { + char name[32]; /* "uefi_a", "boot_b", etc. */ + char base_name[32]; /* "uefi", "boot", etc. */ + char slot_suffix[4]; /* "_a", "_b", or "" */ + int lun; /* SCSI LUN number */ + int partition_num; /* Partition number within LUN */ + bool is_active; /* From GPT vendor attributes */ + bool is_bootable; /* From GPT vendor attributes */ +}; + +struct partition_group { + char base_name[32]; + struct qcom_partition_info *a_slot; + struct qcom_partition_info *b_slot; + struct qcom_partition_info *no_slot; +}; + +struct lun_group { + int lun_number; + struct qcom_partition_info *partitions[MAX_PARTITIONS_PER_LUN]; /* Max partitions per LUN */ + int partition_count; +}; +#endif /* CONFIG_EFI_CAPSULE_FIRMWARE_FIT */ + struct efi_fw_image fw_images[] = { { .image_index = 1, @@ -39,18 +64,26 @@ struct efi_fw_image fw_images[] = { }; struct efi_capsule_update_info update_info = { - /* Filled in by configure_dfu_string() */ + /* Filled in by qcom_configure_capsule_updates() */ .dfu_string = NULL, .num_images = ARRAY_SIZE(fw_images), .images = fw_images, }; +#ifdef CONFIG_EFI_CAPSULE_FIRMWARE_RAW enum target_part_type { TARGET_PART_UEFI = 1, TARGET_PART_XBL, TARGET_PART_BOOT, }; +enum ab_slot { + SLOT_NONE, + SLOT_A, + SLOT_B, +}; +#endif /* CONFIG_EFI_CAPSULE_FIRMWARE_RAW */ + /* LSB first */ struct part_slot_status { u16: 2; @@ -61,26 +94,6 @@ struct part_slot_status { u16 tries_remaining : 4; }; -enum ab_slot { - SLOT_NONE, - SLOT_A, - SLOT_B, -}; - -static enum ab_slot get_part_slot(const char *partname) -{ - int len = strlen(partname); - - if (partname[len - 2] != '_') - return SLOT_NONE; - if (partname[len - 1] == 'a') - return SLOT_A; - if (partname[len - 1] == 'b') - return SLOT_B; - - return SLOT_NONE; -} - /* Shamelessly copied from lib/efi_loader/efi_device_path.c @ 33 */ /* * Determine if an MMC device is an SD card. @@ -98,6 +111,25 @@ static bool is_sd(struct blk_desc *desc) return IS_SD(mmc) != 0U; } +#ifdef CONFIG_EFI_CAPSULE_FIRMWARE_RAW +/* + * RAW Capsule Support + */ + +static enum ab_slot get_part_slot(const char *partname) +{ + int len = strlen(partname); + + if (partname[len - 2] != '_') + return SLOT_NONE; + if (partname[len - 1] == 'a') + return SLOT_A; + if (partname[len - 1] == 'b') + return SLOT_B; + + return SLOT_NONE; +} + /* * Determine which partition U-Boot is flashed to based on the boot source (ABL/XBL), * the slot status, and prioritizing the uefi partition over xbl if found. @@ -156,7 +188,7 @@ static int find_target_partition(int *devnum, enum uclass_id *uclass, * flags might not be set so we assume the A partition unless the B * partition is active. */ - if (!strncmp(info.name, "uefi", strlen("uefi"))) { + if (!strncmp(info.name, "uefi_", strlen("uefi_"))) { /* * If U-Boot was chainloaded somehow we can't be flashed to * the uefi partition @@ -263,7 +295,7 @@ static int find_target_partition(int *devnum, enum uclass_id *uclass, } /* Found no candidate partitions */ - return -1; + return -ENOENT; found: if (desc) { @@ -278,18 +310,7 @@ static int find_target_partition(int *devnum, enum uclass_id *uclass, return partnum; } -/** - * qcom_configure_capsule_updates() - Configure the DFU string for capsule updates - * - * U-Boot is flashed to the boot partition on Qualcomm boards. In most cases there - * are two boot partitions, boot_a and boot_b. As we don't currently support doing - * full A/B updates, we only support updating the currently active boot partition. - * - * So we need to find the current slot suffix and the associated boot partition. - * We do this by looking for the boot partition that has the 'active' flag set - * in the GPT partition vendor attribute bits. - */ -void qcom_configure_capsule_updates(void) +static void configure_raw_capsule_updates(void) { int ret = 0, partnum = -1, devnum; static char dfu_string[32] = { 0 }; @@ -297,7 +318,6 @@ void qcom_configure_capsule_updates(void) enum uclass_id dev_uclass; if (IS_ENABLED(CONFIG_SCSI)) { - /* Scan for SCSI devices */ ret = scsi_scan(false); if (ret) { debug("Failed to scan SCSI devices: %d\n", ret); @@ -339,7 +359,624 @@ void qcom_configure_capsule_updates(void) debug("Unsupported storage uclass: %d\n", dev_uclass); return; } - log_debug("DFU string: '%s'\n", dfu_string); + log_debug("RAW DFU string: '%s'\n", dfu_string); + + /* Set RAW configuration state */ update_info.dfu_string = dfu_string; + update_info.images = fw_images; + update_info.num_images = ARRAY_SIZE(fw_images); + + log_info("RAW capsule update configured (single partition: %s)\n", + target_part_type == TARGET_PART_UEFI ? "uefi" : + target_part_type == TARGET_PART_XBL ? "xbl" : "boot"); +} +#endif /* CONFIG_EFI_CAPSULE_FIRMWARE_RAW */ + +#ifdef CONFIG_EFI_CAPSULE_FIRMWARE_FIT +/* + * FIT Capsule Support - Implementation + */ + +static void parse_partition_name(const char *full_name, char *base_name, char *slot_suffix) +{ + char *underscore = strrchr(full_name, '_'); + + if (underscore && (strcmp(underscore, "_a") == 0 || strcmp(underscore, "_b") == 0)) { + /* Has A/B suffix */ + size_t base_len = underscore - full_name; + + strlcpy(base_name, full_name, base_len + 1); + strcpy(slot_suffix, underscore); + } else { + /* No A/B suffix */ + strcpy(base_name, full_name); + slot_suffix[0] = '\0'; + } +} + +static void parse_partition_info(struct qcom_partition_info *part, + struct disk_partition *info, + int lun, int partnum) +{ + struct part_slot_status *slot_status; + + strlcpy(part->name, info->name, sizeof(part->name)); + part->lun = lun; + part->partition_num = partnum; + + /* Parse slot status from GPT vendor attributes */ + slot_status = (struct part_slot_status *)&info->type_flags; + part->is_active = slot_status->active; + part->is_bootable = !slot_status->unbootable; + + /* Extract base name and slot suffix */ + parse_partition_name(part->name, part->base_name, part->slot_suffix); +} + +static struct partition_group *find_or_create_group(struct partition_group *groups, + int *group_count, + const char *base_name) +{ + /* Find existing group */ + for (int i = 0; i < *group_count; i++) { + if (strcmp(groups[i].base_name, base_name) == 0) + return &groups[i]; + } + + /* Create new group */ + if (*group_count >= MAX_PARTITION_GROUPS) { + log_err("Too many partition groups\n"); + return NULL; + } + + struct partition_group *new_group = &groups[*group_count]; + + strcpy(new_group->base_name, base_name); + new_group->a_slot = NULL; + new_group->b_slot = NULL; + new_group->no_slot = NULL; + + (*group_count)++; + return new_group; +} + +static struct qcom_partition_info *select_ab_target(struct qcom_partition_info *a_slot, + struct qcom_partition_info *b_slot) +{ + /* Priority: Active slot > A slot (fallback) */ + + if (a_slot && a_slot->is_active) { + log_debug("Selected %s (active)\n", a_slot->name); + return a_slot; + } + if (b_slot && b_slot->is_active) { + log_debug("Selected %s (active)\n", b_slot->name); + return b_slot; + } + + /* Both inactive - prefer A slot as fallback */ + struct qcom_partition_info *fallback = a_slot ? a_slot : b_slot; + + if (fallback) + log_debug("Selected %s (fallback - both inactive)\n", fallback->name); + return fallback; +} + +static int discover_all_partitions(struct qcom_partition_info **all_parts, int *all_count) +{ + struct udevice *dev; + struct blk_desc *desc; + struct qcom_partition_info *partition_list; + int partition_count = 0; + int max_partitions = 256; + bool have_ufs = false; + + /* Allocate partition list */ + partition_list = calloc(max_partitions, sizeof(struct qcom_partition_info)); + if (!partition_list) { + log_err("Failed to allocate partition list\n"); + return -ENOMEM; + } + + if (IS_ENABLED(CONFIG_SCSI)) { + if (scsi_scan(false)) { + log_debug("Failed to scan SCSI devices\n"); + free(partition_list); + return -EIO; + } + } + + /* + * Check to see if we have UFS storage, if so firmware MUST be on it and we can skip + * all non-UFS block devices + */ + uclass_foreach_dev_probe(UCLASS_UFS, dev) { + have_ufs = true; + break; + } + + /* Discover partitions with UFS-priority logic */ + uclass_foreach_dev_probe(UCLASS_BLK, dev) { + if (device_get_uclass_id(dev) != UCLASS_BLK) + continue; + + desc = dev_get_uclass_plat(dev); + if (!desc) + continue; + + if (have_ufs) { + if (device_get_uclass_id(dev->parent->parent) != UCLASS_UFS) + continue; + } else { + /* If we don't have UFS, look at eMMC (but skip SD cards) */ + if (desc->uclass_id == UCLASS_MMC) { + if (IS_ENABLED(CONFIG_MMC) && is_sd(desc)) { + log_debug("Skipped SD-Card (devnum %d)\n", desc->devnum); + continue; + } + } else if (desc->uclass_id != UCLASS_SCSI) { + /* Not MMC and not SCSI, skip it */ + continue; + } + } + + int lun = desc->devnum; + + /* Scan all partitions on this device */ + for (int partnum = 1; partnum <= MAX_PARTITIONS_TO_SCAN; partnum++) { + struct disk_partition info; + + if (part_get_info(desc, partnum, &info) != 0) + break; + + if (partition_count >= max_partitions) { + log_warning("Too many partitions discovered, truncating at %d\n", + max_partitions); + break; + } + + /* Parse and store partition info */ + parse_partition_info(&partition_list[partition_count], &info, lun, partnum); + partition_count++; + } + } + + *all_parts = partition_list; + *all_count = partition_count; + + log_debug("Discovered %d partitions across all %s devices\n", + partition_count, have_ufs ? "UFS" : "eMMC"); + return 0; +} + +static int select_target_partitions(struct qcom_partition_info *all_parts, int all_count, + struct qcom_partition_info **selected_parts, + int *selected_count) +{ + struct partition_group groups[MAX_PARTITION_GROUPS]; + struct qcom_partition_info *target_list; + int group_count = 0; + int target_count = 0; + + memset(groups, 0, sizeof(groups)); + + /* Allocate target list */ + target_list = calloc(all_count, sizeof(struct qcom_partition_info)); + if (!target_list) { + log_err("Failed to allocate target partition list\n"); + return -ENOMEM; + } + + /* Group partitions by base name */ + for (int i = 0; i < all_count; i++) { + struct qcom_partition_info *part = &all_parts[i]; + struct partition_group *group = find_or_create_group(groups, &group_count, + part->base_name); + + if (!group) { + log_err("Failed to create group for %s\n", part->base_name); + continue; + } + + if (strcmp(part->slot_suffix, "_a") == 0) { + if (!group->a_slot) { + group->a_slot = part; + } else { + log_info("Duplicate A-slot partition detected\n"); + log_info(" Keeping: %s (LUN %d, partition %d) [first discovered]\n", + group->a_slot->name, group->a_slot->lun, + group->a_slot->partition_num); + log_info(" Ignoring: %s (LUN %d, partition %d) [duplicate]\n", + part->name, part->lun, part->partition_num); + } + } else if (strcmp(part->slot_suffix, "_b") == 0) { + if (!group->b_slot) { + group->b_slot = part; + } else { + log_info("Duplicate B-slot partition detected\n"); + log_info(" Keeping: %s (LUN %d, partition %d) [first discovered]\n", + group->b_slot->name, group->b_slot->lun, + group->b_slot->partition_num); + log_info(" Ignoring: %s (LUN %d, partition %d) [duplicate]\n", + part->name, part->lun, part->partition_num); + } + } else { + if (!group->no_slot) { + group->no_slot = part; + } else { + log_info("Duplicate non-A/B partition detected\n"); + log_info(" Keeping: %s (LUN %d, partition %d) [first discovered]\n", + group->no_slot->name, group->no_slot->lun, + group->no_slot->partition_num); + log_info(" Ignoring: %s (LUN %d, partition %d) [duplicate]\n", + part->name, part->lun, part->partition_num); + } + } + } + + log_debug("Created %d partition groups for selection\n", group_count); + + /* Select target partition for each group */ + for (int i = 0; i < group_count; i++) { + struct partition_group *group = &groups[i]; + struct qcom_partition_info *target = NULL; + + if (group->no_slot) { + /* Non-A/B partition */ + target = group->no_slot; + log_debug("Group %s: selected non-A/B partition %s\n", + group->base_name, target->name); + } else { + /* A/B partition - apply selection logic */ + target = select_ab_target(group->a_slot, group->b_slot); + if (target) { + log_debug("Group %s: selected %s from A/B pair\n", + group->base_name, target->name); + } + } + + if (target) { + /* Copy selected partition to target list */ + memcpy(&target_list[target_count], target, + sizeof(struct qcom_partition_info)); + target_count++; + } else { + log_info("No target selected for group %s\n", group->base_name); + } + } + + *selected_parts = target_list; + *selected_count = target_count; + + log_debug("Selected %d target partitions from %d discovered\n", target_count, all_count); + return 0; +} + +static int group_partitions_by_lun(struct qcom_partition_info *selected_parts, int selected_count, + struct lun_group **lun_groups, int *group_count) +{ + struct lun_group *groups; + int max_groups = MAX_LUN_GROUPS; + int current_groups = 0; + + /* Allocate LUN groups array */ + groups = calloc(max_groups, sizeof(struct lun_group)); + if (!groups) { + log_err("Failed to allocate LUN groups array\n"); + return -ENOMEM; + } + + /* Group partitions by LUN */ + for (int i = 0; i < selected_count; i++) { + struct qcom_partition_info *part = &selected_parts[i]; + struct lun_group *target_group = NULL; + + /* Find existing group for this LUN */ + for (int j = 0; j < current_groups; j++) { + if (groups[j].lun_number == part->lun) { + target_group = &groups[j]; + break; + } + } + + /* Create new group if not found */ + if (!target_group) { + if (current_groups >= max_groups) { + log_err("Too many LUN groups (max %d)\n", max_groups); + free(groups); + return -ENOSPC; + } + + target_group = &groups[current_groups]; + target_group->lun_number = part->lun; + target_group->partition_count = 0; + current_groups++; + } + + /* Add partition to group */ + if (target_group->partition_count >= 64) { + log_err("Too many partitions in LUN %d (max 64)\n", part->lun); + free(groups); + return -ENOSPC; + } + + target_group->partitions[target_group->partition_count] = part; + target_group->partition_count++; + } + + /* Sort groups by LUN number for consistent output */ + for (int i = 0; i < current_groups - 1; i++) { + for (int j = i + 1; j < current_groups; j++) { + if (groups[i].lun_number > groups[j].lun_number) { + struct lun_group temp = groups[i]; + + groups[i] = groups[j]; + groups[j] = temp; + } + } + } + + *lun_groups = groups; + *group_count = current_groups; + + log_debug("Grouped %d partitions into %d LUN groups\n", selected_count, current_groups); + return 0; +} + +static int generate_dfu_string(struct qcom_partition_info *selected_parts, int selected_count, + char *dfu_string, size_t buffer_size) +{ + struct lun_group *lun_groups = NULL; + struct udevice *dev; + struct blk_desc *desc; + int group_count = 0; + char *dfu_ptr = dfu_string; + int remaining = buffer_size; + int ret; + bool is_mmc = false; + + /* Clear the buffer */ + memset(dfu_string, 0, buffer_size); + + /* Determine storage type by checking the first partition's device */ + if (selected_count > 0) { + uclass_foreach_dev_probe(UCLASS_BLK, dev) { + if (device_get_uclass_id(dev) != UCLASS_BLK) + continue; + + desc = dev_get_uclass_plat(dev); + if (!desc) + continue; + + if (desc->devnum == selected_parts[0].lun) { + if (desc->uclass_id == UCLASS_MMC) { + is_mmc = true; + log_debug("Detected MMC/eMMC storage for DFU string generation\n"); + } else if (desc->uclass_id == UCLASS_SCSI) { + is_mmc = false; + log_debug("Detected SCSI/UFS storage for DFU string generation\n"); + } + break; + } + } + } + + /* Group partitions by LUN/device */ + ret = group_partitions_by_lun(selected_parts, selected_count, &lun_groups, &group_count); + if (ret != 0) { + log_err("Failed to group partitions by LUN: %d\n", ret); + return ret; + } + + /* Generate DFU string with appropriate format for storage type */ + for (int i = 0; i < group_count; i++) { + struct lun_group *group = &lun_groups[i]; + int written; + + /* Add device group separator for non-first groups */ + if (i > 0) { + written = snprintf(dfu_ptr, remaining, "&"); + dfu_ptr += written; + remaining -= written; + } + + if (is_mmc) { + /* MMC format: "mmc X=" */ + written = snprintf(dfu_ptr, remaining, "mmc %d=", group->lun_number); + } else { + /* SCSI format: "scsi X=" */ + written = snprintf(dfu_ptr, remaining, "scsi %d=", group->lun_number); + } + dfu_ptr += written; + remaining -= written; + + /* Add partitions within this device group */ + for (int j = 0; j < group->partition_count; j++) { + struct qcom_partition_info *part = group->partitions[j]; + + /* Add partition separator for non-first partitions in group */ + if (j > 0) { + written = snprintf(dfu_ptr, remaining, ";"); + dfu_ptr += written; + remaining -= written; + } + + if (is_mmc) { + /* MMC format: "partition_name part dev_num partition_num" */ + written = snprintf(dfu_ptr, remaining, "%s part %d %d", + part->name, group->lun_number, part->partition_num); + } else { + /* SCSI format: "partition_name part partition_num" */ + written = snprintf(dfu_ptr, remaining, "%s part %d", + part->name, part->partition_num); + } + dfu_ptr += written; + remaining -= written; + + if (remaining <= 10) { + log_err("DFU string buffer overflow at partition %s\n", part->name); + free(lun_groups); + return -ENOSPC; + } + } + } + + /* Clean up */ + free(lun_groups); + + log_debug("Generated %s DFU string (%zu chars): %s\n", + is_mmc ? "MMC" : "SCSI", strlen(dfu_string), dfu_string); + return 0; +} + +/** + * get_board_fit_capsule_guid - Get board-specific FIT capsule GUID + * + * Detect the board type from device tree and return the appropriate GUID + * for FIT capsule updates. + * + * @guid: Pointer to store the GUID + * Return: 0 on success, negative error code on failure + */ +static int get_board_fit_capsule_guid(efi_guid_t *guid) +{ + const char *compatible; + + if (!guid) + return -EINVAL; + + compatible = ofnode_read_string(ofnode_root(), "compatible"); + if (!compatible) { + log_err("Failed to read board compatible string\n"); + return -ENODEV; + } + + /* Check for QCS615 or Talos */ + if (strstr(compatible, "qcs615") || strstr(compatible, "talos")) { + log_debug("Detected QCS615/Talos board\n"); + *guid = (efi_guid_t)QCOM_QCS615_FIT_CAPSULE_GUID; + return 0; + } + + /* Check for QCS6490 */ + if (strstr(compatible, "qcs6490")) { + log_debug("Detected QCS6490 board\n"); + *guid = (efi_guid_t)QCOM_QCS6490_FIT_CAPSULE_GUID; + return 0; + } + + /* Check for Lemans */ + if (strstr(compatible, "lemans") || strstr(compatible, "qcs9100")) { + log_debug("Detected Lemans board\n"); + *guid = (efi_guid_t)QCOM_LEMANS_FIT_CAPSULE_GUID; + return 0; + } + + log_err("Unsupported board for capsule updates: %s\n", compatible); + return -EINVAL; +} + +/* + * For creating FIT-based capsule images from FvUpdate.xml files, see: + * - Tool: tools/fvupdate_to_fit.py + * - Documentation: doc/develop/fvupdate_to_fit.rst + */ +static void configure_fit_capsule_updates(void) +{ + struct qcom_partition_info *all_partitions = NULL; + struct qcom_partition_info *selected_partitions = NULL; + int all_count = 0, selected_count = 0; + static char dfu_string[MAX_DFU_STRING_SIZE] = { 0 }; + static struct efi_fw_image single_fw_image; + efi_guid_t board_guid; + int ret; + + /* Step 1: Discover all partitions across all SCSI LUNs */ + ret = discover_all_partitions(&all_partitions, &all_count); + if (ret != 0) { + log_err("Failed to discover SCSI partitions: %d\n", ret); + return; + } + + if (all_count == 0) { + log_warning("No SCSI partitions discovered\n"); + goto cleanup; + } + + /* Step 2: Apply A/B selection logic to choose target partitions */ + ret = select_target_partitions(all_partitions, all_count, + &selected_partitions, &selected_count); + if (ret != 0) { + log_err("Failed to select target partitions: %d\n", ret); + goto cleanup; + } + + if (selected_count == 0) { + log_warning("No target partitions selected\n"); + goto cleanup; + } + + /* Step 3: Generate DFU string from selected partitions */ + ret = generate_dfu_string(selected_partitions, selected_count, + dfu_string, sizeof(dfu_string)); + if (ret != 0) { + log_err("Failed to generate DFU string: %d\n", ret); + goto cleanup; + } + + /* Step 4: Get board-specific GUID */ + ret = get_board_fit_capsule_guid(&board_guid); + if (ret != 0) { + log_err("Failed to get board-specific GUID: %d\n", ret); + goto cleanup; + } + + /* Step 5: Create SINGLE fw_image entry for ESRT */ + memset(&single_fw_image, 0, sizeof(single_fw_image)); + single_fw_image.fw_name = QCOM_FIT_CAPSULE_NAME; /* Same name for all boards */ + single_fw_image.image_index = 1; + single_fw_image.image_type_id = board_guid; + + /* Step 6: Configure update_info */ + update_info.dfu_string = dfu_string; + update_info.images = &single_fw_image; + update_info.num_images = 1; + + log_info("FIT capsule configured successfully:\n"); + log_info(" Name: %ls\n", QCOM_FIT_CAPSULE_NAME); + log_info(" GUID: %pUl\n", &board_guid); + log_info(" Partitions in DFU string: %d\n", selected_count); + log_info(" ESRT entries: 1 (single entry for all partitions)\n"); + +cleanup: + free(all_partitions); + free(selected_partitions); +} +#endif /* CONFIG_EFI_CAPSULE_FIRMWARE_FIT */ + +/** + * qcom_configure_capsule_updates() - Configure capsule updates + * + * Configures either FIT or RAW capsule updates based on compile-time configuration. + */ +void qcom_configure_capsule_updates(void) +{ +#if defined(CONFIG_EFI_CAPSULE_FIRMWARE_FIT) + log_info("Configuring FIT capsule updates\n"); + configure_fit_capsule_updates(); +#elif defined(CONFIG_EFI_CAPSULE_FIRMWARE_RAW) + log_info("Configuring RAW capsule updates\n"); + configure_raw_capsule_updates(); +#else + log_warning("No capsule firmware configuration enabled\n"); +#endif + + /* Final state logging */ + if (update_info.dfu_string) { + log_info("Capsule update configured successfully with %d image(s)\n", + update_info.num_images); + } else { + log_warning("Capsule update configuration failed\n"); + } } diff --git a/arch/arm/mach-snapdragon/qcom-priv.h b/arch/arm/mach-snapdragon/qcom-priv.h index 39dc8fcc76ad..c06e22fd2390 100644 --- a/arch/arm/mach-snapdragon/qcom-priv.h +++ b/arch/arm/mach-snapdragon/qcom-priv.h @@ -33,6 +33,29 @@ enum qcom_memmap_source { extern enum qcom_memmap_source qcom_memmap_source; #if IS_ENABLED(CONFIG_EFI_HAVE_CAPSULE_SUPPORT) +/* + * Capsule Update GUIDs for FIT capsules + * Each board has a unique GUID to prevent cross-board flashing + */ + +/* QCS615 FIT Capsule GUID: 9fd379d2-670e-4bb3-86a1-40497e6e17b0 */ +#define QCOM_QCS615_FIT_CAPSULE_GUID \ + EFI_GUID(0x9fd379d2, 0x670e, 0x4bb3, 0x86, 0xa1, \ + 0x40, 0x49, 0x7e, 0x6e, 0x17, 0xb0) + +/* QCS6490 FIT Capsule GUID: 6f25bfd2-a165-468b-980f-ac51a0a45c52 */ +#define QCOM_QCS6490_FIT_CAPSULE_GUID \ + EFI_GUID(0x6f25bfd2, 0xa165, 0x468b, 0x98, 0x0f, \ + 0xac, 0x51, 0xa0, 0xa4, 0x5c, 0x52) + +/* Lemans FIT Capsule GUID: 78462415-6133-431c-9fae-48f2bafd5c71 */ +#define QCOM_LEMANS_FIT_CAPSULE_GUID \ + EFI_GUID(0x78462415, 0x6133, 0x431c, 0x9f, 0xae, \ + 0x48, 0xf2, 0xba, 0xfd, 0x5c, 0x71) + +/* Common name for FIT capsule (same for all boards) */ +#define QCOM_FIT_CAPSULE_NAME u"QCOM_FIT_CAPSULE" + void qcom_configure_capsule_updates(void); #else void qcom_configure_capsule_updates(void) {} diff --git a/configs/qcom_defconfig b/configs/qcom_defconfig index 2f3c8f0cee51..9826161c562d 100644 --- a/configs/qcom_defconfig +++ b/configs/qcom_defconfig @@ -9,7 +9,7 @@ CONFIG_SYS_LOAD_ADDR=0xA0000000 # CONFIG_EFI_HAVE_RUNTIME_RESET is not set CONFIG_EFI_RUNTIME_UPDATE_CAPSULE=y CONFIG_EFI_CAPSULE_ON_DISK=y -CONFIG_EFI_CAPSULE_FIRMWARE_RAW=y +CONFIG_EFI_CAPSULE_FIRMWARE_FIT=y CONFIG_BUTTON_CMD=y CONFIG_FIT=y CONFIG_FIT_VERBOSE=y diff --git a/doc/develop/fvupdate_to_fit.rst b/doc/develop/fvupdate_to_fit.rst new file mode 100644 index 000000000000..ef29587198f7 --- /dev/null +++ b/doc/develop/fvupdate_to_fit.rst @@ -0,0 +1,487 @@ +.. SPDX-License-Identifier: GPL-2.0+ +.. Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + +FvUpdate.xml to Capsule Converter +================================== + +This Python script provides complete end-to-end conversion from Qualcomm FvUpdate.xml files to ready-to-deploy EFI capsule files for U-Boot capsule updates. + +Overview +-------- + +The script offers two modes of operation: + +1. **FIT-only mode**: Converts FvUpdate.xml to FIT images +2. **Complete mode**: Full workflow from XML to final capsule file + +The complete workflow includes: + +- FvUpdate.xml parsing and validation +- FIT image generation +- Final capsule creation with provided GUID +- Auto-installation of missing tools + +Features +-------- + +Core Functionality +~~~~~~~~~~~~~~~~~~ + +- **XML Parsing**: Extracts UPDATE operations from FvUpdate.xml +- **Binary Validation**: Ensures all referenced binary files exist +- **ITS Generation**: Creates Image Tree Source files with proper node naming +- **FIT Compilation**: Uses mkimage to generate FIT images +- **Capsule Creation**: Creates final capsule files using mkeficapsule with provided GUID +- **Tool Management**: Auto-installs missing tools (mkimage, mkeficapsule) + +Compatibility +~~~~~~~~~~~~~ + +- Qualcomm U-Boot capsule update system +- Multi-partition firmware updates +- FIT-based capsule payloads +- EFI Firmware Management Protocol (FMP) +- DFU backend integration + +Usage +----- + +FIT Image Only +~~~~~~~~~~~~~~ + +Creates only system.fit:: + + ./fvupdate_to_fit.py FvUpdate.xml + +Complete Capsule Workflow +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Creates system.fit and firmware.capsule:: + + ./fvupdate_to_fit.py FvUpdate.xml \ + --mkeficapsule \ + --guid \ + --fw-version + +Additional Examples +~~~~~~~~~~~~~~~~~~~ + +With custom output names:: + + ./fvupdate_to_fit.py FvUpdate.xml \ + --mkeficapsule /path/to/mkeficapsule \ + --guid 12345678-1234-5678-9abc-123456789abc \ + --fw-version 0.0.1.0 \ + --output custom.fit \ + --capsule-output custom.capsule + +With verbose output:: + + ./fvupdate_to_fit.py FvUpdate.xml \ + --mkeficapsule /path/to/mkeficapsule \ + --guid 12345678-1234-5678-9abc-123456789abc \ + --fw-version 0.0.1.0 \ + --verbose + +With Capsule Signing +~~~~~~~~~~~~~~~~~~~~ + +Creates a signed capsule:: + + ./fvupdate_to_fit.py FvUpdate.xml \ + --mkeficapsule /path/to/mkeficapsule \ + --guid 12345678-1234-5678-9abc-123456789abc \ + --fw-version 0.0.1.0 \ + --monotonic-count 1 \ + --private-key keys/CRT.key \ + --certificate keys/CRT.crt + +Command Line Options +-------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 50 30 + + * - Option + - Description + - Default + * - ``xml_file`` + - Path to FvUpdate.xml file + - Required + * - ``--guid`` + - GUID value of the board (enables complete mode). Board-specific GUIDs: qcs615: ``9FD379D2-670E-4BB3-86A1-40497E6E17B0``, qcs6490-rb3gen2: ``6f25bfd2-a165-468b-980f-ac51a0a45c52``, lemans-evk: ``78462415-6133-431c-9fae-48f2bafd5c71`` + - Required + * - ``--mkeficapsule`` + - Path to mkeficapsule binary + - Required + * - ``--fw-version`` + - Firmware version in "0.0.A.B" format (e.g., "0.0.1.0") + - Optional + * - ``--monotonic-count`` + - Monotonic count for capsule signing + - Optional + * - ``--private-key`` + - Path to the private key for signing + - Optional + * - ``--certificate`` + - Path to the certificate for signing + - Optional + * - ``-o, --output`` + - Output FIT image name + - system.fit + * - ``-c, --capsule-output`` + - Output capsule file name + - firmware.capsule + * - ``-v, --verbose`` + - Enable verbose output + - Disabled + * - ``-h, --help`` + - Show help message + - \- + +Requirements +------------ + +System Requirements +~~~~~~~~~~~~~~~~~~~ + +- Python 3.6+ +- Linux system with package manager (apt, yum, dnf, or pacman) +- sudo access for tool installation + +Required Tools (Auto-installed) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- U-Boot tools (mkimage, mkeficapsule) + +Required Files +~~~~~~~~~~~~~~ + +- FvUpdate.xml file +- Binary files in Images/ directory +- GUID value for the target board (for complete mode) + +Installation +------------ + +1. Make the script executable:: + + chmod +x fvupdate_to_fit.py + +2. The script will auto-install missing tools on first run + +File Structure +-------------- + +The script expects the following directory structure:: + + project/ + ├── FvUpdate.xml + ├── Images/ + │ ├── xbl.elf + │ ├── uefi.bin + │ └── boot.img + └── fvupdate_to_fit.py + +Example FvUpdate.xml +~~~~~~~~~~~~~~~~~~~~ + +An example FvUpdate.xml file is provided for reference: + +.. literalinclude:: fvupdate_to_fit/example_FvUpdate.xml + :language: xml + +This example demonstrates the XML structure with UPDATE operations for +multiple firmware partitions. + +XML Structure Requirements +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The tool only processes the following XML elements: + +**Required Elements:** + +- ```` - Container for each firmware entry +- ```` - Binary filename (must exist in Images/ directory) +- ```` - Must be "UPDATE" to be processed +- ``/`` - Target partition name (case-sensitive) + +**XML Structure:** + +.. code-block:: xml + + + + + firmware.bin + UPDATE + + partition_name + + + + +**Notes:** + +- The tool hardcodes the Images/ directory for binary files +- Only entries with ``UPDATE`` are processed +- Partition names are case-sensitive and must match U-Boot partition discovery + +Output Files +------------ + +Complete Mode +~~~~~~~~~~~~~ + +- ``system.its`` - Image Tree Source file (kept) +- ``system.fit`` - FIT image (kept) +- ``firmware.capsule`` - Final capsule file (ready for deployment) + +FIT-only Mode +~~~~~~~~~~~~~ + +- ``system.its`` - Image Tree Source file +- ``system.fit`` - FIT image + +Deployment Workflow +------------------- + +After generating the capsule: + +1. **Copy capsule to boot partition**:: + + cp firmware.capsule /boot/ + +2. **Deploy in U-Boot**:: + + => fatload mmc 0:1 $loadaddr firmware.capsule + => efidebug capsule update $loadaddr + +Technical Details +----------------- + +FIT Image Structure +~~~~~~~~~~~~~~~~~~~ + +- **Node Names**: Match partition names (e.g., ``xbl_a``, ``uefi_a``) +- **Image Type**: All images marked as "firmware" +- **Hash Algorithm**: SHA256 for integrity verification +- **Configuration**: Single config referencing all firmware images + +Capsule Structure +~~~~~~~~~~~~~~~~~ + +- **GUID**: Provided via --guid option +- **Index**: Always 1 (required for FIT capsules) +- **Payload**: FIT image containing all firmware +- **Version**: Optional firmware version (in 0.0.A.B format) via --fw-version, encoded as (A << 16 | B) +- **Signing**: Optional signing with monotonic count, private key, and certificate + +FMP Driver Compatibility +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The provided GUID must match what the FMP (Firmware Management Protocol) driver expects: + +- Uses actual partition names from FvUpdate.xml +- Compatible with Qualcomm capsule update flow +- Works with ``fit_update()`` processing + +Error Handling +-------------- + +Comprehensive error handling with fail-fast approach: + +- Missing XML files or invalid format +- Missing binary files in Images/ directory +- Tool installation failures +- Capsule creation failures +- Invalid GUID format + +Example Output +-------------- + +Complete Mode +~~~~~~~~~~~~~ + +:: + + Converting FvUpdate.xml to capsule... + ============================================================ + Installing missing tools... + Installed u-boot-tools ✓ + Parsing XML file: FvUpdate.xml + Found 3 FwEntry elements + Added: xbl_a -> xbl.elf + Added: uefi_a -> uefi.bin + Added: boot_a -> boot.img + Successfully parsed 3 UPDATE entries + Validating binary files... + xbl.elf: 524288 bytes + uefi.bin: 1048576 bytes + boot.img: 2097152 bytes + All binary files validated successfully + Generating ITS file: system.its + ITS file generated successfully + Compiling FIT image: system.fit + Running: mkimage -f system.its system.fit + FIT image compiled successfully: 3670016 bytes + Using GUID: 12345678-1234-5678-9abc-123456789abc + Creating capsule: firmware.capsule + Encoded Firmware version: 65536 (from 0.0.1.0) + Command: mkeficapsule -g 12345678-1234-5678-9abc-123456789abc -i 1 -v 65536 system.fit firmware.capsule + Capsule created successfully: 3670144 bytes ✓ + ============================================================ + SUCCESS: Complete capsule workflow completed! + + Files created: + ITS file: system.its + FIT file: system.fit + Capsule file: firmware.capsule (3.5 MB) + + Capsule GUID: 12345678-1234-5678-9abc-123456789abc + + Ready for deployment: + 1. Copy firmware.capsule to boot partition + 2. In U-Boot: fatload mmc 0:1 $loadaddr firmware.capsule + 3. In U-Boot: efidebug capsule update $loadaddr + +FIT-only Mode +~~~~~~~~~~~~~ + +:: + + Converting FvUpdate.xml to FIT image... + ============================================================ + Parsing XML file: FvUpdate.xml + Found 3 FwEntry elements + Added: xbl_a -> xbl.elf + Added: uefi_a -> uefi.bin + Added: boot_a -> boot.img + Successfully parsed 3 UPDATE entries + Validating binary files... + xbl.elf: 524288 bytes + uefi.bin: 1048576 bytes + boot.img: 2097152 bytes + All binary files validated successfully + Generating ITS file: system.its + ITS file generated successfully + Compiling FIT image: system.fit + FIT image compiled successfully: 3670016 bytes + ============================================================ + CONVERSION SUMMARY: + Input XML: FvUpdate.xml + Generated ITS: system.its + Output FIT: system.fit + Partitions: 3 + - xbl_a (xbl.elf) + - uefi_a (uefi.bin) + - boot_a (boot.img) + + ============================================================ + SUCCESS: FIT image created successfully! + Output: system.fit + + To create capsule, run again with: + ./fvupdate_to_fit.py FvUpdate.xml --mkeficapsule --guid --fw-version + +Viewing Generated File Contents +-------------------------------- + +FIT Image Contents +~~~~~~~~~~~~~~~~~~ + +You can inspect the generated ``system.fit`` file using U-Boot tools:: + + # View FIT image structure and metadata + mkimage -l system.fit + + # List all images in the FIT + dumpimage -l system.fit + +Capsule Contents +~~~~~~~~~~~~~~~~ + +You can inspect the generated ``firmware.capsule`` file using mkeficapsule:: + + # View capsule header and metadata + mkeficapsule --dump-capsule firmware.capsule + + # Or using local mkeficapsule binary + /path/to/local/mkeficapsule --dump-capsule firmware.capsule + +Troubleshooting +--------------- + +mkimage not found +~~~~~~~~~~~~~~~~~ + +Install U-Boot tools:: + + # Ubuntu/Debian + sudo apt-get install u-boot-tools + + # CentOS/RHEL + sudo yum install uboot-tools + +mkeficapsule not found +~~~~~~~~~~~~~~~~~~~~~~ + +If you get an error about mkeficapsule not being found, use a locally compiled version:: + + # Use local mkeficapsule binary + ./fvupdate_to_fit.py FvUpdate.xml \ + --mkeficapsule /path/to/local/u-boot/tools/mkeficapsule \ + --guid 12345678-1234-5678-9abc-123456789abc \ + --fw-version 0.0.1.0 + + # Example with U-Boot build directory + ./fvupdate_to_fit.py FvUpdate.xml \ + --mkeficapsule /local/mnt/workspace/bselvana/k2c_le/u-boot_upstream/u-boot_v2025_upstream/tools/mkeficapsule \ + --guid 12345678-1234-5678-9abc-123456789abc \ + --fw-version 0.0.1.0 + +Invalid GUID format +~~~~~~~~~~~~~~~~~~~ + +Ensure the GUID follows the format: ``xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`` + +Supported Boards and GUIDs +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following Qualcomm boards are supported with their respective GUIDs: + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Board + - GUID + * - **qcs615** + - ``9FD379D2-670E-4BB3-86A1-40497E6E17B0`` + * - **qcs6490-rb3gen2** + - ``6f25bfd2-a165-468b-980f-ac51a0a45c52`` + * - **lemans-evk** + - ``78462415-6133-431c-9fae-48f2bafd5c71`` + + +Example valid GUID format: ``12345678-1234-5678-9abc-123456789abc`` + +Missing binary files +~~~~~~~~~~~~~~~~~~~~ + +Ensure all files referenced in FvUpdate.xml exist in the Images/ directory with correct names. + +XML parsing errors +~~~~~~~~~~~~~~~~~~ + +Verify FvUpdate.xml is well-formed XML with proper FwEntry structure. + +mkeficapsule binary not found +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you specify a custom mkeficapsule path, ensure: + +- The file exists and is executable +- The path is correct (absolute or relative to current directory) +- The binary supports capsule creation diff --git a/doc/develop/fvupdate_to_fit/example_FvUpdate.xml b/doc/develop/fvupdate_to_fit/example_FvUpdate.xml new file mode 100644 index 000000000000..c1dea8db4a26 --- /dev/null +++ b/doc/develop/fvupdate_to_fit/example_FvUpdate.xml @@ -0,0 +1,43 @@ + + + + + + + + + u-boot.mbn + UPDATE + + uefi_a + + + + + + tz.mbn + UPDATE + + tz_a + + + + + + \ No newline at end of file diff --git a/lib/efi_loader/efi_firmware.c b/lib/efi_loader/efi_firmware.c index b41969c70fde..ce6ca04ac691 100644 --- a/lib/efi_loader/efi_firmware.c +++ b/lib/efi_loader/efi_firmware.c @@ -52,9 +52,9 @@ struct fmp_payload_header { */ struct fmp_state { u32 fw_version; - u32 lowest_supported_version; /* not used */ - u32 last_attempt_version; /* not used */ - u32 last_attempt_status; /* not used */ + u32 lowest_supported_version; /* not used - read from DTB for security */ + u32 last_attempt_version; /* used for esrt tracking */ + u32 last_attempt_status; /* used for esrt tracking */ }; /** @@ -190,6 +190,67 @@ static void efi_firmware_get_lsv_from_dtb(u8 image_index, } } +/** + * efi_firmware_set_last_attempt - set last attempt information + * @state: Pointer to fmp state + * @attempt_version: Version that was attempted + * @attempt_status: Status of the attempt + * + * Set the last attempt version and status in the fmp_state structure. + */ +static void efi_firmware_set_last_attempt(struct fmp_state *state, + u32 attempt_version, + u32 attempt_status) +{ + state->last_attempt_version = attempt_version; + state->last_attempt_status = attempt_status; +} + +/** + * efi_firmware_get_last_attempt - get last attempt information + * @state: Pointer to fmp state + * @attempt_version: Pointer to store last attempt version + * @attempt_status: Pointer to store last attempt status + * + * Retrieve the last attempt version and status from fmp_state structure. + */ +static void efi_firmware_get_last_attempt(const struct fmp_state *state, + u32 *attempt_version, + u32 *attempt_status) +{ + if (attempt_version) + *attempt_version = state->last_attempt_version; + if (attempt_status) + *attempt_status = state->last_attempt_status; +} + +/** + * efi_firmware_map_error_to_status - map internal errors to UEFI status codes + * @error: Internal error code + * + * Map U-Boot internal error codes to UEFI-compliant last attempt status codes. + * + * Return: UEFI last attempt status code + */ +static u32 efi_firmware_map_error_to_status(efi_status_t error) +{ + switch (error) { + case EFI_SUCCESS: + return LAST_ATTEMPT_STATUS_SUCCESS; + case EFI_OUT_OF_RESOURCES: + return LAST_ATTEMPT_STATUS_ERROR_INSUFFICIENT_RESOURCES; + case EFI_INVALID_PARAMETER: + return LAST_ATTEMPT_STATUS_ERROR_INCORRECT_VERSION; + case EFI_SECURITY_VIOLATION: + return LAST_ATTEMPT_STATUS_ERROR_AUTH_ERROR; + case EFI_UNSUPPORTED: + return LAST_ATTEMPT_STATUS_ERROR_INVALID_FORMAT; + case EFI_DEVICE_ERROR: + default: + return LAST_ATTEMPT_STATUS_ERROR_UNSUCCESSFUL; + } +} + /** * efi_firmware_fill_version_info - fill the version information * @image_info: Image information @@ -237,8 +298,15 @@ void efi_firmware_fill_version_info(struct efi_firmware_image_descriptor *image_ ret = efi_get_variable_int(varname, &fw_array->image_type_id, NULL, &size, var_state, NULL); - if (ret == EFI_SUCCESS && expected_size == size) + if (ret == EFI_SUCCESS && expected_size == size) { image_info->version = var_state[active_index].fw_version; + image_info->last_attempt_version = var_state[active_index].last_attempt_version; + image_info->last_attempt_status = var_state[active_index].last_attempt_status; + } else { + /* Default values if no previous state exists */ + image_info->last_attempt_version = 0; + image_info->last_attempt_status = LAST_ATTEMPT_STATUS_SUCCESS; + } free(var_state); } @@ -403,6 +471,15 @@ efi_status_t efi_firmware_capsule_authenticate(const void **p_image, if (status == EFI_SECURITY_VIOLATION) { printf("Capsule authentication check failed. Aborting update\n"); + /* + * Even though authentication failed, update the pointers + * to skip past the auth wrapper so the caller can read + * the FMP payload header for version information. + */ + image = capsule_payload; + image_size = capsule_payload_size; + *p_image = image; + *p_image_size = image_size; return status; } else if (status != EFI_SUCCESS) { return status; @@ -427,6 +504,10 @@ efi_status_t efi_firmware_capsule_authenticate(const void **p_image, * @image_index: image index * * Update the FmpStateXXXX variable with the firmware update state. + * On successful update (last_attempt_status == LAST_ATTEMPT_STATUS_SUCCESS), + * updates fw_version to the new version. + * On failed update, preserves the old fw_version. + * Always updates last_attempt_version and last_attempt_status. * * Return: status code */ @@ -471,11 +552,19 @@ efi_status_t efi_firmware_set_fmp_state_var(struct fmp_state *state, u8 image_in memset(var_state, 0, num_banks * sizeof(*var_state)); /* - * Only the fw_version is set here. + * Set fw_version and last attempt information. * lowest_supported_version in FmpState variable is ignored since * it can be tampered if the file based EFI variable storage is used. + * + * Only update fw_version if the update succeeded. + * On failure, preserve the old fw_version to maintain accurate ESRT state. */ - var_state[update_bank].fw_version = state->fw_version; + if (state->last_attempt_status == LAST_ATTEMPT_STATUS_SUCCESS) + var_state[update_bank].fw_version = state->fw_version; + /* else: keep existing fw_version (don't update on failure) */ + + var_state[update_bank].last_attempt_version = state->last_attempt_version; + var_state[update_bank].last_attempt_status = state->last_attempt_status; size = num_banks * sizeof(*var_state); ret = efi_set_variable_int(varname, image_type_id, @@ -540,11 +629,10 @@ efi_status_t efi_firmware_verify_image(const void **p_image, efi_guid_t *image_type_id; ret = efi_firmware_capsule_authenticate(p_image, p_image_size); + efi_firmware_get_fw_version(p_image, p_image_size, state); if (ret != EFI_SUCCESS) return ret; - efi_firmware_get_fw_version(p_image, p_image_size, state); - image_type_id = efi_firmware_get_image_type_id(image_index); if (!image_type_id) return EFI_INVALID_PARAMETER; @@ -661,21 +749,40 @@ efi_status_t EFIAPI efi_firmware_fit_set_image( status = efi_firmware_verify_image(&image, &image_size, image_index, &state); - if (status != EFI_SUCCESS) + if (status != EFI_SUCCESS) { + /* Set last attempt information for failed verification */ + efi_firmware_set_last_attempt(&state, state.fw_version, + efi_firmware_map_error_to_status(status)); + efi_firmware_set_fmp_state_var(&state, image_index); return EFI_EXIT(status); + } + + /* Set last attempt version before starting the update */ + efi_firmware_set_last_attempt(&state, state.fw_version, + LAST_ATTEMPT_STATUS_SUCCESS); orig_dfu_env = env_get("dfu_alt_info"); + if (orig_dfu_env) { orig_dfu_env = strdup(orig_dfu_env); if (!orig_dfu_env) { log_err("strdup() failed!\n"); - return EFI_EXIT(EFI_OUT_OF_RESOURCES); + status = EFI_OUT_OF_RESOURCES; + efi_firmware_set_last_attempt(&state, state.fw_version, + efi_firmware_map_error_to_status(status)); + efi_firmware_set_fmp_state_var(&state, image_index); + return EFI_EXIT(status); } } + if (env_set("dfu_alt_info", update_info.dfu_string)) { log_err("Unable to set env variable \"dfu_alt_info\"!\n"); free(orig_dfu_env); - return EFI_EXIT(EFI_DEVICE_ERROR); + status = EFI_DEVICE_ERROR; + efi_firmware_set_last_attempt(&state, state.fw_version, + efi_firmware_map_error_to_status(status)); + efi_firmware_set_fmp_state_var(&state, image_index); + return EFI_EXIT(status); } /* Make sure the update fitImage is properly aligned to 8-bytes */ @@ -698,9 +805,17 @@ efi_status_t EFIAPI efi_firmware_fit_set_image( free(orig_dfu_env); - if (ret) - return EFI_EXIT(EFI_DEVICE_ERROR); + if (ret) { + status = EFI_DEVICE_ERROR; + efi_firmware_set_last_attempt(&state, state.fw_version, + efi_firmware_map_error_to_status(status)); + efi_firmware_set_fmp_state_var(&state, image_index); + return EFI_EXIT(status); + } + /* Update successful - set success status */ + efi_firmware_set_last_attempt(&state, state.fw_version, + LAST_ATTEMPT_STATUS_SUCCESS); efi_firmware_set_fmp_state_var(&state, image_index); return EFI_EXIT(EFI_SUCCESS); @@ -762,8 +877,17 @@ efi_status_t EFIAPI efi_firmware_raw_set_image( status = efi_firmware_verify_image(&image, &image_size, image_index, &state); - if (status != EFI_SUCCESS) + if (status != EFI_SUCCESS) { + /* Set last attempt information for failed verification */ + efi_firmware_set_last_attempt(&state, state.fw_version, + efi_firmware_map_error_to_status(status)); + efi_firmware_set_fmp_state_var(&state, image_index); return EFI_EXIT(status); + } + + /* Set last attempt version before starting the update */ + efi_firmware_set_last_attempt(&state, state.fw_version, + LAST_ATTEMPT_STATUS_SUCCESS); /* * dfu_alt_num is assigned from 0 while image_index starts from 1. @@ -788,13 +912,21 @@ efi_status_t EFIAPI efi_firmware_raw_set_image( orig_dfu_env = strdup(orig_dfu_env); if (!orig_dfu_env) { log_err("strdup() failed!\n"); - return EFI_EXIT(EFI_OUT_OF_RESOURCES); + status = EFI_OUT_OF_RESOURCES; + efi_firmware_set_last_attempt(&state, state.fw_version, + efi_firmware_map_error_to_status(status)); + efi_firmware_set_fmp_state_var(&state, image_index); + return EFI_EXIT(status); } } if (env_set("dfu_alt_info", update_info.dfu_string)) { log_err("Unable to set env variable \"dfu_alt_info\"!\n"); free(orig_dfu_env); - return EFI_EXIT(EFI_DEVICE_ERROR); + status = EFI_DEVICE_ERROR; + efi_firmware_set_last_attempt(&state, state.fw_version, + efi_firmware_map_error_to_status(status)); + efi_firmware_set_fmp_state_var(&state, image_index); + return EFI_EXIT(status); } ret = dfu_write_by_alt(dfu_alt_num, (void *)image, image_size, @@ -805,9 +937,17 @@ efi_status_t EFIAPI efi_firmware_raw_set_image( free(orig_dfu_env); - if (ret) - return EFI_EXIT(EFI_DEVICE_ERROR); + if (ret) { + status = EFI_DEVICE_ERROR; + efi_firmware_set_last_attempt(&state, state.fw_version, + efi_firmware_map_error_to_status(status)); + efi_firmware_set_fmp_state_var(&state, image_index); + return EFI_EXIT(status); + } + /* Update successful - set success status */ + efi_firmware_set_last_attempt(&state, state.fw_version, + LAST_ATTEMPT_STATUS_SUCCESS); efi_firmware_set_fmp_state_var(&state, image_index); return EFI_EXIT(EFI_SUCCESS); diff --git a/tools/fvupdate_to_fit.py b/tools/fvupdate_to_fit.py new file mode 100755 index 000000000000..bfd9cc631d79 --- /dev/null +++ b/tools/fvupdate_to_fit.py @@ -0,0 +1,605 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0+ +# +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +""" +FvUpdate.xml to Capsule-Compatible FIT Image and Capsule Converter + +Complete end-to-end converter that: +1. Processes FvUpdate.xml files +2. Creates FIT images compatible with Qualcomm U-Boot capsule update system +3. Generates GUIDs using mkeficapsule guidgen +4. Creates final capsule files ready for deployment + +Usage: ./fvupdate_to_fit.py +""" + +import xml.etree.ElementTree as ET +import os +import sys +import subprocess +import shutil +import re +from pathlib import Path +import argparse + +class FvUpdateToFitConverter: + """ + Complete FvUpdate.xml to Capsule converter. + + This converter provides end-to-end functionality: + 1. Converts FvUpdate.xml to FIT images + 2. Generates GUIDs using mkeficapsule guidgen + 3. Creates final capsule files + 4. Auto-installs missing tools + 5. Ensures compatibility with Qualcomm U-Boot capsule update system + """ + + def __init__(self, xml_path, mkeficapsule_path=None): + self.xml_path = Path(xml_path) + self.mkeficapsule_path = mkeficapsule_path or "mkeficapsule" + self.base_dir = self.xml_path.parent + self.images_dir = self.base_dir / "Images" + self.fw_entries = [] + + def parse_xml(self): + """ + Parse FvUpdate.xml and extract UPDATE operations only. + + Only processes FwEntry elements with Operation="UPDATE" to ensure + compatibility with capsule update requirements. + """ + print(f"Parsing XML file: {self.xml_path}") + + try: + tree = ET.parse(self.xml_path) + root = tree.getroot() + except ET.ParseError as e: + raise ValueError(f"Invalid XML format: {e}") + except FileNotFoundError: + raise FileNotFoundError(f"XML file not found: {self.xml_path}") + + # Find all FwEntry elements + fw_entries = root.findall('.//FwEntry') + if not fw_entries: + raise ValueError("No FwEntry elements found in XML") + + print(f"Found {len(fw_entries)} FwEntry elements") + + for fw_entry in fw_entries: + # Check operation type + operation_elem = fw_entry.find('Operation') + if operation_elem is None: + print("Warning: FwEntry missing Operation element, skipping") + continue + + operation = operation_elem.text + if operation != "UPDATE": + print(f"Skipping FwEntry with operation: {operation}") + continue + + # Extract required elements + try: + dest = fw_entry.find('.//Dest') + if dest is None: + raise ValueError("FwEntry missing Dest element") + + partition_name_elem = dest.find('PartitionName') + if partition_name_elem is None: + raise ValueError("Dest missing PartitionName element") + partition_name = partition_name_elem.text + + binary_file_elem = fw_entry.find('InputBinary') + if binary_file_elem is None: + raise ValueError("FwEntry missing InputBinary element") + binary_file = binary_file_elem.text + + # Validate partition name + if not partition_name or not partition_name.strip(): + raise ValueError("Empty partition name") + + # Validate binary file name + if not binary_file or not binary_file.strip(): + raise ValueError("Empty binary file name") + + self.fw_entries.append({ + 'partition_name': partition_name.strip(), + 'binary_file': binary_file.strip(), + 'binary_path': self.images_dir / binary_file.strip() + }) + + print(f" Added: {partition_name} -> {binary_file}") + + except ValueError as e: + raise ValueError(f"Invalid FwEntry: {e}") + + if not self.fw_entries: + raise ValueError("No valid UPDATE FwEntry elements found") + + print(f"Successfully parsed {len(self.fw_entries)} UPDATE entries") + + def validate_files(self): + """ + Validate all binary files exist. + + Fails completely if any binary file is missing, as requested. + """ + print("Validating binary files...") + + if not self.images_dir.exists(): + raise FileNotFoundError(f"Images directory not found: {self.images_dir}") + + missing_files = [] + for entry in self.fw_entries: + if not entry['binary_path'].exists(): + missing_files.append(str(entry['binary_path'])) + else: + # Check file size + size = entry['binary_path'].stat().st_size + print(f" {entry['binary_file']}: {size} bytes") + + if missing_files: + raise FileNotFoundError(f"Missing binary files: {missing_files}") + + print("All binary files validated successfully") + + def generate_its(self, its_path): + """ + Generate capsule-compatible ITS file. + + Creates ITS content that works with the Qualcomm capsule update flow: + - Node names match partition names for DFU mapping + - All images marked as "firmware" type + - SHA256 hashes for integrity verification + - Single configuration referencing all firmware + """ + print(f"Generating ITS file: {its_path}") + + its_content = self._build_its_content() + + try: + with open(its_path, 'w') as f: + f.write(its_content) + except IOError as e: + raise IOError(f"Failed to write ITS file: {e}") + + print(f"ITS file generated successfully") + + def _build_its_content(self): + """ + Build ITS content compatible with capsule update flow. + + Format ensures compatibility with: + - qcom_configure_capsule_updates() partition discovery + - fit_update() processing requirements + - fit_image_verify() hash validation + - dfu_write_by_name() partition targeting + """ + images_section = "" + firmware_list = [] + + for entry in self.fw_entries: + # Node name format: partition_name (without @1 to avoid reg issues) + # This MUST match the partition name discovered by U-Boot + node_name = entry['partition_name'] + firmware_list.append(f'"{node_name}"') + + # Generate image node without reg properties to avoid DTC warnings + images_section += f''' + {node_name} {{ + description = "{entry['partition_name']} Firmware"; + data = /incbin/("Images/{entry['binary_file']}"); + type = "firmware"; /* Required for fit_update() */ + arch = "arm64"; /* Target architecture */ + compression = "none"; /* No compression */ + load = <0x00000000>; /* Required by fit_image_get_load() */ + + hash-1 {{ + algo = "sha256"; /* For fit_image_verify() */ + }}; + }};''' + + # Build firmware reference list for configuration + firmware_refs = ", ".join(firmware_list) + + # Complete ITS content + return f'''/dts-v1/; + +/ {{ + description = "Qualcomm Firmware Update Package"; + + images {{{images_section} + }}; + + configurations {{ + default = "config-1"; + config-1 {{ + description = "Qualcomm Multi-Partition Update"; + firmware = {firmware_refs}; + }}; + }}; +}}; +''' + + def compile_fit(self, its_path, fit_path): + """ + Compile ITS to FIT using mkimage. + + Uses U-Boot's mkimage tool to create the final FIT image. + """ + print(f"Compiling FIT image: {fit_path}") + + # Check if mkimage is available + try: + subprocess.run(["mkimage", "-V"], capture_output=True, check=True) + except (subprocess.CalledProcessError, FileNotFoundError): + raise RuntimeError("mkimage tool not found. Please install U-Boot tools.") + + # Compile ITS to FIT + cmd = ["mkimage", "-f", str(its_path), str(fit_path)] + print(f"Running: {' '.join(cmd)}") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, + cwd=self.base_dir, check=True) + + if result.stdout: + print("mkimage output:") + print(result.stdout) + + except subprocess.CalledProcessError as e: + error_msg = f"mkimage compilation failed (exit code {e.returncode})" + if e.stderr: + error_msg += f": {e.stderr}" + raise RuntimeError(error_msg) + + # Verify output file was created + if not fit_path.exists(): + raise RuntimeError("FIT image was not created") + + fit_size = fit_path.stat().st_size + print(f"FIT image compiled successfully: {fit_size} bytes") + + def check_required_tools(self): + """Check if required tools are available, auto-install if missing""" + required_tools = { + 'mkimage': 'u-boot-tools', + 'mkeficapsule': 'u-boot-tools' + } + + missing_tools = [] + for tool, package in required_tools.items(): + if not self.tool_available(tool): + missing_tools.append((tool, package)) + + if missing_tools: + print("Installing missing tools...") + packages_to_install = set() + for tool, package in missing_tools: + packages_to_install.add(package) + + for package in packages_to_install: + self.install_package(package) + print(f" Installed {package} ✓") + + def tool_available(self, tool_name): + """Check if a tool is available in PATH""" + return shutil.which(tool_name) is not None + + def install_package(self, package): + """Install package using system package manager""" + # Detect package manager and install + if shutil.which('apt-get'): + cmd = ['sudo', 'apt-get', 'install', '-y', package] + elif shutil.which('yum'): + cmd = ['sudo', 'yum', 'install', '-y', package] + elif shutil.which('dnf'): + cmd = ['sudo', 'dnf', 'install', '-y', package] + elif shutil.which('pacman'): + cmd = ['sudo', 'pacman', '-S', '--noconfirm', package] + else: + raise RuntimeError(f"Cannot auto-install {package}. Please install manually.") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Failed to install {package}: {e.stderr}") + + def get_primary_partition_name(self): + """Get partition name from first FwEntry with Operation='UPDATE'""" + if not self.fw_entries: + raise ValueError("No UPDATE FwEntry found in XML") + + # Return first UPDATE partition found + primary_partition = self.fw_entries[0]['partition_name'] + print(f"Primary partition selected: {primary_partition} (first UPDATE entry)") + return primary_partition + + def is_valid_guid(self, guid): + """Validate GUID format""" + # GUID format: 8-4-4-4-12 hex digits + guid_pattern = r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' + return re.match(guid_pattern, guid) is not None + + def create_capsule(self, fit_path, guid, capsule_path, fw_version=None, monotonic_count=None, private_key=None, certificate=None): + """Create capsule using mkeficapsule with optional firmware version and signing""" + print(f"Creating capsule: {capsule_path}") + + cmd = [ + self.mkeficapsule_path, + '--guid', guid, + '--index', '1', + ] + + # Add firmware version if provided + if fw_version is not None: + version_parts = fw_version.split('.') + try: + major = int(version_parts[2]) + minor = int(version_parts[3]) + encoded_version = (major << 16) | minor + cmd.extend(['--fw-version', str(encoded_version)]) + print(f" Encoded Firmware version: {encoded_version} (from {fw_version})") + except (ValueError, IndexError): + raise ValueError(f"Invalid version string: {fw_version}") + + # Add signing parameters if provided + if private_key and certificate and monotonic_count is not None: + print("Signing capsule...") + cmd.extend([ + '--monotonic-count', str(monotonic_count), + '--private-key', str(private_key), + '--certificate', str(certificate), + ]) + elif any([private_key, certificate, monotonic_count is not None]): + raise ValueError("All signing parameters (--private-key, --certificate, --monotonic-count) must be provided together.") + + cmd.extend([str(fit_path), str(capsule_path)]) + + print(f" Command: {' '.join(cmd)}") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, + check=True, timeout=60) + + if result.stdout: + print("mkeficapsule output:") + print(result.stdout) + + except subprocess.CalledProcessError as e: + error_msg = f"Capsule creation failed (exit code {e.returncode})" + if e.stderr: + error_msg += f": {e.stderr}" + raise RuntimeError(error_msg) + except subprocess.TimeoutExpired: + raise RuntimeError("Capsule creation timed out") + + # Verify output file was created + if not capsule_path.exists(): + raise RuntimeError("Capsule file was not created") + + capsule_size = capsule_path.stat().st_size + print(f"Capsule created successfully: {capsule_size} bytes ✓") + return capsule_size + + def convert_complete(self, fit_output_name="system.fit", capsule_output_name="firmware.capsule", fw_version=None, guid=None, monotonic_count=None, private_key=None, certificate=None): + """ + Complete conversion workflow: XML → FIT → Capsule + + Performs the full end-to-end conversion: + 1. Check and install required tools + 2. XML parsing and validation + 3. Binary file validation + 4. ITS generation + 5. FIT compilation + 6. Capsule creation with provided GUID and optional firmware version + 7. Keep all intermediate files + """ + print(f"Converting {self.xml_path} to capsule...") + print("=" * 60) + + # Step 1: Check and install required tools + self.check_required_tools() + + # Step 2-5: Generate FIT + self.parse_xml() + self.validate_files() + + # Generate file paths + its_path = self.base_dir / "system.its" + fit_path = self.base_dir / fit_output_name + capsule_path = self.base_dir / capsule_output_name + + self.generate_its(its_path) + self.compile_fit(its_path, fit_path) + + # Step 6: Create capsule with provided GUID and optional firmware version + if not guid: + raise ValueError("GUID is required for capsule creation. Use --guid option.") + + print(f"Using GUID: {guid}") + capsule_size = self.create_capsule(fit_path, guid, capsule_path, fw_version, monotonic_count, private_key, certificate) + + # Step 7: Summary (keep all files) + print("=" * 60) + print("SUCCESS: Complete capsule workflow completed!") + print() + print("Files created:") + print(f" ITS file: {its_path}") + print(f" FIT file: {fit_path}") + print(f" Capsule file: {capsule_path} ({capsule_size / (1024*1024):.1f} MB)") + print() + print(f"Capsule GUID: {guid}") + print() + print("Ready for deployment:") + print(" 1. Copy firmware.capsule to boot partition") + print(" 2. In U-Boot: fatload mmc 0:1 $loadaddr firmware.capsule") + print(" 3. In U-Boot: efidebug capsule update $loadaddr") + + return capsule_path + + def convert(self, output_name="system.fit"): + """ + Main conversion workflow. + + Converts FvUpdate.xml to capsule-compatible FIT image through: + 1. XML parsing and validation + 2. Binary file validation + 3. ITS generation + 4. FIT compilation + """ + print(f"Converting {self.xml_path} to FIT image...") + print("=" * 60) + + # Parse and validate XML + self.parse_xml() + + # Validate binary files + self.validate_files() + + # Generate ITS file + its_path = self.base_dir / "system.its" + self.generate_its(its_path) + + # Compile FIT image + fit_path = self.base_dir / output_name + self.compile_fit(its_path, fit_path) + + print("=" * 60) + print("CONVERSION SUMMARY:") + print(f" Input XML: {self.xml_path}") + print(f" Generated ITS: {its_path}") + print(f" Output FIT: {fit_path}") + print(f" Partitions: {len(self.fw_entries)}") + for entry in self.fw_entries: + print(f" - {entry['partition_name']} ({entry['binary_file']})") + + return fit_path + +def main(): + """Main entry point with command line argument parsing.""" + parser = argparse.ArgumentParser( + description="Convert FvUpdate.xml to capsule-compatible FIT image and capsule", + epilog=""" +Examples: + # Generate FIT image only: + %(prog)s FvUpdate.xml + + # Generate FIT image and create capsule: + %(prog)s FvUpdate.xml --mkeficapsule --guid --fw-version + +The script can operate in two modes: +1. FIT-only mode: Creates FIT image from FvUpdate.xml +2. Complete mode: Creates FIT image and capsule (requires --guid and --mkeficapsule) + +Workflow: +1. Auto-install missing tools (mkimage, mkeficapsule) +2. Parse FvUpdate.xml for UPDATE operations +3. Validate all binary files in Images/ directory +4. Generate system.its file +5. Compile system.fit image using mkimage +6. Create firmware.capsule using mkeficapsule (if --guid provided) + +The generated capsule is ready for deployment in Qualcomm U-Boot systems. + """, + formatter_class=argparse.RawDescriptionHelpFormatter + ) + + parser.add_argument('xml_file', + help='Path to FvUpdate.xml file') + parser.add_argument('--guid', + help='Capsule GUID for capsule generation. Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. ' + 'Board-specific GUIDs: ' + 'qcs615: 9FD379D2-670E-4BB3-86A1-40497E6E17B0, ' + 'qcs6490-rb3gen2: 6f25bfd2-a165-468b-980f-ac51a0a45c52, ' + 'lemans-evk: 78462415-6133-431c-9fae-48f2bafd5c71') + parser.add_argument('--mkeficapsule', + help='Path to mkeficapsule binary (default: use system PATH)') + parser.add_argument('-o', '--output', + default='system.fit', + help='Output FIT image name (default: %(default)s)') + parser.add_argument('-c', '--capsule-output', + default='firmware.capsule', + help='Output capsule file name (default: %(default)s)') + parser.add_argument('--fw-version', + help='Firmware version for capsule in "0.0.A.B" format (e.g., "0.0.1.0"). This version will be stored in ESRT.') + parser.add_argument('--monotonic-count', + type=int, + help='Monotonic count for capsule signing.') + parser.add_argument('--private-key', + help='Path to the private key for signing.') + parser.add_argument('--certificate', + help='Path to the certificate for signing.') + parser.add_argument('-v', '--verbose', + action='store_true', + help='Enable verbose output') + + args = parser.parse_args() + + # Validate input file + xml_path = Path(args.xml_file) + if not xml_path.exists(): + print(f"ERROR: XML file not found: {xml_path}") + sys.exit(1) + + if not xml_path.is_file(): + print(f"ERROR: Path is not a file: {xml_path}") + sys.exit(1) + + # Validate GUID format if provided + if args.guid: + guid_pattern = r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' + if not re.match(guid_pattern, args.guid): + print(f"ERROR: Invalid GUID format: {args.guid}") + print("Expected format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") + sys.exit(1) + + # Validate fw-version format if provided + if args.fw_version: + version_pattern = r'^\d+\.\d+\.\d+\.\d+$' + if not re.match(version_pattern, args.fw_version): + print(f"ERROR: Invalid firmware version format: {args.fw_version}") + print('Expected format: "0.0.A.B" (e.g., "0.0.1.0")') + sys.exit(1) + + # Create converter + converter = FvUpdateToFitConverter(xml_path, args.mkeficapsule) + + try: + if args.guid: + # Complete capsule workflow + capsule_path = converter.convert_complete( + args.output, + args.capsule_output, + args.fw_version, + args.guid, + args.monotonic_count, + args.private_key, + args.certificate + ) + + print("\n" + "=" * 60) + print("SUCCESS: Complete capsule workflow completed!") + print(f"Final capsule: {capsule_path}") + if args.fw_version: + print(f"Firmware version: {args.fw_version}") + + else: + # FIT-only mode + fit_path = converter.convert(args.output) + + print("\n" + "=" * 60) + print("SUCCESS: FIT image created successfully!") + print(f"Output: {fit_path}") + print("\nTo create capsule, run again with:") + print(f" {sys.argv[0]} {args.xml_file} --mkeficapsule --guid --fw-version ") + + except Exception as e: + print(f"\nERROR: {e}") + if args.verbose: + import traceback + traceback.print_exc() + sys.exit(1) + +if __name__ == "__main__": + main()