fix(mqtt): FR-011 Heap-Korruption bei TLS-Verbindungsabbruch

- _rebuildClient(): WiFiClientSecure + PubSubClient vor jedem
  Reconnect-Versuch sauber delete + new auf Core 0
- mbedTLS startet damit immer mit sauberem Heap-Kontext
- Verhindert CORRUPT HEAP / PANIC wenn Broker die TLS-Session
  unerwartet abbricht (MBEDTLS_ERR_NET_CONN_RESET)
- _broker/_port als Members gecacht fuer _rebuildClient()
This commit is contained in:
MaPaLo76 2026-03-01 17:50:18 +01:00
parent c61a67f812
commit 1ef0464be9
5 changed files with 111 additions and 33 deletions

View File

@ -19,6 +19,23 @@ Status: `[ ]` = offen · `[x]` = erledigt
## Offen
- [ ] **FR-011** Bug: PANIC/EXCEPTION durch Heap-Korruption bei MQTT-Verbindungsabbruch (TLS)
- **Symptom**: ESP32 crasht mit `CORRUPT HEAP: Bad tail` + `assert failed: multi_heap_free` wenn der MQTT-Broker nicht erreichbar ist und eine bestehende TLS-Session unerwartet abbricht
- **Fehlermeldung**: `(-76) UNKNOWN ERROR CODE (004C)` = `MBEDTLS_ERR_NET_CONN_RESET`
- **Crash-Kette**:
1. Broker bricht TLS-Verbindung ab (Connection reset by peer)
2. `PubSubClient::connected()``WiFiClientSecure::connected()``available()`
3. `available()` erkennt EOF → ruft intern `stop()``stop_ssl_socket()` auf
4. `mbedtls_ssl_free()` wird auf einem bereits inkonsistenten SSL-Kontext aufgerufen
5. Heap-Corruption → `multi_heap_free` assert → PANIC
- **Backtrace-Frames**: `multi_heap_free``heap_caps_free``esp_mbedtls_mem_free``mbedtls_free``mbedtls_ssl_free``stop_ssl_socket``WiFiClientSecure::stop()``WiFiClientSecure::available()``WiFiClientSecure::connected()``PubSubClient::connected()``MqttClient::_taskLoop()`
- **Root Cause**: `WiFiClientSecure`-Objekt (`_espClient`) wird nach einem TLS-Verbindungsabbruch nicht neu erstellt — der interne mbedTLS-State ist korrupt, beim nächsten `connected()`-Aufruf crasht `mbedtls_ssl_free()`
- **Fix-Strategie**: Nach jedem fehlgeschlagenen Verbindungsversuch (`rc == -2` oder `!client->connected()`) das `WiFiClientSecure`-Objekt zerstören und neu auf Core 0 erstellen (`delete _espClient; _espClient = new WiFiClientSecure(); _espClient->setInsecure(); _client->setClient(*_espClient)`). Damit startet mbedTLS immer mit einem sauberen Kontext.
- **Reproduktion**: Broker stoppen während aktiver TLS-Session → innerhalb weniger Minuten PANIC
- Betroffene Dateien: `src/mqtt_client.cpp`, `include/mqtt_client.h`
- Commit: `TODO`
- Version: 1.4.1
---
## Erledigt
@ -46,7 +63,7 @@ Status: `[ ]` = offen · `[x]` = erledigt
- **Heartbeat `lasercutter/status`**: Feld `"display_on": true/false` hinzugefügt
- **`DisplayManager`**: `setEnabled()` / `isEnabled()` via MAX7219 SHUTDOWN-Modus
- Betroffene Dateien: `src/web_server.cpp`, `src/mqtt_client.cpp`, `include/config.h`, `include/display_manager.h`, `src/display_manager.cpp`
- Commit: `TODO`
- Commit: `c61a67f`
- Version: 1.4.0
### Version 1.2.0

View File

