diff --git a/.gitignore b/.gitignore index c7e87b7ae1..1ad89d29c8 100644 --- a/.gitignore +++ b/.gitignore @@ -478,12 +478,14 @@ tools/unit-tests/unit-versal-qspi-dma tools/unit-tests/unit-xspi-tfd-index tools/unit-tests/unit-zynq-erase-loop tools/unit-tests/unit-zynq-ext-write +tools/unit-tests/unit-sama5d3-ext-read # sources generated by the unit-test extraction rules tools/unit-tests/aurix_erased_extract.h tools/unit-tests/nvm_cache_scrub_extract.h tools/unit-tests/nxp_ls1028a_host.c tools/unit-tests/nxp_p1021_host.c tools/unit-tests/nxp_t10xx_fixup_extract.h +tools/unit-tests/sama5d3_read_extract.h tools/unit-tests/sdhci_host.c tools/unit-tests/stm32l5_write_extract.h tools/unit-tests/stm32u5_write_extract.h diff --git a/docs/Targets.md b/docs/Targets.md index dcc5892848..76643e9d41 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -5736,6 +5736,14 @@ A first stage loader is required to load the wolfBoot image into DDR for executi | fsl_qe_ucode_1021_10_A.bin | 0x01F00000 | | swap block | 0x02200000 | +The QE microcode programmed at `0x01F00000` is validated only for +structural integrity (header magic, version, and size bounds) before it +is activated; it is not cryptographically authenticated. This is out of +scope for the example configuration: the microcode region sits inside +the update partition, so replacing it requires the same flash write +access that would let an attacker replace the update image itself, +which wolfBoot's image authentication already protects against. + ### Building wolfBoot for NXP P1021 PPC By default wolfBoot will use `powerpc-linux-gnu-` cross-compiler prefix. These tools can be installed with the Debian package `gcc-powerpc-linux-gnu` (`sudo apt install gcc-powerpc-linux-gnu`). @@ -5888,6 +5896,13 @@ Note: On T1040, FMAN and QE firmware share the same 128KB NOR erase sector (0xEFF00000-0xEFF1FFFF). They must be programmed together in a single erase/write operation. +The QE and FMan microcode in these NOR regions is validated only for +structural integrity (header magic, version, and size bounds) before it +is activated; it is not cryptographically authenticated. This is out of +scope for the example configurations: programming these regions requires +local write access to the board's flash, which also allows replacing the +wolfBoot image itself - a threat the signed image flow already covers. + ### Design Both T1024 and T1040 use a two-stage boot. Stage1 runs XIP from NOR flash, diff --git a/hal/nxp_t10xx.c b/hal/nxp_t10xx.c index be46f95cc0..954e2ff9f4 100644 --- a/hal/nxp_t10xx.c +++ b/hal/nxp_t10xx.c @@ -3522,7 +3522,7 @@ int hal_dts_fixup(void* dts_addr) /* fixup the fman clock */ off = fdt_node_offset_by_compatible(fdt, -1, "fsl,fman"); - if (off != !FDT_ERR_NOTFOUND) { + if (off != -FDT_ERR_NOTFOUND) { fdt_fixup_val(fdt, off, "fman@", "clock-frequency", hal_get_bus_clk()); } @@ -3617,9 +3617,9 @@ int hal_dts_fixup(void* dts_addr) /* fix SDHC */ off = fdt_node_offset_by_compatible(fdt, -1, "fsl,esdhc"); - if (off != !FDT_ERR_NOTFOUND) { + if (off != -FDT_ERR_NOTFOUND) { fdt_fixup_val(fdt, off, "sdhc@", "clock-frequency", hal_get_bus_clk()); - fdt_fixup_str(fdt, off, "cpu", "status", "okay"); + fdt_fixup_str(fdt, off, "sdhc@", "status", "okay"); } #endif /* !BUILD_LOADER_STAGE1 */ diff --git a/hal/sama5d3.c b/hal/sama5d3.c index e4134c1a47..d572696a89 100644 --- a/hal/sama5d3.c +++ b/hal/sama5d3.c @@ -601,28 +601,29 @@ static int nand_check_bad_block(uint32_t block) int ext_flash_read(uintptr_t address, uint8_t *data, int len) { uint8_t buffer_page[NAND_FLASH_PAGE_SIZE]; - uint32_t block = div_u(address, nand_flash.block_size); /* The block where the address falls in */ - uint32_t page = div_u(address, nand_flash.page_size); /* The page where the address falls in */ - uint32_t start_page_in_block = mod(page, nand_flash.pages_per_block); /* The start page within this block */ - uint32_t in_block_offset = mod(address, nand_flash.block_size); /* The offset of the address within the block */ - uint32_t remaining = nand_flash.block_size - in_block_offset; /* How many bytes remaining to read in the first block */ - int len_to_read = len; - uint8_t *buffer = data; - uint32_t i; - int copy = 0; + uintptr_t addr = address; + uint8_t *dst = data; + uint32_t in_page = mod(address, nand_flash.page_size); /* The offset of the address within the page */ + int remaining = len; int ret; - if (len < (int)nand_flash.page_size) { - buffer = buffer_page; - copy = 1; - len_to_read = nand_flash.page_size; - } + if (len <= 0) + return 0; - while (len_to_read > 0) { - uint32_t sz = len_to_read; - uint32_t pages_to_read; - if (sz > remaining) - sz = remaining; + while (remaining > 0) { + uint32_t block; + uint32_t page; + uint32_t page_in_block; + uint32_t chunk; + + /* Bytes available from the current address to the end of its page */ + chunk = nand_flash.page_size - in_page; + if (chunk > (uint32_t)remaining) + chunk = (uint32_t)remaining; + + block = div_u(addr, nand_flash.block_size); + page = div_u(addr, nand_flash.page_size); + page_in_block = mod(page, nand_flash.pages_per_block); do { ret = nand_check_bad_block(block); @@ -632,32 +633,23 @@ int ext_flash_read(uintptr_t address, uint8_t *data, int len) } } while (ret < 0); - /* Amount of pages to be read from this block */ - pages_to_read = div_u((sz + nand_flash.page_size - 1), nand_flash.page_size); - - if (pages_to_read * nand_flash.page_size > remaining) - pages_to_read--; - - /* Read (remaining) pages off a block */ - for (i = 0; i < pages_to_read; i++) { - nand_read_page(block, start_page_in_block + i, buffer); - if (sz > nand_flash.page_size) - sz = nand_flash.page_size; - len_to_read -= sz; - buffer += sz; + if ((in_page == 0) && (chunk == nand_flash.page_size)) { + /* Full page at the page head: read straight into the caller's + * buffer. */ + nand_read_page(block, page_in_block, dst); } - /* The block is over, move to the next one */ - block++; - start_page_in_block = 0; - remaining = nand_flash.block_size; - } - if (copy) { - uint32_t *dst = (uint32_t *)data; - uint32_t *src = (uint32_t *)buffer_page; - uint32_t tot_len = (uint32_t)len; - for (i = 0; i < (tot_len >> 2); i++) { - dst[i] = src[i]; + else { + /* Partial first/last page: stage through the page buffer and + * copy from the column offset, so a full page is never written + * past the end of the caller's buffer. */ + nand_read_page(block, page_in_block, buffer_page); + memcpy(dst, buffer_page + in_page, chunk); } + + dst += chunk; + remaining -= (int)chunk; + addr += chunk; + in_page = mod(addr, nand_flash.page_size); } return len; } @@ -716,20 +708,23 @@ static void dbgu_init(void) { int ext_flash_write(uintptr_t address, const uint8_t *data, int len) { - /* TODO */ + /* SAMA5D3 NAND page program is not implemented: this HAL only + * supports reading NAND. Report failure instead of pretending the + * data was programmed. */ (void)address; (void)data; (void)len; - return 0; + return -1; } int ext_flash_erase(uintptr_t address, int len) { - /* TODO */ + /* SAMA5D3 NAND block erase is not implemented: see ext_flash_write. */ (void)address; (void)len; - return 0; + + return -1; } /* SAMA5D3 NAND flash does not have an enable pin */ diff --git a/include/fdt.h b/include/fdt.h index 6d982737c0..2f60d3965b 100644 --- a/include/fdt.h +++ b/include/fdt.h @@ -73,6 +73,18 @@ struct fdt_property { #define FDT_ALIGN(x, a) (((x) + (a) - 1) & ~((a) - 1)) #define FDT_TAGALIGN(x) (FDT_ALIGN((x), FDT_TAGSIZE)) +/* Bounds for the attacker-influenced fdt_totalsize before relocating or + * forwarding a DTB. MIN is the FDT v17 header size (also enforced by the + * signer): fdt_check_header validates magic/version but not totalsize, so a + * crafted header with a tiny totalsize must be rejected rather than + * loaded/forwarded as a partial tree. The MAX default assumes a staging + * window at WOLFBOOT_LOAD_DTS_ADDRESS of at least 1 MiB; targets with a + * smaller window must override WOLFBOOT_DTS_MAX_SIZE (see hal/nxp_ppc.h). */ +#ifndef WOLFBOOT_DTS_MAX_SIZE +#define WOLFBOOT_DTS_MAX_SIZE (1024U * 1024U) +#endif +#define WOLFBOOT_DTS_MIN_SIZE (40U) + #define FDT_FIRST_SUPPORTED_VERSION 0x10 #define FDT_LAST_SUPPORTED_VERSION 0x11 diff --git a/src/fdt.c b/src/fdt.c index e949747ad0..b388d0d19a 100644 --- a/src/fdt.c +++ b/src/fdt.c @@ -462,10 +462,24 @@ static int fdt_subnode_offset_namelen(const void *fdt, int offset, int fdt_check_header(const void *fdt) { if (fdt_magic(fdt) == FDT_MAGIC) { + uint32_t off_rsv = fdt_off_mem_rsvmap(fdt); + uint32_t off_struct = fdt_off_dt_struct(fdt); + uint32_t size_struct = fdt_size_dt_struct(fdt); + uint32_t off_strings = fdt_off_dt_strings(fdt); + uint32_t size_strings = fdt_size_dt_strings(fdt); + if (fdt_version(fdt) < FDT_FIRST_SUPPORTED_VERSION) return -FDT_ERR_BADVERSION; if (fdt_last_comp_version(fdt) > FDT_LAST_SUPPORTED_VERSION) return -FDT_ERR_BADVERSION; + /* The three structural areas must sit inside the blob and not + * overlap: reservation map, structure block and string table, + * in that order. The additions are made in 64-bit so the size + * fields cannot wrap around the comparison. */ + if (off_rsv > off_struct + || (uint64_t)off_struct + size_struct > off_strings + || (uint64_t)off_strings + size_strings > fdt_totalsize(fdt)) + return -FDT_ERR_BADSTRUCTURE; } else if (fdt_magic(fdt) == FDT_SW_MAGIC) { if (fdt_size_dt_struct(fdt) == 0) @@ -579,6 +593,17 @@ const char* fdt_get_string(const void *fdt, int stroffset, int *lenp) uint32_t strsize = fdt_size_dt_strings(fdt); const char *s; const char *end; + int err; + + /* off_dt_strings/size_dt_strings are attacker-influenceable header + * fields; validate the layout against totalsize before forming the + * string-table pointer. */ + err = fdt_check_header(fdt); + if (err != 0) { + if (lenp) + *lenp = err; + return NULL; + } if ((stroffset < 0) || ((uint32_t)stroffset >= strsize)) { if (lenp) @@ -726,21 +751,28 @@ int fdt_node_offset_by_compatible(const void *fdt, int startoffset, int len; const char *prop = (const char*)fdt_getprop(fdt, offset, "compatible", &len); - /* property list may contain multiple null terminated strings */ - while (prop != NULL && len >= complen) { + /* property list may contain multiple null terminated strings. + * Locate each entry's NUL terminator within the declared length + * first, then compare: the entry must be exactly as long as the + * wanted string, so no byte is ever read past the property and + * an unterminated trailing entry can neither match nor be + * misread as one. */ + while (prop != NULL && len > 0) { const char* nextprop; - if (memcmp(compatible, prop, complen+1) == 0) { - return offset; - } + int entrylen; + nextprop = memchr(prop, '\0', len); - if (nextprop != NULL) { - len -= (nextprop - prop) + 1; - prop = nextprop + 1; - } - else { + if (nextprop == NULL) { /* No NUL terminator within the declared length, break. */ break; } + entrylen = (int)(nextprop - prop); + if (entrylen == complen && + memcmp(compatible, prop, complen) == 0) { + return offset; + } + len -= entrylen + 1; + prop = nextprop + 1; } } return offset; @@ -908,6 +940,22 @@ int fdt_fixup_val64(void* fdt, int off, const char* node, const char* name, /* FIT Specific */ + +/* Returns the property value only when it is a NUL-terminated C string + * within its declared length, else NULL: property values are opaque + * byte arrays and the names taken from them are passed to + * fdt_find_node_offset()/strcmp(), which strlen() them. */ +static const char* fit_getprop_string(const void* fdt, int offset, + const char* name) +{ + int len = 0; + const char* val = (const char*)fdt_getprop(fdt, offset, name, &len); + + if (val == NULL || len <= 0 || memchr(val, '\0', len) == NULL) + return NULL; + return val; +} + const char* fit_find_images(void* fdt, const char** pkernel, const char** pflat_dt, const char** pramdisk, const char** pfpga) { @@ -936,19 +984,16 @@ const char* fit_find_images(void* fdt, const char** pkernel, const char** pflat_ if (conf == NULL) #endif { - val = fdt_getprop(fdt, off, "default", &len); - if (val != NULL && len > 0) { - conf = (const char*)val; - } + conf = fit_getprop_string(fdt, off, "default"); } } if (conf != NULL) { off = fdt_find_node_offset(fdt, -1, conf); if (off > 0) { - kernel = fdt_getprop(fdt, off, "kernel", &len); - flat_dt = fdt_getprop(fdt, off, "fdt", &len); - ramdisk = fdt_getprop(fdt, off, "ramdisk", &len); - fpga = fdt_getprop(fdt, off, "fpga", &len); + kernel = fit_getprop_string(fdt, off, "kernel"); + flat_dt = fit_getprop_string(fdt, off, "fdt"); + ramdisk = fit_getprop_string(fdt, off, "ramdisk"); + fpga = fit_getprop_string(fdt, off, "fpga"); } } if (kernel == NULL) { @@ -1187,11 +1232,20 @@ static void* fit_load_image_inner(void* fdt, const char* image, int* lenp, * raw. */ comp = (const char*)fdt_getprop(fdt, off, "compression", &complen); - if (comp != NULL && complen > 0) { - if (strcmp(comp, "gzip") == 0) { + /* Compare within the declared property length: the value + * must be exactly "gzip" or "none" (NUL-terminated). Any + * other shape - including an unterminated value - fails + * closed instead of being strncmp()'d past the property. */ + if (comp != NULL) { + if (complen == 5 && comp[4] == '\0' && + memcmp(comp, "gzip", 4) == 0) { is_gzip = 1; } - else if (strcmp(comp, "none") != 0) { + else if (complen == 5 && comp[4] == '\0' && + memcmp(comp, "none", 4) == 0) { + /* uncompressed */ + } + else { is_unknown_comp = 1; } } diff --git a/src/image.c b/src/image.c index f9bf6bfe84..b76648ff51 100644 --- a/src/image.c +++ b/src/image.c @@ -355,6 +355,7 @@ static void wolfBoot_verify_signature_ecc(uint8_t key_slot, #endif #endif /* !WOLFBOOT_CERT_CHAIN_VERIFY */ if (ret != 0) { + wc_ecc_free(&ecc); return; } #else @@ -364,6 +365,7 @@ static void wolfBoot_verify_signature_ecc(uint8_t key_slot, ret = wc_ecc_import_unsigned(&ecc, pubkey, pubkey + point_sz, NULL, ECC_KEY_TYPE); if (ret != 0) { + wc_ecc_free(&ecc); return; } @@ -691,6 +693,7 @@ static void wolfBoot_verify_signature_lms(uint8_t key_slot, wolfBoot_printf("error: wc_LmsKey_SetParameters(%d, %d, %d)" \ " returned %d\n", LMS_LEVELS, LMS_HEIGHT, LMS_WINTERNITZ, ret); + wc_LmsKey_Free(&lms); return; } @@ -703,6 +706,7 @@ static void wolfBoot_verify_signature_lms(uint8_t key_slot, /* Something is wrong with the pub key or LMS parameters. */ wolfBoot_printf("error: wc_LmsKey_ImportPubRaw" \ " returned %d\n", ret); + wc_LmsKey_Free(&lms); return; } @@ -2238,7 +2242,37 @@ int wolfBoot_check_flash_image_elf(uint8_t part, unsigned long* entry_out) /* Handle loadable segments */ if (type == ELF_PT_LOAD) { - uintptr_t load_addr = (uintptr_t)(paddr + BASE_OFF); + uint64_t seg_start; + uintptr_t load_addr; + + /* Validate the segment before hashing: the flash-address hash + * reader consumes a uint32_t length, the file layout must stay + * inside the manifest image, and the paddr range must fit the + * destination (uintptr_t) address width so the load_addr cast + * below cannot wrap. Reject instead of continuing. */ + if (filesz > UINT32_MAX) { + wolfBoot_printf("ELF: [CHECK] ERROR: segment file_size " + "%lu does not fit a 32-bit length\n", + (unsigned long)filesz); + return -1; + } + if (offset > (uint64_t)boot.fw_size || + filesz > (uint64_t)boot.fw_size - offset) { + wolfBoot_printf("ELF: [CHECK] ERROR: segment offset %lu + " + "size %lu exceeds image size %u\n", + (unsigned long)offset, + (unsigned long)filesz, boot.fw_size); + return -1; + } + seg_start = paddr + (uint64_t)BASE_OFF; + if (seg_start < paddr || + seg_start > (uint64_t)UINTPTR_MAX - filesz) { + wolfBoot_printf("ELF: [CHECK] ERROR: segment paddr range " + "overflows\n"); + return -1; + } + + load_addr = (uintptr_t)seg_start; /* Feed the loadable parts to the hash function */ wolfBoot_printf("ELF: [CHECK] Hashing loadable segment: " "paddr = 0x%08lx, loadaddr = 0x%08lx, " @@ -2311,14 +2345,6 @@ int wolfBoot_check_flash_image_elf(uint8_t part, unsigned long* entry_out) if (wolfBoot_hardened_CT_compare(exp_digest, calc_digest, WOLFBOOT_SHA_DIGEST_SIZE) != 0) { wolfBoot_printf("ELF: [CHECK] SHA verification FAILED\n"); - wolfBoot_printf( - "ELF: [CHECK] Expected %02x%02x%02x%02x%02x%02x%02x%02x\n", - exp_digest[0], exp_digest[1], exp_digest[2], exp_digest[3], - exp_digest[4], exp_digest[5], exp_digest[6], exp_digest[7]); - wolfBoot_printf( - "ELF: [CHECK] Calculated %02x%02x%02x%02x%02x%02x%02x%02x\n", - calc_digest[0], calc_digest[1], calc_digest[2], calc_digest[3], - calc_digest[4], calc_digest[5], calc_digest[6], calc_digest[7]); return -2; } wolfBoot_printf("ELF: [CHECK] Verification successful\n"); diff --git a/src/libwolfboot.c b/src/libwolfboot.c index 6274550109..c211be6027 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -1374,8 +1374,11 @@ uint16_t wolfBoot_find_header(uint8_t *haystack, uint16_t type, uint8_t **ptr) } len = p[2] | (p[3] << 8); - /* check len */ - if ((4U + len) > (uint16_t)(IMAGE_HEADER_SIZE - IMAGE_HEADER_OFFSET)) { + /* check len (compare in a 32-bit domain: a uint16_t cast of the + * header budget wraps for headers >= 64 KiB and rejects every + * field) */ + if ((uint32_t)(4U + len) > + (uint32_t)(IMAGE_HEADER_SIZE - IMAGE_HEADER_OFFSET)) { unit_dbg("This field is too large (bigger than the space available " "in the current header)\n"); unit_dbg("%u %u %u\n", (unsigned int)len, @@ -1402,7 +1405,7 @@ uint16_t wolfBoot_find_header(uint8_t *haystack, uint16_t type, uint8_t **ptr) #ifdef EXT_FLASH uint8_t hdr_cpy[IMAGE_HEADER_SIZE] XALIGNED(4); -uint32_t hdr_cpy_done = 0; +int hdr_cpy_done = 0; #endif /** @@ -1521,6 +1524,15 @@ static int decrypt_header(uint8_t *src) return 0; } +/* Scrub the decrypted manifest once the fields of interest have been + * extracted, so no plaintext header survives in .bss through the boot + * handoff (same treatment as disk_decrypted_header_clear in + * update_disk.c). */ +static void dec_hdr_clear(void) +{ + ForceZero(dec_hdr, IMAGE_HEADER_SIZE); +} + #endif /** * @brief Get blob version. @@ -1539,6 +1551,7 @@ uint32_t wolfBoot_get_blob_version(uint8_t *blob) uint32_t *volatile version_field = NULL; uint32_t *magic = NULL; uint8_t *img_bin = blob; + uint32_t version = 0; if (blob == NULL) return 0; #if defined(EXT_ENCRYPTED) && defined(MMU) @@ -1551,11 +1564,12 @@ uint32_t wolfBoot_get_blob_version(uint8_t *blob) if (*magic != WOLFBOOT_MAGIC) return 0; if (wolfBoot_find_header(img_bin + IMAGE_HEADER_OFFSET, HDR_VERSION, - (void *)&version_field) != sizeof(uint32_t)) - return 0; - if (version_field) - return im2n(*version_field); - return 0; + (void *)&version_field) == sizeof(uint32_t) && version_field) + version = im2n(*version_field); +#if defined(EXT_ENCRYPTED) && defined(MMU) + dec_hdr_clear(); +#endif + return version; } /** @@ -1574,6 +1588,7 @@ uint16_t wolfBoot_get_blob_type(uint8_t *blob) uint16_t *volatile type_field = NULL; uint32_t *magic = NULL; uint8_t *img_bin = blob; + uint16_t type = 0; #if defined(EXT_ENCRYPTED) && defined(MMU) if (wolfBoot_initialize_encryption() < 0) return 0; @@ -1584,12 +1599,12 @@ uint16_t wolfBoot_get_blob_type(uint8_t *blob) if (*magic != WOLFBOOT_MAGIC) return 0; if (wolfBoot_find_header(img_bin + IMAGE_HEADER_OFFSET, HDR_IMG_TYPE, - (void *)&type_field) != sizeof(uint16_t)) - return 0; - if (type_field) - return im2ns(*type_field); - - return 0; + (void *)&type_field) == sizeof(uint16_t) && type_field) + type = im2ns(*type_field); +#if defined(EXT_ENCRYPTED) && defined(MMU) + dec_hdr_clear(); +#endif + return type; } /** @@ -1611,6 +1626,7 @@ uint32_t wolfBoot_get_blob_diffbase_version(uint8_t *blob) uint32_t *volatile delta_base = NULL; uint32_t *magic = NULL; uint8_t *img_bin = blob; + uint32_t delta_base_ver = 0; #if defined(EXT_ENCRYPTED) && defined(MMU) if (wolfBoot_initialize_encryption() < 0) return 0; @@ -1621,11 +1637,12 @@ uint32_t wolfBoot_get_blob_diffbase_version(uint8_t *blob) if (*magic != WOLFBOOT_MAGIC) return 0; if (wolfBoot_find_header(img_bin + IMAGE_HEADER_OFFSET, HDR_IMG_DELTA_BASE, - (void *)&delta_base) != sizeof(uint32_t)) - return 0; - if (delta_base) - return im2n(*delta_base); - return 0; + (void *)&delta_base) == sizeof(uint32_t) && delta_base) + delta_base_ver = im2n(*delta_base); +#if defined(EXT_ENCRYPTED) && defined(MMU) + dec_hdr_clear(); +#endif + return delta_base_ver; } @@ -1950,6 +1967,7 @@ void RAMFUNCTION wolfBoot_crypto_set_iv(const uint8_t *nonce, uint32_t iv_counte uint8_t local_nonce[ENCRYPT_NONCE_SIZE]; XMEMCPY(local_nonce, nonce, ENCRYPT_NONCE_SIZE); crypto_set_iv(local_nonce, iv_counter + encrypt_iv_offset); + ForceZero(local_nonce, sizeof(local_nonce)); #else (void)nonce; (void)iv_counter; @@ -2313,12 +2331,18 @@ void aes_set_iv(uint8_t *nonce, uint32_t iv_ctr) iv_buf[i] = wb_reverse_word32(iv_buf[i]); } #endif - iv_buf[3] += iv_ctr; - if (iv_buf[3] < iv_ctr) { /* overflow */ + /* Add the block counter with an unconditional, branch-free carry: + * a conditional carry loop's trip count would depend on the nonce + * content and leak it through timing. */ + { + uint32_t carry; + uint32_t old = iv_buf[3]; + iv_buf[3] = old + iv_ctr; + carry = (uint32_t)(iv_buf[3] < old); for (i = 2; i >= 0; i--) { - iv_buf[i]++; - if (iv_buf[i] != 0) - break; + uint32_t prev = iv_buf[i]; + iv_buf[i] = prev + carry; + carry = (uint32_t)(iv_buf[i] < prev); } } #ifndef BIG_ENDIAN_ORDER @@ -2328,6 +2352,7 @@ void aes_set_iv(uint8_t *nonce, uint32_t iv_ctr) #endif wc_AesSetIV(&aes_enc, (byte *)iv_buf); wc_AesSetIV(&aes_dec, (byte *)iv_buf); + ForceZero(iv_buf, sizeof(iv_buf)); } #elif defined(ENCRYPT_PKCS11) @@ -2472,12 +2497,16 @@ void pkcs11_crypto_set_iv(uint8_t *nonce, uint32_t iv_ctr) cb_words[i] = wb_reverse_word32(cb_words[i]); } #endif - cb_words[3] += iv_ctr; - if (cb_words[3] < iv_ctr) { /* overflow */ + /* Unconditional, branch-free carry (see aes_set_iv) */ + { + uint32_t carry; + uint32_t old = cb_words[3]; + cb_words[3] = old + iv_ctr; + carry = (uint32_t)(cb_words[3] < old); for (i = 2; i >= 0; i--) { - cb_words[i]++; - if (cb_words[i] != 0) - break; + uint32_t prev = cb_words[i]; + cb_words[i] = prev + carry; + carry = (uint32_t)(cb_words[i] < prev); } } #ifndef BIG_ENDIAN_ORDER @@ -2621,13 +2650,13 @@ typedef char wolfBoot_encrypt_stage_size_check[ /** * @brief Write encrypted data to an external flash. * - * This function encrypts the provided data using the AES encryption algorithm - * and writes it to the external flash. + * This function encrypts the provided data using the configured external-flash + * encryption cipher (ChaCha20, AES-CTR, or a PKCS#11-backed cipher, per build + * configuration) and writes it to the external flash. * * @param address The address in the external flash to write the data to. * @param data Pointer to the data buffer to be written. * @param len The length of the data to be written. - * @param forcedEnc force writing encryption, used during final swap * * @return int 0 if successful, -1 on failure. */ @@ -2640,6 +2669,10 @@ int RAMFUNCTION ext_flash_encrypt_write(uintptr_t address, const uint8_t *data, int sz = len, i, step, ret; uint8_t part; uint32_t iv_counter = 0; + /* The one-shot IV offset (fallback IV) is consumed by the set_iv below; + * the partial-block re-syncs further down must re-apply the same offset + * or they would re-anchor the stream at the standard-IV position. */ + uint32_t iv_offset_at_entry = 0; #if defined(EXT_ENCRYPTED) && !defined(WOLFBOOT_SMALL_STACK) && \ !defined(NVM_FLASH_WRITEONCE) uint8_t ENCRYPT_CACHE[NVM_CACHE_SIZE] XALIGNED_STACK(32); @@ -2672,6 +2705,7 @@ int RAMFUNCTION ext_flash_encrypt_write(uintptr_t address, const uint8_t *data, } if (wolfBoot_initialize_encryption() < 0) return -1; + iv_offset_at_entry = encrypt_iv_offset; wolfBoot_crypto_set_iv(encrypt_iv_nonce, iv_counter); break; case PART_SWAP: @@ -2689,16 +2723,28 @@ int RAMFUNCTION ext_flash_encrypt_write(uintptr_t address, const uint8_t *data, step = len; if (ext_flash_read(row_address, block, ENCRYPT_BLOCK_SIZE) != ENCRYPT_BLOCK_SIZE) { - return -1; + ret = -1; + goto exit; } + /* The stored block is ciphertext: decrypt it so the untouched bytes + * can be patched as plaintext and re-encrypted. Re-encrypting the + * ciphertext as-is would double-XOR the untouched bytes and store + * them in plaintext. */ + crypto_decrypt(enc_block, block, ENCRYPT_BLOCK_SIZE); + XMEMCPY(block, enc_block, ENCRYPT_BLOCK_SIZE); XMEMCPY(block + row_offset, data, step); + /* The decrypt above consumed keystream on backends that share one + * stream state between encrypt and decrypt; re-sync the stream to + * this block before re-encrypting. */ + encrypt_iv_offset = iv_offset_at_entry; + wolfBoot_crypto_set_iv(encrypt_iv_nonce, iv_counter); crypto_encrypt(enc_block, block, ENCRYPT_BLOCK_SIZE); ret = ext_flash_write(row_address, enc_block, ENCRYPT_BLOCK_SIZE); if (ret < 0) - return ret; + goto exit; /* The request fits entirely within this block: nothing left to do */ if (step == len) - return ret; + goto exit; address += step; data += step; sz = len - step; @@ -2718,7 +2764,7 @@ int RAMFUNCTION ext_flash_encrypt_write(uintptr_t address, const uint8_t *data, } ret = ext_flash_write(address, ENCRYPT_CACHE, chunk); if (ret < 0) - return ret; + goto exit; address += chunk; data += chunk; step -= chunk; @@ -2729,15 +2775,35 @@ int RAMFUNCTION ext_flash_encrypt_write(uintptr_t address, const uint8_t *data, * the same way the unaligned head above is handled. */ step = sz & (ENCRYPT_BLOCK_SIZE - 1); if (step > 0) { + /* "address" is block-aligned here; index of the block being patched */ + uint32_t tail_iv_counter = (address - WOLFBOOT_PARTITION_UPDATE_ADDRESS) / + ENCRYPT_BLOCK_SIZE; if (ext_flash_read(address, block, ENCRYPT_BLOCK_SIZE) != ENCRYPT_BLOCK_SIZE) { - return -1; + ret = -1; + goto exit; } + /* Sync the decrypt context to this block (on backends with separate + * encrypt/decrypt contexts it did not advance with the full-block + * writes above), then decrypt the stored ciphertext so the untouched + * tail bytes survive the re-encryption. */ + encrypt_iv_offset = iv_offset_at_entry; + wolfBoot_crypto_set_iv(encrypt_iv_nonce, tail_iv_counter); + crypto_decrypt(enc_block, block, ENCRYPT_BLOCK_SIZE); + XMEMCPY(block, enc_block, ENCRYPT_BLOCK_SIZE); XMEMCPY(block, data, step); + encrypt_iv_offset = iv_offset_at_entry; + wolfBoot_crypto_set_iv(encrypt_iv_nonce, tail_iv_counter); crypto_encrypt(enc_block, block, ENCRYPT_BLOCK_SIZE); ret = ext_flash_write(address, enc_block, ENCRYPT_BLOCK_SIZE); } +exit: + /* The head/tail RMW paths above decrypted the stored neighbour blocks + * into block/enc_block; scrub the plaintext (and any stale copies) + * on every exit so it does not outlive the write on the stack. */ + ForceZero(block, sizeof(block)); + ForceZero(enc_block, sizeof(enc_block)); return ret; } @@ -2900,6 +2966,9 @@ int wolfBoot_ram_decrypt(uint8_t *src, uint8_t *dst) * unaligned cast, then convert to native byte order. */ XMEMCPY(&len, dec_hdr + sizeof(uint32_t), sizeof(len)); len = im2n(len); + /* The length is the only field taken from the decrypted manifest; + * scrub it before it can outlive this function in .bss. */ + dec_hdr_clear(); #if !defined(WOLFBOOT_FIXED_PARTITIONS) && !defined(WOLFBOOT_RAMBOOT_MAX_SIZE) # error "WOLFBOOT_FIXED_PARTITIONS or WOLFBOOT_RAMBOOT_MAX_SIZE required to bound the RAM load" diff --git a/src/update_disk.c b/src/update_disk.c index edc5799f49..4ee5bad6a0 100644 --- a/src/update_disk.c +++ b/src/update_disk.c @@ -632,10 +632,19 @@ void RAMFUNCTION wolfBoot_start(void) } #endif if (flat_dt != NULL) { - uint8_t *dts_ptr = fit_load_image(fit, flat_dt, (int*)&dts_size); - if (dts_ptr != NULL && wolfBoot_get_dts_size(dts_ptr) >= 0) { - /* relocate to load DTS address */ + uint8_t *dts_ptr = fit_load_image(fit, flat_dt, NULL); + int parsed = (dts_ptr != NULL) + ? wolfBoot_get_dts_size(dts_ptr) : -1; + if (dts_ptr != NULL && + parsed >= (int)WOLFBOOT_DTS_MIN_SIZE && + (uint32_t)parsed <= WOLFBOOT_DTS_MAX_SIZE) { + /* Relocate to the load DTS address. The copy length is + * the parsed DTB size, clamped to WOLFBOOT_DTS_MAX_SIZE, + * not the FIT-declared property length. The staging window + * at WOLFBOOT_LOAD_DTS_ADDRESS must be at least that large + * (or the bound must be overridden for the target). */ dts_addr = (uint8_t*)WOLFBOOT_LOAD_DTS_ADDRESS; + dts_size = (uint32_t)parsed; wolfBoot_printf("Loading DTS: %p -> %p (%d bytes)\n", dts_ptr, dts_addr, dts_size); if (wolfBoot_fit_memcpy(dts_addr, dts_ptr, dts_size) != 0) { diff --git a/src/update_flash.c b/src/update_flash.c index ed9c4185a6..c2a27e48c8 100644 --- a/src/update_flash.c +++ b/src/update_flash.c @@ -526,6 +526,8 @@ static int RAMFUNCTION wolfBoot_swap_and_final_erase(int resume) if ((resume == 1) && (swapDone == 0) && (updateState != IMG_STATE_FINAL_FLAGS) ) { + /* Keep the invariant that every exit scrubs the staging buffer */ + wolfBoot_zeroize(tmpBuffer, sizeof(tmpBuffer)); return -1; } @@ -569,18 +571,20 @@ static int RAMFUNCTION wolfBoot_swap_and_final_erase(int resume) wb_flash_erase(boot, WOLFBOOT_PARTITION_SIZE - eraseLen, eraseLen); #ifdef EXT_ENCRYPTED - /* Initialize encryption with the saved key */ + /* Initialize encryption with the saved key. The default backend + * manages the internal flash lock itself around the key write (it ends + * with the flash locked), so call it with the flash locked and re-unlock + * afterwards for the remaining writes. */ + hal_flash_lock(); ret = wolfBoot_set_encrypt_key((uint8_t*)tmpBuffer, (uint8_t*)&tmpBuffer[ENCRYPT_KEY_SIZE / sizeof(uint32_t)]); if (ret != 0) { #ifdef EXT_FLASH ext_flash_lock(); #endif - hal_flash_lock(); wolfBoot_zeroize(tmpBuffer, sizeof(tmpBuffer)); return ret; } - /* wolfBoot_set_encrypt_key calls hal_flash_unlock, need to unlock again */ hal_flash_unlock(); #endif /* Restore the original contents of the staging sector (with the magic trailer if encrypted) */ diff --git a/src/update_ram.c b/src/update_ram.c index 0dac446536..8b98cbd40b 100644 --- a/src/update_ram.c +++ b/src/update_ram.c @@ -51,17 +51,6 @@ extern void hal_flash_dualbank_swap(void); extern uint32_t kernel_load_addr; extern uint32_t dts_load_addr; -#if defined(MMU) || defined(WOLFBOOT_FDT) -/* Bounds for the attacker-influenced fdt_totalsize before relocating a DTB. - * MIN is the FDT v17 header size (also enforced by the signer): fdt_check_header - * validates magic/version but not totalsize, so a crafted header with a tiny - * totalsize must be rejected rather than loaded/forwarded as a partial tree. */ -#ifndef WOLFBOOT_DTS_MAX_SIZE -#define WOLFBOOT_DTS_MAX_SIZE (1024U * 1024U) -#endif -#define WOLFBOOT_DTS_MIN_SIZE (40U) -#endif - #if defined(__WOLFBOOT) && defined(WOLFBOOT_LOAD_ADDRESS) extern uint8_t _end[]; /* linker symbol: end of wolfBoot BSS */ #endif @@ -655,10 +644,19 @@ void RAMFUNCTION wolfBoot_start(void) } #endif if (flat_dt != NULL) { - uint8_t *dts_ptr = fit_load_image(fit, flat_dt, (int*)&dts_size); - if (dts_ptr != NULL && wolfBoot_get_dts_size(dts_ptr) >= 0) { - /* relocate to load DTS address */ + uint8_t *dts_ptr = fit_load_image(fit, flat_dt, NULL); + int parsed = (dts_ptr != NULL) + ? wolfBoot_get_dts_size(dts_ptr) : -1; + if (dts_ptr != NULL && + parsed >= (int)WOLFBOOT_DTS_MIN_SIZE && + (uint32_t)parsed <= WOLFBOOT_DTS_MAX_SIZE) { + /* Relocate to the load DTS address. The copy length is + * the parsed DTB size, clamped to WOLFBOOT_DTS_MAX_SIZE, + * not the FIT-declared property length. The staging window + * at WOLFBOOT_LOAD_DTS_ADDRESS must be at least that large + * (or the bound must be overridden for the target). */ dts_addr = (uint8_t*)WOLFBOOT_LOAD_DTS_ADDRESS; + dts_size = (uint32_t)parsed; wolfBoot_printf("Loading DTS: %p -> %p (%d bytes)\n", dts_ptr, dts_addr, dts_size); memcpy(dts_addr, dts_ptr, dts_size); diff --git a/tools/keytools/sign.c b/tools/keytools/sign.c index 01cc242753..103fdd34cc 100644 --- a/tools/keytools/sign.c +++ b/tools/keytools/sign.c @@ -458,8 +458,11 @@ static uint16_t sign_tool_find_header(uint8_t *haystack, uint16_t type, uint8_t } len = p[2] | (p[3] << 8); - /* check len */ - if ((4 + len) > (uint16_t)(CMD.header_sz - IMAGE_HEADER_OFFSET)) { + /* check len (compare in a 32-bit domain: a uint16_t cast of the + * header budget wraps for headers >= 64 KiB and rejects every + * field) */ + if ((uint32_t)(4 + len) > + (uint32_t)(CMD.header_sz - IMAGE_HEADER_OFFSET)) { fprintf(stderr, "This field too large to fit into header " "(%d > %d)\n", (int)(4 + len), (int)(CMD.header_sz - IMAGE_HEADER_OFFSET)); diff --git a/tools/test.mk b/tools/test.mk index ff05573205..4080c3f4f4 100644 --- a/tools/test.mk +++ b/tools/test.mk @@ -1228,31 +1228,31 @@ test-all: clean test-size-all: - make test-size SIGN=NONE LIMIT=5112 NO_ARM_ASM=1 + make test-size SIGN=NONE LIMIT=5116 NO_ARM_ASM=1 make keysclean - make test-size SIGN=ED25519 LIMIT=12224 NO_ARM_ASM=1 + make test-size SIGN=ED25519 LIMIT=12228 NO_ARM_ASM=1 make keysclean - make test-size SIGN=ECC256 LIMIT=18920 NO_ARM_ASM=1 + make test-size SIGN=ECC256 LIMIT=18924 NO_ARM_ASM=1 make clean - make test-size SIGN=ECC256 NO_ASM=1 LIMIT=13952 NO_ARM_ASM=1 + make test-size SIGN=ECC256 NO_ASM=1 LIMIT=13956 NO_ARM_ASM=1 make keysclean make test-size SIGN=RSA2048 LIMIT=11808 NO_ARM_ASM=1 make clean - make test-size SIGN=RSA2048 NO_ASM=1 LIMIT=12368 NO_ARM_ASM=1 + make test-size SIGN=RSA2048 NO_ASM=1 LIMIT=12372 NO_ARM_ASM=1 make keysclean make test-size SIGN=RSA4096 LIMIT=12108 NO_ARM_ASM=1 make clean - make test-size SIGN=RSA4096 NO_ASM=1 LIMIT=12648 NO_ARM_ASM=1 + make test-size SIGN=RSA4096 NO_ASM=1 LIMIT=12652 NO_ARM_ASM=1 make keysclean - make test-size SIGN=ECC384 LIMIT=19604 NO_ARM_ASM=1 + make test-size SIGN=ECC384 LIMIT=19608 NO_ARM_ASM=1 make clean - make test-size SIGN=ECC384 NO_ASM=1 LIMIT=15300 NO_ARM_ASM=1 + make test-size SIGN=ECC384 NO_ASM=1 LIMIT=15316 NO_ARM_ASM=1 make keysclean - make test-size SIGN=ED448 LIMIT=14252 NO_ARM_ASM=1 + make test-size SIGN=ED448 LIMIT=14256 NO_ARM_ASM=1 make keysclean make test-size SIGN=RSA3072 LIMIT=11948 NO_ARM_ASM=1 make clean - make test-size SIGN=RSA3072 NO_ASM=1 LIMIT=12476 NO_ARM_ASM=1 + make test-size SIGN=RSA3072 NO_ASM=1 LIMIT=12480 NO_ARM_ASM=1 make keysclean make test-size SIGN=RSAPSS2048 LIMIT=13744 NO_ARM_ASM=1 make clean @@ -1268,12 +1268,12 @@ test-size-all: make keysclean make test-size SIGN=LMS LMS_LEVELS=2 LMS_HEIGHT=5 LMS_WINTERNITZ=8 \ WOLFBOOT_SMALL_STACK=0 IMAGE_SIGNATURE_SIZE=2644 \ - IMAGE_HEADER_SIZE?=5288 LIMIT=8116 NO_ARM_ASM=1 + IMAGE_HEADER_SIZE?=5288 LIMIT=8120 NO_ARM_ASM=1 make keysclean make test-size SIGN=XMSS XMSS_PARAMS='XMSS-SHA2_10_256' \ IMAGE_SIGNATURE_SIZE=2500 IMAGE_HEADER_SIZE?=4096 \ LIMIT=8768 NO_ARM_ASM=1 make keysclean make clean - make test-size SIGN=ML_DSA ML_DSA_LEVEL=2 LIMIT=19578 \ + make test-size SIGN=ML_DSA ML_DSA_LEVEL=2 LIMIT=19582 \ IMAGE_SIGNATURE_SIZE=2420 IMAGE_HEADER_SIZE?=8192 diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index e94410262c..c4cbfc6cc5 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -53,7 +53,8 @@ endif -TESTS:=unit-parser unit-fdt unit-extflash unit-string unit-spi-flash unit-aes128 \ +TESTS:=unit-parser unit-parser-large-header unit-fdt unit-extflash unit-string \ + unit-spi-flash unit-aes128 \ unit-uart-flash \ unit-aes256 unit-chacha20 unit-pci unit-mock-state unit-sectorflags \ unit-max-space \ @@ -61,7 +62,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-uboot 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-wolfhsm_flash_hal unit-disk \ + unit-update-flash-enc unit-update-flash-enc-full unit-update-ram unit-update-ram-uboot 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-wolfhsm_flash_hal unit-disk \ unit-update-disk unit-update-disk-oob unit-update-disk-fit 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-image-dts \ unit-image-dts-sha384 unit-image-dts-sha3-384 unit-store-sbrk \ @@ -150,8 +151,9 @@ ENABLE_32BIT_TESTS ?= $(HAVE_M32) ifeq ($(ENABLE_32BIT_TESTS),1) TESTS+=unit-linux-loader-e820 TESTS+=unit-linux-loader-syssize +TESTS+=unit-sama5d3-ext-read else -$(info Skipping 32-bit x86 linux-loader unit tests: 'gcc -m32' unavailable (set ENABLE_32BIT_TESTS=1 to force)) +$(info Skipping 32-bit x86 unit tests (linux-loader, sama5d3-ext-read): 'gcc -m32' unavailable (set ENABLE_32BIT_TESTS=1 to force)) endif include unit-sign-encrypted-output.mkfrag @@ -194,6 +196,7 @@ unit-aes128:CFLAGS+=-DEXT_ENCRYPTED -DENCRYPT_WITH_AES128 unit-aes256:CFLAGS+=-DEXT_ENCRYPTED -DENCRYPT_WITH_AES256 unit-chacha20:CFLAGS+=-DEXT_ENCRYPTED -DENCRYPT_WITH_CHACHA unit-parser:CFLAGS+=-DNVM_FLASH_WRITEONCE +unit-parser-large-header:CFLAGS+=-DNVM_FLASH_WRITEONCE unit-fdt:CFLAGS+=-DWOLFBOOT_FDT unit-nvm:CFLAGS+=-DNVM_FLASH_WRITEONCE -DMOCK_PARTITIONS unit-nvm-flagshome:CFLAGS+=-DNVM_FLASH_WRITEONCE -DMOCK_PARTITIONS -DFLAGS_HOME @@ -302,6 +305,9 @@ unit-extflash.o: FORCE unit-parser: ../../include/target.h unit-parser.c gcc -o $@ $^ $(CFLAGS) $(LDFLAGS) +unit-parser-large-header: ../../include/target.h unit-parser-large-header.c + gcc -o $@ $^ $(CFLAGS) $(LDFLAGS) + unit-fdt: ../../include/target.h unit-fdt.c ../../src/fdt.c gcc -o $@ $^ $(CFLAGS) -ffunction-sections -fdata-sections $(LDFLAGS) \ -Wl,--gc-sections @@ -706,6 +712,20 @@ unit-update-flash-enc: ../../include/target.h unit-update-flash.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/chacha.c \ $(CFLAGS) $(LDFLAGS) +# Same encrypted target without UNIT_TEST_FALLBACK_ONLY, so the full +# end-to-end suite (forward updates, rollback, ...) runs against the +# encrypted swap and its IV derivation. +unit-update-flash-enc-full:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN -DUNIT_TEST_AUTH \ + -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED -DEXT_FLASH -DPART_UPDATE_EXT \ + -DPART_SWAP_EXT -DEXT_ENCRYPTED -DENCRYPT_WITH_CHACHA -DHAVE_CHACHA \ + -DCUSTOM_ENCRYPT_KEY \ + -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE +unit-update-flash-enc-full: ../../include/target.h unit-update-flash.c + gcc -o $@ unit-update-flash.c ../../src/image.c \ + $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c \ + $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/chacha.c \ + $(CFLAGS) $(LDFLAGS) + 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) @@ -837,6 +857,20 @@ zynq_write_extract.h: ../../hal/zynq.c unit-zynq-ext-write: unit-zynq-ext-write.c zynq_write_extract.h gcc -o $@ unit-zynq-ext-write.c $(CFLAGS) $(LDFLAGS) +# unit-sama5d3-ext-read runs the real ext_flash_read() from hal/sama5d3.c +# (the intra-page offset of the start address was never applied, sub-page +# reads were copied in 32-bit words, and a multi-page read ending +# mid-page wrote a full page past the caller's buffer). The function and +# the nand_flash geometry struct are extracted verbatim; the test provides +# host div_u()/mod() and emulated nand_read_page()/nand_check_bad_block() +# backed by a byte array. +sama5d3_read_extract.h: ../../hal/sama5d3.c + sed -n '/^struct nand_flash {/,/^} nand_flash = { 0 };/p' $< > $@ + sed -n '/^int ext_flash_read(/,/^}/p' $< >> $@ + +unit-sama5d3-ext-read: unit-sama5d3-ext-read.c sama5d3_read_extract.h + gcc -o $@ unit-sama5d3-ext-read.c $(CFLAGS) $(LDFLAGS) + # unit-versal-qspi-dma drives the real DMA RX path of qspi_transfer() in # hal/versal.c (an unaligned read larger than the 4096-byte # temp buffer copied the full requested length out of the buffer). The diff --git a/tools/unit-tests/unit-extflash.c b/tools/unit-tests/unit-extflash.c index 3fdf22d20b..aae92db6f0 100644 --- a/tools/unit-tests/unit-extflash.c +++ b/tools/unit-tests/unit-extflash.c @@ -373,6 +373,168 @@ START_TEST(test_ext_enc_flash_oversized_write) { } END_TEST +/* A mid-block patch must not destroy the untouched bytes of a block that + * already holds encrypted content: the read-modify-write has to decrypt the + * stored block, splice the patch in, and re-encrypt. Re-encrypting the + * ciphertext as-is double-XORs the untouched bytes and stores plaintext, + * which then reads back as raw ciphertext. */ +START_TEST(test_ext_enc_flash_rmw_head_preserves_neighbors) { + uint32_t address = 0x1000; + uint8_t block_a[TEST_BLOCK_SIZE]; + uint8_t block_p[TEST_BLOCK_SIZE]; + uint8_t expect[TEST_BLOCK_SIZE]; + uint8_t data[TEST_BLOCK_SIZE]; + const int off = 4, len = 8; + int i, rres, wres; + + for (i = 0; i < TEST_BLOCK_SIZE; i++) { + block_a[i] = (uint8_t)(0xA0 + i); + block_p[i] = (uint8_t)(0xB0 + i); + } + + /* Prime the block with known content */ + wres = ext_flash_check_write(address, block_a, TEST_BLOCK_SIZE); + ck_assert_int_eq(wres, 0); + + /* Patch an unaligned mid-block subrange */ + wres = ext_flash_check_write(address + off, block_p, len); + ck_assert_int_eq(wres, 0); + + memcpy(expect, block_a, TEST_BLOCK_SIZE); + memcpy(expect + off, block_p, len); + rres = ext_flash_check_read(address, data, TEST_BLOCK_SIZE); + ck_assert_int_eq(rres, TEST_BLOCK_SIZE); + ck_assert_mem_eq(data, expect, TEST_BLOCK_SIZE); +} +END_TEST + +/* A write whose trailing partial block lands on a block that already holds + * encrypted content: the untouched tail of that block must survive. */ +START_TEST(test_ext_enc_flash_rmw_tail_preserves_neighbors) { + uint32_t address = 0x1000; + uint8_t block_b[TEST_BLOCK_SIZE]; + uint8_t payload[2 * TEST_BLOCK_SIZE]; + uint8_t expect[2 * TEST_BLOCK_SIZE]; + uint8_t data[2 * TEST_BLOCK_SIZE]; + const int tail = 8; + int i, rres, wres; + + for (i = 0; i < TEST_BLOCK_SIZE; i++) { + block_b[i] = (uint8_t)(0xC0 + i); + payload[i] = (uint8_t)(0xD0 + i); + payload[TEST_BLOCK_SIZE + i] = (uint8_t)(0xE0 + i); + } + + /* Two primed blocks */ + wres = ext_flash_check_write(address, payload, TEST_BLOCK_SIZE); + ck_assert_int_eq(wres, 0); + wres = ext_flash_check_write(address + TEST_BLOCK_SIZE, block_b, + TEST_BLOCK_SIZE); + ck_assert_int_eq(wres, 0); + + /* Aligned write of one full block plus a partial tail into block two */ + wres = ext_flash_check_write(address, payload, TEST_BLOCK_SIZE + tail); + ck_assert_int_eq(wres, 0); + + /* Block one is fully replaced; block two holds the spliced tail and the + * original bytes beyond it. */ + memcpy(expect, payload, TEST_BLOCK_SIZE + tail); + memcpy(expect + TEST_BLOCK_SIZE + tail, block_b + tail, + TEST_BLOCK_SIZE - tail); + rres = ext_flash_check_read(address, data, 2 * TEST_BLOCK_SIZE); + ck_assert_int_eq(rres, 2 * TEST_BLOCK_SIZE); + ck_assert_mem_eq(data, expect, 2 * TEST_BLOCK_SIZE); +} +END_TEST + +/* A fallback-IV write whose head and tail partial blocks land on blocks + * that already hold encrypted content: the RMW re-syncs must re-apply the + * one-shot IV offset (wolfBoot_crypto_set_iv consumes it), or the partial + * blocks are re-anchored at the standard-IV position and only those blocks + * read back wrong under the forced fallback-IV read. */ +#ifdef EXT_ENCRYPTED +START_TEST(test_ext_enc_flash_rmw_fallback_iv_preserves_neighbors) { + uint32_t address = 0x1000; + uint8_t block_a[TEST_BLOCK_SIZE]; + uint8_t block_b[TEST_BLOCK_SIZE]; + uint8_t payload[TEST_BLOCK_SIZE + 7]; + uint8_t expect[2 * TEST_BLOCK_SIZE]; + uint8_t data[2 * TEST_BLOCK_SIZE]; + const int off = 4; + const int head_len = TEST_BLOCK_SIZE - off; + const int tail_len = 7 + off; /* len - head_len, block-size independent */ + const int len = TEST_BLOCK_SIZE + 7; + int prevf, i, rres, wres; + + for (i = 0; i < TEST_BLOCK_SIZE; i++) { + block_a[i] = (uint8_t)(0xF0 + i); + block_b[i] = (uint8_t)(0xF8 + i); + } + for (i = 0; i < (int)sizeof(payload); i++) + payload[i] = (uint8_t)(0x0F + i); + + /* Prime both blocks under the fallback IV (aligned full-block writes; + * the offset is one-shot, so re-enable it before every write). */ + wolfBoot_enable_fallback_iv(1); + wres = ext_flash_check_write(address, block_a, TEST_BLOCK_SIZE); + ck_assert_int_eq(wres, 0); + wolfBoot_enable_fallback_iv(1); + wres = ext_flash_check_write(address + TEST_BLOCK_SIZE, block_b, + TEST_BLOCK_SIZE); + ck_assert_int_eq(wres, 0); + + /* One unaligned write: head RMW in block zero, tail RMW in block one */ + wolfBoot_enable_fallback_iv(1); + wres = ext_flash_check_write(address + off, payload, len); + ck_assert_int_eq(wres, 0); + + memcpy(expect, block_a, off); + memcpy(expect + off, payload, head_len); + memcpy(expect + TEST_BLOCK_SIZE, payload + head_len, tail_len); + memcpy(expect + TEST_BLOCK_SIZE + tail_len, block_b + tail_len, + TEST_BLOCK_SIZE - tail_len); + + /* The update flow reads a fallback image with the fallback IV forced */ + prevf = wolfBoot_force_fallback_iv(1); + rres = ext_flash_check_read(address, data, 2 * TEST_BLOCK_SIZE); + wolfBoot_force_fallback_iv(prevf); + ck_assert_int_eq(rres, 2 * TEST_BLOCK_SIZE); + ck_assert_mem_eq(data, expect, 2 * TEST_BLOCK_SIZE); +} +END_TEST +#endif /* EXT_ENCRYPTED */ + +/* A stream written in small, unaligned chunks must round-trip: every chunk + * boundary exercises the partial-block read-modify-write against content the + * previous chunks already encrypted. */ +START_TEST(test_ext_enc_flash_chunked_stream_roundtrip) { + uint32_t address = 0x1000; + const uint32_t total = 3 * TEST_BLOCK_SIZE + 7; + static uint8_t payload[3 * TEST_BLOCK_SIZE + 7]; + static uint8_t data[3 * TEST_BLOCK_SIZE + 7]; + uint32_t written = 0; + int i, rres, wres; + + for (i = 0; i < (int)total; i++) + payload[i] = (uint8_t)(i * 7 + 3); + + while (written < total) { + uint32_t chunk = total - written; + if (chunk > 13) + chunk = 13; + wres = ext_flash_check_write(address + written, payload + written, + chunk); + ck_assert_int_eq(wres, 0); + written += chunk; + } + + memset(data, 0xA5, sizeof(data)); + rres = ext_flash_check_read(address, data, total); + ck_assert_int_eq(rres, (int)total); + ck_assert_mem_eq(data, payload, total); +} +END_TEST + Suite *wolfboot_suite(void) { @@ -386,6 +548,7 @@ Suite *wolfboot_suite(void) TCase *ext_enc_flash_short_read = tcase_create("External encrypted flash short unaligned read"); TCase *ext_enc_flash_short_write = tcase_create("External encrypted flash short unaligned write"); TCase *ext_enc_flash_oversized_write = tcase_create("External encrypted flash oversized write"); + TCase *ext_enc_flash_rmw = tcase_create("External encrypted flash RMW neighbour preservation"); /* Set parameters + add to suite */ tcase_add_test(ext_flash_operations, test_ext_flash_operations); @@ -396,17 +559,29 @@ Suite *wolfboot_suite(void) test_ext_enc_flash_short_unaligned_write); tcase_add_test(ext_enc_flash_oversized_write, test_ext_enc_flash_oversized_write); + tcase_add_test(ext_enc_flash_rmw, + test_ext_enc_flash_rmw_head_preserves_neighbors); + tcase_add_test(ext_enc_flash_rmw, + test_ext_enc_flash_rmw_tail_preserves_neighbors); +#ifdef EXT_ENCRYPTED + tcase_add_test(ext_enc_flash_rmw, + test_ext_enc_flash_rmw_fallback_iv_preserves_neighbors); +#endif + tcase_add_test(ext_enc_flash_rmw, + test_ext_enc_flash_chunked_stream_roundtrip); tcase_set_timeout(ext_flash_operations, 20); tcase_set_timeout(ext_enc_flash_operations, 20); tcase_set_timeout(ext_enc_flash_short_read, 20); tcase_set_timeout(ext_enc_flash_short_write, 20); tcase_set_timeout(ext_enc_flash_oversized_write, 20); + tcase_set_timeout(ext_enc_flash_rmw, 20); suite_add_tcase(s, ext_flash_operations); suite_add_tcase(s, ext_enc_flash_operations); suite_add_tcase(s, ext_enc_flash_short_read); suite_add_tcase(s, ext_enc_flash_short_write); suite_add_tcase(s, ext_enc_flash_oversized_write); + suite_add_tcase(s, ext_enc_flash_rmw); return s; } diff --git a/tools/unit-tests/unit-fdt.c b/tools/unit-tests/unit-fdt.c index 2f8ba163d5..5f05b2417f 100644 --- a/tools/unit-tests/unit-fdt.c +++ b/tools/unit-tests/unit-fdt.c @@ -43,8 +43,12 @@ START_TEST(test_fdt_get_string_rejects_out_of_range_offset) const char *s; memset(&blob, 0, sizeof(blob)); + fdt_set_totalsize(&blob, sizeof(blob.hdr) + sizeof(blob.strings)); fdt_set_off_dt_strings(&blob, sizeof(blob.hdr)); fdt_set_size_dt_strings(&blob, sizeof(blob.strings)); + fdt_set_magic(&blob, FDT_MAGIC); + fdt_set_version(&blob, 17); + fdt_set_last_comp_version(&blob, 16); memcpy(blob.strings, "chosen", sizeof("chosen")); blob.after[0] = 'X'; blob.after[1] = '\0'; @@ -66,8 +70,12 @@ START_TEST(test_fdt_get_string_returns_string_with_valid_offset) const char *s; memset(&blob, 0, sizeof(blob)); + fdt_set_totalsize(&blob, sizeof(blob.hdr) + sizeof(blob.strings)); fdt_set_off_dt_strings(&blob, sizeof(blob.hdr)); fdt_set_size_dt_strings(&blob, sizeof(blob.strings)); + fdt_set_magic(&blob, FDT_MAGIC); + fdt_set_version(&blob, 17); + fdt_set_last_comp_version(&blob, 16); memcpy(blob.strings, "serial\0console\0", 15); s = fdt_get_string(&blob, 7, &len); @@ -203,6 +211,202 @@ START_TEST(test_fdt_node_offset_by_compatible_terminates_on_unterminated_prop) } END_TEST +/* Build a minimal FDT in buf (zero-initialized, so padding bytes are + * 0x00): root node with a `compatible` property whose raw value is + * `len` bytes at `val`. */ +static void build_compat_fdt(uint8_t *buf, size_t size, + const uint8_t *val, uint32_t len) +{ + struct fdt_header *hdr; + uint32_t *s; + uint32_t val_aligned = (len + 3u) & ~3u; + uint32_t struct_off = 0x40; + uint32_t strings_off = 0x80; + + memset(buf, 0, size); + + hdr = (struct fdt_header *)buf; + hdr->magic = fdt32_to_cpu(FDT_MAGIC); + fdt_set_totalsize(hdr, 0x100); + fdt_set_off_dt_struct(hdr, struct_off); + fdt_set_off_dt_strings(hdr, strings_off); + fdt_set_off_mem_rsvmap(hdr, 0x28); + fdt_set_version(hdr, 17); + fdt_set_last_comp_version(hdr, 16); + fdt_set_size_dt_struct(hdr, + 8u + 12u + val_aligned + 4u + 4u); /* node hdr, prop, end, FDT_END */ + fdt_set_size_dt_strings(hdr, sizeof("compatible")); + memcpy(buf + strings_off, "compatible", sizeof("compatible")); + + s = (uint32_t *)(buf + struct_off); + s[0] = fdt32_to_cpu(FDT_BEGIN_NODE); /* root */ + s[1] = 0; /* empty name */ + s[2] = fdt32_to_cpu(FDT_PROP); + s[3] = fdt32_to_cpu(len); + s[4] = fdt32_to_cpu(0); /* nameoff of "compatible" */ + memcpy(buf + struct_off + 8 + 12, val, len); + s[5 + val_aligned / 4u] = fdt32_to_cpu(FDT_END_NODE); + s[6 + val_aligned / 4u] = fdt32_to_cpu(FDT_END); +} + +/* A compatible entry whose declared length equals the search string + * (no room for a NUL) must not match: before the fix the comparison + * read one byte past the declared length (the 4-byte alignment + * padding, zero here) as if it were the terminator and accepted the + * entry. */ +START_TEST(test_fdt_compatible_unterminated_exact_len_no_match) +{ + static uint8_t buf[0x100]; + int off; + + build_compat_fdt(buf, sizeof(buf), (const uint8_t *)"abc", 3); + + off = fdt_node_offset_by_compatible(buf, -1, "abc"); + ck_assert_int_lt(off, 0); +} +END_TEST + +/* A properly terminated entry of exactly the search length matches. */ +START_TEST(test_fdt_compatible_terminated_exact_len_match) +{ + static uint8_t buf[0x100]; + int off; + + build_compat_fdt(buf, sizeof(buf), (const uint8_t *)"abc\0", 4); + + off = fdt_node_offset_by_compatible(buf, -1, "abc"); + ck_assert_int_eq(off, 0); /* the root node */ +} +END_TEST + +/* Multi-string compatible lists keep working: the second entry + * matches. */ +START_TEST(test_fdt_compatible_multi_string_list_match) +{ + static uint8_t buf[0x100]; + int off; + + build_compat_fdt(buf, sizeof(buf), (const uint8_t *)"xy\0abc\0", 8); + + off = fdt_node_offset_by_compatible(buf, -1, "abc"); + ck_assert_int_eq(off, 0); +} +END_TEST + +/* A terminated entry that merely starts with the search string does + * not match (the entry is longer). */ +START_TEST(test_fdt_compatible_prefix_entry_no_match) +{ + static uint8_t buf[0x100]; + int off; + + build_compat_fdt(buf, sizeof(buf), (const uint8_t *)"abcd\0", 5); + + off = fdt_node_offset_by_compatible(buf, -1, "abc"); + ck_assert_int_lt(off, 0); +} +END_TEST + +/* A finalized DTB whose string table lies past the declared end of + * the blob must be rejected: before the fix fdt_check_header() + * validated only magic and version, and fdt_get_string() formed the + * string-table pointer from the unvalidated header fields. */ +START_TEST(test_fdt_check_header_rejects_unbounded_string_area) +{ + static uint8_t buf[0x100]; + int len = 0; + const char *s; + + build_compat_fdt(buf, sizeof(buf), (const uint8_t *)"abc\0", 4); + /* push the string table past the declared end of the blob */ + fdt_set_off_dt_strings((struct fdt_header *)buf, 0x100); + + ck_assert_int_eq(fdt_check_header(buf), -FDT_ERR_BADSTRUCTURE); + + s = fdt_get_string(buf, 0, &len); + ck_assert_ptr_null(s); + ck_assert_int_lt(len, 0); +} +END_TEST + +/* The structure block must not overlap the string table. */ +START_TEST(test_fdt_check_header_rejects_overlapping_areas) +{ + static uint8_t buf[0x100]; + + build_compat_fdt(buf, sizeof(buf), (const uint8_t *)"abc\0", 4); + fdt_set_size_dt_struct((struct fdt_header *)buf, 0x100); + + ck_assert_int_eq(fdt_check_header(buf), -FDT_ERR_BADSTRUCTURE); +} +END_TEST + +/* FIT whose configuration `kernel` property is not NUL-terminated + * within its declared length: fit_find_images() must still honor the + * valid `default` but reject the malformed image name instead of + * passing it on as a C string. */ +static const uint8_t fit_cfg_unterminated_kernel[] = { + /* header */ + 0xd0, 0x0d, 0xfe, 0xed, /* magic */ + 0x00, 0x00, 0x00, 0xa7, /* totalsize = 167 */ + 0x00, 0x00, 0x00, 0x38, /* off_dt_struct = 56 */ + 0x00, 0x00, 0x00, 0x98, /* off_dt_strings = 152 */ + 0x00, 0x00, 0x00, 0x28, /* off_mem_rsvmap = 40 */ + 0x00, 0x00, 0x00, 0x11, /* version = 17 */ + 0x00, 0x00, 0x00, 0x10, /* last_comp_version = 16 */ + 0x00, 0x00, 0x00, 0x00, /* boot_cpuid_phys */ + 0x00, 0x00, 0x00, 0x0f, /* size_dt_strings = 15 */ + 0x00, 0x00, 0x00, 0x60, /* size_dt_struct = 96 */ + /* mem_rsvmap terminator (offset 40) */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* struct block (offset 56) */ + 0x00, 0x00, 0x00, 0x01, /* BEGIN_NODE root */ + 0x00, 0x00, 0x00, 0x00, /* "" */ + 0x00, 0x00, 0x00, 0x01, /* BEGIN_NODE */ + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x00, 0x00, /* "configurations\0" */ + 0x00, 0x00, 0x00, 0x03, /* FDT_PROP */ + 0x00, 0x00, 0x00, 0x07, /* len = 7 */ + 0x00, 0x00, 0x00, 0x00, /* nameoff = 0 ("default") */ + 0x63, 0x6f, 0x6e, 0x66, 0x2d, 0x31, 0x00, 0x00, /* "conf-1\0" */ + 0x00, 0x00, 0x00, 0x01, /* BEGIN_NODE */ + 0x63, 0x6f, 0x6e, 0x66, 0x2d, 0x31, 0x00, 0x00, /* "conf-1\0" */ + 0x00, 0x00, 0x00, 0x03, /* FDT_PROP */ + 0x00, 0x00, 0x00, 0x08, /* len = 8 */ + 0x00, 0x00, 0x00, 0x08, /* nameoff = 8 ("kernel") */ + 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x2d, 0x31, /* "kernel-1" -- no NUL */ + 0x00, 0x00, 0x00, 0x02, /* END_NODE conf-1 */ + 0x00, 0x00, 0x00, 0x02, /* END_NODE configurations */ + 0x00, 0x00, 0x00, 0x02, /* END_NODE root */ + 0x00, 0x00, 0x00, 0x09, /* FDT_END */ + /* strings block (offset 152) */ + 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x00, /* "default\0" */ + 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x00, /* "kernel\0" */ +}; + +START_TEST(test_fit_find_images_rejects_unterminated_image_name) +{ + static uint8_t fit_scratch[sizeof(fit_cfg_unterminated_kernel)]; + const char *conf = NULL, *kern = NULL, *fdt = NULL; + const char *rd = NULL, *fpga = NULL; + + memcpy(fit_scratch, fit_cfg_unterminated_kernel, sizeof(fit_scratch)); + + conf = fit_find_images(fit_scratch, &kern, &fdt, &rd, &fpga); + + /* The valid `default` is still honored... */ + ck_assert_str_eq(conf, "conf-1"); + /* ...but the config's `kernel` property is not NUL-terminated + * within its declared length, so it must be rejected rather than + * passed on to fdt_find_node_offset()/strlen(). */ + ck_assert_ptr_null(kern); + ck_assert_ptr_null(fdt); + ck_assert_ptr_null(rd); + ck_assert_ptr_null(fpga); +} +END_TEST + static Suite *fdt_suite(void) { Suite *s = suite_create("fdt"); @@ -215,6 +419,13 @@ static Suite *fdt_suite(void) tcase_add_test(tc, test_fdt_get_string_returns_string_with_valid_offset); tcase_add_test(tc, test_fit_load_image_rejects_oversized_prop_len); tcase_add_test(tc, test_fdt_shrink_rejects_dt_strings_area_overflow); + tcase_add_test(tc, test_fdt_compatible_unterminated_exact_len_no_match); + tcase_add_test(tc, test_fdt_compatible_terminated_exact_len_match); + tcase_add_test(tc, test_fdt_compatible_multi_string_list_match); + tcase_add_test(tc, test_fdt_compatible_prefix_entry_no_match); + tcase_add_test(tc, test_fdt_check_header_rejects_unbounded_string_area); + tcase_add_test(tc, test_fdt_check_header_rejects_overlapping_areas); + tcase_add_test(tc, test_fit_find_images_rejects_unterminated_image_name); suite_add_tcase(s, tc); tcase_set_timeout(tc_dos, 5); diff --git a/tools/unit-tests/unit-fit-gzip.c b/tools/unit-tests/unit-fit-gzip.c index 373a4be81d..7c175fdacc 100644 --- a/tools/unit-tests/unit-fit-gzip.c +++ b/tools/unit-tests/unit-fit-gzip.c @@ -269,6 +269,44 @@ END_TEST #endif /* WOLFBOOT_GZIP */ +START_TEST(test_fit_to_none_unterminated_fails_closed) +{ + /* The `compression` property is truncated to 4 bytes ("none" with + * no NUL): the declared length no longer covers the terminator. + * Before the fix the value was strcmp()'d and the following byte + * (the dropped NUL) terminated it, so the subimage was passed + * through as raw; a malformed value must fail closed. */ + uint8_t buf[64]; + int len = -1; + void *ret; + static uint8_t fit_scratch[sizeof(fit_with_none_comp)]; + uint8_t *p; + unsigned i; + + memcpy(fit_scratch, fit_with_none_comp, sizeof(fit_scratch)); + + /* the "none" value appears once in the blob (not as a C string - + * scan raw bytes); the property length word sits 8 bytes before + * the data */ + p = NULL; + for (i = 0; i + 4 <= sizeof(fit_scratch); i++) { + if (memcmp(fit_scratch + i, "none", 4) == 0) { + p = fit_scratch + i; + break; + } + } + ck_assert_ptr_nonnull(p); + p[-8] = 0; + p[-7] = 0; + p[-6] = 0; + p[-5] = 4; + + ret = fit_load_image_to(fit_scratch, "kernel-1", + buf, (uint32_t)sizeof(buf), &len); + ck_assert_ptr_null(ret); +} +END_TEST + START_TEST(test_fit_to_lzma_unknown_returns_null) { /* Independent of WOLFBOOT_GZIP - any unknown compression scheme is @@ -320,6 +358,7 @@ static Suite *fit_gzip_suite(void) tcase_add_test(tc, test_fit_to_gzip_disabled_returns_null); #endif tcase_add_test(tc, test_fit_to_lzma_unknown_returns_null); + tcase_add_test(tc, test_fit_to_none_unterminated_fails_closed); tcase_add_test(tc, test_fit_to_none_oversized_rejected); suite_add_tcase(s, tc); diff --git a/tools/unit-tests/unit-image-elf-scatter.c b/tools/unit-tests/unit-image-elf-scatter.c index ad3d1c23dc..c5c6510362 100644 --- a/tools/unit-tests/unit-image-elf-scatter.c +++ b/tools/unit-tests/unit-image-elf-scatter.c @@ -258,6 +258,74 @@ static void patch_expected_digest(const uint8_t *digest) memcpy(manifest + 12, digest, WOLFBOOT_SHA_DIGEST_SIZE); } +/* --- Multi-segment fixtures for the PT_LOAD bounds-rejection tests --- + * + * The manifest holds image header + ELF header + N program headers + * (tightly packed). Each segment's flash-resident payload lives in its + * own static array referenced by ph.paddr. */ +#define SEG1_SIZE 0x2000U +#define SEG2_SIZE 64U + +static uint8_t seg1_flash[SEG1_SIZE]; +static uint8_t seg2_flash[SEG2_SIZE]; + +struct seg_spec { + uint64_t offset; + uint64_t filesz; + uint64_t paddr; + uint8_t *payload; /* pattern-filled for fillsz bytes */ + uint32_t fillsz; +}; + +static void build_scattered_image_n(const struct seg_spec *segs, unsigned n, + uint32_t fw_size) +{ + uint8_t *manifest = (uint8_t *)(uintptr_t)MOCK_ADDRESS_BOOT; + uint32_t magic = WOLFBOOT_MAGIC; + size_t pht_sz = sizeof(elf64_header) + n * sizeof(elf64_program_header); + elf64_header *eh; + elf64_program_header *ph; + unsigned i, j; + + memset(manifest, 0, IMAGE_HEADER_SIZE + pht_sz); + + /* manifest header with a zeroed HDR_HASH TLV (patched by caller) */ + memcpy(manifest + 0, &magic, sizeof(magic)); + memcpy(manifest + 4, &fw_size, sizeof(fw_size)); + write_le16(manifest + 8, HDR_HASH); + write_le16(manifest + 10, WOLFBOOT_SHA_DIGEST_SIZE); + + eh = (elf64_header *)(manifest + IMAGE_HEADER_SIZE); + memcpy(eh->ident, ELF_IDENT_STR, 4); + eh->ident[ELF_CLASS_OFF] = ELF_CLASS_64; + eh->ident[5] = ELF_ENDIAN_LITTLE; + eh->type = ELF_HET_EXEC; + eh->machine = 0; + eh->version = 1; + eh->entry = 0x2000; + eh->ph_offset = sizeof(elf64_header); + eh->flags = 0; + eh->header_size = sizeof(elf64_header); + eh->ph_entry_size = sizeof(elf64_program_header); + eh->ph_entry_count = n; + + ph = (elf64_program_header *)((uint8_t *)eh + sizeof(elf64_header)); + for (i = 0; i < n; i++) { + memset(&ph[i], 0, sizeof(ph[i])); + ph[i].type = ELF_PT_LOAD; + ph[i].offset = segs[i].offset; + ph[i].paddr = segs[i].paddr; + ph[i].file_size = segs[i].filesz; + ph[i].mem_size = segs[i].filesz; + ph[i].align = 1; + for (j = 0; j < segs[i].fillsz; j++) { + segs[i].payload[j] = (uint8_t)(0x60U + i + j); + } + } +} + +#define ELF_HDR_SZ_2 (sizeof(elf64_header) + 2 * sizeof(elf64_program_header)) + static void map_boot_partition(void) { int ret = mmap_file("/tmp/wolfboot-unit-elf-scatter-boot.bin", @@ -330,12 +398,188 @@ START_TEST(test_elf_scatter_corrupted_segment_rejected) } END_TEST +/* A segment whose 64-bit file_size does not fit the uint32_t length the + * flash hash reader consumes must be rejected outright. Pre-fix, the + * size was silently truncated (2^32 -> 0 bytes hashed) and an image + * whose stored digest matched the truncated walk verified OK. */ +START_TEST(test_elf_scatter_filesz_over_32bit_rejected) +{ + uint8_t expected_digest[WOLFBOOT_SHA_DIGEST_SIZE]; + unsigned long entry = 0; + uint32_t fw_size = (uint32_t)ELF_HDR_SZ_2 + SEG2_SIZE; + struct seg_spec segs[2]; + struct wolfBoot_image boot; + wolfBoot_hash_t ctx; + int ret; + + map_boot_partition(); + + memset(seg1_flash, 0, sizeof(seg1_flash)); + memset(seg2_flash, 0, sizeof(seg2_flash)); + segs[0].offset = ELF_HDR_SZ_2 + 0x1000U; /* layout gap, never hashed */ + segs[0].filesz = 0x100000000ULL; /* 2^32: truncates to 0 */ + segs[0].paddr = (uint64_t)(uintptr_t)seg2_flash; /* 0 bytes read */ + segs[0].payload = seg2_flash; + segs[0].fillsz = 0; + segs[1].offset = ELF_HDR_SZ_2; + segs[1].filesz = SEG2_SIZE; + segs[1].paddr = (uint64_t)(uintptr_t)seg2_flash; + segs[1].payload = seg2_flash; + segs[1].fillsz = SEG2_SIZE; + + build_scattered_image_n(segs, 2, fw_size); + + /* Replay the pre-fix walk: the oversized segment contributes zero + * hashed bytes, only seg2 does. */ + ck_assert_int_eq(wolfBoot_open_image(&boot, PART_BOOT), 0); + ck_assert_int_eq(header_hash(&ctx, &boot), 0); + ck_assert_int_eq(update_hash_flash_fwimg(&ctx, &boot, 0, (uint32_t)ELF_HDR_SZ_2), 0); + ck_assert_int_eq( + update_hash_flash_addr(&ctx, (uintptr_t)seg2_flash, SEG2_SIZE, + PART_IS_EXT(&boot)), + 0); + ck_assert_int_eq(final_hash(&ctx, expected_digest), 0); + patch_expected_digest(expected_digest); + + ret = wolfBoot_check_flash_image_elf(PART_BOOT, &entry); + + /* Pre-fix this verified OK (ret 0) with the truncated size. */ + ck_assert_int_eq(ret, -1); + + unmap_boot_partition(); +} +END_TEST + +/* A segment whose file layout (offset + file_size) extends past the + * manifest image must be rejected. Pre-fix, intermediate segments were + * never bounds-checked (only the last one, after the loop) and an image + * whose stored digest matched the out-of-layout walk verified OK. */ +START_TEST(test_elf_scatter_segment_beyond_fw_size_rejected) +{ + uint8_t expected_digest[WOLFBOOT_SHA_DIGEST_SIZE]; + unsigned long entry = 0; + uint32_t fw_size = (uint32_t)ELF_HDR_SZ_2 + SEG2_SIZE; + struct seg_spec segs[2]; + struct wolfBoot_image boot; + wolfBoot_hash_t ctx; + int ret; + + map_boot_partition(); + + memset(seg1_flash, 0, sizeof(seg1_flash)); + memset(seg2_flash, 0, sizeof(seg2_flash)); + segs[0].offset = ELF_HDR_SZ_2; + segs[0].filesz = SEG1_SIZE; /* 0x2000 > the 64-byte layout slack */ + segs[0].paddr = (uint64_t)(uintptr_t)seg1_flash; + segs[0].payload = seg1_flash; + segs[0].fillsz = SEG1_SIZE; + segs[1].offset = ELF_HDR_SZ_2; + segs[1].filesz = SEG2_SIZE; + segs[1].paddr = (uint64_t)(uintptr_t)seg2_flash; + segs[1].payload = seg2_flash; + segs[1].fillsz = SEG2_SIZE; + + build_scattered_image_n(segs, 2, fw_size); + + /* Replay the pre-fix walk: both segments are hashed in full at their + * paddr locations despite seg0's layout extending past fw_size. */ + ck_assert_int_eq(wolfBoot_open_image(&boot, PART_BOOT), 0); + ck_assert_int_eq(header_hash(&ctx, &boot), 0); + ck_assert_int_eq(update_hash_flash_fwimg(&ctx, &boot, 0, (uint32_t)ELF_HDR_SZ_2), 0); + ck_assert_int_eq( + update_hash_flash_addr(&ctx, (uintptr_t)seg1_flash, SEG1_SIZE, + PART_IS_EXT(&boot)), + 0); + ck_assert_int_eq( + update_hash_flash_addr(&ctx, (uintptr_t)seg2_flash, SEG2_SIZE, + PART_IS_EXT(&boot)), + 0); + ck_assert_int_eq(final_hash(&ctx, expected_digest), 0); + patch_expected_digest(expected_digest); + + ret = wolfBoot_check_flash_image_elf(PART_BOOT, &entry); + + /* Pre-fix this verified OK (ret 0). */ + ck_assert_int_eq(ret, -1); + + unmap_boot_partition(); +} +END_TEST + +/* A paddr whose segment range overflows the address space must be + * rejected before any flash read. Pre-fix this walked off into + * unmapped memory (segfault here; bus fault/hang on target). */ +START_TEST(test_elf_scatter_paddr_range_overflow_rejected) +{ + unsigned long entry = 0; + uint32_t fw_size = IMG_FW_SIZE; + struct seg_spec segs[1]; + int ret; + + map_boot_partition(); + + memset(seg2_flash, 0, sizeof(seg2_flash)); + segs[0].offset = ELF_HDR_SZ; + segs[0].filesz = SEG_SIZE; + segs[0].paddr = UINT64_MAX - 4; /* +SEG_SIZE wraps past UINT64_MAX */ + segs[0].payload = seg2_flash; + segs[0].fillsz = SEG_SIZE; + + build_scattered_image_n(segs, 1, fw_size); + + ret = wolfBoot_check_flash_image_elf(PART_BOOT, &entry); + + ck_assert_int_eq(ret, -1); + + unmap_boot_partition(); +} +END_TEST + +#if UINTPTR_MAX < UINT64_MAX +/* A paddr that fits in 64 bits but not in the destination (uintptr_t) + * width must be rejected: the load_addr cast after the check would + * silently wrap and the hash walk would read the wrapped address. + * 32-bit builds only: on 64-bit builds UINTPTR_MAX == UINT64_MAX and + * the case above already covers it. */ +START_TEST(test_elf_scatter_paddr_beyond_pointer_width_rejected) +{ + unsigned long entry = 0; + uint32_t fw_size = IMG_FW_SIZE; + struct seg_spec segs[1]; + int ret; + + map_boot_partition(); + + memset(seg2_flash, 0, sizeof(seg2_flash)); + segs[0].offset = ELF_HDR_SZ; + segs[0].filesz = SEG_SIZE; + segs[0].paddr = 1ULL << 32; /* fits uint64_t, exceeds 32-bit width */ + segs[0].payload = seg2_flash; + segs[0].fillsz = SEG_SIZE; + + build_scattered_image_n(segs, 1, fw_size); + + ret = wolfBoot_check_flash_image_elf(PART_BOOT, &entry); + + ck_assert_int_eq(ret, -1); + + unmap_boot_partition(); +} +END_TEST +#endif + Suite *elf_scatter_suite(void) { Suite *s = suite_create("ELF flash-scatter image check"); TCase *tc = tcase_create("wolfBoot_check_flash_image_elf"); tcase_add_test(tc, test_elf_scatter_valid_image_verifies_ok); tcase_add_test(tc, test_elf_scatter_corrupted_segment_rejected); + tcase_add_test(tc, test_elf_scatter_filesz_over_32bit_rejected); + tcase_add_test(tc, test_elf_scatter_segment_beyond_fw_size_rejected); + tcase_add_test(tc, test_elf_scatter_paddr_range_overflow_rejected); +#if UINTPTR_MAX < UINT64_MAX + tcase_add_test(tc, test_elf_scatter_paddr_beyond_pointer_width_rejected); +#endif tcase_set_timeout(tc, 10); suite_add_tcase(s, tc); return s; diff --git a/tools/unit-tests/unit-parser-large-header.c b/tools/unit-tests/unit-parser-large-header.c new file mode 100644 index 0000000000..7c98b8a4fa --- /dev/null +++ b/tools/unit-tests/unit-parser-large-header.c @@ -0,0 +1,186 @@ +/* unit-parser-large-header.c + * + * Unit test for wolfBoot_find_header() with a manifest header at or above + * the 64 KiB boundary: the per-field budget check must compare in a + * 32-bit domain, since a uint16_t cast of (IMAGE_HEADER_SIZE - + * IMAGE_HEADER_OFFSET) wraps for headers >= 64 KiB and rejects every + * field, breaking the pack/parse roundtrip for large signed TLVs. + * + * + * 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, see . + */ + +/* Must also define DEBUG_WOLFSSL in user_settings.h */ +#define WOLFBOOT_HASH_SHA256 +/* Exactly at the wrap boundary: the payload budget is 0x10000, which a + * uint16_t cast of the pre-fix check turned into 0 (rejecting everything). */ +#define IMAGE_HEADER_SIZE 0x10008 +#define WC_RSA_BLINDING +#define ECC_TIMING_RESISTANT +#include +/* Consume the unit target header up front so its include guard keeps + * libwolfboot.c's later include from running. image.h sanity-checks + * WOLFBOOT_SECTOR_SIZE > IMAGE_HEADER_SIZE, which the shared unit sector + * (0x400) cannot satisfy for a 0x10008 header; override it here. The + * parser under test does no flash I/O, so the sector size is otherwise + * unused. */ +#include "target.h" +#undef WOLFBOOT_SECTOR_SIZE +#define WOLFBOOT_SECTOR_SIZE 0x20000 +#include "libwolfboot.c" +#include +static int locked = 0; + +/* Mocks */ +void hal_init(void) +{ +} +int hal_flash_write(haladdr_t address, const uint8_t *data, int len) +{ + (void)address; + (void)data; + (void)len; + return 0; +} +int hal_flash_erase(haladdr_t address, int len) +{ + (void)address; + (void)len; + return 0; +} +void hal_flash_unlock(void) +{ + ck_assert_msg(locked, "Double unlock detected\n"); + locked--; +} +void hal_flash_lock(void) +{ + ck_assert_msg(!locked, "Double lock detected\n"); + locked++; +} + +void hal_prepare_boot(void) +{ +} +/* End Mocks */ + +/* A 300-byte TLV payload: comfortably inside the 0x10000 budget, but far + * beyond the 244 bytes the wrapped uint16 budget would have allowed for a + * slightly smaller oversized header. */ +#define BIG_TLV_LEN 300 + +static uint8_t big_hdr[IMAGE_HEADER_SIZE]; + +static void build_big_header(void) +{ + uint32_t magic = WOLFBOOT_MAGIC; + uint32_t fw_size = 0x100; + int i; + + memset(big_hdr, 0xFF, sizeof(big_hdr)); + memcpy(big_hdr + 0, &magic, sizeof(magic)); + memcpy(big_hdr + 4, &fw_size, sizeof(fw_size)); + + /* Single TLV at IMAGE_HEADER_OFFSET: HDR_VERSION with a 300-byte + * payload, followed by the end-of-options zero word. */ + big_hdr[8] = (uint8_t)(HDR_VERSION & 0xFF); + big_hdr[9] = (uint8_t)(HDR_VERSION >> 8); + big_hdr[10] = (uint8_t)(BIG_TLV_LEN & 0xFF); + big_hdr[11] = (uint8_t)(BIG_TLV_LEN >> 8); + for (i = 0; i < BIG_TLV_LEN; i++) + big_hdr[12 + i] = (uint8_t)(0xA0 + i); + big_hdr[12 + BIG_TLV_LEN] = 0x00; + big_hdr[13 + BIG_TLV_LEN] = 0x00; +} + +START_TEST (test_parser_large_header_finds_big_tlv) +{ + uint8_t *p; + int i; + + build_big_header(); + + /* The version field must be located despite the header being at the + * uint16 budget wrap boundary. */ + ck_assert_msg(wolfBoot_find_header(big_hdr + 8, HDR_VERSION, &p) == + BIG_TLV_LEN, + "Parser error: cannot locate version field in a >= 64 KiB header"); + + for (i = 0; i < BIG_TLV_LEN; i++) + ck_assert_msg(p[i] == (uint8_t)(0xA0 + i), + "Parser error: version payload does not match"); + + /* A non-existing field must still report not-found. */ + ck_assert_msg(wolfBoot_find_header(big_hdr + 8, HDR_SHA3_384, &p) == 0, + "Parser error: found a non-existing field"); +} +END_TEST + +START_TEST (test_parser_large_header_blobs) +{ + uint32_t ver; + + build_big_header(); + + /* The blob accessor must read through the large header as well. */ + ver = wolfBoot_get_blob_version(big_hdr); + ck_assert_uint_eq(ver, 0); /* 300-byte version is not a uint32: no field */ + + /* Rebuild with a 4-byte version payload: the accessor must return it. */ + big_hdr[10] = 4; + big_hdr[11] = 0; + big_hdr[12] = 0x0d; + big_hdr[13] = 0x0c; + big_hdr[14] = 0x0b; + big_hdr[15] = 0x0a; + ver = wolfBoot_get_blob_version(big_hdr); + ck_assert_uint_eq(ver, 0x0a0b0c0d); +} +END_TEST + +Suite *wolfboot_suite(void) +{ + + /* Suite initialization */ + Suite *s = suite_create("wolfBoot"); + + /* Test cases */ + TCase *parser_big = tcase_create("Parser large header"); + + /* Test function <-> Test case */ + tcase_add_test(parser_big, test_parser_large_header_finds_big_tlv); + tcase_add_test(parser_big, test_parser_large_header_blobs); + + /* Set parameters + add to suite */ + tcase_set_timeout(parser_big, 20); + suite_add_tcase(s, parser_big); + + return s; +} + + +int main(void) +{ + int fails; + Suite *s = wolfboot_suite(); + SRunner *sr = srunner_create(s); + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + return fails; +} diff --git a/tools/unit-tests/unit-sama5d3-ext-read.c b/tools/unit-tests/unit-sama5d3-ext-read.c new file mode 100644 index 0000000000..b571c5fb44 --- /dev/null +++ b/tools/unit-tests/unit-sama5d3-ext-read.c @@ -0,0 +1,244 @@ +/* unit-sama5d3-ext-read.c + * + * Regression test: ext_flash_read() in hal/sama5d3.c never applied the + * intra-page offset of the start address (a partial read returned bytes + * from the head of the page instead of the requested column), copied + * sub-page reads in 32-bit words (dropping a sub-word tail), and wrote + * a full NAND page into the caller's buffer for a multi-page read + * ending mid-page (overrunning the buffer). + * + * The HAL cannot be built on the host, so the Makefile extracts + * ext_flash_read() verbatim along with the nand_flash geometry struct; + * the test provides host div_u()/mod() (the HAL's software-division + * wrappers exist only because the Cortex-A5 has no divider) and + * emulated nand_read_page()/nand_check_bad_block() backed by a + * deterministic byte array. + * 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 + */ + +#include +#include +#include +#include + +/* sama5d3.h constants the extracted function needs. */ +#define NAND_FLASH_PAGE_SIZE 0x800 /* 2KB */ +#define NAND_FLASH_OOB_SIZE 0x40 /* 64B */ +#define MAX_ECC_BYTES 8 +#define wolfBoot_printf(...) do {} while (0) + +/* Emulated NAND: 8 blocks x 16 pages x 2KB = 256KB. */ +#define EMU_BLOCKS 8 +#define EMU_PAGES_PER_BLK 16 +#define EMU_PAGE_SIZE NAND_FLASH_PAGE_SIZE +#define EMU_BLOCK_SIZE (EMU_PAGES_PER_BLK * EMU_PAGE_SIZE) +#define EMU_TOTAL (EMU_BLOCKS * EMU_BLOCK_SIZE) +static uint8_t emu_nand[EMU_TOTAL]; + +/* Host equivalents of the HAL's software-division wrappers. */ +static uint32_t div_u(uint32_t dividend, uint32_t divisor) +{ + return dividend / divisor; +} + +static uint32_t mod(uint32_t dividend, uint32_t divisor) +{ + return dividend % divisor; +} + +/* Emulated NAND primitives: a full page read from column 0, like the + * hardware path; every block is good. */ +static int nand_check_bad_block(uint32_t block) +{ + (void)block; + return 0; +} + +static int nand_read_page(uint32_t block, uint32_t page, uint8_t *data) +{ + uint32_t row = block * EMU_PAGES_PER_BLK + page; + + memcpy(data, emu_nand + row * EMU_PAGE_SIZE, EMU_PAGE_SIZE); + return 0; +} + +/* The real ext_flash_read() and nand_flash struct from hal/sama5d3.c + * (extracted by the Makefile). */ +#include "sama5d3_read_extract.h" + +/* Read buffer plus an adjacent canary region: a multi-page read ending + * mid-page used to write a full page past the requested length. */ +#define SCRATCH_LEN (EMU_PAGE_SIZE * 4) +static uint8_t scratch[SCRATCH_LEN + 64]; +#define CANARY (scratch + SCRATCH_LEN) +#define CANARY_LEN 64 + +static void setup(void) +{ + uint32_t i; + + /* Deterministic pattern: every byte depends on row and column. */ + for (i = 0; i < EMU_TOTAL; i++) + emu_nand[i] = (uint8_t)((i / EMU_PAGE_SIZE) * 7 + i); + + nand_flash.page_size = EMU_PAGE_SIZE; + nand_flash.block_size = EMU_BLOCK_SIZE; + nand_flash.block_count = EMU_BLOCKS; + nand_flash.pages_per_block = EMU_PAGES_PER_BLK; + nand_flash.pages_per_device = EMU_BLOCKS * EMU_PAGES_PER_BLK; + nand_flash.total_size = EMU_TOTAL; +} + +static void teardown(void) +{ +} + +static void fill_expected(uint8_t *dst, uint32_t address, uint32_t len) +{ + uint32_t i; + + for (i = 0; i < len; i++) + dst[i] = emu_nand[address + i]; +} + +/* Run one read and compare against the emulated device byte for byte. */ +static void read_case(uint32_t address, int len) +{ + uint8_t expected[SCRATCH_LEN]; + int i; + int ret; + + ck_assert_int_lt(len, SCRATCH_LEN); + memset(scratch, 0xEE, sizeof(scratch)); + fill_expected(expected, address, (uint32_t)len); + + ret = ext_flash_read(address, scratch, len); + ck_assert_int_eq(ret, len); + if (len > 0) + ck_assert_mem_eq(scratch, expected, (size_t)len); + + /* The canary must be untouched: nothing may be written past len. */ + for (i = 0; i < CANARY_LEN; i++) + ck_assert_uint_eq(CANARY[i], 0xEE); +} + +START_TEST(test_read_zero_length) +{ + memset(scratch, 0xEE, sizeof(scratch)); + ck_assert_int_eq(ext_flash_read(0x4000, scratch, 0), 0); + ck_assert_uint_eq(scratch[0], 0xEE); +} +END_TEST + +START_TEST(test_read_aligned_full_page) +{ + read_case(0, EMU_PAGE_SIZE); + read_case(0x10000, EMU_PAGE_SIZE); +} +END_TEST + +START_TEST(test_read_unaligned_small) +{ + /* Mid-page starts: the head of the page must not be returned. */ + read_case(1, 4); + read_case(0x7FC, 8); + read_case(0x400, 64); +} +END_TEST + +START_TEST(test_read_subword_lengths) +{ + /* 1-3 byte reads copy nothing in a word-count loop. */ + read_case(0x800, 1); + read_case(0x801, 2); + read_case(0x1800, 3); +} +END_TEST + +START_TEST(test_read_sha_block_pattern) +{ + /* The integrity check hashes the image in 64-byte blocks from + * fw_base + offset: every block after the first in each page is + * an unaligned small read. */ + uint32_t offset; + + for (offset = 0; offset < EMU_PAGE_SIZE; offset += 0x40) + read_case(0x800 + offset, 64); +} +END_TEST + +START_TEST(test_read_page_boundaries) +{ + read_case(0, EMU_PAGE_SIZE - 1); + read_case(0, EMU_PAGE_SIZE + 1); + /* Start near the end of a page and cross into the next. */ + read_case(0x7F0, 0x20); + read_case(0x7FF, 0x101); +} +END_TEST + +START_TEST(test_read_multipage_partial_tail) +{ + /* Multi-page reads whose tail is shorter than a page (and not a + * multiple of 4): the tail page must not be written in full. */ + read_case(0x100, 0x903); + read_case(0, 0x1805); + read_case(0x40, 0x1F01); +} +END_TEST + +START_TEST(test_read_cross_block) +{ + read_case(0x7F00, 0x120); + read_case(0x7000, 0x200); + read_case(0x7001, 0x1000); +} +END_TEST + +Suite *sama5d3_ext_read_suite(void) +{ + Suite *s = suite_create("sama5d3-ext-read"); + TCase *tc = tcase_create("sama5d3-ext-read"); + + tcase_add_checked_fixture(tc, setup, teardown); + tcase_add_test(tc, test_read_zero_length); + tcase_add_test(tc, test_read_aligned_full_page); + tcase_add_test(tc, test_read_unaligned_small); + tcase_add_test(tc, test_read_subword_lengths); + tcase_add_test(tc, test_read_sha_block_pattern); + tcase_add_test(tc, test_read_page_boundaries); + tcase_add_test(tc, test_read_multipage_partial_tail); + tcase_add_test(tc, test_read_cross_block); + suite_add_tcase(s, tc); + + return s; +} + +int main(void) +{ + int fails; + Suite *s = sama5d3_ext_read_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + + return fails; +} diff --git a/tools/unit-tests/unit-t10xx-dts-memac.c b/tools/unit-tests/unit-t10xx-dts-memac.c index 44c0249892..cfb86b6e04 100644 --- a/tools/unit-tests/unit-t10xx-dts-memac.c +++ b/tools/unit-tests/unit-t10xx-dts-memac.c @@ -261,7 +261,9 @@ static void dtb_finalize(struct dtb *d) hdr->totalsize = cpu_to_fdt32(0x2000); hdr->off_dt_struct = cpu_to_fdt32(d->struct_off); hdr->off_dt_strings = cpu_to_fdt32(d->strings_off); - hdr->off_mem_rsvmap = cpu_to_fdt32(d->strings_off); /* no reservations */ + hdr->off_mem_rsvmap = cpu_to_fdt32(0x28); + /* the 8 bytes after the 40-byte header are zero: an empty + * reservation list at the canonical spot */ hdr->version = cpu_to_fdt32(17); hdr->last_comp_version = cpu_to_fdt32(16); hdr->boot_cpuid_phys = cpu_to_fdt32(0); @@ -439,6 +441,121 @@ START_TEST(test_memac_mixed_oob_then_valid) } END_TEST +/* Build a DTB with a memory node and one node with the given + * compatible. */ +static void dtb_build_compat_node(struct dtb *d, const char *node, + const char *compat) +{ + uint32_t reg[4] = {cpu_to_fdt32(0), cpu_to_fdt32(0), cpu_to_fdt32(0), + cpu_to_fdt32(0x10000000U)}; + + dtb_init(d); + dtb_begin_node(d, ""); + dtb_begin_node(d, "memory"); + dtb_prop_raw(d, "reg", reg, sizeof(reg)); + dtb_end_node(d); + dtb_begin_node(d, node); + dtb_prop_raw(d, "compatible", compat, strlen(compat) + 1); + dtb_end_node(d); + dtb_end_node(d); /* root */ + dtb_finalize(d); +} + +/* Build a DTB whose root node (struct offset 0) carries the given + * compatible. */ +static void dtb_build_root_compat(struct dtb *d, const char *compat) +{ + dtb_init(d); + dtb_begin_node(d, ""); + dtb_prop_raw(d, "compatible", compat, strlen(compat) + 1); + dtb_end_node(d); + dtb_finalize(d); +} + +/* The fman/esdhc node-found guards used `off != !FDT_ERR_NOTFOUND`, + * which is `off != 0`: a node at struct offset 0 (the root) was + * skipped even when it matched, and the fixup ran with a negative + * offset when no node matched. A root node compatible with fsl,fman + * must get the clock fixup. */ +START_TEST(test_fman_root_node_compatible_fixed) +{ + struct dtb d; + int off, len; + const void *clk; + + dtb_build_root_compat(&d, "fsl,fman"); + ck_assert_int_eq(hal_dts_fixup(d.buf), 0); + + off = fdt_node_offset_by_compatible(d.buf, -1, "fsl,fman"); + ck_assert_int_eq(off, 0); /* the root node */ + clk = fdt_getprop(d.buf, off, "clock-frequency", &len); + ck_assert_ptr_nonnull(clk); + ck_assert_int_eq(len, 4); + ck_assert_uint_eq(fdt32_to_cpu(*(const uint32_t *)clk), 100000000U); +} +END_TEST + +/* A child fman node gets the clock fixup. */ +START_TEST(test_fman_child_node_fixed) +{ + struct dtb d; + int off, len; + const void *clk; + + dtb_build_compat_node(&d, "fman", "fsl,fman"); + ck_assert_int_eq(hal_dts_fixup(d.buf), 0); + + off = fdt_node_offset_by_compatible(d.buf, -1, "fsl,fman"); + ck_assert_int_gt(off, 0); + clk = fdt_getprop(d.buf, off, "clock-frequency", &len); + ck_assert_ptr_nonnull(clk); + ck_assert_int_eq(len, 4); + ck_assert_uint_eq(fdt32_to_cpu(*(const uint32_t *)clk), 100000000U); +} +END_TEST + +/* An esdhc node gets the clock fixup and status=okay. */ +START_TEST(test_esdhc_node_fixed) +{ + struct dtb d; + int off, len; + const void *clk; + const void *status; + + dtb_build_compat_node(&d, "esdhc", "fsl,esdhc"); + ck_assert_int_eq(hal_dts_fixup(d.buf), 0); + + off = fdt_node_offset_by_compatible(d.buf, -1, "fsl,esdhc"); + ck_assert_int_gt(off, 0); + clk = fdt_getprop(d.buf, off, "clock-frequency", &len); + ck_assert_ptr_nonnull(clk); + ck_assert_int_eq(len, 4); + ck_assert_uint_eq(fdt32_to_cpu(*(const uint32_t *)clk), 100000000U); + status = fdt_getprop(d.buf, off, "status", &len); + ck_assert_ptr_nonnull(status); + ck_assert_int_eq(len, 5); + ck_assert_str_eq(status, "okay"); +} +END_TEST + +/* No fman/esdhc nodes: the fixups are skipped cleanly and the fixup + * still succeeds. */ +START_TEST(test_fman_esdhc_absent_skipped) +{ + struct dtb d; + int off; + + dtb_build_compat_node(&d, "ethernet", "fsl,eth"); + ck_assert_int_eq(hal_dts_fixup(d.buf), 0); + + ck_assert_int_eq(fdt_node_offset_by_compatible(d.buf, -1, "fsl,fman"), + -FDT_ERR_NOTFOUND); + off = fdt_node_offset_by_compatible(d.buf, -1, "fsl,eth"); + ck_assert_int_gt(off, 0); + ck_assert_ptr_null(fdt_getprop(d.buf, off, "clock-frequency", NULL)); +} +END_TEST + Suite *t10xx_dts_memac_suite(void) { Suite *s = suite_create("t10xx-dts-memac"); @@ -449,6 +566,10 @@ Suite *t10xx_dts_memac_suite(void) tcase_add_test(tc, test_memac_unmapped_cell_index_skipped); tcase_add_test(tc, test_memac_valid_cell_index_fixed); tcase_add_test(tc, test_memac_mixed_oob_then_valid); + tcase_add_test(tc, test_fman_root_node_compatible_fixed); + tcase_add_test(tc, test_fman_child_node_fixed); + tcase_add_test(tc, test_esdhc_node_fixed); + tcase_add_test(tc, test_fman_esdhc_absent_skipped); suite_add_tcase(s, tc); return s; diff --git a/tools/unit-tests/unit-update-disk-fit.c b/tools/unit-tests/unit-update-disk-fit.c index 5799ada798..0b401d8da5 100644 --- a/tools/unit-tests/unit-update-disk-fit.c +++ b/tools/unit-tests/unit-update-disk-fit.c @@ -54,17 +54,26 @@ #include #define TEST_PAYLOAD_SIZE 64 -#define TEST_DTS_SIZE 32 +/* A plausible DTB size: at least WOLFBOOT_DTS_MIN_SIZE (the 40-byte + * FDT v17 header). */ +#define TEST_DTS_SIZE 48 +/* Staging-region stand-in. The oversized test relies on it being big + * enough that the pre-fix unbounded copy lands fully inside it, so the + * regression is observable as "a copy happened" rather than a crash. */ +#define TEST_DTS_STAGE_SIZE (2U * 1024U * 1024U) static uint8_t load_buffer[TEST_PAYLOAD_SIZE]; #define WOLFBOOT_LOAD_ADDRESS ((uintptr_t)load_buffer) -static uint8_t dts_buffer[TEST_DTS_SIZE]; +static uint8_t dts_buffer[TEST_DTS_STAGE_SIZE]; #define WOLFBOOT_LOAD_DTS_ADDRESS ((uintptr_t)dts_buffer) static uint8_t part_a_image[IMAGE_HEADER_SIZE + TEST_PAYLOAD_SIZE]; static uint8_t part_b_image[IMAGE_HEADER_SIZE + TEST_PAYLOAD_SIZE]; -static uint8_t fit_dts_image[TEST_DTS_SIZE]; +static uint8_t fit_dts_image[TEST_DTS_STAGE_SIZE]; +/* Parsed DTB size the wolfBoot_get_dts_size() stub reports, and (pre + * fix) the FIT-declared length the fit_load_image() stub returns. */ +static int mock_dts_size; static int mock_do_boot_called; static int mock_fit_memcpy_ret; static int mock_fit_memcpy_called; @@ -107,6 +116,7 @@ static void reset_mocks(void) build_image(part_a_image, 1, 0xA1); build_image(part_b_image, 2, 0xB2); memset(fit_dts_image, 0xDD, sizeof(fit_dts_image)); + mock_dts_size = TEST_DTS_SIZE; mock_do_boot_called = 0; mock_fit_memcpy_ret = 0; mock_fit_memcpy_called = 0; @@ -217,11 +227,12 @@ int wolfBoot_verify_authenticity(struct wolfBoot_image* img) } /* The loaded payload is treated as a FIT container, and the sub-image - * returned by fit_load_image() is a valid flat device tree. */ + * returned by fit_load_image() is a flat device tree whose parsed + * size is mock_dts_size. */ int wolfBoot_get_dts_size(void *dts_addr) { (void)dts_addr; - return TEST_DTS_SIZE; + return mock_dts_size; } /* Only reached through the fdt_version()/fdt_totalsize() trace macros here. */ @@ -251,7 +262,7 @@ void* fit_load_image(void* fdt, const char* image, int* lenp) (void)fdt; (void)image; if (lenp != NULL) - *lenp = TEST_DTS_SIZE; + *lenp = mock_dts_size; return fit_dts_image; } @@ -331,12 +342,47 @@ START_TEST(test_update_disk_fit_dts_copy_success_boots) } END_TEST +/* A parsed DTB larger than the staging bound (WOLFBOOT_DTS_MAX_SIZE) + * must be rejected rather than copied: before the fix the copy length + * came from the FIT-declared property length, unbounded against the + * staging region. */ +START_TEST(test_update_disk_fit_dts_oversized_rejected) +{ + reset_mocks(); + mock_dts_size = (1024 * 1024) + 4; /* > WOLFBOOT_DTS_MAX_SIZE */ + + wolfBoot_start(); + + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_do_boot_called, 1); + /* nothing may have been copied into the staging region */ + ck_assert_uint_eq(dts_buffer[0], 0); +} +END_TEST + +/* A parsed DTB smaller than the FDT header size is a partial tree and + * must be rejected. */ +START_TEST(test_update_disk_fit_dts_below_min_rejected) +{ + reset_mocks(); + mock_dts_size = 32; /* < WOLFBOOT_DTS_MIN_SIZE (40) */ + + wolfBoot_start(); + + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_do_boot_called, 1); + ck_assert_uint_eq(dts_buffer[0], 0); +} +END_TEST + Suite *wolfboot_suite(void) { Suite *s = suite_create("wolfBoot"); TCase *tc = tcase_create("update-disk-fit"); tcase_add_test(tc, test_update_disk_fit_dts_copy_failure_zeroizes_key_material); + tcase_add_test(tc, test_update_disk_fit_dts_oversized_rejected); + tcase_add_test(tc, test_update_disk_fit_dts_below_min_rejected); tcase_add_test(tc, test_update_disk_fit_dts_copy_success_boots); suite_add_tcase(s, tc); diff --git a/tools/unit-tests/unit-update-flash.c b/tools/unit-tests/unit-update-flash.c index 19e58805df..dad19d97cd 100644 --- a/tools/unit-tests/unit-update-flash.c +++ b/tools/unit-tests/unit-update-flash.c @@ -85,12 +85,21 @@ static uint16_t host_to_img_u16(uint16_t val) #endif } +/* The update partition is written by the update tool through the + * encryption-aware writer: in EXT_ENCRYPTED builds a raw write would leave + * plaintext in flash that the bootloader then decrypts. The swap partition + * is written raw, since its content is already encrypted when staged. */ +static int update_part_write(uintptr_t addr, const void *buf, int len) +{ + return ext_flash_check_write(addr, buf, len); +} + static void ext_flash_write_le16(uintptr_t addr, uint16_t val) { uint8_t le[2]; le[0] = (uint8_t)(val & 0xFFu); le[1] = (uint8_t)((val >> 8) & 0xFFu); - ext_flash_write(addr, le, sizeof(le)); + update_part_write(addr, le, sizeof(le)); } static void ext_flash_write_le32(uintptr_t addr, uint32_t val) @@ -100,7 +109,7 @@ static void ext_flash_write_le32(uintptr_t addr, uint32_t val) le[1] = (uint8_t)((val >> 8) & 0xFFu); le[2] = (uint8_t)((val >> 16) & 0xFFu); le[3] = (uint8_t)((val >> 24) & 0xFFu); - ext_flash_write(addr, le, sizeof(le)); + update_part_write(addr, le, sizeof(le)); } #ifdef DELTA_UPDATES @@ -133,6 +142,7 @@ int unit_test_wb_patch(WB_PATCH_CTX *ctx, uint8_t *dst, uint32_t len) static int mock_get_encrypt_key_ret = 0; static int mock_set_encrypt_key_ret = 0; static int mock_set_encrypt_key_calls = 0; +static int mock_erase_encrypt_key_calls = 0; int wolfBoot_get_encrypt_key(uint8_t *k, uint8_t *nonce) { @@ -158,6 +168,7 @@ int wolfBoot_set_encrypt_key(const uint8_t *key, const uint8_t *nonce) int wolfBoot_erase_encrypt_key(void) { + mock_erase_encrypt_key_calls++; return 0; } #endif @@ -183,6 +194,32 @@ START_TEST (test_boot_success_sets_state) END_TEST #endif +#ifdef CUSTOM_ENCRYPT_KEY +/* wolfBoot_success() must erase the temporary firmware-decryption key + * from the partition trailer as part of confirming an update. The mock + * records the call; without the erase the plaintext key would stay + * resident in flash. */ +START_TEST (test_boot_success_erases_encrypt_key) +{ + uint8_t state = 0; + + reset_mock_stats(); + prepare_flash(); + hal_flash_unlock(); + wolfBoot_set_partition_state(PART_BOOT, IMG_STATE_TESTING); + hal_flash_lock(); + + wolfBoot_success(); + + ck_assert_int_eq(wolfBoot_get_partition_state(PART_BOOT, &state), 0); + ck_assert_uint_eq(state, IMG_STATE_SUCCESS); + ck_assert_int_eq(mock_erase_encrypt_key_calls, 1); + + cleanup_flash(); +} +END_TEST +#endif + Suite *wolfboot_suite(void); int wolfBoot_staged_ok = 0; @@ -234,6 +271,7 @@ static void reset_mock_stats(void) mock_get_encrypt_key_ret = 0; mock_set_encrypt_key_ret = 0; mock_set_encrypt_key_calls = 0; + mock_erase_encrypt_key_calls = 0; #endif #ifndef ARCH_SIM wolfBoot_panicked = 0; @@ -299,6 +337,11 @@ static void cleanup_flash(void) #define DIGEST_TLV_OFF_IN_HDR 28 +#ifdef EXT_ENCRYPTED +static int add_payload_encrypted(uint8_t part, uint32_t version, uint32_t size, + int use_fallback_iv); +#endif + static int add_payload(uint8_t part, uint32_t version, uint32_t size) { return add_payload_type(part, version, size, @@ -308,6 +351,15 @@ static int add_payload(uint8_t part, uint32_t version, uint32_t size) static int add_payload_type(uint8_t part, uint32_t version, uint32_t size, uint16_t img_type) { +#ifdef EXT_ENCRYPTED + /* The update partition holds ciphertext in EXT_ENCRYPTED builds (the + * update tool writes through the encryption-aware writer); build the + * plaintext image and encrypt it in. add_payload_encrypted builds the + * AUTH_NONE|APP image type, which is what every PART_UPDATE caller + * compiled in the encrypted targets uses. */ + if (part == PART_UPDATE) + return add_payload_encrypted(part, version, size, 0); +#endif uint32_t word; uint32_t magic = WOLFBOOT_MAGIC; uint32_t size_img = host_to_img_u32(size); @@ -445,7 +497,7 @@ START_TEST (test_self_update_newversion_invalid_integrity_denied) HDR_IMG_TYPE_WOLFBOOT | HDR_IMG_TYPE_AUTH); memset(bad_digest, 0xBA, sizeof(bad_digest)); ext_flash_unlock(); - ext_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + DIGEST_TLV_OFF_IN_HDR + 4, + update_part_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + DIGEST_TLV_OFF_IN_HDR + 4, bad_digest, sizeof(bad_digest)); wolfBoot_set_partition_state(PART_UPDATE, IMG_STATE_UPDATING); ext_flash_lock(); @@ -759,6 +811,59 @@ START_TEST (test_fallback_image_verification_rejects_corruption) } END_TEST +/* Positive counterpart of the corruption test above: an image written with + * the fallback IV must read back byte-for-byte through the product's + * decryption path with the fallback IV forced, the way the update flow does + * when the standard-IV open fails. */ +START_TEST (test_fallback_iv_image_roundtrips) +{ + uint32_t size = TEST_SIZE_SMALL; + uint32_t total = size + IMAGE_HEADER_SIZE; + uint8_t *plain = malloc(total); + uint8_t *readback = malloc(total); + struct wolfBoot_image img; + int prev, ret; + uint32_t i; + + reset_mock_stats(); + prepare_flash(); + ck_assert(plain != NULL && readback != NULL); + + /* build_image_buffer is deterministic (srandom(part)), so this is the + * same plaintext add_payload_encrypted writes with the fallback IV */ + ret = build_image_buffer(PART_UPDATE, 2, size, plain, total); + ck_assert_int_eq(ret, 0); + ret = add_payload_encrypted(PART_UPDATE, 2, size, 1); + ck_assert_int_eq(ret, 0); + + /* The update flow verifies and reads a fallback image with the fallback + * IV forced for the whole operation (persistent flag, re-applied by the + * decrypt path on every block). */ + prev = wolfBoot_force_fallback_iv(1); + ret = wolfBoot_open_image(&img, PART_UPDATE); + ck_assert_int_eq(ret, 0); + ck_assert_uint_eq(img.fw_size, size); + for (i = 0; i < total; i += WOLFBOOT_SECTOR_SIZE) { + uint32_t chunk = total - i; + if (chunk > WOLFBOOT_SECTOR_SIZE) + chunk = WOLFBOOT_SECTOR_SIZE; + ret = ext_flash_check_read( + (uintptr_t)WOLFBOOT_PARTITION_UPDATE_ADDRESS + i, + readback + i, (int)chunk); + ck_assert_int_eq(ret, (int)chunk); + } + wolfBoot_enable_fallback_iv(prev); + + i = 0; + while (i < total && readback[i] == plain[i]) + i++; + ck_assert_mem_eq(readback, plain, total); + free(plain); + free(readback); + cleanup_flash(); +} +END_TEST + START_TEST (test_final_swap_propagates_encrypt_key_persist_failure) { int ret; @@ -961,7 +1066,7 @@ START_TEST (test_invalid_update_type) { add_payload(PART_BOOT, 1, TEST_SIZE_SMALL); add_payload(PART_UPDATE, 2, TEST_SIZE_SMALL); ext_flash_unlock(); - ext_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 20, (void *)&word16, 2); + update_part_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 20, (void *)&word16, 2); ext_flash_lock(); wolfBoot_update_trigger(); wolfBoot_start(); @@ -978,7 +1083,7 @@ START_TEST (test_invalid_update_auth_type) { add_payload(PART_BOOT, 1, TEST_SIZE_SMALL); add_payload(PART_UPDATE, 2, TEST_SIZE_SMALL); ext_flash_unlock(); - ext_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 20, (void *)&word16, 2); + update_part_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 20, (void *)&word16, 2); ext_flash_lock(); wolfBoot_update_trigger(); wolfBoot_start(); @@ -996,7 +1101,7 @@ START_TEST (test_update_toolarge) { add_payload(PART_UPDATE, 2, TEST_SIZE_LARGE); /* Change the size in the header to be larger than the actual size */ ext_flash_unlock(); - ext_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 4, (void *)&very_large, 4); + update_part_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 4, (void *)&very_large, 4); ext_flash_lock(); wolfBoot_update_trigger(); @@ -1066,7 +1171,7 @@ START_TEST (test_invalid_sha) { memset(bad_digest, 0xBA, SHA256_DIGEST_SIZE); ext_flash_unlock(); - ext_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + DIGEST_TLV_OFF_IN_HDR + 4, bad_digest, SHA256_DIGEST_SIZE); + update_part_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + DIGEST_TLV_OFF_IN_HDR + 4, bad_digest, SHA256_DIGEST_SIZE); ext_flash_lock(); wolfBoot_update_trigger(); wolfBoot_start(); @@ -1085,8 +1190,9 @@ START_TEST (test_emergency_rollback) { add_payload(PART_UPDATE, 1, TEST_SIZE_SMALL); /* Set the testing flag in the last five bytes of the BOOT partition */ hal_flash_unlock(); - hal_flash_write(WOLFBOOT_PARTITION_BOOT_ADDRESS + WOLFBOOT_PARTITION_SIZE - 5, - testing_flags, 5); + /* PART_BOOT_ENDFLAGS, not the partition end: in EXT_ENCRYPTED builds + * the key/nonce trailer sits between the state trailer and the end. */ + hal_flash_write(PART_BOOT_ENDFLAGS - 5, testing_flags, 5); hal_flash_lock(); wolfBoot_start(); @@ -1107,8 +1213,9 @@ START_TEST (test_emergency_rollback_equal_versions) { add_payload(PART_UPDATE, 1, TEST_SIZE_SMALL); /* Set the testing flag in the last five bytes of the BOOT partition */ hal_flash_unlock(); - hal_flash_write(WOLFBOOT_PARTITION_BOOT_ADDRESS + WOLFBOOT_PARTITION_SIZE - 5, - testing_flags, 5); + /* PART_BOOT_ENDFLAGS, not the partition end: in EXT_ENCRYPTED builds + * the key/nonce trailer sits between the state trailer and the end. */ + hal_flash_write(PART_BOOT_ENDFLAGS - 5, testing_flags, 5); hal_flash_lock(); wolfBoot_start(); @@ -1129,13 +1236,14 @@ START_TEST (test_emergency_rollback_failure_due_to_bad_update) { add_payload(PART_UPDATE, 1, TEST_SIZE_SMALL); /* Set the testing flag in the last five bytes of the BOOT partition */ hal_flash_unlock(); - hal_flash_write(WOLFBOOT_PARTITION_BOOT_ADDRESS + WOLFBOOT_PARTITION_SIZE - 5, - testing_flags, 5); + /* PART_BOOT_ENDFLAGS, not the partition end: in EXT_ENCRYPTED builds + * the key/nonce trailer sits between the state trailer and the end. */ + hal_flash_write(PART_BOOT_ENDFLAGS - 5, testing_flags, 5); hal_flash_lock(); /* Corrupt the update */ ext_flash_unlock(); - ext_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS, wrong_update_magic, 4); + update_part_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS, wrong_update_magic, 4); ext_flash_lock(); wolfBoot_start(); @@ -1163,7 +1271,7 @@ START_TEST (test_empty_boot_but_update_sha_corrupted_denied) { add_payload(PART_UPDATE, 5, TEST_SIZE_SMALL); memset(bad_digest, 0xBA, SHA256_DIGEST_SIZE); ext_flash_unlock(); - ext_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + DIGEST_TLV_OFF_IN_HDR + 4, bad_digest, SHA256_DIGEST_SIZE); + update_part_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + DIGEST_TLV_OFF_IN_HDR + 4, bad_digest, SHA256_DIGEST_SIZE); ext_flash_lock(); wolfBoot_start(); /* We expect to panic */ @@ -1200,33 +1308,38 @@ START_TEST (test_diffbase_version_reads) prepare_flash(); ext_flash_unlock(); - ext_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS, + update_part_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS, (const uint8_t *)&magic, sizeof(magic)); version_le = host_to_img_u32(version); - ext_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 4, + update_part_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 4, (const uint8_t *)&version_le, sizeof(version_le)); word = (4u << 16) | HDR_VERSION; word_le = word; - ext_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 8, + update_part_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 8, (const uint8_t *)&word_le, sizeof(word_le)); - ext_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 12, + update_part_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 12, (const uint8_t *)&version_le, sizeof(version_le)); word = (2u << 16) | HDR_IMG_TYPE; word_le = word; - ext_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 16, + update_part_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 16, (const uint8_t *)&word_le, sizeof(word_le)); img_type_le = host_to_img_u16(img_type); - ext_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 20, + update_part_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 20, (const uint8_t *)&img_type_le, sizeof(img_type_le)); + /* The TLVs follow the sign tool's dense layout: the delta-base TLV + * starts right after the image-type TLV (offset 22). A gap here would + * be padding that only reads as 0xFF in plaintext builds; in encrypted + * builds the gap is ciphertext and the header walker would skip past + * the next TLV. */ word = (4u << 16) | HDR_IMG_DELTA_BASE; word_le = word; - ext_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 24, + update_part_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 22, (const uint8_t *)&word_le, sizeof(word_le)); delta_base_le = host_to_img_u32(delta_base); - ext_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 28, + update_part_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 26, (const uint8_t *)&delta_base_le, sizeof(delta_base_le)); ext_flash_lock(); @@ -1270,7 +1383,7 @@ START_TEST (test_diffbase_version_reads_from_little_endian_bytes) prepare_flash(); ext_flash_unlock(); - ext_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS, + update_part_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS, (const uint8_t *)&magic, sizeof(magic)); ext_flash_write_le32(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 4, TEST_SIZE_SMALL); @@ -1283,14 +1396,26 @@ START_TEST (test_diffbase_version_reads_from_little_endian_bytes) ext_flash_write_le16(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 20, img_type); tag = (4u << 16) | HDR_IMG_DELTA_BASE; - ext_flash_write_le32(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 24, tag); - ext_flash_write_le32(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 28, delta_base); + /* Dense TLV layout, as in the sign tool: no padding gap before the + * delta-base TLV (see test_diffbase_version_reads). */ + ext_flash_write_le32(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 22, tag); + ext_flash_write_le32(WOLFBOOT_PARTITION_UPDATE_ADDRESS + 26, delta_base); ext_flash_lock(); ck_assert_uint_eq(wolfBoot_get_image_version(PART_UPDATE), version); ck_assert_uint_eq(wolfBoot_get_diffbase_version(PART_UPDATE), delta_base); - ck_assert_uint_eq(wolfBoot_get_blob_diffbase_version( - (uint8_t *)(uintptr_t)WOLFBOOT_PARTITION_UPDATE_ADDRESS), delta_base); + + /* The blob accessor expects a plaintext header; in EXT_ENCRYPTED + * builds the partition itself holds ciphertext, so hand it the + * decrypted copy. */ + { + uint8_t hdr[IMAGE_HEADER_SIZE]; + ext_flash_unlock(); + ext_flash_check_read((uintptr_t)WOLFBOOT_PARTITION_UPDATE_ADDRESS, hdr, + IMAGE_HEADER_SIZE); + ext_flash_lock(); + ck_assert_uint_eq(wolfBoot_get_blob_diffbase_version(hdr), delta_base); + } cleanup_flash(); } @@ -1701,8 +1826,12 @@ Suite *wolfboot_suite(void) #ifdef UNIT_TEST_FALLBACK_ONLY #ifdef EXT_ENCRYPTED tcase_add_test(fallback_verify, test_fallback_image_verification_rejects_corruption); + tcase_add_test(fallback_verify, test_fallback_iv_image_roundtrips); tcase_add_test(fallback_verify, test_final_swap_propagates_encrypt_key_read_failure); tcase_add_test(fallback_verify, test_final_swap_propagates_encrypt_key_persist_failure); +#ifdef CUSTOM_ENCRYPT_KEY + tcase_add_test(fallback_verify, test_boot_success_erases_encrypt_key); +#endif suite_add_tcase(s, fallback_verify); tcase_add_test(encrypt_write_bounds, test_encrypt_write_keeps_trailing_partial_block); @@ -1764,8 +1893,19 @@ Suite *wolfboot_suite(void) #endif #ifdef EXT_ENCRYPTED tcase_add_test(fallback_verify, test_fallback_image_verification_rejects_corruption); + tcase_add_test(fallback_verify, test_fallback_iv_image_roundtrips); tcase_add_test(fallback_verify, test_final_swap_propagates_encrypt_key_read_failure); tcase_add_test(fallback_verify, test_final_swap_propagates_encrypt_key_persist_failure); +#ifdef CUSTOM_ENCRYPT_KEY + tcase_add_test(fallback_verify, test_boot_success_erases_encrypt_key); +#endif + tcase_add_test(encrypt_write_bounds, + test_encrypt_write_keeps_trailing_partial_block); + tcase_add_test(encrypt_write_bounds, + test_encrypt_write_zero_length_leaves_flash_untouched); + tcase_add_test(encrypt_write_bounds, + test_encrypt_write_reports_head_block_write_failure); + suite_add_tcase(s, encrypt_write_bounds); #endif suite_add_tcase(s, empty_panic);