diff --git a/src/libraries/Fuzzing/DotnetFuzzing/Assert.cs b/src/libraries/Fuzzing/DotnetFuzzing/Assert.cs index a5f2a9dd1d195b..be3e7a66defc16 100644 --- a/src/libraries/Fuzzing/DotnetFuzzing/Assert.cs +++ b/src/libraries/Fuzzing/DotnetFuzzing/Assert.cs @@ -29,6 +29,9 @@ static void ThrowNull() => throw new Exception("Value is null"); } + public static void SequenceEqual(Span expected, Span actual) => + SequenceEqual((ReadOnlySpan)expected, (ReadOnlySpan)actual); + public static void SequenceEqual(ReadOnlySpan expected, ReadOnlySpan actual) { if (!expected.SequenceEqual(actual)) diff --git a/src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj b/src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj index a0aca709b3e679..f538468d180f19 100644 --- a/src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj +++ b/src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj @@ -9,9 +9,7 @@ $(ArtifactsDir)\bin\win-x64.Debug\corehost\apphost.exe enable enable - $(NoWarn);CS1591;IL3000;SYSLIB1054;CA1512;SYSLIB5005; true - false diff --git a/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/Base64Fuzzer.cs b/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/Base64Fuzzer.cs index 000718de72537e..056874a30a57f3 100644 --- a/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/Base64Fuzzer.cs +++ b/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/Base64Fuzzer.cs @@ -1,7 +1,6 @@ // 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.Buffers.Text; @@ -9,32 +8,47 @@ namespace DotnetFuzzing.Fuzzers { internal sealed class Base64Fuzzer : IFuzzer { + private const int Base64LineBreakPosition = 76; // Needs to be in sync with Convert.Base64LineBreakPosition + public string[] TargetAssemblies => []; - public string[] TargetCoreLibPrefixes => ["System.Buffers.Text"]; + public string[] TargetCoreLibPrefixes => ["System.Buffers.Text.Base64", "System.Convert"]; public void FuzzTarget(ReadOnlySpan bytes) { - using PooledBoundedMemory inputPoisoned = PooledBoundedMemory.Rent(bytes, PoisonPagePlacement.After); - Span input = inputPoisoned.Span; - int maxEncodedLength = Base64.GetMaxEncodedToUtf8Length(bytes.Length); - using PooledBoundedMemory destPoisoned = PooledBoundedMemory.Rent(maxEncodedLength, PoisonPagePlacement.After); + using PooledBoundedMemory inputPoisonBefore = PooledBoundedMemory.Rent(bytes, PoisonPagePlacement.Before); + using PooledBoundedMemory inputPoisonAfter = PooledBoundedMemory.Rent(bytes, PoisonPagePlacement.After); + + TestCases(inputPoisonBefore.Span, PoisonPagePlacement.Before); + TestCases(inputPoisonAfter.Span, PoisonPagePlacement.After); + } + + private void TestCases(Span input, PoisonPagePlacement poison) + { + TestBase64(input, poison); + TestToStringToCharArray(input, Base64FormattingOptions.None); + TestToStringToCharArray(input, Base64FormattingOptions.InsertLineBreaks); + } + + private void TestBase64(Span input, PoisonPagePlacement poison) + { + int maxEncodedLength = Base64.GetMaxEncodedToUtf8Length(input.Length); + using PooledBoundedMemory destPoisoned = PooledBoundedMemory.Rent(maxEncodedLength, poison); Span encoderDest = destPoisoned.Span; - using PooledBoundedMemory decoderDestPoisoned = PooledBoundedMemory.Rent(Base64.GetMaxDecodedFromUtf8Length(maxEncodedLength), PoisonPagePlacement.After); + using PooledBoundedMemory decoderDestPoisoned = PooledBoundedMemory.Rent(Base64.GetMaxDecodedFromUtf8Length(maxEncodedLength), poison); Span decoderDest = decoderDestPoisoned.Span; { // IsFinalBlock = true OperationStatus status = Base64.EncodeToUtf8(input, encoderDest, out int bytesConsumed, out int bytesEncoded); - Assert.Equal(OperationStatus.Done, status); - Assert.Equal(bytes.Length, bytesConsumed); + Assert.Equal(input.Length, bytesConsumed); Assert.Equal(true, maxEncodedLength >= bytesEncoded && maxEncodedLength - 2 <= bytesEncoded); status = Base64.DecodeFromUtf8(encoderDest.Slice(0, bytesEncoded), decoderDest, out int bytesRead, out int bytesDecoded); Assert.Equal(OperationStatus.Done, status); - Assert.Equal(bytes.Length, bytesDecoded); + Assert.Equal(input.Length, bytesDecoded); Assert.Equal(bytesEncoded, bytesRead); - Assert.SequenceEqual(bytes, decoderDest.Slice(0, bytesDecoded)); + Assert.SequenceEqual(input, decoderDest.Slice(0, bytesDecoded)); } { // IsFinalBlock = false @@ -43,18 +57,18 @@ public void FuzzTarget(ReadOnlySpan bytes) OperationStatus status = Base64.EncodeToUtf8(input, encoderDest, out int bytesConsumed, out int bytesEncoded, isFinalBlock: false); Span decodeInput = encoderDest.Slice(0, bytesEncoded); - if (bytes.Length % 3 == 0) + if (input.Length % 3 == 0) { Assert.Equal(OperationStatus.Done, status); - Assert.Equal(bytes.Length, bytesConsumed); + Assert.Equal(input.Length, bytesConsumed); Assert.Equal(true, maxEncodedLength == bytesEncoded); status = Base64.DecodeFromUtf8(decodeInput, decoderDest, out int bytesRead, out int bytesDecoded, isFinalBlock: false); Assert.Equal(OperationStatus.Done, status); - Assert.Equal(bytes.Length, bytesDecoded); + Assert.Equal(input.Length, bytesDecoded); Assert.Equal(bytesEncoded, bytesRead); - Assert.SequenceEqual(bytes, decoderDest.Slice(0, bytesDecoded)); + Assert.SequenceEqual(input, decoderDest.Slice(0, bytesDecoded)); } else { @@ -74,7 +88,7 @@ public void FuzzTarget(ReadOnlySpan bytes) Assert.Equal(OperationStatus.NeedMoreData, status); } - Assert.SequenceEqual(bytes.Slice(0, bytesDecoded), decoderDest.Slice(0, bytesDecoded)); + Assert.SequenceEqual(input.Slice(0, bytesDecoded), decoderDest.Slice(0, bytesDecoded)); } } @@ -89,8 +103,8 @@ public void FuzzTarget(ReadOnlySpan bytes) status = Base64.DecodeFromUtf8InPlace(encoderDest.Slice(0, bytesEncoded), out int bytesDecoded); Assert.Equal(OperationStatus.Done, status); - Assert.Equal(bytes.Length, bytesDecoded); - Assert.SequenceEqual(bytes, encoderDest.Slice(0, bytesDecoded)); + Assert.Equal(input.Length, bytesDecoded); + Assert.SequenceEqual(input, encoderDest.Slice(0, bytesDecoded)); } { // Decode the random input directly, Assert IsValid result matches with decoded result @@ -116,5 +130,42 @@ public void FuzzTarget(ReadOnlySpan bytes) } } } + + private static void TestToStringToCharArray(Span input, Base64FormattingOptions options) + { + int encodedLength = ToBase64_CalculateOutputLength(input.Length, options == Base64FormattingOptions.InsertLineBreaks); + char[] dest = new char[encodedLength]; + + string toStringResult = Convert.ToBase64String(input, options); + byte[] decoded = Convert.FromBase64String(toStringResult); + + Assert.SequenceEqual(input, decoded); + + int written = Convert.ToBase64CharArray(input.ToArray(), 0, input.Length, dest, 0, options); + decoded = Convert.FromBase64CharArray(dest, 0, written); + + Assert.SequenceEqual(input, decoded); + Assert.SequenceEqual(toStringResult.AsSpan(), dest.AsSpan(0, written)); + } + + private static int ToBase64_CalculateOutputLength(int inputLength, bool insertLineBreaks) + { + uint outlen = ((uint)inputLength + 2) / 3 * 4; + + if (outlen == 0) + return 0; + + if (insertLineBreaks) + { + (uint newLines, uint remainder) = Math.DivRem(outlen, Base64LineBreakPosition); + if (remainder == 0) + { + --newLines; + } + outlen += newLines * 2; // 2 line break chars added: "\r\n" + } + + return (int)outlen; + } } } diff --git a/src/libraries/Fuzzing/README.md b/src/libraries/Fuzzing/README.md index 4ac8125e915837..ff9b55c2f46697 100644 --- a/src/libraries/Fuzzing/README.md +++ b/src/libraries/Fuzzing/README.md @@ -18,21 +18,21 @@ Useful links: ### Prerequisites -Build the runtime if you haven't already: +Build the runtime with the desired configuration if you haven't already: ```cmd ./build.cmd clr+libs -rc release ``` -and install the SharpFuzz command line tool: -```cmd -dotnet tool install --global SharpFuzz.CommandLine -``` - > [!TIP] -> The project uses a `Release` runtime + `Debug` libraries configuration by default. +> The `-rc release` configuration here builds runime in `Release` and libraries in `Debug` mode. > Automated fuzzing runs use a `Checked` runtime + `Debug` libraries configuration by default. > You can use any configuration locally, but `Checked` is recommended when testing changes in `System.Private.CoreLib`. +Install the SharpFuzz command line tool: +```cmd +dotnet tool install --global SharpFuzz.CommandLine +``` + ### Fuzzing locally Build the `DotnetFuzzing` fuzzing project. It is self-contained, so it will produce `DotnetFuzzing.exe` along with a copy of all required libraries. @@ -43,14 +43,14 @@ cd src/libraries/Fuzzing/DotnetFuzzing dotnet build ``` -Now you can run `run.bat`, which will create separate directories for each fuzzing target, instrument the relevant assemblies, and generate a helper script for running them locally. +Run `run.bat`, which will create separate directories for each fuzzing target, instrument the relevant assemblies, and generate a helper script for running them locally. When iterating on changes, remember to rebuild the project again: `dotnet build; .\run.bat`. ```cmd run.bat ``` -You can now start fuzzing by running the `local-run.bat` script in the folder of the fuzzer you are interested in. +Start fuzzing by running the `local-run.bat` script in the folder of the fuzzer you are interested in. ```cmd deployment/HttpHeadersFuzzer/local-run.bat ```