diff --git a/components/app_update/esp_ota_ops.c b/components/app_update/esp_ota_ops.c index 597a02a13d..22fff3ab58 100644 --- a/components/app_update/esp_ota_ops.c +++ b/components/app_update/esp_ota_ops.c @@ -1012,6 +1012,52 @@ bool esp_ota_check_rollback_is_possible(void) return false; } +esp_err_t esp_ota_check_image_validity(esp_partition_type_t part_type, + const esp_image_header_t *img_hdr, + const esp_app_desc_t *app_desc) +{ + if (img_hdr == NULL) { + return ESP_ERR_INVALID_ARG; + } + + // Map partition type to image type for bootloader_common API + esp_image_type img_type; + if (part_type == ESP_PARTITION_TYPE_APP) { + img_type = ESP_IMAGE_APPLICATION; + } else if (part_type == ESP_PARTITION_TYPE_BOOTLOADER) { + img_type = ESP_IMAGE_BOOTLOADER; + } else { + return ESP_ERR_INVALID_ARG; + } + + // Check chip ID and chip revision validity + esp_err_t err = bootloader_common_check_chip_validity(img_hdr, img_type); + if (err != ESP_OK) { + return ESP_ERR_INVALID_VERSION; + } + + // Check SPI flash mode if app descriptor is provided + if (part_type == ESP_PARTITION_TYPE_APP && app_desc != NULL) { + // Get the running app's descriptor + const esp_app_desc_t *running_app_desc = esp_app_get_description(); + + if (running_app_desc->spi_flash_mode == 0 || app_desc->spi_flash_mode == 0) { + // Older image format, CONFIG_ESPTOOLPY_FLASHMODE_VAL not stored in the app descriptor + ESP_LOGD(TAG, "Older image format and hence no SPI flash mode info is available"); + return ESP_OK; + } + + // Compare SPI flash modes as stored in app descriptor (CONFIG_ESPTOOLPY_FLASHMODE_VAL) + if (app_desc->spi_flash_mode != running_app_desc->spi_flash_mode) { + ESP_LOGE(TAG, "SPI flash mode mismatch: running app has mode %d, new app has mode %d", + running_app_desc->spi_flash_mode, app_desc->spi_flash_mode); + return ESP_ERR_OTA_SPI_MODE_MISMATCH; + } + } + + return ESP_OK; +} + // if valid == false - will done rollback with reboot. After reboot will boot previous OTA[x] or Factory partition. // if valid == true - it confirm that current OTA[x] is workable. Reboot will not happen. static esp_err_t esp_ota_current_ota_is_workable(bool valid) diff --git a/components/app_update/include/esp_ota_ops.h b/components/app_update/include/esp_ota_ops.h index c08945285e..4d99d7ae8f 100644 --- a/components/app_update/include/esp_ota_ops.h +++ b/components/app_update/include/esp_ota_ops.h @@ -13,6 +13,7 @@ #include "esp_err.h" #include "esp_partition.h" #include "esp_app_desc.h" +#include "esp_app_format.h" #include "esp_bootloader_desc.h" #include "esp_flash_partitions.h" #include "soc/soc_caps.h" @@ -32,7 +33,8 @@ extern "C" #define ESP_ERR_OTA_SMALL_SEC_VER (ESP_ERR_OTA_BASE + 0x04) /*!< Error if the firmware has a secure version less than the running firmware. */ #define ESP_ERR_OTA_ROLLBACK_FAILED (ESP_ERR_OTA_BASE + 0x05) /*!< Error if flash does not have valid firmware in passive partition and hence rollback is not possible */ #define ESP_ERR_OTA_ROLLBACK_INVALID_STATE (ESP_ERR_OTA_BASE + 0x06) /*!< Error if current active firmware is still marked in pending validation state (ESP_OTA_IMG_PENDING_VERIFY), essentially first boot of firmware image post upgrade and hence firmware upgrade is not possible */ -#define ESP_ERR_OTA_ALREADY_IN_PROGRESS (ESP_ERR_OTA_BASE + 0x07) /*!< Error if another OTA operation is already in progress on the same partition */ +#define ESP_ERR_OTA_ALREADY_IN_PROGRESS (ESP_ERR_OTA_BASE + 0x07) /*!< Error if another OTA operation is already in progress on the same partition */ +#define ESP_ERR_OTA_SPI_MODE_MISMATCH (ESP_ERR_OTA_BASE + 0x08) /*!< Error if the firmware's SPI flash mode doesn't match the running firmware */ /** @@ -420,6 +422,31 @@ esp_err_t esp_ota_erase_last_boot_app_partition(void); */ bool esp_ota_check_rollback_is_possible(void); +/** + * @brief Check image validity including chip ID, chip revision, and optionally SPI flash mode. + * + * This function performs comprehensive validation of an OTA image: + * - Verifies the chip ID matches the current chip + * - Verifies the chip revision meets the image requirements + * - Optionally verifies the SPI flash mode matches (if app_desc is provided) + * + * For bootloader partitions (ESP_PARTITION_TYPE_BOOTLOADER), the maximum chip revision check is skipped. + * For application partitions (ESP_PARTITION_TYPE_APP), both minimum and maximum chip revision are checked. + * + * @param[in] part_type Partition type (ESP_PARTITION_TYPE_APP or ESP_PARTITION_TYPE_BOOTLOADER) + * @param[in] img_hdr Pointer to the image header for chip ID and revision checks + * @param[in] app_desc Pointer to the app descriptor for SPI mode check (can be NULL to skip SPI mode verification) + * + * @return + * - ESP_OK: Image is valid for this chip + * - ESP_ERR_INVALID_ARG: img_hdr is NULL or part_type is not APP or BOOTLOADER + * - ESP_ERR_INVALID_VERSION: Chip ID or chip revision mismatch + * - ESP_ERR_OTA_SPI_MODE_MISMATCH: SPI flash mode does not match (only when app_desc is provided) + */ +esp_err_t esp_ota_check_image_validity(esp_partition_type_t part_type, + const esp_image_header_t *img_hdr, + const esp_app_desc_t *app_desc); + #if SOC_EFUSE_SECURE_BOOT_KEY_DIGESTS > 1 && (CONFIG_SECURE_BOOT_V2_ENABLED || __DOXYGEN__) /** diff --git a/components/app_update/test_apps/test_app_update/main/test_ota_ops.c b/components/app_update/test_apps/test_app_update/main/test_ota_ops.c index 0dbfabb76c..c431ec9dd4 100644 --- a/components/app_update/test_apps/test_app_update/main/test_ota_ops.c +++ b/components/app_update/test_apps/test_app_update/main/test_ota_ops.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/components/esp_app_format/esp_app_desc.c b/components/esp_app_format/esp_app_desc.c index eb169aebd7..4b53fdc702 100644 --- a/components/esp_app_format/esp_app_desc.c +++ b/components/esp_app_format/esp_app_desc.c @@ -60,6 +60,9 @@ const __attribute__((weak)) __attribute__((section(".rodata_desc"))) esp_app_de .min_efuse_blk_rev_full = CONFIG_ESP_EFUSE_BLOCK_REV_MIN_FULL, .max_efuse_blk_rev_full = CONFIG_ESP_EFUSE_BLOCK_REV_MAX_FULL, .mmu_page_size = 31 - __builtin_clz(CONFIG_MMU_PAGE_SIZE), +#if !(CONFIG_IDF_TARGET_LINUX || CONFIG_APP_BUILD_TYPE_PURE_RAM_APP) + .spi_flash_mode = CONFIG_ESPTOOLPY_FLASHMODE_VAL, +#endif }; #ifndef CONFIG_APP_EXCLUDE_PROJECT_VER_VAR diff --git a/components/esp_app_format/include/esp_app_desc.h b/components/esp_app_format/include/esp_app_desc.h index 6cb73e3676..675ab78116 100644 --- a/components/esp_app_format/include/esp_app_desc.h +++ b/components/esp_app_format/include/esp_app_desc.h @@ -36,7 +36,8 @@ typedef struct { uint16_t min_efuse_blk_rev_full; /*!< Minimal eFuse block revision supported by image, in format: major * 100 + minor */ uint16_t max_efuse_blk_rev_full; /*!< Maximal eFuse block revision supported by image, in format: major * 100 + minor */ uint8_t mmu_page_size; /*!< MMU page size in log base 2 format */ - uint8_t reserv3[3]; /*!< reserv3 */ + uint8_t spi_flash_mode; /*!< SPI flash mode as per CONFIG_ESPTOOLPY_FLASHMODE_VAL for compatibility check during OTA */ + uint8_t reserv3[2]; /*!< reserv3 */ uint32_t reserv2[18]; /*!< reserv2 */ } esp_app_desc_t; diff --git a/components/esp_common/src/esp_err_to_name.c b/components/esp_common/src/esp_err_to_name.c index 6504f54650..0e5f0ed096 100644 --- a/components/esp_common/src/esp_err_to_name.c +++ b/components/esp_common/src/esp_err_to_name.c @@ -294,6 +294,10 @@ static const esp_err_msg_t esp_err_msg_table[] = { ERR_TBL_IT(ESP_ERR_OTA_ALREADY_IN_PROGRESS), /* 5383 0x1507 Error if another OTA operation is already in progress on the same partition */ +# endif +# ifdef ESP_ERR_OTA_SPI_MODE_MISMATCH + ERR_TBL_IT(ESP_ERR_OTA_SPI_MODE_MISMATCH), /* 5384 0x1508 Error if the firmware's SPI flash mode + doesn't match the running firmware */ # endif // components/efuse/include/esp_efuse.h # ifdef ESP_ERR_EFUSE diff --git a/components/esp_https_ota/Kconfig b/components/esp_https_ota/Kconfig index 9873b3a0fb..eddc95fe47 100644 --- a/components/esp_https_ota/Kconfig +++ b/components/esp_https_ota/Kconfig @@ -33,4 +33,13 @@ menu "ESP HTTPS OTA" This enables use of range header in esp_https_ota component. The firmware image will be downloaded over multiple HTTP requests. + config ESP_HTTPS_OTA_VERIFY_SPI_MODE + bool "Verify SPI flash mode compatibility for application during OTA" + default y + help + When enabled, the OTA process will verify that the SPI flash mode of the new + application image is compatible with the currently running firmware. This helps + prevent flashing firmware with incompatible SPI mode settings which could + cause flash write failures. + endmenu diff --git a/components/esp_https_ota/src/esp_https_ota.c b/components/esp_https_ota/src/esp_https_ota.c index 14c51afa02..a63bbbd3b8 100644 --- a/components/esp_https_ota/src/esp_https_ota.c +++ b/components/esp_https_ota/src/esp_https_ota.c @@ -695,27 +695,24 @@ esp_err_t esp_https_ota_get_bootloader_img_desc(esp_https_ota_handle_t https_ota return get_description_from_image(https_ota_handle, new_img_info); } -static esp_err_t esp_ota_verify_chip_id(const void *arg) +static const esp_app_desc_t *esp_https_ota_get_app_desc(const void *data_buf) { - esp_image_header_t *data = (esp_image_header_t *)(arg); - esp_https_ota_dispatch_event(ESP_HTTPS_OTA_VERIFY_CHIP_ID, (void *)(&data->chip_id), sizeof(esp_chip_id_t)); - - if (data->chip_id != CONFIG_IDF_FIRMWARE_CHIP_ID) { - ESP_LOGE(TAG, "Mismatch chip id, expected %d, found %d", CONFIG_IDF_FIRMWARE_CHIP_ID, data->chip_id); - return ESP_ERR_INVALID_VERSION; - } - return ESP_OK; + return (const esp_app_desc_t *)((const uint8_t *)data_buf + + sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t)); } -static esp_err_t esp_ota_verify_chip_revision(const void *arg) +static esp_err_t esp_https_ota_verify_image(const void *data_buf, esp_partition_type_t part_type, bool verify_spi_mode) { - esp_image_header_t *data = (esp_image_header_t *)(arg); - esp_https_ota_dispatch_event(ESP_HTTPS_OTA_VERIFY_CHIP_REVISION, (void *)(&data->min_chip_rev_full), sizeof(uint16_t)); + const esp_image_header_t *img_hdr = (const esp_image_header_t *) data_buf; - if (!bootloader_common_check_chip_revision_validity(data, true)) { - return ESP_ERR_INVALID_VERSION; - } - return ESP_OK; + // Dispatch verification events + esp_https_ota_dispatch_event(ESP_HTTPS_OTA_VERIFY_CHIP_ID, (void *)(&img_hdr->chip_id), sizeof(esp_chip_id_t)); + esp_https_ota_dispatch_event(ESP_HTTPS_OTA_VERIFY_CHIP_REVISION, (void *)(&img_hdr->min_chip_rev_full), sizeof(uint16_t)); + + // Get app descriptor only if SPI mode verification is needed + const esp_app_desc_t *app_desc = verify_spi_mode ? esp_https_ota_get_app_desc(data_buf) : NULL; + + return esp_ota_check_image_validity(part_type, img_hdr, app_desc); } esp_err_t esp_https_ota_perform(esp_https_ota_handle_t https_ota_handle) @@ -785,12 +782,11 @@ esp_err_t esp_https_ota_perform(esp_https_ota_handle_t https_ota_handle) } #endif // CONFIG_ESP_HTTPS_OTA_DECRYPT_CB if (handle->partition.final->type == ESP_PARTITION_TYPE_APP || handle->partition.final->type == ESP_PARTITION_TYPE_BOOTLOADER) { - err = esp_ota_verify_chip_id(data_buf); - if (err != ESP_OK) { - return err; - } - - err = esp_ota_verify_chip_revision(data_buf); + bool verify_spi_mode = false; +#if CONFIG_ESP_HTTPS_OTA_VERIFY_SPI_MODE + verify_spi_mode = (handle->partition.final->type == ESP_PARTITION_TYPE_APP); +#endif + err = esp_https_ota_verify_image(data_buf, handle->partition.final->type, verify_spi_mode); if (err != ESP_OK) { return err; } diff --git a/components/esptool_py/Kconfig.projbuild b/components/esptool_py/Kconfig.projbuild index 8e6c99d908..de77430213 100644 --- a/components/esptool_py/Kconfig.projbuild +++ b/components/esptool_py/Kconfig.projbuild @@ -88,6 +88,17 @@ menu "Serial flasher config" # information get from efuse, so don't care this dout choice. default "dout" if ESPTOOLPY_FLASHMODE_OPI + # Hidden config providing the numeric value of flash mode + # This value will be stored in the esp_app_desc_t structure in the binary + # Note: legacy images will have this field set to 0 and hence using 1 to start the count + config ESPTOOLPY_FLASHMODE_VAL + int + default 1 if ESPTOOLPY_FLASHMODE_QIO + default 2 if ESPTOOLPY_FLASHMODE_QOUT + default 3 if ESPTOOLPY_FLASHMODE_DIO + default 4 if ESPTOOLPY_FLASHMODE_DOUT + default 5 if ESPTOOLPY_FLASHMODE_OPI + orsource "../spi_flash/$IDF_TARGET/Kconfig.flash_freq" config ESPTOOLPY_FLASHFREQ diff --git a/docs/en/api-reference/system/app_image_format.rst b/docs/en/api-reference/system/app_image_format.rst index 34235d4afd..0f007a942a 100644 --- a/docs/en/api-reference/system/app_image_format.rst +++ b/docs/en/api-reference/system/app_image_format.rst @@ -125,6 +125,10 @@ The ``DROM`` segment of the application binary starts with the :cpp:type:`esp_ap * ``time`` and ``date``: compile time and date * ``idf_ver``: version of ESP-IDF [#f1]_ * ``app_elf_sha256``: contains SHA256 hash for the application ELF file +* ``min_efuse_blk_rev_full``: minimal eFuse block revision supported by the image, in format: major * 100 + minor +* ``max_efuse_blk_rev_full``: maximal eFuse block revision supported by the image, in format: major * 100 + minor +* ``mmu_page_size``: MMU page size in log base 2 format +* ``spi_flash_mode``: SPI flash mode as per ``CONFIG_ESPTOOLPY_FLASHMODE_VAL`` for compatibility check during OTA .. [#f1] The maximum length is 32 characters, including null-termination character. For example, if the length of ``PROJECT_NAME`` exceeds 31 characters, the excess characters will be disregarded. diff --git a/docs/zh_CN/api-reference/system/app_image_format.rst b/docs/zh_CN/api-reference/system/app_image_format.rst index 55251a40ee..ad950970f2 100644 --- a/docs/zh_CN/api-reference/system/app_image_format.rst +++ b/docs/zh_CN/api-reference/system/app_image_format.rst @@ -125,6 +125,10 @@ * ``time`` 和 ``date``:编译时间和日期 * ``idf_ver``:ESP-IDF 的版本 [#f1]_ * ``app_elf_sha256``:包含应用程序 ELF 文件的 sha256 哈希 +* ``min_efuse_blk_rev_full``:镜像支持的最小 eFuse 块版本,格式为:major * 100 + minor +* ``max_efuse_blk_rev_full``:镜像支持的最大 eFuse 块版本,格式为:major * 100 + minor +* ``mmu_page_size``:MMU 页大小,以 log2 格式表示 +* ``spi_flash_mode``:SPI flash 模式,取自 ``CONFIG_ESPTOOLPY_FLASHMODE_VAL``,用于 OTA 过程中的兼容性检查 .. [#f1] 最大长度为 32 个字符,其中包括 null 终止符。也就是说,如果 ``PROJECT_NAME`` 的长度超过 31 个字符,超出的字符将被忽略。 diff --git a/examples/system/ota/advanced_https_ota/pytest_advanced_ota.py b/examples/system/ota/advanced_https_ota/pytest_advanced_ota.py index 49f0c6cd52..7a3d678235 100644 --- a/examples/system/ota/advanced_https_ota/pytest_advanced_ota.py +++ b/examples/system/ota/advanced_https_ota/pytest_advanced_ota.py @@ -622,7 +622,7 @@ def test_examples_protocol_advanced_https_ota_example_invalid_chip_id(dut: Dut) dut.expect('Starting Advanced OTA example', timeout=30) print('writing to device: {}'.format('https://' + host_ip + ':' + str(server_port) + '/' + random_bin_name)) dut.write('https://' + host_ip + ':' + str(server_port) + '/' + random_bin_name) - dut.expect(r'esp_https_ota: Mismatch chip id, expected 0, found \d', timeout=10) + dut.expect(r'boot_comm: mismatch chip ID, expected 0, found \d', timeout=10) try: os.remove(random_binary_file) except OSError: