Category
Hardware Compatibility
Hardware
Other
Is this bug report about any UI component firmware like InkHUD or Meshtatic UI (MUI)?
Firmware Version
2.7.x (current develop branch)
Description
I use an ESP32-S3 zero dev board for development and testing. I came across a bug that prevented a board without a radio connected to crash constantly.
RadioLibInterface::instance becomes a dangling pointer when radio initialization fails, leading to a Guru Meditation Error: Core 1 panic'ed (IllegalInstruction) crash in loop() when resetAGC() or pollMissedIrqs() is called on the freed object.
Root Cause
The RadioLibInterface constructor unconditionally sets the static instance pointer:
// RadioLibInterface.cpp:42
instance = this;
In initLoRa(), when radio hardware is not present, the following sequence occurs:
- A
SX1262Interface (or similar) is constructed via new, which sets RadioLibInterface::instance = this
init() fails (no hardware detected)
- The
std::unique_ptr holding the object is reset or goes out of scope — the object is destroyed and its memory freed
RadioLibInterface::instance still points to the freed heap memory
Later, loop() checks RadioLibInterface::instance != nullptr (which passes — the pointer is non-null, just dangling) and calls virtual methods on the destroyed object:
// main.cpp loop()
if (RadioLibInterface::instance != nullptr) {
RadioLibInterface::instance->pollMissedIrqs(); // use-after-free
RadioLibInterface::instance->resetAGC(); // use-after-free
}
The freed heap memory gets reused for other allocations, overwriting the vtable pointer. The virtual call then follows the corrupted vtable into string data in flash rodata, producing an IllegalInstruction exception.
How to Reproduce
- Use a board variant with
USE_SX1262 defined but no physical SX1262 radio connected (e.g., an ESP32-S3 dev board)
- Boot the firmware
- Wait ~30–40 seconds (until the AGC reset timer fires for the first time at
AGC_RESET_INTERVAL_MS)
- Observe crash:
Guru Meditation Error: Core 1 panic'ed (IllegalInstruction). Exception was unhandled.
PC: 0x6c615674 (string data "tVal" executed as code)
Crash Analysis
- PC =
0x6c615674 — this is ASCII "tVal" (from "BLEAttValue" string in flash), not executable code
- The virtual call on the dangling
instance pointer reads a corrupted vtable pointer (the heap memory was reused), then loads a vtable entry that points into string data in .flash.rodata
- The crash is perfectly deterministic — same PC, same registers every time — because heap reuse follows the same pattern on each boot
Affected Code
Note: pollMissedIrqs() (from #9658) has the same problem but may crash less visibly depending on timing.
Suggested Fix
Add a virtual destructor to RadioLibInterface that clears the static instance pointer:
// In RadioLibInterface.h
public:
virtual ~RadioLibInterface() {
if (instance == this)
instance = nullptr;
}
This ensures that whenever a RadioLibInterface object is destroyed (whether due to failed init, scope exit, or normal shutdown), the instance pointer is safely nullified, and the existing if (instance != nullptr) guard in loop() works as intended.
Environment
- Board: ESP32-S3 (no radio hardware connected)
- Framework: Arduino/ESP-IDF via PlatformIO
- Firmware version: 2.7.x (current
develop branch)
Relevant log output
Guru Meditation Error: Core 1 panic'ed (IllegalInstruction). Exception was unhandled.
Core 1 register dump:
PC : 0x6c615674 PS : 0x00060530 A0 : 0x8201662f A1 : 0x3fcebff0
A2 : 0x6c615674 A3 : 0x3fca6d60 A4 : 0x3fca8640 A5 : 0x3fca1644
A6 : 0x00000012 A7 : 0x3fcc2fac A8 : 0x8207e828 A9 : 0x3fcebfd0
A10 : 0x3fcb2760 A11 : 0x3fcf21a8 A12 : 0x3fcf215c A13 : 0x00000000
A14 : 0x00000032 A15 : 0x3c18d900 SAR : 0x0000000a EXCCAUSE: 0x00000000
EXCVADDR: 0x00000000 LBEG : 0x4207e8f1 LEND : 0x4207e8ff LCOUNT : 0x00000016
Backtrace: 0x6c615671:0x3fcebff0 |<-CORRUPTED
Category
Hardware Compatibility
Hardware
Other
Is this bug report about any UI component firmware like InkHUD or Meshtatic UI (MUI)?
Firmware Version
2.7.x (current
developbranch)Description
I use an ESP32-S3 zero dev board for development and testing. I came across a bug that prevented a board without a radio connected to crash constantly.
RadioLibInterface::instancebecomes a dangling pointer when radio initialization fails, leading to aGuru Meditation Error: Core 1 panic'ed (IllegalInstruction)crash inloop()whenresetAGC()orpollMissedIrqs()is called on the freed object.Root Cause
The
RadioLibInterfaceconstructor unconditionally sets the staticinstancepointer:In
initLoRa(), when radio hardware is not present, the following sequence occurs:SX1262Interface(or similar) is constructed vianew, which setsRadioLibInterface::instance = thisinit()fails (no hardware detected)std::unique_ptrholding the object is reset or goes out of scope — the object is destroyed and its memory freedRadioLibInterface::instancestill points to the freed heap memoryLater,
loop()checksRadioLibInterface::instance != nullptr(which passes — the pointer is non-null, just dangling) and calls virtual methods on the destroyed object:The freed heap memory gets reused for other allocations, overwriting the vtable pointer. The virtual call then follows the corrupted vtable into string data in flash rodata, producing an
IllegalInstructionexception.How to Reproduce
USE_SX1262defined but no physical SX1262 radio connected (e.g., an ESP32-S3 dev board)AGC_RESET_INTERVAL_MS)Crash Analysis
0x6c615674— this is ASCII"tVal"(from"BLEAttValue"string in flash), not executable codeinstancepointer reads a corrupted vtable pointer (the heap memory was reused), then loads a vtable entry that points into string data in.flash.rodataAffected Code
02d42f87d— "Implement 'agc' reset for SX126x & LR11x0 chip families") which added theresetAGC()call inloop()ad7d19c31— "Remove unused global rIf that shadows locals") which removed the globalrIfthat previously kept the radio object aliveRadioLibInterfacehas no destructor to clearinstance, so any failed init path that destroys the object leaves a dangling pointerNote:
pollMissedIrqs()(from #9658) has the same problem but may crash less visibly depending on timing.Suggested Fix
Add a virtual destructor to
RadioLibInterfacethat clears the staticinstancepointer:This ensures that whenever a
RadioLibInterfaceobject is destroyed (whether due to failed init, scope exit, or normal shutdown), theinstancepointer is safely nullified, and the existingif (instance != nullptr)guard inloop()works as intended.Environment
developbranch)Relevant log output