Skip to content

fix(ports): scope the TCP scan timeout to the scanning socket - #3701

Merged
nicolargo merged 1 commit into
nicolargo:developfrom
ntdatt812:fix/ports-scan-socket-timeout-scope
Aug 29, 2026
Merged

nicolargo merged 1 commit into
nicolargo:developfrom
ntdatt812:fix/ports-scan-socket-timeout-scope

Conversation

@ntdatt812

Copy link
Copy Markdown
Contributor

_port_scan_tcp configures its socket with socket.setdefaulttimeout(), which is process-wide, not per-socket — and nothing restores it. After the first port is scanned, every socket created afterwards anywhere in Glances that does not set its own timeout silently inherits the last scanned port's timeout.

Measured on develop, one scan of a closed port with timeout = 3:

1. default timeout at startup       : None
   a fresh socket's timeout         : None
2. after ONE ports scan (timeout=3) : 3.0
   a fresh, unrelated socket now    : 3.0
3. hddtemp's socket inherited it    : 3.0

Line 3 is the point: GlancesGrabHDDTemp.fetch() builds its socket with no timeout of its own, so what it gets now depends on whether the ports plugin happened to run first and on which port it scanned last. Nothing in the tree reads socket.getdefaulttimeout(), so this is not load-bearing anywhere — it is only reach.

_socket.settimeout() sets the same value on the socket the scan actually uses, and leaves the rest of the process alone. Scan results are unchanged: connect_ex behaves the same whether the timeout arrived from the global default or from the socket.

A second defect in the same function

try:
    socket.setdefaulttimeout(port['timeout'])
    _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except Exception as e:
    logger.debug(f"{self.plugin_name}: Error while creating scanning socket ({e})")

# ... falls through anyway
ret = _socket.connect_ex(...)     # UnboundLocalError -> caught, logged as a *scan* error
finally:
    _socket.close()               # UnboundLocalError again -- nothing catches this one

If the socket cannot be created the function keeps going with _socket unbound. The connect_ex failure is swallowed by the "Error while scanning port" handler, which names the wrong thing, and then the finally raises the same error with no handler left:

ESCAPED the handlers -> UnboundLocalError: cannot access local variable '_socket'
where it is not associated with a value

Returning early in the except fixes both.

Tests

tests/test_plugin_ports.py grows a TestTcpScanSocket class: 13 → 17.

Reverting only glances/plugins/ports/__init__.py and keeping the tests:

FAILED tests/test_plugin_ports.py::TestTcpScanSocket::test_scanning_does_not_change_the_process_wide_default_timeout
FAILED tests/test_plugin_ports.py::TestTcpScanSocket::test_a_socket_that_cannot_be_created_is_reported_not_raised
2 failed, 15 passed

With the fix: 17 passed. ruff check and ruff format --check clean on both files.

One note on the first of those two, because it nearly went in useless: it originally read the default timeout at the top and asserted the scan did not change it. That passes on develop — an earlier test in the same session has already set the global to 3, so "unchanged" is trivially true against exactly the state the test exists to forbid. It now pins the default to None first and restores it afterwards, which is what makes it fail without the fix.

The other two cases in the class pin behaviour rather than catching the regression: the scanning socket carries the timeout, and a closed port is still reported offline. Both pass either way, and they are there so a future rewrite cannot quietly drop the timeout or change the result.

The full suite was run on develop and on this branch with the same invocation
(pytest tests/ -q -p no:randomly), and the failure names diffed in both directions:

failed passed errors
develop 52 692 15
this branch 52 696 15

The two failure sets are identical — comm returns nothing in either direction — and the
+4 is exactly the four tests added here. Those 67 pre-existing failures are all in
test_restful.py, test_xmlrpc.py, test_mcp.py, test_browser_restful.py,
test_webui.py and test_perf.py, i.e. the integration tests that need a running server
or node on this (Windows) machine. None are in test_plugin_*.

socket.setdefaulttimeout() is process-wide and was never restored, so
once a port had been scanned every socket created afterwards anywhere in
Glances that does not set its own timeout inherited the last scanned
port's timeout -- the hddtemp grabber among them.

_socket.settimeout() sets the same value on the socket the scan actually
uses and leaves the rest of the process alone. Scan results are
unchanged.

Also return when the socket cannot be created: the code fell through
with _socket unbound, so connect_ex raised UnboundLocalError into the
'Error while scanning port' handler and the finally clause raised it
again with nothing left to catch it.
@nicolargo nicolargo added the bug label Aug 29, 2026
@nicolargo nicolargo added this to the Glances 4.5.7 milestone Aug 29, 2026
@nicolargo
nicolargo merged commit 7df05d8 into nicolargo:develop Aug 29, 2026
17 of 19 checks passed
@nicolargo

Copy link
Copy Markdown
Owner

Thanks (again) @ntdatt812

Just merged into develop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants