wolfSSL v5.9.4-stable, wolfPSA v5.9.4, wolfCOSE v2.0.0; wolfHSM
left at v1.5.0-8-g86dd6df. wolfPSA v5.9.4 changes the store Close
return to int; wolfCOSE v2.0.0 splits the source into 15 files and
drops the deprecated ES256 alg ID, so DICE now signs with ESP256.
The F-14470 fix changed the info-request entry width to 16-bit on a
misreading of the spec: Multiboot2 v2.0 defines mbi_tag_types as
u32[n] ("an array of u32's"), and the original code already divided
by sizeof(uint32_t). A 32-bit request {4} was misread as {4, 0}, so
the follow-up zero-skip was only papering over the misparse; real
FSP images with a mandatory request tag stopped booting (fsp_qemu
CI). Restore the u32 width, drop the zero-skip, and rebuild the
test fixtures with 32-bit entries.
F-14470 corrected the request-list entry width to the spec's 16-bit,
which doubled the entry count. Intel FSP pads the list so the last
entry is zero (the end-tag type), and the old 32-bit width never
reached it. A zero entry is padding, not a mandatory request: skip
it, so FSP images keep booting. New test covers a zero-padded list.
loadFile() in the TPM policy-signing tool read the private key
through a fully buffered stdio stream, leaving a libc-owned copy of
the key material that outlives the call. Open the file unbuffered
(setvbuf _IONBF), the same pattern sign.c already uses for its key
reads.
nsc_mech_prepare() read the non-secure legacy ulPasswordLen pointer
twice: once to size the secure password copy, once again (via nsc_in)
to fill the secure length word the library dereferences. A concurrent
non-secure write between the two reads could desync the length from
the password buffer size. Take a single snapshot and write it into
the secure length word directly. Add the missing test for the legacy
branch: the stub now consumes the length the way wolfPKCS11 does
(through the bounced pointer) and checks scrubbing.
mb2_build_boot_info_header() rejected a valid OS image header when
the optional information-request tag was missing, and rejected any
request type it did not implement without checking the tag's
optional flag (bit 0) the spec defines for skipping. A well-formed
header without the tag now requests nothing, optional unsupported
requests are skipped, and only a mandatory unsupported one rejects
the image. The finder returns NULL for a missing tag and for a
malformed tag list alike, so the absent case walks the list to tell
the two apart: any corrupt tag rejects the header. Tests updated to
the spec semantics (optionality driven by the tag flags) plus
malformed-list cases.
qspi_quad_enable() memset the status register it had just read before
setting the QE bit, so the write-back zeroed every other writable bit
(SRL/CMP in SR2, BP/SRWD on the SR1 path) on the first probe of a part
with QE clear. Drop the memset so the write is a read-modify-write.
Test pins the preserved-bits write and the no-write sunny path.
wolfBoot_record_verify_failure() read the image version from
img->hdr, which for an external-flash image is a device offset, not
a CPU address: the persistent record stored a garbage version, and
the read can fault where the offset is unmapped. Read the version
through wolfBoot_get_image_version(part), which fetches external
headers via ext_flash_check_read. No unit test compiles
update_flash.c with WOLFBOOT_PERSIST_FAILURE_STATUS, so add a
compile check for that ext-flash configuration to the gate.
wolfBoot_get_partition_state() only rejected PART_NONE, so any other
part byte (SWAP, SELF, unknown) passed through the NSC veneer to a
NULL dereference of get_partition_magic() in secure code. Reject
every part without a trailer at the shared entry; the NSC veneer
delegates here, so a non-secure caller can no longer crash the
secure world. The mock get_trailer_at() now matches the real one
(NULL for parts without a trailer), which is what lets the test
prove the deref.
The parser read the SEQUENCE length without checking it, skipped the
AlgorithmIdentifier without comparing it, and ignored trailing bytes,
so only the digest length was pinned. Any free byte in the message is
forgeable with a low-exponent key (e=3) via Bleichenbacher 2006, and
keygen -i imports exponents unchecked. Require the SEQUENCE to span
the whole payload, the AlgorithmIdentifier to match the configured
hash OID byte for byte, and the OCTET STRING to end at the payload
end. Test pins the exact wc_EncodeSignature() encoding and rejects
trailing bytes and the empty-algoid forge shape.
Re-measured all 22 stm32f407 configurations in the CI container
(ghcr.io/wolfssl/wolfboot-ci-arm:v1.0). Growth comes from the uart
printf %u/is_signed change, which is compiled into every build. RSA,
RSAPSS, LMS and ML_DSA measured 8B smaller than their limits; limits
left as-is (shrinks are not ratcheted down).
Programming IMG_STATE_NEW (0xFF) over the update state is a no-op on
NOR flash: programming can only clear bits, so 0x70 (UPDATING) & 0xFF
leaves 0x70 and the update is re-attempted on every boot. Erase the
update partition's trailer sector(s) instead, exactly as the swap
path's final erase does - an erased trailer reads as NEW through the
state machine (the magic is gone, so the state read fails and
wolfBoot_start keeps its IMG_STATE_NEW initial state).
The unit test now asserts the trailer was erased (erased_update >= 1)
and that the state read fails after the update; it fails against the
old program-based fix.
The keytools read and write private keys through fully buffered
stdio streams: for key files smaller than the stdio buffer, libc
copies the raw key bytes into its internal FILE buffer, and
fclose() frees that buffer without scrubbing it, so a copy of
every key survives the explicit zero_and_free()/wc_ForceZero()
wipes. Open every secret-material stream unbuffered with
setvbuf(f, NULL, _IONBF, 0) right after fopen():
- sign.c load_key(): the private key file. Also close the
stream on the short-read failure path, which previously
leaked it.
- sign.c make_header_ex(): the firmware encryption key/IV file.
- keygen.c: the five private-key write sites (RSA, ECC,
Ed25519, Ed448, ML-DSA).
- lms_common.h / xmss_common.h: the LMS/XMSS private-state
write, verify-read and read callbacks.
Public-key export streams are left buffered: not secret.
Host-side hardening only: whoever can read the tool heap can
read the key file too.
wolfBoot_unseal_blob, wolfBoot_read_blob and wolfBoot_check_rot
set the WOLFTPM2_DEV auth slot 0 with the blob/RoT auth value but
only cleared the policy session (slot 1), leaving the auth value
in SRAM after the call. Add wolfTPM2_UnsetAuth(&wolftpm_dev, 0)
on every exit path, matching the existing timestamp/quote
pattern in the same file.
wolfBoot_tpm2_deinit() scrubbed the session and SRK objects but
not the device object, which also holds the last auth slot
contents and the last command/response buffer. Add
TPM2_ForceZero(&wolftpm_dev, sizeof(wolftpm_dev)) on all build
paths, completing the 'leave no session key or SRK auth in SRAM'
intent the deinit comment already states.
Unit tests assert the slot is unset after unseal/read/check_rot
and that deinit scrubs the device object last.
The u/i/d conversions shared one branch that read the argument as int
and ran it through the signed decimal path of uart_writenum, so %u
values >= 2^31 printed as negative numbers (0xFFFFFFFF became -1).
Read %u as unsigned int and add an is_signed parameter to
uart_writenum so the sign handling applies only to d/i, matching the
is_negative design of the 64-bit helper.
The direct update->BOOT copy left the update partition at
IMG_STATE_UPDATING, so wolfBoot_start() re-entered wolfBoot_update(0)
on every boot: a wasted verification, and with ALLOW_DOWNGRADE a full
BOOT re-flash on every power-up. Reset the update state to NEW after
the update succeeds, as the swap path's final erase does; the write is
best effort, so in setups where the bootloader cannot write the update
partition the documented application requirement covers it. Add a
DISABLE_BACKUP unit variant with a state-consumption test.
The final swap phase ignored the return values of the staging sector
backup, the boot trailer erase and the restore copy, so a failed copy
still marked BOOT as TESTING and erased the UPDATE trailer. Check each
operation and abort with the flash locked before advancing the state;
the existing resume machinery in wolfBoot_start then retries or rolls
back. Add failing-copy tests for the backup and restore paths.
Makefile no longer says a scan matches wolfssl:wolfcrypt. Re-vendor
wolfGlass at 48796ea so the generator contract exempts nested wolfcrypt.
Co-authored-by: Cursor <cursoragent@cursor.com>
The G0 and G4 files carry the same double-word fast path and were
fixed in the same commit, but only the G4 copy had a regression
test. Clone the G4 harness with the G0 register set (BSY1, 4-bit
W1C clear).
Returning -1 for a truncated token contradicted the documented
contract (token length, 0 at end of list) and skipped the caller's
existing skip path for unmatchable names. Return the truncated
length: env_leftkey fails to match it and the slot is skipped.
The MCXA ROM API (fsl_romapi.h) takes lengthInBytes, not a sector
count, so the original pass-through was already correct and the
sector-count conversion erased almost nothing. It also called
FLASH_GetFlashConfig, which does not exist in the MCXA ROM API and
only compiled against the unit-test stub. Restore the byte length,
keep the len guard, and drop the invented call from the stub.
The dispatcher split cross-bank requests at FLASH_BANK2_BASE_REL
(0x00100000), but callers pass absolute flash addresses in
FLASHMEM_ADDRESS_SPACE, so the split never fired and the whole
request was programmed on the bank of the start address. Compare
against FLASH_BANK2_BASE (0x08100000); the test now drives
absolute addresses so it exercises the real call convention.
A token longer than the name buffer left its remainder in the stream,
which the next call read as a second, spurious slot name. env_next_name
now consumes the whole token and returns -1; the select_slot gate
treats <= 0 as malformed. Adds unit tests driving the static directly
(70-char token with and without a trailing valid name).
hal_flash_erase() selected the bank geometry from the start address
only; a range extending past the end of that bank was partially
erased and reported as success. Derive the bank end from the sector
geometry and reject cross-bank requests before erasing anything.
New unit test extracts the real hal_flash_erase() and runs it against
an emulated two-bank FAPI.
HFROSCCFG_DIV_SHIFT(d) masked the shifted value with HFROSCCFG_TRIM
(bits 16-23) instead of HFROSCCFG_DIV (bits 0-3), so the divider
always programmed zero.
- mask with HFROSCCFG_DIV
- add unit-hifive1-hfrosccfg pinning the field macros (value,
overlap, and the composed init register value)
wolfBoot_get_blob_version() returns uint32_t, but the value was
assigned to 'int ret' and rejected when 'ret <= 0'. Versions with
bit 31 set (e.g. 0x80000001) are valid unsigned versions and were
silently rejected with 'No valid image found'.
- check the version as uint32_t, rejecting only version 0
- add test_ramboot_high_bit_version (version 0x80000001 ramboots)
- ext_flash_read() backends return the number of bytes read; the
ramboot payload copy only rejected negative returns, so a positive
short read left a truncated image in the RAM load region (executed
when WOLFBOOT_SKIP_BOOT_VERIFY is set)
- require ret == img_size, mirroring the header read in the same
function and the wolfBoot_start() copy (F-12065 fix)
- unit test: mock short read on the payload copy aborts ramboot
uart_init() initialized a local to zero and tested that local, so the
LCR stop-bit field was always zero (one stop bit) and stop values
other than 1/2 were silently accepted. Validate stop against 1/2 and
derive the field from the parameter.
Add unit-x86-uart (linked real driver, io_write8/io_read8 mocked)
covering 8N1/8N2/5O2 LCR encodings plus stop-count and existing
argument rejection; 3 of 5 tests fail pre-fix.
A negative '*' width passed the upper-bound-only clamp and reached
the zero-pad memset as a huge size_t (stack overflow). Clamp to zero
at the parser, the single source of dynamic widths; the existing
zero-to-8 default then applies.
New test test_uart_printf_negative_star_width covers %0*x and
%0*llu; SIGSEGV pre-fix, green post-fix. Full unit suite green.
The UART formatter recognized 'l' and 'z' length modifiers and %p,
but still consumed the argument as int, truncating long, size_t and
pointers on 64-bit targets (undefined behavior). Track the 'z'
modifier and, where PRINTF_LONG_LONG is enabled, consume long /
unsigned long / void * and format through the 64-bit
uart_writenum_ll() path; the 32-bit path is unchanged (long == int
width there).
New test test_uart_printf_64bit_args covers %ld, %lu, %zu, %p, %lx
with values past 32 bits plus %lld and %d regressions; fails
pre-fix (truncated output), green post-fix. 32-bit compile of
string.c verified with -m32 -Wall -Wextra.
The update_store_size()/store_live_size() guards only bounded the
first byte of the 32-byte struct obj_hdr (and used '>'), so a header
offset in (SECTOR_SIZE-20, SECTOR_SIZE] would write/read the 'size'
field into the adjacent cache slot. Compute the offset first and
reject anything past SECTOR_SIZE - sizeof(struct obj_hdr), in all
three copies (pkcs11 update_store_size, pkcs11 store_live_size,
psa update_store_size).
New tests forge a header at SECTOR_SIZE-16 and assert the write is
rejected without touching the adjacent slot (pkcs11) / without any
flash traffic (psa). Full unit suite green.
A corrupted node-table pos was used to compute the object address
without a range check, unlike bitmap_put() which rejects positions
outside KEYVAULT_MAX_ITEMS. Both storage implementations now delete
the node and return NULL when pos is out of range, matching the
existing "cannot recover object payload" path.
Verification: new test seeds the node table with pos =
KEYVAULT_MAX_ITEMS and matching id's in the backup sector, so the
unfixed restore path reaches the out-of-range slot (mock flash
fails on the erase); fails pre-fix in unit-psa_store and
unit-pkcs11_store, 10/10 and 13/13 post-fix; full
make -C tools/unit-tests run green.