diff --git a/patches/meshcore/0010-Harden-emergency-forward-reliability.patch b/patches/meshcore/0010-Harden-emergency-forward-reliability.patch new file mode 100644 index 0000000..43c97a0 --- /dev/null +++ b/patches/meshcore/0010-Harden-emergency-forward-reliability.patch @@ -0,0 +1,92 @@ +diff --git a/examples/companion_radio/BotTypes.h b/examples/companion_radio/BotTypes.h +index 5eb71c6..7ee34c8 100644 +--- a/examples/companion_radio/BotTypes.h ++++ b/examples/companion_radio/BotTypes.h +@@ -17,8 +17,6 @@ + #define BOT_COMMAND_COOLDOWN_MILLIS 5000UL + #define BOT_EMERGENCY_PREFIX "EMERGENCY MESSAGE FROM " + #define BOT_EMERGENCY_MAX_PARTS 3 +-#define BOT_EMERGENCY_RATE_LIMIT_MILLIS 60000UL +-#define BOT_EMERGENCY_RATE_LIMIT_COUNT 3 + #define BOT_PENDING_EMERGENCY_SLOTS BOT_EMERGENCY_MAX_PARTS + #define BOT_COORDINATOR_PENDING_SLOTS 8 + #define BOT_COORDINATOR_RECENT_SLOTS 16 +diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp +index 895be4b..89e130b 100644 +--- a/examples/companion_radio/MyMesh.cpp ++++ b/examples/companion_radio/MyMesh.cpp +@@ -960,23 +960,7 @@ bool MyMesh::findBotChannel(BotChannelKind kind, uint8_t &channel_idx) { + return false; + } + +-bool MyMesh::isEmergencyRateLimited() { +- unsigned long now = _ms->getMillis(); +- if (!emergency_rate_window_started || millisHasNowPassed(emergency_rate_window_started + BOT_EMERGENCY_RATE_LIMIT_MILLIS)) { +- emergency_rate_window_started = now; +- emergency_rate_count = 0; +- return false; +- } +- return emergency_rate_count >= BOT_EMERGENCY_RATE_LIMIT_COUNT; +-} +- +-void MyMesh::recordEmergencyRateLimitEvent() { +- if (emergency_rate_count < 0xFF) emergency_rate_count++; +-} +- + bool MyMesh::enqueueEmergencyForward(const BotMessage &message) { +- if (isEmergencyRateLimited()) return false; +- + BotEmergencyForward forward; + if (!EmergencyForwarder::format(message, forward)) return false; + +@@ -999,7 +983,6 @@ bool MyMesh::enqueueEmergencyForward(const BotMessage &message) { + } + } + +- recordEmergencyRateLimitEvent(); + return true; + } + +@@ -1175,10 +1158,10 @@ void MyMesh::sendQueuedEmergencyForwards() { + + if (success) { + bot_stats.emergency_forwards++; ++ pending->active = false; + } else { + bot_stats.emergency_forward_failures++; + } +- pending->active = false; + } + } + +@@ -1561,8 +1544,6 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe + ResponseCoordinator::clear(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS); + ResponseCoordinator::clearRecent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS); + KnownBotRegistry::clear(known_bot_entries, BOT_KNOWN_BOT_SLOTS); +- emergency_rate_window_started = 0; +- emergency_rate_count = 0; + next_bot_local_advert = 0; + next_bot_flood_advert = 0; + #endif +diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h +index e25f9a5..20497b7 100644 +--- a/examples/companion_radio/MyMesh.h ++++ b/examples/companion_radio/MyMesh.h +@@ -218,8 +218,6 @@ private: + BotFingerprint response_fingerprint); + bool enqueueEmergencyForward(const BotMessage &message); + bool findBotChannel(BotChannelKind kind, uint8_t &channel_idx); +- bool isEmergencyRateLimited(); +- void recordEmergencyRateLimitEvent(); + void sendQueuedBotResponses(); + void sendQueuedEmergencyForwards(); + void tickBot(); +@@ -285,8 +283,6 @@ private: + BotCoordinatorPending bot_coordinator_pending[BOT_COORDINATOR_PENDING_SLOTS]; + BotCoordinatorRecent bot_coordinator_recent[BOT_COORDINATOR_RECENT_SLOTS]; + BotKnownBotEntry known_bot_entries[BOT_KNOWN_BOT_SLOTS]; +- unsigned long emergency_rate_window_started; +- uint8_t emergency_rate_count; + unsigned long next_bot_local_advert; + unsigned long next_bot_flood_advert; + #endif diff --git a/scripts/check-bot-safety.sh b/scripts/check-bot-safety.sh index 518e428..f45959e 100644 --- a/scripts/check-bot-safety.sh +++ b/scripts/check-bot-safety.sh @@ -65,6 +65,31 @@ require_pattern 'recordBotObservation\(|sendQueuedEmergencyForwards\(|tickBot\(' "${MESHCORE_DIR}/examples/companion_radio/MyMesh.cpp" require_pattern '_prefs\.path_hash_mode[[:space:]]*=[[:space:]]*1' 'bot firmware defaults to two-byte path hashes' \ "${MESHCORE_DIR}/examples/companion_radio/MyMesh.cpp" +if grep -R -n -E 'BOT_EMERGENCY_RATE_LIMIT|isEmergencyRateLimited|recordEmergencyRateLimitEvent|emergency_rate_window_started|emergency_rate_count' \ + "${MESHCORE_DIR}/examples/companion_radio"; then + echo "Emergency forwarding must not be globally rate limited or dropped by a bot-local quota." >&2 + exit 1 +fi +if ! python3 - "${MESHCORE_DIR}/examples/companion_radio/MyMesh.cpp" <<'PY' +import re +import sys +text = open(sys.argv[1], encoding="utf-8").read() +start = text.find("void MyMesh::sendQueuedEmergencyForwards() {") +end = text.find("bool MyMesh::sendBotSelfAdvert", start) +if start < 0 or end < 0: + sys.exit(1) +body = text[start:end] +success_block = re.search(r"if \(success\) \{(?P.*?)\n \} else \{", body, re.S) +if not success_block or "pending->active = false;" not in success_block.group("block"): + sys.exit(1) +else_block = re.search(r"\} else \{(?P.*?)\n \}\n \}", body, re.S) +if else_block and "pending->active = false;" in else_block.group("block"): + sys.exit(1) +PY +then + echo "Emergency forwards must stay queued after failed public sends." >&2 + exit 1 +fi require_pattern 'CMESH_BOT_ENABLED=1' 'production bot build flag is enabled' \ "${MESHCORE_DIR}/platformio.ini" require_pattern 'ENABLE_PRIVATE_KEY_IMPORT=0' 'private key import disabled in production bot flags' \