From 99df7f2e4525295d320f8f998c9c36968d77a151 Mon Sep 17 00:00:00 2001 From: bfbachmann Date: Tue, 16 Jun 2026 15:32:27 -0700 Subject: [PATCH 1/2] Fix incorrect IP family when hostnames resolve to IPv6 addresses --- common/transport/DcgmIpc.cpp | 18 +++++++++++++----- testing/python3/DcgmHandle.py | 8 ++++++++ testing/python3/tests/test_connection.py | 4 ++-- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/common/transport/DcgmIpc.cpp b/common/transport/DcgmIpc.cpp index 86e02afc..4f50f7bc 100644 --- a/common/transport/DcgmIpc.cpp +++ b/common/transport/DcgmIpc.cpp @@ -770,19 +770,27 @@ void DcgmIpc::ConnectTcpAsyncImpl(DcgmIpcConnectTcp &tcpConnect) bufferevent_setcb(bev, DcgmIpc::StaticReadCB, NULL, DcgmIpc::StaticEventCB, this); bufferevent_enable(bev, EV_READ | EV_WRITE); - /* Allow IPv6 IPs to override family */ - sa_family_t family = AF_INET; + /* Determine address family: + * - Bracketed or literal IPv6 address -> AF_INET6 + * - Literal IPv4 address -> AF_INET + * - Hostname -> AF_UNSPEC so libevent resolves it to whichever family DNS returns */ + sa_family_t family = AF_UNSPEC; std::string &hostname = tcpConnect.m_hostname; + char buf[sizeof(struct in6_addr)]; + memset(buf, 0, sizeof(buf)); if (hostname.size() >= 3 && hostname[0] == '[' && hostname[hostname.size() - 1] == ']') { hostname = hostname.substr(1, hostname.size() - 2); + family = AF_INET6; } - char buf[16]; - memset(buf, 0, sizeof(buf)); - if (inet_pton(AF_INET6, hostname.c_str(), buf) > 0) + else if (inet_pton(AF_INET6, hostname.c_str(), buf) > 0) { family = AF_INET6; } + else if (inet_pton(AF_INET, hostname.c_str(), buf) > 0) + { + family = AF_INET; + } int ret = bufferevent_socket_connect_hostname(bev, m_dnsBase, family, hostname.c_str(), tcpConnect.m_port); if (0 != ret) diff --git a/testing/python3/DcgmHandle.py b/testing/python3/DcgmHandle.py index cdd89504..309bcdc1 100644 --- a/testing/python3/DcgmHandle.py +++ b/testing/python3/DcgmHandle.py @@ -16,6 +16,7 @@ import dcgm_structs import dcgm_agent import threading +import ipaddress class DcgmHandle: @@ -98,6 +99,13 @@ def __init__(self, connectParams.persistAfterDisconnect = 0 if ipAddress is not None: + # Wrap bare IPv6 literals in brackets so ParseEndpoint can distinguish + # the address colons from the host:port separator (e.g. "::1" -> "[::1]"). + try: + if ipaddress.ip_address(ipAddress).version == 6: + ipAddress = "[" + ipAddress + "]" + except ValueError: + pass # hostname or already-bracketed address — leave as-is connectToAddress = "tcp://" + ipAddress else: connectToAddress = "unix://" + unixSocketPath diff --git a/testing/python3/tests/test_connection.py b/testing/python3/tests/test_connection.py index 1afe409b..6947393b 100644 --- a/testing/python3/tests/test_connection.py +++ b/testing/python3/tests/test_connection.py @@ -45,7 +45,7 @@ def test_connection_disconnect_error_after_shutdown(): @test_utils.run_with_standalone_host_engine(passAppAsArg=True) def test_dcgm_standalone_connection_disconnect_error_after_hostengine_terminate(handle, hostengineApp): ''' - Test that DCGM_ST_CONNECTION_NOT_VALID is returned when the dcgm API is used after + Test that DCGM_ST_CONNECTION_NOT_VALID is returned when the dcgm API is used after the hostengine process is terminated via `nv-hostengine --term`. ''' @@ -70,7 +70,7 @@ def test_dcgm_standalone_connection_disconnect_error_after_hostengine_terminate( @test_utils.run_with_standalone_host_engine(passAppAsArg=True) def test_dcgm_standalone_connection_disconnect_error_after_hostengine_murder(handle, hostengineApp): ''' - Test that DCGM_ST_CONNECTION_NOT_VALID is returned when the dcgm API is used after + Test that DCGM_ST_CONNECTION_NOT_VALID is returned when the dcgm API is used after the hostengine process is killed via a `SIGKILL` signal. ''' handle = pydcgm.DcgmHandle(handle) From c0ac7f5c23d1f88c1b03d4f54a215dd639d085c7 Mon Sep 17 00:00:00 2001 From: bfbachmann Date: Tue, 16 Jun 2026 16:35:29 -0700 Subject: [PATCH 2/2] Add test --- testing/python3/tests/test_connection.py | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/testing/python3/tests/test_connection.py b/testing/python3/tests/test_connection.py index 6947393b..151cd9ac 100644 --- a/testing/python3/tests/test_connection.py +++ b/testing/python3/tests/test_connection.py @@ -129,6 +129,51 @@ def test_dcgm_ipv6_loopback(): nvHe.validate() +@test_utils.run_only_as_root() +@test_utils.run_with_ipv6_enabled() +def test_dcgm_ipv6_hostname_resolution(): + ''' + Verify that a hostname which resolves exclusively to an IPv6 address can connect to a + host engine bound to [::1]. This exercises the AF_UNSPEC path in ConnectTcpAsyncImpl — + the fix for the bug where AF_INET was hardcoded, preventing hostname→IPv6 resolution. + The test is skipped when no IPv6-only hostname alias is available on the system. + ''' + import socket as _socket + + def _resolves_only_to_ipv6(hostname): + try: + if _socket.getaddrinfo(hostname, None, _socket.AF_INET): + return False + except _socket.gaierror: + pass + try: + return bool(_socket.getaddrinfo(hostname, None, _socket.AF_INET6)) + except _socket.gaierror: + return False + + # ip6-loopback / ip6-localhost are standard aliases for ::1 on many Linux distros + test_hostname = next( + (h for h in ('ip6-loopback', 'ip6-localhost') if _resolves_only_to_ipv6(h)), + None, + ) + if test_hostname is None: + test_utils.skip_test("No hostname found that resolves exclusively to an IPv6 address") + + nvHe = apps.NvHostEngineApp(['-b', '[::1]']) + nvHe.start(timeout=90) + + handle = pydcgm.DcgmHandle(ipAddress=test_hostname, timeoutMs=90) + assert handle + dcgmSystem = handle.GetSystem() + dcgmSystem.discovery.GetAllGpuIds() + + del handle + handle = None + + nvHe.terminate() + nvHe.validate() + + @test_utils.run_with_standalone_host_engine(20) @test_utils.run_only_with_live_gpus() def test_dcgm_connection_client_cleanup(handle, gpuIds):