Skip to content

Commit

Permalink
Add fuzzer for Convert.To/FromBase64 APIs (#108247)
Browse files Browse the repository at this point in the history
* Add fuzzer for Convert.To/FromBase64 APIs

* Remove asserting invalid chars, decoding logic should detect that

* Apply suggestions from code review

Co-authored-by: Miha Zupan <[email protected]>

* Add ConvertToBase64Fuzzer to the project

* Move ConvertToBase64Fuzzer logic to Base64Fuzzer, remove analyzer supressions, update readme

* Apply suggestions from code review

Co-authored-by: Miha Zupan <[email protected]>

---------

Co-authored-by: Miha Zupan <[email protected]>
  • Loading branch information
buyaa-n and MihaZupan authored Oct 8, 2024
1 parent 124dd32 commit a0d5071
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 29 deletions.
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>
<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

0 comments on commit a0d5071

Please sign in to comment.