Reject malformed cached adverts before sharing contacts
This commit is contained in:
1 parent
92b9a1cc64
commit
07afacf379
11 files changed
+443
-60
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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());
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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(), ×tamp, 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*",
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user