diff --git a/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/ArrayRecord.cs b/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/ArrayRecord.cs index 4fe6fb1e0942b4..cda3ca97dee6b2 100644 --- a/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/ArrayRecord.cs +++ b/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/ArrayRecord.cs @@ -114,6 +114,22 @@ private void HandleNext(object value, NextInfo info, int size) internal abstract (AllowedRecordTypes allowed, PrimitiveType primitiveType) GetAllowedRecordType(); + /// + /// Throws if nulls are not allowed and the records are known to contain at least one Multiple Null Record. + /// + /// + /// Every record represents a single value, except for the Multiple Null Records which represent more than one null. + /// Because of that, a record count different than the total element count means that nulls are present. + /// It's important to perform this check before allocating the array, as the array can be very large. + /// + private protected void ThrowIfNullsAreNotAllowedButPresent(List records, bool allowNulls) + { + if (!allowNulls && ArrayInfo.FlattenedLength != records.Count) + { + ThrowHelper.ThrowArrayContainedNulls(); + } + } + internal static void Populate(List source, Array destination, int[] lengths, AllowedRecordTypes allowedRecordTypes, bool allowNulls) { int[] indices = new int[lengths.Length]; diff --git a/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/ArraySingleObjectRecord.cs b/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/ArraySingleObjectRecord.cs index 3010b2930494ae..564b2e7924a41c 100644 --- a/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/ArraySingleObjectRecord.cs +++ b/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/ArraySingleObjectRecord.cs @@ -32,6 +32,8 @@ public override TypeName TypeName private SerializationRecord?[] ToArray(bool allowNulls) { + ThrowIfNullsAreNotAllowedButPresent(Records, allowNulls); + SerializationRecord?[] values = new SerializationRecord?[Length]; int valueIndex = 0; diff --git a/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/ArraySingleStringRecord.cs b/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/ArraySingleStringRecord.cs index b276fc8955c5f7..aa104ee0cad646 100644 --- a/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/ArraySingleStringRecord.cs +++ b/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/ArraySingleStringRecord.cs @@ -46,6 +46,8 @@ internal override (AllowedRecordTypes allowed, PrimitiveType primitiveType) GetA private string?[] ToArray(bool allowNulls) { + ThrowIfNullsAreNotAllowedButPresent(Records, allowNulls); + string?[] values = new string?[Length]; int valueIndex = 0; diff --git a/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/JaggedArrayRecord.cs b/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/JaggedArrayRecord.cs index 6ac97ef40675d6..9d596e673d2995 100644 --- a/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/JaggedArrayRecord.cs +++ b/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/JaggedArrayRecord.cs @@ -45,6 +45,8 @@ internal JaggedArrayRecord(ArrayInfo arrayInfo, MemberTypeInfo memberTypeInfo, i [RequiresDynamicCode("May call Array.CreateInstance().")] private protected override Array Deserialize(Type arrayType, bool allowNulls) { + ThrowIfNullsAreNotAllowedButPresent(_records, allowNulls); + // This method returns arrays of ArrayRecords. Array array = _lengths.Length switch { diff --git a/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/RectangularArrayRecord.cs b/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/RectangularArrayRecord.cs index 64cd104eea44e1..ea3d8b10ad86e2 100644 --- a/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/RectangularArrayRecord.cs +++ b/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/RectangularArrayRecord.cs @@ -46,6 +46,8 @@ private protected override Array Deserialize(Type arrayType, bool allowNulls) { bool storeStrings = _elementType == typeof(string); + ThrowIfNullsAreNotAllowedButPresent(_records, allowNulls); + // We can not deserialize non-string types. // This method returns arrays of SerializationRecord for arrays of complex types. Array result = diff --git a/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/SZArrayOfRecords.cs b/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/SZArrayOfRecords.cs index b77a4a57a2a348..c71f352af38a67 100644 --- a/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/SZArrayOfRecords.cs +++ b/src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/SZArrayOfRecords.cs @@ -38,6 +38,8 @@ public override TypeName TypeName private SerializationRecord?[] ToArray(bool allowNulls) { + ThrowIfNullsAreNotAllowedButPresent(Records, allowNulls); + SerializationRecord?[] result = new SerializationRecord?[Length]; int resultIndex = 0; diff --git a/src/libraries/System.Formats.Nrbf/tests/ArrayOfSerializationRecordsTests.cs b/src/libraries/System.Formats.Nrbf/tests/ArrayOfSerializationRecordsTests.cs index 18e39a5fd68e1f..4f60c30bac2f11 100644 --- a/src/libraries/System.Formats.Nrbf/tests/ArrayOfSerializationRecordsTests.cs +++ b/src/libraries/System.Formats.Nrbf/tests/ArrayOfSerializationRecordsTests.cs @@ -512,5 +512,67 @@ private static void Verify(Array input, ArrayRecord arrayRecord, Array output, } } } + +#if !NETFRAMEWORK + // GC.GetAllocatedBytesForCurrentThread() is not available on Full Framework. + [Theory] + [InlineData(SerializationRecordType.ArraySingleObject)] + [InlineData(SerializationRecordType.ArraySingleString)] + [InlineData(SerializationRecordType.BinaryArray)] + public void GetArray_AllowNullsFalse_DoesNotAllocateLargeArray_WhenMultipleNullRecordIsPresent(SerializationRecordType recordType) + { + // The test needs to ensure that 2GB+ array does not get allocated. + // 200k is enough to get the job done and avoid getting false positives. + const long AllocationThreshold = 200_000; + + using MemoryStream stream = new(); + BinaryWriter writer = new(stream, Text.Encoding.UTF8); + + WriteSerializedStreamHeader(writer); + + writer.Write((byte)recordType); + writer.Write(1); // object ID + + if (recordType is SerializationRecordType.BinaryArray) + { + writer.Write((byte)BinaryArrayType.Single); + writer.Write(1); // rank + writer.Write(Array.MaxLength); // length + writer.Write((byte)3); // BinaryType.SystemClass + writer.Write("System.Exception"); // element type name + } + else + { + writer.Write(Array.MaxLength); // length + } + + writer.Write((byte)SerializationRecordType.ObjectNullMultiple); + writer.Write(Array.MaxLength); // null count + writer.Write((byte)SerializationRecordType.MessageEnd); + + stream.Position = 0; + + SerializationRecord serializationRecord = NrbfDecoder.Decode(stream); + Assert.Equal(recordType, serializationRecord.RecordType); + + long before = GC.GetAllocatedBytesForCurrentThread(); + + Assert.Throws(() => + { + if (serializationRecord is SZArrayRecord arrayOfStrings) + { + arrayOfStrings.GetArray(allowNulls: false); + } + else + { + ((SZArrayRecord)serializationRecord).GetArray(allowNulls: false); + } + }); + + long after = GC.GetAllocatedBytesForCurrentThread(); + + Assert.InRange(after, before, before + AllocationThreshold); + } +#endif } }