Skip to content

Commit d15475e

Browse files
buyaa-nMihaZupan
authored andcommitted
Add fuzzer for Convert.To/FromBase64 APIs (#108247)
* 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 <mihazupan.zupan1@gmail.com> * 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 <mihazupan.zupan1@gmail.com> --------- Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
1 parent 771c640 commit d15475e

File tree

4 files changed

+81
-29
lines changed

4 files changed

+81
-29
lines changed

src/libraries/Fuzzing/DotnetFuzzing/Assert.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ static void ThrowNull() =>
2929
throw new Exception("Value is null");
3030
}
3131

32+
public static void SequenceEqual<T>(Span<T> expected, Span<T> actual) =>
33+
SequenceEqual((ReadOnlySpan<T>)expected, (ReadOnlySpan<T>)actual);
34+
3235
public static void SequenceEqual<T>(ReadOnlySpan<T> expected, ReadOnlySpan<T> actual)
3336
{
3437
if (!expected.SequenceEqual(actual))

src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@
99
<AppHostSourcePath>$(ArtifactsDir)\bin\win-x64.Debug\corehost\apphost.exe</AppHostSourcePath>
1010
<ImplicitUsings>enable</ImplicitUsings>
1111
<Nullable>enable</Nullable>
12-
<NoWarn>$(NoWarn);CS1591;IL3000;SYSLIB1054;CA1512;SYSLIB5005;</NoWarn>
1312
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
14-
<EnableTrimAnalyzer>false</EnableTrimAnalyzer>
1513
</PropertyGroup>
1614

1715
<ItemGroup>

src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/Base64Fuzzer.cs

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,54 @@
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-
54
using System.Buffers;
65
using System.Buffers.Text;
76

87
namespace DotnetFuzzing.Fuzzers
98
{
109
internal sealed class Base64Fuzzer : IFuzzer
1110
{
11+
private const int Base64LineBreakPosition = 76; // Needs to be in sync with Convert.Base64LineBreakPosition
12+
1213
public string[] TargetAssemblies => [];
1314

14-
public string[] TargetCoreLibPrefixes => ["System.Buffers.Text"];
15+
public string[] TargetCoreLibPrefixes => ["System.Buffers.Text.Base64", "System.Convert"];
1516

1617
public void FuzzTarget(ReadOnlySpan<byte> bytes)
1718
{
18-
using PooledBoundedMemory<byte> inputPoisoned = PooledBoundedMemory<byte>.Rent(bytes, PoisonPagePlacement.After);
19-
Span<byte> input = inputPoisoned.Span;
20-
int maxEncodedLength = Base64.GetMaxEncodedToUtf8Length(bytes.Length);
21-
using PooledBoundedMemory<byte> destPoisoned = PooledBoundedMemory<byte>.Rent(maxEncodedLength, PoisonPagePlacement.After);
19+
using PooledBoundedMemory<byte> inputPoisonBefore = PooledBoundedMemory<byte>.Rent(bytes, PoisonPagePlacement.Before);
20+
using PooledBoundedMemory<byte> inputPoisonAfter = PooledBoundedMemory<byte>.Rent(bytes, PoisonPagePlacement.After);
21+
22+
TestCases(inputPoisonBefore.Span, PoisonPagePlacement.Before);
23+
TestCases(inputPoisonAfter.Span, PoisonPagePlacement.After);
24+
}
25+
26+
private void TestCases(Span<byte> input, PoisonPagePlacement poison)
27+
{
28+
TestBase64(input, poison);
29+
TestToStringToCharArray(input, Base64FormattingOptions.None);
30+
TestToStringToCharArray(input, Base64FormattingOptions.InsertLineBreaks);
31+
}
32+
33+
private void TestBase64(Span<byte> input, PoisonPagePlacement poison)
34+
{
35+
int maxEncodedLength = Base64.GetMaxEncodedToUtf8Length(input.Length);
36+
using PooledBoundedMemory<byte> destPoisoned = PooledBoundedMemory<byte>.Rent(maxEncodedLength, poison);
2237
Span<byte> encoderDest = destPoisoned.Span;
23-
using PooledBoundedMemory<byte> decoderDestPoisoned = PooledBoundedMemory<byte>.Rent(Base64.GetMaxDecodedFromUtf8Length(maxEncodedLength), PoisonPagePlacement.After);
38+
using PooledBoundedMemory<byte> decoderDestPoisoned = PooledBoundedMemory<byte>.Rent(Base64.GetMaxDecodedFromUtf8Length(maxEncodedLength), poison);
2439
Span<byte> decoderDest = decoderDestPoisoned.Span;
2540
{ // IsFinalBlock = true
2641
OperationStatus status = Base64.EncodeToUtf8(input, encoderDest, out int bytesConsumed, out int bytesEncoded);
27-
2842
Assert.Equal(OperationStatus.Done, status);
29-
Assert.Equal(bytes.Length, bytesConsumed);
43+
Assert.Equal(input.Length, bytesConsumed);
3044
Assert.Equal(true, maxEncodedLength >= bytesEncoded && maxEncodedLength - 2 <= bytesEncoded);
3145

3246
status = Base64.DecodeFromUtf8(encoderDest.Slice(0, bytesEncoded), decoderDest, out int bytesRead, out int bytesDecoded);
3347

3448
Assert.Equal(OperationStatus.Done, status);
35-
Assert.Equal(bytes.Length, bytesDecoded);
49+
Assert.Equal(input.Length, bytesDecoded);
3650
Assert.Equal(bytesEncoded, bytesRead);
37-
Assert.SequenceEqual(bytes, decoderDest.Slice(0, bytesDecoded));
51+
Assert.SequenceEqual(input, decoderDest.Slice(0, bytesDecoded));
3852
}
3953

4054
{ // IsFinalBlock = false
@@ -43,18 +57,18 @@ public void FuzzTarget(ReadOnlySpan<byte> bytes)
4357
OperationStatus status = Base64.EncodeToUtf8(input, encoderDest, out int bytesConsumed, out int bytesEncoded, isFinalBlock: false);
4458
Span<byte> decodeInput = encoderDest.Slice(0, bytesEncoded);
4559

46-
if (bytes.Length % 3 == 0)
60+
if (input.Length % 3 == 0)
4761
{
4862
Assert.Equal(OperationStatus.Done, status);
49-
Assert.Equal(bytes.Length, bytesConsumed);
63+
Assert.Equal(input.Length, bytesConsumed);
5064
Assert.Equal(true, maxEncodedLength == bytesEncoded);
5165

5266
status = Base64.DecodeFromUtf8(decodeInput, decoderDest, out int bytesRead, out int bytesDecoded, isFinalBlock: false);
5367

5468
Assert.Equal(OperationStatus.Done, status);
55-
Assert.Equal(bytes.Length, bytesDecoded);
69+
Assert.Equal(input.Length, bytesDecoded);
5670
Assert.Equal(bytesEncoded, bytesRead);
57-
Assert.SequenceEqual(bytes, decoderDest.Slice(0, bytesDecoded));
71+
Assert.SequenceEqual(input, decoderDest.Slice(0, bytesDecoded));
5872
}
5973
else
6074
{
@@ -74,7 +88,7 @@ public void FuzzTarget(ReadOnlySpan<byte> bytes)
7488
Assert.Equal(OperationStatus.NeedMoreData, status);
7589
}
7690

77-
Assert.SequenceEqual(bytes.Slice(0, bytesDecoded), decoderDest.Slice(0, bytesDecoded));
91+
Assert.SequenceEqual(input.Slice(0, bytesDecoded), decoderDest.Slice(0, bytesDecoded));
7892
}
7993
}
8094

@@ -89,8 +103,8 @@ public void FuzzTarget(ReadOnlySpan<byte> bytes)
89103
status = Base64.DecodeFromUtf8InPlace(encoderDest.Slice(0, bytesEncoded), out int bytesDecoded);
90104

91105
Assert.Equal(OperationStatus.Done, status);
92-
Assert.Equal(bytes.Length, bytesDecoded);
93-
Assert.SequenceEqual(bytes, encoderDest.Slice(0, bytesDecoded));
106+
Assert.Equal(input.Length, bytesDecoded);
107+
Assert.SequenceEqual(input, encoderDest.Slice(0, bytesDecoded));
94108
}
95109

96110
{ // Decode the random input directly, Assert IsValid result matches with decoded result
@@ -116,5 +130,42 @@ public void FuzzTarget(ReadOnlySpan<byte> bytes)
116130
}
117131
}
118132
}
133+
134+
private static void TestToStringToCharArray(Span<byte> input, Base64FormattingOptions options)
135+
{
136+
int encodedLength = ToBase64_CalculateOutputLength(input.Length, options == Base64FormattingOptions.InsertLineBreaks);
137+
char[] dest = new char[encodedLength];
138+
139+
string toStringResult = Convert.ToBase64String(input, options);
140+
byte[] decoded = Convert.FromBase64String(toStringResult);
141+
142+
Assert.SequenceEqual(input, decoded);
143+
144+
int written = Convert.ToBase64CharArray(input.ToArray(), 0, input.Length, dest, 0, options);
145+
decoded = Convert.FromBase64CharArray(dest, 0, written);
146+
147+
Assert.SequenceEqual(input, decoded);
148+
Assert.SequenceEqual(toStringResult.AsSpan(), dest.AsSpan(0, written));
149+
}
150+
151+
private static int ToBase64_CalculateOutputLength(int inputLength, bool insertLineBreaks)
152+
{
153+
uint outlen = ((uint)inputLength + 2) / 3 * 4;
154+
155+
if (outlen == 0)
156+
return 0;
157+
158+
if (insertLineBreaks)
159+
{
160+
(uint newLines, uint remainder) = Math.DivRem(outlen, Base64LineBreakPosition);
161+
if (remainder == 0)
162+
{
163+
--newLines;
164+
}
165+
outlen += newLines * 2; // 2 line break chars added: "\r\n"
166+
}
167+
168+
return (int)outlen;
169+
}
119170
}
120171
}

