Skip to content

Fenrir fixes 2026 07 02#814

Merged
dgarske merged 23 commits into
wolfSSL:masterfrom
danielinux:fenrir-fixes-2026-07-02
Jul 2, 2026
Merged

Fenrir fixes 2026 07 02#814
dgarske merged 23 commits into
wolfSSL:masterfrom
danielinux:fenrir-fixes-2026-07-02

Conversation

@danielinux

Copy link
Copy Markdown
Member

c4e853e F-6131: enforce full-transfer bounds check in spi_flash_read/spi_flash_write (QSPI)
dd16ccb F-6405: pin partition-fit accept boundary in wolfBoot_open_image_address/open_self_address
102ddc3 F-5964: fix stale in-word offset in hal_flash_write byte-wise path (samr21/same51)
eb5574f F-6124: emit build-time warning for SIGN=NONE / WOLFBOOT_NO_SIGN
72186fa F-6126: add mutation-pinning test coverage for wolfBoot_check_flash_image_elf
9a25fae F-6127: fix custom-TLV 8-byte value saturation in arg2num
dd0712e F-6130: clear disk_encrypt_key/nonce before panic paths that skip the final cleanup
e38296c F-6398: fix OOB write in bmpatch when reconstructed image grows past source size
32e41f0 F-6399: fix imx_rt DCACHE invalidation to include down-alignment offset
e09074a F-6401: emit build-time warning for FPGA_NONFATAL=1
f20a1e1 F-6403: add successful-boot test coverage for HW-swap wolfBoot_start
8df9689 F-6408: zeroize UDS from OTP keystore generator's heap and stack buffers
fea97c5 F-5963: fix over-advance of address/len in unaligned hal_flash_write (kinetis.c, mcxa.c, mcxw.c)
9ce750f F-5965: skip wb_diff match candidates whose offset MSB collides with ESC
cd4e4be F-5968: zeroize disk-unlock passphrase from static ATA command buffer
20f11ae F-6129: zeroize disk-unlock secret before the state-mismatch panic in sata_unlock_disk
60acdc2 F-6402: add negative tests pinning wolfBoot_check_rot digest comparison
228c9d6 F-6407: zeroize static cached_sector after each flash commit in psa_store

danielinux added 19 commits July 2, 2026 14:03
…tore

cache_commit() is the single choke point through which every psa_store
helper (create_object, delete_object, wolfPSA_Store_Write's sector loop,
update_store_size, erase_object_payload, check_vault) flushes the static
cached_sector staging buffer to flash. The buffer was never cleared, so
key-object plaintext staged there could remain resident in static SRAM
after the store operation returned. Wipe cached_sector with wc_ForceZero
right after the flash write completes, mirroring the existing pattern in
src/wolfhsm_flash_hal.c.
Add tests asserting rejection on a mismatched pubkey hint and on a
short digestSz from wolfTPM2_NVReadAuth, so a weakened comparison
(e.g. == 0 -> >= 0, or a dropped clause) is caught.
… sata_unlock_disk

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.
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.
A matched block's source offset is encoded as off[0..2] right after the
ESC (0x7f) header marker. When the match offset falls in
[0x7f0000, 0x7fffff], off[0] is 0x7f, so the header begins with ESC ESC
-- the same two bytes wb_patch uses to decode an escaped literal 0x7f.
The decoder then emits a single literal byte, consumes only 2 of the 6
header bytes, and desyncs the rest of the stream, breaking the
wb_patch(wb_diff(A,B)) == B roundtrip for base images >= ~8MB (a
supported MMU/Linux delta-update configuration).

Make wb_diff skip any candidate match whose offset's most-significant
byte equals ESC, in both the forward (base-image) and backward
(previously-patched-image) search paths, so the ambiguous header is
never produced; the position falls back to literal encoding instead.
…(kinetis.c, mcxa.c, mcxw.c)

