Split out of #498's "also noted" section, so it is not lost when that issue closes on its main finding.
Verification status
Inferred from reading the code; not reproduced or timed. Revision: origin/master adfe8e5f plus doc-only commits.
1. connectTimeout is per candidate address, not a total bound
tcp_socket.hpp:133 calls ::poll(&pfd, 1, static_cast<int>(timeout.count())) inside the for (addrinfo* rp = resolved; …) candidate loop, and hints.ai_family = AF_UNSPEC (:98) makes multiple candidates normal — localhost resolves to both ::1 and 127.0.0.1. Worst case the call blocks N × timeout. No elapsed-time accumulator or deadline exists.
Two doc sites state it as a single bound:
tcp_socket.hpp:90-93 — "@PARAM timeout Maximum time to wait for the connection to establish."
socket_backend.hpp:82-83 — "May block up to Config::connectTimeout if destruction races an in-flight (re)connect attempt"
The second matters because it is a destruction claim: a teardown documented as bounded by one timeout can take several.
2. The same poll treats EINTR as connect failure
int const pollRc = ::poll(&pfd, 1, static_cast<int>(timeout.count()));
if (pollRc <= 0) { ::close(fd); continue; }
poll returning -1/EINTR is indistinguishable here from a timeout, so an ordinary delivered signal abandons the candidate — and if it was the only one, connect throws "could not connect".
Every other blocking syscall in this file retries EINTR (:237, :289, :311, :332), and SocketServer::acceptLoop does too (socket_server.hpp:232). The file argues for exactly this at :211-215: "any signal the host happens to deliver (a profiler's timer, SIGCHLD, SIGWINCH) tears down the accept loop and the server silently stops taking connections." The argument is not honoured here.
3. Two smaller ones in the same function
static_cast<int>(timeout.count()) silently truncates a >INT_MAX ms configuration.
- The
getsockopt(SO_ERROR) return at :140 is unchecked, so a failing getsockopt leaves soErr == 0 and a broken socket is returned as connected.
What would change the verdict
- Close (1) if the per-address semantics are intended — then both doc sites need correcting instead, since a caller sizing a teardown budget will get it wrong.
- Raise (2) by running a connect under a periodic signal (a profiler, or
setitimer) and showing spurious failures.
A regression test for (1) needs a host with two candidate addresses where the first blackholes; for (2), a delivered signal mid-connect. Neither exists today, which is why both are invisible.
Split out of #498's "also noted" section, so it is not lost when that issue closes on its main finding.
Verification status
Inferred from reading the code; not reproduced or timed. Revision:
origin/masteradfe8e5fplus doc-only commits.1.
connectTimeoutis per candidate address, not a total boundtcp_socket.hpp:133calls::poll(&pfd, 1, static_cast<int>(timeout.count()))inside thefor (addrinfo* rp = resolved; …)candidate loop, andhints.ai_family = AF_UNSPEC(:98) makes multiple candidates normal —localhostresolves to both::1and127.0.0.1. Worst case the call blocksN × timeout. No elapsed-time accumulator or deadline exists.Two doc sites state it as a single bound:
tcp_socket.hpp:90-93— "@PARAM timeout Maximum time to wait for the connection to establish."socket_backend.hpp:82-83— "May block up toConfig::connectTimeoutif destruction races an in-flight (re)connect attempt"The second matters because it is a destruction claim: a teardown documented as bounded by one timeout can take several.
2. The same
polltreatsEINTRas connect failurepollreturning-1/EINTRis indistinguishable here from a timeout, so an ordinary delivered signal abandons the candidate — and if it was the only one,connectthrows "could not connect".Every other blocking syscall in this file retries
EINTR(:237,:289,:311,:332), andSocketServer::acceptLoopdoes too (socket_server.hpp:232). The file argues for exactly this at:211-215: "any signal the host happens to deliver (a profiler's timer, SIGCHLD, SIGWINCH) tears down the accept loop and the server silently stops taking connections." The argument is not honoured here.3. Two smaller ones in the same function
static_cast<int>(timeout.count())silently truncates a>INT_MAXms configuration.getsockopt(SO_ERROR)return at:140is unchecked, so a failinggetsockoptleavessoErr == 0and a broken socket is returned as connected.What would change the verdict
setitimer) and showing spurious failures.A regression test for (1) needs a host with two candidate addresses where the first blackholes; for (2), a delivered signal mid-connect. Neither exists today, which is why both are invisible.