From 08b6195f8a67d293ddbb693076166d10d5e2cee5 Mon Sep 17 00:00:00 2001 From: Milos Kotlar Date: Tue, 12 May 2026 10:50:40 +0200 Subject: [PATCH 1/5] [Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses Dns falls back to resolving plain 'localhost' when the OS resolver fails or returns zero addresses for a '*.localhost' subdomain (RFC 6761 Section 6.3). However, on Android the bionic getaddrinfo returns non-loopback addresses (link-local fe80::* and globally-routable IPv6) for '*.localhost', bypassing the fallback and causing Dns.GetHostAddresses("foo.localhost.") to return non-loopback addresses. This was caused by the fallback condition only triggering on empty or failed OS responses; it is now extended to also trigger when the OS returns only non-loopback addresses, in both the sync and async paths. Fixes #127965. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Net/Dns.cs | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs index 67c29760dea20b..8f556dc7a1cf25 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs @@ -463,6 +463,24 @@ private static bool IsLocalhostSubdomain(string hostName) return length > Localhost.Length && IsReservedName(hostName, Localhost); } + private static bool HasNoLoopbackAddress(IPAddress[] addresses) + { + if (addresses.Length == 0) + { + return false; + } + + foreach (IPAddress address in addresses) + { + if (IPAddress.IsLoopback(address)) + { + return false; + } + } + + return true; + } + /// /// Tries to handle RFC 6761 "invalid" domain names. /// Returns true if the host name is an invalid domain (exception will be set). @@ -522,10 +540,11 @@ private static object GetHostEntryOrAddressesCore(string hostName, bool justAddr throw CreateException(errorCode, nativeErrorCode); } } - else if (addresses.Length == 0 && IsLocalhostSubdomain(hostName)) + else if (IsLocalhostSubdomain(hostName) && + (addresses.Length == 0 || HasNoLoopbackAddress(addresses))) { - // RFC 6761 Section 6.3: If localhost subdomain returns empty addresses, fall back to plain "localhost". - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned empty, falling back to 'localhost'"); + // RFC 6761 Section 6.3: fall back to plain "localhost" when a localhost subdomain returns no loopback addresses. + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned no loopback addresses, falling back to 'localhost'"); NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: justAddresses ? addresses : (object)new IPHostEntry { AddressList = addresses, HostName = newHostName!, Aliases = aliases }, exception: null); fallbackToLocalhost = true; } @@ -788,10 +807,11 @@ static async Task CompleteAsync(Task task, string hostName, bool justAddresse { result = await ((Task)task).ConfigureAwait(false); - // RFC 6761 Section 6.3: If localhost subdomain returns empty addresses, fall back to plain "localhost". - if (isLocalhostSubdomain && result is IPAddress[] addresses && addresses.Length == 0) + // RFC 6761 Section 6.3: fall back to plain "localhost" when a localhost subdomain returns no loopback addresses. + if (isLocalhostSubdomain && result is IPAddress[] addresses && + (addresses.Length == 0 || HasNoLoopbackAddress(addresses))) { - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned empty, falling back to 'localhost'"); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned no loopback addresses, falling back to 'localhost'"); NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null); fallbackOccurred = true; @@ -799,9 +819,10 @@ static async Task CompleteAsync(Task task, string hostName, bool justAddresse return await ((Task)(Task)Dns.GetHostAddressesAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false); } - if (isLocalhostSubdomain && result is IPHostEntry entry && entry.AddressList.Length == 0) + if (isLocalhostSubdomain && result is IPHostEntry entry && + (entry.AddressList.Length == 0 || HasNoLoopbackAddress(entry.AddressList))) { - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned empty, falling back to 'localhost'"); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned no loopback addresses, falling back to 'localhost'"); NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null); fallbackOccurred = true; From a5360362d6c65e9ed715a7bef253b9889a7f0ca5 Mon Sep 17 00:00:00 2001 From: Milos Kotlar Date: Tue, 12 May 2026 12:43:50 +0200 Subject: [PATCH 2/5] Re-enable Android/Apple-mobile localhost subdomain Dns tests The previous [ActiveIssue] skips on six DnsGetHostAddresses_/DnsGetHostEntry_LocalhostSubdomain* tests (referencing #126456 and #127965) all covered the same root cause: platform resolvers (Android bionic, iOS/tvOS/MacCatalyst) returned non-loopback addresses for '*.localhost' subdomains, bypassing the existing RFC 6761 6.3 fallback. With the fallback now extended to trigger when the OS returns no loopback addresses, these tests are expected to pass on every platform. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/FunctionalTests/GetHostAddressesTest.cs | 3 --- .../tests/FunctionalTests/GetHostEntryTest.cs | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs index 532ee396450df1..e98abe1ab53e2f 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs @@ -231,7 +231,6 @@ public async Task DnsGetHostAddresses_InvalidDomain_ThrowsHostNotFound(string ho [InlineData("test.localhost")] [InlineData("FOO.LOCALHOST")] [InlineData("Test.LocalHost")] - [ActiveIssue("https://github.com/dotnet/runtime/issues/126456", TestPlatforms.Android)] public async Task DnsGetHostAddresses_LocalhostSubdomain_ReturnsLoopback(string hostName) { // The subdomain goes to OS resolver first. If it fails (likely on most systems), @@ -285,7 +284,6 @@ public async Task DnsGetHostAddresses_LocalhostSubdomain_RespectsAddressFamily(A // 3. Different systems configure localhost differently // The key requirement is that localhost subdomains return loopback addresses. [Fact] - [ActiveIssue("https://github.com/dotnet/runtime/issues/126456", TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst | TestPlatforms.Android)] public async Task DnsGetHostAddresses_LocalhostAndSubdomain_BothReturnLoopback() { IPAddress[] localhostAddresses = Dns.GetHostAddresses("localhost"); @@ -308,7 +306,6 @@ public async Task DnsGetHostAddresses_LocalhostAndSubdomain_BothReturnLoopback() } // RFC 6761: Localhost subdomains with trailing dot should work (e.g., "foo.localhost.") - [ActiveIssue("https://github.com/dotnet/runtime/issues/127965", TestPlatforms.Android)] [Theory] [InlineData("foo.localhost.")] [InlineData("bar.test.localhost.")] diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs index eba2b5c8b79ec9..92f2b9c69f842a 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs @@ -353,7 +353,6 @@ public async Task DnsGetHostEntry_InvalidDomain_ThrowsHostNotFound(string hostNa [InlineData("test.localhost")] [InlineData("FOO.LOCALHOST")] [InlineData("Test.LocalHost")] - [ActiveIssue("https://github.com/dotnet/runtime/issues/126456", TestPlatforms.Android)] public async Task DnsGetHostEntry_LocalhostSubdomain_ReturnsLoopback(string hostName) { // The subdomain goes to OS resolver first. If it fails (likely on most systems), @@ -450,7 +449,6 @@ public async Task DnsGetHostEntry_LocalhostSubdomain_RespectsAddressFamily(Addre // 3. Different systems configure localhost differently // The key requirement is that localhost subdomains return loopback addresses. [Fact] - [ActiveIssue("https://github.com/dotnet/runtime/issues/126456", TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst | TestPlatforms.Android)] public async Task DnsGetHostEntry_LocalhostAndSubdomain_BothReturnLoopback() { IPHostEntry localhostEntry = Dns.GetHostEntry("localhost"); @@ -477,7 +475,6 @@ public async Task DnsGetHostEntry_LocalhostAndSubdomain_BothReturnLoopback() [Theory] [InlineData("foo.localhost.")] [InlineData("bar.test.localhost.")] - [ActiveIssue("https://github.com/dotnet/runtime/issues/126456", TestPlatforms.Android)] public async Task DnsGetHostEntry_LocalhostSubdomainWithTrailingDot_ReturnsLoopback(string hostName) { IPHostEntry entry = Dns.GetHostEntry(hostName); From 5c61d5b0cdef4e98426a2e118ed944c3d1e71b5d Mon Sep 17 00:00:00 2001 From: Milos Kotlar Date: Tue, 12 May 2026 19:42:14 +0200 Subject: [PATCH 3/5] Treat empty address array as 'no loopback addresses' in HasNoLoopbackAddress Address review feedback: HasNoLoopbackAddress previously returned false for an empty array, forcing every caller to spell the condition as 'Length == 0 || HasNoLoopbackAddress(...)'. An empty result has no loopback addresses by definition, so the helper now returns true in that case, and the three call sites are simplified accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System.Net.NameResolution/src/System/Net/Dns.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs index 8f556dc7a1cf25..507e9360ff0f58 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs @@ -465,11 +465,6 @@ private static bool IsLocalhostSubdomain(string hostName) private static bool HasNoLoopbackAddress(IPAddress[] addresses) { - if (addresses.Length == 0) - { - return false; - } - foreach (IPAddress address in addresses) { if (IPAddress.IsLoopback(address)) @@ -540,8 +535,7 @@ private static object GetHostEntryOrAddressesCore(string hostName, bool justAddr throw CreateException(errorCode, nativeErrorCode); } } - else if (IsLocalhostSubdomain(hostName) && - (addresses.Length == 0 || HasNoLoopbackAddress(addresses))) + else if (IsLocalhostSubdomain(hostName) && HasNoLoopbackAddress(addresses)) { // RFC 6761 Section 6.3: fall back to plain "localhost" when a localhost subdomain returns no loopback addresses. if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned no loopback addresses, falling back to 'localhost'"); @@ -809,7 +803,7 @@ static async Task CompleteAsync(Task task, string hostName, bool justAddresse // RFC 6761 Section 6.3: fall back to plain "localhost" when a localhost subdomain returns no loopback addresses. if (isLocalhostSubdomain && result is IPAddress[] addresses && - (addresses.Length == 0 || HasNoLoopbackAddress(addresses))) + HasNoLoopbackAddress(addresses)) { if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned no loopback addresses, falling back to 'localhost'"); NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null); @@ -820,7 +814,7 @@ static async Task CompleteAsync(Task task, string hostName, bool justAddresse } if (isLocalhostSubdomain && result is IPHostEntry entry && - (entry.AddressList.Length == 0 || HasNoLoopbackAddress(entry.AddressList))) + HasNoLoopbackAddress(entry.AddressList)) { if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain returned no loopback addresses, falling back to 'localhost'"); NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null); From 672977c1ac60746726e88f500235021bc6b2b220 Mon Sep 17 00:00:00 2001 From: Milos Kotlar Date: Wed, 13 May 2026 13:21:54 +0200 Subject: [PATCH 4/5] Update src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs --- .../tests/FunctionalTests/GetHostAddressesTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs index e98abe1ab53e2f..3273ebc49cffc7 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs @@ -284,6 +284,7 @@ public async Task DnsGetHostAddresses_LocalhostSubdomain_RespectsAddressFamily(A // 3. Different systems configure localhost differently // The key requirement is that localhost subdomains return loopback addresses. [Fact] + [ActiveIssue("https://github.com/dotnet/runtime/issues/126456", TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst | TestPlatforms.Android)] public async Task DnsGetHostAddresses_LocalhostAndSubdomain_BothReturnLoopback() { IPAddress[] localhostAddresses = Dns.GetHostAddresses("localhost"); From 42fbd7226f8da63bb5f29a7d307d01aaecf524ff Mon Sep 17 00:00:00 2001 From: Milos Kotlar Date: Wed, 13 May 2026 13:22:02 +0200 Subject: [PATCH 5/5] Update src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs --- .../tests/FunctionalTests/GetHostEntryTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs index 92f2b9c69f842a..6403b7dcb9195e 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs @@ -449,6 +449,7 @@ public async Task DnsGetHostEntry_LocalhostSubdomain_RespectsAddressFamily(Addre // 3. Different systems configure localhost differently // The key requirement is that localhost subdomains return loopback addresses. [Fact] + [ActiveIssue("https://github.com/dotnet/runtime/issues/126456", TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst | TestPlatforms.Android)] public async Task DnsGetHostEntry_LocalhostAndSubdomain_BothReturnLoopback() { IPHostEntry localhostEntry = Dns.GetHostEntry("localhost");