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 fuzzer for Convert.To/FromBase64 APIs #108247

Merged
merged 7 commits into from
Oct 8, 2024
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
3 changes: 3 additions & 0 deletions src/libraries/Fuzzing/DotnetFuzzing/Assert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ static void ThrowNull() =>
throw new Exception("Value is null");
}

public static void SequenceEqual<T>(Span<T> expected, Span<T> actual) =>
SequenceEqual((ReadOnlySpan<T>)expected, (ReadOnlySpan<T>)actual);

public static void SequenceEqual<T>(ReadOnlySpan<T> expected, ReadOnlySpan<T> actual)
{
if (!expected.SequenceEqual(actual))
Expand Down
2 changes: 0 additions & 2 deletions src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
<AppHostSourcePath>$(ArtifactsDir)\bin\win-x64.Debug\corehost\apphost.exe</AppHostSourcePath>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn);CS1591;IL3000;SYSLIB1054;CA1512;SYSLIB5005;</NoWarn>
Copy link
Member

Choose a reason for hiding this comment

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

Was this part intentional?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, since we set the IsTestSupportProject property in

<IsTestSupportProject>true</IsTestSupportProject>
these analyzer diagnostics are not hit on build, probably because we suppress warning on test build

<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EnableTrimAnalyzer>false</EnableTrimAnalyzer>
</PropertyGroup>

<ItemGroup>
Expand Down
87 changes: 69 additions & 18 deletions src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/Base64Fuzzer.cs
Original file line number Diff line number Diff line change
@@ -1,40 +1,54 @@
// 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;

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<byte> bytes)
{
using PooledBoundedMemory<byte> inputPoisoned = PooledBoundedMemory<byte>.Rent(bytes, PoisonPagePlacement.After);
Span<byte> input = inputPoisoned.Span;
int maxEncodedLength = Base64.GetMaxEncodedToUtf8Length(bytes.Length);
using PooledBoundedMemory<byte> destPoisoned = PooledBoundedMemory<byte>.Rent(maxEncodedLength, PoisonPagePlacement.After);
using PooledBoundedMemory<byte> inputPoisonBefore = PooledBoundedMemory<byte>.Rent(bytes, PoisonPagePlacement.Before);
using PooledBoundedMemory<byte> inputPoisonAfter = PooledBoundedMemory<byte>.Rent(bytes, PoisonPagePlacement.After);

TestCases(inputPoisonBefore.Span, PoisonPagePlacement.Before);
TestCases(inputPoisonAfter.Span, PoisonPagePlacement.After);
}

private void TestCases(Span<byte> input, PoisonPagePlacement poison)
{
TestBase64(input, poison);
TestToStringToCharArray(input, Base64FormattingOptions.None);
TestToStringToCharArray(input, Base64FormattingOptions.InsertLineBreaks);
}

private void TestBase64(Span<byte> input, PoisonPagePlacement poison)
{
int maxEncodedLength = Base64.GetMaxEncodedToUtf8Length(input.Length);
using PooledBoundedMemory<byte> destPoisoned = PooledBoundedMemory<byte>.Rent(maxEncodedLength, poison);
Span<byte> encoderDest = destPoisoned.Span;
using PooledBoundedMemory<byte> decoderDestPoisoned = PooledBoundedMemory<byte>.Rent(Base64.GetMaxDecodedFromUtf8Length(maxEncodedLength), PoisonPagePlacement.After);
using PooledBoundedMemory<byte> decoderDestPoisoned = PooledBoundedMemory<byte>.Rent(Base64.GetMaxDecodedFromUtf8Length(maxEncodedLength), poison);
Span<byte> 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
Expand All @@ -43,18 +57,18 @@ public void FuzzTarget(ReadOnlySpan<byte> bytes)
OperationStatus status = Base64.EncodeToUtf8(input, encoderDest, out int bytesConsumed, out int bytesEncoded, isFinalBlock: false);
Span<byte> 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
{
Expand All @@ -74,7 +88,7 @@ public void FuzzTarget(ReadOnlySpan<byte> 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));
}
}

Expand All @@ -89,8 +103,8 @@ public void FuzzTarget(ReadOnlySpan<byte> 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
Expand All @@ -116,5 +130,42 @@ public void FuzzTarget(ReadOnlySpan<byte> bytes)
}
}
}

private static void TestToStringToCharArray(Span<byte> 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;
}
}
}
18 changes: 9 additions & 9 deletions src/libraries/Fuzzing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
```
Expand Down
Loading