|
| 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