Commit Graph

195 Commits (42e6dba558a27e3162b5b0bf5a4dd24075923b68)

Author SHA1 Message Date
Daniele Lacamera 42e6dba558 pkcs11 store: invalidate the flash read cache after every commit
The store commits sectors with hal_flash_erase()/hal_flash_write() and
then reads them back through the memory map - sector_ptr(),
cache_get_sector()'s refill, and the raw magic reads in check_vault().
On a part that caches flash reads (STM32 ICACHE) those reads can return
pre-erase bytes. check_vault() is the worst case: a stale magic there
does not merely read wrong, it triggers restore_backup() or a full vault
re-initialisation, losing the token.
2026-09-10 10:45:55 +02:00
Daniele Lacamera 609294789f Address Fenrir PR review: make program-window observable in l5/u5 write tests
The new unaligned tests passed against the pre-fix HAL (identical
final bytes), so the alignment fix had no regression coverage. Mock
hal_flash_wait_complete now diffs the flash per program window and
asserts the changed bytes fit in one aligned unit; the 20-byte
unaligned test goes red on the pre-fix HAL (l5: bytes 4-11 across two
8-byte units, u5: bytes 4-19 across two 16-byte units).
2026-09-07 19:19:22 +02:00
Daniele Lacamera 8e86847c1f F-12870: STM32C0: program flash writes through absolute addresses
The HAL contract is absolute (0x08000000-based) addresses, as the
erase path and every NVM caller use, but the double-word fast path
added the flash base on top and targeted an address space past the
flash. The read-modify-write path located its unit from the request
base indexed by i/4, landing in the wrong 8-byte unit once a request
started inside one. Both paths now program the 8-byte unit at
(address + i), and the now-dead FLASHMEM_ADDRESS_SPACE define is
gone.

Add unit-stm32c0-write: runs the extracted hal_flash_write() against
a host register/flash model with a second mapping standing in for
the wrong address space (4/5 checks fail pre-fix, 5/5 pass post-fix).
2026-09-07 16:05:51 +02:00
Daniele Lacamera 55711243c6 F-11026: image: declare wolfHSM DER sig length as word32
wc_ecc_rs_raw_to_sig() takes a word32* outlen, but the wolfHSM
client/server path in wolfBoot_verify_signature_ecc() declared the
buffer length as size_t and cast the pointer. On a 64-bit
big-endian target the API reads the high (zero) half, so the DER
conversion sees outlen 0 and the write-back lands in the wrong
half of the size_t.

Declare tmpSigSz as word32 and pass &tmpSigSz directly; this also
matches the word32 sigLen of wc_ecc_verify_hash() below.

Add a host compile check for the WOLFBOOT_ENABLE_WOLFHSM_CLIENT
build of image.c, which unit CI did not cover (only the PIC32CZ
cross build).
2026-09-04 11:24:13 +02:00
Daniele Lacamera a2d5543ebb F-9740: update_disk: FSP low-mem rejection falls back to other slot
The WOLFBOOT_FSP low-memory size check was the only per-slot rejection
in the boot retry loop that used a bare break, so an image whose
header-declared fw_size exceeded the tolum window aborted the boot
instead of trying the other slot. Every other rejection in the same
loop switches partitions and retries; match that.
2026-09-04 10:32:13 +02:00
Daniele Lacamera 88af5b6472 F-7390: x86 fsp: drop dead FSP auth scaffolding, fix comment
STAGE1_AUTH only authenticates the stage2 wolfBoot payload; the FSP-M and
FSP-S blobs are executed unverified. Remove the dead fsp_m/ret
declarations in start(), the orphaned .sig_fsp_s placeholder section
(no stage1 linker script places it), and correct the comment that
claimed the FSPs were authenticated. Add a compile check for the
STAGE1_AUTH variant (unit-x86-fsp-stage1auth-build.py), which the unit
test CI never builds for lack of an i686 toolchain.

Verification: full unit suite 1096 checks, 0 failures; both
STAGE1_AUTH variants of boot_x86_fsp.c compile clean; no references to
sig_fsp_s remain.
2026-09-04 10:00:54 +02:00
Daniele Lacamera 455c1c6cc6 F-7067: elf scatter: fix PART_IS_EXT arg and check load result
In the DISABLE_BACKUP branch of wolfBoot_update() the ELF-scatter restore
block passed the boot struct by value to the pointer-taking PART_IS_EXT
macro, so DISABLE_BACKUP + WOLFBOOT_ELF_FLASH_SCATTER + EXT_FLASH did not
even compile, and the load result was discarded. Mirror the
wolfBoot_start() pattern (PART_IS_EXT(&boot), panic on load failure), drop
the dead base local, and add a compile check for the combination
(unit-elf-scatter-db-build.py) guarding the one test that needs a
DISABLE_BACKUP-excluded symbol.

Verification: full unit suite 1096 checks, 0 failures; new test fails
pre-fix (struct vs pointer compile error), passes post-fix.
2026-09-04 09:52:22 +02:00
Daniele Lacamera f598b3fd41 F-6878: delta: reject non-multiple SECTOR_SIZE/DELTA_BLOCK_SIZE at build
The per-sector fill loop in wolfBoot_delta_update() advances in
DELTA_BLOCK_SIZE steps, so a WOLFBOOT_SECTOR_SIZE that is not a multiple
of DELTA_BLOCK_SIZE writes past the one-sector SWAP partition and
misaligns the resume path. Enforce the invariant with a #error and add a
negative build test (unit-delta-sector-align.py).

