fix(security): close OOB read in path-decoding callers (BLE + LoRa-anon)

Both mesh::Packet::writePath and ::copyPath did a raw memcpy of the
decoded hash_count*hash_size bytes from src to dest with no bound on
src. Two call sites used phone-supplied or LoRa-anon-supplied buffers
where the path_len byte was attacker-controlled:

  - CompanionMesh CMD_SEND_CHANNEL_DATA accepted len>=4 and called
    writePath with no src bound; a paired phone could leak up to ~65
    bytes of syswq stack into the outgoing LoRa channel-data frame.

  - RepeaterMesh handleAnonRegionsReq / handleAnonOwnerReq /
    handleAnonClockReq read reply_path_len from an unauthenticated
    LoRa anon-request payload and called copyPath without any src
    bound. Any LoRa neighbor could leak repeater stack into the
    reply path.

Hardened the API: both functions now require an explicit src_len
and reject (return 0) when the decoded byte count exceeds it.
Updated all 14 call sites across Packet/Mesh/Dispatcher/BaseChatMesh/
CompanionMesh/RepeaterMesh. Trusted callers (internal MAX_PATH_SIZE
buffers) pass MAX_PATH_SIZE; untrusted callers pass real remaining
length. Added len-5 plumbing through the anon-handler signatures.

CMD_SEND_CHANNEL_DATA also gained a local len>=5 + path_bytes
sanity check for early rejection.
This commit is contained in:
liquidraver committed 2026-05-20 11:52:39 +02:00
1 parent d7e420bf2f
commit bd1e022e88
8 files changed
+86 -30

No files matched your search

+2 -1
View File
@@ -437,7 +437,8 @@ void Dispatcher::checkSend()
memcpy(&raw[len], &outbound->transport_codes[1], 2); len += 2;
}
raw[len++] = outbound->path_len;
len += Packet::writePath(&raw[len], outbound->path, outbound->path_len);
/* Trusted source: outbound->path is MAX_PATH_SIZE-sized. */
len += Packet::writePath(&raw[len], outbound->path, MAX_PATH_SIZE, outbound->path_len);
if (len + outbound->payload_len > MAX_TRANS_UNIT) {
LOG_ERR("checkSend: packet too large len=%d+%d > %d", len, outbound->payload_len, MAX_TRANS_UNIT);
+11 -4
View File
@@ -112,7 +112,8 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet *pkt)
if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) {
Packet tmp;
tmp.header = pkt->header;
tmp.path_len = Packet::copyPath(tmp.path, pkt->path, pkt->path_len);
/* Trusted source: pkt->path is MAX_PATH_SIZE-sized. */
tmp.path_len = Packet::copyPath(tmp.path, pkt->path, MAX_PATH_SIZE, pkt->path_len);
tmp.payload_len = pkt->payload_len - 1;
memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
if (!_tables->hasSeen(&tmp)) {
@@ -130,7 +131,8 @@ void Mesh::routeDirectRecvAcks(Packet *packet, uint32_t delay_millis)
memcpy(&crc, packet->payload, 4);
Packet *a2 = createAck(crc);
if (a2) {
a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len);
/* Trusted source: packet->path is MAX_PATH_SIZE-sized. */
a2->path_len = Packet::copyPath(a2->path, packet->path, MAX_PATH_SIZE, packet->path_len);
a2->header &= ~PH_ROUTE_MASK;
a2->header |= ROUTE_TYPE_DIRECT;
sendPacket(a2, 0, delay_millis);
@@ -385,7 +387,8 @@ DispatcherAction Mesh::onRecvPacket(Packet *pkt)
if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) {
Packet tmp;
tmp.header = pkt->header;
tmp.path_len = Packet::copyPath(tmp.path, pkt->path, pkt->path_len);
/* Trusted source: pkt->path is MAX_PATH_SIZE-sized. */
tmp.path_len = Packet::copyPath(tmp.path, pkt->path, MAX_PATH_SIZE, pkt->path_len);
tmp.payload_len = pkt->payload_len - 1;
memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
@@ -549,7 +552,11 @@ void Mesh::sendDirect(Packet *packet, const uint8_t *path, uint8_t path_len, uin
packet->path_len = 0;
pri = 5;
} else {
packet->path_len = Packet::copyPath(packet->path, path, path_len);
/* path is caller-supplied; existing contract is that the caller has
* ensured at least the decoded path-byte-count is readable. Pass
* MAX_PATH_SIZE as the upper bound — this preserves existing
* behavior while making the API explicit. */
packet->path_len = Packet::copyPath(packet->path, path, MAX_PATH_SIZE, path_len);
if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
pri = 1;
} else {
+9 -5
View File
@@ -24,21 +24,24 @@ bool Packet::isValidPathLen(uint8_t path_len)
return hash_count * hash_size <= MAX_PATH_SIZE;
}
size_t Packet::writePath(uint8_t *dest, const uint8_t *src, uint8_t path_len)
size_t Packet::writePath(uint8_t *dest, const uint8_t *src, size_t src_len, uint8_t path_len)
{
uint8_t hash_count = path_len & 63;
uint8_t hash_size = (path_len >> 6) + 1;
size_t len = hash_count * hash_size;
if (len > MAX_PATH_SIZE) {
return 0; // Error
return 0; // Decoded path exceeds max
}
if (len > src_len) {
return 0; // Would read past source buffer (caller-supplied bound)
}
memcpy(dest, src, len);
return len;
}
uint8_t Packet::copyPath(uint8_t *dest, const uint8_t *src, uint8_t path_len)
uint8_t Packet::copyPath(uint8_t *dest, const uint8_t *src, size_t src_len, uint8_t path_len)
{
size_t written = writePath(dest, src, path_len);
size_t written = writePath(dest, src, src_len, path_len);
return written > 0 ? path_len : 0;
}
@@ -73,7 +76,8 @@ uint8_t Packet::writeTo(uint8_t dest[]) const
memcpy(&dest[i], &transport_codes[1], 2); i += 2;
}
dest[i++] = path_len;
i += writePath(&dest[i], path, path_len);
/* Trusted source: Packet::path is MAX_PATH_SIZE-sized. */
i += writePath(&dest[i], path, MAX_PATH_SIZE, path_len);
memcpy(&dest[i], payload, payload_len); i += payload_len;
return i;
}