Skip to content

socket.getaddrinfo() blocks indefinitely when WiFi active but not connected (RP2350/CYW43) #18797

Description

@paul-matthews

Summary

socket.getaddrinfo() blocks indefinitely on RP2350 (Pico 2 W) when the CYW43 WiFi interface is active (wlan.active(True)) but has no IP address (not yet connected, or connect() still in progress). No timeout is applied, and the call never returns. The device becomes completely unresponsive — no serial output, no exception, no way to recover except a hard reset.

This makes it impossible to write robust networked applications that handle WiFi disconnection gracefully, since any DNS lookup during a WiFi gap will freeze the entire runtime.

Reproduction

# repro_getaddrinfo_hang.py — Raspberry Pi Pico 2 W
# Demonstrates that getaddrinfo() hangs when WiFi is active but not connected.
# Run immediately after boot (before WiFi has connected).

import network
import socket
import time

def run_test():
    wlan = network.WLAN(network.STA_IF)

    # Test 1: WiFi not activated — getaddrinfo fails fast
    wlan.active(False)
    time.sleep(0.5)
    print(f"Test 1: active={wlan.active()}, connected={wlan.isconnected()}")
    try:
        print("  Calling getaddrinfo()...")
        ai = socket.getaddrinfo("example.com", 443, 0, socket.SOCK_STREAM)
        print(f"  Result: {ai[0][-1]}")
    except OSError as e:
        print(f"  OSError: {e}")  # Expected: fails fast with error code -2

    # Test 2: WiFi activated, not connected — getaddrinfo HANGS
    wlan.active(True)
    time.sleep(1)
    print(f"\nTest 2: active={wlan.active()}, connected={wlan.isconnected()}")
    print("  Calling getaddrinfo()...")
    print("  >>> If output stops here, getaddrinfo() is blocking indefinitely <<<")
    try:
        ai = socket.getaddrinfo("example.com", 443, 0, socket.SOCK_STREAM)
        print(f"  Result: {ai[0][-1]}")
    except OSError as e:
        print(f"  OSError: {e}")

    print("\nBoth tests completed without hanging.")

run_test()

Expected behavior

getaddrinfo() should return an error (e.g., OSError with EAI_FAIL or similar) within a reasonable timeout when the network interface has no IP address.

Actual behavior

  • WiFi not active (wlan.active(False)): getaddrinfo() fails immediately with OSError: -2 — correct behavior.
  • WiFi active, not connected (wlan.active(True), no connect() called): getaddrinfo() blocks indefinitely. The call never returns. No timeout, no exception. The device is completely frozen.

Analysis

The issue appears to be in the lwIP DNS resolver. When the WiFi interface is active, lwIP considers the interface "up" and attempts DNS resolution. But with no IP address and no default gateway, the DNS query is sent into the void and lwIP waits for a response that never comes. There is no timeout applied at the getaddrinfo() level.

When the interface is inactive (wlan.active(False)), lwIP correctly determines there is no available interface and fails fast.

Impact

This affects any MicroPython application using requests.get(), urequests.get(), or raw socket.getaddrinfo() on Pico W / Pico 2 W during WiFi connection gaps. The requests module calls getaddrinfo() before connect() or wrap_socket(), so even applications that guard TLS calls will hang at the DNS stage.

The timeout parameter passed to requests.get() has no effect because socket.settimeout() is not applied until after the socket is created — which happens after getaddrinfo().

Environment

  • Board: Raspberry Pi Pico 2 W (RP2350 + CYW43)
  • MicroPython: v1.26.1
  • Pico SDK: 2.1.1
  • Build: Pimoroni Cosmic Unicorn board variant (custom build from pimoroni/unicorn repo, micropython/1.26.0 branch)

Suggested fix

Option A: Apply a timeout to the lwIP DNS query in socket_getaddrinfo() (extmod/modsocket.c), so it fails with OSError after a bounded time.

