Skip to content

Commit 8104cb1

Browse files
authored
Add IPAddress.IsValid (#111433)
* Add IPAddress.IsValid * Speed up fuzzing inner loop * Rename IsValid for bytes to IsValidUtf8
1 parent 73bcb1d commit 8104cb1

File tree

18 files changed

+234
-98
lines changed

18 files changed

+234
-98
lines changed

eng/pipelines/libraries/fuzzing/deploy-to-onefuzz.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ extends:
9898
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
9999
displayName: Send HttpHeadersFuzzer to OneFuzz
100100

101+
- task: onefuzz-task@0
102+
inputs:
103+
onefuzzOSes: 'Windows'
104+
env:
105+
onefuzzDropDirectory: $(fuzzerProject)/deployment/IPAddressFuzzer
106+
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
107+
displayName: Send IPAddressFuzzer to OneFuzz
108+
101109
- task: onefuzz-task@0
102110
inputs:
103111
onefuzzOSes: 'Windows'

src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth
408408

409409
if (sslAuthenticationOptions.IsClient)
410410
{
411-
if (!string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost) && !TargetHostNameHelper.IsValidAddress(sslAuthenticationOptions.TargetHost))
411+
if (!string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost) && !IPAddress.IsValid(sslAuthenticationOptions.TargetHost))
412412
{
413413
// Similar to windows behavior, set SNI on openssl by default for client context, ignore errors.
414414
if (!Ssl.SslSetTlsExtHostName(sslHandle, sslAuthenticationOptions.TargetHost))

src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ internal static bool ShouldHaveIpv4Embedded(ReadOnlySpan<ushort> numbers)
9595

9696
// Remarks: MUST NOT be used unless all input indexes are verified and trusted.
9797
// start must be next to '[' position, or error is reported
98-
internal static unsafe bool IsValidStrict<TChar>(TChar* name, int start, ref int end)
98+
internal static unsafe bool IsValidStrict<TChar>(TChar* name, int start, int end)
9999
where TChar : unmanaged, IBinaryInteger<TChar>
100100
{
101101
Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));
Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
3+
34
using System.Buffers;
4-
using System.Collections.Generic;
55
using System.Globalization;
6-
using System.Runtime.InteropServices;
76

87
namespace System.Net.Security
98
{
@@ -37,45 +36,5 @@ internal static string NormalizeHostName(string? targetHost)
3736

3837
return targetHost;
3938
}
40-
41-
// Simplified version of IPAddressParser.Parse to avoid allocations and dependencies.
42-
// It purposely ignores scopeId as we don't really use so we do not need to map it to actual interface id.
43-
internal static unsafe bool IsValidAddress(string? hostname)
44-
{
45-
if (string.IsNullOrEmpty(hostname))
46-
{
47-
return false;
48-
}
49-
50-
ReadOnlySpan<char> ipSpan = hostname.AsSpan();
51-
52-
int end = ipSpan.Length;
53-
54-
if (ipSpan.Contains(':'))
55-
{
56-
// The address is parsed as IPv6 if and only if it contains a colon. This is valid because
57-
// we don't support/parse a port specification at the end of an IPv4 address.
58-
fixed (char* ipStringPtr = &MemoryMarshal.GetReference(ipSpan))
59-
{
60-
return IPv6AddressHelper.IsValidStrict(ipStringPtr, 0, ref end);
61-
}
62-
}
63-
else if (char.IsDigit(ipSpan[0]))
64-
{
65-
long tmpAddr;
66-
67-
fixed (char* ipStringPtr = &MemoryMarshal.GetReference(ipSpan))
68-
{
69-
tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipStringPtr, 0, ref end, notImplicitFile: true);
70-
}
71-
72-
if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length)
73-
{
74-
return true;
75-
}
76-
}
77-
78-
return false;
79-
}
8039
}
8140
}

src/libraries/Fuzzing/DotnetFuzzing/Assert.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
5+
46
namespace DotnetFuzzing;
57

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

23+
public static void True([DoesNotReturnIf(false)] bool actual) =>
24+
Equal(true, actual);
25+
26+
public static void False([DoesNotReturnIf(true)] bool actual) =>
27+
Equal(false, actual);
28+
2129
public static void NotNull<T>(T value)
2230
{
2331
if (value == null)
@@ -26,7 +34,7 @@ public static void NotNull<T>(T value)
2634
}
2735

