ota: make finalization set the ota status
What changed, and why it matters
This commit refactors how firmware update finalization works in the Blockstream Jade hardware wallet. Previously, the final verification and boot-partition switch only ran if the update had already been marked successful. Now the finalization routine always runs and sets the success/failure status itself. The change is mostly a code-cleanup, but it removes a guard that could have silently skipped final checks when an earlier error was already recorded. There is no direct evidence in the commit that this fixes an exploitable vulnerability, but it hardens the OTA flow so that final verification and boot-partition selection are consistently applied.
Treat as a defensive hardening change. Review the full OTA state machine to confirm that ota_return_status cannot be set to OTA_SUCCESS before ota_finalize() runs, and that all error paths in ota_finalize() correctly leave the device in a safe non-booting state. Consider whether the removed outer 'if (OTA_SUCCESS)' guard could have masked any caller that expected finalization to be skipped; ensure no regression in error reporting to the host.
Security signals we found
Refactor of firmware update finalization logic
Removal of caller-side conditional that skipped final checks on prior error
Centralized status assignment in finalization routine
OTA hash verification and boot-partition selection remain present
No explicit security claim or CVE reference in commit
Evidence from the diff
The patch renames post_ota_check() to ota_finalize(), changes its return type from ota_status_t to void, and makes it mutate joctx->ota_return_status directly. Callers in ota.c and ota_delta.c no longer conditionally invoke it only when ota_return_status == OTA_SUCCESS; they always call ota_finalize(). The function still returns early if the status is not OTA_SUCCESS, but now it is responsible for updating the status on any failure (OTA_ERR_INIT, OTA_ERR_BADHASH, OTA_ERR_FINISH, OTA_ERR_SETPARTITION) and on success. This centralizes status management and ensures the final hash check, esp_ota_end(), and esp_ota_set_boot_partition() are attempted/reported uniformly. The diff does not show any new cryptographic checks or input validation; it is a control-flow refactor.
Changed components
main/process/ota.cmain/process/ota_delta.cmain/process/ota_util.cmain/process/ota_util.hBlockstream Jade OTA update processInspect captured patch +13 / −15
diff --git a/main/process/ota.c b/main/process/ota.c
index 98b340c..5672a7a 100644
--- a/main/process/ota.c
+++ b/main/process/ota.c
@@ -128,9 +128,7 @@ void ota_process(void* process_ptr)
// If all good with the upload do all final checks and then finalise the ota
// and set the new boot partition, etc.
- if (joctx->ota_return_status == OTA_SUCCESS) {
- joctx->ota_return_status = post_ota_check(joctx);
- }
+ ota_finalize(joctx);
// Send final message reply with final status
if (joctx->ota_return_status != OTA_SUCCESS) {
diff --git a/main/process/ota_delta.c b/main/process/ota_delta.c
index 2719727..a54a5ce 100644
--- a/main/process/ota_delta.c
+++ b/main/process/ota_delta.c
@@ -202,9 +202,7 @@ void ota_delta_process(void* process_ptr)
// If all good with the upload do all final checks and then finalise the ota
// and set the new boot partition, etc.
- if (joctx->ota_return_status == OTA_SUCCESS) {
- joctx->ota_return_status = post_ota_check(joctx);
- }
+ ota_finalize(joctx);
// Send final message reply with final status
if (joctx->ota_return_status != OTA_SUCCESS) {
diff --git a/main/process/ota_util.c b/main/process/ota_util.c
index e73a843..bb3b97e 100644
--- a/main/process/ota_util.c
+++ b/main/process/ota_util.c
@@ -246,13 +246,13 @@ cleanup:
return joctx;
}
-ota_status_t post_ota_check(jade_ota_ctx_t* joctx)
+void ota_finalize(jade_ota_ctx_t* joctx)
{
JADE_ASSERT(joctx);
// Ensure no cached error - if so return it now
if (joctx->ota_return_status != OTA_SUCCESS) {
- return joctx->ota_return_status;
+ return;
}
if (joctx->remaining_compressed || joctx->remaining_uncompressed || !joctx->compressedsize
@@ -260,7 +260,8 @@ ota_status_t post_ota_check(jade_ota_ctx_t* joctx)
JADE_LOGE("OTA checks failed: uncompressed size: %u, compressed size: %u, remaining compressed %u, remaining "
"uncompressed %u",
joctx->uncompressedsize, joctx->compressedsize, joctx->remaining_compressed, joctx->remaining_uncompressed);
- return OTA_ERR_INIT;
+ joctx->ota_return_status = OTA_ERR_INIT;
+ return;
}
// Verify calculated compressed file hash matches expected
@@ -277,7 +278,8 @@ ota_status_t post_ota_check(jade_ota_ctx_t* joctx)
JADE_LOGE("Firmware hash mismatch: expected: %s, got: %s", joctx->expected_hash_hexstr, calc_hash_hexstr);
JADE_WALLY_VERIFY(wally_free_string(calc_hash_hexstr));
- return OTA_ERR_BADHASH;
+ joctx->ota_return_status = OTA_ERR_BADHASH;
+ return;
}
// All good, finalise the ota and set the partition to boot
@@ -286,16 +288,16 @@ ota_status_t post_ota_check(jade_ota_ctx_t* joctx)
if (err != ESP_OK) {
JADE_LOGE("esp_ota_end() returned %d", err);
- return OTA_ERR_FINISH;
+ joctx->ota_return_status = OTA_ERR_FINISH;
+ return;
}
err = esp_ota_set_boot_partition(joctx->update_partition);
if (err != ESP_OK) {
JADE_LOGE("esp_ota_set_boot_partition() returned %d", err);
- return OTA_ERR_SETPARTITION;
+ joctx->ota_return_status = OTA_ERR_SETPARTITION;
+ return;
}
-
- return OTA_SUCCESS;
}
// NOTE: 'dest' is assumed to be at least as long as 'strlen(src)'
diff --git a/main/process/ota_util.h b/main/process/ota_util.h
index 7c7770e..2d2e2a4 100644
--- a/main/process/ota_util.h
+++ b/main/process/ota_util.h
@@ -77,8 +77,8 @@ typedef struct {
void handle_in_bin_data(void* ctx, uint8_t* data, size_t rawsize);
jade_ota_ctx_t* ota_init(jade_process_t* process, bool is_delta);
-ota_status_t post_ota_check(jade_ota_ctx_t* joctx);
void ota_user_validate(jade_ota_ctx_t* joctx, const uint8_t* uncompressed);
+void ota_finalize(jade_ota_ctx_t* joctx);
const char* ota_get_status_text(ota_status_t status);
#endif /* JADE_OTA_UTIL_H_ */
Why this scored 45/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.