Summary
src/rp2_common/pico_cyw43_arch/CMakeLists.txt:70 unconditionally sets LWIP_PROVIDE_ERRNO=1 on the pico_cyw43_arch_lwip_sys_freertos variant. Under FreeRTOS+newlib (the only libc bundled with the SDK's FreeRTOS port), this causes a silent storage divergence: lwIP writes errno values to its own extern int errno global, while any POSIX-style code reading errno via <errno.h> hits newlib's thread-local (*__errno()) macro. The two are different memory locations.
The bug is silent in code that uses lwIP's own socket API (which reads lwIP's global directly), but fatal for code that follows the standard mbedTLS BIO pattern — i.e. mbedTLS's stock library/net_sockets.c, or any port of it. That code does non-blocking recv(), reads errno, and translates EAGAIN/EWOULDBLOCK into MBEDTLS_ERR_SSL_WANT_READ so the TLS state machine retries. With the storage mismatch, the shim reads 0 from newlib's untouched thread-local and treats every empty poll as a hard error → session tear-down.
Affected configuration
pico_cyw43_arch_lwip_sys_freertos (Pico W / Pico 2W under FreeRTOS)
- lwIP + mbedTLS + any TCP-bytestream client that uses the stock mbedTLS BIO (or an equivalent shim reading errno via
<errno.h>)
- The bug surfaces at the first non-blocking
recv() after the TLS handshake completes — typical observable: TLS handshake succeeds, application sends e.g. MQTT CONNECT, server queues CONNACK, client immediately FINs the connection without reading the response.
Root cause
pico_cyw43_arch/CMakeLists.txt:
target_compile_definitions(pico_cyw43_arch_lwip_sys_freertos_headers INTERFACE
CYW43_LWIP=1
LWIP_PROVIDE_ERRNO=1 # <-- declares lwIP's own global errno
...
)
LWIP_PROVIDE_ERRNO=1 makes lwip/errno.h declare extern int errno; and define the E* constants itself. That's the correct choice for bare-metal builds where no libc owns errno. For FreeRTOS+newlib, newlib already provides a thread-local errno (with all the same E* constants), and lwIP's global silently diverges from it.
Fix proposal
Switch the FreeRTOS+lwIP variant to LWIP_ERRNO_STDINCLUDE=1, which keeps lwIP's errno-constants behaviour but routes through <errno.h> — so lwIP and the rest of the program share newlib's thread-local storage:
target_compile_definitions(pico_cyw43_arch_lwip_sys_freertos_headers INTERFACE
CYW43_LWIP=1
- LWIP_PROVIDE_ERRNO=1
+ LWIP_ERRNO_STDINCLUDE=1
...
)
This is behaviour-preserving for users who don't touch errno directly (lwIP's E* constants still resolve, just through <errno.h>). It is correcting for users on the stock mbedTLS BIO pattern.
The threadsafe_background (bare-metal) variant intentionally does not set LWIP_PROVIDE_ERRNO=1 in the current code, so no change there is needed.
Workarounds available today
A user can override the SDK's -D by defining LWIP_ERRNO_STDINCLUDE and #undef LWIP_PROVIDE_ERRNO in their own lwipopts.h. This works but the symptom is hard to diagnose without first understanding the storage divergence — the visible failure mode (TLS connection that handshakes successfully then dies on first recv()) leads most developers to look at TLS config, mbedTLS flags, cert chains, etc. first.
PR
PR with the proposed change against develop: #2974
Summary
src/rp2_common/pico_cyw43_arch/CMakeLists.txt:70unconditionally setsLWIP_PROVIDE_ERRNO=1on thepico_cyw43_arch_lwip_sys_freertosvariant. Under FreeRTOS+newlib (the only libc bundled with the SDK's FreeRTOS port), this causes a silent storage divergence: lwIP writes errno values to its ownextern int errnoglobal, while any POSIX-style code readingerrnovia<errno.h>hits newlib's thread-local(*__errno())macro. The two are different memory locations.The bug is silent in code that uses lwIP's own socket API (which reads lwIP's global directly), but fatal for code that follows the standard mbedTLS BIO pattern — i.e. mbedTLS's stock
library/net_sockets.c, or any port of it. That code does non-blockingrecv(), readserrno, and translatesEAGAIN/EWOULDBLOCKintoMBEDTLS_ERR_SSL_WANT_READso the TLS state machine retries. With the storage mismatch, the shim reads0from newlib's untouched thread-local and treats every empty poll as a hard error → session tear-down.Affected configuration
pico_cyw43_arch_lwip_sys_freertos(Pico W / Pico 2W under FreeRTOS)<errno.h>)recv()after the TLS handshake completes — typical observable: TLS handshake succeeds, application sends e.g. MQTTCONNECT, server queuesCONNACK, client immediately FINs the connection without reading the response.Root cause
pico_cyw43_arch/CMakeLists.txt:LWIP_PROVIDE_ERRNO=1makeslwip/errno.hdeclareextern int errno;and define theE*constants itself. That's the correct choice for bare-metal builds where no libc ownserrno. For FreeRTOS+newlib, newlib already provides a thread-local errno (with all the sameE*constants), and lwIP's global silently diverges from it.Fix proposal
Switch the FreeRTOS+lwIP variant to
LWIP_ERRNO_STDINCLUDE=1, which keeps lwIP's errno-constants behaviour but routes through<errno.h>— so lwIP and the rest of the program share newlib's thread-local storage:target_compile_definitions(pico_cyw43_arch_lwip_sys_freertos_headers INTERFACE CYW43_LWIP=1 - LWIP_PROVIDE_ERRNO=1 + LWIP_ERRNO_STDINCLUDE=1 ... )This is behaviour-preserving for users who don't touch
errnodirectly (lwIP'sE*constants still resolve, just through<errno.h>). It is correcting for users on the stock mbedTLS BIO pattern.The
threadsafe_background(bare-metal) variant intentionally does not setLWIP_PROVIDE_ERRNO=1in the current code, so no change there is needed.Workarounds available today
A user can override the SDK's
-Dby definingLWIP_ERRNO_STDINCLUDEand#undef LWIP_PROVIDE_ERRNOin their ownlwipopts.h. This works but the symptom is hard to diagnose without first understanding the storage divergence — the visible failure mode (TLS connection that handshakes successfully then dies on firstrecv()) leads most developers to look at TLS config, mbedTLS flags, cert chains, etc. first.PR
PR with the proposed change against
develop: #2974