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 <angus@redyak.com.au>
This commit is contained in:
Angus Gratton
2026-02-11 16:56:22 +11:00
committed by Damien George
parent 95b3e72799
commit bfc69dbe83
6 changed files with 61 additions and 4 deletions

View File

@@ -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);
}

View File

@@ -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.

View File

@@ -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
//

View File

@@ -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) {

View File

@@ -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)

View File

@@ -0,0 +1,3 @@
--- instance0 ---
active(0) failed
active(1) failed True