Skip to content

net: TcpSocket::connect applies connectTimeout per resolved address and treats EINTR as connect failure #507

Description

@Yaraslaut

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.

Activity

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

    area: coreSubsystem: coretriage: unverifiedPremise plausible but untested; blocked on an experiment

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions