[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses - #128068

Merged
kotlarmilos merged 12 commits into
dotnet:mainfrom
kotlarmilos:fix/127965-dns-localhost-non-loopback-fallback
May 20, 2026
Merged

[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses#128068
kotlarmilos merged 12 commits into
dotnet:mainfrom
kotlarmilos:fix/127965-dns-localhost-non-loopback-fallback

Conversation

@kotlarmilos

@kotlarmiloskotlarmilos commented May 12, 2026

Copy link
Copy Markdown
Member

Description

Dns falls back to resolving plain localhost when the OS resolver fails or returns zero addresses for a *.localhost subdomain (RFC 6761 §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. The same behavior was observed on iOS/tvOS/MacCatalyst. 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.

Six previously-disabled tests covering the same root cause are re-enabled in this PR.

Fixes#127965.
Fixes#127953.

Supersedes the test-skip workarounds:

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @karelz, @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

@kotlarmilos
kotlarmilosforce-pushed the fix/127965-dns-localhost-non-loopback-fallback branch from b7205f9 to 3a51363CompareMay 12, 2026 08:57

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates System.Net.NameResolution DNS resolution behavior for *.localhost (RFC 6761 §6.3) to avoid returning non-loopback addresses on Android when the platform resolver provides unexpected results, by falling back to resolving plain localhost in more cases.

Changes:

  • Add a helper to detect when *.localhost results contain no loopback addresses.
  • Extend the RFC 6761 §6.3 fallback logic in the synchronous resolution path to trigger when the OS returns only non-loopback addresses.
  • Extend the same fallback logic in the async (GetAddrInfoAsync) completion path for both IPAddress[] and IPHostEntry.
Show a summary per file
FileDescription
src/libraries/System.Net.NameResolution/src/System/Net/Dns.csExtends RFC 6761 localhost-subdomain fallback logic to handle non-loopback OS resolver results (sync + async).

Copilot's findings

Comments suppressed due to low confidence (3)

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:556

  • The new RFC 6761 fallback condition only triggers when the OS returns zero loopback addresses (HasNoLoopbackAddress). However the failing Android test report shows a mixed result set ("7 out of 8 items" non-loopback), which would bypass this fallback and still return non-loopback addresses. Consider treating any non-loopback address in a *.localhost result as grounds to discard/filter the OS result (e.g., fall back when not all returned addresses are loopback, or filter the list to loopback addresses only).
 fallbackToLocalhost = true;
}
if (!fallbackToLocalhost)
{
result = justAddresses ? (object)
addresses :
new IPHostEntry

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:830

  • Same issue in the async completion path: the fallback only triggers when there are no loopback addresses at all, so a mixed address list (some loopback + some non-loopback) will not be corrected and can still violate RFC 6761 and the existing loopback-only test. Update the condition/logic to handle mixed results (discard or filter out non-loopback addresses for *.localhost).
 // result is IPAddress[] so justAddresses is guaranteed true here.
return await ((Task<T>)(Task)Dns.GetHostAddressesAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);
}
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 no loopback addresses, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null);
fallbackOccurred = true;
// result is IPHostEntry so justAddresses is guaranteed false here.
return await ((Task<T>)(Task)Dns.GetHostEntryAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:841

  • The IPHostEntry async fallback path has the same mixed-result problem: if entry.AddressList contains at least one loopback plus additional non-loopback addresses, HasNoLoopbackAddress returns false and the method returns the OS result unchanged. To satisfy RFC 6761 and keep behavior consistent with the address-array path, consider falling back (or filtering) when the result contains any non-loopback addresses.

return result;
}
catch (SocketException ex) when (isLocalhostSubdomain && !fallbackOccurred)
{
// RFC 6761 Section 6.3: If localhost subdomain fails, fall back to resolving plain "localhost".
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain resolution failed, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: null, exception: ex);
fallbackOccurred = true;
  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@kotlarmilos
kotlarmilos requested review from liveans and rzikmMay 12, 2026 08:58
@kotlarmiloskotlarmilos added this to the 11.0.0 milestone May 12, 2026
kotlarmilosand others added 2 commits May 12, 2026 12:43
…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.
Fixesdotnet#127965.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous [ActiveIssue] skips on six DnsGetHostAddresses_/DnsGetHostEntry_LocalhostSubdomain* tests (referencing dotnet#126456 and dotnet#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>
@kotlarmilos
kotlarmilosforce-pushed the fix/127965-dns-localhost-non-loopback-fallback branch from 3a51363 to a536036CompareMay 12, 2026 10:43
@kotlarmilos
kotlarmilos marked this pull request as ready for review May 12, 2026 13:55
CopilotAI review requested due to automatic review settings May 12, 2026 13:55
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 1

Comment threadsrc/libraries/System.Net.NameResolution/src/System/Net/Dns.cs Outdated
…Address
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>
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 6

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

…EntryForName
When the looked-up name matches gethostname(), SystemNative_GetHostEntryForName
appends every interface address to the result regardless of the caller's
AddressFamily filter. On Android emulators gethostname() returns 'localhost',
so a managed fallback to 'localhost' triggers this path and returns IPv6
interface addresses even when AF_INET was requested.
Skip mismatched entries in both the counting and copying passes. Behaviour for
AF_UNSPEC callers is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI review requested due to automatic review settings May 19, 2026 11:58

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Revert "Filter getifaddrs supplement by AddressFamily in SystemNative_GetHostEntryForName"
(commit 4a1d8f5).
Also trim the *.localhost comment in GetHostAddressesTest and GetHostEntryTest.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
kotlarmilos enabled auto-merge (squash) May 20, 2026 08:29
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/ba-g osx-x64 Debug Mono_MiniJIT_LibrariesTests timeouts

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

3 participants

@kotlarmilos@rzikm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses - #128068

Merged
kotlarmilos merged 12 commits into
dotnet:mainfrom
kotlarmilos:fix/127965-dns-localhost-non-loopback-fallback
May 20, 2026
Merged

[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses#128068
kotlarmilos merged 12 commits into
dotnet:mainfrom
kotlarmilos:fix/127965-dns-localhost-non-loopback-fallback

Conversation

@kotlarmilos

@kotlarmiloskotlarmilos commented May 12, 2026

Copy link
Copy Markdown
Member

Description

Dns falls back to resolving plain localhost when the OS resolver fails or returns zero addresses for a *.localhost subdomain (RFC 6761 §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. The same behavior was observed on iOS/tvOS/MacCatalyst. 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.

Six previously-disabled tests covering the same root cause are re-enabled in this PR.

Fixes#127965.
Fixes#127953.

Supersedes the test-skip workarounds:

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @karelz, @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

@kotlarmilos
kotlarmilosforce-pushed the fix/127965-dns-localhost-non-loopback-fallback branch from b7205f9 to 3a51363CompareMay 12, 2026 08:57

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates System.Net.NameResolution DNS resolution behavior for *.localhost (RFC 6761 §6.3) to avoid returning non-loopback addresses on Android when the platform resolver provides unexpected results, by falling back to resolving plain localhost in more cases.

Changes:

  • Add a helper to detect when *.localhost results contain no loopback addresses.
  • Extend the RFC 6761 §6.3 fallback logic in the synchronous resolution path to trigger when the OS returns only non-loopback addresses.
  • Extend the same fallback logic in the async (GetAddrInfoAsync) completion path for both IPAddress[] and IPHostEntry.
Show a summary per file
FileDescription
src/libraries/System.Net.NameResolution/src/System/Net/Dns.csExtends RFC 6761 localhost-subdomain fallback logic to handle non-loopback OS resolver results (sync + async).

Copilot's findings

Comments suppressed due to low confidence (3)

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:556

  • The new RFC 6761 fallback condition only triggers when the OS returns zero loopback addresses (HasNoLoopbackAddress). However the failing Android test report shows a mixed result set ("7 out of 8 items" non-loopback), which would bypass this fallback and still return non-loopback addresses. Consider treating any non-loopback address in a *.localhost result as grounds to discard/filter the OS result (e.g., fall back when not all returned addresses are loopback, or filter the list to loopback addresses only).
 fallbackToLocalhost = true;
}
if (!fallbackToLocalhost)
{
result = justAddresses ? (object)
addresses :
new IPHostEntry

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:830

  • Same issue in the async completion path: the fallback only triggers when there are no loopback addresses at all, so a mixed address list (some loopback + some non-loopback) will not be corrected and can still violate RFC 6761 and the existing loopback-only test. Update the condition/logic to handle mixed results (discard or filter out non-loopback addresses for *.localhost).
 // result is IPAddress[] so justAddresses is guaranteed true here.
return await ((Task<T>)(Task)Dns.GetHostAddressesAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);
}
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 no loopback addresses, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null);
fallbackOccurred = true;
// result is IPHostEntry so justAddresses is guaranteed false here.
return await ((Task<T>)(Task)Dns.GetHostEntryAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:841

  • The IPHostEntry async fallback path has the same mixed-result problem: if entry.AddressList contains at least one loopback plus additional non-loopback addresses, HasNoLoopbackAddress returns false and the method returns the OS result unchanged. To satisfy RFC 6761 and keep behavior consistent with the address-array path, consider falling back (or filtering) when the result contains any non-loopback addresses.

return result;
}
catch (SocketException ex) when (isLocalhostSubdomain && !fallbackOccurred)
{
// RFC 6761 Section 6.3: If localhost subdomain fails, fall back to resolving plain "localhost".
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain resolution failed, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: null, exception: ex);
fallbackOccurred = true;
  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@kotlarmilos
kotlarmilos requested review from liveans and rzikmMay 12, 2026 08:58
@kotlarmiloskotlarmilos added this to the 11.0.0 milestone May 12, 2026
kotlarmilosand others added 2 commits May 12, 2026 12:43
…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.
Fixesdotnet#127965.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous [ActiveIssue] skips on six DnsGetHostAddresses_/DnsGetHostEntry_LocalhostSubdomain* tests (referencing dotnet#126456 and dotnet#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>
@kotlarmilos
kotlarmilosforce-pushed the fix/127965-dns-localhost-non-loopback-fallback branch from 3a51363 to a536036CompareMay 12, 2026 10:43
@kotlarmilos
kotlarmilos marked this pull request as ready for review May 12, 2026 13:55
CopilotAI review requested due to automatic review settings May 12, 2026 13:55
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 1

Comment threadsrc/libraries/System.Net.NameResolution/src/System/Net/Dns.cs Outdated
…Address
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>
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 6

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

…EntryForName
When the looked-up name matches gethostname(), SystemNative_GetHostEntryForName
appends every interface address to the result regardless of the caller's
AddressFamily filter. On Android emulators gethostname() returns 'localhost',
so a managed fallback to 'localhost' triggers this path and returns IPv6
interface addresses even when AF_INET was requested.
Skip mismatched entries in both the counting and copying passes. Behaviour for
AF_UNSPEC callers is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI review requested due to automatic review settings May 19, 2026 11:58

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Revert "Filter getifaddrs supplement by AddressFamily in SystemNative_GetHostEntryForName"
(commit 4a1d8f5).
Also trim the *.localhost comment in GetHostAddressesTest and GetHostEntryTest.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
kotlarmilos enabled auto-merge (squash) May 20, 2026 08:29
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/ba-g osx-x64 Debug Mono_MiniJIT_LibrariesTests timeouts

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

3 participants

@kotlarmilos@rzikm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses - #128068

Merged
kotlarmilos merged 12 commits into
dotnet:mainfrom
kotlarmilos:fix/127965-dns-localhost-non-loopback-fallback
May 20, 2026
Merged

[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses#128068
kotlarmilos merged 12 commits into
dotnet:mainfrom
kotlarmilos:fix/127965-dns-localhost-non-loopback-fallback

Conversation

@kotlarmilos

@kotlarmiloskotlarmilos commented May 12, 2026

Copy link
Copy Markdown
Member

Description

Dns falls back to resolving plain localhost when the OS resolver fails or returns zero addresses for a *.localhost subdomain (RFC 6761 §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. The same behavior was observed on iOS/tvOS/MacCatalyst. 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.

Six previously-disabled tests covering the same root cause are re-enabled in this PR.

Fixes#127965.
Fixes#127953.

Supersedes the test-skip workarounds:

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @karelz, @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

@kotlarmilos
kotlarmilosforce-pushed the fix/127965-dns-localhost-non-loopback-fallback branch from b7205f9 to 3a51363CompareMay 12, 2026 08:57

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates System.Net.NameResolution DNS resolution behavior for *.localhost (RFC 6761 §6.3) to avoid returning non-loopback addresses on Android when the platform resolver provides unexpected results, by falling back to resolving plain localhost in more cases.

Changes:

  • Add a helper to detect when *.localhost results contain no loopback addresses.
  • Extend the RFC 6761 §6.3 fallback logic in the synchronous resolution path to trigger when the OS returns only non-loopback addresses.
  • Extend the same fallback logic in the async (GetAddrInfoAsync) completion path for both IPAddress[] and IPHostEntry.
Show a summary per file
FileDescription
src/libraries/System.Net.NameResolution/src/System/Net/Dns.csExtends RFC 6761 localhost-subdomain fallback logic to handle non-loopback OS resolver results (sync + async).

Copilot's findings

Comments suppressed due to low confidence (3)

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:556

  • The new RFC 6761 fallback condition only triggers when the OS returns zero loopback addresses (HasNoLoopbackAddress). However the failing Android test report shows a mixed result set ("7 out of 8 items" non-loopback), which would bypass this fallback and still return non-loopback addresses. Consider treating any non-loopback address in a *.localhost result as grounds to discard/filter the OS result (e.g., fall back when not all returned addresses are loopback, or filter the list to loopback addresses only).
 fallbackToLocalhost = true;
}
if (!fallbackToLocalhost)
{
result = justAddresses ? (object)
addresses :
new IPHostEntry

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:830

  • Same issue in the async completion path: the fallback only triggers when there are no loopback addresses at all, so a mixed address list (some loopback + some non-loopback) will not be corrected and can still violate RFC 6761 and the existing loopback-only test. Update the condition/logic to handle mixed results (discard or filter out non-loopback addresses for *.localhost).
 // result is IPAddress[] so justAddresses is guaranteed true here.
return await ((Task<T>)(Task)Dns.GetHostAddressesAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);
}
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 no loopback addresses, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null);
fallbackOccurred = true;
// result is IPHostEntry so justAddresses is guaranteed false here.
return await ((Task<T>)(Task)Dns.GetHostEntryAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:841

  • The IPHostEntry async fallback path has the same mixed-result problem: if entry.AddressList contains at least one loopback plus additional non-loopback addresses, HasNoLoopbackAddress returns false and the method returns the OS result unchanged. To satisfy RFC 6761 and keep behavior consistent with the address-array path, consider falling back (or filtering) when the result contains any non-loopback addresses.

return result;
}
catch (SocketException ex) when (isLocalhostSubdomain && !fallbackOccurred)
{
// RFC 6761 Section 6.3: If localhost subdomain fails, fall back to resolving plain "localhost".
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain resolution failed, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: null, exception: ex);
fallbackOccurred = true;
  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@kotlarmilos
kotlarmilos requested review from liveans and rzikmMay 12, 2026 08:58
@kotlarmiloskotlarmilos added this to the 11.0.0 milestone May 12, 2026
kotlarmilosand others added 2 commits May 12, 2026 12:43
…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.
Fixesdotnet#127965.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous [ActiveIssue] skips on six DnsGetHostAddresses_/DnsGetHostEntry_LocalhostSubdomain* tests (referencing dotnet#126456 and dotnet#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>
@kotlarmilos
kotlarmilosforce-pushed the fix/127965-dns-localhost-non-loopback-fallback branch from 3a51363 to a536036CompareMay 12, 2026 10:43
@kotlarmilos
kotlarmilos marked this pull request as ready for review May 12, 2026 13:55
CopilotAI review requested due to automatic review settings May 12, 2026 13:55
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 1

Comment threadsrc/libraries/System.Net.NameResolution/src/System/Net/Dns.cs Outdated
…Address
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>
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 6

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

…EntryForName
When the looked-up name matches gethostname(), SystemNative_GetHostEntryForName
appends every interface address to the result regardless of the caller's
AddressFamily filter. On Android emulators gethostname() returns 'localhost',
so a managed fallback to 'localhost' triggers this path and returns IPv6
interface addresses even when AF_INET was requested.
Skip mismatched entries in both the counting and copying passes. Behaviour for
AF_UNSPEC callers is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI review requested due to automatic review settings May 19, 2026 11:58

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Revert "Filter getifaddrs supplement by AddressFamily in SystemNative_GetHostEntryForName"
(commit 4a1d8f5).
Also trim the *.localhost comment in GetHostAddressesTest and GetHostEntryTest.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
kotlarmilos enabled auto-merge (squash) May 20, 2026 08:29
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/ba-g osx-x64 Debug Mono_MiniJIT_LibrariesTests timeouts

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

3 participants

@kotlarmilos@rzikm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses - #128068

Merged
kotlarmilos merged 12 commits into
dotnet:mainfrom
kotlarmilos:fix/127965-dns-localhost-non-loopback-fallback
May 20, 2026
Merged

[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses#128068
kotlarmilos merged 12 commits into
dotnet:mainfrom
kotlarmilos:fix/127965-dns-localhost-non-loopback-fallback

Conversation

@kotlarmilos

@kotlarmiloskotlarmilos commented May 12, 2026

Copy link
Copy Markdown
Member

Description

Dns falls back to resolving plain localhost when the OS resolver fails or returns zero addresses for a *.localhost subdomain (RFC 6761 §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. The same behavior was observed on iOS/tvOS/MacCatalyst. 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.

Six previously-disabled tests covering the same root cause are re-enabled in this PR.

Fixes#127965.
Fixes#127953.

Supersedes the test-skip workarounds:

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @karelz, @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

@kotlarmilos
kotlarmilosforce-pushed the fix/127965-dns-localhost-non-loopback-fallback branch from b7205f9 to 3a51363CompareMay 12, 2026 08:57

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates System.Net.NameResolution DNS resolution behavior for *.localhost (RFC 6761 §6.3) to avoid returning non-loopback addresses on Android when the platform resolver provides unexpected results, by falling back to resolving plain localhost in more cases.

Changes:

  • Add a helper to detect when *.localhost results contain no loopback addresses.
  • Extend the RFC 6761 §6.3 fallback logic in the synchronous resolution path to trigger when the OS returns only non-loopback addresses.
  • Extend the same fallback logic in the async (GetAddrInfoAsync) completion path for both IPAddress[] and IPHostEntry.
Show a summary per file
FileDescription
src/libraries/System.Net.NameResolution/src/System/Net/Dns.csExtends RFC 6761 localhost-subdomain fallback logic to handle non-loopback OS resolver results (sync + async).

Copilot's findings

Comments suppressed due to low confidence (3)

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:556

  • The new RFC 6761 fallback condition only triggers when the OS returns zero loopback addresses (HasNoLoopbackAddress). However the failing Android test report shows a mixed result set ("7 out of 8 items" non-loopback), which would bypass this fallback and still return non-loopback addresses. Consider treating any non-loopback address in a *.localhost result as grounds to discard/filter the OS result (e.g., fall back when not all returned addresses are loopback, or filter the list to loopback addresses only).
 fallbackToLocalhost = true;
}
if (!fallbackToLocalhost)
{
result = justAddresses ? (object)
addresses :
new IPHostEntry

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:830

  • Same issue in the async completion path: the fallback only triggers when there are no loopback addresses at all, so a mixed address list (some loopback + some non-loopback) will not be corrected and can still violate RFC 6761 and the existing loopback-only test. Update the condition/logic to handle mixed results (discard or filter out non-loopback addresses for *.localhost).
 // result is IPAddress[] so justAddresses is guaranteed true here.
return await ((Task<T>)(Task)Dns.GetHostAddressesAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);
}
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 no loopback addresses, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null);
fallbackOccurred = true;
// result is IPHostEntry so justAddresses is guaranteed false here.
return await ((Task<T>)(Task)Dns.GetHostEntryAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:841

  • The IPHostEntry async fallback path has the same mixed-result problem: if entry.AddressList contains at least one loopback plus additional non-loopback addresses, HasNoLoopbackAddress returns false and the method returns the OS result unchanged. To satisfy RFC 6761 and keep behavior consistent with the address-array path, consider falling back (or filtering) when the result contains any non-loopback addresses.

return result;
}
catch (SocketException ex) when (isLocalhostSubdomain && !fallbackOccurred)
{
// RFC 6761 Section 6.3: If localhost subdomain fails, fall back to resolving plain "localhost".
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain resolution failed, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: null, exception: ex);
fallbackOccurred = true;
  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@kotlarmilos
kotlarmilos requested review from liveans and rzikmMay 12, 2026 08:58
@kotlarmiloskotlarmilos added this to the 11.0.0 milestone May 12, 2026
kotlarmilosand others added 2 commits May 12, 2026 12:43
…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.
Fixesdotnet#127965.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous [ActiveIssue] skips on six DnsGetHostAddresses_/DnsGetHostEntry_LocalhostSubdomain* tests (referencing dotnet#126456 and dotnet#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>
@kotlarmilos
kotlarmilosforce-pushed the fix/127965-dns-localhost-non-loopback-fallback branch from 3a51363 to a536036CompareMay 12, 2026 10:43
@kotlarmilos
kotlarmilos marked this pull request as ready for review May 12, 2026 13:55
CopilotAI review requested due to automatic review settings May 12, 2026 13:55
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 1

Comment threadsrc/libraries/System.Net.NameResolution/src/System/Net/Dns.cs Outdated
…Address
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>
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 6

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

…EntryForName
When the looked-up name matches gethostname(), SystemNative_GetHostEntryForName
appends every interface address to the result regardless of the caller's
AddressFamily filter. On Android emulators gethostname() returns 'localhost',
so a managed fallback to 'localhost' triggers this path and returns IPv6
interface addresses even when AF_INET was requested.
Skip mismatched entries in both the counting and copying passes. Behaviour for
AF_UNSPEC callers is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI review requested due to automatic review settings May 19, 2026 11:58

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Revert "Filter getifaddrs supplement by AddressFamily in SystemNative_GetHostEntryForName"
(commit 4a1d8f5).
Also trim the *.localhost comment in GetHostAddressesTest and GetHostEntryTest.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
kotlarmilos enabled auto-merge (squash) May 20, 2026 08:29
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/ba-g osx-x64 Debug Mono_MiniJIT_LibrariesTests timeouts

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

3 participants

@kotlarmilos@rzikm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses - #128068

Merged
kotlarmilos merged 12 commits into
dotnet:mainfrom
kotlarmilos:fix/127965-dns-localhost-non-loopback-fallback
May 20, 2026
Merged

[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses#128068
kotlarmilos merged 12 commits into
dotnet:mainfrom
kotlarmilos:fix/127965-dns-localhost-non-loopback-fallback

Conversation

@kotlarmilos

@kotlarmiloskotlarmilos commented May 12, 2026

Copy link
Copy Markdown
Member

Description

Dns falls back to resolving plain localhost when the OS resolver fails or returns zero addresses for a *.localhost subdomain (RFC 6761 §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. The same behavior was observed on iOS/tvOS/MacCatalyst. 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.

Six previously-disabled tests covering the same root cause are re-enabled in this PR.

Fixes#127965.
Fixes#127953.

Supersedes the test-skip workarounds:

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @karelz, @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

@kotlarmilos
kotlarmilosforce-pushed the fix/127965-dns-localhost-non-loopback-fallback branch from b7205f9 to 3a51363CompareMay 12, 2026 08:57

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates System.Net.NameResolution DNS resolution behavior for *.localhost (RFC 6761 §6.3) to avoid returning non-loopback addresses on Android when the platform resolver provides unexpected results, by falling back to resolving plain localhost in more cases.

Changes:

  • Add a helper to detect when *.localhost results contain no loopback addresses.
  • Extend the RFC 6761 §6.3 fallback logic in the synchronous resolution path to trigger when the OS returns only non-loopback addresses.
  • Extend the same fallback logic in the async (GetAddrInfoAsync) completion path for both IPAddress[] and IPHostEntry.
Show a summary per file
FileDescription
src/libraries/System.Net.NameResolution/src/System/Net/Dns.csExtends RFC 6761 localhost-subdomain fallback logic to handle non-loopback OS resolver results (sync + async).

Copilot's findings

Comments suppressed due to low confidence (3)

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:556

  • The new RFC 6761 fallback condition only triggers when the OS returns zero loopback addresses (HasNoLoopbackAddress). However the failing Android test report shows a mixed result set ("7 out of 8 items" non-loopback), which would bypass this fallback and still return non-loopback addresses. Consider treating any non-loopback address in a *.localhost result as grounds to discard/filter the OS result (e.g., fall back when not all returned addresses are loopback, or filter the list to loopback addresses only).
 fallbackToLocalhost = true;
}
if (!fallbackToLocalhost)
{
result = justAddresses ? (object)
addresses :
new IPHostEntry

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:830

  • Same issue in the async completion path: the fallback only triggers when there are no loopback addresses at all, so a mixed address list (some loopback + some non-loopback) will not be corrected and can still violate RFC 6761 and the existing loopback-only test. Update the condition/logic to handle mixed results (discard or filter out non-loopback addresses for *.localhost).
 // result is IPAddress[] so justAddresses is guaranteed true here.
return await ((Task<T>)(Task)Dns.GetHostAddressesAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);
}
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 no loopback addresses, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null);
fallbackOccurred = true;
// result is IPHostEntry so justAddresses is guaranteed false here.
return await ((Task<T>)(Task)Dns.GetHostEntryAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:841

  • The IPHostEntry async fallback path has the same mixed-result problem: if entry.AddressList contains at least one loopback plus additional non-loopback addresses, HasNoLoopbackAddress returns false and the method returns the OS result unchanged. To satisfy RFC 6761 and keep behavior consistent with the address-array path, consider falling back (or filtering) when the result contains any non-loopback addresses.

return result;
}
catch (SocketException ex) when (isLocalhostSubdomain && !fallbackOccurred)
{
// RFC 6761 Section 6.3: If localhost subdomain fails, fall back to resolving plain "localhost".
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain resolution failed, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: null, exception: ex);
fallbackOccurred = true;
  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@kotlarmilos
kotlarmilos requested review from liveans and rzikmMay 12, 2026 08:58
@kotlarmiloskotlarmilos added this to the 11.0.0 milestone May 12, 2026
kotlarmilosand others added 2 commits May 12, 2026 12:43
…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.
Fixesdotnet#127965.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous [ActiveIssue] skips on six DnsGetHostAddresses_/DnsGetHostEntry_LocalhostSubdomain* tests (referencing dotnet#126456 and dotnet#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>
@kotlarmilos
kotlarmilosforce-pushed the fix/127965-dns-localhost-non-loopback-fallback branch from 3a51363 to a536036CompareMay 12, 2026 10:43
@kotlarmilos
kotlarmilos marked this pull request as ready for review May 12, 2026 13:55
CopilotAI review requested due to automatic review settings May 12, 2026 13:55
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 1

Comment threadsrc/libraries/System.Net.NameResolution/src/System/Net/Dns.cs Outdated
…Address
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>
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 6

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

…EntryForName
When the looked-up name matches gethostname(), SystemNative_GetHostEntryForName
appends every interface address to the result regardless of the caller's
AddressFamily filter. On Android emulators gethostname() returns 'localhost',
so a managed fallback to 'localhost' triggers this path and returns IPv6
interface addresses even when AF_INET was requested.
Skip mismatched entries in both the counting and copying passes. Behaviour for
AF_UNSPEC callers is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI review requested due to automatic review settings May 19, 2026 11:58

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Revert "Filter getifaddrs supplement by AddressFamily in SystemNative_GetHostEntryForName"
(commit 4a1d8f5).
Also trim the *.localhost comment in GetHostAddressesTest and GetHostEntryTest.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
kotlarmilos enabled auto-merge (squash) May 20, 2026 08:29
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/ba-g osx-x64 Debug Mono_MiniJIT_LibrariesTests timeouts

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

3 participants

@kotlarmilos@rzikm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses - #128068

Merged
kotlarmilos merged 12 commits into
dotnet:mainfrom
kotlarmilos:fix/127965-dns-localhost-non-loopback-fallback
May 20, 2026
Merged

[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses#128068
kotlarmilos merged 12 commits into
dotnet:mainfrom
kotlarmilos:fix/127965-dns-localhost-non-loopback-fallback

Conversation

@kotlarmilos

@kotlarmiloskotlarmilos commented May 12, 2026

Copy link
Copy Markdown
Member

Description

Dns falls back to resolving plain localhost when the OS resolver fails or returns zero addresses for a *.localhost subdomain (RFC 6761 §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. The same behavior was observed on iOS/tvOS/MacCatalyst. 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.

Six previously-disabled tests covering the same root cause are re-enabled in this PR.

Fixes#127965.
Fixes#127953.

Supersedes the test-skip workarounds:

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @karelz, @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

@kotlarmilos
kotlarmilosforce-pushed the fix/127965-dns-localhost-non-loopback-fallback branch from b7205f9 to 3a51363CompareMay 12, 2026 08:57

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates System.Net.NameResolution DNS resolution behavior for *.localhost (RFC 6761 §6.3) to avoid returning non-loopback addresses on Android when the platform resolver provides unexpected results, by falling back to resolving plain localhost in more cases.

Changes:

  • Add a helper to detect when *.localhost results contain no loopback addresses.
  • Extend the RFC 6761 §6.3 fallback logic in the synchronous resolution path to trigger when the OS returns only non-loopback addresses.
  • Extend the same fallback logic in the async (GetAddrInfoAsync) completion path for both IPAddress[] and IPHostEntry.
Show a summary per file
FileDescription
src/libraries/System.Net.NameResolution/src/System/Net/Dns.csExtends RFC 6761 localhost-subdomain fallback logic to handle non-loopback OS resolver results (sync + async).

Copilot's findings

Comments suppressed due to low confidence (3)

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:556

  • The new RFC 6761 fallback condition only triggers when the OS returns zero loopback addresses (HasNoLoopbackAddress). However the failing Android test report shows a mixed result set ("7 out of 8 items" non-loopback), which would bypass this fallback and still return non-loopback addresses. Consider treating any non-loopback address in a *.localhost result as grounds to discard/filter the OS result (e.g., fall back when not all returned addresses are loopback, or filter the list to loopback addresses only).
 fallbackToLocalhost = true;
}
if (!fallbackToLocalhost)
{
result = justAddresses ? (object)
addresses :
new IPHostEntry

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:830

  • Same issue in the async completion path: the fallback only triggers when there are no loopback addresses at all, so a mixed address list (some loopback + some non-loopback) will not be corrected and can still violate RFC 6761 and the existing loopback-only test. Update the condition/logic to handle mixed results (discard or filter out non-loopback addresses for *.localhost).
 // result is IPAddress[] so justAddresses is guaranteed true here.
return await ((Task<T>)(Task)Dns.GetHostAddressesAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);
}
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 no loopback addresses, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null);
fallbackOccurred = true;
// result is IPHostEntry so justAddresses is guaranteed false here.
return await ((Task<T>)(Task)Dns.GetHostEntryAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:841

  • The IPHostEntry async fallback path has the same mixed-result problem: if entry.AddressList contains at least one loopback plus additional non-loopback addresses, HasNoLoopbackAddress returns false and the method returns the OS result unchanged. To satisfy RFC 6761 and keep behavior consistent with the address-array path, consider falling back (or filtering) when the result contains any non-loopback addresses.

return result;
}
catch (SocketException ex) when (isLocalhostSubdomain && !fallbackOccurred)
{
// RFC 6761 Section 6.3: If localhost subdomain fails, fall back to resolving plain "localhost".
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain resolution failed, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: null, exception: ex);
fallbackOccurred = true;
  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@kotlarmilos
kotlarmilos requested review from liveans and rzikmMay 12, 2026 08:58
@kotlarmiloskotlarmilos added this to the 11.0.0 milestone May 12, 2026
kotlarmilosand others added 2 commits May 12, 2026 12:43
…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.
Fixesdotnet#127965.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous [ActiveIssue] skips on six DnsGetHostAddresses_/DnsGetHostEntry_LocalhostSubdomain* tests (referencing dotnet#126456 and dotnet#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>
@kotlarmilos
kotlarmilosforce-pushed the fix/127965-dns-localhost-non-loopback-fallback branch from 3a51363 to a536036CompareMay 12, 2026 10:43
@kotlarmilos
kotlarmilos marked this pull request as ready for review May 12, 2026 13:55
CopilotAI review requested due to automatic review settings May 12, 2026 13:55
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 1

Comment threadsrc/libraries/System.Net.NameResolution/src/System/Net/Dns.cs Outdated
…Address
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>
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 6

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

…EntryForName
When the looked-up name matches gethostname(), SystemNative_GetHostEntryForName
appends every interface address to the result regardless of the caller's
AddressFamily filter. On Android emulators gethostname() returns 'localhost',
so a managed fallback to 'localhost' triggers this path and returns IPv6
interface addresses even when AF_INET was requested.
Skip mismatched entries in both the counting and copying passes. Behaviour for
AF_UNSPEC callers is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI review requested due to automatic review settings May 19, 2026 11:58

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Revert "Filter getifaddrs supplement by AddressFamily in SystemNative_GetHostEntryForName"
(commit 4a1d8f5).
Also trim the *.localhost comment in GetHostAddressesTest and GetHostEntryTest.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
kotlarmilos enabled auto-merge (squash) May 20, 2026 08:29
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/ba-g osx-x64 Debug Mono_MiniJIT_LibrariesTests timeouts

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

3 participants

@kotlarmilos@rzikm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses - #128068

Merged
kotlarmilos merged 12 commits into
dotnet:mainfrom
kotlarmilos:fix/127965-dns-localhost-non-loopback-fallback
May 20, 2026
Merged

[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses#128068
kotlarmilos merged 12 commits into
dotnet:mainfrom
kotlarmilos:fix/127965-dns-localhost-non-loopback-fallback

Conversation

@kotlarmilos

@kotlarmiloskotlarmilos commented May 12, 2026

Copy link
Copy Markdown
Member

Description

Dns falls back to resolving plain localhost when the OS resolver fails or returns zero addresses for a *.localhost subdomain (RFC 6761 §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. The same behavior was observed on iOS/tvOS/MacCatalyst. 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.

Six previously-disabled tests covering the same root cause are re-enabled in this PR.

Fixes#127965.
Fixes#127953.

Supersedes the test-skip workarounds:

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @karelz, @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

@kotlarmilos
kotlarmilosforce-pushed the fix/127965-dns-localhost-non-loopback-fallback branch from b7205f9 to 3a51363CompareMay 12, 2026 08:57

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates System.Net.NameResolution DNS resolution behavior for *.localhost (RFC 6761 §6.3) to avoid returning non-loopback addresses on Android when the platform resolver provides unexpected results, by falling back to resolving plain localhost in more cases.

Changes:

  • Add a helper to detect when *.localhost results contain no loopback addresses.
  • Extend the RFC 6761 §6.3 fallback logic in the synchronous resolution path to trigger when the OS returns only non-loopback addresses.
  • Extend the same fallback logic in the async (GetAddrInfoAsync) completion path for both IPAddress[] and IPHostEntry.
Show a summary per file
FileDescription
src/libraries/System.Net.NameResolution/src/System/Net/Dns.csExtends RFC 6761 localhost-subdomain fallback logic to handle non-loopback OS resolver results (sync + async).

Copilot's findings

Comments suppressed due to low confidence (3)

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:556

  • The new RFC 6761 fallback condition only triggers when the OS returns zero loopback addresses (HasNoLoopbackAddress). However the failing Android test report shows a mixed result set ("7 out of 8 items" non-loopback), which would bypass this fallback and still return non-loopback addresses. Consider treating any non-loopback address in a *.localhost result as grounds to discard/filter the OS result (e.g., fall back when not all returned addresses are loopback, or filter the list to loopback addresses only).
 fallbackToLocalhost = true;
}
if (!fallbackToLocalhost)
{
result = justAddresses ? (object)
addresses :
new IPHostEntry

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:830

  • Same issue in the async completion path: the fallback only triggers when there are no loopback addresses at all, so a mixed address list (some loopback + some non-loopback) will not be corrected and can still violate RFC 6761 and the existing loopback-only test. Update the condition/logic to handle mixed results (discard or filter out non-loopback addresses for *.localhost).
 // result is IPAddress[] so justAddresses is guaranteed true here.
return await ((Task<T>)(Task)Dns.GetHostAddressesAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);
}
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 no loopback addresses, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null);
fallbackOccurred = true;
// result is IPHostEntry so justAddresses is guaranteed false here.
return await ((Task<T>)(Task)Dns.GetHostEntryAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:841

  • The IPHostEntry async fallback path has the same mixed-result problem: if entry.AddressList contains at least one loopback plus additional non-loopback addresses, HasNoLoopbackAddress returns false and the method returns the OS result unchanged. To satisfy RFC 6761 and keep behavior consistent with the address-array path, consider falling back (or filtering) when the result contains any non-loopback addresses.

return result;
}
catch (SocketException ex) when (isLocalhostSubdomain && !fallbackOccurred)
{
// RFC 6761 Section 6.3: If localhost subdomain fails, fall back to resolving plain "localhost".
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain resolution failed, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: null, exception: ex);
fallbackOccurred = true;
  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@kotlarmilos
kotlarmilos requested review from liveans and rzikmMay 12, 2026 08:58
@kotlarmiloskotlarmilos added this to the 11.0.0 milestone May 12, 2026
kotlarmilosand others added 2 commits May 12, 2026 12:43
…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.
Fixesdotnet#127965.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous [ActiveIssue] skips on six DnsGetHostAddresses_/DnsGetHostEntry_LocalhostSubdomain* tests (referencing dotnet#126456 and dotnet#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>
@kotlarmilos
kotlarmilosforce-pushed the fix/127965-dns-localhost-non-loopback-fallback branch from 3a51363 to a536036CompareMay 12, 2026 10:43
@kotlarmilos
kotlarmilos marked this pull request as ready for review May 12, 2026 13:55
CopilotAI review requested due to automatic review settings May 12, 2026 13:55
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 1

Comment threadsrc/libraries/System.Net.NameResolution/src/System/Net/Dns.cs Outdated
…Address
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>
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 6

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

…EntryForName
When the looked-up name matches gethostname(), SystemNative_GetHostEntryForName
appends every interface address to the result regardless of the caller's
AddressFamily filter. On Android emulators gethostname() returns 'localhost',
so a managed fallback to 'localhost' triggers this path and returns IPv6
interface addresses even when AF_INET was requested.
Skip mismatched entries in both the counting and copying passes. Behaviour for
AF_UNSPEC callers is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI review requested due to automatic review settings May 19, 2026 11:58

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Revert "Filter getifaddrs supplement by AddressFamily in SystemNative_GetHostEntryForName"
(commit 4a1d8f5).
Also trim the *.localhost comment in GetHostAddressesTest and GetHostEntryTest.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
kotlarmilos enabled auto-merge (squash) May 20, 2026 08:29
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/ba-g osx-x64 Debug Mono_MiniJIT_LibrariesTests timeouts

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

3 participants

@kotlarmilos@rzikm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses - #128068

Merged
kotlarmilos merged 12 commits into
dotnet:mainfrom
kotlarmilos:fix/127965-dns-localhost-non-loopback-fallback
May 20, 2026
Merged

[Android] Fall back to 'localhost' when *.localhost resolves to only non-loopback addresses#128068
kotlarmilos merged 12 commits into
dotnet:mainfrom
kotlarmilos:fix/127965-dns-localhost-non-loopback-fallback

Conversation

@kotlarmilos

@kotlarmiloskotlarmilos commented May 12, 2026

Copy link
Copy Markdown
Member

Description

Dns falls back to resolving plain localhost when the OS resolver fails or returns zero addresses for a *.localhost subdomain (RFC 6761 §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. The same behavior was observed on iOS/tvOS/MacCatalyst. 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.

Six previously-disabled tests covering the same root cause are re-enabled in this PR.

Fixes#127965.
Fixes#127953.

Supersedes the test-skip workarounds:

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @karelz, @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

@kotlarmilos
kotlarmilosforce-pushed the fix/127965-dns-localhost-non-loopback-fallback branch from b7205f9 to 3a51363CompareMay 12, 2026 08:57

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates System.Net.NameResolution DNS resolution behavior for *.localhost (RFC 6761 §6.3) to avoid returning non-loopback addresses on Android when the platform resolver provides unexpected results, by falling back to resolving plain localhost in more cases.

Changes:

  • Add a helper to detect when *.localhost results contain no loopback addresses.
  • Extend the RFC 6761 §6.3 fallback logic in the synchronous resolution path to trigger when the OS returns only non-loopback addresses.
  • Extend the same fallback logic in the async (GetAddrInfoAsync) completion path for both IPAddress[] and IPHostEntry.
Show a summary per file
FileDescription
src/libraries/System.Net.NameResolution/src/System/Net/Dns.csExtends RFC 6761 localhost-subdomain fallback logic to handle non-loopback OS resolver results (sync + async).

Copilot's findings

Comments suppressed due to low confidence (3)

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:556

  • The new RFC 6761 fallback condition only triggers when the OS returns zero loopback addresses (HasNoLoopbackAddress). However the failing Android test report shows a mixed result set ("7 out of 8 items" non-loopback), which would bypass this fallback and still return non-loopback addresses. Consider treating any non-loopback address in a *.localhost result as grounds to discard/filter the OS result (e.g., fall back when not all returned addresses are loopback, or filter the list to loopback addresses only).
 fallbackToLocalhost = true;
}
if (!fallbackToLocalhost)
{
result = justAddresses ? (object)
addresses :
new IPHostEntry

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:830

  • Same issue in the async completion path: the fallback only triggers when there are no loopback addresses at all, so a mixed address list (some loopback + some non-loopback) will not be corrected and can still violate RFC 6761 and the existing loopback-only test. Update the condition/logic to handle mixed results (discard or filter out non-loopback addresses for *.localhost).
 // result is IPAddress[] so justAddresses is guaranteed true here.
return await ((Task<T>)(Task)Dns.GetHostAddressesAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);
}
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 no loopback addresses, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: result, exception: null);
fallbackOccurred = true;
// result is IPHostEntry so justAddresses is guaranteed false here.
return await ((Task<T>)(Task)Dns.GetHostEntryAsync(Localhost, addressFamily, cancellationToken)).ConfigureAwait(false);

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs:841

  • The IPHostEntry async fallback path has the same mixed-result problem: if entry.AddressList contains at least one loopback plus additional non-loopback addresses, HasNoLoopbackAddress returns false and the method returns the OS result unchanged. To satisfy RFC 6761 and keep behavior consistent with the address-array path, consider falling back (or filtering) when the result contains any non-loopback addresses.