2836
static void ThrowNull() =>
29-
throw new Exception("Value is null");
37+
throw new Exception("Value is null");
3038
}
3139

3240
public static void SequenceEqual<T>(ReadOnlySpan<T> expected, ReadOnlySpan<T> actual)

src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<Compile Include="Fuzzers\Base64Fuzzer.cs" />
2323
<Compile Include="Fuzzers\Base64UrlFuzzer.cs" />
2424
<Compile Include="Fuzzers\HttpHeadersFuzzer.cs" />
25+
<Compile Include="Fuzzers\IPAddressFuzzer.cs" />
2526
<Compile Include="Fuzzers\JsonDocumentFuzzer.cs" />
2627
<Compile Include="Fuzzers\NrbfDecoderFuzzer.cs" />
2728
<Compile Include="Fuzzers\SearchValuesByteCharFuzzer.cs" />
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Buffers;
5+
using System.Net;
6+
using System.Net.Sockets;
7+
using System.Runtime.InteropServices;
8+
using System.Text;
9+
using System.Text.Unicode;
10+
11+
namespace DotnetFuzzing.Fuzzers
12+
{
13+
internal sealed class IPAddressFuzzer : IFuzzer
14+
{
15+
public string[] TargetAssemblies => ["System.Net.Primitives", "System.Private.Uri"];
16+
public string[] TargetCoreLibPrefixes => [];
17+
18+
public void FuzzTarget(ReadOnlySpan<byte> bytes)
19+
{
20+
using var poisonedBytes = PooledBoundedMemory<byte>.Rent(bytes, PoisonPagePlacement.After);
21+
using var poisonedChars = PooledBoundedMemory<char>.Rent(MemoryMarshal.Cast<byte, char>(bytes), PoisonPagePlacement.After);
22+
23+
if (IPAddress.IsValidUtf8(poisonedBytes.Span))
24+
{
25+
TestValidInput(bytes: poisonedBytes.Span);
26+
}
27+
else
28+
{
29+
Assert.False(IPAddress.TryParse(poisonedBytes.Span, out _));
30+
}
31+
32+
if (IPAddress.IsValid(poisonedChars.Span))
33+
{
34+
TestValidInput(chars: poisonedChars.Span.ToString());
35+
}
36+
else
37+
{
38+
Assert.False(IPAddress.TryParse(poisonedChars.Span, out _));
39+
}
40+
41+
static void TestValidInput(ReadOnlySpan<byte> bytes = default, string? chars = null)
42+
{
43+
if (chars is null)
44+
{
45+
// bytes past the '%' may not be valid UTF-8: https://github.com/dotnet/runtime/issues/111288
46+
int percentIndex = bytes.IndexOf((byte)'%');
47+
Assert.True(Utf8.IsValid(bytes.Slice(0, percentIndex < 0 ? bytes.Length : percentIndex)));
48+
49+
chars = Encoding.UTF8.GetString(bytes);
50+
}
51+
else
52+
{
53+
bytes = Encoding.UTF8.GetBytes(chars);
54+
}
55+
56+
Assert.True(IPAddress.IsValid(chars));
57+
Assert.True(IPAddress.TryParse(chars, out IPAddress? ipFromChars));
58+
Assert.True(IPAddress.TryParse(bytes, out IPAddress? ipFromBytes));
59+
60+
Assert.True(ipFromChars.Equals(ipFromBytes));
61+
Assert.True(ipFromBytes.Equals(ipFromChars));
62+
63+
Assert.True(IPAddress.IsValid(ipFromChars.ToString()));
64+
65+
TestUri(chars);
66+
}
67+
68+
static void TestUri(string chars)
69+
{
70+
bool isIpv6 = chars.Contains(':');
71+
UriHostNameType hostNameType = isIpv6 ? UriHostNameType.IPv6 : UriHostNameType.IPv4;
72+
73+
if (isIpv6)
74+
{
75+
// Remove the ScopeId
76+
int percentIndex = chars.IndexOf('%');
77+
if (percentIndex >= 0)
78+
{
79+
chars = chars.Substring(0, percentIndex);
80+
if (chars.StartsWith('['))
81+
{
82+
chars = $"{chars}]";
83+
}
84+
}
85+
86+
if (!chars.StartsWith('['))
87+
{
88+
chars = $"[{chars}]";
89+
}
90+
91+
// Remove the port
92+
int bracketIndex = chars.IndexOf(']');
93+
if (bracketIndex >= 0 &&
94+
bracketIndex + 1 < chars.Length &&
95+
chars[bracketIndex + 1] == ':')
96+
{
97+
chars = chars.Substring(0, bracketIndex + 1);
98+
}
99+
}
100+
101+
Assert.True(Uri.TryCreate($"http://{chars}/", UriKind.Absolute, out Uri? uri));
102+
Assert.Equal(hostNameType, uri.HostNameType);
103+
Assert.Equal(hostNameType, Uri.CheckHostName(chars));
104+
}
105+
}
106+
}
107+
}

