Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
55e254e
test(file-handling): add red integration tests for issue #457
Mikefly123 Jul 20, 2026
071e481
fix(file-handling): default downlinkRepeater to UART-only, fixing iss…
Mikefly123 Jul 20, 2026
d89b568
fix(uart): RX back-pressure in ZephyrUartDriver, fixing uplink corrup…
Mikefly123 Jul 21, 2026
3459b7b
fix(security): write-ahead batched sequence-number persistence (issue…
Mikefly123 Jul 22, 2026
9001256
fix(security): remove deadlocking armed/unarmed gate from #461 seq-pe…
Mikefly123 Jul 22, 2026
362209f
fix(security): advance persisted high-water mark unconditionally on w…
Mikefly123 Jul 22, 2026
fb56b6f
fix(security): fail safe on persist failure instead of advancing high…
Mikefly123 Jul 22, 2026
caa1ce3
test(int): protect UART file transfer with board_only marker + 204KB …
Mikefly123 Jul 23, 2026
58067ad
fix(comms): survive buffer-pool exhaustion during SD-stalled uplinks …
Mikefly123 Jul 23, 2026
a0a1654
build(submodules): pin fprime + fprime-extras to OSSF fork fix branch…
Mikefly123 Jul 24, 2026
b7dbcfa
Merge remote-tracking branch 'origin/main' into fix/461-seq-persistence
Mikefly123 Jul 24, 2026
18541c7
fix(topology): drop stale ReferenceDeployment qualifier on RxOverrunC…
Mikefly123 Jul 24, 2026
fba5f70
build(submodules): merge main's fprime-zephyr level into fix/457-uart…
Mikefly123 Jul 24, 2026
b0bc46d
fix(ci): persist exact seq number on planned reboots + keep UART file…
Mikefly123 Jul 24, 2026
45ec7f2
fix(ci): auto-resync ground sequence number around in-suite reboots
Mikefly123 Jul 24, 2026
b7a8570
fix(int-tests): deflake the three remaining CI failures on PR #473
Mikefly123 Jul 24, 2026
0efd521
fix(int-tests): SEND_PKT second arg (F Prime v4.2.2) + tolerate rig-l…
Mikefly123 Jul 24, 2026
65c068b
fix(int-tests): retry the 204KB round-trip downlink once on byte mism…
Mikefly123 Jul 24, 2026
c40790f
fix(int-tests): resync + settle between acceptance uplink attempts
Mikefly123 Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[submodule "lib/fprime"]
path = lib/fprime
url = https://github.com/nasa/fprime.git
url = https://github.com/Open-Source-Space-Foundation/fprime.git

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | sed -n '1,120p'

echo
echo "== .gitmodules =="
cat -n .gitmodules | sed -n '1,80p'

echo
echo "== Makefile submodule references =="
if [ -f Makefile ]; then
  cat -n Makefile | sed -n '1,80p'
else
  fd -a 'Makefile$|makefile$' . | while read -r f; do
    echo "--- $f"
    cat -n "$f" | sed -n '1,120p'
  done
fi

echo
echo "== submodule sync/update references =="
rg -n "submodule|git submodule|sync|--init|--recursive|url =|github.com/Open-Source-Space-Foundation" -S . --glob '!build/**' --glob '!node_modules/**' --glob '!dist/**' --glob '!*.lock' || true

echo
echo "== changed files/stat =="
git diff --stat || true
git diff -- .gitmodules Makefile 2>/dev/null | sed -n '1,220p' || true

Repository: Open-Source-Space-Foundation/proves-core-reference

Length of output: 50406


🌐 Web query:

git submodule sync --recursive updates .git/config cached URL documentation

💡 Result:

The git submodule sync command is used to synchronize the remote URL configuration of your submodules with the values defined in your.gitmodules file [1][2]. When you run git submodule sync --recursive, this synchronization process is applied to all registered submodules and recursively to any nested submodules within them [1][3]. The command works by updating the submodule URL settings stored in your local.git/config file, ensuring they match the URLs currently specified in the.gitmodules file [1][4]. It only affects submodules that have already been initialized (i.e., those that already have a URL entry in.git/config) [1][5][3]. This command is necessary when the upstream URL of a submodule has changed (for example, if a project moves to a different hosting platform) [1][2]. If you pull updates that include a new URL in.gitmodules but do not sync, your local repository may attempt to fetch from the old, potentially obsolete URL, which can cause git submodule update to fail [2]. After running git submodule sync --recursive, you typically need to follow up with git submodule update --init --recursive to actually fetch and check out the commits from the new, synchronized URL [2][6].

Citations:


Synchronize submodule URLs for existing checkouts.

lib/fprime, submodules, and lib/fprime-extras now point to Open-Source-Space-Foundation, but Makefile only runs git submodule update --init --recursive. Existing initialized checkouts keep the cached old URLs in .git/config and can fail to fetch if the old remotes are unavailable.

Add git submodule sync --recursive before submodule updates, or document the one-time migration command.

🤖 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 @.gitmodules at line 3, Update the Makefile’s submodule initialization flow
to run git submodule sync --recursive before git submodule update --init
--recursive, ensuring existing checkouts adopt the current URLs for lib/fprime,
submodules, and lib/fprime-extras.

[submodule "lib/zephyr-workspace/zephyr"]
path = lib/zephyr-workspace/zephyr
url = https://github.com/zephyrproject-rtos/zephyr.git
Expand All @@ -12,4 +12,4 @@
url = https://github.com/jgromes/RadioLib
[submodule "lib/fprime-extras"]
path = lib/fprime-extras
url = https://github.com/LeStarch/fprime-extras
url = https://github.com/Open-Source-Space-Foundation/fprime-extras.git
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ void ResetManager ::handleColdReset() {

// Notify ModeManager to set clean shutdown flag before rebooting
// This allows ModeManager to detect unintended reboots on next startup
if (this->isConnected_prepareForReboot_OutputPort(0)) {
this->prepareForReboot_out(0);
for (FwIndexType i = 0; i < this->getNum_prepareForReboot_OutputPorts(); i++) {
if (this->isConnected_prepareForReboot_OutputPort(i)) {
this->prepareForReboot_out(i);
}
}

sys_reboot(SYS_REBOOT_COLD);
Expand All @@ -71,8 +73,10 @@ void ResetManager ::handleWarmReset() {

// Notify ModeManager to set clean shutdown flag before rebooting
// This allows ModeManager to detect unintended reboots on next startup
if (this->isConnected_prepareForReboot_OutputPort(0)) {
this->prepareForReboot_out(0);
for (FwIndexType i = 0; i < this->getNum_prepareForReboot_OutputPorts(); i++) {
if (this->isConnected_prepareForReboot_OutputPort(i)) {
this->prepareForReboot_out(i);
}
}

sys_reboot(SYS_REBOOT_WARM);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ module Components {
@ Port to invoke a warm reset
sync input port warmReset: Fw.Signal

@ Port to notify ModeManager before reboot (sets clean shutdown flag)
output port prepareForReboot: Fw.Signal
@ Port to notify components before reboot (ModeManager clean-shutdown flag,
@ TcSecurityDeframer exact sequence-number persist)
output port prepareForReboot: [3] Fw.Signal

###############################################################################
# Standard AC Ports: Required for Channels, Events, Commands, and Parameters #
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ TcSecurityDeframer ::TcSecurityDeframer(const char* const compName)
: TcSecurityDeframerComponentBase(compName),
m_sequenceNumberFilePath(),
m_sequenceNumber(0),
m_sequenceNumberWindow(0) {}
m_sequenceNumberWindow(0),
m_persistedHighWater(0),
m_persistRetryBackoff(0) {}

TcSecurityDeframer ::~TcSecurityDeframer() {}

Expand Down Expand Up @@ -56,6 +58,17 @@ void TcSecurityDeframer ::dataIn_handler(FwIndexType portNum, Fw::Buffer& data,
{
Os::ScopeLock lock(this->m_sequenceNumberLock);

// NOTE: there is deliberately NO "unarmed / reject everything" gate here. An earlier
// version of this fix rejected all frames -- including SET_SEQ_NUM itself -- whenever the
// persisted record failed validation, which is a self-inflicted deadlock: SET_SEQ_NUM is
// itself an authenticated command frame that must pass through this same handler, so a
// blanket reject can never be un-done by ground. Instead, an invalid/unreadable persisted
// record falls back to the same behavior as a genuine first boot (sequence number 0,
// frames accepted normally from there) -- see readSequenceNumber() -- with a distinct
// SequenceNumberRecordInvalid event so the anomaly is visible and ground can choose to
// fast-forward via SET_SEQ_NUM if they know the real last-used value, without that ever
// being required to restore basic command capability.

// --- Validate SPI and anti-replay sequence number ---
const PacketValidator::Status validationStatus =
validatePacket(parseResult.securityHeader, this->m_sequenceNumber, this->m_sequenceNumberWindow);
Expand All @@ -79,11 +92,15 @@ void TcSecurityDeframer ::dataIn_handler(FwIndexType portNum, Fw::Buffer& data,
} else {
this->log_WARNING_HI_AuthenticationFailed_ThrottleClear();

// --- Accept: persist new sequence number ---
// --- Accept: advance the in-RAM sequence number (authoritative for runtime
// acceptance decisions) and persist a write-ahead high-water mark only every
// SEQ_NUM_PERSIST_STRIDE frames (issue #461: the previous per-command persist here
// raced FileUplink/FileManager/FileDownlink/PrmDb's own filesystem access on the
// shared SD-card-backed FatFs mount).
// Only fully verified frames advance the counter, so bypass and replayed
// frames can never desync ground and spacecraft (issue #426)
this->m_sequenceNumber = parseResult.securityHeader.sequenceNumber;
this->writeSequenceNumber(this->m_sequenceNumber);
this->writeAheadPersistIfNeeded(this->m_sequenceNumber);
this->tlmWrite_CurrentSequenceNumber(this->m_sequenceNumber);
contextOut.set_authenticated(true);
}
Expand Down Expand Up @@ -129,16 +146,19 @@ void TcSecurityDeframer ::GET_SEQ_NUM_cmdHandler(FwOpcodeType opCode, U32 cmdSeq
void TcSecurityDeframer ::SET_SEQ_NUM_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, U32 seq_num) {
Os::ScopeLock lock(this->m_sequenceNumberLock);

// Write the sequence number to the file system
// Explicit ground command: persist immediately (not subject to the write-ahead stride --
// an operator-issued SET_SEQ_NUM is inherently infrequent and is the one path that should take
// effect durably right away, e.g. to fast-forward past a SequenceNumberRecordInvalid reset).
Os::File::Status status = this->writeSequenceNumber(seq_num);
if (status != Os::File::OP_OK) {
// Return execution error response
this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::EXECUTION_ERROR);
return;
}

// Set runtime sequence number to the new value
// Set runtime sequence number to the new value and track the persisted high-water mark
this->m_sequenceNumber = seq_num;
this->m_persistedHighWater = seq_num;

// Telemeter the updated sequence number
this->tlmWrite_CurrentSequenceNumber(this->m_sequenceNumber);
Expand Down Expand Up @@ -166,12 +186,15 @@ void TcSecurityDeframer ::configure() {
this->m_sequenceNumberFilePath = this->paramGet_SEQ_NUM_FILE_PATH(is_valid);
FW_ASSERT(is_valid == Fw::ParamValid::VALID || is_valid == Fw::ParamValid::DEFAULT);

// Get the sequence number from the file system. On a read failure (already evented
// by readSequenceNumber) fall back to 0 rather than refusing to boot; the operator
// can correct the counter with SET_SEQ_NUM.
// Get the persisted high-water mark from the file system. readSequenceNumber() falls back to
// 0 (same as a genuine first boot) on any read/validation failure -- including a torn-write
// checksum mismatch -- while emitting SequenceNumberRecordInvalid so the anomaly is visible.
// The window always starts at this value (unchanged semantics from before this fix); command
// capability is never blocked on this outcome.
U32 sequenceNumber = 0;
(void)this->readSequenceNumber(sequenceNumber);
this->m_sequenceNumber = sequenceNumber;
this->m_persistedHighWater = sequenceNumber;

// Telemeter the current sequence number
this->tlmWrite_CurrentSequenceNumber(this->m_sequenceNumber);
Expand All @@ -186,28 +209,78 @@ void TcSecurityDeframer ::configure() {
// ----------------------------------------------------------------------

Os::File::Status TcSecurityDeframer ::readSequenceNumber(U32& value) {
// Read the sequence number from the file system
Os::File::Status status = Utilities::FileHelper::readFromFile(this->m_sequenceNumberFilePath.toChar(), value);
if (status != Os::File::OP_OK) {
// Log the failure to read the sequence number
this->log_WARNING_HI_SequenceNumberReadFailed(static_cast<Os::FileStatus::T>(status));
} else {
// Clear throttle for sequence number read failure
this->log_WARNING_HI_SequenceNumberReadFailed_ThrottleClear();
}
// Persisted record layout: a single U64 = (value:32 << 32) | (~value:32). This is a minimal
// torn-write guard -- if power is lost mid-write, FatFs/the SD card may leave a partially
// written U64 whose two halves don't correspond, which the checksum catches. (See issue #461
// for how this record is now written -- write-ahead, batched -- rather than on every command.)
U64 record = 0;
Os::File::Status status = Utilities::FileHelper::readFromFile(this->m_sequenceNumberFilePath.toChar(), record);

// If the sequence number file does not exist, write it to disk with the default value of 0
if (status == Os::File::DOESNT_EXIST) {
// Genuine first boot: no risk of replay since nothing has ever been accepted. Bootstrap
// to 0 -- unchanged from the pre-fix behavior for this specific case.
this->log_WARNING_HI_SequenceNumberReadFailed_ThrottleClear();
value = 0;
return this->writeSequenceNumber(0);
}

return status;
if (status != Os::File::OP_OK) {
// Genuine I/O failure (not a missing file, not (yet) a checksum question). Fall back to 0,
// same as a first boot -- see the SequenceNumberRecordInvalid rationale below. Deliberately
// does NOT block command capability: an early version of this fix rejected all frames
// (including the SET_SEQ_NUM recovery command itself) whenever this path was hit, which is
// a self-inflicted deadlock. Ground can always fast-forward the counter with SET_SEQ_NUM if
// they know the real last-used value; they are never required to in order to command again.
this->log_WARNING_HI_SequenceNumberReadFailed(static_cast<Os::FileStatus::T>(status));
this->log_WARNING_HI_SequenceNumberRecordInvalid(0);
value = 0;
return status;
}
this->log_WARNING_HI_SequenceNumberReadFailed_ThrottleClear();

const U32 storedValue = static_cast<U32>(record >> 32);
const U32 storedChecksum = static_cast<U32>(record & 0xFFFFFFFFu);
if (storedChecksum != static_cast<U32>(~storedValue)) {
// Checksum mismatch: torn write or corruption. Falls back to 0 (same as first boot) rather
// than trusting a possibly-garbage stored value -- but, per the note above, this does NOT
// block command capability. This is a narrower guarantee than fully preventing replay of
// any sequence number ever used before the corruption; the tradeoff is deliberate, since a
// design that could brick command capability on a single flipped bit is a worse operational
// risk than a bounded, visible (see SequenceNumberRecordInvalid) reopening of the window.
this->log_WARNING_HI_SequenceNumberRecordInvalid(storedValue);
value = 0;
return Os::File::Status::OTHER_ERROR;
}

value = storedValue;
return Os::File::Status::OP_OK;
}

void TcSecurityDeframer ::prepareForReboot_handler(FwIndexType portNum) {
// Planned reboot: persist the EXACT current sequence number, not the write-ahead
// high-water mark. On the next boot the counter resumes at precisely the last
// accepted value, so ground (at lastAccepted + 1) stays inside the acceptance
// window with no resync needed. Unplanned reboots (crash/power loss) still resume
// from the write-ahead mark -- that direction is the security-conservative one.
Os::ScopeLock lock(this->m_sequenceNumberLock);
const Os::File::Status status = this->writeSequenceNumber(this->m_sequenceNumber);
if (status == Os::File::OP_OK) {
// Disk now equals lastAccepted: the next accepted frame is at/above the mark,
// which re-triggers a normal write-ahead persist after the reboot.
this->m_persistedHighWater = this->m_sequenceNumber;
this->m_persistRetryBackoff = 0;
}
// On failure writeSequenceNumber already emitted SequenceNumberWriteFailed; the
// stale (higher) write-ahead record stays on disk, which is safe -- it just means
// ground must resync forward after this reboot, same as before this handler existed.
}
Comment on lines +259 to 276

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.

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo files matching relevant names =="
git ls-files | rg '(^|/)(TcSecurityDeframer\.(fpp|cpp|hpp)|Watchdog\.(fpp|cpp|hpp)|\.*)(\.fpp|\.cpp|\.hpp)$' | sed -n '1,120p'

echo
echo "== target snippets =="
fd -a 'TcSecurityDeframer\.cpp|Watchdog\.cpp' . | while read -r f; do
  echo "--- $f"
  wc -l "$f"
done

echo
echo "== TcSecurityDeframer prepareForReboot/sequence write area =="
path=$(git ls-files | rg 'TcSecurityDeframer\.cpp$' | head -n1 || true)
if [ -n "${path:-}" ]; then
  nl -ba "$path" | sed -n '220,305p'
fi

echo
echo "== Watchdog stop handler area =="
path=$(git ls-files | rg 'Watchdog\.cpp$' | head -n1 || true)
if [ -n "${path:-}" ]; then
  nl -ba "$path" | sed -n '1,120p'
fi

echo
echo "== prepareForReboot references =="
rg -n "prepareForReboot|stopWatchdog|fatalHandler|modeManager|stop_handler|prepareForReboot_handler|writeSequenceNumber" -S .

Repository: Open-Source-Space-Foundation/proves-core-reference

Length of output: 229


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== top-level directory =="
pwd
ls -la | sed -n '1,80p'

echo
echo "== tracked files by root =="
git ls-files | sed -n '1,200p'

echo
echo "== search relevant filenames =="
git ls-files | rg -i 'TcSecurityDeframer|Watchdog|SecurityDeframer|Security' | sed -n '1,200p'

Repository: Open-Source-Space-Foundation/proves-core-reference

Length of output: 17554


🏁 Script executed:

#!/bin/bash
set -euo pipefail

deframer=$(git ls-files | rg 'PROVESFlightControllerReference/Components/TcSecurityDeframer/TcSecurityDeframer\.cpp$' | head -n1)
deframer_h=$(git ls-files | rg 'PROVESFlightControllerReference/Components/TcSecurityDeframer/TcSecurityDeframer\.hpp$' | head -n1)
watchdog=$(git ls-files | rg 'PROVESFlightControllerReference/Components/Watchdog/Watchdog\.cpp$' | head -n1)

echo "--- $deframer ($(wc -l < "$deframer") lines) ---"
nl -ba "$deframer" | sed -n '220,310p'

echo
echo "--- tdeframer relevant symbols ---"
rg -n "writeSequenceNumber|prepareForReboot_handler|prepareForReboot|Fp|Fw|invoke_|dispatch|" "$deframer" "$deframer_h" | sed -n '1,200p'

echo
echo "--- $watchdog ($(wc -l < "$watchdog") lines) ---"
nl -ba "$watchdog" | sed -n '1,120p'

echo
echo "--- watchdog relevant stop/fan-out code ---"
rg -n "stopHandler|stop_watchdog|prepareForReboot_out|prepareForReboot|getNum_prepareForReboot_OutputPorts|isConnected_prepareForReboot_OutputPort" "$watchdog" -S

Repository: Open-Source-Space-Foundation/proves-core-reference

Length of output: 333


🏁 Script executed:

#!/bin/bash
set -euo pipefail

deframer=$(git ls-files | rg 'PROVESFlightControllerReference/Components/TcSecurityDeframer/TcSecurityDeframer\.cpp$' | head -n1)
deframer_h=$(git ls-files | rg 'PROVESFlightControllerReference/Components/TcSecurityDeframer/TcSecurityDeframer\.hpp$' | head -n1)
watchdog=$(git ls-files | rg 'PROVESFlightControllerReference/Components/Watchdog/Watchdog\.cpp$' | head -n1)

echo "--- $deframer ($(wc -l "$deframer" | awk '{print $1}') lines) ---"
sed -n '220,310p' "$deframer" | cat -n

echo
echo "--- tdeframer relevant symbols ---"
grep -n -E "writeSequenceNumber|prepareForReboot_handler|prepareForReboot|Fp|Fw|invoke_|dispatch|osFile_write|Os::File::write" "$deframer" "$deframer_h" | sed -n '1,200p' || true

echo
echo "--- $watchdog ($(wc -l "$watchdog" | awk '{print $1}') lines) ---"
sed -n '1,120p' "$watchdog" | cat -n

echo
echo "--- watchdog relevant stop/fan-out code ---"
grep -n -E "stopHandler|stop_watchdog|prepareForReboot_out|prepareForReboot|getNum_prepareForReboot_OutputPorts|isConnected_prepareForReboot_OutputPort|waitCommandQueue|Task" "$watchdog" -S || true

echo
echo "--- global prepareForReboot references ---"
grep -R -n "prepareForReboot" PROVESFlightControllerReference/components PROVESFlightControllerReference/Components PROVESFlightControllerReference/test -S | sed -n '1,240p' || true

Repository: Open-Source-Space-Foundation/proves-core-reference

Length of output: 15541


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- FileHelper implementation/declaration candidates ---"
git ls-files | rg 'FileHelper\.(hpp|cpp)|Utilities|File' | sed -n '1,240p'

echo
echo "--- FileHelper content ---"
f=$(git ls-files | rg 'FileHelper\.hpp$|FileHelper\.cpp$' | head -n1 || true)
if [ -z "${f:-}" ]; then
  f1=$(git ls-files | rg 'FileHelper\.hpp$' | head -n1 || true)
  f2=$(git ls-files | rg 'FileHelper\.cpp$' | head -n1 || true)
  for f in "${f1:-}" "${f2:-}"; do
    [ -n "$f" ] || continue
    echo "+++ $f ($(wc -l "$f" | awk '{print $1}') lines) +++"
    sed -n '1,220p' "$f" | cat -n
  done
fi

echo
echo "--- watchdog prepareForReboot references ---"
grep -R -- 'prepareForReboot\|stopWatchdog\|fatalHandler' PROVESFlightControllerReference/Components/Watchdog PROVESFlightControllerReference/Components/ModeManager PROVESFlightControllerReference -g '*.fpp' -g '*.cpp' -g '*.hpp' | sed -n '1,240p'

echo
echo "--- deframer fprime component interfaces ---"
sed -n '1,130p' PROVESFlightControllerReference/Components/TcSecurityDeframer/TcSecurityDeframer.fpp | cat -n
sed -n '1,120p' PROVESFlightControllerReference/Components/Watchdog/Watchdog.fpp | cat -n

Repository: Open-Source-Space-Foundation/proves-core-reference

Length of output: 9132


🌐 Web query:

Fprime sync input port invocation model timeout receive queue asynchronous port timeout-less

💡 Result:

In the F´ (F Prime) framework, port invocation behavior is determined by the port's kind, which dictates whether it operates synchronously or asynchronously [1][2][3]. Synchronous (sync) Input Ports: These ports operate like a standard function call [1][4]. When a component invokes a synchronous output port connected to a sync input port, the code runs immediately in the execution context of the invoking component (i.e., on the invoker's thread) [1][2][5]. Because these are direct function calls, they are inherently "timeout-less" at the framework level; there is no queue or underlying mechanism that manages timeouts for these invocations [1][6][5]. If the invoked handler hangs or takes a long time, the caller is blocked for that duration [4]. Asynchronous (async) Input Ports: These ports are backed by a message queue in the receiver component [4]. When an output port invokes an async input port, the invocation (message) is placed into the receiver's queue, and the caller returns immediately [4][7]. The actual function handler is later dispatched by the receiver's own thread when it processes its queue [1][2][6]. Because the invocation is decoupled from the caller, the framework does not implement a "timeout" for individual asynchronous port calls [1][4]. Timeouts in F´: The F´ framework does not provide a built-in timeout mechanism for general port invocations. Timeouts are typically managed at the application level through design patterns [7][8]. For example, the Svc::Health component monitors component responsiveness by periodically sending "ping" messages via asynchronous ports and tracking the time taken for a response to return via a separate port [9][8]. If a response does not arrive within a configured number of cycles (rate group ticks), the health service flags a timeout and can trigger events (e.g., WARNING or FATAL) [9][8]. Summary of invocation models: - Sync Input: Direct function call, runs on the caller's thread, no queue, no framework-level timeout [1][6][5]. - Async Input: Queued, runs on the receiver's thread, requires an active/queued component, no framework-level timeout [1][4][5]. - Timeouts: Explicitly managed by application-level logic (e.g., Svc::Health), not by the port invocation itself [9][8].

Citations:


🌐 Web query:

FprimeExtras Utilities FileHelper writeToFile FPrime FileHelper.cpp source

💡 Result:

There is no standard or official component named "FprimeExtras" or "FileHelper" in the core F Prime (F´) framework that provides a writeToFile utility [1][2]. File operations in F Prime are handled by the Operating System abstraction layer, specifically the Os::File class [3][4][5]. If you are looking for how to write data to a file in F Prime, you should use the Os::File class. The standard interface for writing data is the write method, which is defined in the Os::File header [4][5][6]: Status write(const U8* buffer, FwSizeType& size); Status write(const U8* buffer, FwSizeType& size, WaitType wait) override; The Os::File::write method writes data from a buffer to the file [4][5]. When using the overloaded version with a WaitType parameter, you can specify whether the call should block (WAIT) until the data is written to disk or return immediately (NO_WAIT) after the data is handed off to the operating system [4][5]. If you encountered a reference to a "FileHelper" class, it may be part of a custom project-specific utility or an external library not included in the core F Prime framework (e.g., Grid Solutions Framework uses a FileHelper class, but it is unrelated to F Prime) [7]. Top Results: [3] https://fprime.jpl.nasa.gov/latest/docs/reference/api/cpp/html/_os_2_file_8cpp_source.html [4] https://fprime.jpl.nasa.gov/v3.6.0/docs/reference/api/cpp/html/class_os_1_1_file.html [5] https://fprime.jpl.nasa.gov/devel/Os/File.hpp [6] https://fprime.jpl.nasa.gov/v4.0.0/docs/reference/api/cpp/html/_file_8hpp_source.html

Citations:


🌐 Web query:

"FileHelper.hpp" "writeToFile" "Os::File"

💡 Result:

The F' (F Prime) flight software framework provides the Os::File class for file operations, defined in the header file Os/File.hpp [1][2]. The Os::File class does not typically contain a helper class named FileHelper.hpp with a static writeToFile method. Instead, file writing is performed directly through the member functions of the Os::File class [3][4]. To write to a file using Os::File, you must: 1. Instantiate an Os::File object [3]. 2. Open the file using the open method, specifying the file path and the desired mode (e.g., Os::File::OPEN_WRITE or Os::File::OPEN_CREATE) [1][3]. 3. Use the write member function to transfer data from a buffer to the file [1][4]. The Os::File::write method is overloaded [3][4]: - Status write(const U8* buffer, FwSizeType& size): Writes data from the buffer up to the specified size. This version blocks until the requested data has been written [1][4]. - Status write(const U8* buffer, FwSizeType& size, WaitType wait): Allows specifying whether to wait for the write to complete (WAIT) or return once the data is sent to the OS (NO_WAIT) [3][4]. If you are looking for a higher-level "writeToFile" utility, it is not part of the standard F' Os::File API and would need to be implemented as a custom helper function in your application code using the provided Os::File interface [1][4]. Note: The Go programming language standard library also features an os package with functions like os.WriteFile, which is distinct from the F' C++ framework [5]. Ensure you are referencing the correct documentation for your specific environment.

Citations:


Bound the SD-card persist inside the synchronous reboot fan-out. watchdog.stop_handler() invokes all prepareForReboot receivers on the stop path before m_run = false, and TcSecurityDeframer::prepareForReboot_handler() persists m_sequenceNumber synchronously via writeSequenceNumber(). A stalled SD-card persist can therefore delay every receiver in the failsafe reboot chain, including resetting m_run. Use a bounded/non-blocking persist, or queue this persist with an application-level timeout so the watchdog-stops are no longer blocked by SD-card latency.

🧰 Tools
🪛 Clang (14.0.6)

[warning] 266-266: variable 'status' is not initialized

(cppcoreguidelines-init-variables)

📍 Affects 2 files
  • PROVESFlightControllerReference/Components/TcSecurityDeframer/TcSecurityDeframer.cpp#L259-L276 (this comment)
  • PROVESFlightControllerReference/Components/Watchdog/Watchdog.cpp#L49-L58
🤖 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/TcSecurityDeframer/TcSecurityDeframer.cpp`
around lines 259 - 276, Bound the synchronous persistence triggered by
TcSecurityDeframer::prepareForReboot_handler so watchdog.stop_handler cannot be
stalled by SD-card latency. Replace the direct blocking writeSequenceNumber call
with a bounded/non-blocking mechanism or queue it with an application-level
timeout, while preserving the planned-reboot sequence semantics and updating
persistence state only after a successful write. In Watchdog.cpp lines 49-58,
retain the existing stop fan-out but ensure its prepareForReboot invocation is
no longer blocked by this persistence operation.


Os::File::Status TcSecurityDeframer ::writeSequenceNumber(const U32 value) {
Os::File::Status status = Utilities::FileHelper::writeToFile(this->m_sequenceNumberFilePath.toChar(), value);
const U64 record = (static_cast<U64>(value) << 32) | static_cast<U64>(~value);
Os::File::Status status = Utilities::FileHelper::writeToFile(this->m_sequenceNumberFilePath.toChar(), record);
if (status != Os::File::OP_OK) {
// Log the failure to write the default sequence number
// Log the failure to write the sequence number (throttled -- see writeAheadPersistIfNeeded,
// this can now only fire at most once per SEQ_NUM_PERSIST_STRIDE accepted frames)
this->log_WARNING_HI_SequenceNumberWriteFailed(static_cast<Os::FileStatus::T>(status));
} else {
// Clear throttle for sequence number write failure
Expand All @@ -217,4 +290,42 @@ Os::File::Status TcSecurityDeframer ::writeSequenceNumber(const U32 value) {
return status;
}

void TcSecurityDeframer ::writeAheadPersistIfNeeded(U32 acceptedSeqNum) {
// Only consider persisting when the accepted sequence number has caught up to (or passed) the
// last write-ahead high-water mark, AND we are not currently backing off after a prior
// failure. This bounds filesystem writes to at most once every SEQ_NUM_PERSIST_STRIDE accepted
// frames in the steady state instead of once per frame (issue #461's original bug).
if (this->m_persistRetryBackoff > 0) {
--this->m_persistRetryBackoff;
return;
}

if (acceptedSeqNum >= this->m_persistedHighWater) {
// Write comfortably ahead of what we've actually seen so a burst of N-1 more accepted
// frames doesn't require another persist before the next stride boundary.
const U32 newHighWater = acceptedSeqNum + SEQ_NUM_PERSIST_STRIDE;
Os::File::Status status = this->writeSequenceNumber(newHighWater);
if (status == Os::File::OP_OK) {
// CORE INVARIANT: only advance m_persistedHighWater on a CONFIRMED successful write.
// An earlier version of this method advanced it unconditionally (including on
// failure), reasoning it would only cause "the on-disk value to be a bit stale" --
// that was a real security regression: it let accepted sequence numbers advance
// arbitrarily far past a STALE on-disk value while persist writes kept failing, so a
// reboot during a failure streak could reopen a replay window for that entire gap
// (not bounded by SEQ_NUM_PERSIST_STRIDE at all). Only a confirmed-successful write
// is allowed to move the high-water mark forward.
this->m_persistedHighWater = newHighWater;
this->m_persistRetryBackoff = 0;
} else {
// Fail safe, not fail open: do NOT advance the high-water mark, so the invariant
// (disk >= last accepted, whenever a persist has ever succeeded) keeps holding for
// every frame accepted between now and the next successful write. Do NOT retry on
// every subsequent frame either (that degrades back to the original #461 race) --
// back off for a bounded number of frames instead, and make noise every time.
this->m_persistRetryBackoff = SEQ_NUM_PERSIST_RETRY_BACKOFF;
this->log_WARNING_HI_SequenceNumberPersistFailed(static_cast<Os::FileStatus::T>(status), acceptedSeqNum);
}
}
}

} // namespace Components
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,22 @@ module Components {
@ SequenceNumberWriteFailed indicates that there was an error writing the sequence number to file
event SequenceNumberWriteFailed(status: Os.FileStatus) severity warning high id 8 format "Failed to write sequence number, error: {}" throttle 2

@ SequenceNumberPersistFailed indicates that the write-ahead high-water persist (issue #461)
@ failed to reach disk. The in-RAM high-water mark is deliberately NOT advanced in this case
@ (that would reopen the anti-replay window on a subsequent reboot) -- a retry is scheduled
@ after a bounded number of further accepted frames instead of retrying on every single one.
event SequenceNumberPersistFailed(status: Os.FileStatus, accepted_seq_num: U32) severity warning high id 10 format "Write-ahead sequence-number persist failed, error: {} (accepted seq {}); retrying after a bounded backoff, not every frame" throttle 2

@ SequenceNumberInvalid indicates that a received packet had a sequence number that was outside of the acceptable window
event SequenceNumberInvalid(packet_seq_num: U32, seq_num: U32, window: U32) severity warning high id 2 format "Sequence number less than last accepted or out of window: Received={}, LastAccepted={}, Window={}" throttle 2

@ SequenceNumberRecordInvalid indicates that the persisted sequence-number record failed its
@ torn-write validation (checksum mismatch) on boot, or could not be read for another reason.
@ The runtime sequence number falls back to 0 (same as a genuine first boot) so command
@ capability is never blocked on this outcome; ground may issue SET_SEQ_NUM to fast-forward
@ past any previously-used sequence numbers if the real last-used value is known.
event SequenceNumberRecordInvalid(stored_value: U32) severity warning high id 9 format "Persisted sequence-number record failed validation (raw value read: {}); falling back to 0 -- use SET_SEQ_NUM to fast-forward if needed"

@ AuthenticationFailed indicates that a received packet failed authentication
event AuthenticationFailed(auth_status: PacketAuthenticatorStatus, rc: I32) severity warning high id 1 format "Authentication failed: Status={}, PSA Return Code={}" throttle 2

Expand Down Expand Up @@ -82,6 +95,12 @@ module Components {
@ Port receiving back ownership of buffers sent on dataOut
sync input port dataReturnIn: Svc.ComDataWithContext

@ Called before an intentional reboot: persist the EXACT current sequence
@ number (instead of the write-ahead high-water mark) so ground stays in
@ sync across planned reboots and does not need to burn through the
@ written-ahead gap (issue #461 write-ahead persistence).
sync input port prepareForReboot: Fw.Signal

###############################################################################
# Standard AC Ports: Required for Channels, Events, Commands, and Parameters #
###############################################################################
Expand Down
Loading
Loading