Verification: full unit suite 1096 checks, 0 failures; new test fails
pre-fix (misaligned config built), passes post-fix (build rejected).
2026-09-04 09:46:27 +02:00
Daniele Lacamera 6dabdad01e F-12114: pkcs11: wipe the login PIN in crypto deinit
pkcs11_pin is a file-scope copy of the compile-time credential
passed to C_Login() for the token holding the firmware-decryption
key. pkcs11_crypto_deinit() runs on the pre-handoff path but only
closed the session, leaving the credential in retained bootloader
memory where a post-handoff attacker could recover it and
authenticate to the token.

Wipe the copy (volatile zeroize) after the final C_CloseSession().
No re-init path exists after deinit in the product flow (init is
only called from the verification paths), so wiping inside the
deinit is safe.

Add unit-pkcs11-pin-zeroize: a full init/deinit cycle with a
stubbed PKCS#11 backend that asserts the pin copy is all zero
after deinit and the session was closed, plus a no-session deinit
safety case.

Verification: unit-pkcs11-pin-zeroize 1/2 pre-fix (pin byte 0 not
wiped), 2/2 post-fix; full unit suite green; sim build green;
kontron_vx3060_s2 CI build green.
2026-09-02 12:15:51 +02:00
Daniele Lacamera 4c447f4bf9 F-12104: Kontron TGL: apply the SPI BIOS-region lock through the BAR
tgl_lock_bios_region() wrote the protected range and the FLOCKDN
value through PCI configuration space (offsets 0x48 and 0x04, the
status/command dword) instead of the SPI controller's
memory-mapped registers at the BAR0 base, and took the range from
FREG1 (non-BIOS) instead of FREG0 (BIOS). The lock now writes
FPR0 and BIOS/H SFSTS/CTL through mmio_write32(), verifies both
by readback, and returns an error if the bits do not stick.

The helper had no callers: no hal_flash_protect() override
existed, so the weak no-op default ran before handoff and the
BIOS region stayed writable. Add the override routing to
tgl_lock_bios_region().

Including <hal.h> for the hook signature also exposes the
hal_flash_write/hal_flash_erase stubs as mismatching the HAL
contract; fix their address parameter to haladdr_t.

Add unit-kontron-tgl-spi: runs the extracted
tgl_lock_bios_region() and hal_flash_protect() against mocked
PCI config space and an MMIO array at the BAR address (build
fails pre-fix - hal_flash_protect undefined - 4/4 pass
post-fix).
2026-09-02 12:15:51 +02:00
Daniele Lacamera 20c9031965 F-12064: P1021: check bad-block markers per erase block
ext_flash_read() initialized its bad-block page counter once per
request, so the marker was inspected only on the first two pages
read and a bad erase block later in the request was delivered as
valid data. Restart the counter at the start of each erase block.

The skip path also rewound the logical position to a block
boundary without rewinding the output pointer, so a marker found
after some pages had been delivered continued the read past the
end of the caller's buffer. pos and data already agree
(data = original + pos) after any delivered pages, so the skip
only advances the source address.

Add unit-p1021-read-badblock: runs the extracted
ext_flash_read() against a mocked ELBC on a simulated NAND with
three 16 KiB blocks (2/5 checks fail pre-fix, 5/5 pass
post-fix).
2026-09-02 12:15:51 +02:00
Daniele Lacamera 6792b5da01 F-12063: STM32WB: require a full double word in the fast write path
The double-word fast path was selected on 'len - i > 3' but always
reads and programs two 32-bit words, so an aligned 4-7 byte tail
read up to four bytes past the caller's buffer and programmed them
into flash. Require at least eight remaining bytes; shorter tails
fall to the RMW branch, which rewrites the unit with the
out-of-range bytes read back from flash.

Add unit-stm32wb-write: runs the extracted hal_flash_write()
against a host register file with stale destination flash and a
source canary (3/5 checks fail pre-fix, 5/5 pass post-fix).
2026-09-02 12:15:51 +02:00
Daniele Lacamera 40d448b771 F-12062: STM32L4: require a full double word in the fast write path
The double-word fast path of hal_flash_write() was selected on
"len - i > 3" but always reads and programs two 32-bit words, so
an aligned 4-7 byte tail read up to four bytes past the caller's
buffer and programmed them into flash. Require at least eight
remaining bytes before taking the fast path; shorter tails fall to
the RMW branch, which rewrites the unit with the out-of-range bytes
read back from flash. Same fix as the STM32G4 twin (F-11023).

Add unit-stm32l4-write: runs the extracted hal_flash_write()
against a host register/flash model with a canary after the source
(3/5 checks fail pre-fix, 5/5 pass post-fix).
2026-09-02 12:15:51 +02:00
Daniele Lacamera 5c22c271e8 F-12061: RP2350: RMW partial pages in hal_flash_write
flash_range_program() requires a page-aligned address and a
page-multiple length (pico-sdk ROM, invalid_params_if on both). The
partition-state path without NVM_FLASH_WRITEONCE issues 1-byte
(trailer) and 4-byte (magic) writes that violated the contract on
every state transition.

Keep the direct-program fast path for page-aligned page-multiple
writes; otherwise read the page back from XIP, merge the write, and
program the full page. The AND program keeps the trailer flag
accumulation intact.

Add unit-rp2350-flash-write: runs the extracted hal_flash_write
against a mock flash_range_program() that enforces the ROM contract
(4/6 checks fail pre-fix, 6/6 pass post-fix).
2026-09-02 12:15:51 +02:00
David Garske 95227f823c fdt: rewrite device tree parser with capacity bound and full validation 2026-09-02 10:48:52 +02:00
David Garske 664b09a075 update_disk: load signed image from a file when a filesystem is present 2026-09-02 10:47:33 +02:00
David Garske 1091fc6d45 ext4: add read-only extent-based ext4 filesystem support 2026-09-02 10:47:33 +02:00
David Garske f06f081a20 disk_fs: add read-only FAT32 filesystem support for disk boot 2026-09-02 10:47:33 +02:00
Daniele Lacamera b115363291 pci: accept MMIO pools whose exclusive end is 4 GiB
A pool ending exactly at 0x100000000 (e.g. 0xC0000000 + 0x40000000,
the classic top-half 32-bit MMIO layout) was a working configuration:
the old base + length - 1 initialization wrapped to 0xFFFFFFFF in
32-bit arithmetic. The overflow guard from the exclusive-limit fix
rejected such pools with base + length > 0xFFFFFFFF, aborting
enumeration - and the FSP caller discards the return value, so the
platform would boot with no PCI BARs programmed.

The limit fields cannot hold the exclusive end 0x100000000 while
32-bit, so widen mem_limit and mem_pf_limit (and the limit
parameters of pci_enum_next_aligned32 and pci_align_check_up, plus
the local in pci_program_bar) to 64-bit, and reject only pools whose
end is above the 32-bit space. The initialization now casts to
64-bit before the addition so the sum cannot wrap.

The T10xx PCIe setup initializes the same struct with the old
inclusive base + length - 1 form; align it to the exclusive
semantics the allocator enforces, or the last byte of the configured
pool is unusable.

New unit-pci-4gib build of the existing test file with the MMIO
pool [0xC0000000, 0x100000000): pci_enum_do() must accept the pool
and map a 1 MB BAR at the pool base. Fails on the old guard.
2026-08-26 18:59:48 +02:00
Daniele Lacamera 00cbbaeebb samr21: advance flash erase by the 256-byte row size
NVMCMD_ERASE (0x02) is the NVMCTRL row erase: one command erases a
256-byte row (4 pages). The erase loop advanced the address by one
page per iteration, so it issued four row-erase commands against the
same row - the second through fourth with a non-row-aligned address -
quadrupling erase time and wear on every flash erase.

Stride the loop by FLASH_ROW_SIZE (4 * FLASH_PAGESIZE). The row
containing a sub-row request is erased once, as the command
granularity requires.

Unit test updated to row semantics: a sub-row request ends on the
containing row, a two-row range advances to the second row, and an
exact row erases once with no extra row. All three fail on the
page-stride loop.
2026-08-26 18:55:05 +02:00
Daniele Lacamera 9b89b5e643 F-11037: scrub NVM_CACHE after the write-once update trigger
Under NVM_FLASH_WRITEONCE wolfBoot_update_trigger() stages a whole
flash sector into the file-scope NVM_CACHE before rewriting the
update partition flags. In EXT_ENCRYPTED builds that sector is where
the firmware key/nonce live (ENCRYPT_CACHE aliases NVM_CACHE, and
with FLAGS_HOME the update flags sit in the boot trailer), so after
an update trigger the plaintext key material sat in the buffer at a
fixed address. The partition-trailer helpers scrub the buffer with
nvm_cache_scrub() on return (F-9765); the write-once update path
copied the sector and never scrubbed it.

Scrub the staged sector after the final erase, before the flash lock
is released.

unit-update-trigger-scrub extracts the real function together with
nvm_cache_scrub() (Makefile, built with NVM_FLASH_WRITEONCE) and runs
the write-once branch over a staged sector carrying a key/nonce
pattern: one flags write and two sector erases are expected, and the
buffer must be zero after the call. Pre-fix the staged pattern
remained in NVM_CACHE.

Verification:
- Built: gcc (host) unit-update-trigger-scrub with
  -DNVM_FLASH_WRITEONCE: clean, no warnings.
- Tested: unit-update-trigger-scrub 1/1 (pre-fix: key pattern
  remained); unit-nvm-cache-scrub 3/3 (the non-write-once extraction
  build is unaffected).
- Pitfalls: the scrub runs unconditionally in the write-once branch,
  which has no early-return flash-error path; the non-write-once and
  external-flash branches stage nothing and are unchanged.
- Style: cstyle-check.sh FMT diff on src/libwolfboot.c byte-identical
  to the pre-change file; the new test is warning-free.
- Message: F-11037: prefix, no co-author trailers.
2026-08-26 17:41:22 +02:00
Daniele Lacamera c47135746a F-11032: bound the ACMD41 OCR readiness poll in SD card init
sdcard_card_full_init() polled ACMD41 in an unbounded do/while until
the card set OCR ready, so a card that answers every ACMD41 without
ever setting the bit held the bootloader in the loop forever. F-7984
bounded the separate DATA0/CMD13 waits in sdhci_wait_busy(); this is
the OCR readiness path, which still had no limit.

