#include #include #include "dd3_legacy_core.h" #include "payload_codec.h" #include "lora_frame_logic.h" #include "batch_reassembly_logic.h" // =========================================================================== // Fuzz / negative tests for parser entry points (frame, ACK, payload codec, // batch reassembly). Goal: every malformed input must be rejected without // crash, OOB read/write, or undefined behaviour. // =========================================================================== // ---- decode_batch: negative / boundary tests ---- static void test_decode_batch_null_args() { uint8_t dummy[32] = {}; BatchInput out = {}; TEST_ASSERT_FALSE(decode_batch(nullptr, 24, &out)); TEST_ASSERT_FALSE(decode_batch(dummy, 24, nullptr)); TEST_ASSERT_FALSE(decode_batch(nullptr, 0, nullptr)); } static void test_decode_batch_zero_length() { uint8_t dummy[1] = {0}; BatchInput out = {}; TEST_ASSERT_FALSE(decode_batch(dummy, 0, &out)); } static void test_decode_batch_minimal_valid_sync() { // Sync-only (n=0) payload: 24 bytes header, no samples. uint8_t buf[24] = {}; // magic 0xDDB3 LE buf[0] = 0xB3; buf[1] = 0xDD; buf[2] = 3; // schema buf[3] = 0x01; // flags // sender_id=1 buf[4] = 0x01; buf[5] = 0x00; // batch_id=1 buf[6] = 0x01; buf[7] = 0x00; // t_last=1769904000 LE uint32_t t = 1769904000UL; buf[8] = t & 0xFF; buf[9] = (t >> 8) & 0xFF; buf[10] = (t >> 16) & 0xFF; buf[11] = (t >> 24) & 0xFF; // present_mask=0 buf[12] = 0; buf[13] = 0; buf[14] = 0; buf[15] = 0; // n=0 buf[16] = 0; // battery_mV=3750 LE buf[17] = 0xA6; buf[18] = 0x0E; // err fields buf[19] = 0; buf[20] = 0; buf[21] = 0; buf[22] = 0; buf[23] = 0; BatchInput out = {}; TEST_ASSERT_TRUE(decode_batch(buf, 24, &out)); TEST_ASSERT_EQUAL_UINT8(0, out.n); TEST_ASSERT_EQUAL_UINT32(0, out.present_mask); } static void test_decode_batch_n_exceeds_30() { // Forge a header with n=31, which should be rejected. uint8_t buf[24] = {}; buf[0] = 0xB3; buf[1] = 0xDD; buf[2] = 3; buf[3] = 0x01; buf[4] = 0x01; buf[5] = 0x00; buf[6] = 0x01; buf[7] = 0x00; uint32_t t = 1769904000UL; buf[8] = t & 0xFF; buf[9] = (t >> 8) & 0xFF; buf[10] = (t >> 16) & 0xFF; buf[11] = (t >> 24) & 0xFF; buf[12] = 0xFF; buf[13] = 0xFF; buf[14] = 0xFF; buf[15] = 0x3F; // all 30 bits set buf[16] = 31; // n=31 → must reject buf[17] = 0xA6; buf[18] = 0x0E; buf[19] = 0; buf[20] = 0; buf[21] = 0; buf[22] = 0; buf[23] = 0; BatchInput out = {}; TEST_ASSERT_FALSE(decode_batch(buf, 24, &out)); } static void test_decode_batch_present_mask_n_mismatch() { // present_mask has 3 bits but n=5 → must reject. uint8_t buf[24] = {}; buf[0] = 0xB3; buf[1] = 0xDD; buf[2] = 3; buf[3] = 0x01; buf[4] = 0x01; buf[5] = 0x00; buf[6] = 0x01; buf[7] = 0x00; uint32_t t = 1769904000UL; buf[8] = t & 0xFF; buf[9] = (t >> 8) & 0xFF; buf[10] = (t >> 16) & 0xFF; buf[11] = (t >> 24) & 0xFF; buf[12] = 0x07; buf[13] = 0; buf[14] = 0; buf[15] = 0; // 3 bits buf[16] = 5; // n=5 but only 3 mask bits buf[17] = 0xA6; buf[18] = 0x0E; buf[19] = 0; buf[20] = 0; buf[21] = 0; buf[22] = 0; buf[23] = 0; BatchInput out = {}; TEST_ASSERT_FALSE(decode_batch(buf, 24, &out)); } static void test_decode_batch_reserved_mask_bits() { // Bit 30 or 31 set → must reject (only bits 0-29 valid). uint8_t buf[24] = {}; buf[0] = 0xB3; buf[1] = 0xDD; buf[2] = 3; buf[3] = 0x01; buf[4] = 0x01; buf[5] = 0x00; buf[6] = 0x01; buf[7] = 0x00; uint32_t t = 1769904000UL; buf[8] = t & 0xFF; buf[9] = (t >> 8) & 0xFF; buf[10] = (t >> 16) & 0xFF; buf[11] = (t >> 24) & 0xFF; buf[12] = 0x01; buf[13] = 0; buf[14] = 0; buf[15] = 0x40; // bit 30 buf[16] = 1; buf[17] = 0xA6; buf[18] = 0x0E; buf[19] = 0; buf[20] = 0; buf[21] = 0; buf[22] = 0; buf[23] = 0; BatchInput out = {}; TEST_ASSERT_FALSE(decode_batch(buf, 24, &out)); } // ---- uleb128_decode: negative tests ---- static void test_uleb128_decode_unterminated() { // 5 continuation bytes without termination → reject. uint8_t data[] = {0x80, 0x80, 0x80, 0x80, 0x80}; size_t pos = 0; uint32_t val = 0; TEST_ASSERT_FALSE(uleb128_decode(data, sizeof(data), &pos, &val)); } static void test_uleb128_decode_overflow() { // 5th byte has bits in upper nibble → overflow. uint8_t data[] = {0x80, 0x80, 0x80, 0x80, 0x10}; size_t pos = 0; uint32_t val = 0; TEST_ASSERT_FALSE(uleb128_decode(data, sizeof(data), &pos, &val)); } static void test_uleb128_decode_null_args() { size_t pos = 0; uint32_t val = 0; uint8_t data[] = {0x00}; TEST_ASSERT_FALSE(uleb128_decode(nullptr, 1, &pos, &val)); TEST_ASSERT_FALSE(uleb128_decode(data, 1, nullptr, &val)); TEST_ASSERT_FALSE(uleb128_decode(data, 1, &pos, nullptr)); } static void test_uleb128_decode_empty_buffer() { size_t pos = 0; uint32_t val = 0; uint8_t data[1] = {}; TEST_ASSERT_FALSE(uleb128_decode(data, 0, &pos, &val)); } // ---- svarint_decode: negative tests ---- static void test_svarint_decode_overflow() { // The underlying uleb128 overflows uint8_t data[] = {0x80, 0x80, 0x80, 0x80, 0x10}; size_t pos = 0; int32_t val = 0; TEST_ASSERT_FALSE(svarint_decode(data, sizeof(data), &pos, &val)); } // ---- lora_parse_frame: fuzz seeds ---- static void test_frame_parse_all_zeros() { uint8_t buf[5] = {0, 0, 0, 0, 0}; uint8_t kind = 0xFF; uint16_t dev = 0xFFFF; uint8_t payload[16] = {}; size_t plen = 0; // All-zero frame: CRC of first 3 bytes won't match last 2 → CrcFail. LoraFrameDecodeStatus s = lora_parse_frame(buf, sizeof(buf), 1, &kind, &dev, payload, sizeof(payload), &plen); TEST_ASSERT_TRUE(s == LoraFrameDecodeStatus::CrcFail || s == LoraFrameDecodeStatus::Ok); } static void test_frame_parse_max_msg_kind_reject() { // Build valid frame with msg_kind=2, then parse with max_msg_kind=1. uint8_t payload[] = {0x42}; uint8_t frame[32] = {}; size_t flen = 0; TEST_ASSERT_TRUE(lora_build_frame(2, 0xABCD, payload, 1, frame, sizeof(frame), flen)); uint8_t kind = 0; uint16_t dev = 0; uint8_t out[8] = {}; size_t olen = 0; LoraFrameDecodeStatus s = lora_parse_frame(frame, flen, 1, &kind, &dev, out, sizeof(out), &olen); TEST_ASSERT_EQUAL_UINT8(static_cast(LoraFrameDecodeStatus::InvalidMsgKind), static_cast(s)); } static void test_frame_parse_payload_too_large_for_output() { // Build valid frame with 4 bytes payload, parse into 2-byte output → LengthMismatch. uint8_t payload[] = {1, 2, 3, 4}; uint8_t frame[32] = {}; size_t flen = 0; TEST_ASSERT_TRUE(lora_build_frame(0, 0x1234, payload, 4, frame, sizeof(frame), flen)); uint8_t kind = 0; uint16_t dev = 0; uint8_t out[2] = {}; size_t olen = 0; LoraFrameDecodeStatus s = lora_parse_frame(frame, flen, 1, &kind, &dev, out, sizeof(out), &olen); TEST_ASSERT_EQUAL_UINT8(static_cast(LoraFrameDecodeStatus::LengthMismatch), static_cast(s)); } static void test_frame_build_null_args() { uint8_t buf[32] = {}; size_t len = 0; TEST_ASSERT_FALSE(lora_build_frame(0, 0, nullptr, 5, buf, sizeof(buf), len)); TEST_ASSERT_FALSE(lora_build_frame(0, 0, buf, 0, nullptr, sizeof(buf), len)); } // ---- batch_reassembly: negative / abuse tests ---- static void test_reassembly_null_buffer() { BatchReassemblyState state = {}; batch_reassembly_reset(state); uint8_t chunk[] = {1, 2, 3}; uint16_t clen = 0; TEST_ASSERT_EQUAL_UINT8( static_cast(BatchReassemblyStatus::ErrorReset), static_cast(batch_reassembly_push(state, 1, 0, 1, 3, chunk, 3, 100, 5000, 64, nullptr, 0, clen))); } static void test_reassembly_null_chunk_data() { BatchReassemblyState state = {}; batch_reassembly_reset(state); uint8_t buffer[32] = {}; uint16_t clen = 0; TEST_ASSERT_EQUAL_UINT8( static_cast(BatchReassemblyStatus::ErrorReset), static_cast(batch_reassembly_push(state, 1, 0, 1, 3, nullptr, 3, 100, 5000, 64, buffer, sizeof(buffer), clen))); } static void test_reassembly_total_len_zero_with_data() { BatchReassemblyState state = {}; batch_reassembly_reset(state); uint8_t buffer[32] = {}; uint8_t chunk[] = {1}; uint16_t clen = 0; // total_len=0 but chunk_len>0 → must reject. TEST_ASSERT_EQUAL_UINT8( static_cast(BatchReassemblyStatus::ErrorReset), static_cast(batch_reassembly_push(state, 1, 0, 1, 0, chunk, 1, 100, 5000, 64, buffer, sizeof(buffer), clen))); } static void test_reassembly_total_len_exceeds_max() { BatchReassemblyState state = {}; batch_reassembly_reset(state); uint8_t buffer[32] = {}; uint8_t chunk[] = {1}; uint16_t clen = 0; // total_len=5000 > max_total_len=64 → must reject. TEST_ASSERT_EQUAL_UINT8( static_cast(BatchReassemblyStatus::ErrorReset), static_cast(batch_reassembly_push(state, 1, 0, 1, 5000, chunk, 1, 100, 5000, 64, buffer, sizeof(buffer), clen))); } static void test_reassembly_timeout_resets() { BatchReassemblyState state = {}; batch_reassembly_reset(state); uint8_t buffer[32] = {}; uint8_t chunk1[] = {1, 2}; uint8_t chunk2[] = {3}; uint16_t clen = 0; // First chunk at t=1000. TEST_ASSERT_EQUAL_UINT8( static_cast(BatchReassemblyStatus::InProgress), static_cast(batch_reassembly_push(state, 10, 0, 2, 3, chunk1, 2, 1000, 500, 32, buffer, sizeof(buffer), clen))); // Second chunk at t=2000 (>500ms after last) → timeout → ErrorReset. TEST_ASSERT_EQUAL_UINT8( static_cast(BatchReassemblyStatus::ErrorReset), static_cast(batch_reassembly_push(state, 10, 1, 2, 3, chunk2, 1, 2000, 500, 32, buffer, sizeof(buffer), clen))); } static void test_reassembly_different_batch_id_resets() { BatchReassemblyState state = {}; batch_reassembly_reset(state); uint8_t buffer[32] = {}; uint8_t chunk[] = {1, 2}; uint16_t clen = 0; // Start batch 10. TEST_ASSERT_EQUAL_UINT8( static_cast(BatchReassemblyStatus::InProgress), static_cast(batch_reassembly_push(state, 10, 0, 2, 3, chunk, 2, 100, 5000, 32, buffer, sizeof(buffer), clen))); // Receive chunk for batch 11 (different), but index=1 → ErrorReset (non-zero index for new batch). TEST_ASSERT_EQUAL_UINT8( static_cast(BatchReassemblyStatus::ErrorReset), static_cast(batch_reassembly_push(state, 11, 1, 2, 3, chunk, 1, 200, 5000, 32, buffer, sizeof(buffer), clen))); } static void test_reassembly_overflow_buffer() { BatchReassemblyState state = {}; batch_reassembly_reset(state); uint8_t buffer[4] = {}; uint8_t chunk[] = {1, 2, 3, 4, 5}; uint16_t clen = 0; // total_len=5 but buffer_cap=4 → chunk overflows buffer → ErrorReset. TEST_ASSERT_EQUAL_UINT8( static_cast(BatchReassemblyStatus::ErrorReset), static_cast(batch_reassembly_push(state, 1, 0, 1, 5, chunk, 5, 100, 5000, 64, buffer, sizeof(buffer), clen))); } // ---- Byte-flip fuzz of a valid encoded payload ---- static void test_decode_batch_byte_flip_fuzz() { // Encode a valid batch, then flip each byte and ensure decode either // returns false or produces a valid output (no crash, no UB). BatchInput in = {}; in.sender_id = 1; in.batch_id = 42; in.t_last = 1769904000UL; in.present_mask = 0x07; // bits 0-2 in.n = 3; in.battery_mV = 3750; in.energy_wh[0] = 100000; in.energy_wh[1] = 100010; in.energy_wh[2] = 100020; in.p1_w[0] = 100; in.p1_w[1] = 110; in.p1_w[2] = 120; in.p2_w[0] = 200; in.p2_w[1] = 210; in.p2_w[2] = 220; in.p3_w[0] = 300; in.p3_w[1] = 310; in.p3_w[2] = 320; uint8_t encoded[256] = {}; size_t encoded_len = 0; TEST_ASSERT_TRUE(encode_batch(in, encoded, sizeof(encoded), &encoded_len)); TEST_ASSERT_TRUE(encoded_len > 0); for (size_t i = 0; i < encoded_len; ++i) { uint8_t mutated[256]; memcpy(mutated, encoded, encoded_len); mutated[i] ^= 0xFF; // flip all bits of byte i BatchInput out = {}; // Must not crash. Return value may be true (if flip is benign) or false. (void)decode_batch(mutated, encoded_len, &out); } // Verify original still decodes correctly. BatchInput verify = {}; TEST_ASSERT_TRUE(decode_batch(encoded, encoded_len, &verify)); TEST_ASSERT_EQUAL_UINT8(in.n, verify.n); } // ---- lora_parse_frame byte-flip ---- static void test_frame_byte_flip_fuzz() { uint8_t payload[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}; uint8_t frame[32] = {}; size_t frame_len = 0; TEST_ASSERT_TRUE(lora_build_frame(1, 0xF19C, payload, sizeof(payload), frame, sizeof(frame), frame_len)); for (size_t i = 0; i < frame_len; ++i) { uint8_t mutated[32]; memcpy(mutated, frame, frame_len); mutated[i] ^= 0xFF; uint8_t kind = 0; uint16_t dev = 0; uint8_t out[16] = {}; size_t olen = 0; // Must not crash. (void)lora_parse_frame(mutated, frame_len, 1, &kind, &dev, out, sizeof(out), &olen); } } void setup() { dd3_legacy_core_force_link(); UNITY_BEGIN(); // decode_batch negative tests RUN_TEST(test_decode_batch_null_args); RUN_TEST(test_decode_batch_zero_length); RUN_TEST(test_decode_batch_minimal_valid_sync); RUN_TEST(test_decode_batch_n_exceeds_30); RUN_TEST(test_decode_batch_present_mask_n_mismatch); RUN_TEST(test_decode_batch_reserved_mask_bits); // uleb128 / svarint negative tests RUN_TEST(test_uleb128_decode_unterminated); RUN_TEST(test_uleb128_decode_overflow); RUN_TEST(test_uleb128_decode_null_args); RUN_TEST(test_uleb128_decode_empty_buffer); RUN_TEST(test_svarint_decode_overflow); // lora_parse_frame negative tests RUN_TEST(test_frame_parse_all_zeros); RUN_TEST(test_frame_parse_max_msg_kind_reject); RUN_TEST(test_frame_parse_payload_too_large_for_output); RUN_TEST(test_frame_build_null_args); // batch_reassembly negative tests RUN_TEST(test_reassembly_null_buffer); RUN_TEST(test_reassembly_null_chunk_data); RUN_TEST(test_reassembly_total_len_zero_with_data); RUN_TEST(test_reassembly_total_len_exceeds_max); RUN_TEST(test_reassembly_timeout_resets); RUN_TEST(test_reassembly_different_batch_id_resets); RUN_TEST(test_reassembly_overflow_buffer); // Byte-flip fuzz tests RUN_TEST(test_decode_batch_byte_flip_fuzz); RUN_TEST(test_frame_byte_flip_fuzz); UNITY_END(); } void loop() {}