@ -24,8 +24,8 @@
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.71891835"
inkscape:cx="584.90648"
inkscape:cy="639.15464"
inkscape:cx="634.98171"
inkscape:cy="835.28262"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="1912"
@ -149,56 +149,62 @@
xml:space="preserve"
style="font-weight:600;font-size:7.40833px;line-height:0;font-family:'Open Sans';-inkscape-font-specification:'Open Sans, Semi-Bold';text-align:end;writing-mode:lr-tb;direction:ltr;text-anchor:end;fill:#000000;stroke:#241212;stroke-width:0.2;stroke-linejoin:bevel;stroke-opacity:1"
x="204.76865"
y="256.40359"
y="262.22452"
id="text4"><tspan
sodipodi:role="line"
id="tspan4"
style="fill:#000000;stroke-width:0.2"
x="204.76865"
y="256.40359">Freie Testzeit</tspan><tspan
y="262.22452">Freie Testzeit</tspan><tspan
sodipodi:role="line"
style="fill:#000000;stroke-width:0.2"
x="204.76865"
y="256.40359"
y="262.22452"
id="tspan5" /></text>
<text
xml:space="preserve"
style="font-weight:600;font-size:7.40833px;line-height:0;font-family:'Open Sans';-inkscape-font-specification:'Open Sans, Semi-Bold';text-align:end;writing-mode:lr-tb;direction:ltr;text-anchor:end;fill:#000000;stroke:#241212;stroke-width:0.2;stroke-linejoin:bevel;stroke-opacity:1"
x="203.7793"
y="265.51187"
y="271.33279"
id="text6"><tspan
sodipodi:role="line"
id="tspan6"
style="fill:#000000;stroke-width:0.2"
x="203.7793"
y="265.51187">in Sekunden</tspan></text>
y="271.33279">in Sekunden</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:5.29167px;line-height:0;font-family:'Open Sans';-inkscape-font-specification:'Open Sans, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:end;writing-mode:lr-tb;direction:ltr;text-anchor:end;fill:#000000;stroke:#241212;stroke-width:0.2;stroke-linejoin:bevel;stroke-opacity:1"
x="144.94814"
y="256.01343"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:5.29167px;line-height:0;font-family:'Open Sans';-inkscape-font-specification:'Open Sans, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:end;writing-mode:lr-tb;direction:ltr;text-anchor:end;fill:#000000;stroke:#422020;stroke-width:0.2;stroke-linejoin:bevel;stroke-opacity:1"
x="226.37459"
y="252.96211"
id="text4-0"><tspan
id="tspan4-6"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:5.29167px;font-family:'Open Sans';-inkscape-font-specification:'Open Sans, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:0.2"
x="144.94814"
y="256.01343"
sodipodi:role="line">€ Laserzeit</tspan><tspan
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:5.29167px;font-family:'Open Sans';-inkscape-font-specification:'Open Sans, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:0.2"
x="144.94814"
y="256.01343"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:5.29167px;font-family:'Open Sans';-inkscape-font-specification:'Open Sans, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:0.2;stroke:#422020;stroke-opacity:1"
x="226.37459"
y="252.96211"
sodipodi:role="line">€ Laserzeit in Sekunden </tspan><tspan
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:5.29167px;font-family:'Open Sans';-inkscape-font-specification:'Open Sans, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:0.2;stroke:#422020;stroke-opacity:1"
x="226.37459"
y="252.96211"
id="tspan5-1"
sodipodi:role="line" /></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:5.29167px;line-height:0;font-family:'Open Sans';-inkscape-font-specification:'Open Sans, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:end;writing-mode:lr-tb;direction:ltr;text-anchor:end;fill:#000000;stroke:#241212;stroke-width:0.2;stroke-linejoin:bevel;stroke-opacity:1"
x="144.02959"
y="265.11499"
id="text6-3"><tspan
sodipodi:role="line"
id="tspan6-8"
x="-218.73421"
y="225.72589"
id="text4-0-7"
transform="rotate(-89.846243)"><tspan
id="tspan4-6-6"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:5.29167px;font-family:'Open Sans';-inkscape-font-specification:'Open Sans, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:0.2"
x="144.02959"
y="265.11499">Sekunden</tspan></text>
x="-218.73421"
y="225.72589"
sodipodi:role="line">umlaufend</tspan><tspan
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:5.29167px;font-family:'Open Sans';-inkscape-font-specification:'Open Sans, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:0.2"
x="-218.73421"
y="225.72589"
id="tspan5-1-1"
sodipodi:role="line" /></text>
<text
xml:space="preserve"
style="font-weight:600;font-size:7.40833px;line-height:0;font-family:'Open Sans';-inkscape-font-specification:'Open Sans, Semi-Bold';text-align:end;writing-mode:lr-tb;direction:ltr;text-anchor:end;fill:#000000;stroke:#241212;stroke-width:0.2;stroke-linejoin:bevel;stroke-opacity:1"
@ -228,7 +234,7 @@
sodipodi:role="line"
id="tspan8"
x="241.88585"
y="126.91018"></tspan></text>
y="126.91018" /></text>
<text
xml:space="preserve"
style="font-weight:600;font-size:7.40833px;line-height:0;font-family:'Open Sans';-inkscape-font-specification:'Open Sans, Semi-Bold';text-align:end;writing-mode:lr-tb;direction:ltr;text-anchor:end;fill:#000000;stroke:#241212;stroke-width:0.2;stroke-linejoin:bevel;stroke-opacity:1"
@ -296,18 +302,18 @@
y="265.51187">Error</tspan></text>
<path
style="fill:none;stroke:#241212;stroke-width:0.6;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
d="m 148,250.00001 v 20 h 66.99999 l 0,-20"
d="m 148,255.82088 v 20 h 66.99999 v -20"
id="path6"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;stroke:#241212;stroke-width:0.6;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
d="m 114.50001,250.00001 v 20 H 148 v -20"
id="path6-6"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;stroke:#241212;stroke-width:0.6;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
d="m 216,172.81966 -0.13385,-22.4101 -101.04619,-0.15132 0.11406,22.56145"
id="path6-3"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;stroke:#422020;stroke-width:0.6;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
d="m 115,244.00001 v 3 h 104.99999 c -0.0471,0.12248 0,-37.00001 0,-37.00001 h -3"
id="path6-3-7"
sodipodi:nodetypes="ccccc" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -94,6 +94,14 @@ private:
// Verbindungsaufbau (blockierend - NUR aus MQTT-Task auf Core 0 aufrufen!)
bool reconnect();
// TLS-Client komplett abreissen und neu aufbauen (Fix: Heap-Korruption bei
// unerwartetem SSL-Verbindungsabbruch durch kaputten mbedTLS-Kontext)
void _rebuildClient();
// Gecachte Broker-Einstellungen fuer _rebuildClient()
char _broker[64];
uint16_t _port;
// Heartbeat-Publish (lasercutter/status)
void publishHeartbeat();

