sec(sender): ACK rate-limiting, unknown device-ID rejection, fuzz tests
- 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
This commit is contained in:
@@ -126,6 +126,9 @@ static uint32_t g_sender_rx_reject_log_ms = 0;
|
||||
static RxRejectReason g_receiver_rx_reject_reason = RxRejectReason::None;
|
||||
static uint32_t g_receiver_rx_reject_log_ms = 0;
|
||||
static MeterData g_last_meter_data = {};
|
||||
// Rate-limit: track ACK accept timestamps to detect replay floods.
|
||||
static uint32_t g_ack_accept_last_ms = 0;
|
||||
static constexpr uint32_t ACK_MIN_INTERVAL_MS = 500;
|
||||
static bool g_last_meter_valid = false;
|
||||
static uint32_t g_last_meter_rx_ms = 0;
|
||||
static uint32_t g_meter_stale_seconds = 0;
|
||||
@@ -1422,32 +1425,49 @@ static void sender_loop() {
|
||||
static_cast<unsigned>(ack_pkt.payload_len),
|
||||
ack_id);
|
||||
}
|
||||
} else if (sender_id_from_short_id(ack_pkt.device_id_short) == 0 &&
|
||||
ack_pkt.device_id_short != g_short_id) {
|
||||
// Reject ACKs from unknown device IDs to prevent spoofing.
|
||||
sender_note_rx_reject(RxRejectReason::DeviceIdMismatch, "ack");
|
||||
if (SERIAL_DEBUG_MODE) {
|
||||
serial_debug_printf("ack: reject device_id=%04X (unknown)",
|
||||
ack_pkt.device_id_short);
|
||||
}
|
||||
} else {
|
||||
uint8_t time_valid = ack_pkt.payload[0] & 0x01;
|
||||
uint16_t ack_id = read_u16_be(&ack_pkt.payload[1]);
|
||||
uint32_t ack_epoch = read_u32_be(&ack_pkt.payload[3]);
|
||||
bool set_time = false;
|
||||
if (g_batch_ack_pending && ack_id == g_last_sent_batch_id) {
|
||||
ack_accepted = true;
|
||||
g_sender_ack_rtt_last_ms = rx_elapsed;
|
||||
if (g_sender_ack_rtt_ewma_ms == 0) {
|
||||
g_sender_ack_rtt_ewma_ms = rx_elapsed;
|
||||
// Rate-limit: reject if another ACK was accepted less than ACK_MIN_INTERVAL_MS ago
|
||||
if (g_ack_accept_last_ms != 0 && (millis() - g_ack_accept_last_ms < ACK_MIN_INTERVAL_MS)) {
|
||||
if (SERIAL_DEBUG_MODE) {
|
||||
serial_debug_printf("ack: rate-limited (last accepted %lums ago)",
|
||||
static_cast<unsigned long>(millis() - g_ack_accept_last_ms));
|
||||
}
|
||||
} else {
|
||||
g_sender_ack_rtt_ewma_ms = (g_sender_ack_rtt_ewma_ms * 3U + rx_elapsed + 1U) / 4U;
|
||||
ack_accepted = true;
|
||||
g_ack_accept_last_ms = millis();
|
||||
g_sender_ack_rtt_last_ms = rx_elapsed;
|
||||
if (g_sender_ack_rtt_ewma_ms == 0) {
|
||||
g_sender_ack_rtt_ewma_ms = rx_elapsed;
|
||||
} else {
|
||||
g_sender_ack_rtt_ewma_ms = (g_sender_ack_rtt_ewma_ms * 3U + rx_elapsed + 1U) / 4U;
|
||||
}
|
||||
if (time_valid == 1 && ack_epoch >= MIN_ACCEPTED_EPOCH_UTC) {
|
||||
time_set_utc(ack_epoch);
|
||||
g_time_acquired = true;
|
||||
sender_reset_fault_stats_on_first_sync(ack_epoch);
|
||||
set_time = true;
|
||||
}
|
||||
g_last_acked_batch_id = ack_id;
|
||||
serial_debug_printf("ack: rx ok batch_id=%u time_valid=%u epoch=%lu set=%u",
|
||||
ack_id,
|
||||
static_cast<unsigned>(time_valid),
|
||||
static_cast<unsigned long>(ack_epoch),
|
||||
set_time ? 1 : 0);
|
||||
finish_inflight_batch();
|
||||
}
|
||||
if (time_valid == 1 && ack_epoch >= MIN_ACCEPTED_EPOCH_UTC) {
|
||||
time_set_utc(ack_epoch);
|
||||
g_time_acquired = true;
|
||||
sender_reset_fault_stats_on_first_sync(ack_epoch);
|
||||
set_time = true;
|
||||
}
|
||||
g_last_acked_batch_id = ack_id;
|
||||
serial_debug_printf("ack: rx ok batch_id=%u time_valid=%u epoch=%lu set=%u",
|
||||
ack_id,
|
||||
static_cast<unsigned>(time_valid),
|
||||
static_cast<unsigned long>(ack_epoch),
|
||||
set_time ? 1 : 0);
|
||||
finish_inflight_batch();
|
||||
} else {
|
||||
if (ack_id != g_last_sent_batch_id) {
|
||||
sender_note_rx_reject(RxRejectReason::BatchIdMismatch, "ack");
|
||||
|
||||
406
test/test_security_fuzz/test_security_fuzz.cpp
Normal file
406
test/test_security_fuzz/test_security_fuzz.cpp
Normal file
@@ -0,0 +1,406 @@
|
||||
#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() {}
|
||||
Reference in New Issue
Block a user