Repository navigation
-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
Description
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.cdoes the four steps above in a loop and prints every case where that logic says "open" alongsidegetpeername()/TCP_INFO, which Nmap doesn't check.README.mdhas 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 againstv7.80(the version I run in production) and currentmaster(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()). OnSO_ERROR == 0after a wakeup, it callsgetpeername()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_ERRORininit_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
-sTscan 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.
Activity
dmiller-nmap commentedon Oct 2, 2026
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.Verify open ports with getpeername(). Fixes nmap#3523
MrCrLr commentedon Oct 3, 2026
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!
MrCrLr commentedon Oct 6, 2026
@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?