From a3e3770c68dbb55094b95e15d0c73208880d0825 Mon Sep 17 00:00:00 2001 From: Gabriel Mancini Date: Mon, 9 Nov 2026 09:36:40 -0300 Subject: [PATCH] esp32: Drain PPP input before destroying the netif When a PPP session is freed, the RX task has stopped, but input it queued for the TCP/IP task may still point at the netif. With CONFIG_LWIP_TCPIP_CORE_LOCKING enabled, esp_netif_destroy runs at once rather than behind that input, which then reaches a freed netif. free_ppp now posts a callback through the TCP/IP mailbox, which is FIFO, and waits for it before destroying the netif. Not esp_netif_tcpip_exec or tcpip_callback_wait: under core locking both run inline. If the callback cannot be posted, the session is left allocated rather than freed under queued input, and the late-close log line no longer says "freed". Signed-off-by: Gabriel Mancini --- .../components/avm_builtins/network_driver.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/platforms/esp32/components/avm_builtins/network_driver.c b/src/platforms/esp32/components/avm_builtins/network_driver.c index 5c2b5fb6e5..727354bc5e 100644 --- a/src/platforms/esp32/components/avm_builtins/network_driver.c +++ b/src/platforms/esp32/components/avm_builtins/network_driver.c @@ -66,6 +66,7 @@ #include #include #include +#include #define HAVE_PPP 1 #endif #pragma GCC diagnostic pop @@ -1791,6 +1792,11 @@ static bool start_ppp(Context *ctx, term pid, term ref, struct ClientData *data, return false; } +static void ppp_rx_drained(void *arg) +{ + xSemaphoreGive((SemaphoreHandle_t) arg); +} + // Frees what start_ppp set up, once the session is dead (or never started): lwIP keeps timers // on a session until its dead phase, and the netif and its PPP control block must outlive them. static void free_ppp(struct PPPGlue *glue) @@ -1803,6 +1809,15 @@ static void free_ppp(struct PPPGlue *glue) ESP_LOGI(TAG, "PPP closed: %u bytes in, %u bytes out", (unsigned) glue->rx_bytes, (unsigned) glue->tx_bytes); glue->stop = true; xSemaphoreTake(glue->stopped, portMAX_DELAY); + // The RX task has stopped, but input it queued for the TCP/IP task still points at the + // netif. With core locking, esp_netif_destroy runs at once instead of behind that input, + // so a callback through the mailbox, which is FIFO, waits it out. Not + // esp_netif_tcpip_exec or tcpip_callback_wait: under core locking both run inline. + if (tcpip_callback(ppp_rx_drained, glue->stopped) != ERR_OK) { + ESP_LOGE(TAG, "Failed to drain PPP input, left allocated"); + return; + } + xSemaphoreTake(glue->stopped, portMAX_DELAY); } if (glue->stopped != NULL) { vSemaphoreDelete(glue->stopped); @@ -1828,7 +1843,7 @@ static void ppp_teardown_task(void *args) struct PPPGlue *glue = args; if (xSemaphoreTake(glue->died, pdMS_TO_TICKS(PPP_TEARDOWN_MS)) == pdTRUE) { free_ppp(glue); - ESP_LOGI(TAG, "PPP closed late, freed"); + ESP_LOGI(TAG, "PPP late close teardown finished"); } else { ESP_LOGE(TAG, "PPP did not close in %d ms, left allocated", PPP_TEARDOWN_MS); }