Skip to content

Commit f6615d2

Browse files
carlossanloprzikmMirroringAdam Sitnikelinor-fung
authored
Merging internal commits for release/9.0 (#109744)
* Use explicit full-path for loading MsQuic.dll * Fix comment wording * [release/9.0] Simplify array handling to fix issues with jagged and abstract array types Jagged arrays in the payload can contain cycles. In that scenario, no value is correct for `ArrayRecord.FlattenedLength`, and `ArrayRecord.GetArray` does not have enough context to know how to handle the cycles. To address these issues, jagged array handling is simplified so that calling code can handle the cycles in the most appropriate way for the application. Single-dimension arrays can be represented in the payload using abstract types such as `IComparable[]` where the concrete element type is not known. When the concrete element type is known, `ArrayRecord.GetArray` could return either `SZArrayRecord<ClassRecord>` or `SZArrayRecord<object>`; without a concrete type, we need to return something that represents the element abstractly. 1. `ArrayRecord.FlattenedLength` is removed from the API 2. `ArrayRecord.GetArray` now returns `ArrayRecord[]` for jagged arrays instead of trying to populate them 3. `ArrayRecord.GetArray` now returns `SZArrayRecord<SerializationRecord>` for single-dimension arrays instead of either `SZArrayRecord<ClassRecord>` or `SZArrayRecord<object>` * Suppress IL3000 in MsQuicApi constructor A call to `Assembly.Location` was added in a recent fix. It has `IL30000` suppressed via `#pragma warning disable`, but that only applies to the compilation of the library itself. Consumers will hit it when doing something like publishing their app as NativeAOT. This change adds an `[UnconditionalSuppressMessage]` to the `MsQuicApi` static constructor such that `IL30000` should also be suppressed for apps consuming the runtime. This was caught in an aspnetcore deps flow PR coming from runtime. --------- Co-authored-by: Radek Zikmund <[email protected]> Co-authored-by: Radek Zikmund <[email protected]> Co-authored-by: Mirroring <[email protected]> Co-authored-by: Adam Sitnik <[email protected]> Co-authored-by: Elinor Fung <[email protected]>
2 parents 940e395 + e7a0653 commit f6615d2

35 files changed

+1659
-834
lines changed

src/libraries/System.Formats.Nrbf/ref/System.Formats.Nrbf.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ namespace System.Formats.Nrbf
99
public abstract partial class ArrayRecord : System.Formats.Nrbf.SerializationRecord
1010
{
1111
internal ArrayRecord() { }
12-
public virtual long FlattenedLength { get { throw null; } }
1312
public override System.Formats.Nrbf.SerializationRecordId Id { get { throw null; } }
1413
public abstract System.ReadOnlySpan<int> Lengths { get; }
1514
public int Rank { get { throw null; } }

src/libraries/System.Formats.Nrbf/src/PACKAGE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ There are more than a dozen different serialization [record types](https://learn
5454
- `PrimitiveTypeRecord<T>` derives from the non-generic [PrimitiveTypeRecord](https://learn.microsoft.com/dotnet/api/system.formats.nrbf.primitivetyperecord), which also exposes a [Value](https://learn.microsoft.com/dotnet/api/system.formats.nrbf.primitivetyperecord.value) property. But on the base class, the value is returned as `object` (which introduces boxing for value types).
5555
- [ClassRecord](https://learn.microsoft.com/dotnet/api/system.formats.nrbf.classrecord): describes all `class` and `struct` besides the aforementioned primitive types.
5656
- [ArrayRecord](https://learn.microsoft.com/dotnet/api/system.formats.nrbf.arrayrecord): describes all array records, including jagged and multi-dimensional arrays.
57-
- [`SZArrayRecord<T>`](https://learn.microsoft.com/dotnet/api/system.formats.nrbf.szarrayrecord-1): describes single-dimensional, zero-indexed array records, where `T` can be either a primitive type or a `ClassRecord`.
57+
- [`SZArrayRecord<T>`](https://learn.microsoft.com/dotnet/api/system.formats.nrbf.szarrayrecord-1): describes single-dimensional, zero-indexed array records, where `T` can be either a primitive type or a `SerializationRecord`.
5858

5959
```csharp
6060
SerializationRecord rootObject = NrbfDecoder.Decode(payload); // payload is a Stream

src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/AllowedRecordType.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ internal enum AllowedRecordTypes : uint
2828
ArraySingleString = 1 << SerializationRecordType.ArraySingleString,
2929

3030
Nulls = ObjectNull | ObjectNullMultiple256 | ObjectNullMultiple,
31+
Arrays = ArraySingleObject | ArraySinglePrimitive | ArraySingleString | BinaryArray,
3132

3233
/// <summary>
3334
/// Any .NET object (a primitive, a reference type, a reference or single null).
3435
/// </summary>
3536
AnyObject = MemberPrimitiveTyped
36-
| ArraySingleObject | ArraySinglePrimitive | ArraySingleString | BinaryArray
37+
| Arrays
3738
| ClassWithId | ClassWithMembersAndTypes | SystemClassWithMembersAndTypes
3839
| BinaryObjectString
3940
| MemberReference

src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/ArrayRecord.cs

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
using System.Diagnostics.CodeAnalysis;
55
using System.Reflection.Metadata;
66
using System.Formats.Nrbf.Utils;
7+
using System.Collections.Generic;
8+
using System.Diagnostics;
9+
using System.Runtime.Serialization;
710

811
namespace System.Formats.Nrbf;
912

@@ -27,12 +30,6 @@ private protected ArrayRecord(ArrayInfo arrayInfo)
2730
/// <value>A buffer of integers that represent the number of elements in every dimension.</value>
2831
public abstract ReadOnlySpan<int> Lengths { get; }
2932

30-
/// <summary>
31-
/// When overridden in a derived class, gets the total number of all elements in every dimension.
32-
/// </summary>
33-
/// <value>A number that represent the total number of all elements in every dimension.</value>
34-
public virtual long FlattenedLength => ArrayInfo.FlattenedLength;
35-
3633
/// <summary>
3734
/// Gets the rank of the array.
3835
/// </summary>
@@ -118,4 +115,86 @@ private void HandleNext(object value, NextInfo info, int size)
118115
}
119116

120117
internal abstract (AllowedRecordTypes allowed, PrimitiveType primitiveType) GetAllowedRecordType();
118+
119+
internal static void Populate(List<SerializationRecord> source, Array destination, int[] lengths, AllowedRecordTypes allowedRecordTypes, bool allowNulls)
120+
{
121+
int[] indices = new int[lengths.Length];
122+
nuint numElementsWritten = 0; // only for debugging; not used in release builds
123+
124+
foreach (SerializationRecord record in source)
125+
{
126+
object? value = GetActualValue(record, allowedRecordTypes, out int incrementCount);
127+
if (value is not null)
128+
{
129+
// null is a default element for all array of reference types, so we don't call SetValue for nulls.
130+
destination.SetValue(value, indices);
131+
Debug.Assert(incrementCount == 1, "IncrementCount other than 1 is allowed only for null records.");
132+
}
133+
else if (!allowNulls)
134+
{
135+
ThrowHelper.ThrowArrayContainedNulls();
136+
}
137+
138+
while (incrementCount > 0)
139+
{
140+
incrementCount--;
141+
numElementsWritten++;
142+
int dimension = indices.Length - 1;
143+
while (dimension >= 0)
144+
{
145+
indices[dimension]++;
146+
if (indices[dimension] < lengths[dimension])
147+
{
148+
break;
149+
}
150+
indices[dimension] = 0;
151+
dimension--;
152+
}
153+
154+
if (dimension < 0)
155+
{
156+
break;
157+
}
158+
}
159+
}
160+
161+
Debug.Assert(numElementsWritten == (uint)source.Count, "We should have traversed the entirety of the source records collection.");
162+
Debug.Assert(numElementsWritten == (ulong)destination.LongLength, "We should have traversed the entirety of the destination array.");
163+
}
164+
165+
private static object? GetActualValue(SerializationRecord record, AllowedRecordTypes allowedRecordTypes, out int repeatCount)
166+
{
167+
repeatCount = 1;
168+
169+
if (record is NullsRecord nullsRecord)
170+
{
171+
repeatCount = nullsRecord.NullCount;
172+
return null;
173+
}
174+
else if (record.RecordType == SerializationRecordType.MemberReference)
175+
{
176+
record = ((MemberReferenceRecord)record).GetReferencedRecord();
177+
}
178+
179+
if (allowedRecordTypes == AllowedRecordTypes.BinaryObjectString)
180+
{
181+
if (record is not BinaryObjectStringRecord stringRecord)
182+
{
183+
throw new SerializationException(SR.Serialization_InvalidReference);
184+
}
185+
186+
return stringRecord.Value;
187+
}
188+
else if (allowedRecordTypes == AllowedRecordTypes.Arrays)
189+
{
190+
if (record is not ArrayRecord arrayRecord)
191+
{
192+
throw new SerializationException(SR.Serialization_InvalidReference);
193+
}
194+
195+
return arrayRecord;
196+
}
197+
198+
return record;
199+
}
121200
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Diagnostics.CodeAnalysis;
8+
using System.Formats.Nrbf.Utils;
9+
using System.Linq;
10+
using System.Reflection.Metadata;
11+
using System.Runtime.CompilerServices;
12+
using System.Runtime.InteropServices;
13+
using System.Text;
14+
using System.Threading.Tasks;
15+
16+
namespace System.Formats.Nrbf
17+
{
18+
internal sealed class ArrayRectangularPrimitiveRecord<T> : ArrayRecord where T : unmanaged
19+
{
20+
private readonly int[] _lengths;
21+
private readonly IReadOnlyList<T> _values;
22+
private TypeName? _typeName;
23+
24+
internal ArrayRectangularPrimitiveRecord(ArrayInfo arrayInfo, int[] lengths, IReadOnlyList<T> values) : base(arrayInfo)
25+
{
26+
_lengths = lengths;
27+
_values = values;
28+
ValuesToRead = 0; // there is nothing to read anymore
29+
}
30+
31+
public override ReadOnlySpan<int> Lengths => _lengths;
32+
33+
public override SerializationRecordType RecordType => SerializationRecordType.BinaryArray;
34+
35+
public override TypeName TypeName
36+
=> _typeName ??= TypeNameHelpers.GetPrimitiveTypeName(TypeNameHelpers.GetPrimitiveType<T>()).MakeArrayTypeName(Rank);
37+
38+
internal override (AllowedRecordTypes allowed, PrimitiveType primitiveType) GetAllowedRecordType() => throw new InvalidOperationException();
39+
40+
private protected override void AddValue(object value) => throw new InvalidOperationException();
41+
42+
[RequiresDynamicCode("May call Array.CreateInstance().")]
43+
private protected override Array Deserialize(Type arrayType, bool allowNulls)
44+
{
45+
Array result =
46+
#if NET9_0_OR_GREATER
47+
Array.CreateInstanceFromArrayType(arrayType, _lengths);
48+
#else
49+
Array.CreateInstance(typeof(T), _lengths);
50+
#endif
51+
int[] indices = new int[_lengths.Length];
52+
nuint numElementsWritten = 0; // only for debugging; not used in release builds
53+
54+
for (int i = 0; i < _values.Count; i++)
55+
{
56+
result.SetValue(_values[i], indices);
57+
numElementsWritten++;
58+
59+
int dimension = indices.Length - 1;
60+
while (dimension >= 0)
61+
{
62+
indices[dimension]++;
63+
if (indices[dimension] < Lengths[dimension])
64+
{
65+
break;
66+
}
67+
indices[dimension] = 0;
68+
dimension--;
69+
}
70+
71+
if (dimension < 0)
72+
{
73+
break;
74+
}
75+
}
76+
77+
Debug.Assert(numElementsWritten == (uint)_values.Count, "We should have traversed the entirety of the source values collection.");
78+
Debug.Assert(numElementsWritten == (ulong)result.LongLength, "We should have traversed the entirety of the destination array.");
79+
80+
return result;
81+
}
82+
}
83+
}

src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/ArraySingleObjectRecord.cs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ namespace System.Formats.Nrbf;
1515
/// <remarks>
1616
/// ArraySingleObject records are described in <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/982b2f50-6367-402a-aaf2-44ee96e2a5e0">[MS-NRBF] 2.4.3.2</see>.
1717
/// </remarks>
18-
internal sealed class ArraySingleObjectRecord : SZArrayRecord<object?>
18+
internal sealed class ArraySingleObjectRecord : SZArrayRecord<SerializationRecord>
1919
{
20-
private ArraySingleObjectRecord(ArrayInfo arrayInfo) : base(arrayInfo) => Records = [];
20+
internal ArraySingleObjectRecord(ArrayInfo arrayInfo) : base(arrayInfo) => Records = [];
2121

2222
public override SerializationRecordType RecordType => SerializationRecordType.ArraySingleObject;
2323

@@ -27,25 +27,26 @@ public override TypeName TypeName
2727
private List<SerializationRecord> Records { get; }
2828

2929
/// <inheritdoc/>
30-
public override object?[] GetArray(bool allowNulls = true)
31-
=> (object?[])(allowNulls ? _arrayNullsAllowed ??= ToArray(true) : _arrayNullsNotAllowed ??= ToArray(false));
30+
public override SerializationRecord?[] GetArray(bool allowNulls = true)
31+
=> (SerializationRecord?[])(allowNulls ? _arrayNullsAllowed ??= ToArray(true) : _arrayNullsNotAllowed ??= ToArray(false));
3232

33-
private object?[] ToArray(bool allowNulls)
33+
private SerializationRecord?[] ToArray(bool allowNulls)
3434
{
35-
object?[] values = new object?[Length];
35+
SerializationRecord?[] values = new SerializationRecord?[Length];
3636

3737
int valueIndex = 0;
3838
for (int recordIndex = 0; recordIndex < Records.Count; recordIndex++)
3939
{
4040
SerializationRecord record = Records[recordIndex];
4141

42-
int nullCount = record is NullsRecord nullsRecord ? nullsRecord.NullCount : 0;
43-
if (nullCount == 0)
42+
if (record is MemberReferenceRecord referenceRecord)
4443
{
45-
// "new object[] { <SELF> }" is special cased because it allows for storing reference to itself.
46-
values[valueIndex++] = record is MemberReferenceRecord referenceRecord && referenceRecord.Reference.Equals(Id)
47-
? values // a reference to self, and a way to get StackOverflow exception ;)
48-
: record.GetValue();
44+
record = referenceRecord.GetReferencedRecord();
45+
}
46+
47+
if (record is not NullsRecord nullsRecord)
48+
{
49+
values[valueIndex++] = record;
4950
continue;
5051
}
5152

@@ -54,6 +55,7 @@ public override TypeName TypeName
5455
ThrowHelper.ThrowArrayContainedNulls();
5556
}
5657

58+
int nullCount = nullsRecord.NullCount;
5759
do
5860
{
5961
values[valueIndex++] = null;

src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/ArraySinglePrimitiveRecord.cs

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ public override T[] GetArray(bool allowNulls = true)
4747

4848
internal static IReadOnlyList<T> DecodePrimitiveTypes(BinaryReader reader, int count)
4949
{
50+
if (count == 0)
51+
{
52+
return Array.Empty<T>(); // Empty arrays are allowed.
53+
}
54+
5055
// For decimals, the input is provided as strings, so we can't compute the required size up-front.
5156
if (typeof(T) == typeof(decimal))
5257
{
@@ -71,18 +76,15 @@ internal static IReadOnlyList<T> DecodePrimitiveTypes(BinaryReader reader, int c
7176
// allocations to be proportional to the amount of data present in the input stream,
7277
// which is a sufficient defense against DoS.
7378

74-
long requiredBytes = count;
75-
if (typeof(T) == typeof(DateTime) || typeof(T) == typeof(TimeSpan))
76-
{
77-
// We can't assume DateTime as represented by the runtime is 8 bytes.
78-
// The only assumption we can make is that it's 8 bytes on the wire.
79-
requiredBytes *= 8;
80-
}
81-
else if (typeof(T) != typeof(char))
82-
{
83-
requiredBytes *= Unsafe.SizeOf<T>();
84-
}
79+
// We can't assume DateTime as represented by the runtime is 8 bytes.
80+
// The only assumption we can make is that it's 8 bytes on the wire.
81+
int sizeOfT = typeof(T) == typeof(DateTime) || typeof(T) == typeof(TimeSpan)
82+
? 8
83+
: typeof(T) != typeof(char)
84+
? Unsafe.SizeOf<T>()
85+
: 1;
8586

87+
long requiredBytes = (long)count * sizeOfT;
8688
bool? isDataAvailable = reader.IsDataAvailable(requiredBytes);
8789
if (!isDataAvailable.HasValue)
8890
{
@@ -110,26 +112,49 @@ internal static IReadOnlyList<T> DecodePrimitiveTypes(BinaryReader reader, int c
110112

111113
// It's safe to pre-allocate, as we have ensured there is enough bytes in the stream.
112114
T[] result = new T[count];
113-
Span<byte> resultAsBytes = MemoryMarshal.AsBytes<T>(result);
114-
#if NET
115-
reader.BaseStream.ReadExactly(resultAsBytes);
115+
116+
// MemoryMarshal.AsBytes can fail for inputs that need more than int.MaxValue bytes.
117+
// To avoid OverflowException, we read the data in chunks.
118+
int MaxChunkLength =
119+
#if !DEBUG
120+
int.MaxValue / sizeOfT;
116121
#else
117-
byte[] bytes = ArrayPool<byte>.Shared.Rent((int)Math.Min(requiredBytes, 256_000));
122+
// Let's use a different value for non-release builds to ensure this code path
123+
// is covered with tests without the need of decoding enormous payloads.
124+
8_000;
125+
#endif
118126

119-
while (!resultAsBytes.IsEmpty)
127+
#if !NET
128+
byte[] rented = ArrayPool<byte>.Shared.Rent((int)Math.Min(requiredBytes, 256_000));
129+
#endif
130+
131+
Span<T> valuesToRead = result.AsSpan();
132+
while (!valuesToRead.IsEmpty)
120133
{
121-
int bytesRead = reader.Read(bytes, 0, Math.Min(resultAsBytes.Length, bytes.Length));
122-
if (bytesRead <= 0)
134+
int sliceSize = Math.Min(valuesToRead.Length, MaxChunkLength);
135+
136+
Span<byte> resultAsBytes = MemoryMarshal.AsBytes<T>(valuesToRead.Slice(0, sliceSize));
137+
#if NET
138+
reader.BaseStream.ReadExactly(resultAsBytes);
139+
#else
140+
while (!resultAsBytes.IsEmpty)
123141
{
124-
ArrayPool<byte>.Shared.Return(bytes);
125-
ThrowHelper.ThrowEndOfStreamException();
126-
}
142+
int bytesRead = reader.Read(rented, 0, Math.Min(resultAsBytes.Length, rented.Length));
143+
if (bytesRead <= 0)
144+
{
145+
ArrayPool<byte>.Shared.Return(rented);
146+
ThrowHelper.ThrowEndOfStreamException();
147+
}
127148

128-
bytes.AsSpan(0, bytesRead).CopyTo(resultAsBytes);
129-
resultAsBytes = resultAsBytes.Slice(bytesRead);
149+
rented.AsSpan(0, bytesRead).CopyTo(resultAsBytes);
150+
resultAsBytes = resultAsBytes.Slice(bytesRead);
151+
}
152+
#endif
153+
valuesToRead = valuesToRead.Slice(sliceSize);
130154
}
131155

132-
ArrayPool<byte>.Shared.Return(bytes);
156+
#if !NET
157+
ArrayPool<byte>.Shared.Return(rented);
133158
#endif
134159

135160
if (!BitConverter.IsLittleEndian)
@@ -176,7 +201,7 @@ internal static IReadOnlyList<T> DecodePrimitiveTypes(BinaryReader reader, int c
176201
{
177202
// See DontCastBytesToBooleans test to see what could go wrong.
178203
bool[] booleans = (bool[])(object)result;
179-
resultAsBytes = MemoryMarshal.AsBytes<T>(result);
204+
Span<byte> resultAsBytes = MemoryMarshal.AsBytes<T>(result);
180205
for (int i = 0; i < booleans.Length; i++)
181206
{
182207
// We don't use the bool array to get the value, as an optimizing compiler or JIT could elide this.

0 commit comments

Comments
 (0)