return result;
}
catch (SocketException ex) when (isLocalhostSubdomain && !fallbackOccurred)
{
// RFC 6761 Section 6.3: If localhost subdomain fails, fall back to resolving plain "localhost".
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(hostName, "RFC 6761: Localhost subdomain resolution failed, falling back to 'localhost'");
NameResolutionTelemetry.Log.AfterResolution(hostName, activity, answer: null, exception: ex);
fallbackOccurred = true;
  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@kotlarmilos
kotlarmilos requested review from liveans and rzikmMay 12, 2026 08:58
@kotlarmiloskotlarmilos added this to the 11.0.0 milestone May 12, 2026
kotlarmilosand others added 2 commits May 12, 2026 12:43
…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.
Fixesdotnet#127965.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous [ActiveIssue] skips on six DnsGetHostAddresses_/DnsGetHostEntry_LocalhostSubdomain* tests (referencing dotnet#126456 and dotnet#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>
@kotlarmilos
kotlarmilosforce-pushed the fix/127965-dns-localhost-non-loopback-fallback branch from 3a51363 to a536036CompareMay 12, 2026 10:43
@kotlarmilos
kotlarmilos marked this pull request as ready for review May 12, 2026 13:55
CopilotAI review requested due to automatic review settings May 12, 2026 13:55
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 1

Comment threadsrc/libraries/System.Net.NameResolution/src/System/Net/Dns.cs Outdated
…Address
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>
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 6

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

…EntryForName
When the looked-up name matches gethostname(), SystemNative_GetHostEntryForName
appends every interface address to the result regardless of the caller's
AddressFamily filter. On Android emulators gethostname() returns 'localhost',
so a managed fallback to 'localhost' triggers this path and returns IPv6
interface addresses even when AF_INET was requested.
Skip mismatched entries in both the counting and copying passes. Behaviour for
AF_UNSPEC callers is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

CopilotAI review requested due to automatic review settings May 19, 2026 11:58

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Revert "Filter getifaddrs supplement by AddressFamily in SystemNative_GetHostEntryForName"
(commit 4a1d8f5).
Also trim the *.localhost comment in GetHostAddressesTest and GetHostEntryTest.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
kotlarmilos enabled auto-merge (squash) May 20, 2026 08:29
@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

/ba-g osx-x64 Debug Mono_MiniJIT_LibrariesTests timeouts

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

3 participants

@kotlarmilos@rzikm