diff --git a/CMakeLists.txt b/CMakeLists.txt index 728e028..77c6b5f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,6 +111,7 @@ add_library(eboot_core STATIC core/debug_lock.c core/fw_decrypt.c core/image_tlv.c + core/image_slot_bounds.c ) target_include_directories(eboot_core PUBLIC ${EBLDR_INCLUDE_DIR}) target_link_libraries(eboot_core PUBLIC eboot_hal) diff --git a/core/image_slot_bounds.c b/core/image_slot_bounds.c new file mode 100644 index 0000000..00c961b --- /dev/null +++ b/core/image_slot_bounds.c @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 EoS Project +// ISO/IEC 25000 | ISO/IEC/IEEE 15288:2023 + +#include "eos_image.h" + +bool eos_image_fits_slot(const eos_image_header_t *hdr, uint32_t slot_size) +{ + if (!hdr) + return false; + + if (hdr->hdr_size > slot_size) + return false; + + uint32_t remaining = slot_size - hdr->hdr_size; + + if (hdr->image_size > remaining) + return false; + + remaining -= hdr->image_size; + + if (hdr->tlv_len > remaining) + return false; + + return true; +} diff --git a/core/recovery.c b/core/recovery.c index 1e5c816..9e02647 100644 --- a/core/recovery.c +++ b/core/recovery.c @@ -316,8 +316,7 @@ static int recovery_handle_verify(eos_slot_t slot) uint32_t slot_size = eos_hal_slot_size(slot); if (slot_size == 0 || - hdr.hdr_size > slot_size || - hdr.image_size > slot_size - hdr.hdr_size) + !eos_image_fits_slot(&hdr, slot_size)) return recovery_send_nack(); /* eos_image_verify_integrity() adds hdr_size internally — pass base addr only */ diff --git a/core/secure_boot.c b/core/secure_boot.c index 4d80cd3..a35e05c 100644 --- a/core/secure_boot.c +++ b/core/secure_boot.c @@ -85,6 +85,15 @@ eos_secure_boot_result_t eos_secure_boot(const eos_secure_boot_config_t *cfg, return EOS_SBOOT_ERR_BAD_HEADER; } + /* The complete image must fit in the configured slot before any + * integrity/TLV processing can read beyond the slot boundary. */ + if (cfg->slot_size == 0 || + !eos_image_fits_slot(&hdr, cfg->slot_size)) { + attest_record(2, hdr.image_version, hdr.hash, NULL, + EOS_SBOOT_ERR_BAD_HEADER); + return EOS_SBOOT_ERR_BAD_HEADER; + } + /* ---- Step 2: Verify integrity (SHA-256 hash) ---- */ /* Bug Fix: Pass image_addr directly to verify_integrity since verify_integrity already adds hdr_size */ rc = eos_image_verify_integrity(&hdr, cfg->image_addr); diff --git a/core/slot_manager.c b/core/slot_manager.c index 08d8368..b46da6f 100644 --- a/core/slot_manager.c +++ b/core/slot_manager.c @@ -52,8 +52,7 @@ static int verify_slot(eos_slot_t slot) * boot, did not. */ uint32_t slot_size = eos_hal_slot_size(slot); if (slot_size == 0 || - si->header.hdr_size > slot_size || - si->header.image_size > slot_size - si->header.hdr_size) { + !eos_image_fits_slot(&si->header, slot_size)) { si->state = EOS_SLOT_STATE_INVALID; return EOS_ERR_INVALID; } diff --git a/include/eos_image.h b/include/eos_image.h index 24cbb16..0752e3a 100644 --- a/include/eos_image.h +++ b/include/eos_image.h @@ -208,6 +208,17 @@ int eos_image_verify_signature(const eos_image_header_t *hdr); */ int eos_image_check_version(uint32_t candidate_version, uint32_t min_version); +/** + * @brief Check whether an image fits completely within its flash slot. + * + * Validates the image header, payload, and TLV area against the slot size. + * + * @param hdr Image header. + * @param slot_size Available slot size in bytes. + * @return true if the complete image fits within the slot, false otherwise. + */ +bool eos_image_fits_slot(const eos_image_header_t *hdr, uint32_t slot_size); + /** * @brief Compute CRC32 over a flash region, reporting read failures. * diff --git a/include/eos_secure_boot.h b/include/eos_secure_boot.h index 659c4a3..7400171 100644 --- a/include/eos_secure_boot.h +++ b/include/eos_secure_boot.h @@ -28,6 +28,7 @@ typedef struct { bool require_encryption; /* Enforce AES-GCM decryption */ bool lock_debug; /* Disable SWD/JTAG after boot */ bool enable_attestation; /* Log boot measurements */ + uint32_t slot_size; /* Flash capacity available to this image */ } eos_secure_boot_config_t; /* ---- Secure Boot Result ---- */ diff --git a/stage1/jump_app.c b/stage1/jump_app.c index 5ed653f..dbac56e 100644 --- a/stage1/jump_app.c +++ b/stage1/jump_app.c @@ -32,12 +32,11 @@ int eboot_jump_to_app(eos_bootctl_t *bctl, eos_slot_t slot) if (rc != EOS_OK) return rc; - /* Defense-in-depth: ensure the image fits within the actual slot - * before any integrity verification can stream payload bytes. */ + /* Ensure the complete image, including TLVs, fits within the slot + * before any integrity verification can read beyond the slot. */ uint32_t slot_size = eos_hal_slot_size(slot); if (slot_size == 0 || - hdr.hdr_size > slot_size || - hdr.image_size > slot_size - hdr.hdr_size) { + !eos_image_fits_slot(&hdr, slot_size)) { eos_boot_log_append(EOS_LOG_IMAGE_INVALID, slot, EOS_ERR_INVALID); return EOS_ERR_INVALID; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a69f646..967ca5c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -72,7 +72,7 @@ target_link_libraries(eboot_test_slot_size_bounds PRIVATE eboot_core) add_test(NAME test_slot_size_bounds COMMAND eboot_test_slot_size_bounds) add_executable(eboot_test_jump_app_bounds unit/test_jump_app_bounds.c) -target_link_libraries(eboot_test_jump_app_bounds PRIVATE eboot_core eboot_stage1) +target_link_libraries(eboot_test_jump_app_bounds PRIVATE eboot_stage1) add_test(NAME test_jump_app_bounds COMMAND eboot_test_jump_app_bounds) # --- test_device_table: UEFI-style device table --- diff --git a/tests/unit/test_jump_app_bounds.c b/tests/unit/test_jump_app_bounds.c index 5e08b6f..75724a3 100644 --- a/tests/unit/test_jump_app_bounds.c +++ b/tests/unit/test_jump_app_bounds.c @@ -37,8 +37,10 @@ static int sim_flash_read(uint32_t addr, void *buf, size_t len) memcpy(buf, &sim_flash[addr], len); - if (addr >= SLOT_A_ADDR + sizeof(eos_image_header_t) && - addr < SLOT_B_ADDR) + uint32_t payload_start = SLOT_A_ADDR + sizeof(eos_image_header_t); + + if (addr >= payload_start && + addr < SLOT_A_ADDR + SLOT_A_SIZE) payload_bytes_read += len; return EOS_OK; @@ -184,14 +186,34 @@ TEST(test_in_bounds_image_reaches_integrity_check) ASSERT(payload_bytes_read == image_size); } +TEST(test_tlv_beyond_slot_rejected_before_payload_read) +{ + eos_image_header_t hdr; + eos_bootctl_t bctl; + + uint32_t image_size = + SLOT_A_SIZE - sizeof(eos_image_header_t); + + fill_header(&hdr, image_size); + hdr.tlv_len = 1; + write_header(SLOT_A_ADDR, &hdr); + memset(&bctl, 0, sizeof(bctl)); + + int rc = eboot_jump_to_app(&bctl, EOS_SLOT_A); + + ASSERT(rc == EOS_ERR_INVALID); + ASSERT(payload_bytes_read == 0); +} + int main(void) { printf("=== eBootloader: Jump-App Slot-Size Bounds Tests ===\n\n"); run_test_oversized_image_rejected_before_reading_payload(); run_test_in_bounds_image_reaches_integrity_check(); + run_test_tlv_beyond_slot_rejected_before_payload_read(); - tests_run = 2; + tests_run = 3; printf("\n%d/%d tests passed\n", tests_passed, tests_run); diff --git a/tests/unit/test_secure_boot.c b/tests/unit/test_secure_boot.c index 5f9160a..6139cfd 100644 --- a/tests/unit/test_secure_boot.c +++ b/tests/unit/test_secure_boot.c @@ -138,6 +138,7 @@ static eos_secure_boot_config_t base_cfg(void) eos_secure_boot_config_t cfg; memset(&cfg, 0, sizeof(cfg)); cfg.image_addr = IMAGE_ADDR; + cfg.slot_size = 0x8000u; cfg.require_signature = false; cfg.lock_debug = false; return cfg; @@ -198,6 +199,23 @@ TEST(test_decrypt_failure_is_attested) ASSERT(log->entries[log->count - 1].verify_result == EOS_SBOOT_ERR_DECRYPT); } +TEST(test_tlv_beyond_slot_is_rejected) +{ + write_image(0); + + eos_image_header_t hdr; + memcpy(&hdr, &sim_flash[IMAGE_ADDR], sizeof(hdr)); + + hdr.tlv_len = 1; + memcpy(&sim_flash[IMAGE_ADDR], &hdr, sizeof(hdr)); + + eos_secure_boot_config_t cfg = base_cfg(); + cfg.slot_size = sizeof(eos_image_header_t) + PAYLOAD_LEN; + + uint32_t entry = 0; + ASSERT(eos_secure_boot(&cfg, &entry) == EOS_SBOOT_ERR_BAD_HEADER); +} + int main(void) { printf("Secure boot policy tests\n"); @@ -205,6 +223,7 @@ int main(void) run_test_encrypted_image_rejected_while_decrypt_unimplemented(); run_test_plaintext_image_boots_when_encryption_not_required(); run_test_decrypt_failure_is_attested(); + run_test_tlv_beyond_slot_is_rejected(); /* Compare, and let the exit code carry it. `return 0` meant a suite that * ran nothing at all still reported success -- the ASSERT macro exits on * failure, so the only thing this return could ever have signalled is diff --git a/tests/unit/test_secure_boot_policy.c b/tests/unit/test_secure_boot_policy.c index be092d5..3d58454 100644 --- a/tests/unit/test_secure_boot_policy.c +++ b/tests/unit/test_secure_boot_policy.c @@ -181,6 +181,7 @@ TEST(test_secure_boot_refuses_when_the_debug_lock_cannot_be_taken) eos_secure_boot_config_t cfg; memset(&cfg, 0, sizeof(cfg)); + cfg.slot_size = FLASH_SIZE; cfg.image_addr = FLASH_BASE; cfg.require_signature = false; cfg.require_encryption = false; @@ -201,6 +202,7 @@ TEST(test_the_same_image_boots_when_no_debug_lock_is_asked_for) eos_secure_boot_config_t cfg; memset(&cfg, 0, sizeof(cfg)); + cfg.slot_size = FLASH_SIZE; cfg.image_addr = FLASH_BASE; cfg.require_signature = false; cfg.require_encryption = false; @@ -220,6 +222,7 @@ TEST(test_secure_boot_proceeds_when_the_debug_lock_succeeds) eos_secure_boot_config_t cfg; memset(&cfg, 0, sizeof(cfg)); + cfg.slot_size = FLASH_SIZE; cfg.image_addr = FLASH_BASE; cfg.require_signature = false; cfg.require_encryption = false; @@ -257,6 +260,7 @@ TEST(test_the_ordinary_boot_path_is_unaffected_by_the_step_4_change) eos_secure_boot_config_t cfg; memset(&cfg, 0, sizeof(cfg)); + cfg.slot_size = FLASH_SIZE; cfg.image_addr = FLASH_BASE; cfg.require_signature = false; cfg.lock_debug = false;