Audit date: 2026-07-23. Hardware completion evidence was added on 2026-07-27.
Reviewed end-to-end path:
stm32ctl
-> UART framing
-> protocol parser
-> update service
-> streaming installer
-> flash backend
-> boot metadata
-> candidate verification
-> candidate boot
-> confirmation / rollback
The review focused on memory safety, integer overflows, invalid state transitions, parser desynchronization, replay and duplicate handling, retry semantics, unauthorized flash access, rollback protection, candidate release, power-loss behavior, stack usage, bootloader size, and undocumented assumptions.
One concrete correctness issue was found and fixed during the audit:
- When
FINISH_UPDATEhad successfully completed installation and committedCANDIDATE_READY, an I/O failure while sending the final ACK/status frame could makeupdate_service_run()return to the normal boot path instead of forcing the promised controlled reset. - The candidate was already verified and still had to pass boot-time verification before execution, so this was not an integrity bypass.
- The update-service contract was tightened so a
FINISHEDorRESET_REQUESTEDprotocol state requests reset even after a writer/I/O error. - A regression test simulates a final
FINISH_UPDATEACK writer failure and checks thatCANDIDATE_READYremains committed and reset is requested.
No proven path was found to overwrite the active slot, bootloader region, or reserved recovery region through the update path.
- Frames are little-endian, bounded to 1024 payload bytes, and protected by CRC32.
- The host tool validates local update packages through the existing
tools/update_package.pypath and the configured public key. - The tool does not contain private signing material and never signs firmware.
- Non-idempotent update commands are not retried automatically. A lost
WRITE_BLOCKACK fails closed instead of replaying a state-changing block. - Lost ACKs for idempotent commands can surface as timeout or sequence errors. This is an availability limit, not a proven integrity issue.
- The parser is incremental and resynchronizes only on
SUPD. - Oversize payloads are rejected before writing to the fixed frame buffer.
- CRC, protocol version, and payload length are checked before command execution.
- Frames with bad version, bad CRC, or oversize payload do not consume a sequence number.
- Validly decoded but logically rejected commands consume the expected sequence number.
- Tests cover byte-by-byte input, fragmented frames, multiple frames, garbage before magic, wrong version, oversize payloads, CRC errors, unknown commands, duplicate and skipped sequences, mid-frame timeout, and random input.
- The entry window is bounded by poll and byte budgets. UART noise cannot block normal boot forever.
- Only a valid binary
HELLOwith sequence 0 enters update mode. - A failed or incomplete update is aborted when a safe abort is still possible.
- A completed update requests controlled reset even when the final response frame cannot be transmitted successfully.
- The complete package never has to reside in RAM.
- Header, manifest, signature, target, slot, padding, size, and rollback policy are checked before candidate erase.
- The installer determines the inactive slot from confirmed metadata and rejects packages for the active slot.
- Rollback protection runs before
WRITINGand before erase:manifest.image_versionmust be greater than the confirmed metadata version. - Payload offsets must be exactly monotonic. Skipped, duplicate, overlapping, or extra bytes put the session into a failure state.
CANDIDATE_READYis committed only after streaming SHA-512 comparison, flash-readback hashing, installed-image verification, and header/padding checks.signed_image_verify_update_slot_buffer()treats the supplied capacity only as an upper bound and hashes exactlymanifest.image_size.
- The installer uses a restricted flash instance that exposes only metadata A, metadata B, and the inactive candidate slot as writable regions.
boot_flash_program_aligned()checks address, length, alignment, write region, overflow, and readback before reporting success.- Metadata uses two copies, CRC, and a commit marker. Torn records without a valid commit marker are never selected.
WRITINGandREJECTED_INVALIDdo not boot the candidate. After reset during begin, erase, write, or finish, slot selection falls back to the confirmed active slot when it verifies.
CANDIDATE_READYis verified again beforePENDING_TRIALis committed.- Trial boots decrement the attempt counter.
- Without application confirmation, attempts eventually exhaust and Stage 0 falls back to the last confirmed slot.
- Application confirmation is valid only from
PENDING_TRIALfor the running candidate slot and then promotes that slot toCONFIRMED.
Risk: after successful update_installer_finish(), CANDIDATE_READY was
already committed. If sending the final ACK/status frame failed,
update_service_run() treated the error like a generic protocol failure and
returned to the normal boot path.
Impact: flash integrity was not violated, because the candidate still required boot-time verification. The behavior nevertheless violated the update-service contract that a successful update ends in a controlled reset.
Correction:
firmware/exp045_bootloader_v2/src/update_service.c: ifprotocol_status != OK, the service now checksFINISHEDandRESET_REQUESTEDfirst and requests reset for those states.tests/update_protocol/test_update_protocol.c: addedtest_update_service_final_ack_io_error_still_resets().
The 2026-07-27 RDP0 hardware run later found two timing/integration issues and validated their fixes:
BEGIN_UPDATEACK took about 6.3 seconds because the target erases the inactive slot before acknowledging begin. The previous 1 second host timeout was too short for real hardware; the documented and default host timeout is now 15 seconds.- Reset immediately after
FINISH_UPDATEcould truncate the final ACK on hardware. The target now waits for USART transmission complete before requesting reset. - Trial boot initially failed because boot policy used a read-only flash
backend for the
CANDIDATE_READY -> PENDING_TRIALcommit. Boot policy now uses the reviewed metadata-only target flash backend for that transition.
- UART update frames are protected against accidental corruption and parser confusion, but the session is not operator-authenticated. Authorization comes from the Ed25519 package signature and rollback policy.
- Internal flash must remain consistently memory-mapped while hashing,
verifying, and preparing the jump. The target backend handles cache behavior
around flash operations; there is no separate full re-hash immediately before
signed_image_jump(). - After slot selection, the jump path validates the prepared jump context against the flash vector table. It assumes no further bootloader flash writes occur in that phase.
- Lost ACKs for idempotent host commands can become visible sequence NACKs. The host fails rather than trying side-effecting resynchronization.
- No physical update GPIO is selected; update entry uses the bounded binary
HELLOwindow. - No dedicated AFL, Hypothesis, or QuickCheck fuzz target was found. The repository has deterministic parser and random-input tests.
The audit verification run completed successfully:
make -C tests/update_protocol clean test SANITIZE=1
make -C tests/update_storage clean test SANITIZE=1
make -C tests/uart clean test SANITIZE=1
make -C tests/diagnostic_console clean test SANITIZE=1
make -C tests/host_verifier clean test SANITIZE=1
make -C tools clean test SANITIZE=1
make -C tests/rsm_core clean test
python3 -m pytest
.venv-hil/bin/ruff check .
.venv-hil/bin/mypy .
cd tools/secure_boot_hil && ../../.venv-hil/bin/ruff check .
cd tools/secure_boot_hil && ../../.venv-hil/bin/mypy secure_boot_hil host_tests
make -C firmware/exp045_bootloader_v2 clean report
bash audit/run_repository_audit.sh
git diff --check
rg -n "installed_image_buffer|installed_image_buffer_size" . -S
Observed results:
- Pytest:
157 passed in 39.27sat the audit point. - Ruff: all checks passed.
- Mypy: no issues found in 79 source files.
- Bootloader binary size at the audit point:
29,824 / 32,768bytes. installed_image_bufferrepository search: no references.
The current release-readiness results are tracked in
docs/secure-update-release-candidate.md and docs/validation-summary.md.