Skip to content

Commit 2d93a7d

Browse files
committed
[NRBF] Don't use Unsafe.As when decoding DateTime(s) (dotnet#105749)
1 parent 9bff9c5 commit 2d93a7d

File tree

5 files changed

+106
-26
lines changed

5 files changed

+106
-26
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ private static List<T> DecodeFromNonSeekableStream(BinaryReader reader, int coun
237237
}
238238
else if (typeof(T) == typeof(DateTime))
239239
{
240-
values.Add((T)(object)Utils.BinaryReaderExtensions.CreateDateTimeFromData(reader.ReadInt64()));
240+
values.Add((T)(object)Utils.BinaryReaderExtensions.CreateDateTimeFromData(reader.ReadUInt64()));
241241
}
242242
else
243243
{

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ private static SerializationRecord DecodeMemberPrimitiveTypedRecord(BinaryReader
264264
PrimitiveType.Single => new MemberPrimitiveTypedRecord<float>(reader.ReadSingle()),
265265
PrimitiveType.Double => new MemberPrimitiveTypedRecord<double>(reader.ReadDouble()),
266266
PrimitiveType.Decimal => new MemberPrimitiveTypedRecord<decimal>(decimal.Parse(reader.ReadString(), CultureInfo.InvariantCulture)),
267-
PrimitiveType.DateTime => new MemberPrimitiveTypedRecord<DateTime>(Utils.BinaryReaderExtensions.CreateDateTimeFromData(reader.ReadInt64())),
267+
PrimitiveType.DateTime => new MemberPrimitiveTypedRecord<DateTime>(Utils.BinaryReaderExtensions.CreateDateTimeFromData(reader.ReadUInt64())),
268268
// String is handled with a record, never on it's own
269269
_ => new MemberPrimitiveTypedRecord<TimeSpan>(new TimeSpan(reader.ReadInt64())),
270270
};

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

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,29 +75,30 @@ ulong value when TypeNameMatches(typeof(UIntPtr)) => Create(new UIntPtr(value)),
7575
_ => this
7676
};
7777
}
78-
else if (HasMember("_ticks") && MemberValues[0] is long ticks && TypeNameMatches(typeof(TimeSpan)))
78+
else if (HasMember("_ticks") && GetRawValue("_ticks") is long ticks && TypeNameMatches(typeof(TimeSpan)))
7979
{
8080
return Create(new TimeSpan(ticks));
8181
}
8282
}
8383
else if (MemberValues.Count == 2
8484
&& HasMember("ticks") && HasMember("dateData")
85-
&& MemberValues[0] is long value && MemberValues[1] is ulong
85+
&& GetRawValue("ticks") is long && GetRawValue("dateData") is ulong dateData
8686
&& TypeNameMatches(typeof(DateTime)))
8787
{
88-
return Create(Utils.BinaryReaderExtensions.CreateDateTimeFromData(value));
88+
return Create(Utils.BinaryReaderExtensions.CreateDateTimeFromData(dateData));
8989
}
90-
else if(MemberValues.Count == 4
90+
else if (MemberValues.Count == 4
9191
&& HasMember("lo") && HasMember("mid") && HasMember("hi") && HasMember("flags")
92-
&& MemberValues[0] is int && MemberValues[1] is int && MemberValues[2] is int && MemberValues[3] is int
92+
&& GetRawValue("lo") is int lo && GetRawValue("mid") is int mid
93+
&& GetRawValue("hi") is int hi && GetRawValue("flags") is int flags
9394
&& TypeNameMatches(typeof(decimal)))
9495
{
9596
int[] bits =
9697
[
97-
GetInt32("lo"),
98-
GetInt32("mid"),
99-
GetInt32("hi"),
100-
GetInt32("flags")
98+
lo,
99+
mid,
100+
hi,
101+
flags
101102
];
102103

103104
return Create(new decimal(bits));

src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/Utils/BinaryReaderExtensions.cs

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@
33

44
using System.Globalization;
55
using System.IO;
6+
using System.Reflection;
67
using System.Reflection.Metadata;
78
using System.Runtime.CompilerServices;
89
using System.Runtime.Serialization;
10+
using System.Threading;
911

1012
namespace System.Formats.Nrbf.Utils;
1113

1214
internal static class BinaryReaderExtensions
1315
{
16+
private static object? s_baseAmbiguousDstDateTime;
17+
1418
internal static BinaryArrayType ReadArrayType(this BinaryReader reader)
1519
{
1620
byte arrayType = reader.ReadByte();
@@ -70,36 +74,70 @@ internal static object ReadPrimitiveValue(this BinaryReader reader, PrimitiveTyp
7074
PrimitiveType.Single => reader.ReadSingle(),
7175
PrimitiveType.Double => reader.ReadDouble(),
7276
PrimitiveType.Decimal => decimal.Parse(reader.ReadString(), CultureInfo.InvariantCulture),
73-
PrimitiveType.DateTime => CreateDateTimeFromData(reader.ReadInt64()),
77+
PrimitiveType.DateTime => CreateDateTimeFromData(reader.ReadUInt64()),
7478
_ => new TimeSpan(reader.ReadInt64()),
7579
};
7680

77-
// TODO: fix https://github.com/dotnet/runtime/issues/102826
7881
/// <summary>
7982
/// Creates a <see cref="DateTime"/> object from raw data with validation.
8083
/// </summary>
81-
/// <exception cref="SerializationException"><paramref name="data"/> was invalid.</exception>
82-
internal static DateTime CreateDateTimeFromData(long data)
84+
/// <exception cref="SerializationException"><paramref name="dateData"/> was invalid.</exception>
85+
internal static DateTime CreateDateTimeFromData(ulong dateData)
8386
{
84-
// Copied from System.Runtime.Serialization.Formatters.Binary.BinaryParser
85-
86-
// Use DateTime's public constructor to validate the input, but we
87-
// can't return that result as it strips off the kind. To address
88-
// that, store the value directly into a DateTime via an unsafe cast.
89-
// See BinaryFormatterWriter.WriteDateTime for details.
87+
ulong ticks = dateData & 0x3FFFFFFF_FFFFFFFFUL;
88+
DateTimeKind kind = (DateTimeKind)(dateData >> 62);
9089

9190
try
9291
{
93-
const long TicksMask = 0x3FFFFFFFFFFFFFFF;
94-
_ = new DateTime(data & TicksMask);
92+
return ((uint)kind <= (uint)DateTimeKind.Local) ? new DateTime((long)ticks, kind) : CreateFromAmbiguousDst(ticks);
9593
}
9694
catch (ArgumentException ex)
9795
{
98-
// Bad data
9996
throw new SerializationException(ex.Message, ex);
10097
}
10198

102-
return Unsafe.As<long, DateTime>(ref data);
99+
[MethodImpl(MethodImplOptions.NoInlining)]
100+
static DateTime CreateFromAmbiguousDst(ulong ticks)
101+
{
102+
// There's no public API to create a DateTime from an ambiguous DST, and we
103+
// can't use private reflection to access undocumented .NET Framework APIs.
104+
// However, the ISerializable pattern *is* a documented protocol, so we can
105+
// use DateTime's serialization ctor to create a zero-tick "ambiguous" instance,
106+
// then keep reusing it as the base to which we can add our tick offsets.
107+
108+
if (s_baseAmbiguousDstDateTime is not DateTime baseDateTime)
109+
{
110+
#pragma warning disable SYSLIB0050 // Type or member is obsolete
111+
SerializationInfo si = new(typeof(DateTime), new FormatterConverter());
112+
// We don't know the value of "ticks", so we don't specify it.
113+
// If the code somehow runs on a very old runtime that does not know the concept of "dateData"
114+
// (it should not be possible as the library targets .NET Standard 2.0)
115+
// the ctor is going to throw rather than silently return an invalid value.
116+
si.AddValue("dateData", 0xC0000000_00000000UL); // new value (serialized as ulong)
117+
118+
#if NET
119+
baseDateTime = CallPrivateSerializationConstructor(si, new StreamingContext(StreamingContextStates.All));
120+
#else
121+
ConstructorInfo ci = typeof(DateTime).GetConstructor(
122+
BindingFlags.Instance | BindingFlags.NonPublic,
123+
binder: null,
124+
new Type[] { typeof(SerializationInfo), typeof(StreamingContext) },
125+
modifiers: null);
126+
127+
baseDateTime = (DateTime)ci.Invoke(new object[] { si, new StreamingContext(StreamingContextStates.All) });
128+
#endif
129+
130+
#pragma warning restore SYSLIB0050 // Type or member is obsolete
131+
Volatile.Write(ref s_baseAmbiguousDstDateTime, baseDateTime); // it's ok if two threads race here
132+
}
133+
134+
return baseDateTime.AddTicks((long)ticks);
135+
}
136+
137+
#if NET
138+
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
139+
extern static DateTime CallPrivateSerializationConstructor(SerializationInfo si, StreamingContext ct);
140+
#endif
103141
}
104142

105143
internal static bool? IsDataAvailable(this BinaryReader reader, long requiredBytes)

src/libraries/System.Formats.Nrbf/tests/EdgeCaseTests.cs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.IO;
1+
using System.Collections.Generic;
2+
using System.IO;
23
using System.Runtime.Serialization.Formatters;
34
using System.Runtime.Serialization.Formatters.Binary;
45
using Microsoft.DotNet.XUnitExtensions;
@@ -103,4 +104,44 @@ public void FormatterTypeStyleOtherThanTypesAlwaysAreNotSupportedByDesign(Format
103104

104105
Assert.Throws<NotSupportedException>(() => NrbfDecoder.Decode(ms));
105106
}
107+
108+
public static IEnumerable<object[]> CanReadAllKindsOfDateTimes_Arguments
109+
{
110+
get
111+
{
112+
yield return new object[] { new DateTime(1990, 11, 24, 0, 0, 0, DateTimeKind.Local) };
113+
yield return new object[] { new DateTime(1990, 11, 25, 0, 0, 0, DateTimeKind.Utc) };
114+
yield return new object[] { new DateTime(1990, 11, 26, 0, 0, 0, DateTimeKind.Unspecified) };
115+
}
116+
}
117+
118+
[Theory]
119+
[MemberData(nameof(CanReadAllKindsOfDateTimes_Arguments))]
120+
public void CanReadAllKindsOfDateTimes_DateTimeIsTheRootRecord(DateTime input)
121+
{
122+
using MemoryStream stream = Serialize(input);
123+
124+
PrimitiveTypeRecord<DateTime> dateTimeRecord = (PrimitiveTypeRecord<DateTime>)NrbfDecoder.Decode(stream);
125+
126+
Assert.Equal(input.Ticks, dateTimeRecord.Value.Ticks);
127+
Assert.Equal(input.Kind, dateTimeRecord.Value.Kind);
128+
}
129+
130+
[Serializable]
131+
public class ClassWithDateTime
132+
{
133+
public DateTime Value;
134+
}
135+
136+
[Theory]
137+
[MemberData(nameof(CanReadAllKindsOfDateTimes_Arguments))]
138+
public void CanReadAllKindsOfDateTimes_DateTimeIsMemberOfTheRootRecord(DateTime input)
139+
{
140+
using MemoryStream stream = Serialize(new ClassWithDateTime() { Value = input });
141+
142+
ClassRecord classRecord = NrbfDecoder.DecodeClassRecord(stream);
143+
144+
Assert.Equal(input.Ticks, classRecord.GetDateTime(nameof(ClassWithDateTime.Value)).Ticks);
145+
Assert.Equal(input.Kind, classRecord.GetDateTime(nameof(ClassWithDateTime.Value)).Kind);
146+
}
106147
}

0 commit comments

Comments
 (0)