Skip to content

Commit

Permalink
Add IPAddress.IsValid (dotnet#111433)
Browse files Browse the repository at this point in the history
* Add IPAddress.IsValid

* Speed up fuzzing inner loop

* Rename IsValid for bytes to IsValidUtf8
  • Loading branch information
MihaZupan authored Jan 15, 2025
1 parent 73bcb1d commit 8104cb1
Show file tree
Hide file tree
Showing 18 changed files with 234 additions and 98 deletions.
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

0 comments on commit 8104cb1

Please sign in to comment.