src/libraries/Fuzzing/DotnetFuzzing/Program.cs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,33 @@ await DownloadArtifactAsync(
110110
"https://github.com/Metalnem/libfuzzer-dotnet/releases/download/v2023.06.26.1359/libfuzzer-dotnet-windows.exe",
111111
"cbc1f510caaec01b17b5e89fc780f426710acee7429151634bbf4d0c57583458").ConfigureAwait(false);
112112

113-
foreach (IFuzzer fuzzer in fuzzers)
113+
Console.WriteLine("Preparing fuzzers ...");
114+
115+
List<string> exceptions = new();
116+
117+
Parallel.ForEach(fuzzers, fuzzer =>
114118
{
115-
Console.WriteLine();
116-
Console.WriteLine($"Preparing {fuzzer.Name} ...");
119+
try
120+
{
121+
PrepareFuzzer(fuzzer);
122+
}
123+
catch (Exception ex)
124+
{
125+
exceptions.Add($"Failed to prepare {fuzzer.Name}: {ex.Message}");
126+
}
127+
});
117128

129+
if (exceptions.Count != 0)
130+
{
131+
Console.WriteLine(string.Join('\n', exceptions));
132+
throw new Exception($"Failed to prepare {exceptions.Count} fuzzers.");
133+
}
134+
135+
void PrepareFuzzer(IFuzzer fuzzer)
136+
{
118137
string fuzzerDirectory = Path.Combine(outputDirectory, fuzzer.Name);
119138
Directory.CreateDirectory(fuzzerDirectory);
120139

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

139157
InstrumentAssemblies(fuzzer, fuzzerDirectory);
140158

141-
Console.WriteLine("Generating OneFuzzConfig.json");
142159
File.WriteAllText(Path.Combine(fuzzerDirectory, "OneFuzzConfig.json"), GenerateOneFuzzConfigJson(fuzzer));
143-
144-
Console.WriteLine("Generating local-run.bat");
145160
File.WriteAllText(Path.Combine(fuzzerDirectory, "local-run.bat"), GenerateLocalRunHelperScript(fuzzer));
146161
}
147162

@@ -195,8 +210,6 @@ private static void InstrumentAssemblies(IFuzzer fuzzer, string fuzzerDirectory)
195210
{
196211
foreach (var (assembly, prefixes) in GetInstrumentationTargets(fuzzer))
197212
{
198-
Console.WriteLine($"Instrumenting {assembly} {(prefixes is null ? "" : $"({prefixes})")}");
199-
200213
string path = Path.Combine(fuzzerDirectory, assembly);
201214
if (!File.Exists(path))
202215
{

src/libraries/System.Net.HttpListener/src/System/Net/ServiceNameStore.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ public static string[] BuildServiceNames(string uriPrefix)
272272

273273
if (hostname == "*" ||
274274
hostname == "+" ||
275-
IPAddress.TryParse(hostname, out _))
275+
IPAddress.IsValid(hostname))
276276
{
277277
// for a wildcard, register the machine name. If the caller doesn't have DNS permission
278278
// or the query fails for some reason, don't add an SPN.

src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,8 @@ public IPAddress(System.ReadOnlySpan<byte> address, long scopeid) { }
256256
public static int HostToNetworkOrder(int host) { throw null; }
257257
public static long HostToNetworkOrder(long host) { throw null; }
258258
public static bool IsLoopback(System.Net.IPAddress address) { throw null; }
259+
public static bool IsValidUtf8(System.ReadOnlySpan<byte> utf8Text) { throw null; }
260+
public static bool IsValid(System.ReadOnlySpan<char> ipSpan) { throw null; }
259261
public System.Net.IPAddress MapToIPv4() { throw null; }
260262
public System.Net.IPAddress MapToIPv6() { throw null; }
261263
public static short NetworkToHostOrder(short network) { throw null; }

0 commit comments

Comments
 (0)