From 2ffdad0525bb7ac5749d22a6fdc343aea2715464 Mon Sep 17 00:00:00 2001 From: "Gary T. Giesen" Date: Fri, 10 Jul 2026 12:44:04 -0400 Subject: [PATCH] Catch UnicodeError in is_reachable_host for overlong names salt.utils.network.is_reachable_host only caught socket.gaierror, but socket.getaddrinfo raises UnicodeError (an idna "label too long" error, which is not a subclass of gaierror) when a name contains a DNS label longer than 63 characters. A long salt-ssh -E/--pcre target triggers this, so _expand_target crashed instead of treating the target as not a reachable host. Also catch UnicodeError and return False. Fixes #57207 --- changelog/57207.fixed.md | 1 + salt/utils/network.py | 4 ++- tests/pytests/unit/utils/test_network.py | 45 ++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 changelog/57207.fixed.md diff --git a/changelog/57207.fixed.md b/changelog/57207.fixed.md new file mode 100644 index 000000000000..574125cb1c55 --- /dev/null +++ b/changelog/57207.fixed.md @@ -0,0 +1 @@ +Fixed salt-ssh crashing with an uncaught UnicodeError when a long ``-E``/``--pcre`` target produces an overlong IDNA label in ``is_reachable_host`` diff --git a/salt/utils/network.py b/salt/utils/network.py index fe299a28f6b6..fa63a8058860 100644 --- a/salt/utils/network.py +++ b/salt/utils/network.py @@ -302,7 +302,9 @@ def is_reachable_host(entity_name): try: assert type(socket.getaddrinfo(entity_name, 0, 0, 0, 0)) == list ret = True - except socket.gaierror: + except (socket.gaierror, UnicodeError): + # UnicodeError is raised (not a subclass of socket.gaierror) when the + # name has an overlong IDNA label, e.g. a long salt-ssh -E/--pcre target. ret = False return ret diff --git a/tests/pytests/unit/utils/test_network.py b/tests/pytests/unit/utils/test_network.py index c8f7044fb5ac..b44ea8ec576b 100644 --- a/tests/pytests/unit/utils/test_network.py +++ b/tests/pytests/unit/utils/test_network.py @@ -1635,3 +1635,48 @@ def test_ip_addrs(linux_interfaces_dict): ): ret = network.ip_addrs6("eth0") assert ret == ["fe80::e23f:49ff:fe85:6aaf"] + + +def test_is_reachable_host_57207(): + """ + A long salt-ssh -E/--pcre target has a single DNS label longer than 63 + characters. socket.getaddrinfo raises UnicodeError ("label too long") from + the idna codec before any network lookup. UnicodeError is not a subclass of + socket.gaierror, so it must be caught explicitly and reported as not + reachable rather than crashing salt-ssh's _expand_target. + """ + # Production-exact call: _expand_target passes the raw target as the single + # positional arg, e.g. salt.utils.network.is_reachable_host(hostname). + # This is the verbatim -E target from the issue; the real getaddrinfo raises + # UnicodeError deterministically (69-char label, no network needed). + assert ( + network.is_reachable_host( + "some-host|some-host|some-host|some-host|some-host|some-host|some-host" + ) + is False + ) + + +def test_is_reachable_host_gaierror_unchanged(): + """ + Inverse / must-not-regress: broadening the except clause to also catch + UnicodeError must not change the pre-existing socket.gaierror path. A name + that fails normal resolution still returns False. Passes with and without + the fix because the gaierror branch is untouched. + """ + with patch.object(socket, "getaddrinfo", MagicMock(side_effect=socket.gaierror)): + assert network.is_reachable_host("nope.invalid") is False + + +def test_is_reachable_host_resolvable_returns_true(): + """ + Peripheral coverage of the success branch: when getaddrinfo returns a list + the host is reported reachable. Guards against the fix accidentally swallowing + a successful lookup. + """ + with patch.object( + socket, + "getaddrinfo", + MagicMock(return_value=[(socket.AF_INET, 0, 0, "", ("127.0.0.1", 0))]), + ): + assert network.is_reachable_host("localhost") is True