wolfBoot ships as source. Users build it in many ways. Before this
change, only the plain Make build could make an SBOM. So a user could not
make an SBOM for the build that the user runs.
This change adds one shared engine (tools/scripts/wolfboot-sbom.sh, which
calls wolfSSL gen-sbom) and a front end for each build system. Every
build makes a CycloneDX 1.6 and SPDX 2.3 document. The engine captures
the configuration with the host compiler, so the SBOM is the same for
GCC, Clang, LLVM, IAR, armcl, CCRX, and XC32.
Routes:
- Make, arch.mk, and vendor SDKs: make sbom TARGET=<t> SIGN=<a>
- CMake and the Pico SDK: cmake --build <dir> --target sbom
- IAR Embedded Workbench: ide-sbom/iar_sbom.py
- Any IDE with a compilation database: ide-sbom/compdb_sbom.py
- TI CCS, MPLAB X, Renesas, Xilinx: ide-sbom/route_through_sbom.sh
- Per-HAL component: make sbom-hal TARGET=<t>
- Zephyr module: ide-sbom/zephyr_sbom.py
Make the SBOM reproducible. The captured macros can hold an absolute host
path. For example, arch.mk passes -DPICO_SDK_PATH=$(PICO_SDK_PATH). The
driver now redacts each absolute path but keeps the macro name, so the
configuration record stays complete. Add --no-scrub for debug.
Add a validator (ide-sbom/validate_sbom.py) and a CI canary
(.github/workflows/test-sbom.yml) that runs and validates every route.
The canary also checks that no host path leaks into the SBOM.
Add docs/SBOM.md. The tools are product-neutral by design, so they can be
shared across wolfSSL products later without logic changes.
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
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.
dd0712ec added the disk_decrypted_header_clear()/disk_crypto_clear() pair
to the wolfBoot_start() panic paths that were missing it, but the FIT
flat-device-tree load failure was one more: when wolfBoot_fit_memcpy()
fails to relocate the DTS, wolfBoot_panic() is entered with
disk_encrypt_key/disk_encrypt_nonce still live in BSS, and that call never
returns on a real target.
Add unit-update-disk-fit, which drives wolfBoot_start() through the FIT
branch with DISK_ENCRYPT enabled and snapshots the module statics from the
WOLFBOOT_HOOK_PANIC hook.
hal_flash_erase() in hal/mcxw.c rounded the start address down with the
runtime pflash_sector_size (queried from FLASH_GetProperty() in hal_init())
but stepped address and len by the compile-time WOLFBOOT_SECTOR_SIZE. When
the two differ, a larger WOLFBOOT_SECTOR_SIZE steps over hardware sectors
inside the requested range and leaves them unerased, while a smaller one
issues erase commands at non-sector-aligned addresses. A zero size reported
by the driver would also divide by zero.
Take a local sector_size, fall back to WOLFBOOT_SECTOR_SIZE when the driver
reports zero and use it for the alignment and both loop steps, as
hal/mcxn.c already does.
Add unit-flash-erase-mcxw, using the existing WOLFBOOT_UNIT_TEST_FLASH_ERASE
guard convention to compile hal_flash_erase() in isolation without the NXP
MCUXpresso SDK headers.
With WOLFTPM_ADV_IO the TIS layer hands the raw command payload to the
HAL callback, so TPM2_IoCb() stages it in stack-local txBuf/rxBuf. Both
were left intact on the normal return and on the wait-state error return,
keeping a TPM command's plaintext authValue (and the response bytes) in
bootloader stack SRAM. Wipe them like TPM2_TIS_Read()/TPM2_TIS_Write()
already do for their own staging buffers in the non-advanced-IO path.
Adds unit-tpm-advio-zeroize, which drives TPM2_IoCb() through the write,
read, payload-error and wait-state-timeout paths with a mock SPI slave
and inspects the staging buffers afterwards.
wolfBoot_tpm2_read_cert() is a cmse_nonsecure_entry veneer. It dereferenced
the caller-supplied 'certSz' to bound-check 'cert' with
cmse_check_address_range(), then passed the same non-secure pointer to
wolfTPM2_NVReadCert(), which re-reads '*len' as the destination capacity
before copying the NV data (lib/wolfTPM/src/tpm2_wrap.c:7221). The length was
therefore fetched twice from non-secure memory with no snapshot in between,
so a racing non-secure agent could present a small capacity to pass the CMSE
check and enlarge it before wolfTPM's own check, making the secure world write
the certificate past the validated range and into adjacent Secure SRAM.
Single-fetch the capacity into a secure local before validating, hand wolfTPM
the local, and copy the result back, matching ns_outlen_begin() in
src/pkcs11_callable.c and the rsp_capacity handling in src/wolfhsm_callable.c
and src/fwtpm_callable.c.
Add unit-tpm-nsc-cert, which drives the veneer through a CMSE stub that models
Secure SRAM immediately after the validated non-secure buffer and a wolfTPM
stub that enlarges the non-secure length word in the race window. The
out-of-bounds write test fails before this fix and passes after it.
otp-keystore-gen.c reads the device root UDS into the stack buffer
`uds` and copies it into the heap buffer `otp_buf` at OTP_UDS_OFFSET,
then on every exit path calls free(otp_buf) without wiping it first,
and never clears `uds`. Both copies of the highest-value device secret
remain in the host process's freed heap chunk and stack frame.
Add a local secure_zero() helper (no wolfSSL dependency, matching this
standalone host tool's existing bare-gcc build) and call it on the
success path and on the write-failure/short-UDS-read error paths,
before free()/exit(), mirroring the zeroize-before-release pattern
already used elsewhere in this tree (src/x86/ata.c,
src/x86/ahci.c). Paths that exit before `uds` is populated are left
untouched since there is no secret to wipe yet.
security_command_passphrase() copies the plaintext disk-unlock secret
into the file-static DMA buffer `buffer` at ATA_SECURITY_PASSWORD_OFFSET
but never wipes it. On the slot<0 early return no ATA command is even
dispatched, and on failures of ata_security_set_password()/
ata_security_unlock_device() (which sata_unlock_disk() turns into a
panic()) no later IDENTIFY DMA ever overwrites it, so the secret is
left resident in BSS for as long as the device stays powered.
Zeroize the password field on the slot<0 path and after synchronous
command completion. The async path (used only by the currently
unreachable ata_security_erase_unit()) is left untouched because the
HBA may still be DMAing out of the buffer when exec_cmd_slot_ex()
returns ATA_ERR_BUSY; wiping it there would race the transfer.
sata_unlock_disk() calls panic() directly when the post-unlock ATA
security state doesn't match the expected SEC5/SEC6, bypassing the
cleanup: label that zeroizes the plaintext secret[] stack buffer. On
x86, panic() is an infinite hlt() loop, so the plaintext disk-unlock
secret (TPM-unsealed key or password) remains readable in DRAM for as
long as the device stays halted. Zeroize secret[] before panic() on
this path, mirroring the existing cleanup zeroization.
In hal/va416x0.c, FRAM_Write now explicitly aborts the split SPI write transaction on command-phase failure before returning, instead of leaving the bus in the half-open state introduced
by the early return. I also added a host-side regression test in tools/unit-tests/unit-va416x0-fram.c that injects a command-phase HAL_Spi_Transmit(..., false) failure and verifies a later
write still reaches the closing ...true phase.
In src/arm_tee_psa_ipc.c, I extracted the protected-storage dispatch path into a small helper so it can be tested directly without changing runtime behavior. The new test in tools/unit-
tests/unit-arm-tee-psa-ipc.c covers the short-vector invalid-argument branches for SET, GET, GET_INFO, and REMOVE, plus a full success path across those operations. The unit harness
additions are wired up in tools/unit-tests/Makefile and use a tiny local CMSE stub in tools/unit-tests/arm_cmse.h.
update_ram.c stored the firmware versions into int locals via
(int)wolfBoot_current_firmware_version() / (int)wolfBoot_update_firmware_version().
Under WOLFBOOT_FIXED_PARTITIONS those macros resolve to wolfBoot_get_image_version(),
which returns uint32_t. Versions >= 0x80000000 became negative when cast to int,
failed the ">= 0" clamp, and left boot_v/update_v at 0. With both partition
versions above INT_MAX, max_v collapsed to 0 and the rollback guard
"(max_v > 0U) && (active_v < max_v)" was silently skipped, allowing a rolled-back
image to be staged after a fallback. Same fundamental pattern as F-4411 (hwswap)
and the documented 3736 (update_disk).
wolfBoot_get_image_version() returns uint32_t with 0 as its only "invalid" value,
so the negative-clamp logic was both unnecessary and harmful. Use the direct
uint32_t reads.
Add tools/unit-tests/unit-update-ram-noramboot.c, a WOLFBOOT_NO_RAMBOOT build of
wolfBoot_start (the configuration that uses wolfBoot_open_image() and therefore
actually reaches the rollback guard). The rollback test drives BOOT
(v0x80000005, marked oversize so its open is rejected) and UPDATE (v0x80000003,
valid): the boot path falls back to UPDATE and must deny the downgrade. The test
fails before the fix (UPDATE staged, do_boot reached) and passes after. The
existing unit-update-ram target uses the RAMBOOT path, where wolfBoot_ramboot()
has its own separate int-cast of the version that rejects all high-version
images before the guard is reached; that path cannot exercise this guard and is
left unchanged.
update_flash_hwswap.c stored the firmware versions into int locals via
(int)wolfBoot_current_firmware_version() / (int)wolfBoot_update_firmware_version().
Under WOLFBOOT_FIXED_PARTITIONS those macros resolve to wolfBoot_get_image_version(),
which returns uint32_t. Versions >= 0x80000000 became negative when cast to int,
failed the ">= 0" clamp, and left boot_v/update_v at 0. With both partition
versions above INT_MAX, max_v collapsed to 0 and the rollback guard
"(max_v > 0U) && (active_v < max_v)" was silently skipped, allowing a rolled-back
image to be staged. This was a regression introduced in 90670fcd; the original
code used uint32_t directly.
wolfBoot_get_blob_version() returns uint32_t with 0 as its only "invalid" value,
so the negative-clamp logic was both unnecessary and harmful. Restore the direct
uint32_t reads.
Add tools/unit-tests/unit-update-flash-hwswap.c which drives wolfBoot_start with
BOOT (v0x80000002, unbootable) and UPDATE (v0x80000001, valid): the boot path
falls back to UPDATE and must deny the downgrade. The test fails before the fix
(do_boot reached) and passes after.
The alignment round-up current_offset = (current_offset + p_align - 1) &
~(p_align - 1) used p_align straight from a (possibly crafted) program
header without bounds. For ELF64 a p_align near UINT64_MAX wraps the sum to
a tiny value (e.g. 0x78 -> 0); for ELF32 a value that rounds the offset past
2^32 is silently truncated when stored into the uint32_t offset field. Either
way segment data is written at a wrong (often zero) file offset, clobbering
the output ELF header while squashelf still reports success.
Reject such inputs: bound the round-up against UINT64_MAX before applying it
(both classes) and reject an ELF32 offset that exceeds UINT32_MAX. Normal
page-aligned segments are unaffected. Adds test-align-overflow.py covering
the ELF64 wrap, the ELF32 truncation, and the normal-segment regression.
In the PT_LOAD range filter, segmentEnd was computed as
p_paddr + p_memsz - 1 in uint64_t with no overflow check. Both fields
come straight from the (possibly crafted) ELF64 program header. When
p_paddr + p_memsz exceeds 2^64 the result wraps below p_paddr: the
wrapped end can land back inside the requested range, so a segment whose
true span lies entirely outside the range is spuriously included (and the
converse can silently drop a valid segment). squashelf would then emit a
squashed image that wolfBoot signs and boots.
Detect the overflow before computing segmentEnd and treat such a segment
as out-of-range. ELF32 cannot overflow a uint64 sum and is unaffected.
Adds tools/squashelf/test-range-overflow.py (run via `make test`) which
fails before the fix and passes after.
Adds support for running wolfBoot in **Machine Mode (M-mode)** on the PolarFire SoC MPFS250T, booting from eNVM and loading a signed application from **SC QSPI flash** into on-chip LIM (Loosely Integrated Memory). No HSS (Hart Software Services) or DDR is required. It also extends the existing PolarFire HAL with multi-hart support, L2 cache configuration, per-hart UART, and refactors shared code (SDHCI, SCB mailbox, linker/test-app) for both S-mode and M-mode builds.
- Add filesystem access implementation in HAL (`hal/filesystem.c`)
- Introduce new library_fs target and build rules for filesystem-backed partition management
- Provide example configuration for library_fs
- Add CLI application (`hal/library_fs.c`) to query and manage partitions
- Update documentation with usage instructions and build steps for the new CLI tool
- Update .gitignore