Skip to content

-sT connect scan reports a port open on SO_ERROR == 0 alone; a spurious wakeup after an ICMP error can make that read 0 with no connection made #3523

Closed

Description

@MrCrLr

Describe the bug

sendConnectScanProbe / do_one_select_round / handleConnectResult in scan_engine_connect.cc decide a -sT probe is open purely from getsockopt(SO_ERROR) == 0 after select() reports the socket ready (read, write, or except set). There is no further check that a connection actually exists.

On Linux, I found that when the only reply to a connect attempt is an ICMP error (host-unreachable or time-exceeded), select() can wake the caller while SO_ERROR still reads 0, before the ICMP-derived error becomes visible through that call. Re-reading SO_ERROR on the same socket about 1ms later returns the real error. In that window, getpeername() already reports the socket as not connected, and TCP_INFO already shows tcpi_state == 7 (CLOSE). -sT reports the port open regardless.

This is not particular to Nmap's own bookkeeping: a roughly 180-line C program that does only socket(), then a non-blocking connect(), then select() (read+write+except), then getsockopt(SO_ERROR), with no other logic, reproduces the same false "success" against a target that can never legitimately answer. See the attached reproducer.

I ran into this via a target's connection whose route loops between two routers, so every real connection attempt there returns only ICMP errors. -sT intermittently reported open ports on that host that were not open. I could not correlate it to Nmap's own retry/probe bookkeeping; it reproduces even in a program with none of that.

To Reproduce

Attached: nmap-connect-scan-race-mwe.zip

  • setup-netns.sh (root) builds a local routing loop with plain Linux network namespaces. Nothing at the target range (198.18.50.0/24, RFC 2544 benchmarking space, chosen so it can't collide with a real network) can ever answer a connection; only ICMP errors come back.
  • repro.c does the four steps above in a loop and prints every case where that logic says "open" alongside getpeername()/TCP_INFO, which Nmap doesn't check.
  • README.md has the exact commands and what to expect.

I measured 8 false "successes" in 5,000,000 attempts in that environment (order of 1 per several hundred thousand; it's a race, and the rate rose under CPU contention). Every one had the identical signature: SO_ERROR == 0 at the select() wakeup, getpeername() ENOTCONN, TCP_INFO already CLOSE, SO_ERROR correct about 1ms later.

I also confirmed the same signature with nmap -sT itself against the same loop (--reason, -d for debug output), and with an A/B test against a small patch (below): 14 false opens in 1,085,850 probes unpatched, 0 in the same probe count patched (26 caught and retried), no change in scan time.

Expected behavior

-sT should not report a port open unless a connection was actually established. getpeername() (already used a few lines below for the self-connect case) distinguishes this reliably in our testing.

Version info

  • OS: Linux (reproduced on kernel 6.10, x86_64 and arm64; also reproduced independently in a separate Docker-based lab with a real ~60ms path, same signature)
  • nmap --version: reproduced against v7.80 (the version I run in production) and current master (11fd5e7)
  • Also present in master, so it isn't fixed by any Nmap version currently released

Additional context

  • Go's standard library already guards against exactly this (src/net/fd_unix.go, connect()). On SO_ERROR == 0 after a wakeup, it calls getpeername() and waits again if that fails, citing Go issues 14548 and 19289 ("the runtime poller can wake us up spuriously").
  • Nmap already treats ICMP-during-connect specially on Windows (TCP_FAIL_CONNECT_ON_ICMP_ERROR in init_socket), so the class of problem (platforms not reliably surfacing ICMP errors to a connecting socket) is one the codebase already accounts for on that platform. This is the Linux-side equivalent gap.
  • I have a fix and a PR to follow. This affects any -sT scan of a host whose path returns only ICMP errors, for example a routing loop, which is a real (if unusual) network condition, not a lab artifact.

nmap-mwe.zip

Activity

  1. dmiller-nmap commented on Oct 2, 2026

    @dmiller-nmap

    This was a very interesting issue! I was not able to reproduce using the repro.c, but I found several cases of people reporting the same behavior in other applications. I've applied a different fix based on notes by DJB.

  2. added a commit that references this issue on Oct 3, 2026
    f23ea5a
  3. MrCrLr commented on Oct 3, 2026

    @MrCrLr
    Author

    It was indeed very interesting and very confusing for me to get to the bottom of. I had quite a ride tracing this all the way back through several interdependent services. Thanks for taking the time to look into this!

  4. MrCrLr commented on Oct 6, 2026

    @MrCrLr
    Author

    @dmiller-nmap Thanks again for the fix. I have confirmed it on our side: false opens went from 13 to 0 over ~1M probes with f23ea5a, same result backported to 7.80. Is there a rough timeframe for the next release that will include it?

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions