When rx_buf_ is empty during the client hello phase of the noise
handshake, rx_buf_.data() can return nullptr. Passing nullptr to
std::memcpy as the source argument is undefined behavior even when
the size is 0. Guard the memcpy with a size check.
Found by UndefinedBehaviorSanitizer in #14718.
Version the crash data by encoding the version in the magic value
(upper 16 bits = 0xDEAD sentinel, lower 16 bits = version). This
allows safely changing the scratch register layout in future firmware
without misinterpreting old crash data, and costs zero scratch
registers (we only have 8).
Add USE_RP2040_CRASH_HANDLER define and guard all call sites so
the crash handler can be conditionally compiled, matching the ESP32
crash handler pattern.
Add crash_handler_has_data() so callers can check for crash data
without triggering log output.
Log crash data when the API client subscribes to logs so dashboard
and HA users see crash reports even when connecting after boot,
matching the ESP32 crash handler behavior.
Arduino framework already wraps esp_panic_handler for its own
backtrace handler, causing a linker conflict. Only enable our
crash handler when using ESP-IDF framework by gating behind
USE_ESP32_CRASH_HANDLER define (set via cg.add_define).
Co-Authored-By: J. Nick Koston <nick@koston.org>
Store the exception cause register (exccause/mcause) and exception
type (panic_exception_t) in the .noinit struct. At log time, look up
the cause code in architecture-specific tables mirroring ESP-IDF's
internal panic_arch_fill_info() arrays.
Output now shows e.g.:
Reason: Fault - Store access fault (RISC-V)
Reason: Fault - StoreProhibited (Xtensa)
Reason: Interrupt wdt (watchdog, type-only)
Bumps CRASH_DATA_VERSION to 2. Struct gains +4 bytes (cause field)
with no padding increase (exception and pseudo_excause fit in existing
padding alongside backtrace_count and reg_frame_count). All fields
are clamped on read to prevent corrupt .noinit data from causing
out-of-bounds array access.
Co-Authored-By: J. Nick Koston <nick@koston.org>
On RISC-V, register-sourced entries (MEPC/RA) are labeled
"backtrace" while stack-scanned entries are labeled "stack scan"
to help users identify which frames are most trustworthy.
Co-Authored-By: J. Nick Koston <nick@koston.org>
- Split text into lines before calling process_stacktrace in client.py,
since process_stacktrace uses re.match and expects individual lines.
- Use memcpy instead of direct pointer cast for reading the instruction
before a return address, since RISC-V C extension means code addresses
are only 2-byte aligned and addr-4 may not be 4-byte aligned.
Co-Authored-By: J. Nick Koston <nick@koston.org>
- Add compressed c.jalr (2-byte) instruction check alongside 4-byte
JAL/JALR, since ESP32 RISC-V targets have the C extension enabled.
- Track register-sourced entries (MEPC/RA) separately from stack-scanned
ones, and skip return-address validation for register entries since
they are known-good values from the exception frame.
Co-Authored-By: J. Nick Koston <nick@koston.org>
- s_raw_crash_data and s_crash_data_valid must be mutable globals
- __real_/__wrap_ names are mandated by the --wrap linker mechanism
Co-Authored-By: J. Nick Koston <nick@koston.org>
IDF doesn't decode RISC-V backtraces on the console either — it just
dumps raw stack memory. Our crash handler provides a better trace.
Co-Authored-By: J. Nick Koston <nick@koston.org>
Stack scanning captures any value that looks like a code address, which
includes false positives. At log time (flash cache is up), validate each
address by checking if the preceding instruction is a JAL/JALR with
rd=ra. This filters spurious entries like FreeRTOS internals that happen
to be on the stack but aren't part of the actual call chain.
Co-Authored-By: J. Nick Koston <nick@koston.org>
Version first ensures future firmware can always identify the struct
layout without depending on any other field positions.
Co-Authored-By: J. Nick Koston <nick@koston.org>
Allows future firmware to detect and skip incompatible crash data
layouts. Placed alongside backtrace_count to avoid adding padding.
Co-Authored-By: J. Nick Koston <nick@koston.org>
Moving backtrace_count before the variable-length backtrace array
ensures magic, pc, and count are at fixed offsets regardless of
MAX_BACKTRACE value. This makes the .noinit data readable across
firmware versions that may change the max backtrace depth.
Co-Authored-By: J. Nick Koston <nick@koston.org>
Saves 76 bytes of RAM by removing the validated BSS copy and reading
directly from the .noinit struct after magic validation. A single bool
tracks whether valid crash data was found this boot.
Co-Authored-By: J. Nick Koston <nick@koston.org>
8 frames was cutting off useful call stack information. 16 frames
costs an additional 32 bytes of .noinit RAM and covers typical
ESPHome call chains which can be 10-12 frames deep.
The esphome logs command via API wasn't running process_stacktrace
on received log lines, so crash handler backtrace addresses were
displayed but not decoded with addr2line.
- Make s_raw_crash_data static with inline .noinit definition (no extern needed)
- Remove valid=false clearing from crash_handler_log() so both serial (boot)
and API (subscribe) paths can emit the crash data
- Clear valid flag after logging to prevent re-logging on API reconnects
- Cache esp_cpu_process_stack_pc result to avoid redundant call
- Remove unused <cstdint> include from header
When an ESP32 crashes, the backtrace is printed to UART and lost.
Users without a serial cable never see this diagnostic information.
This adds a crash handler that:
- Intercepts esp_panic_handler() via --wrap linker flag
- Captures the faulting PC and backtrace into .noinit memory
- Supports both Xtensa (ESP32/S2/S3) and RISC-V (C3/C6/H2/C2)
- Logs crash data at boot via ESP_LOGE (serial output)
- Re-logs when HA subscribes to logs (visible in HA log viewer)
- Adds CLI stacktrace decoding for the new log format
tcp_pcb_listen is a smaller struct than tcp_pcb — calling tcp_recv()
or tcp_err() on it writes past the struct boundary. Revert the listen
PCB close to plain tcp_close(), which is synchronous for listen PCBs
(no async callbacks to worry about).
ESP8266's settimeofday() returns EINVAL (22) directly as the return
value when the timezone parameter is non-NULL, rather than following
POSIX convention of returning -1 and setting errno. The previous
fallback code checked errno == EINVAL which never matched because
errno was never set, so the retry with nullptr never triggered.
Fix by always passing nullptr on ESP8266 since the platform requires it.
Clear LWIP callbacks (tcp_arg, tcp_recv, tcp_err) before calling
tcp_close() or tcp_abort() to prevent use-after-free.
After tcp_close(), the PCB remains alive during the TCP close
handshake (FIN_WAIT, TIME_WAIT states). If LWIP calls recv/err
callbacks during this period and the socket object has already
been destroyed, the callback writes to freed memory, corrupting
the heap.
This was observed as umm_malloc_core crashes on ESP8266 during
rapid API client connect/disconnect cycles — the heap free-list
got corrupted by a dangling callback writing to a freed
LWIPRawImpl object.
Extract pcb_detach_abort() and pcb_detach_close() helpers to
ensure all close/abort sites consistently clear callbacks first.