src/libraries/Fuzzing/README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,21 @@ Useful links:
1818
1919
### Prerequisites
2020

21-
Build the runtime if you haven't already:
21+
Build the runtime with the desired configuration if you haven't already:
2222
```cmd
2323
./build.cmd clr+libs -rc release
2424
```
2525

26-
and install the SharpFuzz command line tool:
27-
```cmd
28-
dotnet tool install --global SharpFuzz.CommandLine
29-
```
30-
3126
> [!TIP]
32-
> The project uses a `Release` runtime + `Debug` libraries configuration by default.
27+
> The `-rc release` configuration here builds runime in `Release` and libraries in `Debug` mode.
3328
> Automated fuzzing runs use a `Checked` runtime + `Debug` libraries configuration by default.
3429
> You can use any configuration locally, but `Checked` is recommended when testing changes in `System.Private.CoreLib`.
3530
31+
Install the SharpFuzz command line tool:
32+
```cmd
33+
dotnet tool install --global SharpFuzz.CommandLine
34+
```
35+
3636
### Fuzzing locally
3737

3838
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
4343
dotnet build
4444
```
4545

46-
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.
46+
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.
4747
When iterating on changes, remember to rebuild the project again: `dotnet build; .\run.bat`.
4848

4949
```cmd
5050
run.bat
5151
```
5252

53-
You can now start fuzzing by running the `local-run.bat` script in the folder of the fuzzer you are interested in.
53+
Start fuzzing by running the `local-run.bat` script in the folder of the fuzzer you are interested in.
5454
```cmd
5555
deployment/HttpHeadersFuzzer/local-run.bat
5656
```

0 commit comments

Comments
 (0)