Skip to content

[Bug]: Use-after-free on RadioLibInterface::instance causes crash when radio hardware is absent #9880

@duraseb

Description

@duraseb

Category

Hardware Compatibility

Hardware

Other

Is this bug report about any UI component firmware like InkHUD or Meshtatic UI (MUI)?

  • Meshtastic UI aka MUI colorTFT
  • InkHUD ePaper
  • OLED slide UI on any display

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:

  1. A SX1262Interface (or similar) is constructed via new, which sets RadioLibInterface::instance = this
  2. init() fails (no hardware detected)
  3. The std::unique_ptr holding the object is reset or goes out of scope — the object is destroyed and its memory freed
  4. 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

  1. Use a board variant with USE_SX1262 defined but no physical SX1262 radio connected (e.g., an ESP32-S3 dev board)
  2. Boot the firmware
  3. Wait ~30–40 seconds (until the AGC reset timer fires for the first time at AGC_RESET_INTERVAL_MS)
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingneeds-logsDevice logs requested for triage

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions