From d31b4d6c2cfb56976cdeda55318bec885e7403fc Mon Sep 17 00:00:00 2001 From: Greeva Patel Date: Sat, 31 Oct 2026 16:23:41 +0530 Subject: [PATCH] fix(stm32f4): erase every sector in range; give each region its own sectors Two defects in the STM32F407 port, one result: erasing one region destroyed others, and erasing a slot left most of it unerased. 1. stm32f4_flash_erase() did (void)len and erased only the sector that held addr. A slot spans several sectors, so eos_fw_update_begin()'s slot erase cleared one sector and left stale image bytes in the rest, which the program step cannot overwrite (flash bits only go 1 -> 0). 2. The memory map put Slot B's last 64K, the recovery image, both boot control copies and the boot log in sector 11 (0x080E0000, 128K). The F407 erases whole sectors, so every eos_bootctl_save() -- which erases the primary and then the backup copy -- wiped the backup copy, the log, the recovery image and the end of Slot B as well. The erase now takes [first, last] from stm32f4_flash_sector_range() in board_stm32f4.h (RM0090 geometry: 4 x 16K, 1 x 64K, 7 x 128K) and erases each sector in turn; a zero length or a range that wraps or leaves the array is EOS_ERR_INVALID with nothing erased. The map is re-laid on sector boundaries. Stage-0 (s0) and stage-1 (s1-3) are unchanged, so stm32f4_stage0.ld / stm32f4_stage1.ld and the stage-1 vector offset still match. Six regions for the remaining eight sectors leave four 128K sectors for the A/B pair: s4 0x08010000 64K boot log s5-6 0x08020000 256K Slot A s7-8 0x08060000 256K Slot B s9 0x080A0000 128K recovery image s10 0x080C0000 128K boot control (primary) s11 0x080E0000 128K boot control (backup) This shrinks each slot from 448K to 256K and moves Slot A's base from 0x08010000 to 0x08020000, so images built for the old Slot A address must be relinked; the "device config / reserved" area is gone. Docs that state the map (architecture, memory-map, secure_boot_chain, threat_model, the imgpack examples in quickstart/installation_usage) and their copy in llms-full.txt are updated to match. test_stm32f4_flash_layout: new suite that checks the sector lookup against RM0090, that an erase range covers every sector it touches (slot A -> 5..6, slot B -> 7..8, spanning the 16K/64K/128K changes) and rejects empty/out-of-range requests, and that every region is sector-aligned with no sector shared by two regions. Co-Authored-By: Claude Opus 5.5 --- boards/stm32f4/board_stm32f4.c | 49 +++---- boards/stm32f4/board_stm32f4.h | 97 ++++++++++--- docs/architecture.md | 16 ++- docs/installation_usage.md | 2 +- docs/memory-map.md | 23 ++-- docs/quickstart.md | 2 +- docs/secure_boot_chain.md | 2 +- docs/threat_model.md | 4 +- llms-full.txt | 49 ++++--- tests/CMakeLists.txt | 11 ++ tests/unit/test_stm32f4_flash_layout.c | 184 +++++++++++++++++++++++++ 11 files changed, 348 insertions(+), 91 deletions(-) create mode 100644 tests/unit/test_stm32f4_flash_layout.c diff --git a/boards/stm32f4/board_stm32f4.c b/boards/stm32f4/board_stm32f4.c index 6e68af2..db59a92 100644 --- a/boards/stm32f4/board_stm32f4.c +++ b/boards/stm32f4/board_stm32f4.c @@ -124,41 +124,32 @@ static int stm32f4_flash_write(uint32_t addr, const void *buf, size_t len) static int stm32f4_flash_erase(uint32_t addr, size_t len) { - (void)len; - - /* Determine sector number from address */ - int sector = -1; - if (addr >= 0x08000000 && addr < 0x08004000) sector = 0; - else if (addr >= 0x08004000 && addr < 0x08008000) sector = 1; - else if (addr >= 0x08008000 && addr < 0x0800C000) sector = 2; - else if (addr >= 0x0800C000 && addr < 0x08010000) sector = 3; - else if (addr >= 0x08010000 && addr < 0x08020000) sector = 4; - else if (addr >= 0x08020000 && addr < 0x08040000) sector = 5; - else if (addr >= 0x08040000 && addr < 0x08060000) sector = 6; - else if (addr >= 0x08060000 && addr < 0x08080000) sector = 7; - else if (addr >= 0x08080000 && addr < 0x080A0000) sector = 8; - else if (addr >= 0x080A0000 && addr < 0x080C0000) sector = 9; - else if (addr >= 0x080C0000 && addr < 0x080E0000) sector = 10; - else if (addr >= 0x080E0000 && addr < 0x08100000) sector = 11; - - if (sector < 0) + /* Erase every sector [addr, addr + len) touches. Only the sector + * holding addr used to be erased and len was ignored, so erasing a + * 256K slot cleared its first sector and left stale image bytes in + * the rest -- which the following program step cannot overwrite. */ + int first, last; + if (stm32f4_flash_sector_range(addr, len, &first, &last) != 0) return EOS_ERR_INVALID; flash_unlock(); - flash_wait_busy(); - FLASH_CR &= ~(0xF << 3); /* Clear SNB bits */ - FLASH_CR |= (sector << 3); /* Set sector number */ - FLASH_CR |= (1 << 1); /* SER — sector erase */ - FLASH_CR |= (1 << 16); /* STRT — start erase */ + for (int sector = first; sector <= last; sector++) { + flash_wait_busy(); + + FLASH_CR &= ~(0xF << 3); /* Clear SNB bits */ + FLASH_CR |= (sector << 3); /* Set sector number */ + FLASH_CR |= (1 << 1); /* SER — sector erase */ + FLASH_CR |= (1 << 16); /* STRT — start erase */ - flash_wait_busy(); + flash_wait_busy(); - if (FLASH_SR & 0xF0) { - FLASH_SR = 0xF0; - FLASH_CR &= ~(1 << 1); - flash_lock(); - return EOS_ERR_FLASH; + if (FLASH_SR & 0xF0) { + FLASH_SR = 0xF0; + FLASH_CR &= ~(1 << 1); + flash_lock(); + return EOS_ERR_FLASH; + } } FLASH_CR &= ~(1 << 1); /* Clear SER */ diff --git a/boards/stm32f4/board_stm32f4.h b/boards/stm32f4/board_stm32f4.h index 23e1e5e..525a04b 100644 --- a/boards/stm32f4/board_stm32f4.h +++ b/boards/stm32f4/board_stm32f4.h @@ -15,15 +15,25 @@ /* * STM32F407 Flash Memory Map (1MB total) * - * 0x08000000 - 0x08003FFF (16K) Stage-0 (eBootloader) - * 0x08004000 - 0x0800FFFF (48K) Stage-1 (E-Boot) - * 0x08010000 - 0x0807FFFF (448K) Slot A (application firmware) - * 0x08080000 - 0x080EFFFF (448K) Slot B (candidate firmware) - * 0x080F0000 - 0x080F3FFF (16K) Recovery image (golden) - * 0x080F4000 - 0x080F5FFF (8K) Boot control (primary) - * 0x080F6000 - 0x080F7FFF (8K) Boot control (backup) - * 0x080F8000 - 0x080F9FFF (8K) Boot log - * 0x080FA000 - 0x080FFFFF (24K) Device config / reserved + * The F407 erases whole sectors only, and they are not uniform: + * sectors 0-3 are 16K, sector 4 is 64K, sectors 5-11 are 128K. Every + * region therefore owns whole sectors -- erasing one region (a slot + * update, a boot control save, a log clear) must never erase another. + * Slot B, the recovery image, both boot control copies and the boot log + * used to share sector 11, so saving the boot control block wiped the + * other four. + * + * Sector Address range Size Region + * 0 0x08000000 - 0x08003FFF 16K Stage-0 (eBootloader) + * 1-3 0x08004000 - 0x0800FFFF 48K Stage-1 (E-Boot) + * 4 0x08010000 - 0x0801FFFF 64K Boot log + * 5-6 0x08020000 - 0x0805FFFF 256K Slot A (application firmware) + * 7-8 0x08060000 - 0x0809FFFF 256K Slot B (candidate firmware) + * 9 0x080A0000 - 0x080BFFFF 128K Recovery image (golden) + * 10 0x080C0000 - 0x080DFFFF 128K Boot control (primary) + * 11 0x080E0000 - 0x080FFFFF 128K Boot control (backup) + * + * Stage-0 and stage-1 match stm32f4_stage0.ld and stm32f4_stage1.ld. */ #define STM32F4_FLASH_BASE 0x08000000 @@ -35,22 +45,73 @@ #define STM32F4_STAGE1_ADDR 0x08004000 #define STM32F4_STAGE1_SIZE (48 * 1024) -#define STM32F4_SLOT_A_ADDR 0x08010000 -#define STM32F4_SLOT_A_SIZE (448 * 1024) +#define STM32F4_LOG_ADDR 0x08010000 +#define STM32F4_LOG_SIZE (64 * 1024) -#define STM32F4_SLOT_B_ADDR 0x08080000 -#define STM32F4_SLOT_B_SIZE (448 * 1024) +#define STM32F4_SLOT_A_ADDR 0x08020000 +#define STM32F4_SLOT_A_SIZE (256 * 1024) -#define STM32F4_RECOVERY_ADDR 0x080F0000 -#define STM32F4_RECOVERY_SIZE (16 * 1024) +#define STM32F4_SLOT_B_ADDR 0x08060000 +#define STM32F4_SLOT_B_SIZE (256 * 1024) -#define STM32F4_BOOTCTL_ADDR 0x080F4000 -#define STM32F4_BOOTCTL_BACKUP_ADDR 0x080F6000 +#define STM32F4_RECOVERY_ADDR 0x080A0000 +#define STM32F4_RECOVERY_SIZE (128 * 1024) -#define STM32F4_LOG_ADDR 0x080F8000 +#define STM32F4_BOOTCTL_ADDR 0x080C0000 +#define STM32F4_BOOTCTL_SIZE (128 * 1024) + +#define STM32F4_BOOTCTL_BACKUP_ADDR 0x080E0000 +#define STM32F4_BOOTCTL_BACKUP_SIZE (128 * 1024) #define STM32F4_VECTOR_OFFSET (STM32F4_STAGE1_ADDR - STM32F4_FLASH_BASE) +/* ---- Flash sector geometry ---- */ + +#define STM32F4_FLASH_SECTORS 12 + +/** + * @brief Sector number holding @p addr, or -1 outside the 1MB array. + */ +static inline int stm32f4_flash_sector_of(uint32_t addr) +{ + if (addr < STM32F4_FLASH_BASE) + return -1; + uint32_t off = addr - STM32F4_FLASH_BASE; + if (off < 0x10000) /* sectors 0-3: 16K each */ + return (int)(off / 0x4000); + if (off < 0x20000) /* sector 4: 64K */ + return 4; + if (off < STM32F4_FLASH_SIZE) /* sectors 5-11: 128K each */ + return 4 + (int)(off / 0x20000); + return -1; +} + +/** + * @brief First and last sector an erase of [addr, addr + len) must clear. + * + * Every sector the range touches is included. A sector is the smallest + * unit the F407 can erase, so the erase may clear bytes past either end of + * the range; the memory map above keeps each region sector-aligned so + * those bytes never belong to another region. + * + * @return 0 on success; -1 for len == 0, or a range that wraps or leaves + * the flash array (nothing is to be erased then). + */ +static inline int stm32f4_flash_sector_range(uint32_t addr, size_t len, + int *first, int *last) +{ + if (len == 0 || (uint64_t)addr + len > (uint64_t)STM32F4_FLASH_BASE + + STM32F4_FLASH_SIZE) + return -1; + int f = stm32f4_flash_sector_of(addr); + int l = stm32f4_flash_sector_of((uint32_t)(addr + len - 1)); + if (f < 0 || l < 0) + return -1; + *first = f; + *last = l; + return 0; +} + /* UART for recovery */ #define STM32F4_UART_INSTANCE 2 /* USART2 on PA2/PA3 */ diff --git a/docs/architecture.md b/docs/architecture.md index e67396f..dda1164 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -34,15 +34,17 @@ Address Range Size Region ───────────────────────────────────────────────────── 0x08000000 - 0x08003FFF 16K Stage-0 (eBootloader) 0x08004000 - 0x0800FFFF 48K Stage-1 (E-Boot) -0x08010000 - 0x0807FFFF 448K Slot A (active firmware) -0x08080000 - 0x080EFFFF 448K Slot B (candidate firmware) -0x080F0000 - 0x080F3FFF 16K Recovery image -0x080F4000 - 0x080F5FFF 8K Boot control (primary) -0x080F6000 - 0x080F7FFF 8K Boot control (backup) -0x080F8000 - 0x080F9FFF 8K Boot log -0x080FA000 - 0x080FFFFF 24K Device config / reserved +0x08010000 - 0x0801FFFF 64K Boot log +0x08020000 - 0x0805FFFF 256K Slot A (active firmware) +0x08060000 - 0x0809FFFF 256K Slot B (candidate firmware) +0x080A0000 - 0x080BFFFF 128K Recovery image +0x080C0000 - 0x080DFFFF 128K Boot control (primary) +0x080E0000 - 0x080FFFFF 128K Boot control (backup) ``` +The F407 erases whole sectors (4 x 16K, 1 x 64K, 7 x 128K), so every +region owns whole sectors and erasing one never touches another. + ## Boot Flow ``` diff --git a/docs/installation_usage.md b/docs/installation_usage.md index eafef96..d805782 100644 --- a/docs/installation_usage.md +++ b/docs/installation_usage.md @@ -161,7 +161,7 @@ python3 -m pip install -r requirements.txt # Pack firmware with eboot image header. The input limits (size, load and # entry addresses) are listed in quickstart.md and in tools/imgpack.py. python tools/imgpack.py --input firmware.bin --output firmware.packed.bin \ - --version 1.0.0 --load-addr 0x08010000 --entry-addr 0x08010100 + --version 1.0.0 --load-addr 0x08020000 --entry-addr 0x08020100 # Sign the image python tools/sign_image.py firmware.packed.bin --key signing_key.pem diff --git a/docs/memory-map.md b/docs/memory-map.md index f448441..f0f2860 100644 --- a/docs/memory-map.md +++ b/docs/memory-map.md @@ -8,19 +8,17 @@ ├──────────────────────────────────────────────────────────┤ │ 0x08004000 Stage-1 (E-Boot) 48 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x08010000 Slot A (Active Firmware) 448 KB │ +│ 0x08010000 Boot Log 64 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x08080000 Slot B (Candidate Firmware) 448 KB │ +│ 0x08020000 Slot A (Active Firmware) 256 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x080F0000 Recovery Image (Golden) 16 KB │ +│ 0x08060000 Slot B (Candidate Firmware) 256 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x080F4000 Boot Control (Primary) 8 KB │ +│ 0x080A0000 Recovery Image (Golden) 128 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x080F6000 Boot Control (Backup) 8 KB │ +│ 0x080C0000 Boot Control (Primary) 128 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x080F8000 Boot Log 8 KB │ -├──────────────────────────────────────────────────────────┤ -│ 0x080FA000 Device Config / Reserved 24 KB │ +│ 0x080E0000 Boot Control (Backup) 128 KB │ └──────────────────────────────────────────────────────────┘ ``` @@ -29,8 +27,8 @@ 1. **Stage-0 is never updated in the field** — it must remain immutable after manufacturing to guarantee recovery is always possible. -2. **Boot control is dual-copied** — primary at 0x080F4000, backup at - 0x080F6000. Both carry CRC32 protection. On load, if primary is +2. **Boot control is dual-copied** — primary at 0x080C0000, backup at + 0x080E0000. Both carry CRC32 protection. On load, if primary is corrupt the backup is used. 3. **Slot A and Slot B are identical in size** — this simplifies the @@ -42,6 +40,11 @@ 5. **Boot log is a circular buffer** — the `log_head` field in the boot control block tracks the write position. +6. **Every region owns whole erase sectors** — flash is erased a sector at + a time, so a region that shares a sector is wiped whenever its neighbour + is erased. On the F407 (4 x 16K, 1 x 64K, 7 x 128K sectors) that is why + the small regions take a full sector each. + ## Adapting to Other MCUs To port to a different MCU, create a new `boards//` directory and: diff --git a/docs/quickstart.md b/docs/quickstart.md index 19e1eaf..700d386 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -102,7 +102,7 @@ cmake -B build -DEBLDR_BOARD= -DCMAKE_BUILD_TYPE=Release \ # [load-addr, load-addr + size). --load-addr 0 means "unset" and skips the # entry check, as the bootloader does. python3 imgpack.py --input firmware.bin --output firmware.eimg \ - --load-addr 0x08010000 --entry-addr 0x08010100 --version 1.0.0 + --load-addr 0x08020000 --entry-addr 0x08020100 --version 1.0.0 # Sign it. This also sets the SHA-256 flag and marks the header as v2. python3 sign_image.py --image firmware.eimg --method ed25519 \ diff --git a/docs/secure_boot_chain.md b/docs/secure_boot_chain.md index 4b6c512..110c740 100644 --- a/docs/secure_boot_chain.md +++ b/docs/secure_boot_chain.md @@ -56,7 +56,7 @@ The eBootloader implements a staged chain of trust where each boot stage verifie ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ APPLICATION FIRMWARE │ -│ 448 KB × 2 slots (A/B) │ +│ 256 KB × 2 slots (A/B) │ │ │ │ • Application entry point (vector table) │ │ • Self-test → eos_fw_confirm_running_image() │ diff --git a/docs/threat_model.md b/docs/threat_model.md index 7fff178..12a8773 100644 --- a/docs/threat_model.md +++ b/docs/threat_model.md @@ -45,7 +45,7 @@ │ 48 KB — image scan, boot policy, boot log │ ├─ ─ ─ ─ ─ ─ TB-3: Stage-1 → Application ─ ─ ─ ─ ┤ │ Application Firmware │ -│ 448 KB × 2 slots (A/B) │ +│ 256 KB × 2 slots (A/B) │ └──────────────────────────────────────────────────┘ External boundaries: @@ -80,7 +80,7 @@ External boundaries: ### 3.2 Flash Contents -- **Regions:** Slot A (448 KB), Slot B (448 KB), Recovery (16 KB), Boot control primary (8 KB), Boot control backup (8 KB), Boot log (8 KB), Device config (24 KB) +- **Regions:** Slot A (256 KB), Slot B (256 KB), Recovery (128 KB), Boot control primary (128 KB), Boot control backup (128 KB), Boot log (64 KB) -- one or more whole flash sectors each - **Attack vector:** Direct flash modification (physical), corrupted OTA writes, bit-flip attacks ### 3.3 JTAG/SWD Debug Interface diff --git a/llms-full.txt b/llms-full.txt index ddd4f9d..c18fe9e 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -57,15 +57,17 @@ Address Range Size Region ───────────────────────────────────────────────────── 0x08000000 - 0x08003FFF 16K Stage-0 (eBootloader) 0x08004000 - 0x0800FFFF 48K Stage-1 (E-Boot) -0x08010000 - 0x0807FFFF 448K Slot A (active firmware) -0x08080000 - 0x080EFFFF 448K Slot B (candidate firmware) -0x080F0000 - 0x080F3FFF 16K Recovery image -0x080F4000 - 0x080F5FFF 8K Boot control (primary) -0x080F6000 - 0x080F7FFF 8K Boot control (backup) -0x080F8000 - 0x080F9FFF 8K Boot log -0x080FA000 - 0x080FFFFF 24K Device config / reserved +0x08010000 - 0x0801FFFF 64K Boot log +0x08020000 - 0x0805FFFF 256K Slot A (active firmware) +0x08060000 - 0x0809FFFF 256K Slot B (candidate firmware) +0x080A0000 - 0x080BFFFF 128K Recovery image +0x080C0000 - 0x080DFFFF 128K Boot control (primary) +0x080E0000 - 0x080FFFFF 128K Boot control (backup) ``` +The F407 erases whole sectors (4 x 16K, 1 x 64K, 7 x 128K), so every +region owns whole sectors and erasing one never touches another. + ## Boot Flow ``` @@ -190,7 +192,7 @@ The eBootloader implements a staged chain of trust where each boot stage verifie ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ APPLICATION FIRMWARE │ -│ 448 KB × 2 slots (A/B) │ +│ 256 KB × 2 slots (A/B) │ │ │ │ • Application entry point (vector table) │ │ • Self-test → eos_fw_confirm_running_image() │ @@ -657,7 +659,7 @@ Planned for future releases. │ 48 KB — image scan, boot policy, boot log │ ├─ ─ ─ ─ ─ ─ TB-3: Stage-1 → Application ─ ─ ─ ─ ┤ │ Application Firmware │ -│ 448 KB × 2 slots (A/B) │ +│ 256 KB × 2 slots (A/B) │ └──────────────────────────────────────────────────┘ External boundaries: @@ -692,7 +694,7 @@ External boundaries: ### 3.2 Flash Contents -- **Regions:** Slot A (448 KB), Slot B (448 KB), Recovery (16 KB), Boot control primary (8 KB), Boot control backup (8 KB), Boot log (8 KB), Device config (24 KB) +- **Regions:** Slot A (256 KB), Slot B (256 KB), Recovery (128 KB), Boot control primary (128 KB), Boot control backup (128 KB), Boot log (64 KB) -- one or more whole flash sectors each - **Attack vector:** Direct flash modification (physical), corrupted OTA writes, bit-flip attacks ### 3.3 JTAG/SWD Debug Interface @@ -1477,19 +1479,17 @@ value for `EBLDR_PRODUCTION_KEY`. ├──────────────────────────────────────────────────────────┤ │ 0x08004000 Stage-1 (E-Boot) 48 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x08010000 Slot A (Active Firmware) 448 KB │ -├──────────────────────────────────────────────────────────┤ -│ 0x08080000 Slot B (Candidate Firmware) 448 KB │ +│ 0x08010000 Boot Log 64 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x080F0000 Recovery Image (Golden) 16 KB │ +│ 0x08020000 Slot A (Active Firmware) 256 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x080F4000 Boot Control (Primary) 8 KB │ +│ 0x08060000 Slot B (Candidate Firmware) 256 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x080F6000 Boot Control (Backup) 8 KB │ +│ 0x080A0000 Recovery Image (Golden) 128 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x080F8000 Boot Log 8 KB │ +│ 0x080C0000 Boot Control (Primary) 128 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x080FA000 Device Config / Reserved 24 KB │ +│ 0x080E0000 Boot Control (Backup) 128 KB │ └──────────────────────────────────────────────────────────┘ ``` @@ -1498,8 +1498,8 @@ value for `EBLDR_PRODUCTION_KEY`. 1. **Stage-0 is never updated in the field** — it must remain immutable after manufacturing to guarantee recovery is always possible. -2. **Boot control is dual-copied** — primary at 0x080F4000, backup at - 0x080F6000. Both carry CRC32 protection. On load, if primary is +2. **Boot control is dual-copied** — primary at 0x080C0000, backup at + 0x080E0000. Both carry CRC32 protection. On load, if primary is corrupt the backup is used. 3. **Slot A and Slot B are identical in size** — this simplifies the @@ -1511,6 +1511,11 @@ value for `EBLDR_PRODUCTION_KEY`. 5. **Boot log is a circular buffer** — the `log_head` field in the boot control block tracks the write position. +6. **Every region owns whole erase sectors** — flash is erased a sector at + a time, so a region that shares a sector is wiped whenever its neighbour + is erased. On the F407 (4 x 16K, 1 x 64K, 7 x 128K sectors) that is why + the small regions take a full sector each. + ## Adapting to Other MCUs To port to a different MCU, create a new `boards//` directory and: @@ -1698,7 +1703,7 @@ python3 -m pip install -r requirements.txt # Pack firmware with eboot image header. The input limits (size, load and # entry addresses) are listed in quickstart.md and in tools/imgpack.py. python tools/imgpack.py --input firmware.bin --output firmware.packed.bin \ - --version 1.0.0 --load-addr 0x08010000 --entry-addr 0x08010100 + --version 1.0.0 --load-addr 0x08020000 --entry-addr 0x08020100 # Sign the image python tools/sign_image.py firmware.packed.bin --key signing_key.pem @@ -1874,7 +1879,7 @@ cmake -B build -DEBLDR_BOARD= -DCMAKE_BUILD_TYPE=Release \ # [load-addr, load-addr + size). --load-addr 0 means "unset" and skips the # entry check, as the bootloader does. python3 imgpack.py --input firmware.bin --output firmware.eimg \ - --load-addr 0x08010000 --entry-addr 0x08010100 --version 1.0.0 + --load-addr 0x08020000 --entry-addr 0x08020100 --version 1.0.0 # Sign it. This also sets the SHA-256 flag and marks the header as v2. python3 sign_image.py --image firmware.eimg --method ed25519 \ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d0ff509..66e69be 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -242,6 +242,17 @@ target_link_libraries(eboot_test_keystore_production PRIVATE eboot_core) add_test(NAME test_keystore_production COMMAND eboot_test_keystore_production) list(APPEND EBLDR_UNIT_TESTS test_keystore_production) +# --- test_stm32f4_flash_layout: erase covers its range, regions own their sectors --- +# board_stm32f4.c is cross-only, but its sector range helper and memory map +# live in board_stm32f4.h as plain C. +add_executable(eboot_test_stm32f4_flash_layout unit/test_stm32f4_flash_layout.c) +target_include_directories(eboot_test_stm32f4_flash_layout PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../boards/stm32f4 +) +target_link_libraries(eboot_test_stm32f4_flash_layout PRIVATE eboot_core) +add_test(NAME test_stm32f4_flash_layout COMMAND eboot_test_stm32f4_flash_layout) +list(APPEND EBLDR_UNIT_TESTS test_stm32f4_flash_layout) + # --- Valgrind test targets --- # # The list is derived from the suites registered above, not written out again. diff --git a/tests/unit/test_stm32f4_flash_layout.c b/tests/unit/test_stm32f4_flash_layout.c new file mode 100644 index 0000000..01d04f1 --- /dev/null +++ b/tests/unit/test_stm32f4_flash_layout.c @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 EoS Project +// ISO/IEC 25000 | ISO/IEC/IEEE 15288:2023 + +/** + * @file test_stm32f4_flash_layout.c + * @brief STM32F4 erase covers the whole range; no two regions share a sector. + * + * Two defects, one consequence. stm32f4_flash_erase() erased only the + * sector holding addr and ignored len, so a 448K slot erase cleared one + * sector of it. And the memory map put Slot B's tail, the recovery image, + * both boot control copies and the boot log in sector 11, so the erase + * that eos_bootctl_save() issues before every write wiped the other four. + * + * board_stm32f4.c drives flash registers and is built only by a cross + * build; its erase takes the sector range from stm32f4_flash_sector_range() + * in board_stm32f4.h, which is plain C and is what this suite exercises, + * together with the memory map macros in the same header. + */ + +#include "board_stm32f4.h" +#include "eos_bootctl.h" +#include +#include +#include + +static int tests_run = 0; +static int tests_passed = 0; + +#define ASSERT(cond) do { \ + if (!(cond)) { \ + printf("[FAIL] %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + exit(1); \ + } \ +} while (0) + +#define TEST(name) \ + static void name(void); \ + static void run_##name(void) { \ + printf(" %-58s ", #name); \ + tests_run++; \ + name(); \ + tests_passed++; \ + printf("[PASS]\n"); \ + } \ + static void name(void) + +#define FLASH_END ((uint32_t)STM32F4_FLASH_BASE + STM32F4_FLASH_SIZE) + +/* RM0090 table 5: STM32F405/407 main memory sectors. */ +static const uint32_t sector_start[STM32F4_FLASH_SECTORS + 1] = { + 0x08000000, 0x08004000, 0x08008000, 0x0800C000, 0x08010000, 0x08020000, + 0x08040000, 0x08060000, 0x08080000, 0x080A0000, 0x080C0000, 0x080E0000, + 0x08100000, +}; + +static int is_sector_boundary(uint32_t addr) +{ + for (int i = 0; i <= STM32F4_FLASH_SECTORS; i++) + if (sector_start[i] == addr) return 1; + return 0; +} + +TEST(test_sector_of_matches_the_reference_manual) +{ + for (int i = 0; i < STM32F4_FLASH_SECTORS; i++) { + ASSERT(stm32f4_flash_sector_of(sector_start[i]) == i); + ASSERT(stm32f4_flash_sector_of(sector_start[i + 1] - 1) == i); + } + ASSERT(stm32f4_flash_sector_of(STM32F4_FLASH_BASE - 1) == -1); + ASSERT(stm32f4_flash_sector_of(FLASH_END) == -1); +} + +TEST(test_erase_range_covers_every_sector_it_touches) +{ + int first = -1, last = -1; + + /* A whole slot: both of its sectors, not just the first. */ + ASSERT(stm32f4_flash_sector_range(STM32F4_SLOT_A_ADDR, STM32F4_SLOT_A_SIZE, + &first, &last) == 0); + ASSERT(first == 5 && last == 6); + ASSERT(stm32f4_flash_sector_range(STM32F4_SLOT_B_ADDR, STM32F4_SLOT_B_SIZE, + &first, &last) == 0); + ASSERT(first == 7 && last == 8); + + /* Spanning the 16K / 64K / 128K geometry changes. */ + ASSERT(stm32f4_flash_sector_range(0x0800C000, 0x14001, &first, &last) == 0); + ASSERT(first == 3 && last == 5); + + /* The whole array. */ + ASSERT(stm32f4_flash_sector_range(STM32F4_FLASH_BASE, STM32F4_FLASH_SIZE, + &first, &last) == 0); + ASSERT(first == 0 && last == STM32F4_FLASH_SECTORS - 1); + + /* A small erase stays in its sector. */ + ASSERT(stm32f4_flash_sector_range(STM32F4_BOOTCTL_ADDR, sizeof(eos_bootctl_t), + &first, &last) == 0); + ASSERT(first == 10 && last == 10); +} + +TEST(test_erase_range_rejects_what_it_cannot_erase) +{ + int first = 99, last = 99; + + ASSERT(stm32f4_flash_sector_range(STM32F4_SLOT_A_ADDR, 0, &first, &last) != 0); + ASSERT(stm32f4_flash_sector_range(FLASH_END - 4, 8, &first, &last) != 0); + ASSERT(stm32f4_flash_sector_range(STM32F4_FLASH_BASE - 4, 8, &first, &last) != 0); + ASSERT(stm32f4_flash_sector_range(0xFFFFFF00u, 0x200, &first, &last) != 0); + ASSERT(first == 99 && last == 99); +} + +struct region { const char *name; uint32_t addr; uint32_t size; }; + +static const struct region regions[] = { + { "stage0", STM32F4_STAGE0_ADDR, STM32F4_STAGE0_SIZE }, + { "stage1", STM32F4_STAGE1_ADDR, STM32F4_STAGE1_SIZE }, + { "log", STM32F4_LOG_ADDR, STM32F4_LOG_SIZE }, + { "slot_a", STM32F4_SLOT_A_ADDR, STM32F4_SLOT_A_SIZE }, + { "slot_b", STM32F4_SLOT_B_ADDR, STM32F4_SLOT_B_SIZE }, + { "recovery", STM32F4_RECOVERY_ADDR, STM32F4_RECOVERY_SIZE }, + { "bootctl", STM32F4_BOOTCTL_ADDR, STM32F4_BOOTCTL_SIZE }, + { "bootctl_backup", STM32F4_BOOTCTL_BACKUP_ADDR, STM32F4_BOOTCTL_BACKUP_SIZE }, +}; +#define N_REGIONS (sizeof(regions) / sizeof(regions[0])) + +TEST(test_every_region_owns_whole_sectors) +{ + for (size_t i = 0; i < N_REGIONS; i++) { + const struct region *r = ®ions[i]; + if (!is_sector_boundary(r->addr) || !is_sector_boundary(r->addr + r->size)) { + printf("\n %s [0x%08X, +0x%X) is not sector-aligned ", + r->name, (unsigned)r->addr, (unsigned)r->size); + ASSERT(0); + } + } +} + +TEST(test_no_two_regions_share_a_sector) +{ + int owner[STM32F4_FLASH_SECTORS]; + for (int s = 0; s < STM32F4_FLASH_SECTORS; s++) owner[s] = -1; + + for (size_t i = 0; i < N_REGIONS; i++) { + int first, last; + ASSERT(stm32f4_flash_sector_range(regions[i].addr, regions[i].size, + &first, &last) == 0); + for (int s = first; s <= last; s++) { + if (owner[s] != -1) { + printf("\n sector %d holds both %s and %s ", s, + regions[owner[s]].name, regions[i].name); + ASSERT(0); + } + owner[s] = (int)i; + } + } +} + +TEST(test_layout_fits_what_it_holds) +{ + /* A/B slots are interchangeable, so they are the same size. */ + ASSERT(STM32F4_SLOT_A_SIZE == STM32F4_SLOT_B_SIZE); + ASSERT(sizeof(eos_bootctl_t) <= STM32F4_BOOTCTL_SIZE); + ASSERT(sizeof(eos_bootctl_t) <= STM32F4_BOOTCTL_BACKUP_SIZE); + ASSERT(EOS_BOOT_LOG_MAX * sizeof(eos_boot_log_entry_t) <= STM32F4_LOG_SIZE); + /* Stage-1's vector table is where stage-0 jumps; the linker scripts + * place stage-0 at 0x08000000/16K and stage-1 at 0x08004000/48K. */ + ASSERT(STM32F4_STAGE0_ADDR == 0x08000000 && STM32F4_STAGE0_SIZE == 16 * 1024); + ASSERT(STM32F4_STAGE1_ADDR == 0x08004000 && STM32F4_STAGE1_SIZE == 48 * 1024); +} + +int main(void) +{ + printf("STM32F4 flash erase and layout\n\n"); + + run_test_sector_of_matches_the_reference_manual(); + run_test_erase_range_covers_every_sector_it_touches(); + run_test_erase_range_rejects_what_it_cannot_erase(); + run_test_every_region_owns_whole_sectors(); + run_test_no_two_regions_share_a_sector(); + run_test_layout_fits_what_it_holds(); + + printf("\n%d/%d passed\n", tests_passed, tests_run); + return tests_passed == tests_run ? 0 : 1; +}