Browsers negotiate ECDHE cipher suites by default. On ESP32-S3 the
hardware RSA accelerator handles RSA key exchange efficiently, but
there is no ECP hardware accelerator. ECDHE requires the server to
compute an ephemeral key pair: ecp_precompute_comb() builds a comb
table through many sequential ECP point doublings, each dispatched
to the hardware bignum unit (esp_bignum.c), but the ECP layer has
no RTOS yield points between iterations. The entire computation runs
to completion on CPU 0 without ever resetting the task watchdog.
A single handshake does not exceed the watchdog timeout on its own,
but two consecutive handshakes (e.g. a browser retry after a failed
attempt) accumulate enough uninterrupted runtime to starve IDLE0:
E (54924) esp-tls-mbedtls: mbedtls_ssl_handshake returned -0x0050
E (57208) esp-tls-mbedtls: mbedtls_ssl_handshake returned -0x7280
E (57638) task_wdt: Task watchdog got triggered.
E (57638) task_wdt: - IDLE0 (CPU 0)
E (57638) task_wdt: Tasks currently running:
E (57638) task_wdt: CPU 0: httpd
The crash occurs in ecp_precompute_comb() → ecp_double_jac() →
mbedtls_mpi_mul_mpi() during the ServerKeyExchange step.
Fix by wrapping mbedtls_ssl_config_defaults() via the linker --wrap
mechanism. The wrapper intercepts server-side SSL config init
(MBEDTLS_SSL_IS_SERVER) and replaces the cipher suite list with
RSA key exchange only, routing handshakes through the hardware RSA
accelerator and eliminating the ECDH path entirely. MQTT connections
(MBEDTLS_SSL_IS_CLIENT) are unaffected.
Also switch the self-signed cert generator from EC (prime256v1) to
RSA 2048 so the generated certificate matches the restricted cipher
suites.
Changes:
- arch/esp32/tls_cipher_restrict.c: new file implementing the
mbedtls_ssl_config_defaults wrap; restricts server cipher suites
to RSA_WITH_AES_{128,256}_{GCM,CBC}_SHA{256,384}
- platformio.ini: add -Wl,--wrap=mbedtls_ssl_config_defaults and
include tls_cipher_restrict.c in the esp32_base build
- arch/esp32/extra_scripts/generate_web_panel_cert.py: switch key
generation from `openssl ecparam -name prime256v1` to `openssl
genrsa 2048`
On dual-core ESP32-S3 (ARDUINO_RUNNING_CORE=1), the Arduino loop and
LoRa processing run exclusively on core 1. ESP-IDF v4 creates mqtt_task
with tskNO_AFFINITY, meaning FreeRTOS may schedule them on core 1 under
load, preempting the LoRa loop.
ESP-IDF v4 provides no public API to change a task's core affinity after
creation (vTaskCoreAffinitySet is IDF v5+ only), and the esp_mqtt_client
config struct has no task_core_id field. The precompiled Arduino-ESP32
framework cannot be patched via sdkconfig.
Instead, use the GCC/LD --wrap linker mechanism to intercept every call
to xTaskCreatePinnedToCore. Any task created with tskNO_AFFINITY is
redirected to core 0. Tasks that are already explicitly pinned (Wi-Fi
driver, LwIP, httpd, esp_timer, ipc0/ipc1) are passed through unchanged.
We intentionally do not filter by task name. Pinning all unpinned tasks
makes the approach robust against internal ESP-IDF task name changes and
catches any future tasks that may be added with tskNO_AFFINITY.
Verified task layout after the change:
loopTask pri=1 core=1 (Arduino loop / LoRa — unchanged)
mqtt_task pri=5 core=0 (was tskNO_AFFINITY, now pinned)
httpd pri=2 core=0 (core_id set explicitly in WebPanelServer)
tiT pri=18 core=0 (LwIP, already pinned by ESP-IDF)
wifi pri=23 core=0 (Wi-Fi driver, already pinned)
esp_timer pri=22 core=0 (already pinned)
ipc0/ipc1 pri=24 core=0/1 (IPC, already pinned per-core)
The -Wl,--wrap flag and arch/esp32/task_pinning.c are added only to
[esp32_base] (IDF v4). The ESP32-C6 pioarduino target (IDF v5) is
unaffected and can use vTaskCoreAffinitySet() if needed in the future.
Patches Bluefruit library to fix semaphore leak bug that causes device lockup
when BLE central disconnects unexpectedly (e.g., going out of range, supervision timeout).
Co-authored-by: Liam Cottle <liamcottle@users.noreply.github.com>
Co-authored-by: oltaco <oltaco@users.noreply.github.com>