Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions src/mesh/RadioInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1518,15 +1518,17 @@ size_t RadioInterface::beginSending(meshtastic_MeshPacket *p)

// if the sender nodenum is zero, that means uninitialized
assert(radioBuffer.header.from);
// Runtime packet payload size bounds check against radioBuffer to prevent overflow in memcpy()
if (static_cast<size_t>(p->encrypted.size) > sizeof(radioBuffer.payload)) {
LOG_ERROR("Packet payload size %u exceeds radioBuffer capacity %u", static_cast<unsigned>(p->encrypted.size),
static_cast<unsigned>(sizeof(radioBuffer.payload)));
packetPool.release(p);
return 0;

// Oversize is rejected at the radio queue in Router::send(); clamp rather than fail here so this
// stays a call that always succeeds, with no failure return for startSend() to unwind.
size_t payloadLen = p->encrypted.size;
if (payloadLen > MAX_RADIO_PAYLOAD_LEN) {
LOG_ERROR("Payload %u exceeds radioBuffer capacity %u, truncate", (unsigned)payloadLen, (unsigned)MAX_RADIO_PAYLOAD_LEN);
payloadLen = MAX_RADIO_PAYLOAD_LEN;
}
memcpy(radioBuffer.payload, p->encrypted.bytes, p->encrypted.size);

memcpy(radioBuffer.payload, p->encrypted.bytes, payloadLen);

sendingPacket = p;
return p->encrypted.size + sizeof(PacketHeader);
return payloadLen + sizeof(PacketHeader);
}
5 changes: 5 additions & 0 deletions src/mesh/RadioInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ typedef struct {

} RadioBuffer;

/// On-air ceiling for MeshPacket.encrypted. RadioBuffer holds one byte more, but the PHY caps a whole
/// frame at MAX_LORA_PAYLOAD_LEN, so the header comes out of the same budget (matches perhapsEncode).
constexpr size_t MAX_RADIO_PAYLOAD_LEN = MAX_LORA_PAYLOAD_LEN - sizeof(PacketHeader);
static_assert(MAX_RADIO_PAYLOAD_LEN < sizeof(RadioBuffer::payload), "payload ceiling must fit the buffer");

