From 439782dbcbf6c6fea949b5307f80efcdada8749c Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Wed, 25 Sep 2024 10:13:42 -0700 Subject: [PATCH 1/6] Add fuzzer for Convert.To/FromBase64 APIs --- .../libraries/fuzzing/deploy-to-onefuzz.yml | 8 ++ src/libraries/Fuzzing/DotnetFuzzing/Assert.cs | 17 ++++ .../Fuzzers/ConvertToBase64Fuzzer.cs | 98 +++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs diff --git a/eng/pipelines/libraries/fuzzing/deploy-to-onefuzz.yml b/eng/pipelines/libraries/fuzzing/deploy-to-onefuzz.yml index 49e7e517d9c336..9a9a78654565b5 100644 --- a/eng/pipelines/libraries/fuzzing/deploy-to-onefuzz.yml +++ b/eng/pipelines/libraries/fuzzing/deploy-to-onefuzz.yml @@ -89,6 +89,14 @@ extends: SYSTEM_ACCESSTOKEN: $(System.AccessToken) displayName: Send Base64UrlFuzzer to OneFuzz + - task: onefuzz-task@0 + inputs: + onefuzzOSes: 'Windows' + env: + onefuzzDropDirectory: $(fuzzerProject)/deployment/ConvertToBase64Fuzzer + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + displayName: Send ConvertToBase64Fuzzer to OneFuzz + - task: onefuzz-task@0 inputs: onefuzzOSes: 'Windows' diff --git a/src/libraries/Fuzzing/DotnetFuzzing/Assert.cs b/src/libraries/Fuzzing/DotnetFuzzing/Assert.cs index a5f2a9dd1d195b..90aa3238a7ac9b 100644 --- a/src/libraries/Fuzzing/DotnetFuzzing/Assert.cs +++ b/src/libraries/Fuzzing/DotnetFuzzing/Assert.cs @@ -29,6 +29,23 @@ static void ThrowNull() => throw new Exception("Value is null"); } + public static void SequenceEqual(Span expected, Span actual) + { + if (!expected.SequenceEqual(actual)) + { + Throw(expected, actual); + } + + static void Throw(Span expected, Span actual) + { + Equal(expected.Length, actual.Length); + + int diffIndex = expected.CommonPrefixLength(actual); + + throw new Exception($"Expected={expected[diffIndex]} Actual={actual[diffIndex]} at index {diffIndex}"); + } + } + public static void SequenceEqual(ReadOnlySpan expected, ReadOnlySpan actual) { if (!expected.SequenceEqual(actual)) diff --git a/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs b/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs new file mode 100644 index 00000000000000..949eed72899bce --- /dev/null +++ b/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs @@ -0,0 +1,98 @@ +// 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.Text; +using System.Buffers; +using System.Diagnostics; +using System.Text; + +namespace DotnetFuzzing.Fuzzers +{ + internal sealed class ConvertToBase64Fuzzer : IFuzzer + { + private const int Base64LineBreakPosition = 76; + private static readonly SearchValues s_validBase64Chars = SearchValues.Create("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="); + + public string[] TargetAssemblies => []; + + public string[] TargetCoreLibPrefixes { get; } = ["System.Convert"]; + + public string Dictionary => "base64.dict"; + + public void FuzzTarget(ReadOnlySpan bytes) + { + Test(bytes, PoisonPagePlacement.Before); + Test(bytes, PoisonPagePlacement.After); + } + + private void Test(ReadOnlySpan bytes, PoisonPagePlacement poison) + { + string noLineBreakString = TestToStringToCharArray(bytes, Base64FormattingOptions.None, poison); + string lineBreakString = TestToStringToCharArray(bytes, Base64FormattingOptions.InsertLineBreaks, poison); + + Assert.Equal(-1, noLineBreakString.AsSpan().IndexOfAnyExcept(s_validBase64Chars)); + + while (true) + { + int index = lineBreakString.AsSpan().IndexOfAnyExcept(s_validBase64Chars); + if (index < 0) + { + break; + } + + if (!IsWhiteSpace(lineBreakString[index])) + { + throw new Exception($"Non Base64 char: {lineBreakString}, {index}"); + } + + lineBreakString = lineBreakString.Remove(index, 2); // \r\n + } + + Assert.Equal(noLineBreakString, lineBreakString); + } + + private static string TestToStringToCharArray(ReadOnlySpan bytes, Base64FormattingOptions options, PoisonPagePlacement poison) + { + using PooledBoundedMemory inputPoisoned = PooledBoundedMemory.Rent(bytes, poison); + int encodedLength = ToBase64_CalculateOutputLength(bytes.Length, options == Base64FormattingOptions.InsertLineBreaks); + using PooledBoundedMemory destPoisoned = PooledBoundedMemory.Rent(encodedLength, poison); + Span input = inputPoisoned.Span; + char[] dest = destPoisoned.Span.ToArray(); + + 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.Equal(toStringResult, new string(dest, 0, written)); + + return toStringResult; + } + + 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; + } + + private static bool IsWhiteSpace(char value) => value == '\r' || value == '\n'; + } +} From bf27f3f48a16888b11d40b978ebd8be9c42f642f Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Wed, 25 Sep 2024 10:13:42 -0700 Subject: [PATCH 2/6] Remove asserting invalid chars, decoding logic should detect that --- .../Fuzzers/ConvertToBase64Fuzzer.cs | 44 +++---------------- 1 file changed, 5 insertions(+), 39 deletions(-) diff --git a/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs b/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs index 949eed72899bce..5b3280bfbc5da3 100644 --- a/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs +++ b/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs @@ -1,57 +1,27 @@ // 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.Text; using System.Buffers; -using System.Diagnostics; -using System.Text; namespace DotnetFuzzing.Fuzzers { internal sealed class ConvertToBase64Fuzzer : IFuzzer { private const int Base64LineBreakPosition = 76; - private static readonly SearchValues s_validBase64Chars = SearchValues.Create("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="); public string[] TargetAssemblies => []; public string[] TargetCoreLibPrefixes { get; } = ["System.Convert"]; - public string Dictionary => "base64.dict"; - public void FuzzTarget(ReadOnlySpan bytes) { - Test(bytes, PoisonPagePlacement.Before); - Test(bytes, PoisonPagePlacement.After); + TestToStringToCharArray(bytes, Base64FormattingOptions.None, PoisonPagePlacement.Before); + TestToStringToCharArray(bytes, Base64FormattingOptions.InsertLineBreaks, PoisonPagePlacement.Before); + TestToStringToCharArray(bytes, Base64FormattingOptions.None, PoisonPagePlacement.After); + TestToStringToCharArray(bytes, Base64FormattingOptions.InsertLineBreaks, PoisonPagePlacement.After); } - private void Test(ReadOnlySpan bytes, PoisonPagePlacement poison) - { - string noLineBreakString = TestToStringToCharArray(bytes, Base64FormattingOptions.None, poison); - string lineBreakString = TestToStringToCharArray(bytes, Base64FormattingOptions.InsertLineBreaks, poison); - - Assert.Equal(-1, noLineBreakString.AsSpan().IndexOfAnyExcept(s_validBase64Chars)); - - while (true) - { - int index = lineBreakString.AsSpan().IndexOfAnyExcept(s_validBase64Chars); - if (index < 0) - { - break; - } - - if (!IsWhiteSpace(lineBreakString[index])) - { - throw new Exception($"Non Base64 char: {lineBreakString}, {index}"); - } - - lineBreakString = lineBreakString.Remove(index, 2); // \r\n - } - - Assert.Equal(noLineBreakString, lineBreakString); - } - - private static string TestToStringToCharArray(ReadOnlySpan bytes, Base64FormattingOptions options, PoisonPagePlacement poison) + private static void TestToStringToCharArray(ReadOnlySpan bytes, Base64FormattingOptions options, PoisonPagePlacement poison) { using PooledBoundedMemory inputPoisoned = PooledBoundedMemory.Rent(bytes, poison); int encodedLength = ToBase64_CalculateOutputLength(bytes.Length, options == Base64FormattingOptions.InsertLineBreaks); @@ -69,8 +39,6 @@ private static string TestToStringToCharArray(ReadOnlySpan bytes, Base64Fo Assert.SequenceEqual(input, decoded); Assert.Equal(toStringResult, new string(dest, 0, written)); - - return toStringResult; } private static int ToBase64_CalculateOutputLength(int inputLength, bool insertLineBreaks) @@ -92,7 +60,5 @@ private static int ToBase64_CalculateOutputLength(int inputLength, bool insertLi return (int)outlen; } - - private static bool IsWhiteSpace(char value) => value == '\r' || value == '\n'; } } From 9547c722f7e3d46d450ffdc2b3a7a2f3bab46946 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Wed, 25 Sep 2024 12:48:46 -0700 Subject: [PATCH 3/6] Apply suggestions from code review Co-authored-by: Miha Zupan --- src/libraries/Fuzzing/DotnetFuzzing/Assert.cs | 18 ++---------------- .../Fuzzers/ConvertToBase64Fuzzer.cs | 5 ++--- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/src/libraries/Fuzzing/DotnetFuzzing/Assert.cs b/src/libraries/Fuzzing/DotnetFuzzing/Assert.cs index 90aa3238a7ac9b..be3e7a66defc16 100644 --- a/src/libraries/Fuzzing/DotnetFuzzing/Assert.cs +++ b/src/libraries/Fuzzing/DotnetFuzzing/Assert.cs @@ -29,22 +29,8 @@ static void ThrowNull() => throw new Exception("Value is null"); } - public static void SequenceEqual(Span expected, Span actual) - { - if (!expected.SequenceEqual(actual)) - { - Throw(expected, actual); - } - - static void Throw(Span expected, Span actual) - { - Equal(expected.Length, actual.Length); - - int diffIndex = expected.CommonPrefixLength(actual); - - throw new Exception($"Expected={expected[diffIndex]} Actual={actual[diffIndex]} at index {diffIndex}"); - } - } + public static void SequenceEqual(Span expected, Span actual) => + SequenceEqual((ReadOnlySpan)expected, (ReadOnlySpan)actual); public static void SequenceEqual(ReadOnlySpan expected, ReadOnlySpan actual) { diff --git a/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs b/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs index 5b3280bfbc5da3..597e713c0c135f 100644 --- a/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs +++ b/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs @@ -25,9 +25,8 @@ private static void TestToStringToCharArray(ReadOnlySpan bytes, Base64Form { using PooledBoundedMemory inputPoisoned = PooledBoundedMemory.Rent(bytes, poison); int encodedLength = ToBase64_CalculateOutputLength(bytes.Length, options == Base64FormattingOptions.InsertLineBreaks); - using PooledBoundedMemory destPoisoned = PooledBoundedMemory.Rent(encodedLength, poison); Span input = inputPoisoned.Span; - char[] dest = destPoisoned.Span.ToArray(); + char[] dest = new char[encodedLength]; string toStringResult = Convert.ToBase64String(input, options); byte[] decoded = Convert.FromBase64String(toStringResult); @@ -38,7 +37,7 @@ private static void TestToStringToCharArray(ReadOnlySpan bytes, Base64Form decoded = Convert.FromBase64CharArray(dest, 0, written); Assert.SequenceEqual(input, decoded); - Assert.Equal(toStringResult, new string(dest, 0, written)); + Assert.SequenceEqual(toStringResult, dest.AsSpan(0, written)); } private static int ToBase64_CalculateOutputLength(int inputLength, bool insertLineBreaks) From fc5a204a482c1e1487255ce190288c04d9310d49 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Fri, 4 Oct 2024 21:54:12 -0700 Subject: [PATCH 4/6] Add ConvertToBase64Fuzzer to the project --- src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj | 1 + .../Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj b/src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj index a0aca709b3e679..ddee73de5ad544 100644 --- a/src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj +++ b/src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj @@ -23,6 +23,7 @@ + diff --git a/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs b/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs index 597e713c0c135f..972e3b6aacd506 100644 --- a/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs +++ b/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs @@ -37,7 +37,7 @@ private static void TestToStringToCharArray(ReadOnlySpan bytes, Base64Form decoded = Convert.FromBase64CharArray(dest, 0, written); Assert.SequenceEqual(input, decoded); - Assert.SequenceEqual(toStringResult, dest.AsSpan(0, written)); + Assert.SequenceEqual(toStringResult.AsSpan(), dest.AsSpan(0, written)); } private static int ToBase64_CalculateOutputLength(int inputLength, bool insertLineBreaks) From c778ae5e09718afad13a65e6095593aaba387ad3 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Mon, 7 Oct 2024 12:23:52 -0700 Subject: [PATCH 5/6] Move ConvertToBase64Fuzzer logic to Base64Fuzzer, remove analyzer supressions, update readme --- .../libraries/fuzzing/deploy-to-onefuzz.yml | 8 -- .../DotnetFuzzing/DotnetFuzzing.csproj | 3 - .../DotnetFuzzing/Fuzzers/Base64Fuzzer.cs | 89 +++++++++++++++---- .../Fuzzers/ConvertToBase64Fuzzer.cs | 63 ------------- src/libraries/Fuzzing/README.md | 18 ++-- 5 files changed, 80 insertions(+), 101 deletions(-) delete mode 100644 src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs diff --git a/eng/pipelines/libraries/fuzzing/deploy-to-onefuzz.yml b/eng/pipelines/libraries/fuzzing/deploy-to-onefuzz.yml index 6679191b9ea4dc..9f2b06ec638bfd 100644 --- a/eng/pipelines/libraries/fuzzing/deploy-to-onefuzz.yml +++ b/eng/pipelines/libraries/fuzzing/deploy-to-onefuzz.yml @@ -90,14 +90,6 @@ extends: SYSTEM_ACCESSTOKEN: $(System.AccessToken) displayName: Send Base64UrlFuzzer to OneFuzz - - task: onefuzz-task@0 - inputs: - onefuzzOSes: 'Windows' - env: - onefuzzDropDirectory: $(fuzzerProject)/deployment/ConvertToBase64Fuzzer - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - displayName: Send ConvertToBase64Fuzzer to OneFuzz - - task: onefuzz-task@0 inputs: onefuzzOSes: 'Windows' diff --git a/src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj b/src/libraries/Fuzzing/DotnetFuzzing/DotnetFuzzing.csproj index ddee73de5ad544..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 @@ -23,7 +21,6 @@ - diff --git a/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/Base64Fuzzer.cs b/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/Base64Fuzzer.cs index 000718de72537e..2aed832e0efc18 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,49 @@ namespace DotnetFuzzing.Fuzzers { internal sealed class Base64Fuzzer : IFuzzer { + private const int Base64LineBreakPosition = 76; // Need to be in sink Convert.Base64LineBreakPosition private field + 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); + Span input = inputPoisonBefore.Span; + TestCases(input, PoisonPagePlacement.Before); + using PooledBoundedMemory inputPoisonAfter = PooledBoundedMemory.Rent(bytes, PoisonPagePlacement.After); + input = inputPoisonBefore.Span; + TestCases(input, 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 +59,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 +90,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 +105,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 +132,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/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs b/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs deleted file mode 100644 index 972e3b6aacd506..00000000000000 --- a/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/ConvertToBase64Fuzzer.cs +++ /dev/null @@ -1,63 +0,0 @@ -// 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; - -namespace DotnetFuzzing.Fuzzers -{ - internal sealed class ConvertToBase64Fuzzer : IFuzzer - { - private const int Base64LineBreakPosition = 76; - - public string[] TargetAssemblies => []; - - public string[] TargetCoreLibPrefixes { get; } = ["System.Convert"]; - - public void FuzzTarget(ReadOnlySpan bytes) - { - TestToStringToCharArray(bytes, Base64FormattingOptions.None, PoisonPagePlacement.Before); - TestToStringToCharArray(bytes, Base64FormattingOptions.InsertLineBreaks, PoisonPagePlacement.Before); - TestToStringToCharArray(bytes, Base64FormattingOptions.None, PoisonPagePlacement.After); - TestToStringToCharArray(bytes, Base64FormattingOptions.InsertLineBreaks, PoisonPagePlacement.After); - } - - private static void TestToStringToCharArray(ReadOnlySpan bytes, Base64FormattingOptions options, PoisonPagePlacement poison) - { - using PooledBoundedMemory inputPoisoned = PooledBoundedMemory.Rent(bytes, poison); - int encodedLength = ToBase64_CalculateOutputLength(bytes.Length, options == Base64FormattingOptions.InsertLineBreaks); - Span input = inputPoisoned.Span; - 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 ``` From 39c0f4a1ec4ea201d4106b9ce80bc0e05964a127 Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Mon, 7 Oct 2024 14:07:02 -0700 Subject: [PATCH 6/6] Apply suggestions from code review Co-authored-by: Miha Zupan --- .../Fuzzing/DotnetFuzzing/Fuzzers/Base64Fuzzer.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/Base64Fuzzer.cs b/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/Base64Fuzzer.cs index 2aed832e0efc18..056874a30a57f3 100644 --- a/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/Base64Fuzzer.cs +++ b/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/Base64Fuzzer.cs @@ -8,7 +8,7 @@ namespace DotnetFuzzing.Fuzzers { internal sealed class Base64Fuzzer : IFuzzer { - private const int Base64LineBreakPosition = 76; // Need to be in sink Convert.Base64LineBreakPosition private field + private const int Base64LineBreakPosition = 76; // Needs to be in sync with Convert.Base64LineBreakPosition public string[] TargetAssemblies => []; @@ -17,11 +17,10 @@ internal sealed class Base64Fuzzer : IFuzzer public void FuzzTarget(ReadOnlySpan bytes) { using PooledBoundedMemory inputPoisonBefore = PooledBoundedMemory.Rent(bytes, PoisonPagePlacement.Before); - Span input = inputPoisonBefore.Span; - TestCases(input, PoisonPagePlacement.Before); using PooledBoundedMemory inputPoisonAfter = PooledBoundedMemory.Rent(bytes, PoisonPagePlacement.After); - input = inputPoisonBefore.Span; - TestCases(input, PoisonPagePlacement.After); + + TestCases(inputPoisonBefore.Span, PoisonPagePlacement.Before); + TestCases(inputPoisonAfter.Span, PoisonPagePlacement.After); } private void TestCases(Span input, PoisonPagePlacement poison) @@ -40,7 +39,6 @@ private void TestBase64(Span input, PoisonPagePlacement 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(input.Length, bytesConsumed); Assert.Equal(true, maxEncodedLength >= bytesEncoded && maxEncodedLength - 2 <= bytesEncoded);