From: Konstantin Kondrashov Date: Tue, 23 Oct 2018 12:27:32 +0000 (+0800) Subject: bootloader: Add support flags for rollback app X-Git-Tag: v3.3-beta2~160^2 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=dde1fd9b9402b1826ccaeabb8cbc4d531388d6a1;p=esp-idf bootloader: Add support flags for rollback app Added * Set actual ota_seq if both ota are init or incorrect. * Description of rollback * UT tests Closes TW15459 --- diff --git a/components/app_update/esp_ota_ops.c b/components/app_update/esp_ota_ops.c index 5d415ffeca..b11f356999 100644 --- a/components/app_update/esp_ota_ops.c +++ b/components/app_update/esp_ota_ops.c @@ -39,6 +39,7 @@ #include "esp_flash_data_types.h" #include "bootloader_common.h" #include "sys/param.h" +#include "esp_system.h" #define SUB_TYPE_ID(i) (i & 0x0F) @@ -115,6 +116,16 @@ static esp_err_t image_validate(const esp_partition_t *partition, esp_image_load return ESP_OK; } +static esp_ota_img_states_t set_new_state_otadata(void) +{ +#ifdef CONFIG_APP_ROLLBACK_ENABLE + ESP_LOGD(TAG, "Monitoring the first boot of the app is enabled."); + return ESP_OTA_IMG_NEW; +#else + return ESP_OTA_IMG_UNDEFINED; +#endif +} + esp_err_t esp_ota_begin(const esp_partition_t *partition, size_t image_size, esp_ota_handle_t *out_handle) { ota_ops_entry_t *new_entry; @@ -348,10 +359,12 @@ static esp_err_t esp_rewrite_ota_data(esp_partition_subtype_t subtype) i++; } int next_otadata = (~active_otadata)&1; // if 0 -> will be next 1. and if 1 -> will be next 0. + otadata[next_otadata].ota_state = set_new_state_otadata(); return rewrite_ota_seq(otadata, (SUB_TYPE_ID(subtype) + 1) % ota_app_count + i * ota_app_count, next_otadata, otadata_partition); } else { /* Both OTA slots are invalid, probably because unformatted... */ int next_otadata = 0; + otadata[next_otadata].ota_state = set_new_state_otadata(); return rewrite_ota_seq(otadata, SUB_TYPE_ID(subtype) + 1, next_otadata, otadata_partition); } } @@ -547,3 +560,185 @@ esp_err_t esp_ota_get_partition_description(const esp_partition_t *partition, es 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) +{ + esp_ota_select_entry_t otadata[2]; + const esp_partition_t *otadata_partition = read_otadata(otadata); + if (otadata_partition == NULL) { + return ESP_ERR_NOT_FOUND; + } + + int active_otadata = bootloader_common_get_active_otadata(otadata); + if (active_otadata != -1 && get_ota_partition_count() != 0) { + if (valid == true && otadata[active_otadata].ota_state != ESP_OTA_IMG_VALID) { + otadata[active_otadata].ota_state = ESP_OTA_IMG_VALID; + ESP_LOGD(TAG, "OTA[current] partition is marked as VALID"); + return rewrite_ota_seq(otadata, otadata[active_otadata].ota_seq, active_otadata, otadata_partition); + } else if (valid == false) { + ESP_LOGD(TAG, "OTA[current] partition is marked as INVALID"); + otadata[active_otadata].ota_state = ESP_OTA_IMG_INVALID; + esp_err_t err = rewrite_ota_seq(otadata, otadata[active_otadata].ota_seq, active_otadata, otadata_partition); + if (err != ESP_OK) { + return err; + } + ESP_LOGI(TAG, "Rollback to previously worked partition. Restart."); + esp_restart(); + } + } else { + ESP_LOGE(TAG, "Running firmware is factory"); + return ESP_FAIL; + } + return ESP_OK; +} + +esp_err_t esp_ota_mark_app_valid_cancel_rollback() +{ + return esp_ota_current_ota_is_workable(true); +} + +esp_err_t esp_ota_mark_app_invalid_rollback_and_reboot() +{ + return esp_ota_current_ota_is_workable(false); +} + +static bool check_invalid_otadata (const esp_ota_select_entry_t *s) { + return s->ota_seq != UINT32_MAX && + s->crc == bootloader_common_ota_select_crc(s) && + (s->ota_state == ESP_OTA_IMG_INVALID || + s->ota_state == ESP_OTA_IMG_ABORTED); +} + +static int get_last_invalid_otadata(const esp_ota_select_entry_t *two_otadata) +{ + int num_invalid_otadata = -1; + + bool invalid_otadata[2]; + invalid_otadata[0] = check_invalid_otadata(&two_otadata[0]); + invalid_otadata[1] = check_invalid_otadata(&two_otadata[1]); + if (invalid_otadata[0] == true && invalid_otadata[1] == true) { + if (MIN(two_otadata[0].ota_seq, two_otadata[1].ota_seq) == two_otadata[0].ota_seq) { + num_invalid_otadata = 0; + } else { + num_invalid_otadata = 1; + } + } else { + for (int i = 0; i < 2; ++i) { + if(invalid_otadata[i]) { + num_invalid_otadata = i; + break; + } + } + } + + ESP_LOGD(TAG, "Invalid otadata[%d]", num_invalid_otadata); + return num_invalid_otadata; +} + +const esp_partition_t* esp_ota_get_last_invalid_partition() +{ + esp_ota_select_entry_t otadata[2]; + if (read_otadata(otadata) == NULL) { + return NULL; + } + + int invalid_otadata = get_last_invalid_otadata(otadata); + + int ota_app_count = get_ota_partition_count(); + if (invalid_otadata != -1 && ota_app_count != 0) { + int ota_slot = (otadata[invalid_otadata].ota_seq - 1) % ota_app_count; + ESP_LOGD(TAG, "Find invalid ota_%d app", ESP_PARTITION_SUBTYPE_APP_OTA_MIN + ota_slot); + + const esp_partition_t* invalid_partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_MIN + ota_slot, NULL); + if (invalid_partition != NULL) { + if (image_validate(invalid_partition, ESP_IMAGE_VERIFY_SILENT) != ESP_OK) { + ESP_LOGD(TAG, "Last invalid partition has corrupted app"); + return NULL; + } + } + return invalid_partition; + } + return NULL; +} + +esp_err_t esp_ota_get_state_partition(const esp_partition_t *partition, esp_ota_img_states_t *ota_state) +{ + if (partition == NULL || ota_state == NULL) { + return ESP_ERR_INVALID_ARG; + } + + if (!is_ota_partition(partition)) { + return ESP_ERR_NOT_SUPPORTED; + } + + esp_ota_select_entry_t otadata[2]; + int ota_app_count = get_ota_partition_count(); + if (read_otadata(otadata) == NULL || ota_app_count == 0) { + return ESP_ERR_NOT_FOUND; + } + + int req_ota_slot = partition->subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN; + bool not_found = true; + for (int i = 0; i < 2; ++i) { + int ota_slot = (otadata[i].ota_seq - 1) % ota_app_count; + if (ota_slot == req_ota_slot && otadata[i].crc == bootloader_common_ota_select_crc(&otadata[i])) { + *ota_state = otadata[i].ota_state; + not_found = false; + break; + } + } + + if (not_found) { + return ESP_ERR_NOT_FOUND; + } + + return ESP_OK; +} + +esp_err_t esp_ota_erase_last_boot_app_partition(void) +{ + esp_ota_select_entry_t otadata[2]; + const esp_partition_t* ota_data_partition = read_otadata(otadata); + if (ota_data_partition == NULL) { + return ESP_FAIL; + } + + int active_otadata = bootloader_common_get_active_otadata(otadata); + int ota_app_count = get_ota_partition_count(); + if (active_otadata == -1 || ota_app_count == 0) { + return ESP_FAIL; + } + + int inactive_otadata = (~active_otadata)&1; + if (otadata[inactive_otadata].ota_seq == UINT32_MAX || otadata[inactive_otadata].crc != bootloader_common_ota_select_crc(&otadata[inactive_otadata])) { + return ESP_FAIL; + } + + int ota_slot = (otadata[inactive_otadata].ota_seq - 1) % ota_app_count; // Actual OTA partition selection + ESP_LOGD(TAG, "finding last_boot_app_partition ota_%d app...", ESP_PARTITION_SUBTYPE_APP_OTA_MIN + ota_slot); + + const esp_partition_t* last_boot_app_partition_from_otadata = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_MIN + ota_slot, NULL); + if (last_boot_app_partition_from_otadata == NULL) { + return ESP_FAIL; + } + + const esp_partition_t* running_partition = esp_ota_get_running_partition(); + if (running_partition == NULL || last_boot_app_partition_from_otadata == running_partition) { + return ESP_FAIL; + } + + esp_err_t err = esp_partition_erase_range(last_boot_app_partition_from_otadata, 0, last_boot_app_partition_from_otadata->size); + if (err != ESP_OK) { + return err; + } + + int sec_id = inactive_otadata; + err = esp_partition_erase_range(ota_data_partition, sec_id * SPI_FLASH_SEC_SIZE, SPI_FLASH_SEC_SIZE); + if (err != ESP_OK) { + return err; + } + + return ESP_OK; +} diff --git a/components/app_update/include/esp_ota_ops.h b/components/app_update/include/esp_ota_ops.h index 3a2355d9e4..ff07cec5a0 100644 --- a/components/app_update/include/esp_ota_ops.h +++ b/components/app_update/include/esp_ota_ops.h @@ -21,6 +21,7 @@ #include "esp_err.h" #include "esp_partition.h" #include "esp_image_format.h" +#include "esp_flash_data_types.h" #ifdef __cplusplus extern "C" @@ -195,6 +196,52 @@ const esp_partition_t* esp_ota_get_next_update_partition(const esp_partition_t * */ esp_err_t esp_ota_get_partition_description(const esp_partition_t *partition, esp_app_desc_t *app_desc); +/** + * @brief This function is called to indicate that the running app is working well. + * + * @return + * - ESP_OK: if successful. + */ +esp_err_t esp_ota_mark_app_valid_cancel_rollback(); + +/** + * @brief This function is called to roll back to the previously workable app with reboot. + * + * If rollback is successful then device will reset else API will return with error code. + * @return + * - ESP_FAIL: if not successful. + */ +esp_err_t esp_ota_mark_app_invalid_rollback_and_reboot(); + +/** + * @brief Returns last partition with invalid state (ESP_OTA_IMG_INVALID or ESP_OTA_IMG_ABORTED). + * + * @return partition. + */ +const esp_partition_t* esp_ota_get_last_invalid_partition(); + +/** + * @brief Returns state for given partition. + * + * @param[in] partition Pointer to partition. + * @param[out] ota_state state of partition (if this partition has a record in otadata). + * @return + * - ESP_OK: Successful. + * - ESP_ERR_INVALID_ARG: partition or ota_state arguments were NULL. + * - ESP_ERR_NOT_SUPPORTED: partition is not ota. + * - ESP_ERR_NOT_FOUND: Partition table does not have otadata or state was not found for given partition. + */ +esp_err_t esp_ota_get_state_partition(const esp_partition_t *partition, esp_ota_img_states_t *ota_state); + +/** + * @brief Erase previous boot app partition and corresponding otadata select for this partition. + * + * When current app is marked to as valid then you can erase previous app partition. + * @return + * - ESP_OK: Successful, otherwise ESP_ERR. + */ +esp_err_t esp_ota_erase_last_boot_app_partition(void); + #ifdef __cplusplus } #endif diff --git a/components/app_update/test/test_switch_ota.c b/components/app_update/test/test_switch_ota.c index 28248ced08..a3cb113c6a 100644 --- a/components/app_update/test/test_switch_ota.c +++ b/components/app_update/test/test_switch_ota.c @@ -476,3 +476,244 @@ static void test_flow5(void) // 4 Stage: run factory -> check it -> erase OTA_DATA for next tests -> PASS TEST_CASE_MULTIPLE_STAGES("Switching between factory, test, factory", "[app_update][reset=DEEPSLEEP_RESET, DEEPSLEEP_RESET, DEEPSLEEP_RESET]", start_test, test_flow5, test_flow5, test_flow5); #endif + +static const esp_partition_t* app_update(void) +{ + const esp_partition_t *cur_app = get_running_firmware(); + const esp_partition_t* update_partition = esp_ota_get_next_update_partition(NULL); + TEST_ASSERT_NOT_NULL(update_partition); + esp_ota_handle_t update_handle = 0; + TEST_ESP_OK(esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle)); + copy_app_partition(update_handle, cur_app); + TEST_ESP_OK(esp_ota_end(update_handle)); + TEST_ESP_OK(esp_ota_set_boot_partition(update_partition)); + return update_partition; +} + + +static void test_rollback1(void) +{ + boot_count++; + ESP_LOGI(TAG, "boot count %d", boot_count); + const esp_partition_t *cur_app = get_running_firmware(); + esp_ota_img_states_t ota_state = 0x5555AAAA; + const esp_partition_t* update_partition = NULL; + switch (boot_count) { + case 2: + ESP_LOGI(TAG, "Factory"); + TEST_ASSERT_EQUAL(ESP_PARTITION_SUBTYPE_APP_FACTORY, cur_app->subtype); + TEST_ASSERT_NULL(esp_ota_get_last_invalid_partition()); + TEST_ESP_ERR(ESP_ERR_NOT_SUPPORTED, esp_ota_get_state_partition(cur_app, &ota_state)); + update_partition = app_update(); + TEST_ESP_OK(esp_ota_get_state_partition(update_partition, &ota_state)); +#ifndef CONFIG_APP_ROLLBACK_ENABLE + TEST_ASSERT_EQUAL(ESP_OTA_IMG_UNDEFINED, ota_state); +#else + TEST_ASSERT_EQUAL(ESP_OTA_IMG_NEW, ota_state); +#endif + reboot_as_deep_sleep(); + break; + case 3: + ESP_LOGI(TAG, "OTA0"); + TEST_ASSERT_EQUAL(ESP_PARTITION_SUBTYPE_APP_OTA_0, cur_app->subtype); + TEST_ASSERT_NULL(esp_ota_get_last_invalid_partition()); + TEST_ESP_OK(esp_ota_get_state_partition(cur_app, &ota_state)); +#ifndef CONFIG_APP_ROLLBACK_ENABLE + TEST_ASSERT_EQUAL(ESP_OTA_IMG_UNDEFINED, ota_state); +#else + TEST_ASSERT_EQUAL(ESP_OTA_IMG_PENDING_VERIFY, ota_state); +#endif + esp_ota_mark_app_valid_cancel_rollback(); + TEST_ESP_OK(esp_ota_get_state_partition(cur_app, &ota_state)); + TEST_ASSERT_EQUAL(ESP_OTA_IMG_VALID, ota_state); + reboot_as_deep_sleep(); + break; + case 4: + ESP_LOGI(TAG, "OTA0"); + TEST_ASSERT_EQUAL(ESP_PARTITION_SUBTYPE_APP_OTA_0, cur_app->subtype); + TEST_ESP_OK(esp_ota_get_state_partition(cur_app, &ota_state)); + TEST_ASSERT_EQUAL(ESP_OTA_IMG_VALID, ota_state); + TEST_ESP_OK(esp_ota_mark_app_invalid_rollback_and_reboot()); + break; + default: + erase_ota_data(); + TEST_FAIL_MESSAGE("Unexpected stage"); + break; + } +} + +static void test_rollback1_1(void) +{ + boot_count = 5; + esp_ota_img_states_t ota_state = 0x5555AAAA; + ESP_LOGI(TAG, "boot count %d", boot_count); + const esp_partition_t *cur_app = get_running_firmware(); + ESP_LOGI(TAG, "Factory"); + TEST_ASSERT_EQUAL(ESP_PARTITION_SUBTYPE_APP_FACTORY, cur_app->subtype); + + const esp_partition_t *invalid_partition = esp_ota_get_last_invalid_partition(); + const esp_partition_t* next_update_partition = esp_ota_get_next_update_partition(NULL); + TEST_ASSERT_NOT_NULL(invalid_partition); + TEST_ASSERT_NOT_NULL(next_update_partition); + TEST_ASSERT_EQUAL_PTR(invalid_partition, next_update_partition); + TEST_ESP_ERR(ESP_ERR_NOT_SUPPORTED, esp_ota_get_state_partition(cur_app, &ota_state)); + TEST_ESP_OK(esp_ota_get_state_partition(invalid_partition, &ota_state)); + TEST_ASSERT_EQUAL(ESP_OTA_IMG_INVALID, ota_state); + + erase_ota_data(); +} + +// 1 Stage: After POWER_RESET erase OTA_DATA for this test -> reboot through deep sleep. +// 2 Stage: run factory -> check it -> copy factory to next app slot -> reboot --//-- +// 3 Stage: run OTA0 -> check it -> esp_ota_mark_app_valid_cancel_rollback() -> reboot --//-- +// 4 Stage: run OTA0 -> check it -> esp_ota_mark_app_invalid_rollback_and_reboot() -> reboot +// 5 Stage: run factory -> check it -> erase OTA_DATA for next tests -> PASS +TEST_CASE_MULTIPLE_STAGES("Test rollback. factory, OTA0, OTA0, rollback -> factory", "[app_update][reset=DEEPSLEEP_RESET, DEEPSLEEP_RESET, DEEPSLEEP_RESET, SW_CPU_RESET]", start_test, test_rollback1, test_rollback1, test_rollback1, test_rollback1_1); + +static void test_rollback2(void) +{ + boot_count++; + ESP_LOGI(TAG, "boot count %d", boot_count); + const esp_partition_t *cur_app = get_running_firmware(); + esp_ota_img_states_t ota_state = 0x5555AAAA; + const esp_partition_t* update_partition = NULL; + switch (boot_count) { + case 2: + ESP_LOGI(TAG, "Factory"); + TEST_ASSERT_EQUAL(ESP_PARTITION_SUBTYPE_APP_FACTORY, cur_app->subtype); + TEST_ASSERT_NULL(esp_ota_get_last_invalid_partition()); + TEST_ESP_ERR(ESP_ERR_NOT_SUPPORTED, esp_ota_get_state_partition(cur_app, &ota_state)); + update_partition = app_update(); + TEST_ESP_OK(esp_ota_get_state_partition(update_partition, &ota_state)); +#ifndef CONFIG_APP_ROLLBACK_ENABLE + TEST_ASSERT_EQUAL(ESP_OTA_IMG_UNDEFINED, ota_state); +#else + TEST_ASSERT_EQUAL(ESP_OTA_IMG_NEW, ota_state); +#endif + reboot_as_deep_sleep(); + break; + case 3: + ESP_LOGI(TAG, "OTA0"); + TEST_ASSERT_EQUAL(ESP_PARTITION_SUBTYPE_APP_OTA_0, cur_app->subtype); + TEST_ASSERT_NULL(esp_ota_get_last_invalid_partition()); + TEST_ESP_OK(esp_ota_get_state_partition(cur_app, &ota_state)); +#ifndef CONFIG_APP_ROLLBACK_ENABLE + TEST_ASSERT_EQUAL(ESP_OTA_IMG_UNDEFINED, ota_state); +#else + TEST_ASSERT_EQUAL(ESP_OTA_IMG_PENDING_VERIFY, ota_state); +#endif + esp_ota_mark_app_valid_cancel_rollback(); + TEST_ASSERT_NULL(esp_ota_get_last_invalid_partition()); + TEST_ESP_OK(esp_ota_get_state_partition(cur_app, &ota_state)); + TEST_ASSERT_EQUAL(ESP_OTA_IMG_VALID, ota_state); + update_partition = app_update(); + TEST_ESP_OK(esp_ota_get_state_partition(update_partition, &ota_state)); +#ifndef CONFIG_APP_ROLLBACK_ENABLE + TEST_ASSERT_EQUAL(ESP_OTA_IMG_UNDEFINED, ota_state); +#else + TEST_ASSERT_EQUAL(ESP_OTA_IMG_NEW, ota_state); +#endif + reboot_as_deep_sleep(); + break; + case 4: + ESP_LOGI(TAG, "OTA1"); + TEST_ASSERT_EQUAL(ESP_PARTITION_SUBTYPE_APP_OTA_1, cur_app->subtype); + TEST_ASSERT_NULL(esp_ota_get_last_invalid_partition()); + TEST_ESP_OK(esp_ota_get_state_partition(cur_app, &ota_state)); +#ifndef CONFIG_APP_ROLLBACK_ENABLE + TEST_ASSERT_EQUAL(ESP_OTA_IMG_UNDEFINED, ota_state); + TEST_ESP_OK(esp_ota_mark_app_invalid_rollback_and_reboot()); +#else + TEST_ASSERT_EQUAL(ESP_OTA_IMG_PENDING_VERIFY, ota_state); + reboot_as_deep_sleep(); +#endif + break; + default: + erase_ota_data(); + TEST_FAIL_MESSAGE("Unexpected stage"); + break; + } +} + +static void test_rollback2_1(void) +{ + boot_count = 5; + esp_ota_img_states_t ota_state = 0x5555AAAA; + ESP_LOGI(TAG, "boot count %d", boot_count); + const esp_partition_t *cur_app = get_running_firmware(); + ESP_LOGI(TAG, "OTA0"); + TEST_ASSERT_EQUAL(ESP_PARTITION_SUBTYPE_APP_OTA_0, cur_app->subtype); + + const esp_partition_t *invalid_partition = esp_ota_get_last_invalid_partition(); + TEST_ASSERT_EQUAL(ESP_PARTITION_SUBTYPE_APP_OTA_1, invalid_partition->subtype); + const esp_partition_t* next_update_partition = esp_ota_get_next_update_partition(NULL); + TEST_ASSERT_NOT_NULL(invalid_partition); + TEST_ASSERT_NOT_NULL(next_update_partition); + TEST_ASSERT_EQUAL_PTR(invalid_partition, next_update_partition); + TEST_ESP_OK(esp_ota_get_state_partition(cur_app, &ota_state)); + TEST_ASSERT_EQUAL(ESP_OTA_IMG_VALID, ota_state); + TEST_ESP_OK(esp_ota_get_state_partition(invalid_partition, &ota_state)); +#ifndef CONFIG_APP_ROLLBACK_ENABLE + TEST_ASSERT_EQUAL(ESP_OTA_IMG_INVALID, ota_state); +#else + TEST_ASSERT_EQUAL(ESP_OTA_IMG_ABORTED, ota_state); +#endif + erase_ota_data(); +} + +// 1 Stage: After POWER_RESET erase OTA_DATA for this test -> reboot through deep sleep. +// 2 Stage: run factory -> check it -> copy factory to next app slot -> reboot --//-- +// 3 Stage: run OTA0 -> check it -> esp_ota_mark_app_valid_cancel_rollback(), copy to next app slot -> reboot --//-- +// 4 Stage: run OTA1 -> check it -> PENDING_VERIFY/esp_ota_mark_app_invalid_rollback_and_reboot() -> reboot +// 5 Stage: run OTA0(rollback) -> check it -> erase OTA_DATA for next tests -> PASS +TEST_CASE_MULTIPLE_STAGES("Test rollback. factory, OTA0, OTA1, rollback -> OTA0", "[app_update][reset=DEEPSLEEP_RESET, DEEPSLEEP_RESET, DEEPSLEEP_RESET, SW_CPU_RESET]", start_test, test_rollback2, test_rollback2, test_rollback2, test_rollback2_1); + +static void test_erase_last_app_flow(void) +{ + boot_count++; + ESP_LOGI(TAG, "boot count %d", boot_count); + const esp_partition_t *cur_app = get_running_firmware(); + switch (boot_count) { + case 2: + ESP_LOGI(TAG, "Factory"); + TEST_ASSERT_EQUAL(ESP_PARTITION_SUBTYPE_APP_FACTORY, cur_app->subtype); + app_update(); + reboot_as_deep_sleep(); + break; + case 3: + ESP_LOGI(TAG, "OTA0"); + TEST_ASSERT_EQUAL(ESP_PARTITION_SUBTYPE_APP_OTA_0, cur_app->subtype); + app_update(); + reboot_as_deep_sleep(); + break; + case 4: + ESP_LOGI(TAG, "OTA1"); + TEST_ASSERT_EQUAL(ESP_PARTITION_SUBTYPE_APP_OTA_1, cur_app->subtype); + TEST_ESP_OK(esp_ota_erase_last_boot_app_partition()); + TEST_ESP_OK(esp_ota_mark_app_invalid_rollback_and_reboot()); + reboot_as_deep_sleep(); + break; + default: + erase_ota_data(); + TEST_FAIL_MESSAGE("Unexpected stage"); + break; + } +} + +static void test_erase_last_app_rollback(void) +{ + boot_count = 5; + ESP_LOGI(TAG, "boot count %d", boot_count); + const esp_partition_t *cur_app = get_running_firmware(); + ESP_LOGI(TAG, "erase_last_app"); + TEST_ASSERT_EQUAL(ESP_PARTITION_SUBTYPE_APP_FACTORY, cur_app->subtype); + TEST_ESP_ERR(ESP_FAIL, esp_ota_erase_last_boot_app_partition()); + erase_ota_data(); +} + +// 1 Stage: After POWER_RESET erase OTA_DATA for this test -> reboot through deep sleep. +// 2 Stage: run factory -> check it -> copy factory to OTA0 -> reboot --//-- +// 3 Stage: run OTA0 -> check it -> copy factory to OTA1 -> reboot --//-- +// 4 Stage: run OTA1 -> check it -> erase OTA0 and rollback -> reboot +// 5 Stage: run factory -> check it -> erase OTA_DATA for next tests -> PASS +TEST_CASE_MULTIPLE_STAGES("Test erase_last_boot_app_partition. factory, OTA1, OTA0, factory", "[app_update][reset=DEEPSLEEP_RESET, DEEPSLEEP_RESET, DEEPSLEEP_RESET, SW_CPU_RESET]", start_test, test_erase_last_app_flow, test_erase_last_app_flow, test_erase_last_app_flow, test_erase_last_app_rollback); diff --git a/components/bootloader/Kconfig.projbuild b/components/bootloader/Kconfig.projbuild index 3ecbed93b9..a982461e23 100644 --- a/components/bootloader/Kconfig.projbuild +++ b/components/bootloader/Kconfig.projbuild @@ -159,6 +159,18 @@ config BOOTLOADER_WDT_TIME_MS - these options can increase the execution time. Note: RTC_WDT will reset while encryption operations will be performed. +config APP_ROLLBACK_ENABLE + bool "Enable app rollback support" + default n + help + After updating the app, the bootloader runs a new app with the "ESP_OTA_IMG_PENDING_VERIFY" state set. + This state prevents the re-run of this app. + After the first boot of the new app in the user code, the function should be called to confirm + the operability of the app or vice versa about its non-operability. If the app is working, + then it is marked as valid. Otherwise, it is marked as not valid and rolls back to the previous working app. + A reboot is performed, and the app is booted before the software update. + Note: If during the first boot a new app the power goes out or the WDT works, then roll back will happen. + endmenu # Bootloader diff --git a/components/bootloader_support/src/bootloader_common.c b/components/bootloader_support/src/bootloader_common.c index 75b4a31b9b..7486164164 100644 --- a/components/bootloader_support/src/bootloader_common.c +++ b/components/bootloader_support/src/bootloader_common.c @@ -42,7 +42,7 @@ uint32_t bootloader_common_ota_select_crc(const esp_ota_select_entry_t *s) bool bootloader_common_ota_select_invalid(const esp_ota_select_entry_t *s) { - return s->ota_seq == UINT32_MAX; + return s->ota_seq == UINT32_MAX || s->ota_state == ESP_OTA_IMG_INVALID || s->ota_state == ESP_OTA_IMG_ABORTED; } bool bootloader_common_ota_select_valid(const esp_ota_select_entry_t *s) diff --git a/components/bootloader_support/src/bootloader_utility.c b/components/bootloader_support/src/bootloader_utility.c index 06b01f8a46..61099be41c 100644 --- a/components/bootloader_support/src/bootloader_utility.c +++ b/components/bootloader_support/src/bootloader_utility.c @@ -57,6 +57,8 @@ static const char* TAG = "boot"; /* Reduce literal size for some generic string literals */ #define MAP_ERR_MSG "Image contains multiple %s segments. Only the last one will be mapped." +static bool ota_has_initial_contents; + static void load_image(const esp_image_metadata_t* image_data); static void unpack_load_app(const esp_image_metadata_t *data); static void set_cache_and_start_app(uint32_t drom_addr, @@ -220,6 +222,18 @@ static void log_invalid_app_partition(int index) } } +static esp_err_t write_otadata(esp_ota_select_entry_t *otadata, uint32_t offset, bool write_encrypted) +{ + esp_err_t err = bootloader_flash_erase_sector(offset / FLASH_SECTOR_SIZE); + if (err == ESP_OK) { + err = bootloader_flash_write(offset, otadata, sizeof(esp_ota_select_entry_t), write_encrypted); + } + if (err != ESP_OK) { + ESP_LOGE(TAG, "Error in write_otadata operation. err = 0x%x", err); + } + return err; +} + int bootloader_utility_get_selected_boot_partition(const bootloader_state_t *bs) { esp_ota_select_entry_t otadata[2]; @@ -232,10 +246,20 @@ int bootloader_utility_get_selected_boot_partition(const bootloader_state_t *bs) if (read_otadata(&bs->ota_info, otadata) != ESP_OK) { return INVALID_INDEX; } + ota_has_initial_contents = false; ESP_LOGD(TAG, "otadata[0]: sequence values 0x%08x", otadata[0].ota_seq); ESP_LOGD(TAG, "otadata[1]: sequence values 0x%08x", otadata[1].ota_seq); - +#ifdef CONFIG_APP_ROLLBACK_ENABLE + bool write_encrypted = esp_flash_encryption_enabled(); + for (int i = 0; i < 2; ++i) { + if (otadata[i].ota_state == ESP_OTA_IMG_PENDING_VERIFY) { + ESP_LOGD(TAG, "otadata[%d] is marking as ABORTED", i); + otadata[i].ota_state = ESP_OTA_IMG_ABORTED; + write_otadata(&otadata[i], bs->ota_info.offset + FLASH_SECTOR_SIZE * i, write_encrypted); + } + } +#endif if ((bootloader_common_ota_select_invalid(&otadata[0]) && bootloader_common_ota_select_invalid(&otadata[1])) || bs->app_count == 0) { @@ -246,11 +270,25 @@ int bootloader_utility_get_selected_boot_partition(const bootloader_state_t *bs) } else { ESP_LOGI(TAG, "No factory image, trying OTA 0"); boot_index = 0; + // Try to boot from ota_0. + if ((otadata[0].ota_seq == UINT32_MAX || otadata[0].crc != bootloader_common_ota_select_crc(&otadata[0])) && + (otadata[1].ota_seq == UINT32_MAX || otadata[1].crc != bootloader_common_ota_select_crc(&otadata[1]))) { + // Factory is not found and both otadata are initial(0xFFFFFFFF) or incorrect crc. + // will set correct ota_seq. + ota_has_initial_contents = true; + } } } else { int active_otadata = bootloader_common_get_active_otadata(otadata); if (active_otadata != -1) { - ESP_LOGD(TAG, "Active OTADATA copy is #%d", active_otadata); + ESP_LOGD(TAG, "Active otadata[%d]", active_otadata); +#ifdef CONFIG_APP_ROLLBACK_ENABLE + if (otadata[active_otadata].ota_state == ESP_OTA_IMG_NEW) { + ESP_LOGD(TAG, "otadata[%d] is selected as new and marked PENDING_VERIFY state", active_otadata); + otadata[active_otadata].ota_state = ESP_OTA_IMG_PENDING_VERIFY; + write_otadata(&otadata[active_otadata], bs->ota_info.offset + FLASH_SECTOR_SIZE * active_otadata, write_encrypted); + } +#endif uint32_t ota_seq = otadata[active_otadata].ota_seq - 1; // Raw OTA sequence number. May be more than # of OTA slots boot_index = ota_seq % bs->app_count; // Actual OTA partition selection ESP_LOGD(TAG, "Mapping seq %d -> OTA slot %d", ota_seq, boot_index); @@ -284,6 +322,22 @@ static bool try_load_partition(const esp_partition_pos_t *partition, esp_image_m return false; } +// ota_has_initial_contents flag is set if factory does not present in partition table and +// otadata has initial content(0xFFFFFFFF), then set actual ota_seq. +static void set_actual_ota_seq(const bootloader_state_t *bs, int index) +{ + if (index >= 0 && ota_has_initial_contents == true) { + esp_ota_select_entry_t otadata; + otadata.ota_seq = index + 1; + otadata.ota_state = ESP_OTA_IMG_VALID; + otadata.crc = bootloader_common_ota_select_crc(&otadata); + + bool write_encrypted = esp_flash_encryption_enabled(); + write_otadata(&otadata, bs->ota_info.offset + FLASH_SECTOR_SIZE * 0, write_encrypted); + ESP_LOGD(TAG, "Set actual ota_seq=%d in otadata[0]", otadata.ota_seq); + } +} + #define TRY_LOG_FORMAT "Trying partition index %d offs 0x%x size 0x%x" void bootloader_utility_load_boot_image(const bootloader_state_t *bs, int start_index) @@ -309,6 +363,7 @@ void bootloader_utility_load_boot_image(const bootloader_state_t *bs, int start_ } ESP_LOGD(TAG, TRY_LOG_FORMAT, index, part.offset, part.size); if (try_load_partition(&part, &image_data)) { + set_actual_ota_seq(bs, index); load_image(&image_data); } log_invalid_app_partition(index); @@ -322,6 +377,7 @@ void bootloader_utility_load_boot_image(const bootloader_state_t *bs, int start_ } ESP_LOGD(TAG, TRY_LOG_FORMAT, index, part.offset, part.size); if (try_load_partition(&part, &image_data)) { + set_actual_ota_seq(bs, index); load_image(&image_data); } log_invalid_app_partition(index); diff --git a/components/esp32/include/esp_flash_data_types.h b/components/esp32/include/esp_flash_data_types.h index 9a26281b0a..d0f6ff32b8 100644 --- a/components/esp32/include/esp_flash_data_types.h +++ b/components/esp32/include/esp_flash_data_types.h @@ -24,11 +24,22 @@ extern "C" #define ESP_PARTITION_MAGIC 0x50AA #define ESP_PARTITION_MAGIC_MD5 0xEBEB +/// OTA_DATA states for checking operability of the app. +typedef enum { + ESP_OTA_IMG_NEW = 0x0U, /*!< Monitor the first boot. In bootloader this state is changed to ESP_OTA_IMG_PENDING_VERIFY. */ + ESP_OTA_IMG_PENDING_VERIFY = 0x1U, /*!< First boot for this app was. If while the second boot this state is then it will be changed to ABORTED. */ + ESP_OTA_IMG_VALID = 0x2U, /*!< App was confirmed as workable. App can boot and work without limits. */ + ESP_OTA_IMG_INVALID = 0x3U, /*!< App was confirmed as non-workable. This app will not selected to boot at all. */ + ESP_OTA_IMG_ABORTED = 0x4U, /*!< App could not confirm the workable or non-workable. In bootloader IMG_PENDING_VERIFY state will be changed to IMG_ABORTED. This app will not selected to boot at all. */ + ESP_OTA_IMG_UNDEFINED = 0xFFFFFFFFU, /*!< Undefined. App can boot and work without limits. */ +} esp_ota_img_states_t; + /* OTA selection structure (two copies in the OTA data partition.) Size of 32 bytes is friendly to flash encryption */ typedef struct { uint32_t ota_seq; - uint8_t seq_label[24]; + uint8_t seq_label[20]; + uint32_t ota_state; uint32_t crc; /* CRC32 of ota_seq field only */ } esp_ota_select_entry_t; diff --git a/docs/en/api-reference/system/ota.rst b/docs/en/api-reference/system/ota.rst index db02807280..500b9512d2 100644 --- a/docs/en/api-reference/system/ota.rst +++ b/docs/en/api-reference/system/ota.rst @@ -32,6 +32,95 @@ The OTA data partition is two flash sectors (0x2000 bytes) in size, to prevent p while it is being written. Sectors are independently erased and written with matching data, and if they disagree a counter field is used to determine which sector was written more recently. +.. _app_rollback: + +App rollback +------------ + +The main purpose of the application rollback is to keep the device working after the update. This feature allows you to roll back to the previous working application in case a new application has critical errors. When the rollback process is enabled and an OTA update provides a new version of the app, one of three things can happen: + +* The application works fine, :cpp:func:`esp_ota_mark_app_valid_cancel_rollback` marks the running application with the state ``ESP_OTA_IMG_VALID``. There are no restrictions on booting this application. +* The application has critical errors and further work is not possible, a rollback to the previous application is required, :cpp:func:`esp_ota_mark_app_invalid_rollback_and_reboot` marks the running application with the state ``ESP_OTA_IMG_INVALID`` and reset. This application will not be selected by the bootloader for boot and will boot the previously working application. +* If the :ref:`CONFIG_APP_ROLLBACK_ENABLE` option is set, and occur a reset without calling either function then happend and is rolled back. + +Note: The state is not written to the binary image of the application it is written to the ``otadata`` partition. The partition contains a ``ota_seq`` counter which is a pointer to the slot (ota_0, ota_1, ...) from which the application will be selected for boot. + +App OTA State +^^^^^^^^^^^^^ + +States control the process of selecting a boot app: + ++-----------------------------+--------------------------------------------------------+ +| States | Restriction of selecting a boot app in bootloader | ++=============================+========================================================+ +| ESP_OTA_IMG_VALID | None restriction. Will be selected. | ++-----------------------------+--------------------------------------------------------+ +| ESP_OTA_IMG_UNDEFINED | None restriction. Will be selected. | ++-----------------------------+--------------------------------------------------------+ +| ESP_OTA_IMG_INVALID | Will not be selected. | ++-----------------------------+--------------------------------------------------------+ +| ESP_OTA_IMG_ABORTED | Will not be selected. | ++-----------------------------+--------------------------------------------------------+ +| ESP_OTA_IMG_NEW | If :ref:`CONFIG_APP_ROLLBACK_ENABLE` option is set | +| | it will be selected only once. In bootloader the state | +| | immediately changes to ``ESP_OTA_IMG_PENDING_VERIFY``. | ++-----------------------------+--------------------------------------------------------+ +| ESP_OTA_IMG_PENDING_VERIFY | If :ref:`CONFIG_APP_ROLLBACK_ENABLE` option is set | +| | it will not be selected and the state will change to | +| | ``ESP_OTA_IMG_ABORTED``. | ++-----------------------------+--------------------------------------------------------+ + +If :ref:`CONFIG_APP_ROLLBACK_ENABLE` option is not enabled (by default), then the use of the following functions :cpp:func:`esp_ota_mark_app_valid_cancel_rollback` and :cpp:func:`esp_ota_mark_app_invalid_rollback_and_reboot` are optional, and ``ESP_OTA_IMG_NEW`` and ``ESP_OTA_IMG_PENDING_VERIFY`` states are not used. + +An option in Kconfig :ref:`CONFIG_APP_ROLLBACK_ENABLE` allows you to track the first boot of a new application. In this case, the application must confirm its operability by calling :cpp:func:`esp_ota_mark_app_valid_cancel_rollback` function, otherwise the application will be rolled back upon reboot. It allows you to control the operability of the application during the boot phase. Thus, a new application has only one attempt to boot successfully. + +Rollback Process +^^^^^^^^^^^^^^^^ + +The description of the rollback process when :ref:`CONFIG_APP_ROLLBACK_ENABLE` option is enabled: + +* The new application successfully downloaded and :cpp:func:`esp_ota_set_boot_partition` function makes this partition bootable and sets the state ``ESP_OTA_IMG_NEW``. This state means that the application is new and should be monitored for its first boot. +* Reboot :cpp:func:`esp_restart`. +* The bootloader checks for the ``ESP_OTA_IMG_PENDING_VERIFY`` state if it is set, then it will be written to ``ESP_OTA_IMG_ABORTED``. +* The bootloader selects a new application to boot so that the state is not set as ``ESP_OTA_IMG_INVALID`` or ``ESP_OTA_IMG_ABORTED``. +* The bootloader checks the selected application for ``ESP_OTA_IMG_NEW`` state if it is set, then it will be written to ``ESP_OTA_IMG_PENDING_VERIFY``. This state means that the application requires confirmation of its operability, if this does not happen and a reboot occurs, this state will be overwritten to ``ESP_OTA_IMG_ABORTED`` (see above) and this application will no longer be able to start, i.e. there will be a rollback to the previous work application. +* A new application has started and should make a self-test. +* If the self-test has completed successfully, then you must call the function :cpp:func:`esp_ota_mark_app_valid_cancel_rollback` because the application is awaiting confirmation of operability (``ESP_OTA_IMG_PENDING_VERIFY`` state). +* If the self-test fails then call :cpp:func:`esp_ota_mark_app_invalid_rollback_and_reboot` function to roll back to the previous working application, while the invalid application is set ``ESP_OTA_IMG_INVALID`` state. +* If the application has not been confirmed, the state remains ``ESP_OTA_IMG_PENDING_VERIFY``, and the next boot it will be changed to ``ESP_OTA_IMG_ABORTED``. That will prevent re-boot of this application. There will be a rollback to the previous working application. + +Unexpected Reset +^^^^^^^^^^^^^^^^ + +If a power loss or an unexpected crash occurs at the time of the first boot of a new application, it will roll back the application. + +Recommendation: Perform the self-test procedure as quickly as possible, to prevent rollback due to power loss. + +Only ``OTA`` partitions can be rolled back. Factory partition is not rolled back. + +Booting invalid/aborted apps +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Booting an application which was previously set to ``ESP_OTA_IMG_INVALID`` or ``ESP_OTA_IMG_ABORTED`` is possible: + +* Get the last invalid application partition :cpp:func:`esp_ota_get_last_invalid_partition`. +* Pass the received partition to :cpp:func:`esp_ota_set_boot_partition`, this will update the ``otadata``. +* Restart :cpp:func:`esp_restart`. The bootloader will boot the specified application. + +To determine if self-tests should be run during startup of an application, call the :cpp:func:`esp_ota_get_state_partition` function. If result is ``ESP_OTA_IMG_PENDING_VERIFY`` then self-testing and subsequent confirmation of operability is required. + +Where the states are set +^^^^^^^^^^^^^^^^^^^^^^^^ + +A brief description of where the states are set: + +* ``ESP_OTA_IMG_VALID`` state is set by :cpp:func:`esp_ota_mark_app_valid_cancel_rollback` function. +* ``ESP_OTA_IMG_UNDEFINED`` state is set by :cpp:func:`esp_ota_set_boot_partition` function if :ref:`CONFIG_APP_ROLLBACK_ENABLE` option is not enabled. +* ``ESP_OTA_IMG_NEW`` state is set by :cpp:func:`esp_ota_set_boot_partition` function if :ref:`CONFIG_APP_ROLLBACK_ENABLE` option is enabled. +* ``ESP_OTA_IMG_INVALID`` state is set by :cpp:func:`esp_ota_mark_app_invalid_rollback_and_reboot` function. +* ``ESP_OTA_IMG_ABORTED`` state is set if there was no confirmation of the application operability and occurs reboots (if :ref:`CONFIG_APP_ROLLBACK_ENABLE` option is enabled). +* ``ESP_OTA_IMG_PENDING_VERIFY`` state is set in a bootloader if :ref:`CONFIG_APP_ROLLBACK_ENABLE` option is enabled and selected app has ``ESP_OTA_IMG_NEW`` state. + .. _secure-ota-updates: Secure OTA Updates Without Secure boot