< Summary

Information
Class: DotnetFuzzing.Fuzzers.NrbfDecoderFuzzer
Assembly: DotnetFuzzing
File(s): D:\runner\runtime\src\libraries\Fuzzing\DotnetFuzzing\Fuzzers\NrbfDecoderFuzzer.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 199
Coverable lines: 199
Total lines: 288
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 130
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
FuzzTarget(...)100%110%
Test(...)100%110%
Test(...)0%440%
Consume(...)0%28280%
ConsumePrimitiveValue(...)0%84840%
TestNumber(T)0%12120%
.ctor(...)100%110%

File(s)

D:\runner\runtime\src\libraries\Fuzzing\DotnetFuzzing\Fuzzers\NrbfDecoderFuzzer.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Buffers;
 5using System.Formats.Nrbf;
 6using System.Numerics;
 7using System.Runtime.CompilerServices;
 8using System.Runtime.Serialization;
 9using System.Text;
 10
 11namespace DotnetFuzzing.Fuzzers
 12{
 13    internal sealed class NrbfDecoderFuzzer : IFuzzer
 14    {
 015        public string[] TargetAssemblies { get; } = ["System.Formats.Nrbf"];
 16
 017        public string[] TargetCoreLibPrefixes => [];
 18
 019        public string Corpus => "nrbf";
 20
 21        public void FuzzTarget(ReadOnlySpan<byte> bytes)
 022        {
 023            Test(bytes, PoisonPagePlacement.Before);
 024            Test(bytes, PoisonPagePlacement.After);
 025        }
 26
 27        private static void Test(ReadOnlySpan<byte> bytes, PoisonPagePlacement poisonPagePlacement)
 028        {
 029            using PooledBoundedMemory<byte> inputPoisoned = PooledBoundedMemory<byte>.Rent(bytes, poisonPagePlacement);
 30
 031            using MemoryStream seekableStream = new(inputPoisoned.Memory.ToArray());
 032            Test(inputPoisoned.Span, seekableStream);
 33
 34            // NrbfDecoder has few code paths dedicated to non-seekable streams, let's test them as well.
 035            using NonSeekableStream nonSeekableStream = new(inputPoisoned.Memory.ToArray());
 036            Test(inputPoisoned.Span, nonSeekableStream);
 037        }
 38
 39        private static void Test(Span<byte> testSpan, Stream stream)
 040        {
 041            if (NrbfDecoder.StartsWithPayloadHeader(testSpan))
 042            {
 043                HashSet<SerializationRecordId> visited = new();
 044                Queue<SerializationRecord> queue = new();
 45                try
 046                {
 047                    SerializationRecord record = NrbfDecoder.Decode(stream, out IReadOnlyDictionary<SerializationRecordI
 48
 049                    Assert.Equal(true, recordMap.ContainsKey(record.Id)); // make sure the loop below includes it
 050                    foreach (SerializationRecord fromMap in recordMap.Values)
 051                    {
 052                        visited.Add(fromMap.Id);
 053                        queue.Enqueue(fromMap);
 054                    }
 055                }
 056                catch (SerializationException) { /* Reading from the stream encountered invalid NRBF data.*/ }
 057                catch (NotSupportedException) { /* Reading from the stream encountered unsupported records */ }
 058                catch (DecoderFallbackException) { /* Reading from the stream encountered an invalid UTF8 sequence. */ }
 059                catch (EndOfStreamException) { /* The end of the stream was reached before reading SerializationRecordTy
 060                catch (IOException) { /* An I/O error occurred. */ }
 61
 62                // Lets consume it outside of the try/catch block to not swallow any exceptions by accident.
 063                Consume(visited, queue);
 064            }
 65            else
 066            {
 67                try
 068                {
 069                    NrbfDecoder.Decode(stream);
 070                    throw new Exception("Decoding supposed to fail!");
 71                }
 072                catch (SerializationException) { /* Everything has to start with a header */ }
 073                catch (NotSupportedException) { /* Reading from the stream encountered unsupported records */ }
 074                catch (EndOfStreamException) { /* The end of the stream was reached before reading SerializationRecordTy
 075            }
 076        }
 77
 78        private static void Consume(HashSet<SerializationRecordId> visited, Queue<SerializationRecord> queue)
 079        {
 080            while (queue.Count > 0)
 081            {
 082                SerializationRecord serializationRecord = queue.Dequeue();
 83
 084                if (serializationRecord is PrimitiveTypeRecord primitiveTypeRecord)
 085                {
 086                    ConsumePrimitiveValue(primitiveTypeRecord.Value);
 087                }
 088                else if (serializationRecord is ClassRecord classRecord)
 089                {
 090                    foreach (string memberName in classRecord.MemberNames)
 091                    {
 092                        ConsumePrimitiveValue(memberName);
 93
 094                        Assert.Equal(true, classRecord.HasMember(memberName));
 95
 96                        object? rawValue;
 97
 98                        try
 099                        {
 0100                            rawValue = classRecord.GetRawValue(memberName);
 0101                        }
 0102                        catch (SerializationException ex) when (ex.Message == "Invalid member reference.")
 0103                        {
 104                            // It was a reference to a non-existing record, just continue.
 0105                            continue;
 106                        }
 107
 0108                        if (rawValue is not null)
 0109                        {
 0110                            if (rawValue is SerializationRecord nestedRecord)
 0111                            {
 0112                                TryEnqueue(nestedRecord);
 0113                            }
 114                            else
 0115                            {
 0116                                ConsumePrimitiveValue(rawValue);
 0117                            }
 0118                        }
 0119                    }
 0120                }
 0121                else if (serializationRecord is ArrayRecord arrayRecord)
 0122                {
 123                    Type? type;
 124
 125                    try
 0126                    {
 127                        // THIS IS VERY BAD IDEA FOR ANY KIND OF PRODUCT CODE!!
 128                        // IT'S USED ONLY FOR THE PURPOSE OF TESTING, DO NOT COPY IT.
 0129                        type = Type.GetType(arrayRecord.TypeName.AssemblyQualifiedName, throwOnError: false);
 0130                        if (type is null)
 0131                        {
 0132                            continue;
 133                        }
 0134                    }
 0135                    catch (Exception) // throwOnError passed to GetType does not prevent from all kinds of exceptions
 0136                    {
 137                        // It was some type made up by the Fuzzer.
 138                        // Since it's currently impossible to get the array without providing the type,
 139                        // we just bail here (in the future we may add an enumerator to ArrayRecord).
 0140                        continue;
 141                    }
 142
 143                    Array? array;
 144                    try
 0145                    {
 0146                        array = arrayRecord.GetArray(type);
 0147                    }
 0148                    catch (SerializationException ex) when (ex.Message == "Invalid member reference.")
 0149                    {
 150                        // It contained a reference to a non-existing record, just continue.
 0151                        continue;
 152                    }
 153
 0154                    ReadOnlySpan<int> lengths = arrayRecord.Lengths;
 0155                    long totalElementsCount = 1;
 0156                    for (int i = 0; i < arrayRecord.Rank; i++)
 0157                    {
 0158                        Assert.Equal(lengths[i], array.GetLength(i));
 0159                        totalElementsCount *= lengths[i];
 0160                    }
 161
 162                    // This array contains indices that are used to get values of multi-dimensional array.
 163                    // At the beginning, all values are set to 0, so we start from the first element.
 0164                    int[] indices = new int[arrayRecord.Rank];
 165
 0166                    long flatIndex = 0;
 0167                    int maxIndex = 1_000_000; // to prevent long loops timing out the fuzzer
 0168                    for (; flatIndex < Math.Min(totalElementsCount, maxIndex); flatIndex++)
 0169                    {
 0170                        object? rawValue = array.GetValue(indices);
 0171                        if (rawValue is not null)
 0172                        {
 0173                            if (rawValue is SerializationRecord record)
 0174                            {
 0175                                TryEnqueue(record);
 0176                            }
 177                            else
 0178                            {
 0179                                ConsumePrimitiveValue(rawValue);
 0180                            }
 0181                        }
 182
 183                        // The loop below is responsible for incrementing the multi-dimensional indices.
 184                        // It finds the dimension and then performs an increment.
 0185                        int dimension = indices.Length - 1;
 0186                        while (dimension >= 0)
 0187                        {
 0188                            indices[dimension]++;
 0189                            if (indices[dimension] < lengths[dimension])
 0190                            {
 0191                                break;
 192                            }
 0193                            indices[dimension] = 0;
 0194                            dimension--;
 0195                        }
 0196                    }
 197
 198                    // We track the flat index to ensure that we have enumerated over all elements.
 0199                    Assert.Equal(Math.Min(totalElementsCount, maxIndex), flatIndex);
 0200                }
 201                else
 0202                {
 203                    // The map may currently contain it (it may change in the future)
 0204                    Assert.Equal(SerializationRecordType.BinaryLibrary, serializationRecord.RecordType);
 0205                }
 0206            }
 207
 208            void TryEnqueue(SerializationRecord record)
 0209            {
 0210                if (visited.Add(record.Id)) // avoid unbounded recursion
 0211                {
 0212                    queue.Enqueue(record);
 0213                }
 0214            }
 0215        }
 216
 217        [MethodImpl(MethodImplOptions.NoInlining)]
 218        private static void ConsumePrimitiveValue(object value)
 0219        {
 0220            if (value is string text)
 0221                Assert.Equal(text, text.ToString()); // we want to touch all elements to see if memory is not corrupted
 0222            else if (value is bool boolean)
 0223                Assert.Equal(true, Unsafe.BitCast<bool, byte>(boolean) is 1 or 0); // other values are illegal!!
 0224            else if (value is sbyte @sbyte)
 0225                TestNumber(@sbyte);
 0226            else if (value is byte @byte)
 0227                TestNumber(@byte);
 0228            else if (value is char character)
 0229                TestNumber(character);
 0230            else if (value is short @short)
 0231                TestNumber(@short);
 0232            else if (value is ushort @ushort)
 0233                TestNumber(@ushort);
 0234            else if (value is int integer)
 0235                TestNumber(integer);
 0236            else if (value is uint @uint)
 0237                TestNumber(@uint);
 0238            else if (value is long @long)
 0239                TestNumber(@long);
 0240            else if (value is ulong @ulong)
 0241                TestNumber(@ulong);
 0242            else if (value is float @float)
 0243            {
 0244                if (!float.IsNaN(@float) && !float.IsInfinity(@float))
 0245                {
 0246                    TestNumber(@float);
 0247                }
 0248            }
 0249            else if (value is double @double)
 0250            {
 0251                if (!double.IsNaN(@double) && !double.IsInfinity(@double))
 0252                {
 0253                    TestNumber(@double);
 0254                }
 0255            }
 0256            else if (value is decimal @decimal)
 0257                TestNumber(@decimal);
 0258            else if (value is nint @nint)
 0259                TestNumber(@nint);
 0260            else if (value is nuint @nuint)
 0261                TestNumber(@nuint);
 0262            else if (value is DateTime datetime)
 0263                Assert.Equal(true, datetime >= DateTime.MinValue && datetime <= DateTime.MaxValue);
 0264            else if (value is TimeSpan timeSpan)
 0265                Assert.Equal(true, timeSpan >= TimeSpan.MinValue && timeSpan <= TimeSpan.MaxValue);
 266            else
 0267                throw new InvalidOperationException();
 268
 269            static void TestNumber<T>(T value) where T : IComparable<T>, IMinMaxValue<T>
 0270            {
 0271                if (value.CompareTo(T.MinValue) < 0)
 0272                {
 0273                    throw new Exception($"Expected {value} to be more or equal {T.MinValue}, {value.CompareTo(T.MinValue
 274                }
 0275                if (value.CompareTo(T.MaxValue) > 0)
 0276                {
 0277                    throw new Exception($"Expected {value} to be less or equal {T.MaxValue}, {value.CompareTo(T.MaxValue
 278                }
 0279            }
 0280        }
 281
 282        private sealed class NonSeekableStream : MemoryStream
 283        {
 0284            public NonSeekableStream(byte[] buffer) : base(buffer) { }
 0285            public override bool CanSeek => false;
 286        }
 287    }
 288}