refactor(mqtt): optimize MQTT status and diagnostic reply formatting

Replace multiple snprintf calls with a single replyAppendf function to
improve buffer management and prevent overflow. This change enhances
the readability and maintainability of the code while ensuring that
the reply buffer is handled safely across various MQTT status and
diagnostic replies.
This commit is contained in:
agessaman committed 2026-07-20 10:00:48 -07:00
1 parent 2019a8c0ca
commit fed561113a
3 files changed
+208 -29

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <stddef.h>
#include <stdarg.h>
#include <stdio.h>
// Bounded, clamping printf-append for the fixed-size CLI reply buffers used by
// MQTTBridge's status/stats/diag formatters. Factored out of MQTTBridge so the
// bound is provable on the host instead of holding "by input-size accident"
// (see the A1 out-of-bounds-write finding, 2026-07-19 MQTT observer review).
//
// Appends `fmt...` to `buf` starting at offset `*pos`, then advances `*pos` by
// the number of characters actually written, CLAMPED to [0, bufsize-1]. Because
// snprintf returns the *would-have-written* length, the naive
// `*pos += snprintf(buf + *pos, bufsize - *pos, ...)` idiom can push `*pos` past
// `bufsize` after a truncated append; the next append then computes
// `bufsize - *pos` as a huge size_t and `buf + *pos` past the end, writing out of
// bounds. Clamping `*pos` here makes every subsequent append a safe no-op once
// the buffer is full.
//
// buf is always left NUL-terminated (vsnprintf guarantees this for bufsize > 0).
// No-ops on null buf/pos or bufsize == 0. A negative incoming *pos is treated as
// 0. Typical use: `int pos = 0;` then a sequence of replyAppendf() calls.
static inline void replyAppendf(char* buf, size_t bufsize, int* pos, const char* fmt, ...) {
if (!buf || !pos || bufsize == 0) return;
if (*pos < 0) *pos = 0;
// Full: no room for anything but the terminator. Keep buf NUL-terminated and
// leave *pos pinned at the last writable index.
if ((size_t)*pos >= bufsize - 1) {
*pos = (int)bufsize - 1;
buf[*pos] = '\0';
return;
}
size_t remaining = bufsize - (size_t)*pos;
va_list args;
va_start(args, fmt);
int n = vsnprintf(buf + *pos, remaining, fmt, args);
va_end(args);
// Encoding error: vsnprintf still NUL-terminated buf + *pos; leave *pos as-is.
if (n < 0) return;
*pos += n;
if ((size_t)*pos >= bufsize) *pos = (int)bufsize - 1; // clamp truncated append
}
+33 -29
View File
@@ -2,6 +2,7 @@
#include "../MQTTConnectionPolicy.h"
#include "../MQTTMessageBuilder.h"
#include "../MQTTPacketQueuePolicy.h"
#include "../MQTTReplyFormat.h"
#include "../MQTTRuntimeBufferLifecycle.h"
#include "../MQTTTopicRouter.h"
#include "../TxtDataHelpers.h"
@@ -241,8 +242,11 @@ void MQTTBridge::formatMqttStatusReply(char* buf, size_t bufsize, const MQTTPref
q = b->_queue_count;
#endif
int pos = snprintf(buf, bufsize, "> msgs: %s", msgs);
for (int i = 0; i < RUNTIME_MQTT_SLOTS && pos < (int)bufsize - 1; i++) {
// replyAppendf clamps pos into the buffer on every call, so no per-append
// guard or trailing clamp is needed (see MQTTReplyFormat.h / A1).
int pos = 0;
replyAppendf(buf, bufsize, &pos, "> msgs: %s", msgs);
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
const MQTTSlot& slot = b->_slots[i];
const char* name = nullptr;
const char* state = nullptr;
@@ -265,17 +269,13 @@ void MQTTBridge::formatMqttStatusReply(char* buf, size_t bufsize, const MQTTPref
name = slot.preset ? slot.preset->name : "custom";
state = "disc";
}
pos += snprintf(buf + pos, bufsize - pos, ", %d: %s (%s)", i + 1, name, state);
replyAppendf(buf, bufsize, &pos, ", %d: %s (%s)", i + 1, name, state);
}
// snprintf returns the would-be length, so a full buffer can push pos past
// bufsize; clamp before the remaining appends so bufsize - pos can't underflow.
if (pos >= (int)bufsize) pos = (int)bufsize - 1;
pos += snprintf(buf + pos, bufsize - pos, ", q:%d", q);
if (pos >= (int)bufsize) pos = (int)bufsize - 1;
replyAppendf(buf, bufsize, &pos, ", q:%d", q);
#if defined(WITH_MQTT_NEIGHBORS)
// Periodic neighbors: time to next publish + how the last one went.
if (obs && obs->mqtt_neighbors_enabled && pos < (int)bufsize - 1) {
if (obs && obs->mqtt_neighbors_enabled) {
char when[16];
switch (b->_neighbors_phase.load(std::memory_order_relaxed)) {
case NBR_ACTIVE: strcpy(when, "active"); break;
@@ -291,7 +291,7 @@ void MQTTBridge::formatMqttStatusReply(char* buf, size_t bufsize, const MQTTPref
case NBR_RESULT_FAIL: last = "failed"; break;
default: last = "none"; break;
}
snprintf(buf + pos, bufsize - pos, ", nbr: %s/%s", when, last);
replyAppendf(buf, bufsize, &pos, ", nbr: %s/%s", when, last);
}
#endif
}
@@ -324,14 +324,15 @@ void MQTTBridge::formatMqttStatsReply(char* buf, size_t bufsize) {
if (b->_slots[i].client) outbox_total += b->_slots[i].client->getOutboxSize();
}
int pos = snprintf(buf, bufsize, "> Free=%d Max=%d q:%d/%d Outbox=%u |",
(int)ESP.getFreeHeap(), (int)ESP.getMaxAllocHeap(),
q, MAX_QUEUE_SIZE, (unsigned)outbox_total);
for (int i = 0; i < RUNTIME_MQTT_SLOTS && pos < (int)bufsize - 1; i++) {
int pos = 0;
replyAppendf(buf, bufsize, &pos, "> Free=%d Max=%d q:%d/%d Outbox=%u |",
(int)ESP.getFreeHeap(), (int)ESP.getMaxAllocHeap(),
q, MAX_QUEUE_SIZE, (unsigned)outbox_total);
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
if (!b->_slots[i].enabled || !b->_slots[i].client) continue;
pos += snprintf(buf + pos, bufsize - pos, " s%d=%lu/%lu", i + 1,
b->_slots[i].client->getPublishOk(),
b->_slots[i].client->getPublishErr());
replyAppendf(buf, bufsize, &pos, " s%d=%lu/%lu", i + 1,
b->_slots[i].client->getPublishOk(),
b->_slots[i].client->getPublishErr());
}
}
@@ -467,18 +468,21 @@ void MQTTBridge::formatSlotDiagReply(char* buf, size_t bufsize, int slot_index)
state = "disc";
}
int pos = snprintf(buf, bufsize, "> mqtt%d: %s", slot_index + 1, state);
// replyAppendf clamps pos on every call, so the chained appends below can't
// walk past the reply buffer even if the accumulated text exceeds it (A1).
int pos = 0;
replyAppendf(buf, bufsize, &pos, "> mqtt%d: %s", slot_index + 1, state);
if (slot.disconnect_count > 0) {
pos += snprintf(buf + pos, bufsize - pos, ", dc:%lu", (unsigned long)slot.disconnect_count);
replyAppendf(buf, bufsize, &pos, ", dc:%lu", (unsigned long)slot.disconnect_count);
if (slot.first_disconnect_time > 0) {
unsigned long first_disc_age_sec = (millis() - slot.first_disconnect_time) / 1000;
pos += snprintf(buf + pos, bufsize - pos, ", first_disc:%lus", first_disc_age_sec);
replyAppendf(buf, bufsize, &pos, ", first_disc:%lus", first_disc_age_sec);
}
}
// If connected with no errors, we're done
if (slot.connected && slot.last_error_time == 0) {
snprintf(buf + pos, bufsize - pos, ", no errors");
replyAppendf(buf, bufsize, &pos, ", no errors");
return;
}
@@ -488,30 +492,30 @@ void MQTTBridge::formatSlotDiagReply(char* buf, size_t bufsize, int slot_index)
if (slot.last_tls_err != 0) {
const char* desc = tlsErrorStr(slot.last_tls_err);
if (desc) {
pos += snprintf(buf + pos, bufsize - pos, ", %s (0x%04X)", desc, (unsigned)slot.last_tls_err);
replyAppendf(buf, bufsize, &pos, ", %s (0x%04X)", desc, (unsigned)slot.last_tls_err);
} else {
pos += snprintf(buf + pos, bufsize - pos, ", tls:0x%04X", (unsigned)slot.last_tls_err);
replyAppendf(buf, bufsize, &pos, ", tls:0x%04X", (unsigned)slot.last_tls_err);
}
}
// mbedTLS stack error (shown as negative hex per convention)
if (slot.last_tls_stack_err != 0) {
pos += snprintf(buf + pos, bufsize - pos, ", mbedtls:-0x%04X", (unsigned)(-slot.last_tls_stack_err));
replyAppendf(buf, bufsize, &pos, ", mbedtls:-0x%04X", (unsigned)(-slot.last_tls_stack_err));
}
// Socket errno
if (slot.last_sock_errno != 0) {
pos += snprintf(buf + pos, bufsize - pos, ", sock:%d", slot.last_sock_errno);
replyAppendf(buf, bufsize, &pos, ", sock:%d", slot.last_sock_errno);
}
// Time ago
unsigned long ago_sec = (millis() - slot.last_error_time) / 1000;
if (ago_sec < 60) {
snprintf(buf + pos, bufsize - pos, ", %lus ago", ago_sec);
replyAppendf(buf, bufsize, &pos, ", %lus ago", ago_sec);
} else if (ago_sec < 3600) {
snprintf(buf + pos, bufsize - pos, ", %lum ago", ago_sec / 60);
replyAppendf(buf, bufsize, &pos, ", %lum ago", ago_sec / 60);
} else {
snprintf(buf + pos, bufsize - pos, ", %luh ago", ago_sec / 3600);
replyAppendf(buf, bufsize, &pos, ", %luh ago", ago_sec / 3600);
}
} else if (!slot.connected) {
snprintf(buf + pos, bufsize - pos, ", no error info");
replyAppendf(buf, bufsize, &pos, ", no error info");
}
}