Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IPAddress.IsValid #111433

Merged
merged 3 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions eng/pipelines/libraries/fuzzing/deploy-to-onefuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ extends:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
displayName: Send HttpHeadersFuzzer to OneFuzz

- task: onefuzz-task@0
inputs:
onefuzzOSes: 'Windows'
env:
onefuzzDropDirectory: $(fuzzerProject)/deployment/IPAddressFuzzer
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
displayName: Send IPAddressFuzzer to OneFuzz

- task: onefuzz-task@0
inputs:
onefuzzOSes: 'Windows'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth

if (sslAuthenticationOptions.IsClient)
{
if (!string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost) && !TargetHostNameHelper.IsValidAddress(sslAuthenticationOptions.TargetHost))
if (!string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost) && !IPAddress.IsValid(sslAuthenticationOptions.TargetHost))
{
// Similar to windows behavior, set SNI on openssl by default for client context, ignore errors.
if (!Ssl.SslSetTlsExtHostName(sslHandle, sslAuthenticationOptions.TargetHost))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ internal static bool ShouldHaveIpv4Embedded(ReadOnlySpan<ushort> numbers)

// Remarks: MUST NOT be used unless all input indexes are verified and trusted.
// start must be next to '[' position, or error is reported
internal static unsafe bool IsValidStrict<TChar>(TChar* name, int start, ref int end)
internal static unsafe bool IsValidStrict<TChar>(TChar* name, int start, int end)
where TChar : unmanaged, IBinaryInteger<TChar>
{
Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.InteropServices;

namespace System.Net.Security
{
Expand Down Expand Up @@ -37,45 +36,5 @@ internal static string NormalizeHostName(string? targetHost)

return targetHost;
}

// Simplified version of IPAddressParser.Parse to avoid allocations and dependencies.
// It purposely ignores scopeId as we don't really use so we do not need to map it to actual interface id.
internal static unsafe bool IsValidAddress(string? hostname)
{
if (string.IsNullOrEmpty(hostname))
{
return false;
}

ReadOnlySpan<char> ipSpan = hostname.AsSpan();

int end = ipSpan.Length;

if (ipSpan.Contains(':'))
{
// The address is parsed as IPv6 if and only if it contains a colon. This is valid because
// we don't support/parse a port specification at the end of an IPv4 address.
fixed (char* ipStringPtr = &MemoryMarshal.GetReference(ipSpan))
{
return IPv6AddressHelper.IsValidStrict(ipStringPtr, 0, ref end);
}
}
else if (char.IsDigit(ipSpan[0]))
{
long tmpAddr;

fixed (char* ipStringPtr = &MemoryMarshal.GetReference(ipSpan))
{
tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipStringPtr, 0, ref end, notImplicitFile: true);
}

if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length)
{
return true;
}
}

return false;
}
}
}
10 changes: 9 additions & 1 deletion src/libraries/Fuzzing/DotnetFuzzing/Assert.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;

namespace DotnetFuzzing;

internal static class Assert
Expand All @@ -18,6 +20,12 @@ static void Throw(T expected, T actual) =>
throw new Exception($"Expected={expected} Actual={actual}");
}

public static void True([DoesNotReturnIf(false)] bool actual) =>
Equal(true, actual);

public static void False([DoesNotReturnIf(true)] bool actual) =>
Equal(false, actual);

public static void NotNull<T>(T value)
{
if (value == null)
Expand All @@ -26,7 +34,7 @@ public static void NotNull<T>(T value)
}

static void ThrowNull() =>
throw new Exception("Value is null");
throw new Exception("Value is null");
}

public static void SequenceEqual<T>(ReadOnlySpan<T> expected, ReadOnlySpan<T> actual)
Expand Down
1 change: 1 addition & 0 deletions src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<Compile Include="Fuzzers\Base64Fuzzer.cs" />
<Compile Include="Fuzzers\Base64UrlFuzzer.cs" />
<Compile Include="Fuzzers\HttpHeadersFuzzer.cs" />
<Compile Include="Fuzzers\IPAddressFuzzer.cs" />
<Compile Include="Fuzzers\JsonDocumentFuzzer.cs" />
<Compile Include="Fuzzers\NrbfDecoderFuzzer.cs" />
<Compile Include="Fuzzers\SearchValuesByteCharFuzzer.cs" />
Expand Down
107 changes: 107 additions & 0 deletions src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/IPAddressFuzzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Unicode;

