Files
DD3-LoRa-Bridge-MultiSender/test/test_security_fuzz/test_security_fuzz.cpp
T
acidburns def09160d0 refactor lora payload timing
Bump the batch payload codec to schema v4 with separate meter-time and UTC anchors, then use meter seconds for sparse batch slotting and receiver reconstruction.

Update the current 868 MHz bench configuration, allow ACKs from configured receiver short IDs, improve AP-to-STA recovery, quiet the test build, and document the changed protocol in the README.
2026-06-30 12:19:27 +02:00

421 lines
15 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, 28, &out));
TEST_ASSERT_FALSE(decode_batch(dummy, 28, 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: 28 bytes header, no samples.
uint8_t buf[28] = {};
// magic 0xDDB3 LE
buf[0] = 0xB3; buf[1] = 0xDD;
buf[2] = 4; // schema
buf[3] = 0x01; // flags
// sender_id=1
buf[4] = 0x01; buf[5] = 0x00;
// batch_id=1
buf[6] = 0x01; buf[7] = 0x00;
// meter_t_last=123456789 LE
uint32_t meter_t = 123456789UL;
buf[8] = meter_t & 0xFF; buf[9] = (meter_t >> 8) & 0xFF;
buf[10] = (meter_t >> 16) & 0xFF; buf[11] = (meter_t >> 24) & 0xFF;
// ts_utc_last=1769904000 LE
uint32_t utc_t = 1769904000UL;
buf[12] = utc_t & 0xFF; buf[13] = (utc_t >> 8) & 0xFF;
buf[14] = (utc_t >> 16) & 0xFF; buf[15] = (utc_t >> 24) & 0xFF;
// present_mask=0
buf[16] = 0; buf[17] = 0; buf[18] = 0; buf[19] = 0;
// n=0
buf[20] = 0;
// battery_mV=3750 LE
buf[21] = 0xA6; buf[22] = 0x0E;
// err fields
buf[23] = 0; buf[24] = 0; buf[25] = 0; buf[26] = 0; buf[27] = 0;
BatchInput out = {};
TEST_ASSERT_TRUE(decode_batch(buf, 28, &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[28] = {};
buf[0] = 0xB3; buf[1] = 0xDD;
buf[2] = 4; buf[3] = 0x01;
buf[4] = 0x01; buf[5] = 0x00;
buf[6] = 0x01; buf[7] = 0x00;
uint32_t meter_t = 123456789UL;
buf[8] = meter_t & 0xFF; buf[9] = (meter_t >> 8) & 0xFF;
buf[10] = (meter_t >> 16) & 0xFF; buf[11] = (meter_t >> 24) & 0xFF;
uint32_t utc_t = 1769904000UL;
buf[12] = utc_t & 0xFF; buf[13] = (utc_t >> 8) & 0xFF;
buf[14] = (utc_t >> 16) & 0xFF; buf[15] = (utc_t >> 24) & 0xFF;
buf[16] = 0xFF; buf[17] = 0xFF; buf[18] = 0xFF; buf[19] = 0x3F; // all 30 bits set
buf[20] = 31; // n=31 → must reject
buf[21] = 0xA6; buf[22] = 0x0E;
buf[23] = 0; buf[24] = 0; buf[25] = 0; buf[26] = 0; buf[27] = 0;
BatchInput out = {};
TEST_ASSERT_FALSE(decode_batch(buf, 28, &out));
}
static void test_decode_batch_present_mask_n_mismatch() {
// present_mask has 3 bits but n=5 → must reject.
uint8_t buf[28] = {};
buf[0] = 0xB3; buf[1] = 0xDD;
buf[2] = 4; buf[3] = 0x01;
buf[4] = 0x01; buf[5] = 0x00;
buf[6] = 0x01; buf[7] = 0x00;
uint32_t meter_t = 123456789UL;
buf[8] = meter_t & 0xFF; buf[9] = (meter_t >> 8) & 0xFF;
buf[10] = (meter_t >> 16) & 0xFF; buf[11] = (meter_t >> 24) & 0xFF;
uint32_t utc_t = 1769904000UL;
buf[12] = utc_t & 0xFF; buf[13] = (utc_t >> 8) & 0xFF;
buf[14] = (utc_t >> 16) & 0xFF; buf[15] = (utc_t >> 24) & 0xFF;
buf[16] = 0x07; buf[17] = 0; buf[18] = 0; buf[19] = 0; // 3 bits
buf[20] = 5; // n=5 but only 3 mask bits
buf[21] = 0xA6; buf[22] = 0x0E;
buf[23] = 0; buf[24] = 0; buf[25] = 0; buf[26] = 0; buf[27] = 0;
BatchInput out = {};
TEST_ASSERT_FALSE(decode_batch(buf, 28, &out));
}
static void test_decode_batch_reserved_mask_bits() {
// Bit 30 or 31 set → must reject (only bits 0-29 valid).
uint8_t buf[28] = {};
buf[0] = 0xB3; buf[1] = 0xDD;
buf[2] = 4; buf[3] = 0x01;
buf[4] = 0x01; buf[5] = 0x00;
buf[6] = 0x01; buf[7] = 0x00;
uint32_t meter_t = 123456789UL;
buf[8] = meter_t & 0xFF; buf[9] = (meter_t >> 8) & 0xFF;
buf[10] = (meter_t >> 16) & 0xFF; buf[11] = (meter_t >> 24) & 0xFF;
uint32_t utc_t = 1769904000UL;
buf[12] = utc_t & 0xFF; buf[13] = (utc_t >> 8) & 0xFF;
buf[14] = (utc_t >> 16) & 0xFF; buf[15] = (utc_t >> 24) & 0xFF;
buf[16] = 0x01; buf[17] = 0; buf[18] = 0; buf[19] = 0x40; // bit 30
buf[20] = 1;
buf[21] = 0xA6; buf[22] = 0x0E;
buf[23] = 0; buf[24] = 0; buf[25] = 0; buf[26] = 0; buf[27] = 0;
BatchInput out = {};
TEST_ASSERT_FALSE(decode_batch(buf, 28, &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.meter_t_last = 123456789UL;
in.ts_utc_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() {}