Option B: Check interface state (has IP address) before initiating DNS resolution and fail immediately if the interface has no network path.

Activity

  1. dpgeorge commented on Feb 10, 2026

    @dpgeorge
    Member

    Thanks for the report. Indeed socket.getaddrinfo() blocks indefinitely if there's no network up.

    But, it's not unresponsive. You can break out of the loop using Ctrl-C at the REPL.

    We should definitely implement Option A (a timeout), and also probably Option B (fail early when possible).

    (We also need an asynchronous DNS interface, eg aiodns.DNSResolver(), but that's a separate issue.)

  2. added
    extmodRelates to extmod/ directory in source
    on Feb 10, 2026
  3. added this to the release-1.28.0 milestone on Feb 10, 2026
  4. added a commit that references this issue on Feb 10, 2026
    b181487
  5. paul-matthews commented on Feb 10, 2026

    @paul-matthews
    Author

    Root Cause Investigation

    I investigated this on a Raspberry Pi Pico 2 W (RP2350 + CYW43439) and found the root cause is two factors combining:

    1. Cached DNS server addresses

    After WiFi disconnect, DNS server addresses from the previous DHCP lease remain in lwIP's state. When dns_gethostbyname() is called, it sees valid DNS servers and enqueues a query, returning ERR_INPROGRESS — even though no network path exists to reach those servers.

    2. Soft timer self-disables (rp2 port)

    At ports/rp2/mpnetworkport.c:150-161, the lwIP soft timer switches to ONE_SHOT mode when no netif has NETIF_FLAG_LINK_UP:

    NETIF_FOREACH(netif) {
        if (netif->flags & NETIF_FLAG_LINK_UP) {
            keep_running = true;
            break;
        }
    }
    mp_network_soft_timer.mode = keep_running ? SOFT_TIMER_MODE_PERIODIC : SOFT_TIMER_MODE_ONE_SHOT;

    Without the periodic timer, dns_tmr() never fires, so lwIP's built-in DNS retry/timeout mechanism is dead. The polling loop at extmod/modlwip.c (the ERR_INPROGRESS case) spins forever.

    Why the hang is indefinite (not ~14s)

    lwIP's DNS has internal retry logic (4 retries × ~14s total), but that retry logic depends on dns_tmr() being called periodically. When the soft timer disables itself, dns_tmr() stops, and the timeout counter never advances. The query sits in a DNS slot permanently.

    Fix

    Submitted as #18801. Two changes to the ERR_INPROGRESS case in extmod/modlwip.c:

    1. Early-fail: Check netif_is_up() && netif_is_link_up() && !ip4_addr_isany_val() before polling. If no interface is fully connected, fail immediately with -2. The LINK_UP check is essential because after disconnect, the DHCP IP lingers but link is down.

    2. Safety-net timeout (20s): Breaks the polling loop if DNS doesn't resolve within 20 seconds, guarding against any other scenario where dns_tmr() doesn't fire.

    Tested all 5 scenarios on Pico 2 W — details in the PR.

  6. peterhinch commented on Feb 10, 2026

    @peterhinch
    Contributor

    The unofficial aiodns is a nonblocking DNS client.

  7. dpgeorge commented on Feb 11, 2026

    @dpgeorge
    Member

    The unofficial aiodns is a nonblocking DNS client.

    Ah, thanks for the link! That's good to know about.

  8. projectgus commented on Feb 17, 2026

    @projectgus
    Contributor

    I've submitted a similar but slightly different fix in #18805 - @paul-matthews if you have a chance to test this out then it'd be much appreciated.

  9. paul-matthews commented on Feb 22, 2026

    @paul-matthews
    Author

    I've submitted a similar but slightly different fix in #18805 - @paul-matthews if you have a chance to test this out then it'd be much appreciated.

    Yep, I'll upload to my Pi Pico 2 W and run my repro scripts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    extmodRelates to extmod/ directory in source

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions