lwip_freertos_init() never restores lwip_context after lwip_freertos_deinit(), causing LOCK_TCPIP_CORE() to deadlock on re-init (e.g. cyw43 Wi-Fi power-cycling) #3175

Closed
opened 2026-09-04 03:22:49 -05:00 by jondurrant · 0 comments
jondurrant commented 2026-09-04 03:22:49 -05:00 (Migrated from github.com)

Title

lwip_freertos_init() never restores lwip_context after lwip_freertos_deinit(), causing LOCK_TCPIP_CORE() to deadlock on re-init (e.g. cyw43 Wi-Fi power-cycling)

Environment

  • pico-sdk: 2.3.0 (commit 98a542c1a6)
  • FreeRTOS-Kernel: v11.3.1 (bundled)
  • configSUPPORT_STATIC_ALLOCATION = 0, configSUPPORT_DYNAMIC_ALLOCATION = 1
  • Async context: cyw43_arch_freertos (PICO_CYW43_ARCH_FREERTOS)
  • Board: Raspberry Pi Pico 2 W

Summary

An application that calls cyw43_arch_init() / cyw43_arch_deinit() repeatedly (e.g. to power down Wi-Fi between transmissions to save power) can successfully connect the first time, but every subsequent cyw43_arch_init() → Wi-Fi join fails/hangs until timeout. The failure is caused by a bug in src/rp2_common/pico_lwip/lwip_freertos.c.

Root cause

static async_context_t * volatile lwip_context;
static SemaphoreHandle_t tcpip_task_blocker;

bool lwip_freertos_init(async_context_t *context) {
    assert(!lwip_context || lwip_context == context);
    static bool done_lwip_init;
    if (!done_lwip_init) {
        lwip_context = context;
        done_lwip_init = true;
        ...
        tcpip_init(tcpip_init_done, init_sem);
        ...
    } else {
        xSemaphoreGive(tcpip_task_blocker);
    }
    return true;
}

static uint32_t clear_lwip_context(__unused void *param) {
    lwip_context = NULL;
    return 0;
}

void lwip_freertos_deinit(__unused async_context_t *context) {
    async_context_execute_sync(context, clear_lwip_context, NULL);
}
  • done_lwip_init is a function-static bool that persists for the entire lifetime of the program — it is set to true the first time lwip_freertos_init() is ever called, and is never reset by lwip_freertos_deinit().
  • lwip_freertos_deinit() sets lwip_context = NULL.
  • On the first cyw43_arch_init(), done_lwip_init is false, so lwip_context is correctly set and tcpip_init() runs.
  • On every subsequent cyw43_arch_init() (after any earlier cyw43_arch_deinit()), done_lwip_init is already true, so the else branch runs — which gives tcpip_task_blocker but never restores lwip_context.

This port's LOCK_TCPIP_CORE() implementation is:

void pico_lwip_custom_lock_tcpip_core(void) {
    while (!lwip_context) {
        xSemaphoreTake(tcpip_task_blocker, portMAX_DELAY);
    }
    async_context_acquire_lock_blocking(lwip_context);
}

Because lwip_context is never set back to non-NULL after the second lwip_freertos_init(), any caller waiting in this loop consumes the one semaphore "give," re-checks lwip_context (still NULL), and blocks on xSemaphoreTake again — permanently, since nothing gives the semaphore again. Any lwIP operation that needs the tcpip core lock during the second Wi-Fi session (netif setup, DHCP, etc., invoked from cyw43_wifi_join / cyw43_arch_wifi_connect_timeout_ms) hangs until the caller's own timeout expires, which surfaces to the application as a failed/timed-out Wi-Fi join.

Reproduction

cyw43_arch_init();
cyw43_arch_enable_sta_mode();
cyw43_arch_wifi_connect_timeout_ms(ssid, pass, CYW43_AUTH_WPA2_AES_PSK, 30000); // succeeds
cyw43_arch_lwip_begin();
cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA);
cyw43_arch_lwip_end();
cyw43_arch_disable_sta_mode();
cyw43_arch_deinit();

// ... later ...
cyw43_arch_init();
cyw43_arch_enable_sta_mode();
cyw43_arch_wifi_connect_timeout_ms(ssid, pass, CYW43_AUTH_WPA2_AES_PSK, 30000); // fails/times out

Suggested fix

Restore lwip_context in the else branch of lwip_freertos_init():

} else {
    lwip_context = context;
    xSemaphoreGive(tcpip_task_blocker);
}

Files involved

  • src/rp2_common/pico_lwip/lwip_freertos.c (lwip_freertos_init, lwip_context, pico_lwip_custom_lock_tcpip_core)

Note

This is separate from, but related in spirit to, the static-allocation task-TCB race in async_context_freertos_deinit() that can also crash FreeRTOS apps doing Wi-Fi power-cycling (see report for that issue) — both surfaced while implementing power-saving Wi-Fi on/off cycling with cyw43_arch_init()/cyw43_arch_deinit().

## Title `lwip_freertos_init()` never restores `lwip_context` after `lwip_freertos_deinit()`, causing `LOCK_TCPIP_CORE()` to deadlock on re-init (e.g. cyw43 Wi-Fi power-cycling) ## Environment - pico-sdk: 2.3.0 (commit 98a542c1a62fb549ffb5d66a3e5892b06276b670) - FreeRTOS-Kernel: v11.3.1 (bundled) - `configSUPPORT_STATIC_ALLOCATION` = 0, `configSUPPORT_DYNAMIC_ALLOCATION` = 1 - Async context: `cyw43_arch_freertos` (`PICO_CYW43_ARCH_FREERTOS`) - Board: Raspberry Pi Pico 2 W ## Summary An application that calls `cyw43_arch_init()` / `cyw43_arch_deinit()` repeatedly (e.g. to power down Wi-Fi between transmissions to save power) can successfully connect the **first** time, but every subsequent `cyw43_arch_init()` → Wi-Fi join fails/hangs until timeout. The failure is caused by a bug in `src/rp2_common/pico_lwip/lwip_freertos.c`. ## Root cause ```c static async_context_t * volatile lwip_context; static SemaphoreHandle_t tcpip_task_blocker; bool lwip_freertos_init(async_context_t *context) { assert(!lwip_context || lwip_context == context); static bool done_lwip_init; if (!done_lwip_init) { lwip_context = context; done_lwip_init = true; ... tcpip_init(tcpip_init_done, init_sem); ... } else { xSemaphoreGive(tcpip_task_blocker); } return true; } static uint32_t clear_lwip_context(__unused void *param) { lwip_context = NULL; return 0; } void lwip_freertos_deinit(__unused async_context_t *context) { async_context_execute_sync(context, clear_lwip_context, NULL); } ``` - `done_lwip_init` is a **function-static bool** that persists for the entire lifetime of the program — it is set to `true` the first time `lwip_freertos_init()` is ever called, and is never reset by `lwip_freertos_deinit()`. - `lwip_freertos_deinit()` sets `lwip_context = NULL`. - On the **first** `cyw43_arch_init()`, `done_lwip_init` is `false`, so `lwip_context` is correctly set and `tcpip_init()` runs. - On **every subsequent** `cyw43_arch_init()` (after any earlier `cyw43_arch_deinit()`), `done_lwip_init` is already `true`, so the `else` branch runs — which gives `tcpip_task_blocker` but **never restores `lwip_context`**. This port's `LOCK_TCPIP_CORE()` implementation is: ```c void pico_lwip_custom_lock_tcpip_core(void) { while (!lwip_context) { xSemaphoreTake(tcpip_task_blocker, portMAX_DELAY); } async_context_acquire_lock_blocking(lwip_context); } ``` Because `lwip_context` is never set back to non-NULL after the second `lwip_freertos_init()`, any caller waiting in this loop consumes the one semaphore "give," re-checks `lwip_context` (still NULL), and blocks on `xSemaphoreTake` again — permanently, since nothing gives the semaphore again. Any lwIP operation that needs the tcpip core lock during the second Wi-Fi session (netif setup, DHCP, etc., invoked from `cyw43_wifi_join` / `cyw43_arch_wifi_connect_timeout_ms`) hangs until the caller's own timeout expires, which surfaces to the application as a failed/timed-out Wi-Fi join. ## Reproduction ```c cyw43_arch_init(); cyw43_arch_enable_sta_mode(); cyw43_arch_wifi_connect_timeout_ms(ssid, pass, CYW43_AUTH_WPA2_AES_PSK, 30000); // succeeds cyw43_arch_lwip_begin(); cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA); cyw43_arch_lwip_end(); cyw43_arch_disable_sta_mode(); cyw43_arch_deinit(); // ... later ... cyw43_arch_init(); cyw43_arch_enable_sta_mode(); cyw43_arch_wifi_connect_timeout_ms(ssid, pass, CYW43_AUTH_WPA2_AES_PSK, 30000); // fails/times out ``` ## Suggested fix Restore `lwip_context` in the `else` branch of `lwip_freertos_init()`: ```c } else { lwip_context = context; xSemaphoreGive(tcpip_task_blocker); } ``` ## Files involved - `src/rp2_common/pico_lwip/lwip_freertos.c` (`lwip_freertos_init`, `lwip_context`, `pico_lwip_custom_lock_tcpip_core`) ## Note This is separate from, but related in spirit to, the static-allocation task-TCB race in `async_context_freertos_deinit()` that can also crash FreeRTOS apps doing Wi-Fi power-cycling (see report for that issue) — both surfaced while implementing power-saving Wi-Fi on/off cycling with `cyw43_arch_init()`/`cyw43_arch_deinit()`.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
gh-raspberrypi/pico-sdk#3175
No description provided.