Skip to content

Commit a5940b4

Browse files
authored
Guard the deferred local queue and depth counter (#11284)
* Guard the deferred local queue and depth counter * Close the drain and enqueue race on the deferred queue * Route the raced loopback through handleReceived * Make the last-frame check and depth decrement atomic * Correct the handleReceived doc comment
1 parent fc67590 commit a5940b4

2 files changed

Lines changed: 60 additions & 24 deletions

File tree

src/mesh/Router.cpp

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,7 +1249,12 @@ bool Router::dequeueDeferredLocal(DeferredLocal &out)
12491249
void Router::deliverLocal(meshtastic_MeshPacket *p, RxSource src)
12501250
{
12511251
// Top level: handle synchronously, exactly as before the depth guard existed.
1252-
if (handleDepth == 0) {
1252+
bool nested;
1253+
{
1254+
concurrency::LockGuard g(&deferredLock);
1255+
nested = handleDepth > 0;
1256+
}
1257+
if (!nested) {
12531258
handleReceived(p, src);
12541259
return;
12551260
}
@@ -1258,8 +1263,26 @@ void Router::deliverLocal(meshtastic_MeshPacket *p, RxSource src)
12581263
// handleReceived() drains it once the current dispatch unwinds, instead of stacking another
12591264
// handleReceived() frame on top of the module handler (nRF52 stack overflow on config save).
12601265
meshtastic_MeshPacket *copy = packetPool.allocCopy(*p);
1261-
if (copy && enqueueDeferredLocal(copy, src))
1262-
return;
1266+
if (copy) {
1267+
// Re-check depth under the lock that also gates the drain's decrement, so a drain finishing
1268+
// while we allocated cannot leave this copy stranded in the ring.
1269+
bool stillNested = false, queued = false;
1270+
{
1271+
concurrency::LockGuard g(&deferredLock);
1272+
stillNested = handleDepth > 0;
1273+
if (stillNested)
1274+
queued = enqueueDeferredLocal(copy, src);
1275+
}
1276+
if (queued)
1277+
return;
1278+
if (!stillNested) {
1279+
// The drain finished first, so nothing would pick this up. Go through handleReceived()
1280+
// rather than dispatchReceived() so a loopback from its modules still defers.
1281+
handleReceived(copy, src);
1282+
packetPool.release(copy);
1283+
return;
1284+
}
1285+
}
12631286

12641287
// Pool exhausted or queue full: drop the deferral. Leak-free and degraded but safe - the
12651288
// packet still followed its normal non-loopback path (SHOULD_RELEASE, or the TX path for a
@@ -1278,29 +1301,39 @@ void Router::deliverLocal(meshtastic_MeshPacket *p, RxSource src)
12781301
*/
12791302
void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
12801303
{
1281-
handleDepth++;
1304+
{
1305+
concurrency::LockGuard g(&deferredLock);
1306+
handleDepth++;
12821307
#ifdef PIO_UNIT_TESTING
1283-
if (handleDepth > maxHandleDepthObserved)
1284-
maxHandleDepthObserved = handleDepth;
1308+
if (handleDepth > maxHandleDepthObserved)
1309+
maxHandleDepthObserved = handleDepth;
12851310
#endif
1311+
}
12861312

12871313
dispatchReceived(p, src);
12881314

1289-
// Only the outermost frame drains. Deferred packets were produced by modules sending from
1290-
// inside dispatchReceived()'s callModules(); process them here, after the triggering frame has
1291-
// unwound, so a second handleReceived() never sits on top of a module handler. handleDepth
1292-
// stays >= 1 through the drain, so a drained packet whose own modules send more loopback
1293-
// packets enqueues them for this same loop rather than recursing: the stack stays flat and
1294-
// processing is breadth-first.
1295-
if (handleDepth == 1) {
1315+
// Decide "am I the last frame" and drop the depth in one critical section. Splitting them lets
1316+
// two frames both read the same pre-decrement value, skip the drain, and strand the ring.
1317+
for (;;) {
12961318
DeferredLocal d;
1297-
while (dequeueDeferredLocal(d)) {
1298-
dispatchReceived(d.p, d.src);
1299-
packetPool.release(d.p);
1319+
{
1320+
concurrency::LockGuard g(&deferredLock);
1321+
if (handleDepth > 1) {
1322+
// Another frame is still live and will own the drain once it is last.
1323+
handleDepth--;
1324+
return;
1325+
}
1326+
if (!dequeueDeferredLocal(d)) {
1327+
// Last frame and nothing queued, so zero is reached only with the ring empty.
1328+
handleDepth--;
1329+
return;
1330+
}
13001331
}
1332+
// Depth stays at 1 across the drain, so a loopback from these modules defers instead of
1333+
// recursing, and dispatch runs outside the lock.
1334+
dispatchReceived(d.p, d.src);
1335+
packetPool.release(d.p);
13011336
}
1302-
1303-
handleDepth--;
13041337
}
13051338

13061339
void Router::dispatchReceived(meshtastic_MeshPacket *p, RxSource src)

src/mesh/Router.h

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "PacketHistory.h"
88
#include "PointerQueue.h"
99
#include "RadioInterface.h"
10+
#include "concurrency/LockGuard.h"
1011
#include "concurrency/OSThread.h"
1112
#include <memory>
1213

@@ -162,12 +163,8 @@ class Router : protected concurrency::OSThread, protected PacketHistory
162163
void perhapsHandleReceived(meshtastic_MeshPacket *p);
163164

164165
/**
165-
* Called from perhapsHandleReceived() - allows subclass message delivery behavior.
166-
* Handle any packet that is received by an interface on this node.
167-
* Note: some packets may merely being passed through this node and will be forwarded elsewhere.
168-
*
169-
* Note: this packet will never be called for messages sent/generated by this node.
170-
* Note: this method will free the provided packet.
166+
* Called from perhapsHandleReceived() for radio ingress and from deliverLocal() for our own
167+
* loopback, so p may be locally generated. Does NOT free p; the caller still owns it.
171168
*/
172169
void handleReceived(meshtastic_MeshPacket *p, RxSource src = RX_SRC_RADIO);
173170

@@ -192,6 +189,10 @@ class Router : protected concurrency::OSThread, protected PacketHistory
192189
/// so a locally-sent loopback packet must be deferred rather than handled synchronously.
193190
uint8_t handleDepth = 0;
194191

192+
/// Guards handleDepth and the deferred ring below. nRF52 drives the router from the BLE task as
193+
/// well as the loop task, so both are read-modify-written from two tasks.
194+
concurrency::Lock deferredLock;
195+
195196
/// A local loopback packet whose handleReceived() was deferred because it was produced from
196197
/// inside callModules(). The queue owns the packet; its RxSource travels with it so the drain
197198
/// dispatches it with the origin the sender intended (RX_SRC_LOCAL stays local).
@@ -210,8 +211,10 @@ class Router : protected concurrency::OSThread, protected PacketHistory
210211
uint8_t deferredLocalCount = 0; // entries currently queued
211212

212213
/// Queue a deferred local packet. Returns false (and queues nothing) when full.
214+
/// Caller must hold deferredLock: the enqueue decision is atomic with the drain's depth update.
213215
bool enqueueDeferredLocal(meshtastic_MeshPacket *p, RxSource src);
214216
/// Pop the oldest deferred local packet into out. Returns false when empty.
217+
/// Caller must hold deferredLock.
215218
bool dequeueDeferredLocal(DeferredLocal &out);
216219

217220
/** Frees the provided packet, and generates a NAK indicating the specifed error while sending */

0 commit comments

Comments
 (0)