bootloader/stage1: fix erase handling for partially erased blocks
What changed, and why it matters
This update fixes the BitBox02 bootloader's firmware-erase routine. Previously, when erasing leftover padding after a firmware update, the bootloader started erasing at the exact page where the firmware ended. Because flash memory can only be erased in larger fixed-size blocks, this could leave the final firmware chunk sharing an erase block with padding. That shared block might be erased unintentionally, potentially corrupting the newly written firmware. The fix aligns the erase start to a full erase-block boundary and re-checks whether pages are already erased before acting. The changelog explicitly calls this a fix for 'partially erased flash blocks.'
Treat this as a security-relevant bootloader fix and ensure devices are updated to bootloader v1.2.2. Review whether the previous behavior could lead to a bricked or partially corrupted firmware update under specific size/alignment conditions, and confirm the rewritten last chunk path was safe in prior releases.
Security signals we found
Bootloader firmware erase routine could erase a flash block containing both firmware and padding
Fix aligns erase start to erase-block boundary and re-checks erased state before erasing
Changelog describes the change as a fix for 'partially erased flash blocks'
Version bump indicates this is a released bootloader update
Evidence from the diff
In src/bootloader/bootloader.c, _api_firmware_erase() previously computed firmware_num_pages from the chunk count and began erasing padding at that page. Since FLASH_ERASE_PAGE_NUM is the minimum erase granularity, a firmware size not aligned to that boundary meant the last erase block contained both firmware data and padding. Erasing that block would wipe valid firmware bytes, which would later need to be rewritten—if the rewrite path handled it correctly. The patch introduces _flash_pages_erased() to verify by word that a block is all 0xFF, computes erase_start_page rounded down to the next lower FLASH_ERASE_PAGE_NUM boundary, and only erases padding blocks that are not already erased. It also bumps the bootloader version to v1.2.2.
Changed components
BitBox02 bootloader stage1src/bootloader/bootloader.cfirmware upgrade erase pathInspect captured patch +23 / −8
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cb76a8f..31b1b2d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -202,6 +202,9 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
## Bootloader
+### v1.2.2
+- Fix stage1 firmware erase handling for partially erased flash blocks
+
### v1.2.1
- Bugfix to allow full-sized firmware upgrades
diff --git a/src/bootloader/bootloader.c b/src/bootloader/bootloader.c
index c4df93f..ed2263a 100644
--- a/src/bootloader/bootloader.c
+++ b/src/bootloader/bootloader.c
@@ -433,9 +433,22 @@ static size_t _api_write_chunk(const uint8_t* buf, uint8_t chunknum, uint8_t* ou
return len;
}
+static bool _flash_pages_erased(uint32_t addr, uint32_t num_pages)
+{
+ const uint32_t* words = (const uint32_t*)addr;
+ const uint32_t num_words = num_pages * FLASH_PAGE_SIZE / sizeof(uint32_t);
+ for (uint32_t i = 0; i < num_words; i++) {
+ if (words[i] != UINT32_MAX) {
+ return false;
+ }
+ }
+ return true;
+}
+
/**
- * This function erases only the padding bytes, if not already erased. Other
- * bytes get erased and written when writing the firmware chunks.
+ * This function erases the padding bytes, if not already erased. Erasing starts
+ * at an erase-block boundary, so if the firmware ends halfway through a block,
+ * the last firmware chunk is erased here and written again later.
*
* The number of chunks is put into RAM in order to show the correct
* progress in the next step flashing the firmware.
@@ -455,19 +468,18 @@ static size_t _api_firmware_erase(uint8_t firmware_num_chunks, uint8_t* output)
return _report_status(OP_STATUS_ERR_UNLOCK, output);
}
}
- uint8_t empty_page[FLASH_PAGE_SIZE];
- memset(empty_page, 0xff, sizeof(empty_page));
uint16_t firmware_num_pages = firmware_num_chunks * FIRMWARE_CHUNK_LEN / FLASH_PAGE_SIZE;
- for (uint32_t i = firmware_num_pages; i < (uint32_t)FLASH_APP_PAGE_NUM;
+ uint16_t erase_start_page = firmware_num_pages - (firmware_num_pages % FLASH_ERASE_PAGE_NUM);
+ for (uint32_t i = erase_start_page; i < (uint32_t)FLASH_APP_PAGE_NUM;
i += FLASH_ERASE_PAGE_NUM) {
const uint32_t addr = FLASH_APP_START + i * FLASH_PAGE_SIZE;
- if (MEMEQ((const void*)addr, empty_page, sizeof(empty_page))) {
+ if (_flash_pages_erased(addr, FLASH_ERASE_PAGE_NUM)) {
continue;
}
if (flash_erase(&FLASH_0, addr, FLASH_ERASE_PAGE_NUM) != ERR_NONE) {
return _report_status(OP_STATUS_ERR_ERASE, output);
}
- if (!MEMEQ((const void*)addr, empty_page, sizeof(empty_page))) {
+ if (!_flash_pages_erased(addr, FLASH_ERASE_PAGE_NUM)) {
return _report_status(OP_STATUS_ERR_CHECK, output);
}
}
diff --git a/versions.json b/versions.json
index 4e95f07..3903999 100644
--- a/versions.json
+++ b/versions.json
@@ -1,5 +1,5 @@
{
"firmware": "v9.26.2",
- "bootloader": "v1.2.1",
+ "bootloader": "v1.2.2",
"stage0": 1
}
Why this scored 59/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.