namespace DotnetFuzzing.Fuzzers
{
internal sealed class IPAddressFuzzer : IFuzzer
{
public string[] TargetAssemblies => ["System.Net.Primitives", "System.Private.Uri"];
public string[] TargetCoreLibPrefixes => [];

public void FuzzTarget(ReadOnlySpan<byte> bytes)
{
using var poisonedBytes = PooledBoundedMemory<byte>.Rent(bytes, PoisonPagePlacement.After);
using var poisonedChars = PooledBoundedMemory<char>.Rent(MemoryMarshal.Cast<byte, char>(bytes), PoisonPagePlacement.After);

if (IPAddress.IsValidUtf8(poisonedBytes.Span))
{
TestValidInput(bytes: poisonedBytes.Span);
}
else
{
Assert.False(IPAddress.TryParse(poisonedBytes.Span, out _));
}

if (IPAddress.IsValid(poisonedChars.Span))
{
TestValidInput(chars: poisonedChars.Span.ToString());
}
else
{
Assert.False(IPAddress.TryParse(poisonedChars.Span, out _));
}

static void TestValidInput(ReadOnlySpan<byte> bytes = default, string? chars = null)
{
if (chars is null)
{
// bytes past the '%' may not be valid UTF-8: https://github.com/dotnet/runtime/issues/111288
int percentIndex = bytes.IndexOf((byte)'%');
Assert.True(Utf8.IsValid(bytes.Slice(0, percentIndex < 0 ? bytes.Length : percentIndex)));

chars = Encoding.UTF8.GetString(bytes);
}
else
{
bytes = Encoding.UTF8.GetBytes(chars);
}

Assert.True(IPAddress.IsValid(chars));
Assert.True(IPAddress.TryParse(chars, out IPAddress? ipFromChars));
Assert.True(IPAddress.TryParse(bytes, out IPAddress? ipFromBytes));

Assert.True(ipFromChars.Equals(ipFromBytes));
Assert.True(ipFromBytes.Equals(ipFromChars));

Assert.True(IPAddress.IsValid(ipFromChars.ToString()));

TestUri(chars);
}

static void TestUri(string chars)
{
bool isIpv6 = chars.Contains(':');
UriHostNameType hostNameType = isIpv6 ? UriHostNameType.IPv6 : UriHostNameType.IPv4;

if (isIpv6)
{
// Remove the ScopeId
int percentIndex = chars.IndexOf('%');
if (percentIndex >= 0)
{
chars = chars.Substring(0, percentIndex);
if (chars.StartsWith('['))
{
chars = $"{chars}]";
}
}

if (!chars.StartsWith('['))
{
chars = $"[{chars}]";
}

// Remove the port
int bracketIndex = chars.IndexOf(']');
if (bracketIndex >= 0 &&
bracketIndex + 1 < chars.Length &&
chars[bracketIndex + 1] == ':')
{
chars = chars.Substring(0, bracketIndex + 1);
}
}

Assert.True(Uri.TryCreate($"http://{chars}/", UriKind.Absolute, out Uri? uri));
Assert.Equal(hostNameType, uri.HostNameType);
Assert.Equal(hostNameType, Uri.CheckHostName(chars));
}
}
}
}
31 changes: 22 additions & 9 deletions src/libraries/Fuzzing/DotnetFuzzing/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,33 @@ await DownloadArtifactAsync(
"https://github.com/Metalnem/libfuzzer-dotnet/releases/download/v2023.06.26.1359/libfuzzer-dotnet-windows.exe",
"cbc1f510caaec01b17b5e89fc780f426710acee7429151634bbf4d0c57583458").ConfigureAwait(false);

