-
Notifications
You must be signed in to change notification settings - Fork 18
fix: seq-number persistence rework (#461) + survive comms buffer-pool exhaustion (#471) #473
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
55e254e
071e481
d89b568
3459b7b
9001256
362209f
fb56b6f
caa1ce3
58067ad
a0a1654
b7dbcfa
18541c7
fba5f70
b0bc46d
45ec7f2
b7a8570
0efd521
65c068b
c40790f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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() {} | ||
|
|
||
|
|
@@ -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); | ||
|
|
@@ -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); | ||
| } | ||
|
|
@@ -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); | ||
|
|
@@ -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); | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" -SRepository: 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' || trueRepository: 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 -nRepository: Open-Source-Space-Foundation/proves-core-reference Length of output: 9132 🌐 Web query:
💡 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:
💡 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:
💡 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. 🧰 Tools🪛 Clang (14.0.6)[warning] 266-266: variable 'status' is not initialized (cppcoreguidelines-init-variables) 📍 Affects 2 files
🤖 Prompt for AI Agents |
||
|
|
||
| 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 | ||
|
|
@@ -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 | ||
There was a problem hiding this comment.
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:
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, andlib/fprime-extrasnow point toOpen-Source-Space-Foundation, butMakefileonly runsgit submodule update --init --recursive. Existing initialized checkouts keep the cached old URLs in.git/configand can fail to fetch if the old remotes are unavailable.Add
git submodule sync --recursivebefore submodule updates, or document the one-time migration command.🤖 Prompt for AI Agents