UDP: ICMP errors are reported to the recv callback on Unix but silently discarded on Windows
- Version: v1.x (
f87c8e4); the behaviour long predates it - Platform: Windows (any), contrasted with Linux
What happens
When a UDP datagram elicits an ICMP port unreachable, the resulting error reaches the receive callback on Unix, but is silently discarded on Windows.
src/win/udp.c treats WSAECONNRESET/WSAENETRESET on a receive as "not a real
error" in both the overlapped-completion path and the non-blocking path:
} else if (err == WSAECONNRESET || err == WSAENETRESET) {
/* WSAECONNRESET/WSANETRESET is ignored because this just indicates
* that a previous sendto operation failed.
*/
handle->recv_cb(handle, 0, &buf, NULL, 0);
}Since nread == 0 with addr == NULL is documented as "there is nothing to read", the
application cannot distinguish this from an ordinary spurious wakeup, and the error is
lost. On Unix the same situation is delivered as nread < 0 (UV_ECONNREFUSED).
Why it matters
The information is available from the OS: CPython's own IOCP-based asyncio proactor
surfaces this exact condition as ERROR_PORT_UNREACHABLE (winerror 1234) and hands it
to the application. So this is a libuv policy choice rather than a platform limitation.
It surfaces as a cross-platform behaviour split for libuv bindings. Concretely, in AnyIO
(Python) a task blocked in receive() on a connected UDP socket is woken with an error
on Linux (under uvloop as well as the stdlib backends) and on Windows under the stdlib
proactor event loop, but under winloop (libuv on Windows) it is never woken at all,
because the protocol's error_received() is never called. Context:
https://github.com/agronholm/anyio/pull/1295
Prior art
- #1449 raised the same symptom in 2017 and was closed on the grounds that the docs
describe
nread == 0 && addr == NULLas "nothing to read". The swallow itself dates back to joyent/libuv#1426. UV_UDP_LINUX_RECVERR(flag32) was added in #2872 so applications can opt into fuller ICMP error reporting on Linux, which suggests the project considers this information worth surfacing when it is asked for.docs/src/udp.rstalready documents theuv_udp_bindflag under the platform-neutral nameUV_UDP_RECVERR, even though the enum isUV_UDP_LINUX_RECVERR.
Suggested resolution
Extend the existing opt-in to Windows rather than changing the default:
- Add
UV_UDP_RECVERR = 32as a platform-neutral name, keepingUV_UDP_LINUX_RECVERRas an alias with the same value, so nothing existing breaks. - On Windows, when a handle was bound with that flag, deliver
WSAECONNRESET/WSAENETRESETto the receive callback as a negativenreadwithUV_UDP_RECVERRinflags(mirroring whatuv__udp_recvmsg_errqueuealready does on Linux), without callinguv_udp_recv_stop()— unlike a genuine receive error the socket stays usable, and Unix does not stop reading either.
Failing that, documenting the difference in docs/src/udp.rst would at least let binding
authors discover it from the docs rather than from a hanging test.
I have a patch for the above ready to go (include/uv.h, src/uv-common.h,
src/win/udp.c, docs, and enabling the existing can_recverr path in
test/test-udp-send-unreachable.c on Windows) and will open it as a PR if the approach
looks acceptable. The full test suite passes on Linux.
One caveat in the interest of full disclosure: I do not have a Windows machine, so the Windows code path in that patch is reviewed but not compiled or run — the behaviour described above was observed through winloop in CI and traced back to this code. I have not written a standalone C reproducer either; happy to add one if that would help.
Source: libuv/libuv