diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index cb95472d7f96d3..51a88bdb625cd0 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -2617,6 +2617,9 @@ Common Language Runtime detected an invalid program. + + The time zone ID '{0}' is invalid. + The time zone ID '{0}' was found on the local computer, but the file at '{1}' was corrupt. @@ -2641,6 +2644,9 @@ This assembly does not have a file table because it was loaded from memory. + + Unsupported unseekable file. + Unable to read beyond the end of the stream. diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.NonAndroid.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.NonAndroid.cs index 998ece9eb2d913..dcf589b79d8b2d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.NonAndroid.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.NonAndroid.cs @@ -24,17 +24,83 @@ private static TimeZoneInfo GetLocalTimeZoneCore() return GetLocalTimeZoneFromTzFile(); } + internal static byte[] ReadAllBytesFromSeekableNonZeroSizeFile(string path, int maxFileSize) + { + using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1, FileOptions.SequentialScan)) + { + if (!fs.CanSeek) + { + throw new IOException(SR.IO_UnseekableFile); + } + + long fileLength = fs.Length; + if (fileLength > maxFileSize) + { + throw new IOException(SR.IO_FileTooLong); + } + + if (fileLength == 0) + { + throw new IOException(SR.IO_InvalidReadLength); + } + + int index = 0; + int count = (int)fileLength; + byte[] bytes = new byte[count]; + while (count > 0) + { + int n = fs.Read(bytes, index, count); + if (n == 0) + { + ThrowHelper.ThrowEndOfFileException(); + } + index += n; + count -= n; + } + return bytes; + } + } + + // Bitmap covering the ASCII range. The bits is set for the characters [a-z], [A-Z], [0-9], '/', '-', and '_'. + private static byte[] asciiBitmap = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0xA8, 0xFF, 0x03, 0xFE, 0xFF, 0xFF, 0x87, 0xFE, 0xFF, 0xFF, 0x07 }; + + private static bool IdContainsAnyDisallowedChars(string zoneId) + { + for (int i = 0; i < zoneId.Length; i++) + { + int c = zoneId[i]; + if (c > 0x7F) + { + return true; + } + + int value = c >> 3; + if ((asciiBitmap[value] & (ulong)(1UL << (c - (value << 3)))) == 0) + { + return true; + } + } + + return false; + } + private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e) { value = null; e = null; + if (Path.IsPathRooted(id) || IdContainsAnyDisallowedChars(id)) + { + e = new TimeZoneNotFoundException(SR.Format(SR.InvalidTimeZone_InvalidId, id)); + return TimeZoneInfoResult.TimeZoneNotFoundException; + } + string timeZoneDirectory = GetTimeZoneDirectory(); string timeZoneFilePath = Path.Combine(timeZoneDirectory, id); byte[] rawData; try { - rawData = File.ReadAllBytes(timeZoneFilePath); + rawData = ReadAllBytesFromSeekableNonZeroSizeFile(timeZoneFilePath, maxFileSize: 20 * 1024 * 1024 /* 20 MB */); // timezone files usually less than 1 MB. } catch (UnauthorizedAccessException ex) { @@ -51,7 +117,7 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, e = ex; return TimeZoneInfoResult.TimeZoneNotFoundException; } - catch (IOException ex) + catch (Exception ex) when (ex is IOException || ex is OutOfMemoryException) { e = new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidFileData, id, timeZoneFilePath), ex); return TimeZoneInfoResult.InvalidTimeZoneException; diff --git a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs index b1cad7231f63d9..ba45b1b47398d7 100644 --- a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs @@ -2091,6 +2091,11 @@ public static IEnumerable ConvertTime_DateTimeOffset_InvalidDestinatio yield return new object[] { s_strPacific + "\\Display" }; yield return new object[] { s_strPacific + "\n" }; // no trailing newline yield return new object[] { new string('a', 100) }; // long string + yield return new object[] { "/dev/random" }; + yield return new object[] { "Invalid Id" }; + yield return new object[] { "Invalid/Invalid" }; + yield return new object[] { $"./{s_strPacific}" }; + yield return new object[] { $"{s_strPacific}/../{s_strPacific}" }; } [Theory] @@ -2098,7 +2103,8 @@ public static IEnumerable ConvertTime_DateTimeOffset_InvalidDestinatio public static void ConvertTime_DateTimeOffset_InvalidDestination_TimeZoneNotFoundException(string destinationId) { DateTimeOffset time1 = new DateTimeOffset(2006, 5, 12, 0, 0, 0, TimeSpan.Zero); - VerifyConvertException(time1, destinationId); + Exception ex = Record.Exception(() => TimeZoneInfo.ConvertTime(time1, TimeZoneInfo.FindSystemTimeZoneById(destinationId))); + Assert.True(ex is InvalidTimeZoneException || ex is TimeZoneNotFoundException); } [Fact]