keystore_get_size() returned slot->pubkey_size verbatim from the OTP
keystore slot with no upper bound. A corrupted or mis-provisioned slot
with pubkey_size > KEYSTORE_PUBKEY_SIZE produces a positive value that
passes every caller guard (pubkey_sz < 0 / <= 0). The callers in image.c
(key_sha256/key_sha384/key_sha3_384 and the ECC verify y-coordinate
offset) then read past otp_slot_item_cache, which only holds
KEYSTORE_PUBKEY_SIZE pubkey bytes.
Reject an out-of-range pubkey_size by returning -1, matching the existing
defensive validation of item_count in keystore_num_pubkeys() and the -1
error convention the callers already handle.
Add unit-otp-keystore, which compiles flash_otp_keystore.c in isolation
and verifies keystore_get_size() rejects oversized slots.
The Bank 2 branch of hal_flash_erase() subtracted the absolute base
FLASH_BANK2_BASE (0x08100000) from the bank-relative loop offset p,
instead of the relative FLASH_BANK2_BASE_REL (0x00100000) used by every
other comparison in the function. For a Bank 2 offset (p >= 0x00100000)
this underflowed uint32_t to ~0xF8000000, with two effects:
1. the SNB sector index programmed into FLASH_CR2 came from the
underflowed value (always sector 0, with stray high bits leaking
into other CR2 fields), and
2. the subtraction mutated the loop variable p itself, so after
p += FLASH_PAGE_SIZE the offset jumped past any end_address and the
loop exited after a single iteration.
The combined result: any multi-sector Bank 2 erase touched only one
sector with the wrong index, silently leaving the remaining requested
sectors (e.g. the SWAP partition at 0x081C0000) unerased.
Compute the sector index in a temporary from FLASH_BANK2_BASE_REL so the
loop variable is preserved, and mask it with FLASH_CR_SNB_MASK before
shifting into FLASH_CR2.
Adds unit-flash-erase-h7, which compiles hal_flash_erase() in isolation
(guarded by WOLFBOOT_UNIT_TEST_FLASH_ERASE, mirroring the unit-mpusize
approach for boot_arm.c) against mocked flash registers and asserts that
a two-sector Bank 2 erase programs sectors 6 and 7 across two iterations.
base_diff() captured patch_inv_off = len3 + CMD.header_sz before calling
make_header_delta(), which signs the delta image via make_header_ex(is_diff=1).
When a certificate chain is present, the delta (is_diff=1) header needs ~72
more bytes than the non-delta header for the four delta TLVs plus the base-hash
TLV. For a window of cert-chain sizes, header_required_size(is_diff=0) still fit
the current CMD.header_sz while header_required_size(is_diff=1) did not, so
make_header_ex(is_diff=1) grew CMD.header_sz to the next power of two *after*
patch_inv_off was captured. The HDR_IMG_DELTA_INVERSE TLV then encoded a stale,
too-small offset; the bootloader (update_flash.c) uses it as a raw byte offset
into the update partition to locate the inverse patch, so rollback read from the
wrong offset and failed.
Resolve the is_diff=1 header-size expansion (same logic as make_header_ex)
before computing patch_inv_off. Add unit-sign-delta-cert-inv-off.py, which signs
an ed25519 delta with a 300-byte chain (inside the triggering window) and
asserts the inverse patch is the trailing HDR_IMG_DELTA_INVERSE_SIZE bytes of
the file; it fails before this fix.
The mpusize() lookup in boot_arm.c only covered sizes up to 64KB and
returned MPUSIZE_ERR for anything larger. mpu_init() passes the wolfBoot
.text+.rodata span (_stored_data - _start_text) to mpusize() and bails
out at the MPUSIZE_ERR guard before reaching mpu_on(). Any build whose
bootloader image exceeds 64KB (TrustZone, PQC, delta-update, or several
crypto algorithms) therefore left MPU_CTRL clear, silently disabling all
five MPU regions for the lifetime of the bootloader.
Fill in the missing power-of-two entries from 128KB through 128MB
(ARMv7-M SIZE field = log2(bytes)-1, shifted into the RASR layout) so
the flash region size is resolved and mpu_on() is reached.
Add tools/unit-tests/unit-mpusize.c, which includes the real mpusize()
from boot_arm.c (guarded to its host-portable MPU helpers via
WOLFBOOT_UNIT_TEST_MPU) and checks that sizes above 64KB no longer map
to MPUSIZE_ERR. The test fails before this fix and passes after.
sign.py encoded HDR_IMG_DELTA_SIZE and HDR_IMG_DELTA_INVERSE_SIZE with a
2-byte length via struct.pack("<H", ...), but wolfBoot_get_delta_info()
accepts those tags only when wolfBoot_find_header() returns
sizeof(uint32_t). Delta images produced by sign.py were therefore signed
with parseable TLVs yet rejected by the bootloader before the patch was
applied. Encode both size TLVs as 4-byte little-endian values, matching
sign.c (header_append_tag_u32) and the bootloader parser.
Add a regression test that signs a real delta image with sign.py and
asserts the bootloader-side parse recovers each delta TLV with the
required 4-byte length.
load_linux() computed the protected-mode kernel size as the uint32_t
product param.hdr.syssize * 16 (src/x86/linux_loader.c), where syssize
is copied verbatim from the (authenticated) bzImage at offset 0x1f4.
The multiplication wraps for any syssize > 0x0FFFFFFF: syssize=0x10000000
yields kernel_size=0 (DoS), and syssize=0x1FFFFFFF/0xFFFFFFFF yields
kernel_size=0xFFFFFFF0 (~4 GiB). That value fed straight into
memcpy((uint8_t*)KERNEL_LOAD_ADDRESS, linux_image + param_size,
kernel_size) with no cap, overwriting wolfBoot stage2, FSP data, and the
heap (CWE-190 -> CWE-680).
Fix at the root: linux_kernel_size() computes syssize * 16 in 64-bit and
rejects the image (panic) when the result is zero or does not fit in the
destination window [KERNEL_LOAD_ADDRESS, tolum). tolum is the top of low
usable memory the FSP already reports and that the ELF boot path uses as
its load upper bound (src/boot_x86_fsp_payload.c). The kernel load only
runs under WOLFBOOT_FSP (the non-FSP path panics earlier at the memory
map step), so tolum is always available there.
Add unit-linux-loader-syssize regression test (x86 32bit, standalone)
that feeds the PoC overflow values and asserts they are rejected while a
legitimate kernel and the exact-fit boundary are accepted.
e820_add_entry_cb() appended every FSP-supplied resource descriptor into
boot_params->e820_table[] with no check against E820_MAX_ENTRIES_ZEROPAGE
(128). A HOB list with more than 128 EFI_HOB_TYPE_RESOURCE_DESCRIPTOR
entries therefore wrote FSP-controlled addr/size/type triples past the
fixed-size table into the stack-allocated boot_params in load_linux(),
corrupting adjacent fields and the saved return address.
Reject any entry once the table is full (return non-zero, which aborts the
HOB iteration). e820_entries stays uint8_t since it is a fixed-offset field
in the Linux zero-page layout and the cap makes the 256 wrap unreachable.
Add unit-linux-loader-e820 regression test (x86 32bit, standalone) that
feeds 200 descriptors and asserts the table never overflows.
src/fdt.c, include/fdt.h
- Propagate fdt_fixup_initrd error in fit_load_ramdisk so a /chosen
patch failure no longer silently boots a kernel with no initrd.
- Add fit_load_image_to(): decompress (or memcpy) directly to a
caller-supplied destination buffer instead of going through the
FIT-declared `load` address. fit_load_ramdisk now uses this when
WOLFBOOT_LOAD_RAMDISK_ADDRESS is set, so the override is a real
safety bound for compressed ramdisks (previously the gzip stream
was still inflated to the FIT `load` and only memcpy'd afterward).
- Refactor fit_load_image_ex into a shared inner helper.
- Reword the WOLFBOOT_FIT_MAX_DECOMP comment: the cap is a sanity
ceiling, not a per-destination memory-safety bound. Authenticity
is provided by the outer wolfBoot signature; tighter bounds need
fit_load_image_ex / _to with an explicit out_max / dst_max.
- Add WOLFBOOT_FIT_MAX_RAMDISK (defaults to WOLFBOOT_FIT_MAX_DECOMP)
so targets can pin a tighter ramdisk decompression bound.
src/update_ram.c, src/update_disk.c
- Panic when fit_load_image() returns NULL for the kernel subimage
instead of letting load_address=NULL propagate into do_boot().
tools/unit-tests/unit-gzip.c
- Add deterministic stored / fixed-Huffman / dynamic-Huffman gzip
fixtures so the inflater's BTYPE 00/01/10 paths are exercised
independent of host gzip(1) heuristics.
- Add FEXTRA / FNAME / FCOMMENT / FHCRC and combined-flag fixtures
plus a truncated-FEXTRA negative case to cover the optional gzip
header parser.
tools/unit-tests/unit-fit-gzip.c (new), tools/unit-tests/Makefile
- New libcheck binary covering the FIT loader's compression
branches: gzip success, gzip stream corruption, unknown
compression, compression="none" baseline, and the no-load
fail-closed path. Built twice from the same source - once with
WOLFBOOT_GZIP for the success / runtime-failure paths, and once
without it so the compile-time fail-closed branch is also tested.
New src/gzip.c implements DEFLATE (RFC 1951) plus the gzip wrapper
(RFC 1952) from the RFC text only. Single-pass inflate, no allocations:
the output buffer doubles as the LZ77 sliding window, so back-references
read from out[out_pos - distance]. Canonical Huffman decode using
counts[] / symbols[] tables, ~10x smaller code than fast lookup tables
which matters in the bootloader. CRC32 + ISIZE verified against the
gzip trailer. Gated by WOLFBOOT_GZIP.
include/gzip.h carries the public entry point plus the RFC-canonical
constants (magic bytes, CM=DEFLATE, fixed Huffman boundaries, EOB
symbol, dynamic block field widths, run-length repeat metadata, CRC32
init/final-XOR, header/trailer sizes, alphabet sizes) so future
maintainers can cross-reference the RFC sections by name instead of
chasing literal numbers.
Tests in tools/unit-tests/unit-gzip.c round-trip 6 corpora through host
gzip(1) and back through wolfBoot_gunzip (empty, short text, all-zeros,
structured text, pseudo-random, ~2 MB kernel-sized). 9 negative cases
cover bad magic, bad CM, reserved FLG bits, truncated header,
truncated DEFLATE body, CRC32 mismatch, ISIZE mismatch, output overflow,
and NULL parameters. All 15 pass under libcheck.
Pass WOLFBOOT_ORIGIN and BOOTLOADER_PARTITION_SIZE to unit-update-ram and provide a hal_flash_protect stub so the update_ram harness still builds after bootloader protection was added.