fix: don't boot-loop on a corrupted coredump

setup() prints the stored coredump summary on every boot and trusted its
backtrace depth. A half-written dump reads back as the 0xa5 stack fill, so
depth was 0xa5a5a5a5: the loop read past bt[16], snprintf ran past its
256-byte buffer once the size went negative, and the interrupt watchdog
reset the board. The dump stays in flash, so every boot hit it again
(seen 2026-10-21: 162 TG1WDT resets in a row, each stopping after "Cause:").

formatBacktrace() in Utils reads at most bt[]'s 16 frames and stops when the
buffer is full, and the line notes IDF's corrupted flag. The task name is
printed at most 16 chars, since exc_task isn't always terminated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-21 12:07:53 +02:00
parent b74f382650
commit 54641af039
4 changed files with 53 additions and 7 deletions

View file

@ -78,16 +78,15 @@ void setup() {
#define CRASH_LOG(fmt, ...) do { if(newCrash) ESP_LOGW(TAG, fmt, ##__VA_ARGS__); else ESP_LOGI(TAG, fmt, ##__VA_ARGS__); } while(0)
if(newCrash) ESP_LOGW(TAG, "*** Crash coredump from the last boot ***");
else ESP_LOGI(TAG, "Stored coredump from an earlier crash");
CRASH_LOG(" Task: %s", summary.exc_task);
// exc_task isn't always terminated.
CRASH_LOG(" Task: %.*s", (int)sizeof(summary.exc_task), summary.exc_task);
CRASH_LOG(" PC: 0x%08x", summary.exc_pc);
#ifdef CONFIG_IDF_TARGET_ARCH_XTENSA
CRASH_LOG(" Cause: %d", summary.ex_info.exc_cause);
char bt_buf[256] = {0};
int pos = 0;
for (int i = 0; i < summary.exc_bt_info.depth; i++) {
pos += snprintf(bt_buf + pos, sizeof(bt_buf) - pos, " 0x%08x", summary.exc_bt_info.bt[i]);
}
CRASH_LOG(" Backtrace:%s", bt_buf);
char bt_buf[256];
formatBacktrace(bt_buf, sizeof(bt_buf), summary.exc_bt_info.bt,
sizeof(summary.exc_bt_info.bt) / sizeof(summary.exc_bt_info.bt[0]), summary.exc_bt_info.depth);
CRASH_LOG(" Backtrace:%s%s", bt_buf, summary.exc_bt_info.corrupted ? " (corrupted)" : "");
#elif CONFIG_IDF_TARGET_ARCH_RISCV
CRASH_LOG(" Cause: %d", summary.ex_info.mcause);
CRASH_LOG(" MTVAL: 0x%08x RA: 0x%08x SP: 0x%08x",

View file

@ -147,6 +147,18 @@ void RssiBroadcast::sent(bool broadcast, int rssi, int channel, uint32_t now) {
bool worthRoaming(bool connected, int currentRssi, int apRssi, int minGain) {
return !connected || apRssi >= currentRssi + minGain;
}
void formatBacktrace(char *buf, size_t size, const uint32_t *bt, size_t maxFrames, uint32_t depth) {
// depth comes from flash. A half-written dump reads back as 0xa5a5a5a5,
// so never trust it past bt[] or the buffer.
buf[0] = '\0';
size_t pos = 0;
size_t frames = depth < maxFrames ? depth : maxFrames;
for (size_t i = 0; i < frames && pos < size - 1; i++) {
int n = snprintf(buf + pos, size - pos, " 0x%08x", (unsigned int)bt[i]);
if (n < 0) break;
pos += (size_t)n;
}
}
const char *wifiAuthModeName(uint8_t authMode) {
switch(authMode) {
case 0: return "OPEN";

View file

@ -82,6 +82,9 @@ enum class heap_emit_t : uint8_t { none, client, broadcast, room };
// num is the socket client asked for, 255 for everyone. A broadcast goes out
// every intervalMs; in between only the clients in room 0 get one.
heap_emit_t heapEmitTarget(uint8_t num, uint32_t sinceLastMs, uint32_t intervalMs, bool roomHasClients);
// A coredump backtrace as " 0x%08x" per frame. bt holds maxFrames entries and
// depth is how many the dump claims are valid.
void formatBacktrace(char *buf, size_t size, const uint32_t *bt, size_t maxFrames, uint32_t depth);
// How well loop() keeps up: the passes it made per second and its longest
// pass, over the window since the last memStatus broadcast. Only the loop
// task calls record(), rate(), maxUs() and publish(). publish() closes the

View file

@ -257,6 +257,35 @@ static void test_worthRoaming_any_ap_while_disconnected() {
TEST_ASSERT_TRUE(worthRoaming(false, 0, -90, 6));
}
/*********************************************************************
* formatBacktrace
********************************************************************/
static void test_formatBacktrace_frames_the_dump_holds() {
const uint32_t bt[16] = {0x40083c19, 0x4008e42d, 0x00000011};
char buf[256];
formatBacktrace(buf, sizeof(buf), bt, 16, 3);
TEST_ASSERT_EQUAL_STRING(" 0x40083c19 0x4008e42d 0x00000011", buf);
}
static void test_formatBacktrace_garbage_depth_stays_in_bounds() {
// A half-written dump reads back as the 0xa5 stack fill: depth 0xa5a5a5a5.
// setup() looped that many times and overran bt[] and its buffer.
struct { uint32_t bt[16]; uint32_t depth; } summary;
memset(&summary, 0xa5, sizeof(summary));
struct { char buf[256]; char guard[16]; } out;
memset(out.guard, 'G', sizeof(out.guard));
formatBacktrace(out.buf, sizeof(out.buf), summary.bt, 16, summary.depth);
TEST_ASSERT_EQUAL(16 * 11, strlen(out.buf));
for (size_t i = 0; i < sizeof(out.guard); i++) TEST_ASSERT_EQUAL_CHAR('G', out.guard[i]);
}
static void test_formatBacktrace_stops_when_the_buffer_is_full() {
const uint32_t bt[16] = {0};
struct { char buf[30]; char guard[16]; } out;
memset(out.guard, 'G', sizeof(out.guard));
formatBacktrace(out.buf, sizeof(out.buf), bt, 16, 16);
TEST_ASSERT_EQUAL(sizeof(out.buf) - 1, strlen(out.buf));
for (size_t i = 0; i < sizeof(out.guard); i++) TEST_ASSERT_EQUAL_CHAR('G', out.guard[i]);
}
/*********************************************************************
* wifiAuthModeName
********************************************************************/
@ -384,6 +413,9 @@ int main(int argc, char **argv) {
RUN_TEST(test_worthRoaming_needs_the_full_gain);
RUN_TEST(test_worthRoaming_never_to_a_weaker_ap);
RUN_TEST(test_worthRoaming_any_ap_while_disconnected);
RUN_TEST(test_formatBacktrace_frames_the_dump_holds);
RUN_TEST(test_formatBacktrace_garbage_depth_stays_in_bounds);
RUN_TEST(test_formatBacktrace_stops_when_the_buffer_is_full);
RUN_TEST(test_wifiAuthModeName_matches_the_idf_enum);
RUN_TEST(test_wifiAuthModeName_unknown_past_the_shared_values);
RUN_TEST(test_heapEmitTarget_one_client_always_gets_it);