foreach (IFuzzer fuzzer in fuzzers)
Console.WriteLine("Preparing fuzzers ...");

List<string> exceptions = new();

Parallel.ForEach(fuzzers, fuzzer =>
{
Console.WriteLine();
Console.WriteLine($"Preparing {fuzzer.Name} ...");
try
{
PrepareFuzzer(fuzzer);
}
catch (Exception ex)
{
exceptions.Add($"Failed to prepare {fuzzer.Name}: {ex.Message}");
}
});

if (exceptions.Count != 0)
{
Console.WriteLine(string.Join('\n', exceptions));
throw new Exception($"Failed to prepare {exceptions.Count} fuzzers.");
}

void PrepareFuzzer(IFuzzer fuzzer)
{
string fuzzerDirectory = Path.Combine(outputDirectory, fuzzer.Name);
Directory.CreateDirectory(fuzzerDirectory);

Console.WriteLine($"Copying artifacts to {fuzzerDirectory}");
// NOTE: The expected fuzzer directory structure is currently flat.
// If we ever need to support subdirectories, OneFuzzConfig.json must also be updated to use PreservePathsJobDependencies.
foreach (string file in Directory.GetFiles(publishDirectory))
Expand All @@ -138,10 +156,7 @@ await DownloadArtifactAsync(

InstrumentAssemblies(fuzzer, fuzzerDirectory);

Console.WriteLine("Generating OneFuzzConfig.json");
File.WriteAllText(Path.Combine(fuzzerDirectory, "OneFuzzConfig.json"), GenerateOneFuzzConfigJson(fuzzer));

Console.WriteLine("Generating local-run.bat");
File.WriteAllText(Path.Combine(fuzzerDirectory, "local-run.bat"), GenerateLocalRunHelperScript(fuzzer));
}

Expand Down Expand Up @@ -195,8 +210,6 @@ private static void InstrumentAssemblies(IFuzzer fuzzer, string fuzzerDirectory)
{
foreach (var (assembly, prefixes) in GetInstrumentationTargets(fuzzer))
{
Console.WriteLine($"Instrumenting {assembly} {(prefixes is null ? "" : $"({prefixes})")}");

string path = Path.Combine(fuzzerDirectory, assembly);
if (!File.Exists(path))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ public static string[] BuildServiceNames(string uriPrefix)

if (hostname == "*" ||
hostname == "+" ||
IPAddress.TryParse(hostname, out _))
IPAddress.IsValid(hostname))
{
// for a wildcard, register the machine name. If the caller doesn't have DNS permission
// or the query fails for some reason, don't add an SPN.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ public IPAddress(System.ReadOnlySpan<byte> address, long scopeid) { }
public static int HostToNetworkOrder(int host) { throw null; }
public static long HostToNetworkOrder(long host) { throw null; }
public static bool IsLoopback(System.Net.IPAddress address) { throw null; }
public static bool IsValidUtf8(System.ReadOnlySpan<byte> utf8Text) { throw null; }
public static bool IsValid(System.ReadOnlySpan<char> ipSpan) { throw null; }
public System.Net.IPAddress MapToIPv4() { throw null; }
public System.Net.IPAddress MapToIPv6() { throw null; }
public static short NetworkToHostOrder(short network) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,14 @@ internal IPAddress(int newAddress)
PrivateAddress = (uint)newAddress;
}

/// <summary>Determines whether the provided span contains a valid <see cref="IPAddress"/>.</summary>
/// <param name="ipSpan">The text to parse.</param>
public static bool IsValid(ReadOnlySpan<char> ipSpan) => IPAddressParser.IsValid(ipSpan);

/// <summary>Determines whether the provided span contains a valid <see cref="IPAddress"/>.</summary>
/// <param name="utf8Text">The text to parse.</param>
public static bool IsValidUtf8(ReadOnlySpan<byte> utf8Text) => IPAddressParser.IsValid(utf8Text);

/// <devdoc>
/// <para>
/// Converts an IP address string to an <see cref='System.Net.IPAddress'/> instance.
Expand Down
Loading
Loading