Bound the poll with the same shape as sdhci_wait_busy(): a
30000 ms budget (SDCARD_ACMD41_TIMEOUT_MS, #ifndef-able) measured
against hal_get_timer_us(), the watchdog serviced inside the loop,
and -1 returned to fail the SD boot path. A healthy card reports
ready in milliseconds, so the budget is far above any real
initialization time.

unit-sdhci-acmd41-timeout compiles the real driver (generated
sdhci_host.c, as in the wait-busy test) against a scripted
controller: commands complete without error, SRS12 is modeled
write-1-to-clear, and the card model sets OCR ready after a
configurable number of ACMD41 polls. A never-ready card must return
-1 inside the shipped budget (and service the watchdog); a card
ready after 5 polls must exit the loop promptly and proceed to the
end of the init path. A command-write cap turns the pre-fix infinite
loop into an abort instead of a hung build.

Verification:
- Built: gcc (host) unit-sdhci-acmd41-timeout with -DDISK_SDCARD:
  clean.
- Tested: unit-sdhci-acmd41-timeout 2/2; pre-fix the never-ready case
  aborted at the 50001st command (the loop never terminates); post-fix
  it runs 3001 polls, reaches the 30 s budget, pets the watchdog
  every iteration and returns -1.
- Pitfalls: the timeout returns -1 from the SD path, the same
  contract as a failed CMD0/CMD8; the budget is per init call, not
  shared with sdhci_wait_busy, and a card that becomes ready before
  the deadline is unaffected.
- Style: cstyle-check.sh FMT diff on src/sdhci.c byte-identical to
  the pre-change file; the new test is flag-free.
- Unverified: no SD card hardware execution.
- Message: F-11032: prefix, no co-author trailers.
2026-08-26 17:41:22 +02:00
Daniele Lacamera 300e5db110 F-11036: advance the page address in the SAMR21 erase loop
hal_flash_erase() used the length decrement as the unbraced body of the
NVMREADY wait loop. With the peripheral idle (NVMREADY set) the wait
body never ran, the length never shrank, and the outer loop re-erased
the first page of the range forever; whatever the wait duration, the
number of decrements tracked wait-loop iterations instead of completed
erases, and the address was never advanced, so later pages of the
requested range were never erased.

Brace the ready wait, and after a completed erase advance the address
by FLASH_PAGESIZE and decrement the length once, as the sibling
P1021 multi-block erase loop does (F-11034).

unit-samr21-erase-advance extracts the real function and register
macros and runs it against a host NVMCTRL window with NVMREADY preset
(an idle peripheral): a 128-byte range must end with page 0x1040
programmed, a 256-byte range with page 0xC0, and a single 64-byte
erase must complete. Pre-fix all three cases hang in the re-erase loop
and fail on the tcase timeout.

Verification:
- Built: arm-none-eabi-gcc -fsyntax-only -Wall -Wextra hal/samr21.c:
  clean.
- Tested: unit-samr21-erase-advance 3/3; pre-fix all three timed out
  (10 s tcase limit).
- Pitfalls: single-page erases and page-aligned ranges behave as
  before; a non-page-multiple len erases the final partial page's
  page, unchanged from the pre-existing decrement semantics.
- Style: cstyle-check.sh FMT diff on hal/samr21.c byte-identical to
  the pre-change file; the new test trips only the uncrustify
  START_TEST brace class the sibling unit tests trip.
- Unverified: no SAMR21 board execution.
- Message: F-11036: prefix, no co-author trailers.
2026-08-26 17:22:39 +02:00
Daniele Lacamera 83a1f73a2c Address Copilot review: test guard hygiene and clean list
Four review items on this PR, all valid:

- unit-hifive1-flash-write: the over-read test assumed the canary
  array landed right after the data array on the stack, which C does
  not guarantee (and the canary was filled but never read). Use a
  single contiguous buffer split into data and canary regions so an
  out-of-range read lands on known bytes, and assert the canary stays
  intact after the call.
- unit-fwtpm-rsp-overrun: rsp_fitting_capacity_gets_response checked
  the guard bytes with a loop bounded by rspSz, which the call had
  already overwritten to the produced size (10), so the loop never
  ran. Snapshot the offered capacity before the call and bound the
  guard check with that.
- unit-fdt-memrsv-wrap: put32() encoded header fields by calling
  fdt32_to_cpu(), which reads as the inverse operation. Call
  cpu_to_fdt32() directly, matching how production code writes FDT
  fields.
- Makefile: the five new extraction headers (fdt_memrsv, hifive1
  flash write, t10xx flash status, p1021 erase x2) were not in
  GENERATED_SRC, so make clean left them behind. Listed now.

Verification:
- Built: the three affected unit tests compile clean on the rebased
  branch.
- Tested: unit-hifive1-flash-write 3/3, unit-fwtpm-rsp-overrun 3/3,
  unit-fdt-memrsv-wrap 3/3; make -n clean now removes all five
  extraction headers.
- Pitfalls: the hifive1 test now passes an explicit request length
  (data is a pointer into the shared buffer, so sizeof would be
  wrong).
- Style: cstyle-check.sh flag count unchanged per file (1 pre-existing
  FMT pointer-alignment class each).
- Message: no co-author trailers.
2026-08-25 08:51:10 +02:00
Daniele Lacamera 1ecf2248c1 F-11045: validate the FDT layout before the memreserve memmove
fdt_add_mem_rsv() added the 32-bit string-block offset and size
without overflow checks. A wrapped data_end bypassed the capacity
check, and the same wrapped expression derived the memmove length, so
a malformed (or attacker-supplied) DTB produced a huge memmove -
broad boot-time memory corruption. fdt_check_header validates only
magic and version, so raw-DTB callers reach this code with
inconsistent layout fields.

Compute the block end in 64-bit, validate the layout before touching
it (structure block starts after the reserve map terminator, string
block after the structure block, shifted layout fits in totalsize),
and derive the move length only from the validated 64-bit end. The
reserve-map scan bound uses 64-bit arithmetic as well, so a wrapped
32-bit sum cannot pass it.

unit-fdt-memrsv-wrap extracts the real fdt_add_mem_rsv (plus the
byte-order helpers) and feeds it crafted DTB headers:
- a wrapped end below off_dt: pre-fix the memmove length wraps to
  ~2^32 and the process segfaults; post-fix rejected (-FDT_ERR_NOSPACE)
- a wrapped end inside [off_dt, total): pre-fix the layout was
  accepted (ret == 0) with a silently corrupted FDT; post-fix rejected
- a consistent layout: the entry is inserted, the terminator moves
  down one, structure and string blocks shift by 16 bytes, and the
  header offsets follow (regression guard, passed pre-fix as well)

Verification:
- Built: unit test compiles the extracted real function (host).
- Tested: unit-fdt-memrsv-wrap 3/3 post-fix; pre-fix (fix stashed)
  1 segfault + 1 assertion failure on the wrap cases, valid case
  passing - red demonstrated on both corruption modes.
- Pitfalls: the helper that builds the crafted DTB only writes block
  contents where the offsets fit the buffer, so the malformed cases
  cannot corrupt memory in the test itself before reaching the code
  under test; validation runs before any block access.
- Style: cstyle-check.sh flag count on src/fdt.c unchanged (1
  pre-existing FMT class); the new test trips only the uncrustify
  class the sibling unit tests trip.
- Message: F-11045: prefix, no co-author trailers.
- Unverified: no target build needed (pure C, host-compiled from the
  real source); fdt.c compiles as part of the normal wolfBoot build
  paths unchanged.
2026-08-25 08:51:09 +02:00
Daniele Lacamera 0412238414 F-11044: stage the fwTPM command in secure memory before processing
wcs_fwtpm_transmit() verified that cmd identifies non-secure memory
and then passed that mutable buffer directly to FWTPM_ProcessCommand.
The processor parses the packet more than once (authentication, then
handler execution), so a DMA-capable non-secure attacker who rewrites
the command buffer in the window between the two parses can make the
authenticated command differ from the executed command.

Copy exactly cmdSz bytes into a secure staging buffer after the range
validation and invoke the processor only on that copy: an NS DMA
master cannot rewrite secure memory, so both parses see the same
bytes. The command and response staging are zeroed before returning
(the response may carry auth tags or unsealed data).

unit-fwtpm-cmd-toctou includes the real fwtpm_callable.c and mocks
FWTPM_ProcessCommand with two parse points (authentication,
execution). The test plays the attacker, rewriting the NS command
buffer at the window between the parses - the mock may only touch the
caller's buffer, never secure staging, mirroring the hardware
boundary. Pre-fix the processor authenticated the original bytes and
executed the rewritten ones (test fails); post-fix both parses see
the original command.

Verification:
- Built: unit tests compile the real veneer (host, poisoned fwtpm
  headers, same pattern as unit-fwtpm-nv-oob).
- Tested: unit-fwtpm-cmd-toctou 2/2 (red demonstrated against the
  pre-fix veneer via git stash of the fix); sibling
  unit-fwtpm-rsp-overrun 3/3 and unit-fwtpm-nv-oob 4/4 after the
  change.
- Pitfalls: the staging copy happens after all NS range checks and
  before any processor access; zeroing covers the full staging
  buffers regardless of the produced length.
- Style: cstyle-check.sh flag count on src/fwtpm_callable.c unchanged
  (1 pre-existing); the new test trips only the uncrustify
  pointer-alignment class the sibling unit tests trip.
- Message: F-11044: prefix, no co-author trailers.
- Unverified: no CMSE/armclang build and no m33mu emulator run here
  (lib/wolftpm is not checked out in this tree); the trustzone-emulator
  workflow covers the full build on push.
2026-08-25 08:51:09 +02:00
Daniele Lacamera 82004cea3d F-11043: stage the fwTPM response before copying to the caller buffer
wcs_fwtpm_transmit() validated the caller-supplied response capacity
and then passed that buffer directly to FWTPM_ProcessCommand with
rspLen initialized to the capacity. The fwTPM processor emits a
10-byte TPM error response even for a malformed short command,
regardless of the offered capacity, so a non-secure caller offering
less than 10 bytes got an out-of-range write into its response buffer
before the wrapper compared rspLen against the capacity.

Process into a max-sized staging buffer inside the veneer and copy to
the caller's buffer only after verifying the produced length fits the
snapshotted capacity; when it does not fit, return TPM_RC_FAILURE
without touching the buffer. The staging buffer is file-scope static:
the CMSE secure callable is not preemptible, so no locking is needed.

unit-fwtpm-rsp-overrun includes the real fwtpm_callable.c and mocks
FWTPM_ProcessCommand to emulate the processor's behavior of writing
the full 10-byte error response no matter the offered capacity. A
6-byte capacity must leave the guard bytes past the capacity intact
(fails pre-fix: bytes 6-9 were overwritten); 16-byte and exact-10
capacities must receive the full response.

Verification:
- Built: unit tests compile the real veneer (host, poisoned fwtpm
  headers, same pattern as unit-fwtpm-nv-oob).
- Tested: unit-fwtpm-rsp-overrun 3/3 (pre-fix the short-capacity test
  caught the overrun at offset 6); sibling unit-fwtpm-nv-oob 4/4
  after the change.
- Pitfalls: the copy uses the snapshotted capacity taken before the
  call, so a concurrent NS write to *rspSz cannot widen the range;
  the NS_R/NS_RW checks are unchanged.
- Style: cstyle-check.sh flag count on src/fwtpm_callable.c unchanged
  from the pre-change file; the new test trips only the uncrustify
  pointer-alignment class the sibling unit tests trip.
- Message: F-11043: prefix, no co-author trailers.
- Unverified: no CMSE/armclang build and no m33mu emulator run here
  (lib/wolftpm is not checked out in this tree); the trustzone-emulator
  workflow covers the full build on push.
2026-08-25 08:51:09 +02:00
Daniele Lacamera 2765348e34 F-11035: size the final partial page from remaining bytes in hifive1 write
hal_flash_write() in hal/hifive1.c selected the page path and clamped
the partial-page length from the original total len instead of the
bytes still remaining (len - j). A page-aligned multi-page write that
ended in a partial page therefore took the full-page branch on the
last iteration: it read past the end of the caller's buffer and
programmed a full 256-byte page where only the remaining bytes were
requested, clobbering flash past the update range.

Compute remaining = len - j at the top of the loop and use it for both
the branch test and the rel_len clamp; j still advances only by the
bytes actually consumed (256 on the full-page path, rel_len on the
partial path).

unit-hifive1-flash-write runs the real extracted function against a
mock fespi model (FLASH_BASE points at a flash image buffer,
fespi_write_address/fespi_sw_tx program into it, and the RMW path
reads the image back through FLASH_BASE as on hardware). The
regression case is a 356-byte aligned write: the last page's tail must
stay erased, which fails pre-fix (the over-read bytes are programmed
instead).

Verification:
- Built: riscv-none-elf-gcc 15.2 -fsyntax-only -Wall with ARCH_RISCV:
  clean.
- Tested: unit-hifive1-flash-write 3/3; pre-fix the 356-byte case
  wrote non-erased bytes past offset 356 of the flash image.
  Unaligned single-page RMW and exact-full-page cases unchanged.
- Pitfalls: relative (sub-FLASH_BASE) addresses are accepted as-is by
  the function, which is what the test passes so the 32-bit address
  parameter never carries a 64-bit host pointer.
- Style: cstyle-check.sh flag count on hal/hifive1.c unchanged from
  the pre-change file; the new test trips only the uncrustify
  pointer-alignment class the sibling unit tests trip.
- Message: F-11035: prefix, no co-author trailers.
- Unverified: no HiFive1 board execution.
2026-08-25 08:51:09 +02:00
Daniele Lacamera f29309ed3f F-11034: advance the address in the P1021 multi-block NAND erase loop
ext_flash_erase() decremented the remaining length each iteration but
never advanced the address, so the derived page (address / page_size) was identical
every pass: the first block of the range was re-erased for the whole
loop and every later block was left intact (the caller then programmed
an update image into un-erased NAND). The sibling ext_flash_write()
loop already advanced address/pos/data and was not affected.

Advance address by block_size after each successful erase.

unit-p1021-erase-advance runs the real extracted function against
mocked ELBC register access and records the page programmed per erase
command: two blocks must hit page 0 then page 32 (16 KiB block / 512
page), and a failing command must stop the loop after one attempt.

Verification:
- Built: powerpc-linux-gnu-gcc -fsyntax-only -Wall with
  TARGET_nxp_p1021: clean.
- Tested: unit-p1021-erase-advance 2/2; pre-fix the second erase
  re-targeted page 0.
- Pitfalls: single-block erases (len <= block_size) behave exactly as
  before; the error path is unchanged (break on hal_flash_command
  failure).
- Style: cstyle-check.sh flag count on hal/nxp_p1021.c unchanged from
  the pre-change file; the new test trips only the uncrustify
  pointer-alignment class the sibling unit tests trip.
- Message: F-11034: prefix, no co-author trailers.
- Unverified: no P1021 board execution.
2026-08-25 08:51:09 +02:00
Daniele Lacamera 1ae15e3abb F-11033: propagate program/erase timeouts from the T10xx flash HAL
hal_flash_status_wait() returns -1 when the NOR does not settle its
status bits within the poll budget, but hal_flash_write() and
hal_flash_erase() discarded the result and returned 0 unconditionally:
a stuck program or erase reported success, and the update flow
continued as if the flash held the new image.

Capture every wait result and return it to the caller on the first
failure. No state-restore command is needed: this driver works through
the memory-mapped QPI window, where reads are plain loads and the
controller issues the read command per access, so there is no device
command state to recover.

unit-t10xx-flash-status runs the real extracted functions against a
mock QPI status model (offset 0 reports the DQ status byte: toggling
while busy, 0x44 after a program, 0x4C after an erase). A stuck
device burns the full 200 ms / 1.1 s poll budget with a no-op udelay,
so the timeout path runs in milliseconds.

Verification:
- Built: powerpc-linux-gnu-gcc -fsyntax-only -Wall with
  TARGET_nxp_t1024: clean.
- Tested: unit-t10xx-flash-status 4/4; pre-fix both timeout tests got
  ret == 0 from a stuck device, now -1. Success paths (write lands in
  the model, erase completes) unchanged.
- Pitfalls: the first failing page/sector now aborts the rest of the
  operation, which is the desired behavior (the caller aborts the
  update); no callers depended on the unconditional 0.
- Style: cstyle-check.sh flag count on hal/nxp_t10xx.c unchanged from
  the pre-change file; the new test trips only the uncrustify
  pointer-alignment class the sibling unit tests trip.
- Message: F-11033: prefix, no co-author trailers.
- Unverified: no T10xx board execution.
2026-08-25 08:51:09 +02:00
Daniele Lacamera e02a734e72 F-11027: abort the ELF load when an mmu_cb mapping fails
elf_load_image_mmu() skipped a segment (continue) when its mmu_cb
mapping failed, then kept loading the rest, published the ELF entry
point and returned success for a partially loaded image. The x86 FSP
payload path (boot_x86_fsp_payload.c) passes a real mmu_cb and only
panics on a non-zero return, so the buggy continue booted a payload
with a missing segment.

Return -6 with a fail-loud message, matching the program-header
clobber guard that aborts for the same reason: never silently drop a
a PT_LOAD segment.

unit-elf-mmu-fail fails the first segment's mapping and checks the
load is rejected with no entry point published; a second test checks
successful mappings still load the segments and publish the entry.

Verification:
- Built: unit test compiles elf.c (WOLFBOOT_ELF config); elf.c
  syntax-clean under the WOLFBOOT_FSP config (gcc -fsyntax-only).
- Tested: unit-elf-mmu-fail 2/2; pre-fix the failure-path test got
  ret == 0 (entry published for a partially loaded image).
- Pitfalls: no caller switches on the exact code (all check != 0);
  -6 is new and dedicated to the mapping failure.
- Style: cstyle-check.sh on src/elf.c flags pre-existing FMT/R1
  issues also present on the pre-change file; the new test trips the
  uncrustify pointer-alignment class the sibling unit tests trip and
  matches their local style.
- Message: F-11027: prefix, no co-author trailers.
2026-08-25 08:51:09 +02:00
Daniele Lacamera c9fcdddefb F-11024: pass full-width fields to wc_ecc_rs_raw_to_sig in the wolfHSM verify path
The wolfHSM verify path of wolfBoot_verify_signature_ecc() converts
the fixed-width raw R||S signature to DER with wc_ecc_rs_raw_to_sig().
It passed the minimal field sizes (mp_unsigned_bin_size) while leaving
the pointers at the start of each fixed-width field, so whenever R or
S had a leading zero byte the conversion encoded a zero-padded integer
with the low bytes truncated, and wc_ecc_verify_hash() rejected an
otherwise valid signature (each component has roughly a 1 in 256
chance of a leading zero).

Pass the full-width fields (point_sz for both): the raw signature is
fixed-width and left-zero-padded, and the conversion strips the
padding itself. The multiprecision sizing only existed to compute the
minimal lengths and is dropped with the fix.

unit-ecc-raw-der signs until a leading-zero signature shows up, then
checks that the minimal-size pattern rejects it while the full-width
pattern accepts it, and that both patterns agree for leading-zero-free
signatures.

Verification:
- Built: src/image.c syntax-clean with WOLFBOOT_ENABLE_WOLFHSM_CLIENT
  (gcc -fsyntax-only, __WOLFBOOT, ECC256/SHA256 config, partition
  stubs); normal library build via make test-lib.
- Tested: unit-ecc-raw-der 3/3: the minimal pattern rejects the
  leading-zero signature the full-width pattern accepts; both agree
  on leading-zero-free signatures.
- Pitfalls: no key material touched; the dropped mp values had no
  matching mp_clear before the fix either (stack variables).
- Style: cstyle-check.sh on src/image.c flags two pre-existing
  violations (L1869 anonymous union, L2079 C99 declaration) outside
  this change; the new test trips the same uncrustify
  pointer-alignment class the unit-stm32l5/u5-write twins trip and
  matches their local style.
- Message: F-11024: prefix, no co-author trailers.
- Unverified: no wolfHSM target builds in CI; the HSM branch was
  checked by syntax-only compile, not a full target build.
2026-08-25 08:50:42 +02:00
Daniele Lacamera 639866a50e F-11023: require a full double word in the STM32 fast write paths
The double-word fast path in hal_flash_write() on STM32G4, STM32C0
and STM32G0 was selected on 'len - i > 3' but programs an 8-byte
unit, so an aligned 4-7 byte tail read up to 4 bytes past the
caller's buffer and programmed those bytes into flash.

Require len - i >= 8 before taking the fast path. Shorter tails
fall through to the existing RMW branch, which rewrites the unit
with the out-of-range bytes read back from flash, so nothing past
len is read or programmed.

Add unit-stm32g4-write (same harness as the STM32L5/STM32U5 twins),
which fails on the 60-byte tail before the fix.
2026-08-25 08:50:42 +02:00
David Garske 527c63007e elf: read the ELF entry point before loading segments over it 2026-08-25 08:46:40 +02:00
David Garske a5c94502d4
Merge pull request #868 from danielinux/fenrir-fixes-2026-08-21
Fenrir fixes 2026 08 21
2026-08-24 13:34:37 -04:00
David Garske 8ec8443345 Fixes from skoll review findings 2026-08-24 12:47:34 +02:00
David Garske 88984858d5 Peer review fixes (thanks Jack) 2026-08-24 12:47:34 +02:00
David Garske 67c819bc0b cm4: add RAUC A/B slot selection via U-Boot env (wolfBoot replaces U-Boot) 2026-08-24 12:47:34 +02:00
David Garske 71b855f7b5 cm4: add wolfCrypt FIPS 140-3 support + CI, SDHCI unit test, review fixes 2026-08-24 12:47:34 +02:00
Daniele Lacamera 4769c723e9 name the full 32-bit gated set in the skip message
unit-sama5d3-ext-read joined the ENABLE_32BIT_TESTS gate but the info
line still only named the linux-loader tests, which misleads anyone
debugging a skipped suite.

Skoll review finding 5, 2026-08-21 wolfboot review.
2026-08-21 09:12:05 +02:00
Daniele Lacamera 04531cbf2d F-9751: Add positive E2E encrypted-update test, fix what it exposes
Add unit-update-flash-enc-full, the full end-to-end suite (forward
updates, rollback, empty boot, diffbase) against the encrypted
swap, plus a byte-for-byte fallback-IV roundtrip test.

The suite exposes two product defects:

- ext_flash_encrypt_write() partial-block re-syncs re-anchored the
  keystream at the standard-IV position once the one-shot fallback
  IV offset had been consumed by the initial set_iv, corrupting the
  tail of fallback-IV images. Capture the IV offset in effect at
  entry and re-apply it on every re-sync.

- wolfBoot_final_swap() called wolfBoot_set_encrypt_key() with the
  internal flash unlocked, but the backend expects the flash locked
  (it manages the unlock/lock around the key write itself) and ends
  with the flash locked. Lock before the call and drop the now
  redundant lock on the failure path.

Test plumbing for the encrypted target: update-partition writes in
the tests now go through the encryption-aware writer, as the update
tool does; the hand-rolled TLV headers use the sign tool's dense
layout (padding gaps are ciphertext in encrypted builds); and the
testing-flag sites anchor on the state trailer, which sits ahead of
the key/nonce region in encrypted builds.

Verified: unit-update-flash-enc-full 35/35, unit-update-flash-enc
8/8, unit-extflash + AES128/256/ChaCha20 variants 8/8 each, full
unit suite green, stm32wb + AES256 cross-build green.
2026-08-21 08:52:59 +02:00
Daniele Lacamera 00bc8b5823 F-6762: compare the TLV field budget in a 32-bit domain
wolfBoot_find_header() and the sign tool's re-parser checked each
field's 4+len against (uint16_t)(header_size - IMAGE_HEADER_OFFSET).
For any header of 64 KiB or more the cast wraps (0x10000 -> 0), so the
guard rejects every field and an image the tool signs cannot be parsed
by the bootloader - a pack/parse roundtrip break, fail-safe but fatal
for large TLVs (post-quantum signatures, big cert chains).

Compare in the uint32_t domain in both walkers. No shipped config
reaches this size yet (largest example is 12288), so this pins the
roundtrip for future large-header configs.

unit-parser-large-header (new) builds the walker with
IMAGE_HEADER_SIZE = 0x10008 - exactly the wrap boundary - and asserts
a 300-byte TLV and a 4-byte version field are located (both were
rejected pre-fix, proven against the pre-fix walker in a scratch
build).
2026-08-21 08:52:59 +02:00
Daniele Lacamera f5401b0e23 F-7380: make SAMA5D3 ext_flash_read byte-accurate for partial pages
ext_flash_read() ignored the intra-page offset of the start address (a
partial read returned bytes from the head of the page), copied sub-page
reads in 32-bit words (dropping a sub-word tail), and wrote a full NAND
page into the caller's buffer when a multi-page read ended mid-page
(overrunning it). The integrity check hashes the image in 64-byte blocks
from fw_base + offset, so any read crossing a page mid-block returned the
wrong bytes.

Walk the read page by page from the exact address: full aligned pages go
straight into the caller's buffer; partial first/last pages are staged
through the page buffer and copied from the column offset.

Proven by unit-sama5d3-ext-read, which extracts the real function and
runs it against an emulated device: unaligned starts, 1-3 byte lengths,
the 64-byte integrity-hash block pattern, page/block boundaries, and
multi-page reads with a short tail. Pre-fix 6 of the 8 checks failed.
2026-08-21 08:52:59 +02:00
Daniele Lacamera 57db8948d1 Remove tracker ids from code, condense comments
Drop the internal report numbers from source, test and Makefile
comments: they mean nothing outside the tracker and do not belong in
the tree. Condense the long inline comment blocks the review flagged,
and note in the raspi3 encrypted example and its docs that CI builds
but does not boot it, so the end-to-end path has no automated
coverage.

Comments and docs only; no functional change.
2026-08-18 19:48:12 +02:00
Daniele Lacamera 52a1b085e0 libwolfboot: scrub NVM_CACHE without pulling in wolfCrypt
Widening the misc.c include guard with NVM_FLASH_WRITEONCE made this
file include <wolfssl/wolfcrypt/types.h> and <wolfcrypt/src/misc.c> in
every such build, including the two that cannot resolve them:
tools/check_config has no wolfSSL include path, and the STM32Cube
test-app has no stm32wbxx_hal_conf.h. Both fail to compile.

Scrub with a local volatile byte loop instead and put the guard back.
That also suits the RAMFUNCTION callers better, since ForceZero() lives
in flash and must not be called while flash is being programmed.
2026-08-18 10:16:44 +02:00
Daniele Lacamera 5f26943e6f t10xx: map the 10G memac cell-index instead of skipping it
NXP's qoriq-fman3 dtsi numbers the 10G memacs 0x8/0x9, and phydevs
holds the 10G port at FM1_10GEC1. Bounding the raw cell-index against
the table traded an out-of-bounds read for a silently dropped MAC
fixup on exactly the DTBs that motivated the guard. Translate instead,
and skip only indexes with no slot.
2026-08-18 09:59:01 +02:00
Daniele Lacamera 88b637130c sdhci: service the watchdog inside the busy waits
sdhci_wait_busy() runs at the top of every transfer and can now spin
for the full 30 s budget. Neither loop petted the watchdog, so on a
platform with one a stuck card produced a reset instead of the clean
I/O error the timeout exists to give. The shared deadline between the
two loops is deliberate and now says so.

The weak sdhci_platform_wdt_pet() default moves out of the
SDHCI_BLOCK_VIA_PDMA guard, since the busy waits call it on every
build. The test drops its SDHCI_WAIT_BUSY_TIMEOUT_MS=50 override and
steps its timer 1 ms per read instead, so the shipped default is what
is tested, and it counts watchdog services.
2026-08-18 09:56:20 +02:00
Daniele Lacamera e5bb64a59a unit-tests: fix asm stripping, clean, and gitignore
The versal host copy deleted every asm statement, including the three
with output operands, leaving reads of uninitialised automatics -- at
-O0 an infinite loop in hal_delay_us(). Rewrite those three into
assignments from a host counter and only delete asm with an empty
output field; a new rule guard fails the build if any asm survives,
so a future one cannot be silently dropped.

clean now removes the 17 generated sources and the conditionally-built
tests, and .gitignore covers all 125 test binaries plus the generated
sources -- a full build left 30 untracked files before. The gnu-efi
skip announces itself like the adjacent multilib probe.
2026-08-18 09:50:42 +02:00
Daniele Lacamera a8afca38e3 unit-tests: delete extraction targets on error
The extract rules append with >> across several commands. Without
.DELETE_ON_ERROR: a failure partway through leaves a truncated header
with a fresh mtime that the next make treats as up to date.
2026-08-18 09:19:13 +02:00
Daniele Lacamera 87df758d1a unit-tests: generate the test extracts with sed instead of awk
The extraction rules added for the fenrir-fixes tests used awk one-shot
scripts. They are all plain 'print from line A to line B' jobs, which
is sed -n '/A/,/B/p' territory; replace every awk rule with sed (and
grep for the single WRITE_BLOCK_SIZE define the TI extract carries).

Two ranges cannot stop at the 4th '} QE_PACKED;' (sed ranges cannot
count), so those blocks are anchored on the unique line that follows
them: the qe_check_firmware() comment for the T10xx extract and
hal_fman_init() for the T2080 one (the signature trimmed with
$$d). The regenerated extracts are byte-identical to the awk output
except trailing blank lines/comments.

Full suite re-run: 123/123 green.
2026-08-18 08:07:22 +02:00