View File

@ -23,7 +23,7 @@ build_flags =
-DCORE_DEBUG_LEVEL=1 ; 0=keine, 1=Fehler, 3=Info, 5=Verbose
-DARDUINO_LOOP_STACK_SIZE=8192
-DELEGANTOTA_USE_ASYNC_WEBSERVER=1
-DFIRMWARE_VERSION='"1.4.0"' ; Semantic Versioning hier erhöhen bei neuem Release
-DFIRMWARE_VERSION='"1.4.1"' ; Semantic Versioning hier erhöhen bei neuem Release
lib_deps =
majicDesigns/MD_Parola @ ^3.7.3
majicDesigns/MD_MAX72XX @ ^3.5.1

View File

@ -46,6 +46,8 @@ MqttClient::MqttClient()
{
_clientId[0] = '\0';
_resetReason[0] = '\0';
_broker[0] = '\0';
_port = 0;
}
// --------------------------------------------------------------------------
@ -200,6 +202,43 @@ void MqttClient::publishHeartbeat() {
LOG_I("MQTT", "Heartbeat: %s -> %s", buf, ok ? "OK" : "FEHLER");
}
// --------------------------------------------------------------------------
// _rebuildClient() - TLS-Client sauber abreissen und neu aufbauen
// MUSS auf Core 0 aufgerufen werden (mbedtls Heap-Kontext-Binding)
// --------------------------------------------------------------------------
void MqttClient::_rebuildClient() {
// Alten Client sauber schliessen und freigeben
if (_client) {
_client->disconnect();
delete _client;
_client = nullptr;
}
if (_secureClient) {
delete _secureClient;
_secureClient = nullptr;
}
if (_wifiClient) {
delete _wifiClient;
_wifiClient = nullptr;
}
// Frische Objekte auf Core 0 anlegen
_wifiClient = new WiFiClient();
_secureClient = new WiFiClientSecure();
if (_port == 8883) {
_secureClient->setInsecure();
_client = new PubSubClient(*_secureClient);
} else {
_client = new PubSubClient(*_wifiClient);
}
_client->setServer(_broker, _port);
_client->setCallback(MqttClient::onMessage);
_client->setKeepAlive(60);
_client->setBufferSize(512);
LOG_I("MQTT", "Client neu aufgebaut (sauberer TLS-Kontext)");
}
// --------------------------------------------------------------------------
// reconnect() - Verbindungsaufbau, non-blocking
// --------------------------------------------------------------------------
@ -255,6 +294,11 @@ void MqttClient::_taskLoop() {
const char* broker = cfg.mqttBroker[0] != '\0' ? cfg.mqttBroker : DEFAULT_MQTT_BROKER;
uint16_t port = cfg.mqttPort > 0 ? cfg.mqttPort : DEFAULT_MQTT_PORT;
// Broker/Port für _rebuildClient() sichern
strncpy(_broker, broker, sizeof(_broker) - 1);
_broker[sizeof(_broker) - 1] = '\0';
_port = port;
// Netzwerk-Objekte auf Core 0 anlegen (mbedtls/WiFiClientSecure bindet
// Heap-Kontexte an den erstellenden Task/Core -> kein Cross-Core-Zugriff).
_wifiClient = new WiFiClient();
@ -284,6 +328,9 @@ void MqttClient::_taskLoop() {
uint32_t now = millis();
if (now - _lastReconnectMs >= MQTT_RECONNECT_MS) {
_lastReconnectMs = now;
// Sicherstellen, dass kein korrupter mbedTLS-Kontext vom letzten
// Verbindungsabbruch uebrig bleibt (Fix FR-011: Heap-Korruption)
_rebuildClient();
reconnect(); // blockiert hier (TLS); Core 1 laeuft unbehelligt
}
vTaskDelay(pdMS_TO_TICKS(200));