In the unaligned/partial-word path, address/len were advanced by the full
flash-word-relative loop index "i" (which starts at start_off), instead of
by the number of data bytes actually consumed (i - start_off). On a write
spanning more than one flash word this drops start_off bytes of input data
and misdirects the following word write. Mirrors the already-correct form
in hal/kinetis_kl26.c.
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.
test_hwswap_highversion_rollback_denied was the only test for
update_flash_hwswap.c's wolfBoot_start and always ends in boot_panic,
so the entire successful-boot path (IMG_STATE_UPDATING->TESTING
transitions before and after the HW dualbank swap, and the same-version
boundary of the anti-rollback check) was never exercised and mutations
there would go undetected. Add three tests that reach do_boot() and pin
each of those code paths.
FPGA_NONFATAL downgrades a failed PL bitstream load from fatal (panic)
to a logged non-fatal warning, letting boot continue without the
programmable logic. Every other fail-safe-weakening option in this
file (ALLOW_DOWNGRADE, DISABLE_BACKUP,
WOLFBOOT_UDS_UID_FALLBACK_FORTEST) emits a $(warning ...) so the
tradeoff is visible at build time; FPGA_NONFATAL was missing one.
hal_flash_write() and hal_flash_erase() aligned the invalidation start
address down to a 32-byte cache line but rounded the length up from
"len" alone, omitting the (address - aligned_address) offset. Whenever
(address % 32) + (len % 32) > 32, the invalidated range fell short of
address + len, leaving the last cache line stale after a write/erase.

Extract the range computation into hal_flash_cache_align_range()
(hal/imx_rt.h, dependency-free so it's unit-testable without the
NXP SDK) and include the down-alignment offset before rounding the
length up, so the invalidated range always covers [address, address+len).
…source size

bmpatch mmaps `base` sized to the source file (len1) then memcpy's the
reconstructed output into base+len3, which runs past the mapping once the
destination image is larger than the source (a common case for delta
updates). This causes writes into the zero-fill slack of the last mapped
page to be silently dropped (never synced to the file even after the
trailing ftruncate) or, for larger overruns, a SIGBUS. Reproduced with the
existing `delta-test` fixtures: patching 1.txt back to the (larger) 0.txt
already corrupts the tail of the output today.

Write reconstructed blocks with pwrite(fd1, ...) instead of memcpy into the
undersized mapping; regular file writes grow the file naturally instead of
faulting/silently discarding data past the mapped region. The mmap of
`base` is only used for reads, so this is a minimal, in-place-preserving
fix. Verified `make -C tools/delta delta-test` now passes both directions
(shrinking 0->1 and growing 1->0).
… final cleanup

update_disk.c wolfBoot_start() already zeroizes disk_encrypt_key/nonce and
the decrypted header before most wolfBoot_panic() halts, but four paths
were missed: anti-rollback rejection, FIT FPGA load failure, FIT kernel
load failure, and flash-protect failure. Since wolfBoot_panic() halts
forever on real targets, these paths left the key live in BSS on a halted
device. Add the same disk_decrypted_header_clear()/disk_crypto_clear()
pair used on the other panic paths immediately before each.
arg2num() parsed --custom-tlv values with signed strtoll(), which
saturates to LLONG_MAX (0x7FFFFFFFFFFFFFFF) on positive overflow. For
LEN==8 no masking is applied afterwards (unlike LEN 1/2/4), so any
value >= 2^63 silently encoded as 0x7FFFFFFFFFFFFFFF instead of the
value the user supplied, breaking the TLV encode/decode roundtrip.

Switch to strtoull() and reject (exit 16) when it reports ERANGE for
an 8-byte value, mirroring the existing fw_version range check.
…mage_elf

The scattered-ELF flash integrity check's final image_CT_compare()
rejection branch had zero unit test coverage, so a weakened comparison
(e.g. != 0 -> == 0, or a dropped return -2) would silently pass. Add a
positive case (correctly-hashed scattered image verifies OK) and a
negative case (single corrupted byte in a scattered segment's
flash-resident payload is rejected via the digest-mismatch branch).
SIGN=NONE disables firmware signature authenticity verification
entirely (wolfBoot_verify_authenticity() becomes a stub that always
confirms), leaving only a hash check an attacker can satisfy. Every
other fail-safe-weakening option in this file (ALLOW_DOWNGRADE,
DISABLE_BACKUP, WOLFBOOT_UDS_UID_FALLBACK_FORTEST, FPGA_NONFATAL)
emits a $(warning ...) so the tradeoff is visible at build time;
SIGN=NONE was missing one despite being the most security-critical.
…amr21/same51)

