diff --git a/platformio.ini b/platformio.ini index 6513ce8d..8367ba0e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -9,10 +9,13 @@ ; https://docs.platformio.org/page/projectconf.html [platformio] -default_envs = native +default_envs = native17 [env] +build_unflags = + -fno-exceptions build_flags = + -fexceptions -Wall ;-Wextra -Wno-missing-field-initializers @@ -35,7 +38,7 @@ test_build_src = true platform = native build_flags = ${env.build_flags} - -std=c++11 + -std=gnu++11 -DNATIVE lib_deps = ${env.lib_deps} @@ -43,10 +46,12 @@ lib_compat_mode = off [env:native17] platform = native -build_unflags = -std=gnu++11 +build_unflags = + ${env.build_unflags} + -std=gnu++11 build_flags = ${env.build_flags} - -std=c++17 + -std=gnu++17 -DNATIVE lib_deps = ${env.lib_deps} @@ -54,27 +59,43 @@ lib_compat_mode = off [env:native20] platform = native -build_unflags = -std=gnu++11 +build_unflags = + ${env.build_unflags} + -std=gnu++11 build_flags = ${env.build_flags} - -std=c++20 + -std=gnu++20 -DNATIVE lib_deps = ${env.lib_deps} lib_compat_mode = off +[env:mcu] +build_unflags = + ${env.build_unflags} + ;-std=gnu++11 +build_flags = + ${env.build_flags} + ;-std=gnu++17 +lib_deps = + ${env.lib_deps} + [env:ttgo-lora32-v21] platform = espressif32 board = ttgo-lora32-v21 board_build.partitions = no_ota.csv framework = arduino monitor_speed = 115200 +build_unflags = + ${env:mcu.build_unflags} + -std=gnu++11 build_flags = - ${env.build_flags} + ${env:mcu.build_flags} + -std=gnu++17 -DBOARD_ESP32 -DMSGPACK_USE_BOOST=OFF lib_deps = - ${env.lib_deps} + ${env:mcu.lib_deps} [env:ttgo-t-beam] platform = espressif32 @@ -84,11 +105,11 @@ board_build.partitions = no_ota.csv framework = arduino monitor_speed = 115200 build_flags = - ${env.build_flags} + ${env:mcu.build_flags} -DBOARD_ESP32 -DMSGPACK_USE_BOOST=OFF lib_deps = - ${env.lib_deps} + ${env:mcu.lib_deps} [env:lilygo_tbeam_supreme] platform = espressif32 @@ -98,11 +119,11 @@ board_build.partitions = no_ota.csv framework = arduino monitor_speed = 115200 build_flags = - ${env.build_flags} + ${env:mcu.build_flags} -DBOARD_ESP32 -DMSGPACK_USE_BOOST=OFF lib_deps = - ${env.lib_deps} + ${env:mcu.lib_deps} [env:wiscore_rak4631] platform = nordicnrf52 @@ -110,9 +131,9 @@ board = rak4630 board_build.partitions = no_ota.csv framework = arduino monitor_speed = 115200 -build_src_filter = ${env.build_src_filter}+<../variants/rak4630> +build_src_filter = ${env:mcu.build_src_filter}+<../variants/rak4630> build_flags = - ${env.build_flags} + ${env:mcu.build_flags} -I variants/rak4630 -fexceptions -DBOARD_NRF52 @@ -120,12 +141,12 @@ build_flags = -DRNS_USE_TLSF=1 -DMSGPACK_USE_BOOST=OFF lib_deps = - ${env.lib_deps} + ${env:mcu.lib_deps} adafruit/Adafruit TinyUSB Library ; Overrides for specific tests (not working!) [test_env:test_objects] build_flags = - ${env.build_flags} + ${env:mcu.build_flags} ;-DTEST_OBJECT_DATA=1 -DTEST_OBJECT_IMPL=1 diff --git a/src/Bytes.h b/src/Bytes.h index 120e1e31..29af2619 100644 --- a/src/Bytes.h +++ b/src/Bytes.h @@ -196,6 +196,7 @@ MEM("Creating from data-move..."); inline void assign(const Bytes& bytes) { #ifdef COW + // shared_ptr copy only — O(1), no heap allocation _data = bytes.shareData(); _exclusive = false; #else diff --git a/src/Destination.cpp b/src/Destination.cpp index d15a4609..778c79d2 100644 --- a/src/Destination.cpp +++ b/src/Destination.cpp @@ -194,6 +194,7 @@ Packet Destination::announce(const Bytes& app_data, bool path_response, const In throw std::invalid_argument("Only IN destination types can be announced"); } + try { double now = OS::time(); auto it = _object->_path_responses.begin(); while (it != _object->_path_responses.end()) { @@ -284,7 +285,15 @@ Packet Destination::announce(const Bytes& app_data, bool path_response, const In } // CBA ACCUMULATES - _object->_path_responses.insert({tag, {OS::time(), announce_data}}); + try { + _object->_path_responses.insert({tag, {OS::time(), announce_data}}); + } + catch (const std::bad_alloc&) { + ERROR("announce: out of memory, path response not stored for " + _object->_hash.toHex()); + } + catch (const std::exception& e) { + ERROR(std::string("announce: exception storing path response: ") + e.what()); + } } //TRACE("Destination::announce: announce_data:" + announce_data.toHex()); @@ -306,6 +315,15 @@ Packet Destination::announce(const Bytes& app_data, bool path_response, const In else { return announce_packet; } + } + catch (const std::bad_alloc&) { + ERROR("announce: out of memory, announce not sent for " + _object->_hash.toHex()); + return {Type::NONE}; + } + catch (const std::exception& e) { + ERROR(std::string("announce: exception during announce: ") + e.what()); + return {Type::NONE}; + } } Packet Destination::announce(const Bytes& app_data /*= {}*/, bool path_response /*= false*/) { @@ -385,7 +403,15 @@ void Destination::incoming_link_request(const Bytes& data, const Packet& packet) TRACE("***** Accepting link request"); RNS::Link link = Link::validate_request(*this, data, packet); if (link) { - _object->_links.insert(link); + try { + _object->_links.insert(link); + } + catch (const std::bad_alloc&) { + ERROR("incoming_link_request: out of memory, link not tracked"); + } + catch (const std::exception& e) { + ERROR(std::string("incoming_link_request: exception tracking link: ") + e.what()); + } } } } diff --git a/src/Identity.cpp b/src/Identity.cpp index 06196831..aa404dc2 100644 --- a/src/Identity.cpp +++ b/src/Identity.cpp @@ -19,12 +19,15 @@ using namespace RNS::Type::Identity; using namespace RNS::Cryptography; using namespace RNS::Utilities; +#ifndef RNS_KNOWN_DESTINATIONS_MAX +#define RNS_KNOWN_DESTINATIONS_MAX 100 +#endif + /*static*/ std::map Identity::_known_destinations; /*static*/ bool Identity::_saving_known_destinations = false; // CBA // CBA ACCUMULATES -/*static*/ //uint16_t Identity::_known_destinations_maxsize = 100; -/*static*/ uint16_t Identity::_known_destinations_maxsize = 100; +/*static*/ uint16_t Identity::_known_destinations_maxsize = RNS_KNOWN_DESTINATIONS_MAX; Identity::Identity(bool create_keys /*= true*/) : _object(new Object()) { if (create_keys) { @@ -196,7 +199,15 @@ Can be used to load previously created and saved identities into Reticulum. else { //p _known_destinations[destination_hash] = {OS::time(), packet_hash, public_key, app_data}; // CBA ACCUMULATES - _known_destinations.insert({destination_hash, {OS::time(), packet_hash, public_key, app_data}}); + try { + _known_destinations.insert({destination_hash, {OS::time(), packet_hash, public_key, app_data}}); + } + catch (const std::bad_alloc&) { + ERROR("remember: out of memory, identity not stored for " + destination_hash.toHex()); + } + catch (const std::exception& e) { + ERROR(std::string("remember: exception storing identity: ") + e.what()); + } } } @@ -352,32 +363,49 @@ Recall last heard app_data for a destination hash. } /*static*/ void Identity::cull_known_destinations() { - TRACE("Transport::cull_path_table()"); + TRACE("Identity::cull_known_destinations()"); if (_known_destinations.size() > _known_destinations_maxsize) { - // prune by age - uint16_t count = 0; - std::vector> sorted_pairs; - // Copy key/value pairs from map into vector - std::for_each(_known_destinations.begin(), _known_destinations.end(), [&](const std::pair& ref) { - sorted_pairs.push_back(ref); - }); - // Sort vector using specified comparator - std::sort(sorted_pairs.begin(), sorted_pairs.end(), [](const std::pair &left, const std::pair &right) { - return left.second._timestamp < right.second._timestamp; - }); - // Iterate vector of sorted values - for (auto& [destination_hash, identity_entry] : sorted_pairs) { - TRACE("Transport::cull_path_table: Removing destination " + destination_hash.toHex() + " from known destinations"); - // Remove destination from known destinations - if (_known_destinations.erase(destination_hash) < 1) { - WARNING("Failed to remove destination " + destination_hash.toHex() + " from known destinations"); + try { + // Build lightweight (timestamp, key) index to avoid copying full IdentityEntry + // objects — prevents OOM on heap-constrained devices when the table is full. + std::vector> sorted_keys; + sorted_keys.reserve(_known_destinations.size()); + for (const auto& [key, entry] : _known_destinations) { + sorted_keys.emplace_back(entry._timestamp, key); } - ++count; - if (_known_destinations.size() <= _known_destinations_maxsize) { - break; + // Sort ascending by timestamp (oldest first) + std::sort(sorted_keys.begin(), sorted_keys.end()); + + uint16_t count = 0; + for (const auto& [timestamp, destination_hash] : sorted_keys) { + TRACE("Identity::cull_known_destinations: Removing destination " + destination_hash.toHex() + " from known destinations"); + if (_known_destinations.erase(destination_hash) < 1) { + WARNING("Failed to remove destination " + destination_hash.toHex() + " from known destinations"); + } + ++count; + if (_known_destinations.size() <= _known_destinations_maxsize) { + break; + } + } + DEBUG("Removed " + std::to_string(count) + " path(s) from known destinations"); + } + catch (const std::bad_alloc& e) { + ERROR("cull_known_destinations: out of memory building sort index, falling back to single erase"); + // Fallback: std::min_element does no heap allocation — erase one oldest entry + auto oldest = std::min_element( + _known_destinations.begin(), _known_destinations.end(), + [](const std::pair& a, + const std::pair& b) { + return a.second._timestamp < b.second._timestamp; + } + ); + if (oldest != _known_destinations.end()) { + _known_destinations.erase(oldest); } } - DEBUG("Removed " + std::to_string(count) + " path(s) from known destinations"); + catch (const std::exception& e) { + ERROR(std::string("cull_known_destinations: exception: ") + e.what()); + } } } diff --git a/src/Identity.h b/src/Identity.h index 0459aa11..84efeffc 100644 --- a/src/Identity.h +++ b/src/Identity.h @@ -164,6 +164,8 @@ namespace RNS { inline const Cryptography::Ed25519PrivateKey::Ptr sig_prv() const { assert(_object); return _object->_sig_prv; } inline const Cryptography::X25519PublicKey::Ptr pub() const { assert(_object); return _object->_pub; } inline const Cryptography::Ed25519PublicKey::Ptr sig_pub() const { assert(_object); return _object->_sig_pub; } + inline static uint16_t known_destinations_maxsize() { return _known_destinations_maxsize; } + inline static void known_destinations_maxsize(uint16_t known_destinations_maxsize) { _known_destinations_maxsize = known_destinations_maxsize; } inline std::string toString() const { if (!_object) return ""; return "{Identity:" + _object->_hash.toHex() + "}"; } diff --git a/src/Link.cpp b/src/Link.cpp index d358f8ef..05dbc059 100644 --- a/src/Link.cpp +++ b/src/Link.cpp @@ -697,6 +697,7 @@ void Link::teardown_packet(const Packet& packet) { } } catch (std::exception& e) { + ERRORF("Error while decrypting teardown packet from %s. The contained exception was: %s", toString().c_str(), e.what()); } } diff --git a/src/Packet.cpp b/src/Packet.cpp index 0e68a49c..f544a223 100644 --- a/src/Packet.cpp +++ b/src/Packet.cpp @@ -419,6 +419,10 @@ bool Packet::unpack() { _object->_packed = false; update_hash(); } + catch (const std::bad_alloc&) { + ERROR("Packet::unpack: out of memory unpacking packet"); + return false; + } catch (std::exception& e) { ERROR(std::string("Received malformed packet, dropping it. The contained exception was: ") + e.what()); return false; diff --git a/src/Transport.cpp b/src/Transport.cpp index 84bbf56f..0f726a1e 100644 --- a/src/Transport.cpp +++ b/src/Transport.cpp @@ -18,6 +18,18 @@ using namespace RNS; using namespace RNS::Type::Transport; using namespace RNS::Utilities; +#ifndef RNS_PATH_TABLE_MAX +#define RNS_PATH_TABLE_MAX 100 +#endif + +#ifndef RNS_HASHLIST_MAX +#define RNS_HASHLIST_MAX 100 +#endif + +#ifndef RNS_PR_TAGS_MAX +#define RNS_PR_TAGS_MAX 32 +#endif + #if defined(INTERFACES_SET) ///*static*/ std::set, std::less> Transport::_interfaces; /*static*/ std::set, std::less> Transport::_interfaces; @@ -80,19 +92,16 @@ using namespace RNS::Utilities; /*static*/ bool Transport::_saving_path_table = false; // CBA ACCUMULATES // CBA MCU -/*static*/ //uint16_t Transport::_hashlist_maxsize = 1000000; -/*static*/ //uint16_t Transport::_hashlist_maxsize = 100; -/*static*/ uint16_t Transport::_hashlist_maxsize = 100; +/*static*/ uint16_t Transport::_hashlist_maxsize = RNS_HASHLIST_MAX; // CBA ACCUMULATES // CBA MCU -/*static*/ //uint16_t Transport::_max_pr_tags = 32000; -/*static*/ uint16_t Transport::_max_pr_tags = 32; +/*static*/ uint16_t Transport::_max_pr_tags = RNS_PR_TAGS_MAX; // CBA // CBA ACCUMULATES -/*static*/ uint16_t Transport::_path_table_maxsize = 100; +/*static*/ uint16_t Transport::_path_table_maxsize = RNS_PATH_TABLE_MAX; // CBA ACCUMULATES -/*static*/ uint16_t Transport::_path_table_maxpersist = 100; +/*static*/ uint16_t Transport::_path_table_maxpersist = RNS_PATH_TABLE_MAX; /*static*/ double Transport::_last_saved = 0.0; /*static*/ float Transport::_save_interval = 3600.0; /*static*/ uint32_t Transport::_destination_table_crc = 0; @@ -443,153 +452,197 @@ using namespace RNS::Utilities; //cull_path_table(); // Cull the reverse table according to timeout - std::vector stale_reverse_entries; - for (const auto& [packet_hash, reverse_entry] : _reverse_table) { - if (OS::time() > (reverse_entry._timestamp + REVERSE_TIMEOUT)) { - stale_reverse_entries.push_back(packet_hash); + try { + std::vector stale_reverse_entries; + stale_reverse_entries.reserve(_reverse_table.size()); + for (const auto& [packet_hash, reverse_entry] : _reverse_table) { + if (OS::time() > (reverse_entry._timestamp + REVERSE_TIMEOUT)) { + stale_reverse_entries.push_back(packet_hash); + } } + remove_reverse_entries(stale_reverse_entries); + } + catch (const std::bad_alloc&) { + ERROR("jobs: out of memory culling reverse table"); + } + catch (const std::exception& e) { + ERROR("jobs: failed to cull reverse table: " + std::string(e.what())); } // Cull the link table according to timeout - std::vector stale_links; - for (const auto& [link_id, link_entry] : _link_table) { - if (link_entry._validated) { - if (OS::time() > (link_entry._timestamp + LINK_TIMEOUT)) { - stale_links.push_back(link_id); - } - } - else { - if (OS::time() > link_entry._proof_timeout) { - stale_links.push_back(link_id); - - double last_path_request = 0.0; - const auto& iter = _path_requests.find(link_entry._destination_hash); - if (iter != _path_requests.end()) { - last_path_request = (*iter).second; + try { + std::vector stale_links; + stale_links.reserve(_link_table.size()); + for (const auto& [link_id, link_entry] : _link_table) { + if (link_entry._validated) { + if (OS::time() > (link_entry._timestamp + LINK_TIMEOUT)) { + stale_links.push_back(link_id); } + } + else { + if (OS::time() > link_entry._proof_timeout) { + stale_links.push_back(link_id); - uint8_t lr_taken_hops = link_entry._hops; + double last_path_request = 0.0; + const auto& iter = _path_requests.find(link_entry._destination_hash); + if (iter != _path_requests.end()) { + last_path_request = (*iter).second; + } - bool path_request_throttle = (OS::time() - last_path_request) < PATH_REQUEST_MI; - bool path_request_conditions = false; - - // If the path has been invalidated between the time of - // making the link request and now, try to rediscover it - if (!has_path(link_entry._destination_hash)) { - DEBUG("Trying to rediscover path for " + link_entry._destination_hash.toHex() + " since an attempted link was never established, and path is now missing"); - path_request_conditions = true; - } + uint8_t lr_taken_hops = link_entry._hops; - // If this link request was originated from a local client - // attempt to rediscover a path to the destination, if this - // has not already happened recently. - else if (!path_request_throttle && lr_taken_hops == 0) { - DEBUG("Trying to rediscover path for " + link_entry._destination_hash.toHex() + " since an attempted local client link was never established"); - path_request_conditions = true; - } + bool path_request_throttle = (OS::time() - last_path_request) < PATH_REQUEST_MI; + bool path_request_conditions = false; + + // If the path has been invalidated between the time of + // making the link request and now, try to rediscover it + if (!has_path(link_entry._destination_hash)) { + DEBUG("Trying to rediscover path for " + link_entry._destination_hash.toHex() + " since an attempted link was never established, and path is now missing"); + path_request_conditions = true; + } - // If the link destination was previously only 1 hop - // away, this likely means that it was local to one - // of our interfaces, and that it roamed somewhere else. - // In that case, try to discover a new path. - else if (!path_request_throttle && hops_to(link_entry._destination_hash) == 1) { - DEBUG("Trying to rediscover path for " + link_entry._destination_hash.toHex() + " since an attempted link was never established, and destination was previously local to an interface on this instance"); - path_request_conditions = true; - } + // If this link request was originated from a local client + // attempt to rediscover a path to the destination, if this + // has not already happened recently. + else if (!path_request_throttle && lr_taken_hops == 0) { + DEBUG("Trying to rediscover path for " + link_entry._destination_hash.toHex() + " since an attempted local client link was never established"); + path_request_conditions = true; + } - // If the link destination was previously only 1 hop - // away, this likely means that it was local to one - // of our interfaces, and that it roamed somewhere else. - // In that case, try to discover a new path. - else if ( !path_request_throttle and lr_taken_hops == 1) { - DEBUG("Trying to rediscover path for " + link_entry._destination_hash.toHex() + " since an attempted link was never established, and link initiator is local to an interface on this instance"); - path_request_conditions = true; - } + // If the link destination was previously only 1 hop + // away, this likely means that it was local to one + // of our interfaces, and that it roamed somewhere else. + // In that case, try to discover a new path. + else if (!path_request_throttle && hops_to(link_entry._destination_hash) == 1) { + DEBUG("Trying to rediscover path for " + link_entry._destination_hash.toHex() + " since an attempted link was never established, and destination was previously local to an interface on this instance"); + path_request_conditions = true; + } - if (path_request_conditions) { - if (path_requests.count(link_entry._destination_hash) == 0) { - // CBA ACCUMULATES - path_requests.insert(link_entry._destination_hash); + // If the link destination was previously only 1 hop + // away, this likely means that it was local to one + // of our interfaces, and that it roamed somewhere else. + // In that case, try to discover a new path. + else if ( !path_request_throttle and lr_taken_hops == 1) { + DEBUG("Trying to rediscover path for " + link_entry._destination_hash.toHex() + " since an attempted link was never established, and link initiator is local to an interface on this instance"); + path_request_conditions = true; } - if (!Reticulum::transport_enabled()) { - // Drop current path if we are not a transport instance, to - // allow using higher-hop count paths or reused announces - // from newly adjacent transport instances. - expire_path(link_entry._destination_hash); + if (path_request_conditions) { + if (path_requests.count(link_entry._destination_hash) == 0) { + // CBA ACCUMULATES + path_requests.insert(link_entry._destination_hash); + } + + if (!Reticulum::transport_enabled()) { + // Drop current path if we are not a transport instance, to + // allow using higher-hop count paths or reused announces + // from newly adjacent transport instances. + expire_path(link_entry._destination_hash); + } } } } } + remove_links(stale_links); + } + catch (const std::bad_alloc&) { + ERROR("jobs: out of memory culling link table"); + } + catch (const std::exception& e) { + ERROR("jobs: failed to cull link table: " + std::string(e.what())); } // Cull the path table - std::vector stale_paths; - for (const auto& [destination_hash, destination_entry] : _destination_table) { - const Interface& attached_interface = destination_entry.receiving_interface(); - double destination_expiry; - if (attached_interface && attached_interface.mode() == Type::Interface::MODE_ACCESS_POINT) { - destination_expiry = destination_entry._timestamp + AP_PATH_TIME; - } - else if (attached_interface && attached_interface.mode() == Type::Interface::MODE_ROAMING) { - destination_expiry = destination_entry._timestamp + ROAMING_PATH_TIME; - } - else { - destination_expiry = destination_entry._timestamp + DESTINATION_TIMEOUT; - } + try { + std::vector stale_paths; + stale_paths.reserve(_destination_table.size()); + for (const auto& [destination_hash, destination_entry] : _destination_table) { + const Interface& attached_interface = destination_entry.receiving_interface(); + double destination_expiry; + if (attached_interface && attached_interface.mode() == Type::Interface::MODE_ACCESS_POINT) { + destination_expiry = destination_entry._timestamp + AP_PATH_TIME; + } + else if (attached_interface && attached_interface.mode() == Type::Interface::MODE_ROAMING) { + destination_expiry = destination_entry._timestamp + ROAMING_PATH_TIME; + } + else { + destination_expiry = destination_entry._timestamp + DESTINATION_TIMEOUT; + } - if (OS::time() > destination_expiry) { - stale_paths.push_back(destination_hash); - DEBUG("Path to " + destination_hash.toHex() + " timed out and was removed"); - } - else if (_interfaces.count(attached_interface.get_hash()) == 0) { - stale_paths.push_back(destination_hash); - DEBUG("Path to " + destination_hash.toHex() + " was removed since the attached interface no longer exists"); + if (OS::time() > destination_expiry) { + stale_paths.push_back(destination_hash); + DEBUG("Path to " + destination_hash.toHex() + " timed out and was removed"); + } + else if (_interfaces.count(attached_interface.get_hash()) == 0) { + stale_paths.push_back(destination_hash); + DEBUG("Path to " + destination_hash.toHex() + " was removed since the attached interface no longer exists"); + } } + remove_paths(stale_paths); + } + catch (const std::bad_alloc&) { + ERROR("jobs: out of memory culling path table"); + } + catch (const std::exception& e) { + ERROR("jobs: failed to cull path table: " + std::string(e.what())); } // Cull the pending discovery path requests table - std::vector stale_discovery_path_requests; - for (const auto& [destination_hash, path_entry] : _discovery_path_requests) { - if (OS::time() > path_entry._timeout) { - stale_discovery_path_requests.push_back(destination_hash); - DEBUG("Waiting path request for " + destination_hash.toString() + " timed out and was removed"); + try { + std::vector stale_discovery_path_requests; + stale_discovery_path_requests.reserve(_discovery_path_requests.size()); + for (const auto& [destination_hash, path_entry] : _discovery_path_requests) { + if (OS::time() > path_entry._timeout) { + stale_discovery_path_requests.push_back(destination_hash); + DEBUG("Waiting path request for " + destination_hash.toString() + " timed out and was removed"); + } } + remove_discovery_path_requests(stale_discovery_path_requests); + } + catch (const std::bad_alloc&) { + ERROR("jobs: out of memory culling discovery path requests"); + } + catch (const std::exception& e) { + ERROR("jobs: failed to cull discovery path requests: " + std::string(e.what())); } // Cull the tunnel table - count = 0; - std::vector stale_tunnels; - for (const auto& [tunnel_id, tunnel_entry] : _tunnels) { - if (OS::time() > tunnel_entry._expires) { - stale_tunnels.push_back(tunnel_id); - TRACE("Tunnel " + tunnel_id.toHex() + " timed out and was removed"); - } - else { - std::vector stale_tunnel_paths; - for (const auto& [destination_hash, destination_entry] : tunnel_entry._serialised_paths) { - if (OS::time() > (destination_entry._timestamp + DESTINATION_TIMEOUT)) { - stale_tunnel_paths.push_back(destination_hash); - TRACE("Tunnel path to " + destination_hash.toHex() + " timed out and was removed"); - } + try { + count = 0; + std::vector stale_tunnels; + stale_tunnels.reserve(_tunnels.size()); + for (const auto& [tunnel_id, tunnel_entry] : _tunnels) { + if (OS::time() > tunnel_entry._expires) { + stale_tunnels.push_back(tunnel_id); + TRACE("Tunnel " + tunnel_id.toHex() + " timed out and was removed"); } + else { + std::vector stale_tunnel_paths; + for (const auto& [destination_hash, destination_entry] : tunnel_entry._serialised_paths) { + if (OS::time() > (destination_entry._timestamp + DESTINATION_TIMEOUT)) { + stale_tunnel_paths.push_back(destination_hash); + TRACE("Tunnel path to " + destination_hash.toHex() + " timed out and was removed"); + } + } - //for (const auto& destination_hash : stale_tunnel_paths) { - for (const Bytes& destination_hash : stale_tunnel_paths) { - const_cast(tunnel_entry)._serialised_paths.erase(destination_hash); - ++count; + //for (const auto& destination_hash : stale_tunnel_paths) { + for (const Bytes& destination_hash : stale_tunnel_paths) { + const_cast(tunnel_entry)._serialised_paths.erase(destination_hash); + ++count; + } } } + if (count > 0) { + TRACE("Removed " + std::to_string(count) + " tunnel paths"); + } + remove_tunnels(stale_tunnels); } - if (count > 0) { - TRACE("Removed " + std::to_string(count) + " tunnel paths"); + catch (const std::bad_alloc&) { + ERROR("jobs: out of memory culling tunnel table"); + } + catch (const std::exception& e) { + ERROR("jobs: failed to cull tunnel table: " + std::string(e.what())); } - - remove_reverse_entries(stale_reverse_entries); - remove_links(stale_links); - remove_paths(stale_paths); - remove_discovery_path_requests(stale_discovery_path_requests); - remove_tunnels(stale_tunnels); //#ifndef NDEBUG dump_stats(); @@ -1989,10 +2042,24 @@ using namespace RNS::Utilities; packet.get_hash() ); // CBA ACCUMULATES - if (_destination_table.insert({packet.destination_hash(), destination_table_entry}).second) { - ++_destinations_added; + // Pre-emptively cull before insert to avoid hitting OOM at max capacity + if (_destination_table.size() >= _path_table_maxsize) { cull_path_table(); } + try { + if (_destination_table.insert({packet.destination_hash(), destination_table_entry}).second) { + ++_destinations_added; + if (_destination_table.size() > _path_table_maxsize) { + cull_path_table(); + } + } + } + catch (const std::bad_alloc&) { + ERROR("inbound: out of memory storing destination entry"); + } + catch (const std::exception& e) { + ERROR(std::string("inbound: exception storing destination entry: ") + e.what()); + } DEBUG("Destination " + packet.destination_hash().toHex() + " is now " + std::to_string(announce_hops) + " hops away via " + received_from.toHex() + " on " + packet.receiving_interface().toString()); //TRACE("Transport::inbound: Destination " + packet.destination_hash().toHex() + " has data: " + packet.data().toHex()); @@ -3933,78 +4000,59 @@ TRACE("Transport::write_path_table: buffer size " + std::to_string(Persistence:: /*static*/ void Transport::cull_path_table() { TRACE("Transport::cull_path_table()"); if (_destination_table.size() > _path_table_maxsize) { - // TODO prune by age, or better yet by last use -/* - std::map::iterator iter = _destination_table.begin(); - // naively erase from front of table - std::advance(iter, _destination_table.size() - _path_table_maxsize + 1); - _destination_table.erase(_destination_table.begin(), iter); -*/ -/* - uint16_t count = 0; - std::set sorted_values; - MapToValues(_destination_table, sorted_values); - for (auto& destination_entry : sorted_values) { - Packet announce_packet = destination_entry.announce_packet(); - TRACE("Transport::cull_path_table: Removing destination " + announce_packet.destination_hash().toHex() + " from path table"); - // Remove destination from path table - if (_destination_table.erase(announce_packet.destination_hash()) < 1) { - WARNING("Failed to remove destination " + announce_packet.destination_hash().toHex() + " from path table"); - } - // Remove announce packet from packet table - //if (_packet_table.erase(destination_entry._announce_packet) < 1) { - // WARNING("Failed to remove packet " + destination_entry._announce_packet.toHex() + " from packet table"); - //} -#if defined(RNS_USE_FS) && defined(RNS_PERSIST_PATHS) - // Remove cached packet file - char packet_cache_path[Type::Reticulum::FILEPATH_MAXSIZE]; - snprintf(packet_cache_path, Type::Reticulum::FILEPATH_MAXSIZE, "%s/%s", Reticulum::_cachepath, destination_entry._announce_packet.toHex().c_str()); - if (OS::file_exists(packet_cache_path)) { - OS::remove_file(packet_cache_path); + try { + // Build lightweight (timestamp, key) index to avoid copying full DestinationEntry + // objects (which contain nested std::set) — prevents OOM on heap-constrained + // devices when the table hits max capacity. + std::vector> sorted_keys; + sorted_keys.reserve(_destination_table.size()); + for (const auto& [key, entry] : _destination_table) { + sorted_keys.emplace_back(entry._timestamp, key); } + // Sort ascending by timestamp so oldest entries are removed first + std::sort(sorted_keys.begin(), sorted_keys.end()); + + uint16_t count = 0; + for (const auto& [timestamp, destination_hash] : sorted_keys) { + TRACE("Transport::cull_path_table: Removing destination " + destination_hash.toHex() + " from path table"); +#if defined(RNS_USE_FS) && defined(RNS_PERSIST_PATHS) + auto it = _destination_table.find(destination_hash); + if (it != _destination_table.end()) { + // Remove cached packet file + char packet_cache_path[Type::Reticulum::FILEPATH_MAXSIZE]; + snprintf(packet_cache_path, Type::Reticulum::FILEPATH_MAXSIZE, "%s/%s", Reticulum::_cachepath, it->second._announce_packet.toHex().c_str()); + if (OS::file_exists(packet_cache_path)) { + OS::remove_file(packet_cache_path); + } + } #endif - ++count; - if (_destination_table.size() <= _path_table_maxsize) { - break; + if (_destination_table.erase(destination_hash) < 1) { + WARNING("Failed to remove destination " + destination_hash.toHex() + " from path table"); + } + ++count; + if (_destination_table.size() <= _path_table_maxsize) { + break; + } } + DEBUG("Removed " + std::to_string(count) + " path(s) from path table"); } - DEBUG("Removed " + std::to_string(count) + " path(s) from path table"); -*/ - uint16_t count = 0; - std::vector> sorted_pairs; - // Copy key/value pairs from map into vector - std::for_each(_destination_table.begin(), _destination_table.end(), [&](const std::pair& ref) { - sorted_pairs.push_back(ref); - }); - // Sort vector using specified comparator - std::sort(sorted_pairs.begin(), sorted_pairs.end(), [](const std::pair &left, const std::pair &right) { - return left.second._timestamp < right.second._timestamp; - }); - // Iterate vector of sorted values - for (auto& [destination_hash, destination_entry] : sorted_pairs) { - TRACE("Transport::cull_path_table: Removing destination " + destination_hash.toHex() + " from path table"); - // Remove destination from path table - if (_destination_table.erase(destination_hash) < 1) { - WARNING("Failed to remove destination " + destination_hash.toHex() + " from path table"); - } - // Remove announce packet from packet table - //if (_packet_table.erase(destination_entry._announce_packet) < 1) { - // WARNING("Failed to remove packet " + destination_entry._announce_packet.toHex() + " from packet table"); - //} -#if defined(RNS_USE_FS) && defined(RNS_PERSIST_PATHS) - // Remove cached packet file - char packet_cache_path[Type::Reticulum::FILEPATH_MAXSIZE]; - snprintf(packet_cache_path, Type::Reticulum::FILEPATH_MAXSIZE, "%s/%s", Reticulum::_cachepath, destination_entry._announce_packet.toHex().c_str()); - if (OS::file_exists(packet_cache_path)) { - OS::remove_file(packet_cache_path); + catch (const std::bad_alloc& e) { + ERROR("cull_path_table: out of memory building sort index, falling back to single erase"); + // Fallback: std::min_element does no heap allocation — erase one oldest entry + auto oldest = std::min_element( + _destination_table.begin(), _destination_table.end(), + [](const std::pair& a, + const std::pair& b) { + return a.second._timestamp < b.second._timestamp; } -#endif - ++count; - if (_destination_table.size() <= _path_table_maxsize) { - break; + ); + if (oldest != _destination_table.end()) { + _destination_table.erase(oldest); } } - DEBUG("Removed " + std::to_string(count) + " path(s) from path table"); + catch (const std::exception& e) { + ERROR(std::string("cull_path_table: exception: ") + e.what()); + } } } diff --git a/src/Transport.h b/src/Transport.h index 5150cc61..118e2502 100644 --- a/src/Transport.h +++ b/src/Transport.h @@ -380,6 +380,10 @@ namespace RNS { static inline const Identity& identity() { return _identity; } inline static uint16_t path_table_maxsize() { return _path_table_maxsize; } inline static void path_table_maxsize(uint16_t path_table_maxsize) { _path_table_maxsize = path_table_maxsize; } + inline static uint16_t hashlist_maxsize() { return _hashlist_maxsize; } + inline static void hashlist_maxsize(uint16_t hashlist_maxsize) { _hashlist_maxsize = hashlist_maxsize; } + inline static uint16_t max_pr_tags() { return _max_pr_tags; } + inline static void max_pr_tags(uint16_t max_pr_tags) { _max_pr_tags = max_pr_tags; } inline static uint16_t probe_destination_enabled() { return _path_table_maxpersist; } inline static void path_table_maxpersist(uint16_t path_table_maxpersist) { _path_table_maxpersist = path_table_maxpersist; } // CBA TEST diff --git a/src/Type.h b/src/Type.h index 4fbf0663..ebb9d42f 100644 --- a/src/Type.h +++ b/src/Type.h @@ -5,6 +5,28 @@ #include #include +#ifndef RNS_QUEUED_ANNOUNCES_MAX +#define RNS_QUEUED_ANNOUNCES_MAX 20 +#endif + +#ifndef RNS_RECEIPTS_MAX +#define RNS_RECEIPTS_MAX 20 +#endif + +#ifndef RNS_RATE_TIMESTAMPS_MAX +#define RNS_RATE_TIMESTAMPS_MAX 16 +#endif + +#ifndef RNS_RANDOM_BLOBS_PERSIST_MAX +//#define RNS_RANDOM_BLOBS_PERSIST_MAX 32 +#define RNS_RANDOM_BLOBS_PERSIST_MAX 16 +#endif + +#ifndef RNS_RANDOM_BLOBS_MAX +//#define RNS_RANDOM_BLOBS_MAX 64 +#define RNS_RANDOM_BLOBS_MAX 32 +#endif + namespace RNS { namespace Type { @@ -61,7 +83,7 @@ namespace RNS { namespace Type { */ static const bool LINK_MTU_DISCOVERY = true; - static const uint16_t MAX_QUEUED_ANNOUNCES = 16384; + static const uint16_t MAX_QUEUED_ANNOUNCES = RNS_QUEUED_ANNOUNCES_MAX; static const uint32_t QUEUED_ANNOUNCE_LIFE = 60*60*24; static const uint8_t ANNOUNCE_CAP = 2; @@ -427,11 +449,10 @@ namespace RNS { namespace Type { static constexpr const float LINK_TIMEOUT = Link::STALE_TIME * 1.25; static const uint16_t REVERSE_TIMEOUT = 30*60; // Reverse table entries are removed after 30 minutes // CBA MCU - //static const uint16_t MAX_RECEIPTS = 1024; // Maximum number of receipts to keep track of - static const uint16_t MAX_RECEIPTS = 20; // Maximum number of receipts to keep track of - static const uint8_t MAX_RATE_TIMESTAMPS = 16; // Maximum number of announce timestamps to keep per destination - static const uint8_t PERSIST_RANDOM_BLOBS = 32; // Maximum number of random blobs per destination to persist to disk - static const uint8_t MAX_RANDOM_BLOBS = 64; // Maximum number of random blobs per destination to keep in memory + static const uint16_t MAX_RECEIPTS = RNS_RECEIPTS_MAX; // Maximum number of receipts to keep track of + static const uint8_t MAX_RATE_TIMESTAMPS = RNS_RATE_TIMESTAMPS_MAX; // Maximum number of announce timestamps to keep per destination + static const uint8_t PERSIST_RANDOM_BLOBS = RNS_RANDOM_BLOBS_PERSIST_MAX; // Maximum number of random blobs per destination to persist to disk + static const uint8_t MAX_RANDOM_BLOBS = RNS_RANDOM_BLOBS_MAX; // Maximum number of random blobs per destination to keep in memory // CBA MCU //static const uint32_t DESTINATION_TIMEOUT = 60*60*24*7; // Destination table entries are removed if unused for one week