Skip to content

fix(startup): harden boot count persistence against hard-reset corruption - #470

Merged
ineskhou merged 6 commits into
mainfrom
harden/boot-count-persistence
Jul 27, 2026
Merged

fix(startup): harden boot count persistence against hard-reset corruption#470
ineskhou merged 6 commits into
mainfrom
harden/boot-count-persistence

Conversation

@Mikefly123

@Mikefly123 Mikefly123 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Problem

The test_safe_09_command_loss_triggers_safe_mode_and_reboot HWIL failures on PR #460 turned out not to be test flakiness — the test was correctly detecting that boot-count persistence is unreliable across watchdog hard resets.

Evidence (from the debug session on integration-uart runs 29894110572 / 29979002023)

Reconstructed from the GDS event/command/channel logs:

  1. Read race (test-side, fixed in Make mode manager test more reliable #460): after the watchdog reboot, the test's GET_BOOT_COUNT answered at TB_PROC_TIME 4.492s80 ms before StartupManager's first 1 Hz tick performed the lazy increment (evidenced by the tick's startup.bin sequence dispatch at TB 4.571s). PR Make mode manager test more reliable #460 now polls, closing that race.
  2. File corruption (this PR): after a series of test-commanded STOP_WATCHDOG hard resets, GET_BOOT_COUNT returned 215637388754419713 (0x02FE191005000001) — a successful full-length 8-byte read of torn data. The read<> helper's status/size checks rule out an unmounted filesystem or short read: this was a well-formed file containing junk, i.e. a torn flash write.
  3. Unbounded propagation: the next boot read the garbage, incremented it, and durably persisted 215637388754419714 (garbage + 1). The increment machinery works perfectly — and will carry poisoned data forever, because nothing validates the value.

Contributing factor: GET_BOOT_COUNT performed an unconditional write-back on every query (increment=false included), so every ground/test query of the boot count was a flash write that a reset could tear mid-commit.

(Ruled out during the session: watchdog non-rearm — Watchdog::m_run initializes true, so feeding resumes on every boot; the repeated resets in the logs were commanded by subsequent tests.)

Changes (StartupManager only)

  • Corruption guard: raw values > 1,000,000 are treated as a failed read and reported via new WARNING_HI BootCountCorrupted(raw) — mirrors the existing quiescence-file guard for "partial flash write leaving 0xFF bytes".
  • GET_BOOT_COUNT is now read-only: removes the largest source of boot-count write traffic and its tear window.
  • Increment retry: if the first-tick persist fails (e.g. filesystem not ready), run_handler retries each 1 Hz tick — the increment is delayed, not lost. The failure EVR is de-duplicated per failure streak to avoid 1 Hz spam.
  • Write-then-rename persist: write <path>.tmp, flush, then Os::FileSystem::rename over the target. Correction (2026-07-26): the flight FS is FAT (ELM FatFs, CONFIG_FAT_FILESYSTEM_ELM), not littlefs — FAT renames are not guaranteed power-cut atomic. The design still closes the observed torn-in-place-write window (data fully on storage before it replaces the old file); the residual worst case during the rename window is a missing file, which reads as a failed read and re-initializes visibly rather than propagating silent garbage.

Test plan

  • CI build + unit-test green
  • integration-uart green, specifically test_safe_09 (asserts boot count == initial+1 across a watchdog reset — the regression seam for all four changes)
  • Post-merge: re-run integration-uart 2–3× for flake confidence

Related: PR #460 (test-side retry), and a CI follow-up worth filing separately: the pre-test filesystem format (#446) leaves /seq/startup.bin and /seq/enter_safe.bin missing for every integration run.

🤖 Generated with Claude Code

…tion

HWIL evidence (integration-uart runs 29894110572 / 29979002023, PR #460):
the boot count file was read back as 0x02FE191005000001 after a sequence
of watchdog-commanded hard resets, and the next boot persisted exactly
garbage+1 - the increment machinery works, but nothing guards against a
torn flash write poisoning the count forever.

Four changes to StartupManager:
- Corruption guard: values > 1,000,000 are treated as a failed read
  (mirrors the existing quiescence-file 0xFF-fill guard) and reported
  via new WARNING_HI BootCountCorrupted with the raw value.
- GET_BOOT_COUNT is now read-only: the unconditional write-back on every
  query was the largest source of boot-count write traffic, and each
  write is a window for a reset to tear the file.
- Increment retry: if the first-tick persist fails (e.g. filesystem not
  ready), run_handler retries each 1Hz tick until it sticks - the
  increment is delayed, not lost. Failure warning de-duplicated per streak.
- Atomic persist: write temp file then Os::FileSystem::rename over the
  target; littlefs renames are atomic so a mid-update reset leaves either
  the old or new file, never a torn one.

Regression seam: test_safe_09_command_loss_triggers_safe_mode_and_reboot
asserts boot count == initial+1 across a watchdog reset (with the retry
poll from PR #460).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

StartupManager now detects implausible boot counts, reports corruption, persists counts through flushed temporary-file replacement, and retries failed persistence on later run-handler ticks.

Changes

Boot Count Reliability

Layer / File(s) Summary
Boot-count contract and persistence state
PROVESFlightControllerReference/Components/StartupManager/StartupManager.hpp, PROVESFlightControllerReference/Components/StartupManager/StartupManager.fpp
Documents corrupt-value handling, adds persist_boot_count, declares BootCountCorrupted, and tracks persistence and warning state.
Atomic persistence and retry flow
PROVESFlightControllerReference/Components/StartupManager/StartupManager.cpp
Flushes writes, rejects implausible counts, uses temporary-file writes followed by rename, and retries failed persistence on subsequent ticks.
Persistence behavior documentation
PROVESFlightControllerReference/Components/StartupManager/docs/sdd.md, docs-site/components/StartupManager.md
Documents corruption handling, read-only queries, atomic persistence, retry behavior, events, state flags, and new requirements.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant StartupManager
  participant FileSystem
  participant BootCountFile
  StartupManager->>FileSystem: Read boot count
  FileSystem->>BootCountFile: Load stored value
  StartupManager->>FileSystem: Write and flush temporary value
  FileSystem->>BootCountFile: Store temporary value
  StartupManager->>FileSystem: Rename temporary file
  FileSystem-->>StartupManager: Return persistence status
  StartupManager->>StartupManager: Retry on later run-handler ticks after failure
Loading

Poem

I’m a rabbit counting boots in the night,
Guarding each number with filesystem might.
Temp files hop, then rename with care,
Corrupt counts get a warning flare.
If writes miss a beat, I retry with delight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main change to harden boot-count persistence.
Description check ✅ Passed The description covers the problem, fix, test plan, and related context, though it doesn't use the template headings verbatim.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@PROVESFlightControllerReference/Components/StartupManager/StartupManager.cpp`:
- Around line 140-150: The write path used by persist_boot_count must flush the
temporary file before considering it persisted. Update the underlying write
helper invoked by persist_boot_count to call file.flush() before file.close(),
propagate any flush failure as a write failure, and only allow
persist_boot_count to rename the temporary file after a successful flush; do not
compare file.close() with a status value because it returns void.

In
`@PROVESFlightControllerReference/Components/StartupManager/StartupManager.hpp`:
- Around line 111-118: Initialize StartupManager::m_boot_count to 0 before any
run_handler logic reads it, using either an in-class member initializer or the
constructor initializer list. Preserve the existing boot-count persistence and
telemetry behavior after initialization.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2eb78bd6-6e19-4588-a248-57c0eafab100

📥 Commits

Reviewing files that changed from the base of the PR and between 6944fca and a99b4a7.

📒 Files selected for processing (3)
  • PROVESFlightControllerReference/Components/StartupManager/StartupManager.cpp
  • PROVESFlightControllerReference/Components/StartupManager/StartupManager.fpp
  • PROVESFlightControllerReference/Components/StartupManager/StartupManager.hpp

Comment thread PROVESFlightControllerReference/Components/StartupManager/StartupManager.hpp Outdated
…ot_count

Addresses CodeRabbit review on #470: Os::File::close() is void and cannot
report a deferred flush failure, so the write helper now requires
file.flush() == OP_OK before reporting SUCCESS - persist_boot_count no
longer renames a temp file whose data may not have reached storage.
Also gives m_boot_count an in-class zero initializer; run_handler's
first-tick guard reads it before any assignment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mikefly123 added a commit that referenced this pull request Jul 23, 2026
Second flake in test_safe_09, seen on PR #470's CI (run 29983198334):
CommandLossDetected asserted fine, but the immediate history scrape ran
before EnteringSafeMode (emitted ~40ms later in the same firmware code
path) reached the GDS. Use assert_event with a timeout so the check
waits for the event instead of racing it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ineskhou
ineskhou previously approved these changes Jul 25, 2026

@ineskhou ineskhou 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.

Passing Relevant Testing, and I agree with the post-merge monitoriing of the test this should fix

The code itself doesn't seem like it would impact any non-testable components in the rest of the system

Would like a sdd update before merge, but can also track separately

LGTM

Mikefly123 and others added 2 commits July 26, 2026 10:23
Conflict resolution: kept main's hardcoded-startup additions (multi-sequencer
ports, transmit-enable countdown) alongside this branch's boot-count hardening
(zero-init sentinel, persist retry branch, Os/FileSystem include).

SDD: documents the hardened persistence path (corruption guard, read-only
GET_BOOT_COUNT, per-tick increment retry, atomic temp+rename), the two new
state variables, BootCountCorrupted event, and REQ-SM-010/011.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
PROVESFlightControllerReference/Components/StartupManager/docs/sdd.md (1)

65-70: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Update the documented port contract.

Both tables advertise nonexistent sequenceStarted and completeSequence ports. Document the FPP-defined startupCompleteSequence, startupsequenceStarted, safe-mode, payload, and loraFirstStart ports so topology integrations use valid names.

  • PROVESFlightControllerReference/Components/StartupManager/docs/sdd.md#L65-L70: replace the obsolete generic sequencer rows with the actual FPP ports.
  • docs-site/components/StartupManager.md#L65-L70: make the same replacement.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PROVESFlightControllerReference/Components/StartupManager/docs/sdd.md` around
lines 65 - 70, Update the port tables in
PROVESFlightControllerReference/Components/StartupManager/docs/sdd.md lines
65-70 and docs-site/components/StartupManager.md lines 65-70 by replacing the
obsolete sequenceStarted and completeSequence rows with the FPP-defined
startupCompleteSequence, startupsequenceStarted, safe-mode, payload, and
loraFirstStart ports, preserving their correct directions, types, and
descriptions in both files.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@PROVESFlightControllerReference/Components/StartupManager/docs/sdd.md`:
- Around line 65-70: Update the port tables in
PROVESFlightControllerReference/Components/StartupManager/docs/sdd.md lines
65-70 and docs-site/components/StartupManager.md lines 65-70 by replacing the
obsolete sequenceStarted and completeSequence rows with the FPP-defined
startupCompleteSequence, startupsequenceStarted, safe-mode, payload, and
loraFirstStart ports, preserving their correct directions, types, and
descriptions in both files.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 80a9613a-de12-45db-95b7-dba7bfaa1ac4

📥 Commits

Reviewing files that changed from the base of the PR and between a99b4a7 and 9dd274d.

📒 Files selected for processing (5)
  • PROVESFlightControllerReference/Components/StartupManager/StartupManager.cpp
  • PROVESFlightControllerReference/Components/StartupManager/StartupManager.fpp
  • PROVESFlightControllerReference/Components/StartupManager/StartupManager.hpp
  • PROVESFlightControllerReference/Components/StartupManager/docs/sdd.md
  • docs-site/components/StartupManager.md

The persist design claimed littlefs-atomic renames; the flight filesystem
is FAT (CONFIG_FAT_FILESYSTEM_ELM, zephyr,fstab,fatfs). FAT renames are
not guaranteed power-cut atomic. The write-then-rename design still closes
the observed torn-in-place-write window (data fully flushed before it
replaces the old file); the residual worst case is a missing file, which
reads as a failed read and re-initializes visibly instead of propagating
silent garbage. Comments and SDD updated to state this accurately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Mikefly123
Mikefly123 requested a review from ineskhou July 26, 2026 18:43

@ineskhou ineskhou 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.

Thanks for the merge and the docs udpates! LGTM

@ineskhou
ineskhou merged commit c03b206 into main Jul 27, 2026
7 checks passed
@ineskhou
ineskhou deleted the harden/boot-count-persistence branch July 27, 2026 05:15
@github-project-automation github-project-automation Bot moved this to Done in V1.X.X Jul 27, 2026
wrong-formatt added a commit to Open-Source-Space-Foundation/broncospace-cubesat that referenced this pull request Aug 2, 2026
nateinaction added a commit that referenced this pull request Aug 31, 2026
Unlike the crypto bump, this moves fatfs off the revision Zephyr 4.4.2
pins. The fixes landed in the Zephyr FatFs tree in July 2026 but did not
make the 4.4.2 release and are not on v4.4-branch, so waiting for 4.4.3
would not pick them up. Our previous pin dates to October 2025 and is a
pristine ChaN R0.16 with none of them applied.

Three are mount-time validation, which is the reason to take this. The
flight filesystem is FAT with exFAT enabled (prj.conf), and the realistic
trigger here is not an attacker-supplied volume but a superblock torn by
a hard reset -- the same corruption class that motivated the boot count
hardening in #470. Today a malformed BPB faults; with these it is
rejected at mount, and CONFIG_FS_FATFS_MOUNT_MKFS reformats and recovers.

- CVE-2026-6682: BPB_FATSz32 times the FAT count overflowed a DWORD,
  laying the data region inside the FAT. Now capped before the multiply.
- CVE-2026-6683 (exFAT): a BPB reporting zero clusters made sync_fs()
  divide by zero on first write or unmount. Rejected at mount instead.
- CVE-2026-6687 (exFAT): f_getlabel() looped over an unvalidated on-disk
  count and overran the caller's buffer. Clamped to the 11-char maximum.

The fourth, CVE-2026-6686, zeroes the range exposed when f_lseek()
extends a file past end-of-file; that range previously read back stale
flash contents. The fifth, CVE-2026-6685, upstream documents as a
readability change rather than a real fix.

The six commits touch only ff.c and documentation, so there is no
Zephyr-side glue to adapt. Costs 280 bytes of flash (64.67% -> 64.70%)
for the fill_zero helper; RAM is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants