From bfc69dbe833a5a513cd51c82f9554a666695d76b Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 11 Feb 2026 16:56:22 +1100 Subject: [PATCH] extmod,rp2: Keep LWIP timer running if lwip poll_sockets() is called. Fixes rp2 issue where socket.getaddrinfo() could block indefinitely if an interface goes down and still has a DNS server configured, as the LWIP timer stops running and can't time out the DNS query. Adds a regression test under multi_wlan that times out on rp2 without this fix. Fixes issue #18797. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- extmod/modlwip.c | 6 +++++ ports/rp2/mpconfigport.h | 1 + ports/rp2/mphalport.h | 1 + ports/rp2/mpnetworkport.c | 15 ++++++++--- tests/multi_wlan/getaddrinfo.py | 39 +++++++++++++++++++++++++++++ tests/multi_wlan/getaddrinfo.py.exp | 3 +++ 6 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 tests/multi_wlan/getaddrinfo.py create mode 100644 tests/multi_wlan/getaddrinfo.py.exp diff --git a/extmod/modlwip.c b/extmod/modlwip.c index 59ef54ca08..4a72a05d84 100644 --- a/extmod/modlwip.c +++ b/extmod/modlwip.c @@ -95,6 +95,11 @@ #define MICROPY_PY_LWIP_EXIT #endif +#ifndef MICROPY_PY_LWIP_POLL_HOOK +// Optional port-level hook called if/when LWIP is being polled +#define MICROPY_PY_LWIP_POLL_HOOK +#endif + #ifdef MICROPY_PY_LWIP_SLIP #include "netif/slipif.h" #include "lwip/sio.h" @@ -360,6 +365,7 @@ static inline bool socket_is_timedout(lwip_socket_obj_t *socket, mp_uint_t ticks } static inline void poll_sockets(void) { + MICROPY_PY_LWIP_POLL_HOOK mp_event_wait_ms(1); } diff --git a/ports/rp2/mpconfigport.h b/ports/rp2/mpconfigport.h index 3c73aa446f..c8df4cdb0e 100644 --- a/ports/rp2/mpconfigport.h +++ b/ports/rp2/mpconfigport.h @@ -294,6 +294,7 @@ typedef intptr_t mp_off_t; #include "pico/rand.h" extern void lwip_lock_acquire(void); extern void lwip_lock_release(void); +extern void lwip_poll_hook(void); #if MICROPY_PY_BLUETOOTH || MICROPY_PY_BLUETOOTH_CYW43 // Bluetooth code only runs in the scheduler, no locking/mutex required. diff --git a/ports/rp2/mphalport.h b/ports/rp2/mphalport.h index 4afc74e6c9..c63354c90c 100644 --- a/ports/rp2/mphalport.h +++ b/ports/rp2/mphalport.h @@ -51,6 +51,7 @@ #define MICROPY_PY_LWIP_ENTER lwip_lock_acquire(); #define MICROPY_PY_LWIP_REENTER lwip_lock_acquire(); #define MICROPY_PY_LWIP_EXIT lwip_lock_release(); +#define MICROPY_PY_LWIP_POLL_HOOK lwip_poll_hook(); // Port level Wait-for-Event macro // diff --git a/ports/rp2/mpnetworkport.c b/ports/rp2/mpnetworkport.c index 675552d1e5..f47429d977 100644 --- a/ports/rp2/mpnetworkport.c +++ b/ports/rp2/mpnetworkport.c @@ -134,6 +134,15 @@ void lwip_lock_release(void) { pendsv_resume(); } +void lwip_poll_hook(void) { + // Start the network soft timer if necessary (it may still only run once, + // but ensure timeouts will complete inside any loop that's polling lwip) + if (mp_network_soft_timer.mode == SOFT_TIMER_MODE_ONE_SHOT) { + mp_network_soft_timer.mode = SOFT_TIMER_MODE_PERIODIC; + soft_timer_reinsert(&mp_network_soft_timer, LWIP_TICK_RATE_MS); + } +} + // This is called by soft_timer and executes at PendSV level. static void mp_network_soft_timer_callback(soft_timer_entry_t *self) { // Run the lwIP internal updates. @@ -179,10 +188,8 @@ void mod_network_lwip_init(void) { static void mp_network_netif_status_cb(struct netif *netif, netif_nsc_reason_t reason, const netif_ext_callback_args_t *args) { // Start the network soft timer any time an interface comes up, unless // it's already running - if (reason == LWIP_NSC_LINK_CHANGED && args->link_changed.state - && mp_network_soft_timer.mode == SOFT_TIMER_MODE_ONE_SHOT) { - mp_network_soft_timer.mode = SOFT_TIMER_MODE_PERIODIC; - soft_timer_reinsert(&mp_network_soft_timer, LWIP_TICK_RATE_MS); + if (reason == LWIP_NSC_LINK_CHANGED && args->link_changed.state) { + lwip_poll_hook(); } if (reason == LWIP_NSC_NETIF_REMOVED) { diff --git a/tests/multi_wlan/getaddrinfo.py b/tests/multi_wlan/getaddrinfo.py new file mode 100644 index 0000000000..6b95c7e6f0 --- /dev/null +++ b/tests/multi_wlan/getaddrinfo.py @@ -0,0 +1,39 @@ +# This is a regression test to ensure getaddrinfo() fails (after a timeout) +# when Wi-Fi is disconnected. +# +# It doesn't require multiple instances, but it does require Wi-Fi to be present +# but not active, and for no other network interface to be configured. The only +# tests which already meet these conditions is here in multi_wlan tests. +try: + from network import WLAN +except (ImportError, NameError): + print("SKIP") + raise SystemExit + +import socket + + +def instance0(): + WLAN(WLAN.IF_AP).active(0) + wlan = WLAN(WLAN.IF_STA) + wlan.active(0) + + multitest.next() + + try: + socket.getaddrinfo("micropython.org", 80) + except OSError as er: + print( + "active(0) failed" + ) # This may fail with code -6 or -2 depending on whether WLAN has been active since reset + + wlan.active(1) + + try: + socket.getaddrinfo("micropython.org", 80) + except OSError as er: + print( + "active(1) failed", er.errno in (-2, -202) + ) # This one should always be -2 or -202 depending on port + + wlan.active(0) diff --git a/tests/multi_wlan/getaddrinfo.py.exp b/tests/multi_wlan/getaddrinfo.py.exp new file mode 100644 index 0000000000..5616a23c72 --- /dev/null +++ b/tests/multi_wlan/getaddrinfo.py.exp @@ -0,0 +1,3 @@ +--- instance0 --- +active(0) failed +active(1) failed True