/**
* Basic operations all radio chipsets must implement.
*
Expand Down
74 changes: 24 additions & 50 deletions src/mesh/RadioLibInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@
#include "MeshTypes.h"
#include "NodeDB.h"
#include "PowerMon.h"
#include "RadioTxHook.h"
#include "SPILock.h"
#include "Throttle.h"
#include "UptimeClock.h"
#include "configuration.h"
#include "error.h"
#include "main.h"
#include "mesh-pb-constants.h"
#if !MESHTASTIC_EXCLUDE_BEACON
#include "modules/MeshBeaconModule.h"
#endif
#include <pb_decode.h>
#include <pb_encode.h>

Expand Down Expand Up @@ -248,8 +246,10 @@ bool RadioLibInterface::isSending()
bool RadioLibInterface::cancelSending(NodeNum from, PacketId id)
{
auto p = txQueue.remove(from, id);
if (p)
if (p) {
RadioTxHooks::packetReleased(this, p);
packetPool.release(p); // free the packet we just removed
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

bool result = (p != NULL);
LOG_DEBUG("cancelSending id=0x%08x, removed=%d", id, result);
Expand Down Expand Up @@ -408,14 +408,10 @@ void RadioLibInterface::onNotify(uint32_t notification)
switch (notification) {
case ISR_TX:
handleTransmitInterrupt(); // completeSending() already restored the radio to the home config
#if !MESHTASTIC_EXCLUDE_BEACON
// Pre-switch the radio to the NEXT queued packet's beacon config (no-op for normal traffic).
// Not required for correctness - TRANSMIT_DELAY_COMPLETED would switch before CAD anyway - but
// doing it here lets the next beacon skip the switch-only delay cycle and, more importantly,
// keeps the post-TX listen window (and the CAD/LBT that follows) on the channel we're about to
// transmit on. Only engages when the next packet is itself a beacon - exactly when we want it.
MeshBeaconModule::reconfigureForBeaconTX(this, txQueue.getFront());
#endif
// Let the hooks pre-stage the radio for the NEXT queued packet. Not required for correctness -
// TRANSMIT_DELAY_COMPLETED asks again before the scan, which is where the answer is acted on -
// but it keeps the post-TX listen window on the channel we are about to transmit on.
(void)RadioTxHooks::beforeTransmit(this, txQueue.getFront());
Comment thread
NomDeTom marked this conversation as resolved.
startReceive();
setTransmitDelay();
break;
Expand Down Expand Up @@ -443,25 +439,20 @@ void RadioLibInterface::onNotify(uint32_t notification)
if (txp->tx_after && !Throttle::deadlinePassedAt(now, txp->tx_after)) {
// There's still some delay pending on this packet, so resume waiting for it to elapse
notifyLater(txp->tx_after - now, TRANSMIT_DELAY_COMPLETED, txTimerOverwrite);
#if !MESHTASTIC_EXCLUDE_BEACON
} else if (MeshBeaconModule::beaconTxConfigInvalid(txp)) {
// The beacon's target radio config is invalid (bad preset/region, or an
// unlicensed node keying up on a ham-only region). Drop the packet - never
// transmit it on the current (home) config - and move on to the next queued packet.
LOG_DEBUG("Beacon: invalid TX radio config, drop packet 0x%08x", txp->id);
} else if (const RadioTxHook::PreTxAction action = RadioTxHooks::beforeTransmit(this, txp);
action == RadioTxHook::PRETX_DROP) {
// A module refuses this packet on the radio config we are holding: drop it rather
// than transmit it, and move on to the next queued packet.
meshtastic_MeshPacket *bad = txQueue.dequeue();
MeshBeaconModule::clearTargetRadioSettings(bad);
LOG_DEBUG("Drop Tx packet 0x%08x, refused before transmit", bad->id);
RadioTxHooks::packetReleased(this, bad);
packetPool.release(bad);
setTransmitDelay();
} else if (MeshBeaconModule::reconfigureForBeaconTX(this, txp)) {
setTransmitDelay();
#endif
} else if (action == RadioTxHook::PRETX_DEFER) {
setTransmitDelay(); // the radio config moved, so re-run the delay and scan on it
} else {
if (isChannelActive()) { // check if there is currently a LoRa packet on the channel
#if !MESHTASTIC_EXCLUDE_BEACON
if (!MeshBeaconModule::hasTargetRadioSettings(txp))
#endif
{
if (!RadioTxHooks::holdsRadio(txp)) {
startReceive(); // try receiving this packet, afterwards we'll be trying to transmit again
}
setTransmitDelay();
Expand Down Expand Up @@ -561,6 +552,7 @@ bool RadioLibInterface::removePendingTXPacket(NodeNum from, PacketId id, uint32_
meshtastic_MeshPacket *p = txQueue.remove(from, id, true, true, hop_limit_lt);
if (p) {
LOG_DEBUG("Drop pending-TX packet 0x%08x, hop limit %d", p->id, p->hop_limit);
RadioTxHooks::packetReleased(this, p);
packetPool.release(p);
return true;
}
Expand Down Expand Up @@ -595,15 +587,13 @@ void RadioLibInterface::completeSending()
if (!isFromUs(p))
txRelay++;
printPacket("Completed sending", p);
#if !MESHTASTIC_EXCLUDE_BEACON
MeshBeaconModule::clearTargetRadioSettings(p);
#endif
// Keep this inside `if (p)`: completeSending() also runs on every setStandby(), where a hook
// undoing its own pre-TX switch would recurse back through reconfigure().
RadioTxHooks::packetReleased(this, p);

// We are done sending that packet, release it
packetPool.release(p);
}
#if !MESHTASTIC_EXCLUDE_BEACON
MeshBeaconModule::reconfigureForBeaconTX(this, nullptr);
#endif
}

void RadioLibInterface::handleReceiveInterrupt()
Expand Down Expand Up @@ -771,30 +761,14 @@ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp)
channel scan and actual transmit as low as possible to avoid collisions. */
if (disabled || !config.lora.tx_enabled) {
LOG_WARN("Drop Tx packet: LoRa Tx disabled");
#if !MESHTASTIC_EXCLUDE_BEACON
// This packet may have already triggered a beacon radio switch in TRANSMIT_DELAY_COMPLETED;
// since it never reaches completeSending() here, restore the radio so it isn't left on the
// beacon config (which would also break RX on the home channel).
MeshBeaconModule::clearTargetRadioSettings(txp);
MeshBeaconModule::reconfigureForBeaconTX(this, nullptr);
#endif
// Never reaches completeSending(), so any per-packet radio state has to be released here.
RadioTxHooks::packetReleased(this, txp);
packetPool.release(txp);
return false;
} else {
configHardwareForSend(); // must be after setStandby

#if !MESHTASTIC_EXCLUDE_BEACON
MeshBeaconModule::clearTargetRadioSettings(txp);
#endif
size_t numbytes = beginSending(txp);
if (numbytes == 0) {
if (!sendingPacket) {
completeSending();
powerMon->clearState(meshtastic_PowerMon_State_Lora_TXOn);
startReceive();
}
return false;
}

int res = iface->startTransmit((uint8_t *)&radioBuffer, numbytes);
if (res != RADIOLIB_ERR_NONE) {
Expand Down
43 changes: 43 additions & 0 deletions src/mesh/RadioTxHook.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#include "RadioTxHook.h"

RadioTxHook *RadioTxHook::hookList = nullptr;

RadioTxHook::RadioTxHook()
{
nextHook = hookList;
hookList = this;
}

RadioTxHook::~RadioTxHook()
{
for (RadioTxHook **slot = &hookList; *slot; slot = &(*slot)->nextHook) {
if (*slot == this) {
*slot = nextHook;
break;
}
}
}

RadioTxHook::PreTxAction RadioTxHooks::beforeTransmit(RadioInterface *iface, meshtastic_MeshPacket *p)
{
for (RadioTxHook *h = RadioTxHook::hookList; h; h = h->nextHook) {
const RadioTxHook::PreTxAction action = h->beforeTransmit(iface, p);
if (action != RadioTxHook::PRETX_SEND)
return action;
}
return RadioTxHook::PRETX_SEND;
}

bool RadioTxHooks::holdsRadio(const meshtastic_MeshPacket *p)
{
for (RadioTxHook *h = RadioTxHook::hookList; h; h = h->nextHook)
if (h->holdsRadio(p))
return true;
return false;
}

void RadioTxHooks::packetReleased(RadioInterface *iface, const meshtastic_MeshPacket *p)
{
for (RadioTxHook *h = RadioTxHook::hookList; h; h = h->nextHook)
h->packetReleased(iface, p);
}
50 changes: 50 additions & 0 deletions src/mesh/RadioTxHook.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#pragma once

#include "MeshTypes.h"

class RadioInterface;

/**
* A module's hook into the radio driver's per-packet TX lifecycle.
*
* The driver knows this interface and nothing about who implements it: a module needing per-packet
* radio state (MeshBeacon's preset switch) subclasses this, and one instance registers itself for
* the life of the program.
*/
class RadioTxHook
{
friend class RadioTxHooks;

RadioTxHook *nextHook = nullptr;
static RadioTxHook *hookList;

public:
/// What the driver should do with the packet at the head of the TX queue.
enum PreTxAction {
PRETX_SEND, ///< nothing pending, transmit as usual
PRETX_DEFER, ///< the radio config changed, re-run the transmit delay before sending
PRETX_DROP ///< this packet must not go out on the current radio config
};

RadioTxHook();
virtual ~RadioTxHook();

/// The driver is about to transmit p (NULL when the queue is empty): set up any state it needs.
virtual PreTxAction beforeTransmit(RadioInterface *iface, meshtastic_MeshPacket *p) { return PRETX_SEND; }

/// True while p needs the radio left on its own config, so the driver must not listen instead.
virtual bool holdsRadio(const meshtastic_MeshPacket *p) { return false; }

/// The driver is done with p - sent, cancelled or dropped. Release anything held for it.
virtual void packetReleased(RadioInterface *iface, const meshtastic_MeshPacket *p) {}
};

/// Driver-side fan-out over the registered hooks; every call is a no-op when none are registered.
class RadioTxHooks
{
public:
/// The first hook not returning PRETX_SEND decides, and the rest are not consulted.
static RadioTxHook::PreTxAction beforeTransmit(RadioInterface *iface, meshtastic_MeshPacket *p);
static bool holdsRadio(const meshtastic_MeshPacket *p);
static void packetReleased(RadioInterface *iface, const meshtastic_MeshPacket *p);
};
9 changes: 9 additions & 0 deletions src/mesh/Router.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,15 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
}
#endif

// Only already-encrypted frames (relayed, phone-sourced) reach here oversized; perhapsEncode()
// bounds everything it encodes. No NAK: p->channel is a wire hash by now, not an index.
if (p->encrypted.size > MAX_RADIO_PAYLOAD_LEN) {
LOG_WARN("Drop 0x%08x: payload %u exceeds radio capacity %u", p->id, (unsigned)p->encrypted.size,
(unsigned)MAX_RADIO_PAYLOAD_LEN);
packetPool.release(p);
return meshtastic_Routing_Error_TOO_LARGE;
}

assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside)
return iface->send(p);
}
Expand Down
Loading
Loading