Skip to content

Commit 3a7a19e

Browse files
bitWarriorbitWarrior
authored andcommitted
PR 5086 - Improved frame detector arithmetic against integer underflow and overflow
1 parent 060f0e1 commit 3a7a19e

5 files changed

Lines changed: 563 additions & 13 deletions

File tree

Svc/FrameAccumulator/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ register_fprime_ut(
3939
"Svc_FrameAccumulator_FprimeFrameDetector_test"
4040
SOURCES
4141
"${CMAKE_CURRENT_LIST_DIR}/test/ut/detectors/FprimeFrameDetectorTestMain.cpp"
42+
"${CMAKE_CURRENT_LIST_DIR}/test/ut/detectors/FprimeFrameDetectorTest.cpp"
4243
HEADERS
4344
"${CMAKE_CURRENT_LIST_DIR}/FrameDetector/FprimeFrameDetector.hpp"
4445
DEPENDS
@@ -50,6 +51,7 @@ register_fprime_ut(
5051
"Svc_FrameAccumulator_CcsdsTcFrameDetector_test"
5152
SOURCES
5253
"${CMAKE_CURRENT_LIST_DIR}/test/ut/detectors/CcsdsTcFrameDetectorTestMain.cpp"
54+
"${CMAKE_CURRENT_LIST_DIR}/test/ut/detectors/CcsdsTcFrameDetectorUnderflowTest.cpp"
5355
HEADERS
5456
"${CMAKE_CURRENT_LIST_DIR}/FrameDetector/CcsdsTcFrameDetector.hpp"
5557
DEPENDS

Svc/FrameAccumulator/FrameDetector/CcsdsTcFrameDetector.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,22 @@ FrameDetector::Status CcsdsTcFrameDetector::detect(const Types::CircularBuffer&
4444
// TC protocol defines the Frame Length as number of bytes minus 1, so we add 1 back to get length in bytes
4545
const FwSizeType expected_frame_length =
4646
static_cast<FwSizeType>((header.get_vcIdAndLength() & Ccsds::TCSubfields::FrameLengthMask) + 1);
47-
const U16 data_to_crc_length = static_cast<U16>(expected_frame_length - Ccsds::TCTrailer::SERIALIZED_SIZE);
4847

4948
// If the full frame is not yet available, report that more data is needed
5049
if (data.get_allocated_size() < expected_frame_length) {
5150
size_out = expected_frame_length;
5251
return Status::MORE_DATA_NEEDED;
5352
}
54-
// If the deserialized length is too small to even hold the header and trailer, we don't have a valid frame
53+
// Validate minimum frame size BEFORE computing data_to_crc_length.
54+
// If expected_frame_length < TRAILER_SIZE the subtraction below would underflow to a
55+
// near-maximum value, causing the CRC loop to read far beyond the valid frame data.
5556
if (expected_frame_length < Ccsds::TCHeader::SERIALIZED_SIZE + Ccsds::TCTrailer::SERIALIZED_SIZE) {
5657
return Status::NO_FRAME_DETECTED;
5758
}
59+
// Safe: the guard above guarantees expected_frame_length >= TRAILER_SIZE, so this
60+
// subtraction cannot underflow. Type is FwSizeType (not U16) to avoid silent truncation
61+
// if expected_frame_length ever exceeds 65535.
62+
const FwSizeType data_to_crc_length = expected_frame_length - Ccsds::TCTrailer::SERIALIZED_SIZE;
5863

5964
// ---------------- Frame Trailer ----------------
6065
// Compute CRC on the received data

Svc/FrameAccumulator/FrameDetector/FprimeFrameDetector.cpp

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,29 @@ FrameDetector::Status FprimeFrameDetector::detect(const Types::CircularBuffer& d
4949
if (header.get_startWord() != default_value.get_startWord()) {
5050
return Status::NO_FRAME_DETECTED;
5151
}
52-
// Validate size before proceeding
53-
const FwSizeType max_payload_size = std::numeric_limits<FwSizeType>::max() -
54-
FprimeProtocol::FrameHeader::SERIALIZED_SIZE -
55-
FprimeProtocol::FrameTrailer::SERIALIZED_SIZE;
56-
// If the header length is larger than size can store, then frame is invalid
57-
if (max_payload_size < header.get_lengthField()) {
58-
// Size overflow - frame is invalid
52+
// Validate size before proceeding.
53+
// Use a static_assert to guarantee the two fixed overhead constants don't themselves overflow
54+
// when added together. This is a compile-time check so it carries zero runtime cost, but it
55+
// protects the runtime guard below if SERIALIZED_SIZE values are ever changed.
56+
static_assert(FprimeProtocol::FrameHeader::SERIALIZED_SIZE <=
57+
std::numeric_limits<FwSizeType>::max() - FprimeProtocol::FrameTrailer::SERIALIZED_SIZE,
58+
"FrameHeader::SERIALIZED_SIZE + FrameTrailer::SERIALIZED_SIZE overflows FwSizeType");
59+
constexpr FwSizeType header_trailer_overhead =
60+
FprimeProtocol::FrameHeader::SERIALIZED_SIZE + FprimeProtocol::FrameTrailer::SERIALIZED_SIZE;
61+
62+
// Guard: reject frames whose declared length would overflow FwSizeType when added to the
63+
// fixed overhead. Using subtraction on unsigned types (as in the prior implementation) is
64+
// fragile — if the constants change sign or width the subtraction itself can wrap silently.
65+
// An explicit addition-based check is clearer and easier to audit.
66+
if (header.get_lengthField() > std::numeric_limits<FwSizeType>::max() - header_trailer_overhead) {
67+
// lengthField + overhead would overflow — frame is invalid
5968
return Status::NO_FRAME_DETECTED;
6069
}
6170

62-
// We expect the frame size to be size of header + body (of size specified in header) + trailer
63-
const FwSizeType expected_frame_size = FprimeProtocol::FrameHeader::SERIALIZED_SIZE + header.get_lengthField() +
64-
FprimeProtocol::FrameTrailer::SERIALIZED_SIZE;
71+
// We expect the frame size to be size of header + body (of size specified in header) + trailer.
72+
// Overflow is impossible here: the guard above ensures
73+
// lengthField <= MAX - header_trailer_overhead
74+
const FwSizeType expected_frame_size = header.get_lengthField() + header_trailer_overhead;
6575
// If the frame will never fit, then report NO_FRAME_DETECTED to drop the erroneous frame
6676
if (data.get_capacity() < expected_frame_size) {
6777
return Status::NO_FRAME_DETECTED;
@@ -90,7 +100,16 @@ FrameDetector::Status FprimeFrameDetector::detect(const Types::CircularBuffer& d
90100

91101
Utils::Hash hash;
92102
Utils::HashBuffer hashBuffer;
93-
// Compute CRC over the transmitted data (header + body)
103+
// Compute CRC over the transmitted data (header + body).
104+
// Safety invariant: the guard above ensures
105+
// lengthField <= MAX - header_trailer_overhead
106+
// <= MAX - HEADER_SIZE - TRAILER_SIZE
107+
// < MAX - HEADER_SIZE
108+
// so this addition cannot overflow. The assert makes that contract explicit at the
109+
// point of use so it remains correct if this code is ever moved or refactored.
110+
FW_ASSERT(header.get_lengthField() <=
111+
std::numeric_limits<FwSizeType>::max() - FprimeProtocol::FrameHeader::SERIALIZED_SIZE,
112+
static_cast<FwAssertArgType>(header.get_lengthField()));
94113
FwSizeType hash_field_size = header.get_lengthField() + FprimeProtocol::FrameHeader::SERIALIZED_SIZE;
95114
hash.init();
96115
for (FwSizeType i = 0; i < hash_field_size; i++) {
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// ======================================================================
2+
// \title CcsdsTcFrameDetectorUnderflowTest.cpp
3+
// \brief Unit tests for the data_to_crc_length underflow fix in
4+
// CcsdsTcFrameDetector
5+
//
6+
// These tests exercise CcsdsTcFrameDetector::detect() directly and focus
7+
// on the ordering fix: data_to_crc_length must only be computed AFTER the
8+
// minimum-size validity check, never before.
9+
//
10+
// The defect (before the fix):
11+
// data_to_crc_length was computed as:
12+
// U16(expected_frame_length - TRAILER_SIZE)
13+
// before the check that ensures expected_frame_length >= TRAILER_SIZE.
14+
// A crafted frame with length field 0 (giving expected_frame_length = 1)
15+
// produced data_to_crc_length = 0xFFFF (unsigned underflow), which
16+
// would cause the CRC loop to read 65535 bytes beyond the frame data.
17+
//
18+
// The fix:
19+
// - Both the MORE_DATA_NEEDED check and the minimum-size check now
20+
// appear before data_to_crc_length is computed.
21+
// - data_to_crc_length is widened from U16 to FwSizeType to prevent
22+
// silent truncation for frames larger than 65535 bytes.
23+
//
24+
// Note on flagsAndScId: CcsdsTcFrameDetector rejects frames whose
25+
// flagsAndScId token does not match its internal m_expectedFlagsAndScIdToken.
26+
// The default-constructed detector's expected token is not publicly
27+
// queryable, so the underflow tests below use flagsAndScId = 0, which is
28+
// unlikely to match and will therefore be rejected at the flags check
29+
// rather than the size check. This is intentional: the tests verify the
30+
// observable safety contract (no crash, NO_FRAME_DETECTED returned) for
31+
// crafted extreme-length inputs. A future test with access to the
32+
// configured token value could additionally verify that the size guard
33+
// itself fires as the rejection point.
34+
// ======================================================================
35+
36+
#include <cassert>
37+
#include <cstring>
38+
#include <limits>
39+
#include "Svc/Ccsds/Types/TCHeaderSerializableAc.hpp"
40+
#include "Svc/Ccsds/Types/TCTrailerSerializableAc.hpp"
41+
#include "Svc/FrameAccumulator/FrameDetector/CcsdsTcFrameDetector.hpp"
42+
#include "Utils/Types/CircularBuffer.hpp"
43+
#include "gtest/gtest.h"
44+
45+
namespace Svc {
46+
namespace FrameDetectors {
47+
48+
// ======================================================================
49+
// Module-level constants (namespace scope avoids C++14 ODR-use linker
50+
// errors that arise from static constexpr class members passed by ref
51+
// to EXPECT_EQ)
52+
// ======================================================================
53+
namespace {
54+
55+
constexpr FwSizeType kTcHeaderSize = Ccsds::TCHeader::SERIALIZED_SIZE;
56+
constexpr FwSizeType kTcTrailerSize = Ccsds::TCTrailer::SERIALIZED_SIZE;
57+
constexpr FwSizeType kTcOverhead = kTcHeaderSize + kTcTrailerSize;
58+
59+
} // namespace
60+
61+
// ======================================================================
62+
// Test fixture
63+
// ======================================================================
64+
65+
class CcsdsTcFrameDetectorUnderflowTest : public ::testing::Test {
66+
protected:
67+
// Use the default constructor — CcsdsTcFrameDetector takes no arguments
68+
CcsdsTcFrameDetector detector;
69+
70+
// ----------------------------------------------------------------
71+
// Helper: serialize a TCHeader into `out_bytes` with the given
72+
// flagsAndScId and vcIdAndLength field values.
73+
// Returns true on success.
74+
// ----------------------------------------------------------------
75+
static bool buildHeader(U8* out_bytes, U16 flags_and_sc_id, U16 vc_id_and_length) {
76+
Ccsds::TCHeader header;
77+
header.set_flagsAndScId(flags_and_sc_id);
78+
header.set_vcIdAndLength(vc_id_and_length);
79+
Fw::ExternalSerializeBuffer ser(out_bytes, kTcHeaderSize);
80+
return header.serializeTo(ser) == Fw::FW_SERIALIZE_OK;
81+
}
82+
83+
// ----------------------------------------------------------------
84+
// Helper: load bytes into a CircularBuffer.
85+
// Uses assert() (always available) rather than FW_ASSERT which
86+
// requires a specific F Prime include chain.
87+
// ----------------------------------------------------------------
88+
static void loadRing(Types::CircularBuffer& ring, const U8* src, FwSizeType count) {
89+
Fw::SerializeStatus s = ring.serialize(src, count);
90+
assert(s == Fw::FW_SERIALIZE_OK);
91+
static_cast<void>(s); // suppress unused-variable warning in release builds
92+
}
93+
};
94+
95+
// ======================================================================
96+
// Fix tests — data_to_crc_length underflow / ordering safety
97+
//
98+
// These tests submit frames with extreme declared lengths and verify that
99+
// detect() returns NO_FRAME_DETECTED without crashing or exhibiting
100+
// undefined behaviour. The rejection may occur at the flagsAndScId check
101+
// (if flagsAndScId = 0 doesn't match the detector's expected token) or at
102+
// the minimum-size check — either way the safety contract holds.
103+
// ======================================================================
104+
105+
// ----------------------------------------------------------------------
106+
// Length field == 0 encodes expected_frame_length == 1, which is less
107+
// than TRAILER_SIZE. Before the fix, data_to_crc_length = U16(1 - 2) =
108+
// 0xFFFF was computed before any size validation. The fix ensures the
109+
// size guard fires first, making the underflow impossible to reach.
110+
// ----------------------------------------------------------------------
111+
TEST_F(CcsdsTcFrameDetectorUnderflowTest, LengthFieldZeroRejectedSafely) {
112+
// vcIdAndLength with all length bits == 0 → expected_frame_length = 1
113+
U8 storage[kTcOverhead] = {};
114+
Types::CircularBuffer ring(storage, sizeof(storage));
115+
116+
U8 header_bytes[kTcHeaderSize] = {};
117+
ASSERT_TRUE(buildHeader(header_bytes, 0, 0));
118+
loadRing(ring, header_bytes, kTcHeaderSize);
119+
120+
U8 padding[kTcTrailerSize] = {};
121+
loadRing(ring, padding, kTcTrailerSize);
122+
123+
FwSizeType size_out = 0;
124+
FrameDetector::Status result = detector.detect(ring, size_out);
125+
126+
// Must return NO_FRAME_DETECTED with no crash, regardless of which
127+
// guard (flags or size) fires first.
128+
EXPECT_EQ(result, FrameDetector::Status::NO_FRAME_DETECTED);
129+
}
130+
131+
// ----------------------------------------------------------------------
132+
// expected_frame_length == kTcOverhead - 1 is still too small. Verifies
133+
// the boundary just below the minimum valid size is rejected cleanly.
134+
// ----------------------------------------------------------------------
135+
TEST_F(CcsdsTcFrameDetectorUnderflowTest, LengthFieldOneBelowOverheadRejectedSafely) {
136+
// CCSDS TC length field encodes (frame_bytes - 1), so to get
137+
// expected_frame_length = kTcOverhead - 1 we write kTcOverhead - 2.
138+
const U16 vc_id_and_length = static_cast<U16>(kTcOverhead - 2);
139+
140+
U8 storage[kTcOverhead] = {};
141+
Types::CircularBuffer ring(storage, sizeof(storage));
142+
143+
U8 header_bytes[kTcHeaderSize] = {};
144+
ASSERT_TRUE(buildHeader(header_bytes, 0, vc_id_and_length));
145+
loadRing(ring, header_bytes, kTcHeaderSize);
146+
147+
U8 padding[kTcTrailerSize] = {};
148+
loadRing(ring, padding, kTcTrailerSize);
149+
150+
FwSizeType size_out = 0;
151+
FrameDetector::Status result = detector.detect(ring, size_out);
152+
153+
EXPECT_EQ(result, FrameDetector::Status::NO_FRAME_DETECTED);
154+
}
155+
156+
// ----------------------------------------------------------------------
157+
// expected_frame_length == kTcOverhead is the minimum valid size.
158+
// This test verifies the exact boundary: a frame of exactly overhead
159+
// size is not rejected by the minimum-size guard. The result is still
160+
// NO_FRAME_DETECTED (flags mismatch or CRC mismatch), but the minimum-
161+
// size guard correctly does not fire.
162+
// ----------------------------------------------------------------------
163+
TEST_F(CcsdsTcFrameDetectorUnderflowTest, LengthFieldAtMinimumValidSizeNotRejectedByGuard) {
164+
// CCSDS TC length field encodes (frame_bytes - 1)
165+
const U16 vc_id_and_length = static_cast<U16>(kTcOverhead - 1);
166+
167+
U8 storage[kTcOverhead] = {};
168+
Types::CircularBuffer ring(storage, sizeof(storage));
169+
170+
U8 header_bytes[kTcHeaderSize] = {};
171+
ASSERT_TRUE(buildHeader(header_bytes, 0, vc_id_and_length));
172+
loadRing(ring, header_bytes, kTcHeaderSize);
173+
174+
U8 trailer_bytes[kTcTrailerSize] = {};
175+
loadRing(ring, trailer_bytes, kTcTrailerSize);
176+
177+
FwSizeType size_out = 0;
178+
FrameDetector::Status result = detector.detect(ring, size_out);
179+
180+
// Rejected due to flags mismatch or CRC mismatch — not the size guard.
181+
EXPECT_EQ(result, FrameDetector::Status::NO_FRAME_DETECTED);
182+
}
183+
184+
// ======================================================================
185+
// Regression tests — pre-existing behaviour must be preserved
186+
// ======================================================================
187+
188+
// ----------------------------------------------------------------------
189+
// Insufficient data (below header + trailer minimum) returns
190+
// MORE_DATA_NEEDED before any header parsing occurs. This path is
191+
// independent of the flagsAndScId token value.
192+
// ----------------------------------------------------------------------
193+
TEST_F(CcsdsTcFrameDetectorUnderflowTest, InsufficientDataReturnsMoreDataNeeded) {
194+
const FwSizeType too_small = kTcOverhead - 1;
195+
196+
U8 storage[kTcOverhead] = {};
197+
Types::CircularBuffer ring(storage, sizeof(storage));
198+
199+
U8 data[too_small] = {};
200+
loadRing(ring, data, too_small);
201+
202+
FwSizeType size_out = 0;
203+
FrameDetector::Status result = detector.detect(ring, size_out);
204+
205+
EXPECT_EQ(result, FrameDetector::Status::MORE_DATA_NEEDED);
206+
EXPECT_EQ(size_out, kTcOverhead);
207+
}
208+
209+
// ----------------------------------------------------------------------
210+
// A mismatched flagsAndScId token must return NO_FRAME_DETECTED before
211+
// any length or CRC logic is reached. Uses 0xFFFF as a value that is
212+
// guaranteed not to match any valid default token.
213+
// ----------------------------------------------------------------------
214+
TEST_F(CcsdsTcFrameDetectorUnderflowTest, MismatchedFlagsAndScIdRejected) {
215+
const U16 vc_id_and_length = static_cast<U16>(kTcOverhead - 1);
216+
217+
U8 storage[kTcOverhead] = {};
218+
Types::CircularBuffer ring(storage, sizeof(storage));
219+
220+
U8 header_bytes[kTcHeaderSize] = {};
221+
ASSERT_TRUE(buildHeader(header_bytes, 0xFFFF, vc_id_and_length));
222+
loadRing(ring, header_bytes, kTcHeaderSize);
223+
224+
U8 padding[kTcTrailerSize] = {};
225+
loadRing(ring, padding, kTcTrailerSize);
226+
227+
FwSizeType size_out = 0;
228+
FrameDetector::Status result = detector.detect(ring, size_out);
229+
230+
EXPECT_EQ(result, FrameDetector::Status::NO_FRAME_DETECTED);
231+
}
232+
233+
} // namespace FrameDetectors
234+
} // namespace Svc

0 commit comments

Comments
 (0)