From e3198edf6119c595cfd7ad373d030f3cffeeb3b4 Mon Sep 17 00:00:00 2001 From: Greeva Patel Date: Sat, 31 Oct 2026 16:15:51 +0530 Subject: [PATCH] fix(recovery): carry out a requested factory reset instead of looping eos_fw_factory_reset() sets EOS_FLAG_FACTORY_RESET and documents that the next boot "erases both slots and resets boot control to defaults". The flag only made stage-0 (ebldr_recovery_triggered) and stage-1 (eos_boot_policy_should_recover) enter recovery. eos_recovery_enter() cleared FORCE_RECOVERY but not FACTORY_RESET, and the only code that erased anything was the authenticated UART FACTORY command. So nothing was reset, and because the flag survived, every later boot (including after a recovery RESET) came straight back to recovery. eos_recovery_enter() now performs the requested reset on entry: erase Slot A and Slot B, return the control block to its defaults (which clears the flag), save, and log EOS_LOG_FACTORY_RESET. If either erase fails the flag stays set so the reset is retried on the next boot rather than silently dropped. With both slots empty the device then waits in recovery for a new image, which is the documented end state; once one is installed it boots normally. test_recovery: a FACTORY_RESET request erases both slots and leaves a default control block with the flag cleared; a plain FORCE_RECOVERY entry erases nothing. Co-Authored-By: Claude Opus 5.5 --- core/recovery.c | 16 +++++++++++ tests/unit/test_recovery.c | 54 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/core/recovery.c b/core/recovery.c index 1356b81..3082e51 100644 --- a/core/recovery.c +++ b/core/recovery.c @@ -511,6 +511,22 @@ int eos_recovery_enter(eos_bootctl_t *bctl) bctl->flags &= ~EOS_FLAG_FORCE_RECOVERY; eos_bootctl_save(bctl); + /* Carry out a factory reset the application requested with + * eos_fw_factory_reset(): erase both slots and return the boot control + * block to its defaults, which also clears the request. The flag used to + * route the boot here and nothing more -- no slot was erased, and since + * only FORCE_RECOVERY is cleared above, every later boot came back to + * recovery. If an erase fails the flag is left set, so the reset is + * retried on the next boot rather than reported as done. */ + if (bctl->flags & EOS_FLAG_FACTORY_RESET) { + if (eos_slot_erase(EOS_SLOT_A) == EOS_OK && + eos_slot_erase(EOS_SLOT_B) == EOS_OK) { + eos_bootctl_init_defaults(bctl); + if (eos_bootctl_save(bctl) == EOS_OK) + eos_boot_log_append(EOS_LOG_FACTORY_RESET, EOS_SLOT_NONE, 0); + } + } + /* Reset auth state on entry */ auth_state = RCVR_AUTH_NONE; auth_fail_count = 0; diff --git a/tests/unit/test_recovery.c b/tests/unit/test_recovery.c index cf86f75..e4ea5cf 100644 --- a/tests/unit/test_recovery.c +++ b/tests/unit/test_recovery.c @@ -609,6 +609,58 @@ TEST(test_info_reports_a_board_without_an_entropy_source) ASSERT((out_buf[21] & RCVR_CAP_OTP) != 0); /* OTP is still there */ } +/* eos_fw_factory_reset() sets EOS_FLAG_FACTORY_RESET and documents that the + * next boot "erases both slots and resets boot control to defaults". The + * flag routed the boot into recovery and nothing else: the slots were left + * as they were, and the flag was never cleared, so every boot after that + * came back to recovery. */ +TEST(test_requested_factory_reset_is_carried_out_on_entry) +{ + memset(&sim_flash[SIM_SLOT_A_ADDR], 0x5A, SIM_SLOT_A_SIZE); + memset(&sim_flash[SIM_SLOT_B_ADDR], 0x5A, SIM_SLOT_B_SIZE); + + eos_bootctl_t bctl; + eos_bootctl_init_defaults(&bctl); + bctl.active_slot = EOS_SLOT_B; + bctl.boot_count = 42; + ASSERT(eos_bootctl_request_factory_reset(&bctl) == EOS_OK); + + if (setjmp(exit_jmp) == 0) { + eos_recovery_enter(&bctl); + } + + for (uint32_t i = 0; i < SIM_SLOT_A_SIZE; i++) + ASSERT(sim_flash[SIM_SLOT_A_ADDR + i] == 0xFF); + for (uint32_t i = 0; i < SIM_SLOT_B_SIZE; i++) + ASSERT(sim_flash[SIM_SLOT_B_ADDR + i] == 0xFF); + + /* The persisted control block is back to defaults, request cleared. */ + eos_bootctl_t saved; + ASSERT(eos_bootctl_load(&saved) == EOS_OK); + ASSERT((saved.flags & EOS_FLAG_FACTORY_RESET) == 0); + ASSERT(saved.active_slot == EOS_SLOT_A); + ASSERT(saved.boot_count == 0); +} + +/* Only a factory-reset request erases anything: an ordinary entry into + * recovery leaves both slots alone. */ +TEST(test_plain_recovery_entry_erases_nothing) +{ + memset(&sim_flash[SIM_SLOT_A_ADDR], 0x5A, SIM_SLOT_A_SIZE); + memset(&sim_flash[SIM_SLOT_B_ADDR], 0x5A, SIM_SLOT_B_SIZE); + + eos_bootctl_t bctl; + eos_bootctl_init_defaults(&bctl); + ASSERT(eos_bootctl_request_recovery(&bctl) == EOS_OK); + + if (setjmp(exit_jmp) == 0) { + eos_recovery_enter(&bctl); + } + + ASSERT(sim_flash[SIM_SLOT_A_ADDR] == 0x5A); + ASSERT(sim_flash[SIM_SLOT_B_ADDR] == 0x5A); +} + int main(void) { printf("=== test_recovery ===\n"); @@ -621,6 +673,8 @@ int main(void) run_test_verify_rejects_oversized_image_before_reading_payload(); run_test_info_sends_the_packed_layout_the_client_parses_and_nothing_else(); run_test_info_reports_a_board_without_an_entropy_source(); + run_test_requested_factory_reset_is_carried_out_on_entry(); + run_test_plain_recovery_entry_erases_nothing(); printf("%d/%d tests passed\n", tests_passed, tests_run); return (tests_passed == tests_run) ? 0 : 1; }