@@ -1249,7 +1249,12 @@ bool Router::dequeueDeferredLocal(DeferredLocal &out)
12491249void 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 */
12791302void 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
13061339void Router::dispatchReceived (meshtastic_MeshPacket *p, RxSource src)
0 commit comments