In the else branch of hal_flash_write, off was computed once per call from
the original (call-time) "address" instead of the current position
"address + i". The word index dst_idx advanced with i, but the fill loop
kept starting at the stale off, so once destination address and source
buffer had different alignment mod 4, every word after the first was
filled at the wrong byte offset, dropping and misplacing data. Derive off
and dst from address + i so each word's offset tracks the current
position, mirroring the fix already applied to mcxa.c for the same bug
class (F-5963).
…ess/open_self_address

The fw_size > (WOLFBOOT_PARTITION_SIZE - IMAGE_HEADER_SIZE) guard was only
tested on the reject side (oversize + 1); no test asserted that a firmware
payload exactly filling the partition payload budget is accepted. A >/>=
mutation at image.c:1402 or image.c:1618 would silently reject a
maximally-sized valid image and survive the existing suite. Add positive
boundary assertions (via wolfBoot_open_image()/wolfBoot_open_image_address()
and wolfBoot_open_self_address()) paired with the existing reject-side
checks in test_open_image.
…h_write (QSPI)

spi_flash_read only rejected address > FLASH_DEVICE_SIZE (off-by-one,
admits address == FLASH_DEVICE_SIZE) and never validated address+len
against the device size, so an in-bounds start with an out-of-range
extent was not rejected. spi_flash_write had no bounds check at all.
Add an inclusive/full-extent check to both.
Copilot AI review requested due to automatic review settings July 2, 2026 14:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is a batch of correctness + security-hardening fixes across wolfBoot’s update/flash/delta/signing paths, along with targeted regression and mutation-pinning unit tests to prevent reintroducing the reported issues.

Changes:

  • Hardened bounds/offset handling in flash/QSPI/delta patching code paths to prevent OOB and under-coverage cases.
  • Added/extended secret zeroization on error/panic paths (disk encryption material, ATA unlock secrets, PSA store cache sector, OTP UDS buffers).
  • Added multiple new host unit tests to pin edge cases and reported regressions (HW-swap boot transitions, scattered-ELF hashing integrity, cache-line alignment, unaligned flash writes, delta ESC collision, etc.).

Reviewed changes

