From abf217c38df1cd05c422f367a468a66c56a6a4d6 Mon Sep 17 00:00:00 2001 From: Andrew Yong Date: Sat, 25 Jul 2026 19:28:42 +0800 Subject: [PATCH] fix(mesh): don't assert on malloc() failure in MemoryDynamic::alloc() (#11197) * fix(mesh): don't assert on malloc() failure in MemoryDynamic::alloc() MemoryDynamic::alloc() called assert(p) right after malloc(), instead of returning nullptr like the static MemoryPool::alloc() already does on exhaustion. All of packetPool's callers were already hardened to null-check allocCopy()/allocZeroed() (#10948, #10951), but that path is unreachable on real OOM here: assert() fires first, one level down, before the caller ever gets a chance to check anything. On STM32WL (packetPool is MemoryDynamic there - not enough static RAM for the fixed pool), assert() failures are wrapped to an infinite `while(true);` loop rather than aborting or resetting, so the thread just hangs forever instead of returning nullptr. Reproduced on wio-e5 hardware under a burst of incoming DMs, with free heap draining toward zero right before the hang. Assisted-by: Claude Sonnet 5 Signed-off-by: Andrew Yong * fix: null-check the two allocUnique*() call sites missed by prior audits #10948/#10951 null-checked every raw-pointer packetPool.allocCopy()/ allocZeroed() call site, but missed the two spots using the UniquePacketPoolPacket (unique_ptr) wrapper: UdpMulticastHandler::onReceive() and MQTT::onReceive() both dereferenced the allocation result unconditionally. Previously unreachable in practice: MemoryDynamic::alloc() asserted before ever returning null, so nothing downstream saw it. Fixed alongside that assert removal (this branch) since these two are now reachable with a genuine null - same fix as everywhere else, just an unchecked pointer that turns up under OOM. Assisted-by: Claude Sonnet 5 Signed-off-by: Andrew Yong * fix(SimRadio): don't leave isReceiving stuck true on allocation failure startReceive() set isReceiving = true before allocCopy(), so a failed allocation (now reachable with a real nullptr instead of hanging in assert()) left the simulated radio permanently marked as receiving: handleReceiveInterrupt() returns immediately on a null receivingPacket, before ever reaching the code that would clear isReceiving. Assisted-by: Claude Sonnet 5 Signed-off-by: Andrew Yong --------- Signed-off-by: Andrew Yong Co-authored-by: Ben Meadors --- src/mesh/MemoryPool.h | 5 ++++- src/mesh/udp/UdpMulticastHandler.h | 2 ++ src/mqtt/MQTT.cpp | 2 ++ src/platform/portduino/SimRadio.cpp | 10 ++++++++-- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/mesh/MemoryPool.h b/src/mesh/MemoryPool.h index d10c8cf96..ed20fb334 100644 --- a/src/mesh/MemoryPool.h +++ b/src/mesh/MemoryPool.h @@ -114,7 +114,10 @@ template class MemoryDynamic : public Allocator virtual T *alloc(TickType_t maxWait) override { T *p = (T *)malloc(sizeof(T)); - assert(p); + if (!p) { + LOG_WARN("malloc(%u) failed, heap exhausted!", (unsigned)sizeof(T)); + return nullptr; + } this->auditAdd((int32_t)sizeof(T)); return p; } diff --git a/src/mesh/udp/UdpMulticastHandler.h b/src/mesh/udp/UdpMulticastHandler.h index 93f5dd6ac..5a949c91a 100644 --- a/src/mesh/udp/UdpMulticastHandler.h +++ b/src/mesh/udp/UdpMulticastHandler.h @@ -84,6 +84,8 @@ class UdpMulticastHandler final mp.pki_encrypted = false; mp.public_key.size = 0; UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp); + if (!p) + return; // Unset received SNR/RSSI p->rx_snr = 0; p->rx_rssi = 0; diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 251c632a1..6427b4c31 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -143,6 +143,8 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length) } UniquePacketPoolPacket p = packetPool.allocUniqueZeroed(); + if (!p) + return; p->from = e.packet->from; p->to = e.packet->to; p->id = e.packet->id; diff --git a/src/platform/portduino/SimRadio.cpp b/src/platform/portduino/SimRadio.cpp index d0f2f1140..2786903eb 100644 --- a/src/platform/portduino/SimRadio.cpp +++ b/src/platform/portduino/SimRadio.cpp @@ -323,13 +323,19 @@ void SimRadio::startReceive(meshtastic_MeshPacket *p) return; } } - isReceiving = true; receivingPacket = packetPool.allocCopy(*p); + if (!receivingPacket) { + return; + } + isReceiving = true; uint32_t airtimeMsec = getPacketTime(p, true); notifyLater(airtimeMsec, ISR_RX, false); // Model the time it is busy receiving #else - isReceiving = true; receivingPacket = packetPool.allocCopy(*p); + if (!receivingPacket) { + return; + } + isReceiving = true; handleReceiveInterrupt(); // Simulate receiving the packet immediately startTransmitTimer(); #endif