Skip to content

extmod,rp2: Keep LWIP timer running if lwip poll_sockets() is called. - #18805

Merged
dpgeorge merged 1 commit into
micropython:masterfrom
projectgus:bugfix/rp2_dns_timeout
Mar 4, 2026
Merged

dpgeorge merged 1 commit into
micropython:masterfrom
projectgus:bugfix/rp2_dns_timeout

Conversation

@projectgus

@projectgus projectgus commented Feb 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

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 (since #17272) and can't time out the DNS query.

Fixes #18797.

This work was funded through GitHub Sponsors.

Testing

Was going to commit a regression test for this, but haven't found a good approach - the current net_inet tests don't know which interface is in use so they can't disable it, and the other multi_wlan tests don't assume internet connectivity. Suggestions welcome!

Manual test code is simple:

# have boot.py configure `wlan` interface and connect
print(wlan)
dns = socket.getaddrinfo('micropython.org', 80)
print(dns)
print('disconnecting and retrying...')
wlan.disconnect()
wlan.active(False)
time.sleep(1)
dns = socket.getaddrinfo('example.com', 80)
print(dns)

Hangs indefinitely without this fix. Fails after the DNS timeout with this fix.

Trade-offs and Alternatives

  • A different fix was submitted by @paul-matthews as extmod/modlwip: Fix getaddrinfo blocking indefinitely without network. #18801 (thanks!) Compared to that PR, this PR should have smaller code size impact and may help LWIP's internal accounting if there's other places MicroPython blocks in a loop calling poll_sockets() (unconfirmed).

    The other PR also immediately fails getaddrinfo() if no netif has the link up. I'm not sure if this is desirable behaviour or not, as there are cases where the link can "flap" (i.e. poor Wi-Fi signal, other end of network cable power cycles, etc) and in those cases the internal LWIP DNS retries may actually succeed. I think another way to implement "fast failures" may be to have either wlan.disconnect() or wlan.active(False) clear the DNS server setting (and/or release DHCP). Curious what you think, @paul-matthews?

  • I also tried changing the "stop timer" logic to keep the timer running if any UDP sockets were active (currently this check is for active TCP sockets only). This would cover DNS and other possible UDP activity. However, this approach isn't viable as LWIP DHCP currently keeps a UDP pbuf registered even when the interface is down, and the LWIP mDNS resolver permanently registers a single static UDP pbuf on init and never removes it.

@projectgus
projectgus requested a review from dpgeorge February 11, 2026 06:15
@projectgus projectgus added extmod Relates to extmod/ directory in source port-rp2 labels Feb 11, 2026
@projectgus

projectgus commented Feb 11, 2026 •

Copy link
Copy Markdown
Contributor Author

@dpgeorge Unfortunately I still don't really know how we handle this for a hypothetical future "non-blocking getaddrinfo()" as previous discussed. Probably for this we would need to reference count pending DNS queries and use this as an extra condition for keeping the timer alive.

EDIT: However there are other problems with a hypothetical async version, such as the current code assumes at most one outstanding DNS query. So can probably tackle if/when we write the async version.

@github-actions

github-actions Bot commented Feb 11, 2026 •

Copy link
Copy Markdown

Code size report:

Reference:  tests/extmod_hardware/machine_pwm.py: Add alif pin config. [95b3e72]
Comparison: extmod,rp2: Keep LWIP timer running if lwip poll_sockets() is called. [merge of bfc69db]
  mpy-cross:    +0 +0.000% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32:    +0 +0.000% PYBV10
      esp32:    +0 +0.000% ESP32_GENERIC
     mimxrt:    +0 +0.000% TEENSY40
        rp2:    +8 +0.001% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

@codecov

codecov Bot commented Feb 11, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.42%. Comparing base (95b3e72) to head (bfc69db).
⚠️ Report is 4 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #18805   +/-   ##
=======================================
  Coverage   98.42%   98.42%           
=======================================
  Files         174      174           
  Lines       22331    22331           
=======================================
  Hits        21980    21980           
  Misses        351      351           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@dpgeorge

Copy link
Copy Markdown
Member

Thanks for fixing this. The solution here looks good, although it would have been ideal if we could have somehow queried lwIP's active UDPs like we do with TCPs. But as you say, that won't work because of permanent UDP pbufs 😞

So I think the approach here is the next best thing.

Regarding tests: I realised that multi tests can work just fine with only a single instance, and could write the following "multi" test:

import socket, network

def instance0():
    wlan = network.WLAN()
    wlan.active(0)

    try:
        socket.getaddrinfo('micropython.org', 80)
    except OSError as er:
        print(repr(er))

    wlan.active(1)

    try:
        socket.getaddrinfo('micropython.org', 80)
    except OSError as er:
        print(repr(er))

    wlan.active(0)

You can run that with ./run-multitests.py -t a0 multi_wlan/getaddrinfo.py and it'll work. Without the fix here it will time out. With the fix it will run to completion and print two OSError's. Unfortunately the first one is -6 if the interface was never activated, but -2 if it has been activated (and hence the netif registered with lwIP). So it needs some tweaking to make it a proper test, but I think this is an acceptable way to test this, because the test needs network.WLAN but not an active connection (so multi_wlan/ seems like the best place for it).

@paul-matthews

Copy link
Copy Markdown

Thanks for the thorough analysis and the cleaner fix, @projectgus. I agree #18805 is the right root-cause fix — keeping the timer alive so DNS timeouts actually work is much better than working around it at the getaddrinfo level. The +8 vs +64 byte difference speaks for itself too.

On the fast-failure question — my use case is a 24/7 IoT device where WiFi drops and reconnects periodically. Application-level retry logic (priority queue with exponential backoff) handles reconnection. From that perspective, fast-failing DNS when WiFi is known-down is genuinely useful: waiting ~14s for lwIP's internal DNS timeout when there's no link wastes time the application could spend reconnecting WiFi. But I can see how on a device with a flaky WiFi signal, the link might recover mid-DNS-timeout, and lwIP's retries could succeed without the application needing to know.

The idea of clearing DNS server settings on wlan.disconnect() / wlan.active(False) is appealing. It would mean dns_gethostbyname() sees no DNS servers and returns ERR_ARG immediately — fast failure without needing netif checks in getaddrinfo(), and the cleanup is semantically correct (disconnected = no DNS config). DHCP release would be the cleanest version of this since it clears DNS servers, IP, gateway, and netmask all at once.

If you go that route, the combination of #18805 (timer stays alive → DNS timeouts work) plus DNS cleanup on disconnect (→ fast failure when interface is explicitly taken down) would cover both the indefinite-hang bug and the "don't waste 14s when WiFi is gone" scenario cleanly. Happy to close #18801 in favour of that approach.

@projectgus
projectgus force-pushed the bugfix/rp2_dns_timeout branch from 3d9ed78 to 4cff0a5 Compare February 25, 2026 04:54
@projectgus

Copy link
Copy Markdown
Contributor Author

Thanks @dpgeorge, have added the test code you suggested to the PR and verified it fails on rp2 without this fix, and passes on both rp2 and esp32 ports otherwise.

Thanks @paul-matthews for the feedback. Will have a look at adding a fix so DNS is cleared when Wi-Fi disconnects from the MicroPython side. It's hard to know what to do if Wi-Fi disconnects (perhaps temporarily) from the other side, @dpgeorge is keen to add a non-blocking getaddrinfo variant and that might be the best in the long term (i.e. if you detect Wi-Fi disconnected while waiting for a DNS response then your application can abort whatever it's doing and go back to wait for Wi-Fi to connect.)

I'll look at putting up a PR to handle the case of failing immediately if the Python code has disconnected or deactivated the interface, though.

@dpgeorge dpgeorge added this to the release-1.28.0 milestone Feb 25, 2026
Comment thread tests/multi_wlan/getaddrinfo.py
Comment thread tests/multi_wlan/getaddrinfo.py Outdated


def instance0():
wlan = network.WLAN()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran this test as per ./run-multitests.py -t a0 multi_wlan/*.py (against a RPI_PICO_W and standard unix micropython) and it didn't work as expected. Namely, it would pass without the fix from this PR.

That's because multi_wlan/01_ap_sta.py runs first and enables the AP interface without disabling it (because the test is skipped, but before it knows it should be skipped it's already started the AP). Since AP is active, that keeps the lwIP timer active.

Solution which worked for me was to add a line here (at the start of instance0) which disables the AP:

network.WLAN(network.WLAN.IF_AP).active(0)

(may want to guard that by if hasattr(network.WLAN, "IF_AP"): to be careful)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right, I had run multi_wlan/*.py but only with two instances. Fixed now.

I'd also been relying on the default argument of network.WLAN(), but this isn't documented to return any particular interface so I made it explicit.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for updating. I tested it:

  • without the fix in this PR, running all the multi_wlan tests over and over, the one added here fails consistently
  • with the fix, it passes consistently

@projectgus
projectgus force-pushed the bugfix/rp2_dns_timeout branch from 4cff0a5 to 641aeb3 Compare March 3, 2026 03:09
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 micropython#18797.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <[email protected]>
@dpgeorge
dpgeorge force-pushed the bugfix/rp2_dns_timeout branch from 641aeb3 to bfc69db Compare March 4, 2026 00:45
@dpgeorge
dpgeorge merged commit bfc69db into micropython:master Mar 4, 2026
72 of 73 checks passed
@projectgus
projectgus deleted the bugfix/rp2_dns_timeout branch March 5, 2026 05:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

extmod Relates to extmod/ directory in source port-rp2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants