From 9449dfa7fe4cdf39519eac45738629d0bf5b84c3 Mon Sep 17 00:00:00 2001 From: Marco Oliverio Date: Wed, 17 Jun 2026 10:55:56 +0200 Subject: [PATCH 1/6] fix: bound unauthenticated image size before RAM load --- src/libwolfboot.c | 20 ++ src/update_ram.c | 19 +- tools/unit-tests/Makefile | 28 ++- tools/unit-tests/unit-update-ram-enc.c | 334 +++++++++++++++++++++++++ 4 files changed, 393 insertions(+), 8 deletions(-) create mode 100644 tools/unit-tests/unit-update-ram-enc.c diff --git a/src/libwolfboot.c b/src/libwolfboot.c index 5a37ac5f9e..e2edf9e732 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -2408,6 +2408,26 @@ int wolfBoot_ram_decrypt(uint8_t *src, uint8_t *dst) } len = *((uint32_t*)(dec_hdr + sizeof(uint32_t))); +#if !defined(WOLFBOOT_FIXED_PARTITIONS) && !defined(WOLFBOOT_RAMBOOT_MAX_SIZE) +# error "WOLFBOOT_RAMBOOT_MAX_SIZE required when WOLFBOOT_NO_PARTITIONS=1" +#endif + /* Bound the UNAUTHENTICATED image length before it drives the copy into the + * RAM load region: the image is loaded to RAM before its signature is + * verified, so this length (read from the not-yet-authenticated header) is + * attacker-influenceable and must be range checked first. */ +#ifdef WOLFBOOT_FIXED_PARTITIONS + if (WOLFBOOT_PARTITION_SIZE <= IMAGE_HEADER_SIZE || + len > (uint32_t)(WOLFBOOT_PARTITION_SIZE - IMAGE_HEADER_SIZE)) { + wolfBoot_printf("Invalid encrypted image size %u at %p\n", len, src); + return -1; + } +#elif defined(WOLFBOOT_RAMBOOT_MAX_SIZE) + if (len > WOLFBOOT_RAMBOOT_MAX_SIZE) { + wolfBoot_printf("Invalid encrypted image size %u at %p\n", len, src); + return -1; + } +#endif + /* decrypt content */ while (dst_offset < (len + IMAGE_HEADER_SIZE)) { wolfBoot_crypto_set_iv(encrypt_iv_nonce, iv_counter); diff --git a/src/update_ram.c b/src/update_ram.c index 0fdbb31cd6..7e18115640 100644 --- a/src/update_ram.c +++ b/src/update_ram.c @@ -91,16 +91,21 @@ int wolfBoot_ramboot(struct wolfBoot_image *img, uint8_t *src, uint8_t *dst) /* determine size of partition */ img_size = wolfBoot_image_size((uint8_t*)dst); -#if defined(WOLFBOOT_NO_PARTITIONS) -# ifndef WOLFBOOT_RAMBOOT_MAX_SIZE -# error "WOLFBOOT_RAMBOOT_MAX_SIZE required when WOLFBOOT_NO_PARTITIONS=1" -# endif - if (img_size > WOLFBOOT_RAMBOOT_MAX_SIZE) { +#if !defined(WOLFBOOT_FIXED_PARTITIONS) && !defined(WOLFBOOT_RAMBOOT_MAX_SIZE) +# error "WOLFBOOT_RAMBOOT_MAX_SIZE required when WOLFBOOT_NO_PARTITIONS=1" +#endif + /* Bound the UNAUTHENTICATED image length before it drives the copy into the + * RAM load region: the image is loaded to RAM before its signature is + * verified, so this length (read from the not-yet-authenticated header) is + * attacker-influenceable and must be range checked first. */ +#ifdef WOLFBOOT_FIXED_PARTITIONS + if (WOLFBOOT_PARTITION_SIZE <= IMAGE_HEADER_SIZE || + img_size > (uint32_t)(WOLFBOOT_PARTITION_SIZE - IMAGE_HEADER_SIZE)) { wolfBoot_printf("Invalid image size %u at %p\n", img_size, src); return -1; } -#elif defined(WOLFBOOT_PARTITION_SIZE) - if (img_size > (WOLFBOOT_PARTITION_SIZE - IMAGE_HEADER_SIZE)) { +#elif defined(WOLFBOOT_RAMBOOT_MAX_SIZE) + if (img_size > WOLFBOOT_RAMBOOT_MAX_SIZE) { wolfBoot_printf("Invalid image size %u at %p\n", img_size, src); return -1; } diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index a40c523085..7bf48e6b7d 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -51,7 +51,7 @@ TESTS:=unit-parser unit-fdt unit-extflash unit-string unit-spi-flash unit-aes128 unit-enc-nvm-flagshome unit-delta unit-gzip unit-update-flash unit-update-flash-delta \ unit-update-flash-hook \ unit-update-flash-self-update \ - unit-update-flash-enc unit-update-ram unit-update-ram-nofixed unit-update-ram-noramboot unit-update-flash-hwswap unit-pkcs11_store unit-psa_store unit-disk \ + unit-update-flash-enc unit-update-ram unit-update-ram-enc unit-update-ram-enc-nopart unit-update-ram-nofixed unit-update-ram-noramboot unit-update-flash-hwswap unit-pkcs11_store unit-psa_store unit-disk \ unit-update-disk unit-multiboot unit-boot-x86-fsp unit-loader-tpm-init unit-qspi-flash unit-fwtpm-stub unit-tpm-rsa-exp \ unit-image-nopart unit-image-sha384 unit-image-sha3-384 unit-store-sbrk \ unit-tpm-blob unit-policy-create unit-policy-sign unit-rot-auth unit-sdhci-response-bits \ @@ -165,6 +165,20 @@ unit-update-ram-noramboot:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN -DUNIT_TE -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED -DEXT_FLASH -DPART_UPDATE_EXT \ -DPART_SWAP_EXT -DPART_BOOT_EXT -DWOLFBOOT_DUALBOOT -DNO_XIP -DWOLFBOOT_NO_RAMBOOT \ -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE +unit-update-ram-enc:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN -DUNIT_TEST_AUTH \ + -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED -DEXT_FLASH -DPART_UPDATE_EXT \ + -DPART_SWAP_EXT -DPART_BOOT_EXT -DWOLFBOOT_DUALBOOT -DNO_XIP -DMMU \ + -DEXT_ENCRYPTED -DENCRYPT_WITH_CHACHA -DHAVE_CHACHA \ + -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE +# Same source as unit-update-ram-enc, but with a WOLFBOOT_RAMBOOT_MAX_SIZE that +# is tighter than the partition cap (NO_PARTITIONS / RAM-load-region flavour), +# so the bound check is exercised through its RAMBOOT_MAX_SIZE branch. +unit-update-ram-enc-nopart:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN -DUNIT_TEST_AUTH \ + -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED -DEXT_FLASH -DPART_UPDATE_EXT \ + -DPART_SWAP_EXT -DPART_BOOT_EXT -DWOLFBOOT_DUALBOOT -DNO_XIP -DMMU \ + -DEXT_ENCRYPTED -DENCRYPT_WITH_CHACHA -DHAVE_CHACHA \ + -DWOLFBOOT_NO_PARTITIONS -DWOLFBOOT_RAMBOOT_MAX_SIZE=0x1000 \ + -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE unit-update-ram-nofixed:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN \ -DUNIT_TEST_AUTH -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED -DEXT_FLASH \ -DPART_UPDATE_EXT -DPART_SWAP_EXT -DPART_BOOT_EXT -DWOLFBOOT_DUALBOOT \ @@ -473,6 +487,18 @@ unit-update-flash-enc: ../../include/target.h unit-update-flash.c unit-update-ram: ../../include/target.h unit-update-ram.c gcc -o $@ unit-update-ram.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) +unit-update-ram-enc: ../../include/target.h unit-update-ram-enc.c + gcc -o $@ unit-update-ram-enc.c \ + $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c \ + $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/chacha.c \ + $(CFLAGS) $(LDFLAGS) + +unit-update-ram-enc-nopart: ../../include/target.h unit-update-ram-enc.c + gcc -o $@ unit-update-ram-enc.c \ + $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c \ + $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/chacha.c \ + $(CFLAGS) $(LDFLAGS) + unit-update-ram-nofixed: ../../include/target.h unit-update-ram-nofixed.c gcc -o $@ unit-update-ram-nofixed.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) diff --git a/tools/unit-tests/unit-update-ram-enc.c b/tools/unit-tests/unit-update-ram-enc.c new file mode 100644 index 0000000000..945f59c09a --- /dev/null +++ b/tools/unit-tests/unit-update-ram-enc.c @@ -0,0 +1,334 @@ +/* unit-update-ram-enc.c + * + * Unit tests for wolfBoot_ram_decrypt() (EXT_ENCRYPTED + MMU ramboot path). + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Regression coverage for the bounds check in wolfBoot_ram_decrypt(). + * + * EXT_ENCRYPTED uses an unauthenticated stream cipher (no MAC), so the image + * length field, read from the decrypted-but-not-yet-authenticated header, is + * attacker-influenceable: a single flipped ciphertext bit changes the decrypted + * 'len'. These tests confirm an oversized 'len' (including the 32-bit (len + + * IMAGE_HEADER_SIZE) wrap value) is rejected, while a legitimately-sized image + * still decrypts correctly. + */ + +#ifndef WOLFBOOT_HASH_SHA256 + #define WOLFBOOT_HASH_SHA256 +#endif +#define IMAGE_HEADER_SIZE 256 +#define MOCK_ADDRESS_UPDATE 0xCC000000 +#define MOCK_ADDRESS_BOOT 0xCD000000 +#define MOCK_ADDRESS_SWAP 0xCE000000 +#include "target.h" + +#include +#include +#include +#include +#include +#include +#include "user_settings.h" +#include "wolfboot/wolfboot.h" +#include "encrypt.h" +#include "libwolfboot.c" +#include +#include "unit-mock-flash.c" +#include + +Suite *wolfboot_suite(void); + +int hal_flash_protect(haladdr_t address, int len) +{ + (void)address; + (void)len; + return 0; +} + +/* Needed only to satisfy the non-fixed-partition dualboot path libwolfboot.c + * compiles under WOLFBOOT_NO_PARTITIONS (unit-update-ram-enc-nopart build). + * The tests call wolfBoot_ram_decrypt() directly and never reach these. */ +void* hal_get_primary_address(void) +{ + return (void *)(uintptr_t)WOLFBOOT_PARTITION_BOOT_ADDRESS; +} + +void* hal_get_update_address(void) +{ + return (void *)(uintptr_t)WOLFBOOT_PARTITION_UPDATE_ADDRESS; +} + +/* Provision a deterministic, valid (not all-0x00 / all-0xFF) key + nonce and + * initialize the crypto so that encrypt_iv_nonce mirrors the boot-time state. */ +static void setup_crypto_key(void) +{ + uint8_t key[ENCRYPT_KEY_SIZE]; + uint8_t nonce[ENCRYPT_NONCE_SIZE]; + unsigned int i; + + for (i = 0; i < ENCRYPT_KEY_SIZE; i++) + key[i] = (uint8_t)(i + 1); + for (i = 0; i < ENCRYPT_NONCE_SIZE; i++) + nonce[i] = (uint8_t)(0xA0 + i); + + encrypt_initialized = 0; + ck_assert_int_eq(wolfBoot_set_encrypt_key(key, nonce), 0); + ck_assert_int_eq(crypto_init(), 0); +} + +/* Encrypt 'size' bytes of 'plain' into 'enc' using exactly the per-block IV + * schedule that decrypt_header()/wolfBoot_ram_decrypt() use to decrypt, so the + * produced ciphertext round-trips through the function under test. 'size' must + * be a multiple of ENCRYPT_BLOCK_SIZE. */ +static void encrypt_blob(uint8_t *enc, const uint8_t *plain, uint32_t size) +{ + uint32_t off; + for (off = 0; off < size; off += ENCRYPT_BLOCK_SIZE) { + wolfBoot_crypto_set_iv(encrypt_iv_nonce, off / ENCRYPT_BLOCK_SIZE); + crypto_encrypt(enc + off, plain + off, ENCRYPT_BLOCK_SIZE); + } +} + +/* Build a plaintext header (magic + chosen length field), zero-padded to one + * IMAGE_HEADER_SIZE, then encrypt it into 'enc'. Enough to reach the length + * check in wolfBoot_ram_decrypt(); the body is irrelevant for the reject path. */ +static void make_encrypted_header(uint8_t *enc, uint32_t len) +{ + uint8_t plain[IMAGE_HEADER_SIZE]; + uint32_t magic = WOLFBOOT_MAGIC; + + memset(plain, 0, sizeof(plain)); + memcpy(plain, &magic, sizeof(magic)); + memcpy(plain + sizeof(uint32_t), &len, sizeof(len)); + encrypt_blob(enc, plain, IMAGE_HEADER_SIZE); +} + +/* Attacker primitive, NO key required. XOR a bit-mask into the ciphertext bytes + * that cover the length field (header offset sizeof(uint32_t)). The cipher is a + * stream cipher (ciphertext = keystream XOR plaintext), so each flipped + * ciphertext bit flips the same bit of the decrypted length: the decrypted + * 'len' becomes (original_len XOR mask), with no knowledge of the key, the + * keystream, or even the original length. 'mask' is applied in the same byte + * order the header stores the length, so (len ^ mask) holds on any endianness. */ +static void flip_len_ciphertext_bits(uint8_t *enc, uint32_t mask) +{ + uint8_t mask_bytes[sizeof(uint32_t)]; + unsigned int i; + + memcpy(mask_bytes, &mask, sizeof(mask)); + for (i = 0; i < sizeof(uint32_t); i++) + enc[sizeof(uint32_t) + i] ^= mask_bytes[i]; +} + +#define DST_CANARY 0x5A +/* A valid, in-bounds image decrypts successfully and matches the plaintext, and + * the copy must touch ONLY the image region: everything past it is canaried + * before the call and verified untouched afterwards. */ +START_TEST(test_ram_decrypt_valid) +{ + const uint32_t len = 1024; + const uint32_t total = IMAGE_HEADER_SIZE + len; + uint8_t *plain = malloc(total); + uint8_t *enc = malloc(total); + uint8_t *dst = malloc(WOLFBOOT_PARTITION_SIZE); + uint32_t magic = WOLFBOOT_MAGIC; + uint32_t touched_past = 0; + uint32_t i; + int ret; + + ck_assert_ptr_nonnull(plain); + ck_assert_ptr_nonnull(enc); + ck_assert_ptr_nonnull(dst); + ck_assert_uint_eq(total % ENCRYPT_BLOCK_SIZE, 0); + + for (i = 0; i < total; i++) + plain[i] = (uint8_t)(i * 7 + 3); + memcpy(plain, &magic, sizeof(magic)); + memcpy(plain + sizeof(uint32_t), &len, sizeof(len)); + + setup_crypto_key(); + encrypt_blob(enc, plain, total); + + /* Canary the whole destination so any write past the image is detectable. */ + memset(dst, DST_CANARY, WOLFBOOT_PARTITION_SIZE); + + ret = wolfBoot_ram_decrypt(enc, dst); + ck_assert_int_eq(ret, 0); + ck_assert_mem_eq(dst, plain, total); + + /* Memory after the IMAGE_HEADER_SIZE + len image bytes must be untouched. */ + for (i = total; i < WOLFBOOT_PARTITION_SIZE; i++) { + if (dst[i] != DST_CANARY) + touched_past++; + } + ck_assert_uint_eq(touched_past, 0); + + free(plain); + free(enc); + free(dst); +} +END_TEST + +/* An oversized length must be rejected before the copy loop runs. Without the + * bounds check this writes IMAGE_HEADER_SIZE + len bytes into a dst sized for a + * maximum valid image, overrunning it (caught by ASan under ASAN=1). */ +START_TEST(test_ram_decrypt_oversize_rejected) +{ + const uint32_t len = WOLFBOOT_PARTITION_SIZE; /* > max valid payload */ + /* src large enough to feed the (unfixed) loop without a source overread, + * so the destination overflow is the signal ASan would report. */ + uint8_t *enc = malloc(WOLFBOOT_PARTITION_SIZE + 2 * IMAGE_HEADER_SIZE); + uint8_t *dst = malloc(WOLFBOOT_PARTITION_SIZE); + int ret; + + ck_assert_ptr_nonnull(enc); + ck_assert_ptr_nonnull(dst); + memset(enc, 0, WOLFBOOT_PARTITION_SIZE + 2 * IMAGE_HEADER_SIZE); + + setup_crypto_key(); + make_encrypted_header(enc, len); + + ret = wolfBoot_ram_decrypt(enc, dst); + ck_assert_int_eq(ret, -1); + + free(enc); + free(dst); +} +END_TEST + +/* The specific length that makes (len + IMAGE_HEADER_SIZE) wrap to 0 on 32-bit + * unsigned math must also be rejected (previously it returned 0 having copied + * nothing, reporting success on a bogus image). */ +START_TEST(test_ram_decrypt_overflow_len_rejected) +{ + const uint32_t len = (uint32_t)(0u - IMAGE_HEADER_SIZE); /* len + HDR == 0 */ + uint8_t *enc = malloc(2 * IMAGE_HEADER_SIZE); + uint8_t *dst = malloc(WOLFBOOT_PARTITION_SIZE); + int ret; + + ck_assert_ptr_nonnull(enc); + ck_assert_ptr_nonnull(dst); + memset(enc, 0, 2 * IMAGE_HEADER_SIZE); + + setup_crypto_key(); + make_encrypted_header(enc, len); + + ret = wolfBoot_ram_decrypt(enc, dst); + ck_assert_int_eq(ret, -1); + + free(enc); + free(dst); +} +END_TEST + +/* Realistic threat model: the attacker cannot encrypt (no key) but can write to + * the encrypted partition. Starting from a legitimately-encrypted, correctly- + * sized image, they flip ciphertext bits in the length field to forge an + * arbitrarily large decrypted 'len'. */ +START_TEST(test_ram_decrypt_len_bitflip_rejected) +{ + const uint32_t real_len = 1024; /* legitimate image length */ + const uint32_t mask = 0x80000000u; /* flip only the top bit of len */ + const uint32_t total = IMAGE_HEADER_SIZE + real_len; + uint8_t *plain = malloc(total); + uint8_t *enc = malloc(total); + uint8_t *dst = malloc(WOLFBOOT_PARTITION_SIZE); + uint32_t magic = WOLFBOOT_MAGIC; + uint32_t decoded_len = 0; + uint32_t i; + int ret; + + ck_assert_ptr_nonnull(plain); + ck_assert_ptr_nonnull(enc); + ck_assert_ptr_nonnull(dst); + + /* Owner builds a legitimate, correctly-sized encrypted image. */ + for (i = 0; i < total; i++) + plain[i] = (uint8_t)(i * 7 + 3); + memcpy(plain, &magic, sizeof(magic)); + memcpy(plain + sizeof(uint32_t), &real_len, sizeof(real_len)); + + setup_crypto_key(); + encrypt_blob(enc, plain, total); + + /* Attacker tampers with the ciphertext only (no key, no plaintext). */ + flip_len_ciphertext_bits(enc, mask); + + /* Verify the malleation worked (this decrypt uses the key purely to inspect + * the result; the attack above used only the ciphertext): the magic is + * still valid and the decrypted length is now the forged, oversized value. */ + ck_assert_int_eq(decrypt_header(enc), 0); + decoded_len = *((uint32_t*)(dec_hdr + sizeof(uint32_t))); + ck_assert_uint_eq(decoded_len, real_len ^ mask); + ck_assert(decoded_len > (WOLFBOOT_PARTITION_SIZE - IMAGE_HEADER_SIZE)); + + /* The forged length must be rejected before the copy loop runs. */ + ret = wolfBoot_ram_decrypt(enc, dst); + ck_assert_int_eq(ret, -1); + + free(plain); + free(enc); + free(dst); +} +END_TEST + +Suite *wolfboot_suite(void) +{ + Suite *s = suite_create("wolfboot-ram-decrypt"); + TCase *valid = tcase_create("ram_decrypt valid image"); + TCase *oversize = tcase_create("ram_decrypt oversize rejected"); + TCase *overflow = tcase_create("ram_decrypt length-overflow rejected"); + TCase *bitflip = tcase_create("ram_decrypt length bit-flip rejected"); + + tcase_add_test(valid, test_ram_decrypt_valid); + tcase_add_test(oversize, test_ram_decrypt_oversize_rejected); + tcase_add_test(overflow, test_ram_decrypt_overflow_len_rejected); + tcase_add_test(bitflip, test_ram_decrypt_len_bitflip_rejected); + + suite_add_tcase(s, valid); + suite_add_tcase(s, oversize); + suite_add_tcase(s, overflow); + suite_add_tcase(s, bitflip); + + tcase_set_timeout(bitflip, 5); + tcase_set_timeout(valid, 5); + tcase_set_timeout(oversize, 5); + tcase_set_timeout(overflow, 5); + + return s; +} + +int main(int argc, char *argv[]) +{ + int fails; + Suite *s; + SRunner *sr; + + (void)argc; + argv0 = strdup(argv[0]); + s = wolfboot_suite(); + sr = srunner_create(s); + srunner_set_fork_status(sr, CK_NOFORK); + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + return fails; +} From 4d1c961ae2a223ea2f73d11ee31d25d72e385d50 Mon Sep 17 00:00:00 2001 From: Marco Oliverio Date: Thu, 18 Jun 2026 10:22:32 +0200 Subject: [PATCH 2/6] ram_decrypt: read header length endian-safe and aligned --- src/libwolfboot.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libwolfboot.c b/src/libwolfboot.c index e2edf9e732..f67a34ce4e 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -2406,7 +2406,10 @@ int wolfBoot_ram_decrypt(uint8_t *src, uint8_t *dst) wolfBoot_printf("Error decrypting header at %p!\n", src); return -1; } - len = *((uint32_t*)(dec_hdr + sizeof(uint32_t))); + /* dec_hdr is a byte buffer: copy the little-endian length field without an + * unaligned cast, then convert to native byte order. */ + XMEMCPY(&len, dec_hdr + sizeof(uint32_t), sizeof(len)); + len = im2n(len); #if !defined(WOLFBOOT_FIXED_PARTITIONS) && !defined(WOLFBOOT_RAMBOOT_MAX_SIZE) # error "WOLFBOOT_RAMBOOT_MAX_SIZE required when WOLFBOOT_NO_PARTITIONS=1" From 0649e5de995eacbbe2d1969cb45aa939ed79ae31 Mon Sep 17 00:00:00 2001 From: Marco Oliverio Date: Thu, 18 Jun 2026 10:22:55 +0200 Subject: [PATCH 3/6] ramboot: match #error text to the bounds-check guard --- src/libwolfboot.c | 2 +- src/update_ram.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libwolfboot.c b/src/libwolfboot.c index f67a34ce4e..b0e4d2e02b 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -2412,7 +2412,7 @@ int wolfBoot_ram_decrypt(uint8_t *src, uint8_t *dst) len = im2n(len); #if !defined(WOLFBOOT_FIXED_PARTITIONS) && !defined(WOLFBOOT_RAMBOOT_MAX_SIZE) -# error "WOLFBOOT_RAMBOOT_MAX_SIZE required when WOLFBOOT_NO_PARTITIONS=1" +# error "WOLFBOOT_FIXED_PARTITIONS or WOLFBOOT_RAMBOOT_MAX_SIZE required to bound the RAM load" #endif /* Bound the UNAUTHENTICATED image length before it drives the copy into the * RAM load region: the image is loaded to RAM before its signature is diff --git a/src/update_ram.c b/src/update_ram.c index 7e18115640..f7eff9ce47 100644 --- a/src/update_ram.c +++ b/src/update_ram.c @@ -92,7 +92,7 @@ int wolfBoot_ramboot(struct wolfBoot_image *img, uint8_t *src, uint8_t *dst) /* determine size of partition */ img_size = wolfBoot_image_size((uint8_t*)dst); #if !defined(WOLFBOOT_FIXED_PARTITIONS) && !defined(WOLFBOOT_RAMBOOT_MAX_SIZE) -# error "WOLFBOOT_RAMBOOT_MAX_SIZE required when WOLFBOOT_NO_PARTITIONS=1" +# error "WOLFBOOT_FIXED_PARTITIONS or WOLFBOOT_RAMBOOT_MAX_SIZE required to bound the RAM load" #endif /* Bound the UNAUTHENTICATED image length before it drives the copy into the * RAM load region: the image is loaded to RAM before its signature is From e454c082feba1c2c7179087049e05c2b646ffada Mon Sep 17 00:00:00 2001 From: Marco Oliverio Date: Thu, 18 Jun 2026 11:26:15 +0200 Subject: [PATCH 4/6] ramboot: prefer RAMBOOT_MAX_SIZE bound over partition size --- src/libwolfboot.c | 14 ++++++++------ src/update_ram.c | 14 ++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/libwolfboot.c b/src/libwolfboot.c index b0e4d2e02b..c4ce10df7d 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -2417,15 +2417,17 @@ int wolfBoot_ram_decrypt(uint8_t *src, uint8_t *dst) /* Bound the UNAUTHENTICATED image length before it drives the copy into the * RAM load region: the image is loaded to RAM before its signature is * verified, so this length (read from the not-yet-authenticated header) is - * attacker-influenceable and must be range checked first. */ -#ifdef WOLFBOOT_FIXED_PARTITIONS - if (WOLFBOOT_PARTITION_SIZE <= IMAGE_HEADER_SIZE || - len > (uint32_t)(WOLFBOOT_PARTITION_SIZE - IMAGE_HEADER_SIZE)) { + * attacker-influenceable and must be range checked first. When both are + * configured, WOLFBOOT_RAMBOOT_MAX_SIZE takes precedence: it is the explicit + * cap on the RAM load region and may be tighter than the partition size. */ +#if defined(WOLFBOOT_RAMBOOT_MAX_SIZE) + if (len > WOLFBOOT_RAMBOOT_MAX_SIZE) { wolfBoot_printf("Invalid encrypted image size %u at %p\n", len, src); return -1; } -#elif defined(WOLFBOOT_RAMBOOT_MAX_SIZE) - if (len > WOLFBOOT_RAMBOOT_MAX_SIZE) { +#elif defined(WOLFBOOT_FIXED_PARTITIONS) + if (WOLFBOOT_PARTITION_SIZE <= IMAGE_HEADER_SIZE || + len > (uint32_t)(WOLFBOOT_PARTITION_SIZE - IMAGE_HEADER_SIZE)) { wolfBoot_printf("Invalid encrypted image size %u at %p\n", len, src); return -1; } diff --git a/src/update_ram.c b/src/update_ram.c index f7eff9ce47..9407e0a93a 100644 --- a/src/update_ram.c +++ b/src/update_ram.c @@ -97,15 +97,17 @@ int wolfBoot_ramboot(struct wolfBoot_image *img, uint8_t *src, uint8_t *dst) /* Bound the UNAUTHENTICATED image length before it drives the copy into the * RAM load region: the image is loaded to RAM before its signature is * verified, so this length (read from the not-yet-authenticated header) is - * attacker-influenceable and must be range checked first. */ -#ifdef WOLFBOOT_FIXED_PARTITIONS - if (WOLFBOOT_PARTITION_SIZE <= IMAGE_HEADER_SIZE || - img_size > (uint32_t)(WOLFBOOT_PARTITION_SIZE - IMAGE_HEADER_SIZE)) { + * attacker-influenceable and must be range checked first. When both are + * configured, WOLFBOOT_RAMBOOT_MAX_SIZE takes precedence: it is the explicit + * cap on the RAM load region and may be tighter than the partition size. */ +#if defined(WOLFBOOT_RAMBOOT_MAX_SIZE) + if (img_size > WOLFBOOT_RAMBOOT_MAX_SIZE) { wolfBoot_printf("Invalid image size %u at %p\n", img_size, src); return -1; } -#elif defined(WOLFBOOT_RAMBOOT_MAX_SIZE) - if (img_size > WOLFBOOT_RAMBOOT_MAX_SIZE) { +#elif defined(WOLFBOOT_FIXED_PARTITIONS) + if (WOLFBOOT_PARTITION_SIZE <= IMAGE_HEADER_SIZE || + img_size > (uint32_t)(WOLFBOOT_PARTITION_SIZE - IMAGE_HEADER_SIZE)) { wolfBoot_printf("Invalid image size %u at %p\n", img_size, src); return -1; } From 6017dc5f2fcba6b038f56c20a9c147e9f8dee5e9 Mon Sep 17 00:00:00 2001 From: Marco Oliverio Date: Thu, 18 Jun 2026 10:24:22 +0200 Subject: [PATCH 5/6] ram_decrypt: assert RAM load region is block-aligned --- src/libwolfboot.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/libwolfboot.c b/src/libwolfboot.c index c4ce10df7d..5f23b655b6 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -2376,6 +2376,17 @@ int RAMFUNCTION ext_flash_decrypt_read(uintptr_t address, uint8_t *data, int len #endif /* __WOLFBOOT */ #if defined(MMU) +/* The decrypt loop in wolfBoot_ram_decrypt() copies in ENCRYPT_BLOCK_SIZE + * granules, rounding the final write up to a block boundary. The size bound is + * only safe if the RAM load region is itself block-aligned; assert that where + * the region size is a compile-time constant (a macro in every such config). */ +#if defined(WOLFBOOT_FIXED_PARTITIONS) && defined(WOLFBOOT_PARTITION_SIZE) +typedef char wolfBoot_ramboot_blockalign_check[ + ((WOLFBOOT_PARTITION_SIZE) % ENCRYPT_BLOCK_SIZE == 0) ? 1 : -1]; +#elif defined(WOLFBOOT_RAMBOOT_MAX_SIZE) +typedef char wolfBoot_ramboot_blockalign_check[ + ((WOLFBOOT_RAMBOOT_MAX_SIZE) % ENCRYPT_BLOCK_SIZE == 0) ? 1 : -1]; +#endif /** * @brief Decrypt data from RAM. * From 4a1fc49aa671b1f6db802c3eae3fd69bdf68008f Mon Sep 17 00:00:00 2001 From: Marco Oliverio Date: Thu, 18 Jun 2026 11:02:44 +0200 Subject: [PATCH 6/6] test/ram-enc: add exact-max and one-block-over boundary tests --- tools/unit-tests/unit-update-ram-enc.c | 77 ++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tools/unit-tests/unit-update-ram-enc.c b/tools/unit-tests/unit-update-ram-enc.c index 945f59c09a..3ed885f4a3 100644 --- a/tools/unit-tests/unit-update-ram-enc.c +++ b/tools/unit-tests/unit-update-ram-enc.c @@ -290,6 +290,75 @@ START_TEST(test_ram_decrypt_len_bitflip_rejected) } END_TEST +/* The maximum in-bounds payload, matching the branch the bound check uses. */ +#ifdef WOLFBOOT_FIXED_PARTITIONS +# define RAM_DECRYPT_MAX_PAYLOAD (WOLFBOOT_PARTITION_SIZE - IMAGE_HEADER_SIZE) +#elif defined(WOLFBOOT_RAMBOOT_MAX_SIZE) +# define RAM_DECRYPT_MAX_PAYLOAD WOLFBOOT_RAMBOOT_MAX_SIZE +#endif + +/* The exact maximum in-bounds length must decrypt successfully. Paired with the + * one-block-over test below this brackets the accept/reject boundary, locking + * the comparison's off-by-one ('>' vs '>=', +/- IMAGE_HEADER_SIZE). */ +START_TEST(test_ram_decrypt_max_valid) +{ + const uint32_t len = RAM_DECRYPT_MAX_PAYLOAD; + const uint32_t total = IMAGE_HEADER_SIZE + len; + uint8_t *plain = malloc(total); + uint8_t *enc = malloc(total); + uint8_t *dst = malloc(WOLFBOOT_PARTITION_SIZE); + uint32_t magic = WOLFBOOT_MAGIC; + uint32_t i; + int ret; + + ck_assert_ptr_nonnull(plain); + ck_assert_ptr_nonnull(enc); + ck_assert_ptr_nonnull(dst); + ck_assert_uint_eq(total % ENCRYPT_BLOCK_SIZE, 0); + ck_assert(total <= WOLFBOOT_PARTITION_SIZE); + + for (i = 0; i < total; i++) + plain[i] = (uint8_t)(i * 7 + 3); + memcpy(plain, &magic, sizeof(magic)); + memcpy(plain + sizeof(uint32_t), &len, sizeof(len)); + + setup_crypto_key(); + encrypt_blob(enc, plain, total); + + ret = wolfBoot_ram_decrypt(enc, dst); + ck_assert_int_eq(ret, 0); + ck_assert_mem_eq(dst, plain, total); + + free(plain); + free(enc); + free(dst); +} +END_TEST + +/* One block past the maximum in-bounds length must be rejected before the copy + * loop runs (the reject side of the boundary pair). */ +START_TEST(test_ram_decrypt_one_over_rejected) +{ + const uint32_t len = RAM_DECRYPT_MAX_PAYLOAD + ENCRYPT_BLOCK_SIZE; + uint8_t *enc = malloc(2 * IMAGE_HEADER_SIZE); + uint8_t *dst = malloc(WOLFBOOT_PARTITION_SIZE); + int ret; + + ck_assert_ptr_nonnull(enc); + ck_assert_ptr_nonnull(dst); + memset(enc, 0, 2 * IMAGE_HEADER_SIZE); + + setup_crypto_key(); + make_encrypted_header(enc, len); + + ret = wolfBoot_ram_decrypt(enc, dst); + ck_assert_int_eq(ret, -1); + + free(enc); + free(dst); +} +END_TEST + Suite *wolfboot_suite(void) { Suite *s = suite_create("wolfboot-ram-decrypt"); @@ -297,21 +366,29 @@ Suite *wolfboot_suite(void) TCase *oversize = tcase_create("ram_decrypt oversize rejected"); TCase *overflow = tcase_create("ram_decrypt length-overflow rejected"); TCase *bitflip = tcase_create("ram_decrypt length bit-flip rejected"); + TCase *maxvalid = tcase_create("ram_decrypt exact-max valid"); + TCase *oneover = tcase_create("ram_decrypt one block over rejected"); tcase_add_test(valid, test_ram_decrypt_valid); tcase_add_test(oversize, test_ram_decrypt_oversize_rejected); tcase_add_test(overflow, test_ram_decrypt_overflow_len_rejected); tcase_add_test(bitflip, test_ram_decrypt_len_bitflip_rejected); + tcase_add_test(maxvalid, test_ram_decrypt_max_valid); + tcase_add_test(oneover, test_ram_decrypt_one_over_rejected); suite_add_tcase(s, valid); suite_add_tcase(s, oversize); suite_add_tcase(s, overflow); suite_add_tcase(s, bitflip); + suite_add_tcase(s, maxvalid); + suite_add_tcase(s, oneover); tcase_set_timeout(bitflip, 5); tcase_set_timeout(valid, 5); tcase_set_timeout(oversize, 5); tcase_set_timeout(overflow, 5); + tcase_set_timeout(maxvalid, 5); + tcase_set_timeout(oneover, 5); return s; }