From 0ae44d70170bb73f46f6b4a44ed235982b7072dd Mon Sep 17 00:00:00 2001 From: Andrew Yong Date: Thu, 9 Jul 2026 17:36:36 +0800 Subject: [PATCH] fix(router): null-check packetPool/clientNotificationPool allocations (#10948) On platforms where these pools are heap-backed (MemoryDynamic - used whenever there isn't enough static RAM for a fixed pool, e.g. ARCH_STM32WL or BOARD_HAS_PSRAM), allocCopy()/allocZeroed() return nullptr on allocation failure and already log a warning, but most callers dereferenced the result unconditionally. Under real heap pressure this reliably produced a HardFault - reproduced on STM32WL hardware under sustained mesh traffic, including the RX entry point (RadioLibInterface::handleReceiveInterrupt) where every received packet is allocated. Adds null checks at all call sites that were missing one, mirroring the guard pattern already used correctly elsewhere in the same files (e.g. RadioInterface.cpp's sendErrorNotification). On allocation failure, callers now skip the send/retransmission/notification and log nothing further (the allocator already did) rather than crash. Assisted-by: Claude Sonnet 5 Signed-off-by: Andrew Yong --- src/mesh/MeshService.cpp | 11 ++++++++--- src/mesh/NextHopRouter.cpp | 23 ++++++++++++++++------- src/mesh/NodeDB.cpp | 10 ++++++---- src/mesh/PhoneAPI.cpp | 2 ++ src/mesh/RadioLibInterface.cpp | 4 ++++ src/mesh/ReliableRouter.cpp | 3 ++- src/mesh/Router.cpp | 17 ++++++++++------- 7 files changed, 48 insertions(+), 22 deletions(-) diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index dac6c4d98..fb56b127f 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -111,7 +111,8 @@ int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp) } printPacket("Forwarding to phone", mp); - sendToPhone(packetPool.allocCopy(*mp)); + if (auto *toPhone = packetPool.allocCopy(*mp)) + sendToPhone(toPhone); return 0; } @@ -206,7 +207,8 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p) DEBUG_HEAP_BEFORE; auto a = packetPool.allocCopy(p); DEBUG_HEAP_AFTER("MeshService::handleToRadio", a); - sendToMesh(a, RX_SRC_USER); + if (a) + sendToMesh(a, RX_SRC_USER); bool loopback = false; // if true send any packet the phone sends back itself (for testing) if (loopback) { @@ -226,6 +228,8 @@ bool MeshService::cancelSending(PacketId id) ErrorCode MeshService::sendQueueStatusToPhone(const meshtastic_QueueStatus &qs, ErrorCode res, uint32_t mesh_packet_id) { meshtastic_QueueStatus *copied = queueStatusPool.allocCopy(qs); + if (!copied) + return ERRNO_UNKNOWN; copied->res = res; copied->mesh_packet_id = mesh_packet_id; @@ -266,7 +270,8 @@ void MeshService::sendToMesh(meshtastic_MeshPacket *p, RxSource src, bool ccToPh auto a = packetPool.allocCopy(*p); DEBUG_HEAP_AFTER("MeshService::sendToMesh", a); - sendToPhone(a); + if (a) + sendToPhone(a); } // Router may ask us to release the packet if it wasn't sent diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index 471aa22ad..f33de779c 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -31,8 +31,10 @@ ErrorCode NextHopRouter::send(meshtastic_MeshPacket *p) // If it's from us, ReliableRouter already handles retransmissions if want_ack is set. If a next hop is set and hop limit is // not 0 or want_ack is set, start retransmissions - if ((!isFromUs(p) || !p->want_ack) && p->next_hop != NO_NEXT_HOP_PREFERENCE && (p->hop_limit > 0 || p->want_ack)) - startRetransmission(packetPool.allocCopy(*p)); // start retransmission for relayed packet + if ((!isFromUs(p) || !p->want_ack) && p->next_hop != NO_NEXT_HOP_PREFERENCE && (p->hop_limit > 0 || p->want_ack)) { + if (auto *copy = packetPool.allocCopy(*p)) + startRetransmission(copy); // start retransmission for relayed packet + } return Router::send(p); } @@ -168,6 +170,8 @@ bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p) // ambiguous next_hop byte is ever learned (sniffReceived) or originated (getNextHop). if (p->next_hop == NO_NEXT_HOP_PREFERENCE || p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum())) { meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it + if (!tosend) + return true; LOG_INFO("Rebroadcast received message coming from %x", p->relay_node); // If exhausting hops, force hop_limit = 0 regardless of other logic @@ -397,7 +401,8 @@ int32_t NextHopRouter::doRetransmissions() trafficManagementModule->clearNextHop(p.packet->to); } #endif - FloodingRouter::send(packetPool.allocCopy(*p.packet)); + if (auto *copy = packetPool.allocCopy(*p.packet)) + FloodingRouter::send(copy); } else { #if NEXTHOP_EARLY_FLOOD_ON_UNVERIFIED // M4 (gated): if the route isn't proven healthy, don't spend a second directed @@ -411,18 +416,22 @@ int32_t NextHopRouter::doRetransmissions() meshtastic_NodeInfoLite *sentTo = nodeDB->getMeshNode(p.packet->to); if (sentTo) sentTo->next_hop = NO_NEXT_HOP_PREFERENCE; - FloodingRouter::send(packetPool.allocCopy(*p.packet)); + if (auto *copy = packetPool.allocCopy(*p.packet)) + FloodingRouter::send(copy); } else { - NextHopRouter::send(packetPool.allocCopy(*p.packet)); + if (auto *copy = packetPool.allocCopy(*p.packet)) + NextHopRouter::send(copy); } #else - NextHopRouter::send(packetPool.allocCopy(*p.packet)); + if (auto *copy = packetPool.allocCopy(*p.packet)) + NextHopRouter::send(copy); #endif } } else { // Note: we call the superclass version because we don't want to have our version of send() add a new // retransmission record - FloodingRouter::send(packetPool.allocCopy(*p.packet)); + if (auto *copy = packetPool.allocCopy(*p.packet)) + FloodingRouter::send(copy); } // Queue again diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 76e7384dc..e05f8b8f0 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -3348,10 +3348,12 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde "to regenerate your public keys."; LOG_WARN(warning, safeName); meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_WARNING; - cn->time = getValidTime(RTCQualityFromNet); - snprintf(cn->message, sizeof(cn->message), warning, safeName); - service->sendClientNotification(cn); + if (cn) { + cn->level = meshtastic_LogRecord_Level_WARNING; + cn->time = getValidTime(RTCQualityFromNet); + snprintf(cn->message, sizeof(cn->message), warning, safeName); + service->sendClientNotification(cn); + } } return false; } diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index cf58c23b4..8fac33e84 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -1640,6 +1640,8 @@ bool PhoneAPI::available() void PhoneAPI::sendNotification(meshtastic_LogRecord_Level level, uint32_t replyId, const char *message) { meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + if (!cn) + return; cn->has_reply_id = true; cn->reply_id = replyId; cn->level = meshtastic_LogRecord_Level_WARNING; diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index db60b7b15..5a9b292cd 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -663,6 +663,10 @@ void RadioLibInterface::handleReceiveInterrupt() // This allows the router and other apps on our node to sniff packets (usually routing) between other // nodes. meshtastic_MeshPacket *mp = packetPool.allocZeroed(); + if (!mp) { + airTime->logAirtime(RX_LOG, rxMsec); + return; + } // Keep the assigned fields in sync with src/mqtt/MQTT.cpp:onReceiveProto mp->from = radioBuffer.header.from; diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index 9c800b027..7ec6bb4b7 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -21,7 +21,8 @@ ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p) auto copy = packetPool.allocCopy(*p); DEBUG_HEAP_AFTER("ReliableRouter::send", copy); - startRetransmission(copy, NUM_RELIABLE_RETX); + if (copy) + startRetransmission(copy, NUM_RELIABLE_RETX); } /* If we have pending retransmissions, add the airtime of this packet to it, because during that time we cannot receive an diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index c4142c7a8..67d724433 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -312,12 +312,15 @@ ErrorCode Router::send(meshtastic_MeshPacket *p) LOG_WARN("Duty cycle limit exceeded. Aborting send for now, you can send again in %d mins", silentMinutes); meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->has_reply_id = true; - cn->reply_id = p->id; - cn->level = meshtastic_LogRecord_Level_WARNING; - cn->time = getValidTime(RTCQualityFromNet); - snprintf(cn->message, sizeof(cn->message), "Duty cycle limit exceeded. You can send again in %d mins", silentMinutes); - service->sendClientNotification(cn); + if (cn) { + cn->has_reply_id = true; + cn->reply_id = p->id; + cn->level = meshtastic_LogRecord_Level_WARNING; + cn->time = getValidTime(RTCQualityFromNet); + snprintf(cn->message, sizeof(cn->message), "Duty cycle limit exceeded. You can send again in %d mins", + silentMinutes); + service->sendClientNotification(cn); + } meshtastic_Routing_Error err = meshtastic_Routing_Error_DUTY_CYCLE_LIMIT; if (isFromUs(p)) { // only send NAK to API, not to the mesh @@ -405,7 +408,7 @@ ErrorCode Router::send(meshtastic_MeshPacket *p) } #if !MESHTASTIC_EXCLUDE_MQTT // Only publish to MQTT if we're the original transmitter of the packet - if (moduleConfig.mqtt.enabled && isFromUs(p) && mqtt) { + if (moduleConfig.mqtt.enabled && isFromUs(p) && mqtt && p_decoded) { mqtt->onSend(*p, *p_decoded, chIndex); } #endif