diff --git a/.github/workflows/inter-branch-merge-flow.yml b/.github/workflows/inter-branch-merge-flow.yml new file mode 100644 index 00000000000000..7cd5b0c7a53ff1 --- /dev/null +++ b/.github/workflows/inter-branch-merge-flow.yml @@ -0,0 +1,13 @@ +name: Inter-branch merge workflow +on: + push: + branches: + - release/** + +permissions: + contents: write + pull-requests: write + +jobs: + Merge: + uses: dotnet/arcade/.github/workflows/inter-branch-merge-base.yml@main diff --git a/eng/Versions.props b/eng/Versions.props index d2fe124f3a6e09..a3e99e3a19f76d 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -136,6 +136,8 @@ 5.0.0 5.0.0 7.0.0 + + 7.0.3 8.0.0-rc.1.23406.6 6.0.0 7.0.0 diff --git a/src/coreclr/debug/createdump/createdump.h b/src/coreclr/debug/createdump/createdump.h index 50cf53dccfc7aa..baaa128c54f027 100644 --- a/src/coreclr/debug/createdump/createdump.h +++ b/src/coreclr/debug/createdump/createdump.h @@ -151,6 +151,9 @@ extern MINIDUMP_TYPE GetMiniDumpType(DumpType dumpType); #ifdef HOST_WINDOWS extern std::string GetLastErrorString(); +extern DWORD GetTempPathWrapper(IN DWORD nBufferLength, OUT LPSTR lpBuffer); +#else +#define GetTempPathWrapper GetTempPathA #endif extern void printf_status(const char* format, ...); extern void printf_error(const char* format, ...); diff --git a/src/coreclr/debug/createdump/createdumpmain.cpp b/src/coreclr/debug/createdump/createdumpmain.cpp index e39538c4cb1c38..f2663169727fde 100644 --- a/src/coreclr/debug/createdump/createdumpmain.cpp +++ b/src/coreclr/debug/createdump/createdumpmain.cpp @@ -205,7 +205,7 @@ int createdump_main(const int argc, const char* argv[]) ArrayHolder tmpPath = new char[MAX_LONGPATH]; if (options.DumpPathTemplate == nullptr) { - if (::GetTempPathA(MAX_LONGPATH, tmpPath) == 0) + if (GetTempPathWrapper(MAX_LONGPATH, tmpPath) == 0) { printf_error("GetTempPath failed\n"); return -1; diff --git a/src/coreclr/debug/createdump/createdumpwindows.cpp b/src/coreclr/debug/createdump/createdumpwindows.cpp index d1b843f1a2fd2b..43e6f2c12bd521 100644 --- a/src/coreclr/debug/createdump/createdumpwindows.cpp +++ b/src/coreclr/debug/createdump/createdumpwindows.cpp @@ -135,3 +135,38 @@ GetLastErrorString() return result; } + +typedef DWORD(WINAPI *pfnGetTempPathA)(DWORD nBufferLength, LPSTR lpBuffer); + +static volatile pfnGetTempPathA +g_pfnGetTempPathA = nullptr; + + +DWORD +GetTempPathWrapper( + IN DWORD nBufferLength, + OUT LPSTR lpBuffer) +{ + if (g_pfnGetTempPathA == nullptr) + { + HMODULE hKernel32 = LoadLibraryExW(L"kernel32.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); + + pfnGetTempPathA pLocalGetTempPathA = NULL; + if (hKernel32 != NULL) + { + // store to thread local variable to prevent data race + pLocalGetTempPathA = (pfnGetTempPathA)::GetProcAddress(hKernel32, "GetTempPath2A"); + } + + if (pLocalGetTempPathA == NULL) // method is only available with Windows 10 Creators Update or later + { + g_pfnGetTempPathA = &GetTempPathA; + } + else + { + g_pfnGetTempPathA = pLocalGetTempPathA; + } + } + + return g_pfnGetTempPathA(nBufferLength, lpBuffer); +} \ No newline at end of file diff --git a/src/coreclr/inc/longfilepathwrappers.h b/src/coreclr/inc/longfilepathwrappers.h index ee394aa0b77586..6407680900dc14 100644 --- a/src/coreclr/inc/longfilepathwrappers.h +++ b/src/coreclr/inc/longfilepathwrappers.h @@ -59,10 +59,6 @@ SearchPathWrapper( _Out_opt_ LPWSTR * lpFilePart ); -DWORD WINAPI GetTempPathWrapper( - SString& lpBuffer - ); - DWORD GetModuleFileNameWrapper( _In_opt_ HMODULE hModule, diff --git a/src/coreclr/inc/winwrap.h b/src/coreclr/inc/winwrap.h index 652c0b80653310..98570ed3eac1d9 100644 --- a/src/coreclr/inc/winwrap.h +++ b/src/coreclr/inc/winwrap.h @@ -204,9 +204,6 @@ //Can not use extended syntax #define WszGetFullPathName GetFullPathNameW -//Long Files will not work on these till redstone -#define WszGetTempPath GetTempPathWrapper - //APIS which have a buffer as an out parameter #define WszGetEnvironmentVariable GetEnvironmentVariableWrapper #define WszSearchPath SearchPathWrapper diff --git a/src/coreclr/utilcode/longfilepathwrappers.cpp b/src/coreclr/utilcode/longfilepathwrappers.cpp index 120b3c04c9322d..079fce749ae638 100644 --- a/src/coreclr/utilcode/longfilepathwrappers.cpp +++ b/src/coreclr/utilcode/longfilepathwrappers.cpp @@ -184,47 +184,6 @@ GetModuleFileNameWrapper( return ret; } -DWORD WINAPI GetTempPathWrapper( - SString& lpBuffer - ) -{ - CONTRACTL - { - NOTHROW; - } - CONTRACTL_END; - - HRESULT hr = S_OK; - DWORD ret = 0; - DWORD lastError = 0; - - EX_TRY - { - //Change the behaviour in Redstone to retry - COUNT_T size = MAX_LONGPATH; - - ret = GetTempPathW( - size, - lpBuffer.OpenUnicodeBuffer(size - 1) - ); - - lastError = GetLastError(); - lpBuffer.CloseBuffer(ret); - } - EX_CATCH_HRESULT(hr); - - if (hr != S_OK) - { - SetLastError(hr); - } - else if (ret == 0) - { - SetLastError(lastError); - } - - return ret; -} - DWORD WINAPI GetEnvironmentVariableWrapper( _In_opt_ LPCTSTR lpName, _Out_opt_ SString& lpBuffer diff --git a/src/installer/managed/Microsoft.NET.HostModel/Microsoft.NET.HostModel.csproj b/src/installer/managed/Microsoft.NET.HostModel/Microsoft.NET.HostModel.csproj index de2e9ac36a7949..3eb55ac664eea2 100644 --- a/src/installer/managed/Microsoft.NET.HostModel/Microsoft.NET.HostModel.csproj +++ b/src/installer/managed/Microsoft.NET.HostModel/Microsoft.NET.HostModel.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/libraries/Common/src/System/Security/Cryptography/DSAKeyFormatHelper.cs b/src/libraries/Common/src/System/Security/Cryptography/DSAKeyFormatHelper.cs index 63c7b451e142b4..57f3344c18954c 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/DSAKeyFormatHelper.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/DSAKeyFormatHelper.cs @@ -25,16 +25,6 @@ internal static void ReadDsaPrivateKey( throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } - DssParms parms = DssParms.Decode(algId.Parameters.Value, AsnEncodingRules.BER); - - ret = new DSAParameters - { - P = parms.P.ToByteArray(isUnsigned: true, isBigEndian: true), - Q = parms.Q.ToByteArray(isUnsigned: true, isBigEndian: true), - }; - - ret.G = parms.G.ExportKeyParameter(ret.P.Length); - BigInteger x; try @@ -57,6 +47,34 @@ internal static void ReadDsaPrivateKey( throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); } + DssParms parms = DssParms.Decode(algId.Parameters.Value, AsnEncodingRules.BER); + + // Sanity checks from FIPS 186-4 4.1/4.2. Since FIPS 186-5 withdrew DSA/DSS + // these will never change again. + // + // This technically allows a non-standard combination of 1024-bit P and 256-bit Q, + // but that will get filtered out by the underlying provider. + // These checks just prevent obviously bad data from wasting work on reinterpretation. + + if (parms.P.Sign < 0 || + parms.Q.Sign < 0 || + !IsValidPLength(parms.P.GetBitLength()) || + !IsValidQLength(parms.Q.GetBitLength()) || + parms.G <= 1 || + parms.G >= parms.P || + x <= 1 || + x >= parms.Q) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + ret = new DSAParameters + { + P = parms.P.ToByteArray(isUnsigned: true, isBigEndian: true), + Q = parms.Q.ToByteArray(isUnsigned: true, isBigEndian: true), + }; + + ret.G = parms.G.ExportKeyParameter(ret.P.Length); ret.X = x.ExportKeyParameter(ret.Q.Length); // The public key is not contained within the format, calculate it. @@ -69,6 +87,11 @@ internal static void ReadDsaPublicKey( in AlgorithmIdentifierAsn algId, out DSAParameters ret) { + if (!algId.Parameters.HasValue) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + BigInteger y; try @@ -88,13 +111,27 @@ internal static void ReadDsaPublicKey( throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); } - if (!algId.Parameters.HasValue) + DssParms parms = DssParms.Decode(algId.Parameters.Value, AsnEncodingRules.BER); + + // Sanity checks from FIPS 186-4 4.1/4.2. Since FIPS 186-5 withdrew DSA/DSS + // these will never change again. + // + // This technically allows a non-standard combination of 1024-bit P and 256-bit Q, + // but that will get filtered out by the underlying provider. + // These checks just prevent obviously bad data from wasting work on reinterpretation. + + if (parms.P.Sign < 0 || + parms.Q.Sign < 0 || + !IsValidPLength(parms.P.GetBitLength()) || + !IsValidQLength(parms.Q.GetBitLength()) || + parms.G <= 1 || + parms.G >= parms.P || + y <= 1 || + y >= parms.P) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } - DssParms parms = DssParms.Decode(algId.Parameters.Value, AsnEncodingRules.BER); - ret = new DSAParameters { P = parms.P.ToByteArray(isUnsigned: true, isBigEndian: true), @@ -105,6 +142,25 @@ internal static void ReadDsaPublicKey( ret.Y = y.ExportKeyParameter(ret.P.Length); } + private static bool IsValidPLength(long pBitLength) + { + return pBitLength switch + { + // FIPS 186-3/186-4 + 1024 or 2048 or 3072 => true, + // FIPS 186-1/186-2 + >= 512 and < 1024 => pBitLength % 64 == 0, + _ => false, + }; + } + + private static bool IsValidQLength(long qBitLength) + { + // FIPS 186-1/186-2 only allows 160 + // FIPS 186-3/186-4 allow 160/224/256 + return qBitLength is 160 or 224 or 256; + } + internal static void ReadSubjectPublicKeyInfo( ReadOnlySpan source, out int bytesRead, diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/DSA/DSAKeyFileTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/DSA/DSAKeyFileTests.cs index 980a8a9194c83e..3d655f77ea96b1 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/DSA/DSAKeyFileTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/DSA/DSAKeyFileTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Formats.Asn1; +using System.Numerics; using System.Security.Cryptography.Encryption.RC2.Tests; using System.Text; using Test.Cryptography; @@ -302,6 +304,150 @@ public static void ReadWriteDsa2048SubjectPublicKeyInfo() DSATestData.GetDSA2048Params()); } + [Fact] + [SkipOnPlatform(TestPlatforms.OSX, "DSASecurityTransforms goes straight to OS, has different failure mode")] + public static void ImportNonsensePublicParameters() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + + DSAParameters validParameters = DSATestData.GetDSA2048Params(); + BigInteger p = new BigInteger(validParameters.P, true, true); + BigInteger q = new BigInteger(validParameters.Q, true, true); + BigInteger g = new BigInteger(validParameters.G, true, true); + BigInteger y = new BigInteger(validParameters.Y, true, true); + + using (DSA dsa = DSAFactory.Create()) + { + // 1 < y < p, 1 < g < p, q is 160/224/256 bits + // p is 512..1024 % 64, or 1024/2048/3072 bits + ImportSPKI(dsa, p, q, g, p, writer); + ImportSPKI(dsa, p, q, g, BigInteger.One, writer); + ImportSPKI(dsa, p, q, g, BigInteger.MinusOne, writer); + ImportSPKI(dsa, p, q, p, y, writer); + ImportSPKI(dsa, p, q, -g, y, writer); + ImportSPKI(dsa, p, q, BigInteger.One, y, writer); + ImportSPKI(dsa, p, q, BigInteger.MinusOne, y, writer); + ImportSPKI(dsa, p, q << 1, g, y, writer); + ImportSPKI(dsa, p, q >> 1, g, y, writer); + ImportSPKI(dsa, p, -q, g, y, writer); + ImportSPKI(dsa, p >> 1, q, g, y, writer); + ImportSPKI(dsa, p << 1, q, g, y, writer); + ImportSPKI(dsa, BigInteger.One << 4095, q, 2, 97, writer); + } + + static void ImportSPKI( + DSA key, + BigInteger p, + BigInteger q, + BigInteger g, + BigInteger y, + AsnWriter writer) + { + writer.Reset(); + writer.WriteInteger(y); + byte[] encodedPublicKey = writer.Encode(); + writer.Reset(); + + using (writer.PushSequence()) + { + using (writer.PushSequence()) + { + writer.WriteObjectIdentifier("1.2.840.10040.4.1"); + + using (writer.PushSequence()) + { + writer.WriteInteger(p); + writer.WriteInteger(q); + writer.WriteInteger(g); + } + } + + writer.WriteBitString(encodedPublicKey); + } + + byte[] spki = writer.Encode(); + writer.Reset(); + + AssertExtensions.ThrowsContains( + () => key.ImportSubjectPublicKeyInfo(spki, out _), + "corrupted"); + } + } + + [Fact] + public static void ImportNonsensePrivateParameters() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + + DSAParameters validParameters = DSATestData.GetDSA2048Params(); + BigInteger p = new BigInteger(validParameters.P, true, true); + BigInteger q = new BigInteger(validParameters.Q, true, true); + BigInteger g = new BigInteger(validParameters.G, true, true); + BigInteger x = new BigInteger(validParameters.X, true, true); + + using (DSA dsa = DSAFactory.Create()) + { + // 1 < x < q, 1 < g < p, q is 160/224/256 bits + // p is 512..1024 % 64, or 1024/2048/3072 bits + ImportPkcs8(dsa, p, q, g, q, writer); + ImportPkcs8(dsa, p, q, g, BigInteger.One, writer); + // x = -1 gets re-interpreted as x = 255 because of a CAPI compat issue. + //ImportPkcs8(dsa, p, q, g, BigInteger.MinusOne, writer); + ImportPkcs8(dsa, p, q, g, -x, writer); + ImportPkcs8(dsa, p, q, p, x, writer); + ImportPkcs8(dsa, p, q, -g, x, writer); + ImportPkcs8(dsa, p, q, BigInteger.One, x, writer); + ImportPkcs8(dsa, p, q, BigInteger.MinusOne, x, writer); + ImportPkcs8(dsa, p, q << 1, g, x, writer); + ImportPkcs8(dsa, p, q >> 1, g, x, writer); + ImportPkcs8(dsa, p >> 1, q, g, x, writer); + ImportPkcs8(dsa, p << 1, q, g, x, writer); + ImportPkcs8(dsa, -q, q, g, x, writer); + ImportPkcs8(dsa, BigInteger.One << 4095, q, 2, 97, writer); + ImportPkcs8(dsa, -p, q, g, x, writer); + } + + static void ImportPkcs8( + DSA key, + BigInteger p, + BigInteger q, + BigInteger g, + BigInteger x, + AsnWriter writer) + { + writer.Reset(); + + using (writer.PushSequence()) + { + writer.WriteInteger(0); + + using (writer.PushSequence()) + { + writer.WriteObjectIdentifier("1.2.840.10040.4.1"); + + using (writer.PushSequence()) + { + writer.WriteInteger(p); + writer.WriteInteger(q); + writer.WriteInteger(g); + } + } + + using (writer.PushOctetString()) + { + writer.WriteInteger(x); + } + } + + byte[] pkcs8 = writer.Encode(); + writer.Reset(); + + AssertExtensions.ThrowsContains( + () => key.ImportPkcs8PrivateKey(pkcs8, out _), + "corrupted"); + } + } + [Fact] public static void NoFuzzySubjectPublicKeyInfo() { diff --git a/src/libraries/Microsoft.Extensions.DependencyModel/src/Microsoft.Extensions.DependencyModel.csproj b/src/libraries/Microsoft.Extensions.DependencyModel/src/Microsoft.Extensions.DependencyModel.csproj index 6cd3d26b2b6f63..9cb379a7ad24d8 100644 --- a/src/libraries/Microsoft.Extensions.DependencyModel/src/Microsoft.Extensions.DependencyModel.csproj +++ b/src/libraries/Microsoft.Extensions.DependencyModel/src/Microsoft.Extensions.DependencyModel.csproj @@ -3,6 +3,8 @@ $(NetCoreAppCurrent);$(NetCoreAppPrevious);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum) true true + true + 1 Provides abstractions for reading `.deps` files. When a .NET application is compiled, the SDK generates a JSON manifest file (`<ApplicationName>.deps.json`) that contains information about application dependencies. You can use `Microsoft.Extensions.DependencyModel` to read information from this manifest at run time. This is useful when you want to dynamically compile code (for example, using Roslyn Emit API) referencing the same dependencies as your main application. By default, the dependency manifest contains information about the application's target framework and runtime dependencies. Set the PreserveCompilationContext project property to `true` to additionally include information about reference assemblies used during compilation. diff --git a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/EmailAddressAttribute.cs b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/EmailAddressAttribute.cs index 2e28b0cb6ac3df..de5a86a64ac8c0 100644 --- a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/EmailAddressAttribute.cs +++ b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/EmailAddressAttribute.cs @@ -7,6 +7,9 @@ namespace System.ComponentModel.DataAnnotations AllowMultiple = false)] public sealed class EmailAddressAttribute : DataTypeAttribute { + private static bool EnableFullDomainLiterals { get; } = + AppContext.TryGetSwitch("System.Net.AllowFullDomainLiterals", out bool enable) ? enable : false; + public EmailAddressAttribute() : base(DataType.EmailAddress) { @@ -27,6 +30,11 @@ public override bool IsValid(object? value) return false; } + if (!EnableFullDomainLiterals && (valueAsString.Contains('\r') || valueAsString.Contains('\n'))) + { + return false; + } + // only return true if there is only 1 '@' character // and it is neither the first nor the last character int index = valueAsString.IndexOf('@'); diff --git a/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/EmailAddressAttributeTests.cs b/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/EmailAddressAttributeTests.cs index a0c67ed7d2b92b..da80016608e402 100644 --- a/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/EmailAddressAttributeTests.cs +++ b/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/EmailAddressAttributeTests.cs @@ -29,6 +29,7 @@ protected override IEnumerable InvalidValues() yield return new TestCase(new EmailAddressAttribute(), 0); yield return new TestCase(new EmailAddressAttribute(), ""); yield return new TestCase(new EmailAddressAttribute(), " \r \t \n" ); + yield return new TestCase(new EmailAddressAttribute(), "someName@[\r\n\tsomeDomain]"); yield return new TestCase(new EmailAddressAttribute(), "@someDomain.com"); yield return new TestCase(new EmailAddressAttribute(), "@someDomain@abc.com"); yield return new TestCase(new EmailAddressAttribute(), "someName"); diff --git a/src/libraries/System.Formats.Asn1/src/Resources/Strings.resx b/src/libraries/System.Formats.Asn1/src/Resources/Strings.resx index 3b028a6f3c90d5..c05cd4bee55a45 100644 --- a/src/libraries/System.Formats.Asn1/src/Resources/Strings.resx +++ b/src/libraries/System.Formats.Asn1/src/Resources/Strings.resx @@ -144,6 +144,9 @@ The encoded named bit list value is larger than the value size of the '{0}' enum. + + The encoded object identifier (OID) exceeds the limits supported by this library. Supported OIDs are limited to 64 arcs and each subidentifier is limited to a 128-bit value. + The encoded value uses a constructed encoding, which is invalid for '{0}' values. diff --git a/src/libraries/System.Formats.Asn1/src/System.Formats.Asn1.csproj b/src/libraries/System.Formats.Asn1/src/System.Formats.Asn1.csproj index a899353cd4a050..f8831d6ee17e0a 100644 --- a/src/libraries/System.Formats.Asn1/src/System.Formats.Asn1.csproj +++ b/src/libraries/System.Formats.Asn1/src/System.Formats.Asn1.csproj @@ -4,6 +4,8 @@ true $(DefineConstants);CP_NO_ZEROMEMORY true + true + 1 Provides classes that can read and write the ASN.1 BER, CER, and DER data formats. Commonly Used Types: @@ -12,6 +14,9 @@ System.Formats.Asn1.AsnWriter + + Common\System\LocalAppContextSwitches.Common.cs + Common\System\Security\Cryptography\CryptoPool.cs @@ -49,6 +54,7 @@ System.Formats.Asn1.AsnWriter + diff --git a/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnDecoder.Oid.cs b/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnDecoder.Oid.cs index fac90e12aec1c2..e46f04b6f308ad 100644 --- a/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnDecoder.Oid.cs +++ b/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnDecoder.Oid.cs @@ -90,15 +90,55 @@ private static void ReadSubIdentifier( throw new AsnContentException(); } + // Set semanticBits to a value such that on the first + // iteration of the loop it becomes the correct value. + // So each entry here is [real semantic bits for this value] - 7. + int semanticBits = source[0] switch + { + >= 0b1100_0000 => 0, + >= 0b1010_0000 => -1, + >= 0b1001_0000 => -2, + >= 0b1000_1000 => -3, + >= 0b1000_0100 => -4, + >= 0b1000_0010 => -5, + >= 0b1000_0001 => -6, + _ => 0, + }; + // First, see how long the segment is int end = -1; int idx; + // None of T-REC-X.660-201107, T-REC-X.680-201508, or T-REC-X.690-201508 + // have any recommendations for a minimum (or maximum) size of a + // sub-identifier. + // + // T-REC-X.667-201210 (and earlier versions) discuss the no-registration- + // required UUID space at 2.25.{UUID}, where UUIDs are defined as 128-bit + // values. This gives us a minimum lower bound of 128-bit. + // + // Windows Crypt32 has historically only supported 64-bit values, and + // the "size limitations" FAQ on oid-info.com says that the largest arc + // value is a 39-digit value that corresponds to a 2.25.UUID value. + // + // So, until something argues for a bigger number, our bit-limit is 128. + const int MaxAllowedBits = 128; + for (idx = 0; idx < source.Length; idx++) { // If the high bit isn't set this marks the end of the sub-identifier. bool endOfIdentifier = (source[idx] & 0x80) == 0; + if (!LocalAppContextSwitches.AllowAnySizeOid) + { + semanticBits += 7; + + if (semanticBits > MaxAllowedBits) + { + throw new AsnContentException(SR.ContentException_OidTooBig); + } + } + if (endOfIdentifier) { end = idx; @@ -265,8 +305,21 @@ private static string ReadObjectIdentifier(ReadOnlySpan contents) contents = contents.Slice(bytesRead); + const int MaxArcs = 64; + int remainingArcs = MaxArcs - 2; + while (!contents.IsEmpty) { + if (!LocalAppContextSwitches.AllowAnySizeOid) + { + if (remainingArcs <= 0) + { + throw new AsnContentException(SR.ContentException_OidTooBig); + } + + remainingArcs--; + } + ReadSubIdentifier(contents, out bytesRead, out smallValue, out largeValue); // Exactly one should be non-null. Debug.Assert((smallValue == null) != (largeValue == null)); diff --git a/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/LocalAppContextSwitches.cs b/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/LocalAppContextSwitches.cs new file mode 100644 index 00000000000000..62722d7f4d0008 --- /dev/null +++ b/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/LocalAppContextSwitches.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +namespace System +{ + internal static partial class LocalAppContextSwitches + { + private static int s_allowAnySizeOid; + public static bool AllowAnySizeOid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => GetCachedSwitchValue("System.Formats.Asn1.AllowAnySizeOid", ref s_allowAnySizeOid); + } + } +} diff --git a/src/libraries/System.Formats.Asn1/tests/Reader/ReadObjectIdentifier.cs b/src/libraries/System.Formats.Asn1/tests/Reader/ReadObjectIdentifier.cs index c1add074220a06..6584fd47b8f6ba 100644 --- a/src/libraries/System.Formats.Asn1/tests/Reader/ReadObjectIdentifier.cs +++ b/src/libraries/System.Formats.Asn1/tests/Reader/ReadObjectIdentifier.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; +using Microsoft.DotNet.RemoteExecutor; using Test.Cryptography; using Xunit; @@ -198,70 +199,188 @@ public static void ExpectedTag_IgnoresConstructed( [InlineData(AsnEncodingRules.BER)] [InlineData(AsnEncodingRules.CER)] [InlineData(AsnEncodingRules.DER)] - public static void ReadVeryLongOid(AsnEncodingRules ruleSet) + public static void ReadMaximumArcOid(AsnEncodingRules ruleSet) { - byte[] inputData = new byte[100000]; - // 06 83 02 00 00 (OBJECT IDENTIFIER, 65536 bytes). - inputData[0] = 0x06; - inputData[1] = 0x83; - inputData[2] = 0x01; - inputData[3] = 0x00; - inputData[4] = 0x00; - // and the rest are all zero. - - // The first byte produces "0.0". Each of the remaining 65535 bytes produce - // another ".0". - const int ExpectedLength = 65536 * 2 + 1; - StringBuilder builder = new StringBuilder(ExpectedLength); - builder.Append('0'); - - for (int i = 0; i <= ushort.MaxValue; i++) + const int MaxArcs = 64; + // MaxArcs content bytes (all 0x7F) (which includes one for failure), plus one for the tag + // plus one for the encoded length. + byte[] input = new byte[MaxArcs + 2]; + input.AsSpan().Fill(0x7F); + input[0] = 0x06; + // The first two arcs are encoded in the first sub-identifier, so MaxArcs - 1. + input[1] = MaxArcs - 1; + + string decoded = AsnDecoder.ReadObjectIdentifier(input, ruleSet, out int consumed); + Assert.Equal(input.Length - 1, consumed); + + StringBuilder expected = new StringBuilder(4 * MaxArcs); + expected.Append("2.47"); + + for (int i = 2; i < MaxArcs; i++) { - builder.Append('.'); - builder.Append(0); + expected.Append(".127"); } - AsnReader reader = new AsnReader(inputData, ruleSet); - string oidString = reader.ReadObjectIdentifier(); + Assert.Equal(expected.ToString(), decoded); - Assert.Equal(ExpectedLength, oidString.Length); - Assert.Equal(builder.ToString(), oidString); + input[1] = MaxArcs; + AsnContentException ex = Assert.Throws( + () => AsnDecoder.ReadObjectIdentifier(input, ruleSet, out _)); + Assert.Contains("OID", ex.Message); } [Theory] [InlineData(AsnEncodingRules.BER)] [InlineData(AsnEncodingRules.CER)] [InlineData(AsnEncodingRules.DER)] - public static void ReadVeryLongOidArc(AsnEncodingRules ruleSet) + public static void ReadMaximumInitialSubIdentifier(AsnEncodingRules ruleSet) { - byte[] inputData = new byte[255]; - // 06 81 93 (OBJECT IDENTIFIER, 147 bytes). - inputData[0] = 0x06; - inputData[1] = 0x81; - inputData[2] = 0x93; - - // With 147 bytes we get 147*7 = 1029 value bits. - // The smallest legal number to encode would have a top byte of 0x81, - // leaving 1022 bits remaining. If they're all zero then we have 2^1022. - // - // Since it's our first sub-identifier it's really encoding "2.(2^1022 - 80)". - inputData[3] = 0x81; - // Leave the last byte as 0. - new Span(inputData, 4, 145).Fill(0x80); - - const string ExpectedOid = - "2." + - "449423283715578976932326297697256183404494244735576643183575" + - "202894331689513752407831771193306018840052800284699678483394" + - "146974422036041556232118576598685310944419733562163713190755" + - "549003115235298632707380212514422095376705856157203684782776" + - "352068092908376276711465745599868114846199290762088390824060" + - "56034224"; + // First sub-identifier is 2^128 - 1, second is 1 + byte[] valid = + { + 0x06, 0x14, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x01, + }; - AsnReader reader = new AsnReader(inputData, ruleSet); + // First sub-identifier is 2^128, second is 1 + byte[] invalid = + { + 0x06, 0x14, 0x84, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x00, 0x01, + }; + + string oid = AsnDecoder.ReadObjectIdentifier(valid, ruleSet, out int consumed); + Assert.Equal(valid.Length, consumed); + Assert.Equal("2.340282366920938463463374607431768211375.1", oid); + + AsnContentException ex = Assert.Throws( + () => AsnDecoder.ReadObjectIdentifier(invalid, ruleSet, out _)); + Assert.Contains("OID", ex.Message); + } - string oidString = reader.ReadObjectIdentifier(); - Assert.Equal(ExpectedOid, oidString); + [Theory] + [InlineData(AsnEncodingRules.BER)] + [InlineData(AsnEncodingRules.CER)] + [InlineData(AsnEncodingRules.DER)] + public static void ReadMaximumNonInitialSubIdentifier(AsnEncodingRules ruleSet) + { + // First sub-identifier is 1, second is 2^128 - 1 + byte[] valid = + { + 0x06, 0x14, 0x01, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, + }; + + // First sub-identifier is 1, second is 2^128 + byte[] invalid = new byte[] + { + 0x06, 0x14, 0x01, 0x84, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, + }; + + string oid = AsnDecoder.ReadObjectIdentifier(valid, ruleSet, out int consumed); + Assert.Equal(valid.Length, consumed); + Assert.Equal("0.1.340282366920938463463374607431768211455", oid); + + AsnContentException ex = Assert.Throws( + () => AsnDecoder.ReadObjectIdentifier(invalid, ruleSet, out _)); + Assert.Contains("OID", ex.Message); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void ReadVeryLongOid_WithAppContext() + { + RemoteExecutor.Invoke( + static () => + { + AppContext.SetSwitch("System.Formats.Asn1.AllowAnySizeOid", true); + + byte[] inputData = new byte[100000]; + // 06 83 02 00 00 (OBJECT IDENTIFIER, 65536 bytes). + inputData[0] = 0x06; + inputData[1] = 0x83; + inputData[2] = 0x01; + inputData[3] = 0x00; + inputData[4] = 0x00; + // and the rest are all zero. + + // The first byte produces "0.0". Each of the remaining 65535 bytes produce + // another ".0". + const int ExpectedLength = 65536 * 2 + 1; + StringBuilder builder = new StringBuilder(ExpectedLength); + builder.Append('0'); + + for (int i = 0; i <= ushort.MaxValue; i++) + { + builder.Append('.'); + builder.Append(0); + } + + AsnEncodingRules[] ruleSets = + { + AsnEncodingRules.BER, + AsnEncodingRules.CER, + AsnEncodingRules.DER, + }; + + foreach (AsnEncodingRules ruleSet in ruleSets) + { + AsnReader reader = new AsnReader(inputData, ruleSet); + string oidString = reader.ReadObjectIdentifier(); + + Assert.Equal(ExpectedLength, oidString.Length); + Assert.Equal(builder.ToString(), oidString); + } + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void ReadVeryLongOidArc_WithAppContext() + { + RemoteExecutor.Invoke( + static () => + { + AppContext.SetSwitch("System.Formats.Asn1.AllowAnySizeOid", true); + + byte[] inputData = new byte[255]; + // 06 81 93 (OBJECT IDENTIFIER, 147 bytes). + inputData[0] = 0x06; + inputData[1] = 0x81; + inputData[2] = 0x93; + + // With 147 bytes we get 147*7 = 1029 value bits. + // The smallest legal number to encode would have a top byte of 0x81, + // leaving 1022 bits remaining. If they're all zero then we have 2^1022. + // + // Since it's our first sub-identifier it's really encoding "2.(2^1022 - 80)". + inputData[3] = 0x81; + // Leave the last byte as 0. + new Span(inputData, 4, 145).Fill(0x80); + + const string ExpectedOid = + "2." + + "449423283715578976932326297697256183404494244735576643183575" + + "202894331689513752407831771193306018840052800284699678483394" + + "146974422036041556232118576598685310944419733562163713190755" + + "549003115235298632707380212514422095376705856157203684782776" + + "352068092908376276711465745599868114846199290762088390824060" + + "56034224"; + + AsnEncodingRules[] ruleSets = + { + AsnEncodingRules.BER, + AsnEncodingRules.CER, + AsnEncodingRules.DER, + }; + + foreach (AsnEncodingRules ruleSet in ruleSets) + { + AsnReader reader = new AsnReader(inputData, ruleSet); + + string oidString = reader.ReadObjectIdentifier(); + Assert.Equal(ExpectedOid, oidString); + } + }).Dispose(); } } } diff --git a/src/libraries/System.Formats.Asn1/tests/System.Formats.Asn1.Tests.csproj b/src/libraries/System.Formats.Asn1/tests/System.Formats.Asn1.Tests.csproj index 68bdc72edf895f..83d8041222f6c9 100644 --- a/src/libraries/System.Formats.Asn1/tests/System.Formats.Asn1.Tests.csproj +++ b/src/libraries/System.Formats.Asn1/tests/System.Formats.Asn1.Tests.csproj @@ -2,6 +2,7 @@ true $(NetCoreAppCurrent);$(NetFrameworkMinimum) + true diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs index 47ac65dbf9b5d4..644f7502ff7763 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs @@ -15,6 +15,9 @@ namespace System.Net.Mail // public partial class MailAddress { + private static bool EnableFullDomainLiterals { get; } = + AppContext.TryGetSwitch("System.Net.AllowFullDomainLiterals", out bool enable) ? enable : false; + // These components form an e-mail address when assembled as follows: // "EncodedDisplayname" private readonly Encoding _displayNameEncoding; @@ -216,6 +219,12 @@ private string GetHost(bool allowUnicode) throw new SmtpException(SR.Format(SR.SmtpInvalidHostName, Address), argEx); } } + + if (!EnableFullDomainLiterals && domain.AsSpan().IndexOfAny('\r', '\n') >= 0) + { + throw new SmtpException(SR.Format(SR.SmtpInvalidHostName, Address)); + } + return domain; } diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs index 306368df202183..a9cf0bca251ce8 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs @@ -9,11 +9,15 @@ // (C) 2006 John Luke // +using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Net.NetworkInformation; using System.Net.Sockets; +using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Microsoft.DotNet.RemoteExecutor; using Systen.Net.Mail.Tests; using System.Net.Test.Common; using Xunit; @@ -573,5 +577,60 @@ public void TestGssapiAuthentication() Assert.Equal("GSSAPI", server.AuthMethodUsed, StringComparer.OrdinalIgnoreCase); } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData("foo@[\r\n bar]")] + [InlineData("foo@[bar\r\n ]")] + [InlineData("foo@[bar\r\n baz]")] + public void MultiLineDomainLiterals_Enabled_Success(string input) + { + RemoteExecutor.Invoke(static (string @input) => + { + AppContext.SetSwitch("System.Net.AllowFullDomainLiterals", true); + + var address = new MailAddress(@input); + + // Using address with new line breaks the protocol so we cannot easily use LoopbackSmtpServer + // Instead we call internal method that does the extra validation. + string? host = (string?)typeof(MailAddress).InvokeMember("GetAddress", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod, null, address, new object[] { true }); + Assert.Equal(input, host); + }, input).Dispose(); + } + + [Theory] + [MemberData(nameof(SendMail_MultiLineDomainLiterals_Data))] + public async Task SendMail_MultiLineDomainLiterals_Disabled_Throws(string from, string to, bool asyncSend) + { + using var server = new LoopbackSmtpServer(); + + using SmtpClient client = server.CreateClient(); + client.Credentials = new NetworkCredential("Foo", "Bar"); + + using var msg = new MailMessage(@from, @to, "subject", "body"); + + await Assert.ThrowsAsync(async () => + { + if (asyncSend) + { + await client.SendMailAsync(msg).WaitAsync(TimeSpan.FromSeconds(30)); + } + else + { + client.Send(msg); + } + }); + } + + public static IEnumerable SendMail_MultiLineDomainLiterals_Data() + { + foreach (bool async in new[] { true, false }) + { + foreach (string address in new[] { "foo@[\r\n bar]", "foo@[bar\r\n ]", "foo@[bar\r\n baz]" }) + { + yield return new object[] { address, "foo@example.com", async }; + yield return new object[] { "foo@example.com", address, async }; + } + } + } } } diff --git a/src/libraries/System.Runtime.Serialization.Formatters/src/System/Runtime/Serialization/Formatters/Binary/BinaryObjectWriter.cs b/src/libraries/System.Runtime.Serialization.Formatters/src/System/Runtime/Serialization/Formatters/Binary/BinaryObjectWriter.cs index 925e6eca7e207e..2e6620a3c13f75 100644 --- a/src/libraries/System.Runtime.Serialization.Formatters/src/System/Runtime/Serialization/Formatters/Binary/BinaryObjectWriter.cs +++ b/src/libraries/System.Runtime.Serialization.Formatters/src/System/Runtime/Serialization/Formatters/Binary/BinaryObjectWriter.cs @@ -367,7 +367,7 @@ private void WriteMembers(NameInfo memberNameInfo, return; } - if (!WriteKnownValueClass(memberNameInfo, memberTypeNameInfo, memberData!)) + if (!WriteKnownValueClass(memberNameInfo, memberTypeNameInfo, memberData!, ref assignUniqueIdToValueType)) { if (outType == null) { @@ -616,10 +616,10 @@ private void WriteArrayMember(WriteObjectInfo objectInfo, NameInfo arrayElemType actualTypeInfo._isArrayItem = true; } - if (!WriteKnownValueClass(arrayElemTypeNameInfo, actualTypeInfo, data!)) + bool assignUniqueIdForValueTypes = false; + if (!WriteKnownValueClass(arrayElemTypeNameInfo, actualTypeInfo, data!, ref assignUniqueIdForValueTypes)) { object obj = data!; - bool assignUniqueIdForValueTypes = false; if (ReferenceEquals(arrayElemTypeNameInfo._type, Converter.s_typeofObject)) { assignUniqueIdForValueTypes = true; @@ -799,11 +799,12 @@ private long Schedule(object obj, bool assignUniqueIdToValueType, Type? type, Wr } // Determines if a type is a primitive type, if it is it is written - private bool WriteKnownValueClass(NameInfo memberNameInfo, NameInfo typeNameInfo, object data) + private bool WriteKnownValueClass(NameInfo memberNameInfo, NameInfo typeNameInfo, object data, ref bool assignUniqueIdToValueType) { if (ReferenceEquals(typeNameInfo._type, Converter.s_typeofString)) { WriteString(memberNameInfo, typeNameInfo, data); + return true; } else { @@ -817,18 +818,22 @@ private bool WriteKnownValueClass(NameInfo memberNameInfo, NameInfo typeNameInfo if (typeNameInfo._isArray) // null if an array { _serWriter.WriteItem(memberNameInfo, typeNameInfo, data); + return true; } - else + else if (memberNameInfo._type == typeNameInfo._type + || memberNameInfo._type == typeof(object) + || (memberNameInfo._type != null && Nullable.GetUnderlyingType(memberNameInfo._type) != null)) { _serWriter.WriteMember(memberNameInfo, typeNameInfo, data); + return true; } } } - return true; + assignUniqueIdToValueType = true; + return false; } - // Writes an object reference to the stream. private void WriteObjectRef(NameInfo nameInfo, long objectId) => _serWriter!.WriteMemberObjectRef(nameInfo, (int)objectId); diff --git a/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTests.cs b/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTests.cs index 95891b7bcfa3a3..cbc6107a5ad3b3 100644 --- a/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTests.cs +++ b/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTests.cs @@ -536,6 +536,25 @@ public void Roundtrip_ArrayContainingArrayAtNonZeroLowerBound() BinaryFormatterHelpers.Clone(Array.CreateInstance(typeof(uint[]), new[] { 5 }, new[] { 1 })); } + [Fact] + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, ".NET Framework has not been patched yet.")] + public void BypassingSerializationBinders() + { + Tuple tuple = new Tuple(42, new byte[] { 1, 2, 3, 4 }); + BinaryFormatter formatter = new BinaryFormatter(); + + using (MemoryStream stream = new MemoryStream()) + { + formatter.Serialize(stream, tuple); + + stream.Position = 0; + + Tuple deserialized = (Tuple)formatter.Deserialize(stream); + Assert.Equal(tuple.Item1, deserialized.Item1); + Assert.Equal(tuple.Item2, deserialized.Item2); + } + } + private static void ValidateEqualityComparer(object obj) { Type objType = obj.GetType(); diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 10d8f70c130538..bb62fa90611143 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -8,8 +8,8 @@ CS8969 true true - false - 3 + true + 4 Provides high-performance and low-allocating types that serialize objects to JavaScript Object Notation (JSON) text and deserialize JSON text to objects, with UTF-8 support built-in. Also provides types to read and write JSON text encoded as UTF-8, and to create an in-memory document object model (DOM), that is read-only, for random access of the JSON elements within a structured view of the data. The System.Text.Json library is built-in as part of the shared framework in .NET Runtime. The package can be installed when you need to use it in other target frameworks. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadBufferState.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadBufferState.cs index c84c00a35247af..8107fa5edbdb38 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadBufferState.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadBufferState.cs @@ -20,6 +20,27 @@ internal struct ReadBufferState : IDisposable private bool _isFirstBlock; private bool _isFinalBlock; + // An "unsuccessful read" in this context refers to a buffer read operation that + // wasn't sufficient to advance the reader to the next token. This occurs primarily + // when consuming large JSON strings (which don't support streaming today) but is + // also possible with other token types such as numbers, booleans, or nulls. + // + // The JsonSerializer.DeserializeAsyncEnumerable methods employ a special buffering + // strategy where rather than attempting to fill the entire buffer, the deserializer + // will be invoked as soon as the first chunk of data is read from the stream. + // This is to ensure liveness: data should be surfaced on the IAE as soon as they + // are streamed from the server. On the other hand, this can create performance + // problems in cases where the underlying stream uses extremely fine-grained buffering. + // For this reason, we employ a threshold that will revert to buffer filling once crossed. + // The counter is reset to zero whenever the JSON reader has been advanced successfully. + // + // The threshold is set to 5 unsuccessful reads. This is a relatively conservative threshold + // but should still make fallback unlikely in most scenaria. It should ensure that fallback + // isn't triggered in null or boolean tokens even in the worst-case scenario where they are + // streamed one byte at a time. + private const int UnsuccessfulReadCountThreshold = 5; + private int _unsuccessfulReadCount; + public ReadBufferState(int initialBufferSize) { _buffer = ArrayPool.Shared.Rent(Math.Max(initialBufferSize, JsonConstants.Utf8Bom.Length)); @@ -46,6 +67,7 @@ public readonly async ValueTask ReadFromStreamAsync( // make all updates on a copy which is returned once complete. ReadBufferState bufferState = this; + int minBufferCount = fillBuffer || _unsuccessfulReadCount > UnsuccessfulReadCountThreshold ? bufferState._buffer.Length : 0; do { int bytesRead = await utf8Json.ReadAsync( @@ -64,7 +86,7 @@ public readonly async ValueTask ReadFromStreamAsync( bufferState._count += bytesRead; } - while (fillBuffer && bufferState._count < bufferState._buffer.Length); + while (bufferState._count < minBufferCount); bufferState.ProcessReadBytes(); return bufferState; @@ -107,6 +129,7 @@ public void AdvanceBuffer(int bytesConsumed) Debug.Assert(bytesConsumed <= _count); Debug.Assert(!_isFinalBlock || _count == bytesConsumed, "The reader should have thrown if we have remaining bytes."); + _unsuccessfulReadCount = bytesConsumed == 0 ? _unsuccessfulReadCount + 1 : 0; _count -= bytesConsumed; if (!_isFinalBlock) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.DeserializeAsyncEnumerable.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.DeserializeAsyncEnumerable.cs index aa578a51e9c8ec..ee6da9261a204a 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.DeserializeAsyncEnumerable.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.DeserializeAsyncEnumerable.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Text.Json.Serialization.Metadata; @@ -228,6 +230,47 @@ await Assert.ThrowsAsync(async () => }); } + [Theory] + [InlineData(5, 1024)] + [InlineData(5, 1024 * 1024)] + public static async Task DeserializeAsyncEnumerable_SlowStreamWithLargeStrings(int totalStrings, int stringLength) + { + var options = new JsonSerializerOptions + { + Converters = { new StringLengthConverter() } + }; + + using var stream = new SlowStream(GenerateJsonCharacters()); + string expectedElement = stringLength.ToString(CultureInfo.InvariantCulture); + IAsyncEnumerable asyncEnumerable = JsonSerializer.DeserializeAsyncEnumerable(stream, options); + + await foreach (string? value in asyncEnumerable) + { + Assert.Equal(expectedElement, value); + } + + IEnumerable GenerateJsonCharacters() + { + // ["xxx...x","xxx...x",...,"xxx...x"] + yield return (byte)'['; + for (int i = 0; i < totalStrings; i++) + { + yield return (byte)'"'; + for (int j = 0; j < stringLength; j++) + { + yield return (byte)'x'; + } + yield return (byte)'"'; + + if (i < totalStrings - 1) + { + yield return (byte)','; + } + } + yield return (byte)']'; + } + } + public static IEnumerable GetAsyncEnumerableSources() { yield return WrapArgs(Enumerable.Empty(), 1, DeserializeAsyncEnumerableOverload.JsonSerializerOptions); @@ -276,5 +319,48 @@ private static async Task> ToListAsync(this IAsyncEnumerable sourc } return list; } + + private sealed class SlowStream(IEnumerable byteSource) : Stream, IDisposable + { + private readonly IEnumerator _enumerator = byteSource.GetEnumerator(); + private long _position; + + public override bool CanRead => true; + public override int Read(byte[] buffer, int offset, int count) + { + Debug.Assert(buffer != null); + Debug.Assert(offset >= 0 && count <= buffer.Length - offset); + + if (count == 0 || !_enumerator.MoveNext()) + { + return 0; + } + + _position++; + buffer[offset] = _enumerator.Current; + return 1; + } + + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Position { get => _position; set => throw new NotSupportedException(); } + public override long Length => throw new NotSupportedException(); + public override void Flush() => throw new NotSupportedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + void IDisposable.Dispose() => _enumerator.Dispose(); + } + + private sealed class StringLengthConverter : JsonConverter + { + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Debug.Assert(!reader.ValueIsEscaped && !reader.HasValueSequence); + return reader.ValueSpan.Length.ToString(CultureInfo.InvariantCulture); + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => throw new NotImplementedException(); + } } } diff --git a/src/native/corehost/hostmisc/pal.windows.cpp b/src/native/corehost/hostmisc/pal.windows.cpp index b11610492d3214..ca0608cd56935e 100644 --- a/src/native/corehost/hostmisc/pal.windows.cpp +++ b/src/native/corehost/hostmisc/pal.windows.cpp @@ -10,6 +10,38 @@ #include #include +namespace +{ + typedef DWORD(WINAPI *get_temp_path_func_ptr)(DWORD buffer_len, LPWSTR buffer); + static volatile get_temp_path_func_ptr s_get_temp_path_func = nullptr; + + DWORD get_temp_path(DWORD buffer_len, LPWSTR buffer) + { + if (s_get_temp_path_func == nullptr) + { + HMODULE kernel32 = ::LoadLibraryExW(L"kernel32.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); + + get_temp_path_func_ptr get_temp_path_func_local = NULL; + if (kernel32 != NULL) + { + // store to thread local variable to prevent data race + get_temp_path_func_local = (get_temp_path_func_ptr)::GetProcAddress(kernel32, "GetTempPath2W"); + } + + if (get_temp_path_func_local == NULL) // method is only available with Windows 10 Creators Update or later + { + s_get_temp_path_func = &GetTempPathW; + } + else + { + s_get_temp_path_func = get_temp_path_func_local; + } + } + + return s_get_temp_path_func(buffer_len, buffer); + } +} + bool GetModuleFileNameWrapper(HMODULE hModule, pal::string_t* recv) { pal::string_t path; @@ -639,7 +671,7 @@ bool get_extraction_base_parent_directory(pal::string_t& directory) const size_t max_len = MAX_PATH + 1; pal::char_t temp_path[max_len]; - size_t len = GetTempPathW(max_len, temp_path); + size_t len = get_temp_path(max_len, temp_path); if (len == 0) { return false;