Reject malformed cached adverts before sharing contacts

This commit is contained in:
mikecarper committed 2026-10-16 04:13:21 -07:00
1 parent 92b9a1cc64
commit 07afacf379
11 files changed
+443 -60

No files matched your search

+32 -53
View File
@@ -801,7 +801,16 @@ bool BaseChatMesh::shareContactZeroHop(const ContactInfo& contact) {
auto packet = obtainNewPacket();
if (packet == NULL) return false; // no Packets available
packet->readFrom(temp_buf, plen); // restore Packet from 'blob'
// A cache read may be short or corrupt. Never share a partial packet or a
// different identity that happens to occupy the same cache-key prefix.
if (!packet->readFrom(temp_buf, plen)
|| packet->getPayloadType() != PAYLOAD_TYPE_ADVERT
|| packet->payload_len < PUB_KEY_SIZE + 4 + SIGNATURE_SIZE
|| packet->payload_len > PUB_KEY_SIZE + 4 + SIGNATURE_SIZE + MAX_ADVERT_DATA_SIZE
|| memcmp(packet->payload, contact.id.pub_key, PUB_KEY_SIZE) != 0) {
releasePacket(packet);
return false;
}
uint16_t codes[2];
codes[0] = codes[1] = 0; // { 0, 0 } means 'send this nowhere'
sendZeroHop(packet, codes);
@@ -830,6 +839,24 @@ bool BaseChatMesh::importContact(const uint8_t src_buf[], uint8_t len) {
return false; // error
}
// Keep the identical request/login routing tail shared on flash-limited nodes.
__attribute__((noinline))
int BaseChatMesh::sendContactPacket(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t& est_timeout) {
if (pkt) {
uint32_t t = getTransmitAirtime(pkt);
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
if (!sendFloodScoped(recipient, pkt)) return MSG_SEND_FAILED;
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
if (!sendDirect(pkt, recipient.getPath(), recipient.out_path_len)) return MSG_SEND_FAILED;
est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
return MSG_SEND_SENT_DIRECT;
}
}
return MSG_SEND_FAILED;
}
int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout) {
mesh::Packet* pkt;
{
@@ -850,19 +877,7 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password,
pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.getSharedSecret(self_id), temp, tlen);
}
if (pkt) {
uint32_t t = getTransmitAirtime(pkt);
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
if (!sendFloodScoped(recipient, pkt)) return MSG_SEND_FAILED;
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
if (!sendDirect(pkt, recipient.getPath(), recipient.out_path_len)) return MSG_SEND_FAILED;
est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
return MSG_SEND_SENT_DIRECT;
}
}
return MSG_SEND_FAILED;
return sendContactPacket(recipient, pkt, est_timeout);
}
bool BaseChatMesh::allocateRequestTag(uint32_t& tag) {
@@ -880,19 +895,7 @@ int BaseChatMesh::sendAnonReq(const ContactInfo& recipient, const uint8_t* data,
pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + len);
}
if (pkt) {
uint32_t t = getTransmitAirtime(pkt);
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
if (!sendFloodScoped(recipient, pkt)) return MSG_SEND_FAILED;
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
if (!sendDirect(pkt, recipient.getPath(), recipient.out_path_len)) return MSG_SEND_FAILED;
est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
return MSG_SEND_SENT_DIRECT;
}
}
return MSG_SEND_FAILED;
return sendContactPacket(recipient, pkt, est_timeout);
}
int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout) {
@@ -907,19 +910,7 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_
pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + data_len);
}
if (pkt) {
uint32_t t = getTransmitAirtime(pkt);
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
if (!sendFloodScoped(recipient, pkt)) return MSG_SEND_FAILED;
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
if (!sendDirect(pkt, recipient.getPath(), recipient.out_path_len)) return MSG_SEND_FAILED;
est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
return MSG_SEND_SENT_DIRECT;
}
}
return MSG_SEND_FAILED;
return sendContactPacket(recipient, pkt, est_timeout);
}
int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout) {
@@ -934,19 +925,7 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, u
pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, sizeof(temp));
}
if (pkt) {
uint32_t t = getTransmitAirtime(pkt);
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
if (!sendFloodScoped(recipient, pkt)) return MSG_SEND_FAILED;
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
if (!sendDirect(pkt, recipient.getPath(), recipient.out_path_len)) return MSG_SEND_FAILED;
est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
return MSG_SEND_SENT_DIRECT;
}
}
return MSG_SEND_FAILED;
return sendContactPacket(recipient, pkt, est_timeout);
}
bool BaseChatMesh::startConnection(const ContactInfo& contact, uint16_t keep_alive_secs) {
+1
View File
@@ -97,6 +97,7 @@ class BaseChatMesh : public mesh::Mesh {
ConnectionInfo connections[MAX_CONNECTIONS];
mesh::Packet* composeMsgPacket(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char *text, uint32_t& expected_ack);
int sendContactPacket(const ContactInfo& recipient, mesh::Packet* packet, uint32_t& est_timeout);
protected:
void sendAckTo(const ContactInfo& dest, const uint8_t* ack_hash, uint8_t ack_len=4);
+3 -1
View File
@@ -43,6 +43,7 @@ public:
unsigned fail_rename = 0, fail_remove = 0, fail_config = 0;
bool fail_metadata = false, fail_open = false, partial_open = false;
bool fail_read = false, short_write = false, fail_close = false, fail_format = false;
size_t read_limit = SIZE_MAX;
size_t max_backend_write = 0;
bool exists(const char* path) const { return files.count(path) != 0; }
bool mkdir(const char*) { ++operations; return true; }
@@ -113,7 +114,8 @@ inline size_t File::read(uint8_t* data, size_t length) {
assert(stream && !stream->writer); ++stream->fs->operations;
if (stream->fs->fail_read) return 0;
const auto& bytes = stream->fs->files.at(stream->path);
size_t n = std::min(length, bytes.size()-std::min(stream->position, bytes.size()));
size_t n = std::min(std::min(length, stream->fs->read_limit),
bytes.size()-std::min(stream->position, bytes.size()));
if (n) memcpy(data, bytes.data()+stream->position, n);
stream->position += n; return n;
}
+170
View File
@@ -0,0 +1,170 @@
// Execute production cache reads, packet restoration, pool reuse and sharing.
// Only storage peripherals and the final radio queue boundary are isolated.
#include "FakeFilesystem.h"
#include <Packet.h>
#include <helpers/StaticPoolPacketManager.h>
#include <cstdlib>
#include <string>
#define FILESYSTEM FakeFilesystem
using mesh::FLOOD_RETRY_POLICY_DEFAULT;
using mesh::RADIO_TX_AUTO;
class DataStore {
public:
FakeFilesystem* _fs;
#include "advert_state.h"
explicit DataStore(FakeFilesystem& fs) : _fs(&fs) {}
File openRead(FakeFilesystem* fs, const char* path) { return fs->open(path); }
uint8_t getBlobByKey(const uint8_t[], int, uint8_t[]);
bool putBlobByKey(const uint8_t[], int, const uint8_t[], uint8_t);
bool queueAdvertByKey(const uint8_t[], int, const uint8_t[], uint8_t);
};
#include "share_store.h"
struct ContactInfo {
struct { uint8_t pub_key[PUB_KEY_SIZE] = {}; } id;
};
class BaseChatMesh {
public:
DataStore& store;
StaticPoolPacketManager* _mgr;
unsigned _err_flags = 0;
bool receive_context_active = false;
uint8_t receive_context_profile = 0;
uint32_t receive_context_generation = 0;
uint8_t temp_buf[MAX_TRANS_UNIT] = {};
unsigned released = 0, sent = 0;
mesh::Packet last;
struct Tables {
unsigned marked = 0;
void markSent(const mesh::Packet*) { ++marked; }
} table, *_tables = &table;
BaseChatMesh(DataStore& s, StaticPoolPacketManager& p) : store(s), _mgr(&p) {}
int getBlobByKey(const uint8_t* key, int length, uint8_t* bytes) {
return store.getBlobByKey(key, length, bytes);
}
mesh::Packet* obtainNewPacket();
void releasePacket(mesh::Packet* packet) { ++released; _mgr->free(packet); }
bool shareContactZeroHop(const ContactInfo&);
void sendZeroHop(mesh::Packet*, uint16_t*, uint32_t = 0);
bool sendPacket(mesh::Packet* packet, uint8_t, uint32_t) {
// The actual Dispatcher shape guards still apply at this isolated final
// boundary; no radio, crypto verification or RF delivery is simulated.
const bool accepted = !packet->violatesRoutePolicy()
&& mesh::Packet::isValidPathLen(packet->path_len)
&& packet->payload_len <= MAX_PACKET_PAYLOAD;
if (accepted) { ++sent; last = *packet; }
releasePacket(packet);
return accepted;
}
};
#include "share_methods.h"
static void require(bool condition, const char* message) {
if (condition) return;
fprintf(stderr, "CHECK: %s\n", message);
abort();
}
int main(int argc, char** argv) {
require(argc == 2, "one share scenario is required");
const std::string scenario = argv[1];
FakeFilesystem fs;
DataStore store(fs);
StaticPoolPacketManager pool(1);
BaseChatMesh chat(store, pool);
ContactInfo contact;
contact.id.pub_key[0] = 0x42;
mesh::Packet advert;
advert.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT);
advert.setPathHashSizeAndCount(1, 2);
advert.path[0] = 0x12;
advert.path[1] = 0x34;
if (scenario == "short3") advert.path_len = 0;
const size_t fixed = PUB_KEY_SIZE + 4 + SIGNATURE_SIZE;
const char name[] = "Probe node";
advert.payload_len = fixed + sizeof(name);
memset(advert.payload, 0xA5, advert.payload_len);
memcpy(advert.payload, contact.id.pub_key, PUB_KEY_SIZE);
advert.payload[fixed] = 0x81; // ADV_TYPE_CHAT | ADV_NAME_MASK.
memcpy(advert.payload + fixed + 1, name, sizeof(name) - 1);
std::vector<uint8_t> expected_payload(advert.payload, advert.payload + advert.payload_len);
uint8_t bytes[MAX_TRANS_UNIT];
const auto length = advert.writeTo(bytes);
require(store.putBlobByKey(contact.id.pub_key, PUB_KEY_SIZE, bytes, length),
"valid cache preparation failed");
char path[64];
makeBlobPath(contact.id.pub_key, PUB_KEY_SIZE, path, sizeof(path));
bool expected = scenario == "valid" || scenario == "valid-reused"
|| scenario == "valid-min" || scenario == "valid-max" || scenario == "ram-valid";
bool allocation_failed = false;
mesh::Packet* held = nullptr;
if (scenario == "short1") fs.read_limit = 1;
else if (scenario == "short3") fs.read_limit = 3;
else if (scenario == "truncated-path") fs.files.at(path).resize(3);
else if (scenario == "reserved-path") fs.files.at(path)[1] = 0xC0;
else if (scenario == "bad-version") fs.files.at(path)[0] |= 0x40;
else if (scenario == "truncated-transport") {
fs.files.at(path)[0] = ROUTE_TYPE_TRANSPORT_FLOOD | (PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT);
fs.files.at(path).resize(3);
} else if (scenario == "wrong-type") {
fs.files.at(path)[0] = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_GRP_TXT << PH_TYPE_SHIFT);
} else if (scenario == "short-advert") fs.files.at(path).resize(4 + fixed - 1);
else if (scenario == "oversize-advert") fs.files.at(path).resize(4 + fixed + MAX_ADVERT_DATA_SIZE + 1);
else if (scenario == "wrong-key") fs.files.at(path)[4 + PUB_KEY_SIZE - 1] ^= 0x80;
else if (scenario == "empty-cache") fs.files.at(path).clear();
else if (scenario == "missing-cache") fs.files.erase(path);
else if (scenario == "no-allocation") { held = pool.allocNew(); allocation_failed = true; }
else if (scenario == "valid-min") {
fs.files.at(path).resize(4 + fixed);
expected_payload.resize(fixed);
} else if (scenario == "valid-max") {
fs.files.at(path).resize(4 + fixed + MAX_ADVERT_DATA_SIZE);
expected_payload.resize(fixed + MAX_ADVERT_DATA_SIZE);
}
else if (scenario == "ram-valid" || scenario == "ram-wrong-key") {
ContactInfo cached = contact;
if (scenario == "ram-wrong-key") bytes[4 + PUB_KEY_SIZE - 1] ^= 0x80;
require(store.queueAdvertByKey(cached.id.pub_key, PUB_KEY_SIZE, bytes, length),
"RAM advert preparation failed");
fs.files.erase(path);
} else require(scenario == "valid" || scenario == "valid-reused", "unknown share scenario");
if (scenario == "valid-reused" || scenario == "short1") {
// Actual pool reuse retains the prior header. obtainNewPacket resets
// lengths, so ignoring readFrom's early failure can send an empty ACK.
auto* previous = pool.allocNew();
previous->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT);
pool.free(previous);
}
const bool result = chat.shareContactZeroHop(contact);
if (expected) {
require(result && chat.sent == 1, "valid cached advert was not shared");
require(chat.last.getPayloadType() == PAYLOAD_TYPE_ADVERT,
"shared packet type changed");
require(chat.last.getRouteType() == ROUTE_TYPE_TRANSPORT_DIRECT && chat.last.path_len == 0,
"valid share must remain zero-hop transport-direct");
require(chat.last.transport_codes[0] == 0 && chat.last.transport_codes[1] == 0,
"valid share must retain zero transport codes");
require(!memcmp(chat.last.payload, contact.id.pub_key, PUB_KEY_SIZE),
"valid share identity changed");
require(chat.last.payload_len == expected_payload.size()
&& !memcmp(chat.last.payload, expected_payload.data(), expected_payload.size()),
"valid share must preserve every cached payload byte");
require(chat.table.marked == 1 && chat.released == 1,
"valid share must mark sent once and retire once");
} else {
require(!result && chat.sent == 0, "invalid cached advert must fail without transmission");
require(chat.table.marked == 0, "rejected advert must not be marked sent");
const unsigned releases = scenario == "empty-cache" || scenario == "missing-cache"
|| allocation_failed ? 0 : 1;
require(chat.released == releases, "failed share must release exactly its owned packet");
}
if (held) pool.free(held);
require(pool.getFreeCount() == 1, "share leaked or double-freed its packet");
printf("PASS: cached advert share %s\n", scenario.c_str());
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
// Dispatcher includes identity declarations, but the packet-pool/share
// methods exercised here do not invoke identity or crypto operations.
namespace mesh { class Identity {}; class LocalIdentity {}; }
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
namespace mesh {
class Utils {
public:
static void toHex(char* dest, const uint8_t* bytes, size_t length) {
static const char hex[] = "0123456789ABCDEF";
for (size_t i = 0; i < length; ++i) {
dest[2 * i] = hex[bytes[i] >> 4];
dest[2 * i + 1] = hex[bytes[i] & 15];
}
dest[2 * length] = 0;
}
};
}
@@ -108,6 +108,7 @@ struct BaseChatMesh {
uint32_t calcFloodTimeoutMillisFor(uint32_t) const { return 5000; }
uint32_t calcDirectTimeoutMillisFor(uint32_t, uint8_t) const { return direct_timeout; }
void startConnection(const ContactInfo&, uint16_t) { ++connection_starts; }
int sendContactPacket(const ContactInfo&, mesh::Packet*, uint32_t&);
int sendLogin(const ContactInfo&, const char*, uint32_t&);
int sendAnonReq(const ContactInfo&, const uint8_t*, uint8_t, uint32_t&, uint32_t&);
int sendRequest(const ContactInfo&, const uint8_t*, uint8_t, uint32_t&, uint32_t&);
+130 -5
View File
@@ -153,9 +153,15 @@ protected:
const char* getLogDateTime() { return "test"; }
public:
Packet packet;
unsigned released = 0, queued = 0;
unsigned released = 0, queued = 0, queue_attempts = 0;
bool queue_accepts = true;
void releasePacket(Packet*) { ++released; }
bool sendPacket(Packet*, uint8_t, uint32_t) { ++queued; return true; }
bool sendPacket(Packet* packet, uint8_t, uint32_t) {
++queue_attempts;
if (!queue_accepts) { releasePacket(packet); return false; }
++queued;
return true;
}
bool sendDirect(Packet*, const uint8_t*, uint8_t, uint32_t = 0);
};
}
@@ -166,6 +172,7 @@ public:
#define ADV_TYPE_ROOM 3
class BaseChatMesh : public mesh::Mesh {
uint32_t getTransmitAirtime(const mesh::Packet* packet) const {
++airtime_queries;
return _radio->getEstAirtimeFor(packet->getRawLength());
}
struct Radio { uint32_t getEstAirtimeFor(int) { return 20; } } radio;
@@ -176,23 +183,38 @@ class BaseChatMesh : public mesh::Mesh {
Clock* getRTCClock() { return &clock; }
Rng* getRNG() { return &rng; }
mesh::Packet* createDatagram(uint8_t type, const mesh::Identity&,
const uint8_t*, const uint8_t*, size_t length) {
const uint8_t* secret, const uint8_t* data, size_t length) {
++allocations;
payload.assign(data, data + length);
memcpy(shared_secret.data(), secret, shared_secret.size());
if (!allocation_succeeds) return nullptr;
packet = mesh::Packet();
packet.header = type << PH_TYPE_SHIFT;
packet.payload_len = length;
assert(length <= sizeof(packet.payload));
memcpy(packet.payload, data, length);
return &packet;
}
mesh::Packet* createAnonDatagram(uint8_t type, const mesh::LocalIdentity&,
const mesh::Identity& peer, const uint8_t* secret, const uint8_t* data, size_t length) {
return createDatagram(type, peer, secret, data, length);
}
bool sendFloodScoped(const ContactInfo&, mesh::Packet* p) { return sendPacket(p, 0, 0); }
bool sendFloodScoped(const ContactInfo&, mesh::Packet* p) {
++flood_calls;
return sendPacket(p, 0, 0);
}
uint32_t calcFloodTimeoutMillisFor(uint32_t t) { return t * 2; }
uint32_t calcDirectTimeoutMillisFor(uint32_t t, uint8_t n) { return t * (n + 1); }
int sendContactPacket(const ContactInfo&, mesh::Packet*, uint32_t&);
public:
unsigned allocations = 0, tag_checks = 0, flood_calls = 0;
mutable unsigned airtime_queries = 0;
bool allocation_succeeds = true, tag_allowed = true;
std::vector<uint8_t> payload;
std::array<uint8_t, 32> shared_secret = {};
int sendLogin(const ContactInfo&, const char*, uint32_t&);
int sendAnonReq(const ContactInfo&, const uint8_t*, uint8_t, uint32_t&, uint32_t&);
virtual bool allowRequestTag(uint32_t) { return true; }
virtual bool allowRequestTag(uint32_t) { ++tag_checks; return tag_allowed; }
virtual bool allocateRequestTag(uint32_t&);
int sendRequest(const ContactInfo&, const uint8_t*, uint8_t, uint32_t&, uint32_t&);
int sendRequest(const ContactInfo&, uint8_t, uint32_t&, uint32_t&);
@@ -392,6 +414,108 @@ static void failed_path_reads_fail_sends_without_leaking_packets() {
}
}
static void shared_request_tail_keeps_payload_routes_and_timeouts() {
Fixture f(1);
mesh::contactSecretCache().attach(&f.store);
BaseChatMesh mesh;
ContactInfo recipient = f.host.contacts[0];
recipient.sync_since = 0x12345678;
std::vector<uint8_t> request(MAX_PACKET_PAYLOAD, 0x6D);
std::array<uint8_t, 32> first_secret = {};
bool have_secret = false;
unsigned traces = 0;
for (unsigned kind = 0; kind != 4; ++kind) {
for (uint8_t path_len : {uint8_t(OUT_PATH_UNKNOWN), uint8_t(0), uint8_t(0x42)}) {
recipient.out_path_len = path_len;
for (unsigned outcome = 0; outcome != 4; ++outcome) {
if (kind == 0 && outcome == 3) continue; // Login does not allocate request tags.
const unsigned variants = kind == 0 ? 6 : kind == 3 ? 1 : 2;
for (unsigned variant = 0; variant != variants; ++variant) {
mesh.allocation_succeeds = outcome != 2;
mesh.queue_accepts = outcome != 1;
mesh.tag_allowed = outcome != 3;
const unsigned allocations = mesh.allocations, attempted = mesh.queue_attempts;
const unsigned queued = mesh.queued, released = mesh.released;
const unsigned airtime_queries = mesh.airtime_queries, floods = mesh.flood_calls;
const unsigned tag_checks = mesh.tag_checks;
uint32_t timeout = 0xDEADBEEF, tag = 0xCAFEBABE;
int result;
std::vector<uint8_t> expected(4);
const uint32_t timestamp = 100;
memcpy(expected.data(), &timestamp, sizeof(timestamp));
if (kind == 0) {
recipient.type = variant < 3 ? ADV_TYPE_ROOM : 1;
const size_t lengths[] = {0, 15, 16};
const std::string password(lengths[variant % 3], 'p');
if (recipient.type == ADV_TYPE_ROOM) {
const auto* since = reinterpret_cast<const uint8_t*>(&recipient.sync_since);
expected.insert(expected.end(), since, since + 4);
}
expected.insert(expected.end(), std::min(password.size(), size_t(15)), 'p');
result = mesh.sendLogin(recipient, password.c_str(), timeout);
} else if (kind == 1 || kind == 2) {
// Leave the same timestamp/encryption envelope required by the
// production datagram builders, not just the caller's scratch.
const unsigned overhead = kind == 1 ? 1 + PUB_KEY_SIZE : CIPHER_MAC_SIZE;
const uint8_t length = variant == 0 ? 0
: MAX_PACKET_PAYLOAD - 4 - overhead - (CIPHER_BLOCK_SIZE - 1);
expected.insert(expected.end(), request.begin(), request.begin() + length);
result = kind == 1
? mesh.sendAnonReq(recipient, request.data(), length, tag, timeout)
: mesh.sendRequest(recipient, request.data(), length, tag, timeout);
} else {
expected.push_back(0x71);
expected.insert(expected.end(), 4, 0);
expected.insert(expected.end(), 4, 42);
result = mesh.sendRequest(recipient, 0x71, tag, timeout);
}
++traces;
assert(mesh.tag_checks == tag_checks + unsigned(kind != 0));
if (outcome == 3) {
assert(result == MSG_SEND_FAILED && tag == timestamp);
assert(mesh.allocations == allocations && mesh.queue_attempts == attempted);
assert(mesh.airtime_queries == airtime_queries && mesh.released == released);
assert(timeout == 0xDEADBEEF);
continue;
}
assert(mesh.allocations == allocations + 1 && mesh.payload == expected);
if (mesh.allocation_succeeds)
assert(mesh.packet.getPayloadType() == (kind < 2 ? PAYLOAD_TYPE_ANON_REQ : PAYLOAD_TYPE_REQ));
if (kind != 0) assert(tag == timestamp);
if (!have_secret) { first_secret = mesh.shared_secret; have_secret = true; }
assert(mesh.shared_secret == first_secret);
if (outcome == 2) {
assert(result == MSG_SEND_FAILED && timeout == 0xDEADBEEF);
assert(mesh.queue_attempts == attempted && mesh.released == released);
assert(mesh.airtime_queries == airtime_queries && mesh.flood_calls == floods);
continue;
}
assert(mesh.airtime_queries == airtime_queries + 1);
assert(mesh.queue_attempts == attempted + 1);
assert(mesh.flood_calls == floods + unsigned(path_len == OUT_PATH_UNKNOWN));
if (outcome == 1) {
assert(result == MSG_SEND_FAILED && timeout == 0xDEADBEEF);
assert(mesh.queued == queued && mesh.released == released + 1);
} else {
const bool flood = path_len == OUT_PATH_UNKNOWN;
assert(result == (flood ? MSG_SEND_SENT_FLOOD : MSG_SEND_SENT_DIRECT));
assert(timeout == (flood ? 40u : 20u * (path_len + 1u)));
assert(mesh.queued == queued + 1 && mesh.released == released);
if (!flood) {
assert(mesh.packet.getRouteType() == ROUTE_TYPE_DIRECT);
const unsigned path_bytes = (path_len & 63) * ((path_len >> 6) + 1);
assert(mesh.packet.path_len == path_len);
assert(!memcmp(mesh.packet.path, route(0).data(), path_bytes));
}
}
}
}
}
}
assert(traces == 114);
printf("PASS: %u shared request/login payload and routing traces\n", traces);
}
static void contact_reload_preserves_disk_on_cache_exhaustion() {
Fixture f;
#if defined(NRF52_PLATFORM)
@@ -841,6 +965,7 @@ static void deferred_cleanup_publishes_actual_cached_handles() {
int main() {
contact_tx_policy_round_trips_without_growing_records();
shared_request_tail_keeps_payload_routes_and_timeouts();
#if defined(ESP32_PLATFORM)
deferred_cleanup_publishes_actual_cached_handles();
cooperative_save_services_commands_and_preserves_snapshots();
@@ -47,7 +47,8 @@ def production_methods(source, base, packet, dispatcher):
))
methods += "\n#if COMPANION_FEATURE_TEXT_TERMINAL\n" + terminal + "\n#endif\n"
methods += "\n" + "\n".join(extract_braced(base, signature) for signature in (
"bool BaseChatMesh::onContactPathRecv(", "int BaseChatMesh::sendLogin(",
"bool BaseChatMesh::onContactPathRecv(", "int BaseChatMesh::sendContactPacket(",
"int BaseChatMesh::sendLogin(",
"bool BaseChatMesh::allocateRequestTag(",
"int BaseChatMesh::sendAnonReq(",
"int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t*",
+2
View File
@@ -107,6 +107,7 @@ class ContactCacheTest(unittest.TestCase):
(temp / "send_under_test.h").write_text(
"namespace mesh {\n" + method(mesh, "bool Mesh::sendDirect(") + "\n}\n"
+ "\n".join(method(chat, signature) for signature in (
"int BaseChatMesh::sendContactPacket(",
"int BaseChatMesh::sendLogin(", "int BaseChatMesh::sendAnonReq(",
"bool BaseChatMesh::allocateRequestTag(",
"int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t*",
@@ -133,6 +134,7 @@ class ContactCacheTest(unittest.TestCase):
self.assertIn("crc_read_passes == 104", result.stderr)
else:
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn('PASS: 114 shared request/login payload and routing traces', result.stdout)
if __name__ == "__main__":
+79
View File
@@ -1,10 +1,13 @@
#!/usr/bin/env python3
"""Exercise production ESP advert jobs and their MyMesh route under sanitizers."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
from test_t096_full_memory import method
from test_serial_wifi_sessions import SANITIZER_FLAGS
ROOT = Path(__file__).resolve().parents[1]
@@ -19,6 +22,82 @@ class AdvertCacheTest(unittest.TestCase):
def test_synchronous_received_advert_is_detected(self):
self.run_fixture(0, synchronous=True)
def test_contact_sharing_validates_restored_cached_adverts(self):
self.run_share_fixture()
def test_unchecked_cached_advert_share_negative_control(self):
self.run_share_fixture(legacy=True)
def run_share_fixture(self, legacy=False):
compiler = os.environ.get('CXX') or shutil.which('g++') or shutil.which('clang++')
if compiler is None:
self.skipTest('a host C++17 compiler is required')
store = (ROOT / 'examples/companion_radio/DataStore.cpp').read_text()
header = (ROOT / 'examples/companion_radio/DataStore.h').read_text()
start = header.index(' struct AdvertWriteState {')
state = header[start:header.index('\n#endif', start)]
blob = store[store.rindex('inline void makeBlobPath('):]
presence = method(store, 'static bool companionPathPresence(').replace(
'::stat(vfs_path, &info)', 'fixtureStat(fs, vfs_path, &info)')
implementation = '\n'.join((presence, method(store, 'static File openWrite(')))
implementation += '\n' + '\n'.join(method(blob, signature) for signature in (
'inline void makeBlobPath(', 'uint8_t DataStore::getBlobByKey(',
'bool DataStore::putBlobByKey('))
implementation += '\n' + '\n'.join(method(store, signature) for signature in (
'void DataStore::invalidateAdvertWrite(', 'void DataStore::retireAdvertWrite(',
'bool DataStore::queueAdvertByKey('))
share = method((ROOT / 'src/helpers/BaseChatMesh.cpp').read_text(),
'bool BaseChatMesh::shareContactZeroHop(')
if legacy:
start = share.index(' // A cache read may be short or corrupt.')
stop = share.index(' uint16_t codes[2];', start)
share = share[:start] + " packet->readFrom(temp_buf, plen);\n" + share[stop:]
obtain = method((ROOT / 'src/Dispatcher.cpp').read_text(),
'Packet* Dispatcher::obtainNewPacket()').replace(
'Packet* Dispatcher::', 'mesh::Packet* BaseChatMesh::', 1)
zero_hop = method((ROOT / 'src/Mesh.cpp').read_text(),
'void Mesh::sendZeroHop(Packet* packet, uint16_t*').replace(
'void Mesh::sendZeroHop(Packet*',
'void BaseChatMesh::sendZeroHop(mesh::Packet*', 1)
scenarios = ('valid', 'valid-reused', 'valid-min', 'valid-max', 'ram-valid',
'short1', 'short3', 'truncated-path', 'reserved-path', 'bad-version',
'truncated-transport', 'wrong-type', 'short-advert', 'oversize-advert',
'wrong-key', 'ram-wrong-key', 'empty-cache', 'missing-cache', 'no-allocation')
controls = {'valid', 'valid-reused', 'valid-min', 'valid-max', 'ram-valid',
'empty-cache', 'missing-cache', 'no-allocation'}
fixture = ROOT / 'test/fixtures/advert_cache'
with tempfile.TemporaryDirectory(prefix='mesh-advert-share-') as directory:
temp = Path(directory)
(temp / 'advert_state.h').write_text(state, encoding='ascii')
(temp / 'share_store.h').write_text(implementation, encoding='ascii')
(temp / 'share_methods.h').write_text('\n'.join((obtain, zero_hop, share)),
encoding='ascii')
binary = temp / 'share.exe'
built = subprocess.run([
compiler, '-std=c++17', '-O1', '-g', '-Wall', '-Wextra',
'-Wno-unused-parameter', '-Wno-reorder', *SANITIZER_FLAGS,
'-DESP32_PLATFORM=1', '-I' + str(fixture / 'share_mocks'),
'-I' + str(ROOT / 'test/mocks'), '-I' + str(fixture),
'-I' + str(temp), '-I' + str(ROOT / 'src'),
str(fixture / 'share_contact.cpp'), str(ROOT / 'src/Packet.cpp'),
str(ROOT / 'src/helpers/StaticPoolPacketManager.cpp'), '-o', str(binary),
], capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
# The actual pool owns firmware-lifetime allocations and has no
# destructor. Keep ASan/UBSan active without treating those as leaks.
environment = dict(os.environ, ASAN_OPTIONS='detect_leaks=0')
for scenario in scenarios:
with self.subTest(scenario=scenario, legacy=legacy):
result = subprocess.run([str(binary), scenario], capture_output=True,
text=True, timeout=10, env=environment)
if legacy and scenario not in controls:
self.assertNotEqual(result.returncode, 0)
self.assertIn('invalid cached advert must fail without transmission',
result.stderr)
else:
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn('PASS: cached advert share ' + scenario, result.stdout)
def run_fixture(self, cache, synchronous=False):
source = (ROOT / 'examples/companion_radio/DataStore.cpp').read_text()
header = (ROOT / 'examples/companion_radio/DataStore.h').read_text()