- Add 500 ms minimum interval between accepted ACKs to mitigate replay floods - Reject ACK packets from unrecognised device IDs (DeviceIdMismatch) - Add test/test_security_fuzz: negative/boundary tests for decode_batch, uleb128_decode, svarint_decode, lora_parse_frame entry points
407 lines
14 KiB
C++
407 lines
14 KiB
C++
#include <Arduino.h>
|
|
#include <unity.h>
|
|
|
|
#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<uint8_t>(LoraFrameDecodeStatus::InvalidMsgKind), static_cast<uint8_t>(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<uint8_t>(LoraFrameDecodeStatus::LengthMismatch), static_cast<uint8_t>(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<uint8_t>(BatchReassemblyStatus::ErrorReset),
|
|
static_cast<uint8_t>(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<uint8_t>(BatchReassemblyStatus::ErrorReset),
|
|
static_cast<uint8_t>(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<uint8_t>(BatchReassemblyStatus::ErrorReset),
|
|
static_cast<uint8_t>(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<uint8_t>(BatchReassemblyStatus::ErrorReset),
|
|
static_cast<uint8_t>(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<uint8_t>(BatchReassemblyStatus::InProgress),
|
|
static_cast<uint8_t>(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<uint8_t>(BatchReassemblyStatus::ErrorReset),
|
|
static_cast<uint8_t>(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<uint8_t>(BatchReassemblyStatus::InProgress),
|
|
static_cast<uint8_t>(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<uint8_t>(BatchReassemblyStatus::ErrorReset),
|
|
static_cast<uint8_t>(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<uint8_t>(BatchReassemblyStatus::ErrorReset),
|
|
static_cast<uint8_t>(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() {}
|