mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-26 16:27:57 +00:00
canSerialize() validated payload_len and the destination size but not whether the path encoding is one writePath() will actually emit. writePath() self-guards against overrunning the path array, but it does so by writing nothing and returning 0 — a correctness problem, not a safety one, because getRawLength() still counts the path. An over-long or reserved encoding therefore passed the size check and then serialized to a truncated frame that was published as the packet. Worst case is path_len 0xFF with no payload: 63 hops of 4 bytes counts as 254 bytes, inside the 255-byte buffer, while writeTo() emits just the 2-byte header. The `raw` field would carry 4 hex chars presented as the frame. Reserved 4-byte hash encodings passed too, producing frames Packet::readFrom() rejects. Now gated on Packet::isValidPathLen(), which rejects the reserved 4-byte hash size and any count * size above MAX_PATH_SIZE in one predicate. It is the same check readFrom() applies to every received packet, and TX packets are built via setPathHashSizeAndCount() with real hash sizes, so no decodable packet is turned away. Two tests added for the cases a destination-size check cannot reach. The existing truncation test passed for the wrong reason -- its payload_len of 4 pushed getRawLength() to 258 and tripped the size check, masking the hole -- so it is split into the >0xFF truncation case and the counted-length-fits case, with the 254/2-byte asymmetry asserted explicitly so it cannot be masked again. 274/274 native tests; both observer envs and an nRF52 repeater build clean.
133 lines
5.2 KiB
C++
133 lines
5.2 KiB
C++
// Boundary tests for the wire-format scratch sizing used by the MQTT raw/packet
|
|
// publish paths. Packet::writeTo() cannot report an overrun (uint8_t return) and
|
|
// trusts the packet's own length fields, so canSerialize() is what keeps it in
|
|
// bounds — these cases pin the exact accept/reject edges.
|
|
#include <gtest/gtest.h>
|
|
|
|
#include "helpers/MQTTWireScratch.h"
|
|
|
|
namespace {
|
|
|
|
// A packet that serializes to the largest legal wire form: transport codes present,
|
|
// a full path, and a full payload.
|
|
mesh::Packet maxPacket() {
|
|
mesh::Packet p;
|
|
p.header = ROUTE_TYPE_TRANSPORT_DIRECT | (PAYLOAD_TYPE_TXT_MSG << PH_TYPE_SHIFT);
|
|
p.transport_codes[0] = 0x1234;
|
|
p.transport_codes[1] = 0x5678;
|
|
// The hop count field is 6 bits, so MAX_PATH_SIZE one-byte hops is NOT encodable
|
|
// (64 & 63 == 0). 32 hops of 2 bytes is the widest path that reaches MAX_PATH_SIZE.
|
|
p.setPathHashSizeAndCount(2, 32);
|
|
EXPECT_EQ(MAX_PATH_SIZE, p.getPathByteLen());
|
|
p.payload_len = MAX_PACKET_PAYLOAD;
|
|
memset(p.payload, 0xAB, sizeof(p.payload));
|
|
return p;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST(MQTTWireScratch, MaxLegalPacketFitsTheScratchBuffer) {
|
|
mesh::Packet p = maxPacket();
|
|
// 1 header + 4 transport + 1 path_len + 64 path + 184 payload = 254.
|
|
EXPECT_EQ(254, p.getRawLength());
|
|
EXPECT_TRUE(MQTTWireScratch::canSerialize(p, MQTTWireScratch::kWireBytes));
|
|
|
|
uint8_t buf[MQTTWireScratch::kWireBytes];
|
|
const uint8_t written = p.writeTo(buf);
|
|
EXPECT_EQ(254, (int)written);
|
|
EXPECT_LE((size_t)written, sizeof(buf));
|
|
// The hex buffer must hold two chars per byte plus the NUL.
|
|
EXPECT_GE(MQTTWireScratch::kWireHexChars, (size_t)written * 2 + 1);
|
|
}
|
|
|
|
TEST(MQTTWireScratch, RejectsPayloadLenPastTheArrayEvenWhenEncodedLengthFits) {
|
|
mesh::Packet p;
|
|
p.header = ROUTE_TYPE_FLOOD;
|
|
p.setPathHashSizeAndCount(1, 0);
|
|
// getRawLength() == 2 + 0 + 185 == 187, comfortably inside MAX_TRANS_UNIT, but
|
|
// writeTo() would memcpy 185 bytes out of a 184-byte array.
|
|
p.payload_len = MAX_PACKET_PAYLOAD + 1;
|
|
EXPECT_LE(p.getRawLength(), (int)MQTTWireScratch::kWireBytes);
|
|
EXPECT_FALSE(MQTTWireScratch::canSerialize(p, MQTTWireScratch::kWireBytes));
|
|
|
|
p.payload_len = MAX_PACKET_PAYLOAD;
|
|
EXPECT_TRUE(MQTTWireScratch::canSerialize(p, MQTTWireScratch::kWireBytes));
|
|
}
|
|
|
|
TEST(MQTTWireScratch, RejectsPathLenThatWouldTruncateIntoOneWireByte) {
|
|
mesh::Packet p;
|
|
p.header = ROUTE_TYPE_FLOOD;
|
|
p.payload_len = 4;
|
|
p.path_len = 0x100; // writeTo() stores this in a single byte
|
|
EXPECT_FALSE(MQTTWireScratch::canSerialize(p, MQTTWireScratch::kWireBytes));
|
|
}
|
|
|
|
// The case a destination-size check cannot catch, and which an earlier version of
|
|
// these tests masked by using a payload big enough to push getRawLength() over the
|
|
// limit: 0xFF encodes 63 hops of 4 bytes, so with no payload the counted length is
|
|
// 254 — inside the buffer — while writePath() refuses the 252-byte path and writeTo()
|
|
// emits only the 2-byte header. Publishing that would put 4 hex chars in the `raw`
|
|
// field and call them the packet.
|
|
TEST(MQTTWireScratch, RejectsOverlongPathEvenWhenTheCountedLengthFits) {
|
|
mesh::Packet p;
|
|
p.header = ROUTE_TYPE_FLOOD;
|
|
p.path_len = 0xFF;
|
|
p.payload_len = 0;
|
|
|
|
ASSERT_EQ(254, p.getRawLength());
|
|
ASSERT_LE((size_t)p.getRawLength(), MQTTWireScratch::kWireBytes);
|
|
uint8_t buf[MQTTWireScratch::kWireBytes];
|
|
ASSERT_EQ(2, (int)p.writeTo(buf));
|
|
|
|
EXPECT_FALSE(MQTTWireScratch::canSerialize(p, MQTTWireScratch::kWireBytes));
|
|
}
|
|
|
|
// hash_size 4 is reserved: isValidPathLen() and therefore Packet::readFrom() reject
|
|
// it, so serializing one produces a frame no receiver can parse back — even though the
|
|
// hop bytes fit and writePath() copies them happily.
|
|
TEST(MQTTWireScratch, RejectsReservedFourByteHashEncoding) {
|
|
mesh::Packet p;
|
|
p.header = ROUTE_TYPE_FLOOD;
|
|
p.setPathHashSizeAndCount(4, 2);
|
|
p.payload_len = 4;
|
|
|
|
ASSERT_EQ(4, p.getPathHashSize());
|
|
ASSERT_EQ(8, p.getPathByteLen()); // fits the path array
|
|
ASSERT_LE(p.getRawLength(), (int)MQTTWireScratch::kWireBytes);
|
|
ASSERT_FALSE(mesh::Packet::isValidPathLen((uint8_t)p.path_len));
|
|
|
|
EXPECT_FALSE(MQTTWireScratch::canSerialize(p, MQTTWireScratch::kWireBytes));
|
|
}
|
|
|
|
TEST(MQTTWireScratch, DestinationEdgeIsInclusive) {
|
|
mesh::Packet p = maxPacket();
|
|
const size_t exact = (size_t)p.getRawLength();
|
|
EXPECT_TRUE(MQTTWireScratch::canSerialize(p, exact));
|
|
EXPECT_FALSE(MQTTWireScratch::canSerialize(p, exact - 1));
|
|
}
|
|
|
|
// A zero-payload packet serializes fine but does not survive readFrom(), which
|
|
// requires at least one payload byte. canSerialize() deliberately does not reject it:
|
|
// the raw/packet publish paths only ever write the bytes out. Any future change that
|
|
// reconstructs a Packet from queued wire bytes has to handle this asymmetry.
|
|
TEST(MQTTWireScratch, ZeroPayloadPacketSerializesButDoesNotRoundTrip) {
|
|
mesh::Packet p;
|
|
p.header = ROUTE_TYPE_FLOOD;
|
|
p.setPathHashSizeAndCount(1, 0);
|
|
p.payload_len = 0;
|
|
EXPECT_EQ(2, p.getRawLength());
|
|
EXPECT_TRUE(MQTTWireScratch::canSerialize(p, MQTTWireScratch::kWireBytes));
|
|
|
|
uint8_t buf[MQTTWireScratch::kWireBytes];
|
|
const uint8_t written = p.writeTo(buf);
|
|
EXPECT_EQ(2, (int)written);
|
|
|
|
mesh::Packet restored;
|
|
EXPECT_FALSE(restored.readFrom(buf, written));
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
return RUN_ALL_TESTS();
|
|
}
|