Copilot reviewed 38 out of 39 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tools/unit-tests/unit-update-flash-hwswap.c Adds successful-boot coverage and helpers for HW-assisted swap state transitions.
tools/unit-tests/unit-update-disk.c Verifies disk encryption key/nonce are cleared on panic paths.
tools/unit-tests/unit-tpm-rsa-exp.c Adds negative tests for RoT digest mismatch/size handling.
tools/unit-tests/unit-sign-custom-tlv-le.py Adds regression cases for 8-byte custom TLV parsing saturation and refactors signing checks.
tools/unit-tests/unit-qspi-flash.c Adds tests for QSPI read/write bounds rejection.
tools/unit-tests/unit-psa_store.c Adds test ensuring cached_sector is zeroized after commit.
tools/unit-tests/unit-otp-keystore-gen-zeroize.c New unit test interposing alloc/free/read to assert OTP UDS zeroization before free/exit.
tools/unit-tests/unit-imx-rt-cache-align.c New test for i.MX RT cache invalidation alignment coverage correctness.
tools/unit-tests/unit-image.c Pins partition-fit boundary acceptance for open_image/open_self boundary conditions.
tools/unit-tests/unit-image-elf-scatter.c New mutation-pinning tests for scattered-ELF hash verification and corruption detection.
tools/unit-tests/unit-flash-write-samr21.c New regression test for byte-wise unaligned write offset bug (samr21).
tools/unit-tests/unit-flash-write-same51.c New regression test for byte-wise unaligned write offset bug (same51).
tools/unit-tests/unit-flash-write-mcxa.c New regression test for unaligned write bookkeeping bug (mcxa).
tools/unit-tests/unit-delta.c Adds coverage for ESC-colliding match-offset MSB behavior.
tools/unit-tests/unit-ata-security-passphrase-zeroize.c New tests ensuring ATA command buffer passphrase is wiped on success/error.
tools/unit-tests/unit-ahci-unlock-panic.c New test ensuring AHCI unlock stack secret is wiped before state-mismatch panic.
tools/unit-tests/mcxa_fsl_stub/fsl_spc.h Adds stub header for MCUXpresso dependency-free unit tests.
tools/unit-tests/mcxa_fsl_stub/fsl_romapi.h Adds stub ROM API header used by hal/mcxa.c unit test.
tools/unit-tests/mcxa_fsl_stub/fsl_common.h Adds stub common types for MCUXpresso headers in tests.
tools/unit-tests/mcxa_fsl_stub/fsl_clock.h Adds stub header for MCUXpresso dependency-free unit tests.
tools/unit-tests/Makefile Registers new unit tests and builds; adds CFLAGS for scattered-ELF test.
tools/keytools/sign.c Fixes custom TLV numeric parsing to use unsigned conversion and detect 8-byte ERANGE.
tools/keytools/otp/otp-keystore-gen.c Adds secure_zero() and wipes UDS in heap+stack on all exit paths.
tools/delta/bmdiff.c Fixes bmpatch OOB write by writing output via pwrite() instead of mmapped memcpy().
src/x86/ata.c Adds secure zeroization of passphrase bytes in static ATA command buffer (sync paths + early error).
src/x86/ahci.c Zeroizes unlock secret before panic on post-unlock state mismatch.
src/update_disk.c Clears decrypted header + crypto material before panic in several error paths.
src/qspi_flash.c Enforces full-transfer bounds checks in spi_flash_read/write.
src/psa_store.c Zeroizes cached_sector after flash commit to avoid retaining plaintext in RAM.
src/delta.c Skips wb_diff match candidates whose offset MSB collides with ESC escape marker.
options.mk Emits build-time warnings for SIGN=NONE and FPGA_NONFATAL=1 insecure/non-fatal configs.
hal/samr21.c Fixes stale in-word offset in byte-wise flash write path; gates cpsid for host unit tests.
hal/same51.c Fixes stale in-word offset in byte-wise flash write path; gates cpsid for host unit tests.
hal/mcxw.c Fixes unaligned write bookkeeping (address/len over-advance).
hal/mcxa.c Fixes unaligned write bookkeeping (address/len over-advance).
hal/kinetis.c Fixes unaligned write bookkeeping (address/len over-advance).
hal/imx_rt.h New helper to compute cache-line-aligned invalidate ranges (SDK-free).
hal/imx_rt.c Uses hal_flash_cache_align_range() to correctly include down-alignment offset in invalidate length.
.gitignore Ignores newly added unit-test binaries.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tools/delta/bmdiff.c
Comment thread tools/unit-tests/Makefile
The bulk FLASH_Program path read data+w but never advanced w, so a
partial-word tail after an aligned run re-read the input from offset 0.
Affects kinetis, mcxa, mcxw. Pin with an mcxa bulk+tail test case.
empty_qword[0] was 0xFFFFFFF (7 nibbles), so a fully-erased 16-byte
word never compared equal and was always reprogrammed.
Detect short writes and errors instead of advancing the output offset
as if all bytes were written, which could silently corrupt the image.
src/keystore.c is generated and gitignored, so the test failed to build
on a clean tree. Use the checked-in unit-keystore.c stub instead.

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenrir Automated Review — PR #814

Scan targets checked: wolfboot-bugs, wolfboot-src

No new issues found in the changed files. ✅

@dgarske dgarske merged commit 277cbbe into wolfSSL:master Jul 2, 2026
389 of 390 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants