From fe6f852de3847e6bb9e24cd3be81ac4b68e24d14 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:30:07 -0700 Subject: [PATCH 1/5] [release/10.0] Support querying `TlsCipherSuite` on Http.Sys / IIS (#63768) * init query tls cipher suite info * correct types * dont throw excp * parse it in IIS * test ITlsHandshakeFeature data exposed * add debug publish profile for testing purposes * remove * address PR comments * share SecPkgContext_CipherInfo between httpsys+iis * add panaroid code --------- Co-authored-by: Korolev Dmitry --- .../samples/TlsFeaturesObserve/Program.cs | 11 ++++- src/Servers/HttpSys/src/LoggerEventIds.cs | 1 + .../HttpSys/src/NativeInterop/HttpApi.cs | 2 + .../HttpSys/src/RequestProcessing/Request.cs | 5 +++ .../RequestContext.FeatureCollection.cs | 3 ++ .../RequestProcessing/RequestContext.Log.cs | 3 ++ .../src/RequestProcessing/RequestContext.cs | 40 +++++++++++++++++++ .../IIS/samples/NativeIISSample/Startup.cs | 14 +++++++ .../IIS/IIS/src/Core/IISHttpContext.cs | 24 +++++++++++ .../NativeInterop/SecPkgContext_CipherInfo.cs | 29 ++++++++++++++ 10 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 src/Shared/HttpSys/NativeInterop/SecPkgContext_CipherInfo.cs diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs index ed61268897ef..1b9e03dc5706 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Runtime.InteropServices; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -30,6 +31,7 @@ { var connectionFeature = context.Features.GetRequiredFeature(); var httpSysPropFeature = context.Features.GetRequiredFeature(); + var tlsHandshakeFeature = context.Features.GetRequiredFeature(); // first time invocation to find out required size var success = httpSysPropFeature.TryGetTlsClientHello(Array.Empty(), out var bytesReturned); @@ -41,7 +43,14 @@ success = httpSysPropFeature.TryGetTlsClientHello(bytes, out _); Debug.Assert(success); - await context.Response.WriteAsync($"[Response] connectionId={connectionFeature.ConnectionId}; tlsClientHello.length={bytesReturned}; tlsclienthello start={string.Join(' ', bytes.AsSpan(0, 30).ToArray())}"); + await context.Response.WriteAsync( + $""" + connectionId = {connectionFeature.ConnectionId}; + negotiated cipher suite = {tlsHandshakeFeature.NegotiatedCipherSuite}; + tlsClientHello.length = {bytesReturned}; + tlsclienthello start = {string.Join(' ', bytes.AsSpan(0, 30).ToArray())} + """); + await next(context); }); diff --git a/src/Servers/HttpSys/src/LoggerEventIds.cs b/src/Servers/HttpSys/src/LoggerEventIds.cs index e6d745f506be..a9f2c969c6bc 100644 --- a/src/Servers/HttpSys/src/LoggerEventIds.cs +++ b/src/Servers/HttpSys/src/LoggerEventIds.cs @@ -60,4 +60,5 @@ internal static class LoggerEventIds public const int AcceptObserveExpectationMismatch = 53; public const int RequestParsingError = 54; public const int TlsListenerError = 55; + public const int QueryTlsCipherSuiteError = 56; } diff --git a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs index f5dfbc96a6cd..fbada3843642 100644 --- a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs +++ b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs @@ -70,6 +70,7 @@ internal static unsafe uint HttpSetRequestProperty(SafeHandle requestQueueHandle internal static bool SupportsReset { get; } internal static bool SupportsDelegation { get; } internal static bool SupportsClientHello { get; } + internal static bool SupportsQueryTlsCipherInfo { get; } internal static bool Supported { get; } static unsafe HttpApi() @@ -86,6 +87,7 @@ static unsafe HttpApi() SupportsTrailers = IsFeatureSupported(HTTP_FEATURE_ID.HttpFeatureResponseTrailers); SupportsDelegation = IsFeatureSupported(HTTP_FEATURE_ID.HttpFeatureDelegateEx); SupportsClientHello = IsFeatureSupported((HTTP_FEATURE_ID)11 /* HTTP_FEATURE_ID.HttpFeatureCacheTlsClientHello */) && HttpGetRequestPropertySupported; + SupportsQueryTlsCipherInfo = IsFeatureSupported((HTTP_FEATURE_ID)15 /* HTTP_FEATURE_ID.HttpFeatureQueryCipherInfo */) && HttpGetRequestPropertySupported; } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index 8e4babf7ca21..3d99fe8e718b 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Net; +using System.Net.Security; using System.Security; using System.Security.Authentication; using System.Security.Cryptography; @@ -334,6 +335,8 @@ private AspNetCore.HttpSys.Internal.SocketAddress LocalEndPoint public SslProtocols Protocol { get; private set; } + public TlsCipherSuite? NegotiatedCipherSuite { get; private set; } + [Obsolete(Obsoletions.RuntimeTlsCipherAlgorithmEnumsMessage, DiagnosticId = Obsoletions.RuntimeTlsCipherAlgorithmEnumsDiagId, UrlFormat = Obsoletions.RuntimeSharedUrlFormat)] public CipherAlgorithmType CipherAlgorithm { get; private set; } @@ -356,6 +359,8 @@ private void GetTlsHandshakeResults() { var handshake = RequestContext.GetTlsHandshake(); Protocol = (SslProtocols)handshake.Protocol; + + NegotiatedCipherSuite = RequestContext.GetTlsCipherSuite(); #pragma warning disable SYSLIB0058 // Type or member is obsolete CipherAlgorithm = (CipherAlgorithmType)handshake.CipherType; CipherStrength = (int)handshake.CipherStrength; diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs index 1c80f92febc2..a66e44d1484c 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO.Pipelines; using System.Net; +using System.Net.Security; using System.Security.Authentication; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; @@ -593,6 +594,8 @@ bool IHttpBodyControlFeature.AllowSynchronousIO SslProtocols ITlsHandshakeFeature.Protocol => Request.Protocol; + TlsCipherSuite? ITlsHandshakeFeature.NegotiatedCipherSuite => Request.NegotiatedCipherSuite; + #pragma warning disable SYSLIB0058 // Type or member is obsolete CipherAlgorithmType ITlsHandshakeFeature.CipherAlgorithm => Request.CipherAlgorithm; diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs index d7766698bc41..0fce6896778b 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs @@ -23,5 +23,8 @@ private static partial class Log [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to invoke QueryTlsClientHello; RequestId: {RequestId}; Win32 Error code: {Win32Error}", EventName = "TlsClientHelloRetrieveError")] public static partial void TlsClientHelloRetrieveError(ILogger logger, ulong requestId, uint win32Error); + + [LoggerMessage(LoggerEventIds.QueryTlsCipherSuiteError, LogLevel.Debug, "Failed to invoke QueryTlsCipherSuite; RequestId: {RequestId}; Win32 Error code: {Win32Error}", EventName = "QueryTlsCipherSuiteError")] + public static partial void QueryTlsCipherSuiteError(ILogger logger, ulong requestId, uint win32Error); } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index eba7d33ff3b8..5aefb9df1cf3 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net.Security; using System.Runtime.InteropServices; using System.Security.Principal; using Microsoft.AspNetCore.Http; @@ -219,6 +220,45 @@ internal void ForceCancelRequest() } } + /// + /// Gets TLS cipher suite used for the request, if supported by the OS and http.sys. + /// + /// + /// null, if query of TlsCipherSuite is not supported or the query failed. + /// TlsCipherSuite value, if query is successful. + /// + internal unsafe TlsCipherSuite? GetTlsCipherSuite() + { + if (!HttpApi.SupportsQueryTlsCipherInfo) + { + return default; + } + + var requestId = PinsReleased ? Request.RequestId : RequestId; + + SecPkgContext_CipherInfo cipherInfo = default; + + var statusCode = HttpApi.HttpGetRequestProperty( + requestQueueHandle: Server.RequestQueue.Handle, + requestId, + propertyId: (HTTP_REQUEST_PROPERTY)14 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsCipherInfo */, + qualifier: null, + qualifierSize: 0, + output: &cipherInfo, + outputSize: (uint)sizeof(SecPkgContext_CipherInfo), + bytesReturned: IntPtr.Zero, + overlapped: IntPtr.Zero); + + if (statusCode is ErrorCodes.ERROR_SUCCESS) + { + return checked((TlsCipherSuite)cipherInfo.dwCipherSuite); + } + + // OS supports querying TlsCipherSuite, but request failed. + Log.QueryTlsCipherSuiteError(Logger, requestId, statusCode); + return null; + } + /// /// Attempts to get the client hello message bytes from the http.sys. /// If successful writes the bytes into , and shows how many bytes were written in . diff --git a/src/Servers/IIS/IIS/samples/NativeIISSample/Startup.cs b/src/Servers/IIS/IIS/samples/NativeIISSample/Startup.cs index 0ff3b86369c6..e3559fa5b1e0 100644 --- a/src/Servers/IIS/IIS/samples/NativeIISSample/Startup.cs +++ b/src/Servers/IIS/IIS/samples/NativeIISSample/Startup.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; @@ -52,6 +53,19 @@ public void Configure(IApplicationBuilder app) await context.Response.WriteAsync("ClientCert: " + context.Connection.ClientCertificate + Environment.NewLine); await context.Response.WriteAsync(Environment.NewLine); + var handshakeFeature = context.Features.Get(); + if (handshakeFeature is not null) + { + await context.Response.WriteAsync(Environment.NewLine); + await context.Response.WriteAsync("TLS Information:" + Environment.NewLine); + await context.Response.WriteAsync($"Protocol: {handshakeFeature.Protocol}" + Environment.NewLine); + + if (handshakeFeature.NegotiatedCipherSuite.HasValue) + { + await context.Response.WriteAsync($"Cipher Suite: {handshakeFeature.NegotiatedCipherSuite.Value}" + Environment.NewLine); + } + } + await context.Response.WriteAsync("User: " + context.User.Identity.Name + Environment.NewLine); if (_authSchemeProvider != null) { diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs index 3ddc9315cf66..4a1417ed1b52 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs @@ -402,6 +402,8 @@ private void GetTlsHandshakeResults() { var handshake = GetTlsHandshake(); Protocol = (SslProtocols)handshake.Protocol; + + NegotiatedCipherSuite = GetTlsCipherSuite(); #pragma warning disable SYSLIB0058 // Type or member is obsolete CipherAlgorithm = (CipherAlgorithmType)handshake.CipherType; CipherStrength = (int)handshake.CipherStrength; @@ -415,6 +417,28 @@ private void GetTlsHandshakeResults() SniHostName = sni.Hostname.ToString(); } + private unsafe TlsCipherSuite? GetTlsCipherSuite() + { + SecPkgContext_CipherInfo cipherInfo = default; + + var statusCode = NativeMethods.HttpQueryRequestProperty( + RequestId, + (HTTP_REQUEST_PROPERTY)14 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsCipherInfo */, + qualifier: null, + qualifierSize: 0, + output: &cipherInfo, + outputSize: (uint)sizeof(SecPkgContext_CipherInfo), + bytesReturned: null, + overlapped: IntPtr.Zero); + + if (statusCode == NativeMethods.HR_OK) + { + return checked((TlsCipherSuite)cipherInfo.dwCipherSuite); + } + + return default; + } + private unsafe HTTP_REQUEST_PROPERTY_SNI GetClientSni() { var buffer = new byte[HttpApiTypes.SniPropertySizeInBytes]; diff --git a/src/Shared/HttpSys/NativeInterop/SecPkgContext_CipherInfo.cs b/src/Shared/HttpSys/NativeInterop/SecPkgContext_CipherInfo.cs new file mode 100644 index 000000000000..1bba1c1afcef --- /dev/null +++ b/src/Shared/HttpSys/NativeInterop/SecPkgContext_CipherInfo.cs @@ -0,0 +1,29 @@ +// 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.InteropServices; + +namespace Microsoft.AspNetCore.HttpSys.Internal; + +// From Schannel.h +[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] +internal unsafe struct SecPkgContext_CipherInfo +{ + private const int SZ_ALG_MAX_SIZE = 64; + + private readonly int dwVersion; + private readonly int dwProtocol; + public readonly int dwCipherSuite; + private readonly int dwBaseCipherSuite; + private fixed char szCipherSuite[SZ_ALG_MAX_SIZE]; + private fixed char szCipher[SZ_ALG_MAX_SIZE]; + private readonly int dwCipherLen; + private readonly int dwCipherBlockLen; // in bytes + private fixed char szHash[SZ_ALG_MAX_SIZE]; + private readonly int dwHashLen; + private fixed char szExchange[SZ_ALG_MAX_SIZE]; + private readonly int dwMinExchangeLen; + private readonly int dwMaxExchangeLen; + private fixed char szCertificate[SZ_ALG_MAX_SIZE]; + private readonly int dwKeyType; +} From b3602b41e7fcabfc17d991287d209566d586c2c5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:43:50 -0700 Subject: [PATCH 2/5] Fix runtime architecture detection logic in ANCM. (#63769) Co-authored-by: Aditya Mandaleeka --- .../CommonLib/Environment.cpp | 17 +++++ .../CommonLib/Environment.h | 3 + .../CommonLib/HostFxrResolver.cpp | 72 ++++++++++--------- .../CommonLib/HostFxrResolver.h | 4 +- .../CommonLib/ProcessorArchitecture.h | 28 ++++++++ 5 files changed, 88 insertions(+), 36 deletions(-) create mode 100644 src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ProcessorArchitecture.h diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.cpp b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.cpp index 8112de09b6e6..b0d5a8b2266b 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.cpp @@ -133,6 +133,23 @@ std::wstring Environment::GetDllDirectoryValue() return expandedStr; } +ProcessorArchitecture Environment::GetCurrentProcessArchitecture() +{ + // Use compile-time detection - we know which architectures we support + // and this is the most reliable and efficient approach. IsWow64Process2 + // doesn't show the correct architecture when running under x64 emulation + // on ARM64. +#if defined(_M_ARM64) + return ProcessorArchitecture::ARM64; +#elif defined(_M_AMD64) + return ProcessorArchitecture::AMD64; +#elif defined(_M_IX86) + return ProcessorArchitecture::x86; +#else + static_assert(false, "Unknown target architecture"); +#endif +} + bool Environment::IsRunning64BitProcess() { // Check the bitness of the currently running process diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.h index 9e3e1b1bf772..a9e6e85d9ecc 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.h @@ -5,6 +5,7 @@ #include #include +#include "ProcessorArchitecture.h" class Environment { @@ -23,6 +24,8 @@ class Environment static bool IsRunning64BitProcess(); static + ProcessorArchitecture GetCurrentProcessArchitecture(); + static HRESULT CopyToDirectory(const std::wstring& source, const std::filesystem::path& destination, bool cleanDest, const std::filesystem::path& directoryToIgnore, int& copiedFileCount); static bool CheckUpToDate(const std::wstring& source, const std::filesystem::path& destination, const std::wstring& extension, const std::filesystem::path& directoryToIgnore); diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.cpp b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.cpp index 8233d68a115e..8fc74b47c993 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.cpp @@ -411,7 +411,6 @@ HostFxrResolver::InvokeWhereToFindDotnet() HandleWrapper hThread; CComBSTR pwzDotnetName = nullptr; DWORD dwFilePointer = 0; - BOOL fIsCurrentProcess64Bit = FALSE; DWORD dwExitCode = 0; STRU struDotnetSubstring; STRU struDotnetLocationsString; @@ -426,6 +425,7 @@ HostFxrResolver::InvokeWhereToFindDotnet() securityAttributes.bInheritHandle = TRUE; LOG_INFO(L"Invoking where.exe to find dotnet.exe"); + auto currentProcessArch = Environment::GetCurrentProcessArchitecture(); // Create a read/write pipe that will be used for reading the result of where.exe FINISHED_LAST_ERROR_IF(!CreatePipe(&hStdOutReadPipe, &hStdOutWritePipe, &securityAttributes, 0)); @@ -499,13 +499,9 @@ HostFxrResolver::InvokeWhereToFindDotnet() } FINISHED_IF_FAILED(struDotnetLocationsString.CopyA(pzFileContents, dwNumBytesRead)); - LOG_INFOF(L"where.exe invocation returned: '%ls'", struDotnetLocationsString.QueryStr()); - fIsCurrentProcess64Bit = Environment::IsRunning64BitProcess(); - - LOG_INFOF(L"Current process bitness type detected as isX64=%d", fIsCurrentProcess64Bit); - + // Look for a dotnet.exe that matches the current process architecture while (TRUE) { index = struDotnetLocationsString.IndexOf(L"\r\n", prevIndex); @@ -518,28 +514,38 @@ HostFxrResolver::InvokeWhereToFindDotnet() // \r\n is two wchars, so add 2 here. prevIndex = index + 2; - LOG_INFOF(L"Processing entry '%ls'", struDotnetSubstring.QueryStr()); - - if (fIsCurrentProcess64Bit == IsX64(struDotnetSubstring.QueryStr())) + ProcessorArchitecture dotnetArch = GetFileProcessorArchitecture(struDotnetSubstring.QueryStr()); + if (dotnetArch == currentProcessArch) { - // The bitness of dotnet matched with the current worker process bitness. + LOG_INFOF(L"Found dotnet.exe matching current process architecture (%ls) '%ls'", + ProcessorArchitectureToString(dotnetArch), + struDotnetSubstring.QueryStr()); + return std::make_optional(struDotnetSubstring.QueryStr()); } + else + { + LOG_INFOF(L"Skipping dotnet.exe with non-matching architecture %ls (need %ls). '%ls'", + ProcessorArchitectureToString(dotnetArch), + ProcessorArchitectureToString(currentProcessArch), + struDotnetSubstring.QueryStr()); + } } Finished: return result; } -BOOL HostFxrResolver::IsX64(const WCHAR* dotnetPath) +// Reads the PE header of the binary to determine its architecture. +ProcessorArchitecture HostFxrResolver::GetFileProcessorArchitecture(const WCHAR* binaryPath) { // Errors while reading from the file shouldn't throw unless // file.exception(bits) is set - std::ifstream file(dotnetPath, std::ios::binary); + std::ifstream file(binaryPath, std::ios::binary); if (!file.is_open()) { - LOG_TRACEF(L"Failed to open file %ls", dotnetPath); - return false; + LOG_TRACEF(L"Failed to open file %ls", binaryPath); + return ProcessorArchitecture::Unknown; } // Read the DOS header @@ -547,8 +553,8 @@ BOOL HostFxrResolver::IsX64(const WCHAR* dotnetPath) file.read(reinterpret_cast(&dosHeader), sizeof(dosHeader)); if (dosHeader.e_magic != IMAGE_DOS_SIGNATURE) // 'MZ' { - LOG_TRACEF(L"%ls is not a valid executable file (missing MZ header).", dotnetPath); - return false; + LOG_TRACEF(L"%ls is not a valid executable file (missing MZ header).", binaryPath); + return ProcessorArchitecture::Unknown; } // Seek to the PE header @@ -559,32 +565,30 @@ BOOL HostFxrResolver::IsX64(const WCHAR* dotnetPath) file.read(reinterpret_cast(&peSignature), sizeof(peSignature)); if (peSignature != IMAGE_NT_SIGNATURE) // 'PE\0\0' { - LOG_TRACEF(L"%ls is not a valid PE file (missing PE header).", dotnetPath); - return false; + LOG_TRACEF(L"%ls is not a valid PE file (missing PE header).", binaryPath); + return ProcessorArchitecture::Unknown; } // Read the file header IMAGE_FILE_HEADER fileHeader{}; file.read(reinterpret_cast(&fileHeader), sizeof(fileHeader)); - // Read the optional header magic field - WORD magic{}; - file.read(reinterpret_cast(&magic), sizeof(magic)); - - // Determine the architecture based on the magic value - if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) + // Determine the architecture based on the machine type + switch (fileHeader.Machine) { - LOG_INFOF(L"%ls is 32-bit", dotnetPath); - return false; - } - else if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) - { - LOG_INFOF(L"%ls is 64-bit", dotnetPath); - return true; + case IMAGE_FILE_MACHINE_I386: + LOG_INFOF(L"%ls is x86 (32-bit)", binaryPath); + return ProcessorArchitecture::x86; + case IMAGE_FILE_MACHINE_AMD64: + LOG_INFOF(L"%ls is AMD64 (x64)", binaryPath); + return ProcessorArchitecture::AMD64; + case IMAGE_FILE_MACHINE_ARM64: + LOG_INFOF(L"%ls is ARM64", binaryPath); + return ProcessorArchitecture::ARM64; + default: + LOG_INFOF(L"%ls has unknown architecture (machine type: 0x%X)", binaryPath, fileHeader.Machine); + return ProcessorArchitecture::Unknown; } - - LOG_INFOF(L"%ls is unknown architecture %i", dotnetPath, fileHeader.Machine); - return false; } std::optional diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.h index 08ec650aec54..9065e2aecd2b 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.h @@ -8,8 +8,8 @@ #include #include #include - #include "ErrorContext.h" +#include "ProcessorArchitecture.h" #define READ_BUFFER_SIZE 4096 @@ -74,7 +74,7 @@ class HostFxrResolver const std::filesystem::path & requestedPath ); - static BOOL IsX64(const WCHAR* dotnetPath); + static ProcessorArchitecture GetFileProcessorArchitecture(const WCHAR* binaryPath); struct LocalFreeDeleter { diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ProcessorArchitecture.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ProcessorArchitecture.h new file mode 100644 index 000000000000..195feddcae7b --- /dev/null +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ProcessorArchitecture.h @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +enum class ProcessorArchitecture +{ + Unknown, + x86, + AMD64, + ARM64 +}; + +inline const wchar_t* ProcessorArchitectureToString(ProcessorArchitecture arch) +{ + switch (arch) + { + case ProcessorArchitecture::x86: + return L"x86"; + case ProcessorArchitecture::AMD64: + return L"AMD64"; + case ProcessorArchitecture::ARM64: + return L"ARM64"; + case ProcessorArchitecture::Unknown: + default: + return L"Unknown"; + } +} \ No newline at end of file From 1bee00b35e31835802906586a29c206c8f4d166b Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 15:17:50 -0700 Subject: [PATCH 3/5] Update dependencies from https://github.com/dotnet/extensions build 20250918.3 (#63764) On relative base path root Microsoft.Extensions.Caching.Hybrid , Microsoft.Extensions.Diagnostics.Testing , Microsoft.Extensions.TimeProvider.Testing From Version 9.10.0-preview.1.25462.1 -> To Version 9.10.0-preview.1.25468.3 Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.props | 6 +++--- eng/Version.Details.xml | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 38a30def8f02..f0a08d4fc977 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -104,9 +104,9 @@ This file should be imported by eng/Versions.props 4.13.0-3.24613.7 4.13.0-3.24613.7 - 9.10.0-preview.1.25462.1 - 9.10.0-preview.1.25462.1 - 9.10.0-preview.1.25462.1 + 9.10.0-preview.1.25468.3 + 9.10.0-preview.1.25468.3 + 9.10.0-preview.1.25468.3 1.0.0-prerelease.25458.1 1.0.0-prerelease.25458.1 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index d2f6a5f99199..9c5efdfa49ae 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -390,17 +390,17 @@ https://github.com/dotnet/dotnet 2dea164f01d307c409cfe0d0ee5cb8a0691e3c94 - + https://github.com/dotnet/extensions - d299e16f15234f9808b18fef50bf7770113fb4b2 + 53ef1158f9f42632e111d6873a8cd72b803b4ae6 - + https://github.com/dotnet/extensions - d299e16f15234f9808b18fef50bf7770113fb4b2 + 53ef1158f9f42632e111d6873a8cd72b803b4ae6 - + https://github.com/dotnet/extensions - d299e16f15234f9808b18fef50bf7770113fb4b2 + 53ef1158f9f42632e111d6873a8cd72b803b4ae6 https://dev.azure.com/dnceng/internal/_git/dotnet-optimization From b862c38c0df064f649ebd75ff1778a7566f6b956 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 15:18:04 -0700 Subject: [PATCH 4/5] Update dependencies from https://dev.azure.com/dnceng/internal/_git/dotnet-optimization build 20250917.1 (#63762) On relative base path root optimization.linux-arm64.MIBC.Runtime , optimization.linux-x64.MIBC.Runtime , optimization.windows_nt-arm64.MIBC.Runtime , optimization.windows_nt-x64.MIBC.Runtime , optimization.windows_nt-x86.MIBC.Runtime From Version 1.0.0-prerelease.25458.1 -> To Version 1.0.0-prerelease.25467.1 Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.props | 10 +++++----- eng/Version.Details.xml | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index f0a08d4fc977..34e606099902 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -108,11 +108,11 @@ This file should be imported by eng/Versions.props 9.10.0-preview.1.25468.3 9.10.0-preview.1.25468.3 - 1.0.0-prerelease.25458.1 - 1.0.0-prerelease.25458.1 - 1.0.0-prerelease.25458.1 - 1.0.0-prerelease.25458.1 - 1.0.0-prerelease.25458.1 + 1.0.0-prerelease.25467.1 + 1.0.0-prerelease.25467.1 + 1.0.0-prerelease.25467.1 + 1.0.0-prerelease.25467.1 + 1.0.0-prerelease.25467.1 17.12.36 17.12.36 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 9c5efdfa49ae..298dcd5d1569 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -402,25 +402,25 @@ https://github.com/dotnet/extensions 53ef1158f9f42632e111d6873a8cd72b803b4ae6 - + https://dev.azure.com/dnceng/internal/_git/dotnet-optimization - 373ed5bf1a64c212e655063e58967eb62f9187ef + 59dc6a9bf1b3e3ab71c73d94160c2049fb104cd1 - + https://dev.azure.com/dnceng/internal/_git/dotnet-optimization - 373ed5bf1a64c212e655063e58967eb62f9187ef + 59dc6a9bf1b3e3ab71c73d94160c2049fb104cd1 - + https://dev.azure.com/dnceng/internal/_git/dotnet-optimization - 373ed5bf1a64c212e655063e58967eb62f9187ef + 59dc6a9bf1b3e3ab71c73d94160c2049fb104cd1 - + https://dev.azure.com/dnceng/internal/_git/dotnet-optimization - 373ed5bf1a64c212e655063e58967eb62f9187ef + 59dc6a9bf1b3e3ab71c73d94160c2049fb104cd1 - + https://dev.azure.com/dnceng/internal/_git/dotnet-optimization - 373ed5bf1a64c212e655063e58967eb62f9187ef + 59dc6a9bf1b3e3ab71c73d94160c2049fb104cd1 From 9ee3a381a6a5d9df2cd9492d57c97e9ba4e28c6e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 16:36:44 -0700 Subject: [PATCH 5/5] [release/10.0] [Blazor] Increment MaxItemCount when OverscanCount > MaxItemCount (#63767) * Virtualize test * Fix test * Fix test --------- Co-authored-by: Javier Calvarro Nelson --- .../Web/src/Virtualization/Virtualize.cs | 4 + .../test/E2ETest/Tests/VirtualizationTest.cs | 99 ++++++++++++++++++- .../test/testassets/BasicTestApp/Index.razor | 1 + .../VirtualizationLargeOverscan.razor | 12 +++ 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 src/Components/test/testassets/BasicTestApp/VirtualizationLargeOverscan.razor diff --git a/src/Components/Web/src/Virtualization/Virtualize.cs b/src/Components/Web/src/Virtualization/Virtualize.cs index 6ce14ee0952d..8e2d84f2f11b 100644 --- a/src/Components/Web/src/Virtualization/Virtualize.cs +++ b/src/Components/Web/src/Virtualization/Virtualize.cs @@ -362,6 +362,10 @@ private void CalcualteItemDistribution( _ => MaxItemCount }; + // Count the OverscanCount as used capacity, so we don't end up in a situation where + // the user has set a very low MaxItemCount and we end up in an infinite loading loop. + maxItemCount += OverscanCount * 2; + itemsInSpacer = Math.Max(0, (int)Math.Floor(spacerSize / _itemSize) - OverscanCount); visibleItemCapacity = (int)Math.Ceiling(containerSize / _itemSize) + 2 * OverscanCount; unusedItemCapacity = Math.Max(0, visibleItemCapacity - maxItemCount); diff --git a/src/Components/test/E2ETest/Tests/VirtualizationTest.cs b/src/Components/test/E2ETest/Tests/VirtualizationTest.cs index 565dc0190fcd..4fa21ef3fe11 100644 --- a/src/Components/test/E2ETest/Tests/VirtualizationTest.cs +++ b/src/Components/test/E2ETest/Tests/VirtualizationTest.cs @@ -291,14 +291,14 @@ public void CanLimitMaxItemsRendered(bool useAppContext) // we only render 10 items due to the MaxItemCount setting var scrollArea = Browser.Exists(By.Id("virtualize-scroll-area")); var getItems = () => scrollArea.FindElements(By.ClassName("my-item")); - Browser.Equal(10, () => getItems().Count); + Browser.Equal(16, () => getItems().Count); Browser.Equal("Id: 0; Name: Thing 0", () => getItems().First().Text); // Scrolling still works and loads new data, though there's no guarantee about // exactly how many items will show up at any one time Browser.ExecuteJavaScript("document.getElementById('virtualize-scroll-area').scrollTop = 300;"); Browser.NotEqual("Id: 0; Name: Thing 0", () => getItems().First().Text); - Browser.True(() => getItems().Count > 3 && getItems().Count <= 10); + Browser.True(() => getItems().Count > 3 && getItems().Count <= 16); } [Fact] @@ -573,6 +573,101 @@ public void EmptyContentRendered_Async() int GetPlaceholderCount() => Browser.FindElements(By.Id("async-placeholder")).Count; } + [Fact] + public void CanElevateEffectiveMaxItemCount_WhenOverscanExceedsMax() + { + Browser.MountTestComponent(); + var container = Browser.Exists(By.Id("virtualize-large-overscan")); + // Ensure we have an initial contiguous batch and the elevated effective max has kicked in (>= OverscanCount) + var indices = GetVisibleItemIndices(); + Browser.True(() => indices.Count >= 200); + + // Give focus so PageDown works + container.Click(); + + var js = (IJavaScriptExecutor)Browser; + var lastMaxIndex = -1; + var lastScrollTop = -1L; + + // Check if we've reached (or effectively reached) the bottom + var scrollHeight = (long)js.ExecuteScript("return arguments[0].scrollHeight", container); + var clientHeight = (long)js.ExecuteScript("return arguments[0].clientHeight", container); + var scrollTop = (long)js.ExecuteScript("return arguments[0].scrollTop", container); + while (scrollTop + clientHeight < scrollHeight) + { + // Validate contiguity on the current page + Browser.True(() => IsCurrentViewContiguous(indices)); + + // Track progress in indices + var currentMax = indices.Max(); + Assert.True(currentMax >= lastMaxIndex, $"Unexpected backward movement: previous max {lastMaxIndex}, current max {currentMax}."); + lastMaxIndex = currentMax; + + // Send PageDown + container.SendKeys(Keys.PageDown); + + // Wait for scrollTop to change (progress) to avoid infinite loop + var prevScrollTop = scrollTop; + Browser.True(() => + { + var st = (long)js.ExecuteScript("return arguments[0].scrollTop", container); + if (st > prevScrollTop) + { + lastScrollTop = st; + return true; + } + return false; + }); + scrollHeight = (long)js.ExecuteScript("return arguments[0].scrollHeight", container); + clientHeight = (long)js.ExecuteScript("return arguments[0].clientHeight", container); + scrollTop = (long)js.ExecuteScript("return arguments[0].scrollTop", container); + } + + // Final contiguous assertion at bottom + Browser.True(() => IsCurrentViewContiguous()); + + // Helper: check visible items contiguous with no holes + bool IsCurrentViewContiguous(List existingIndices = null) + { + var indices = existingIndices ?? GetVisibleItemIndices(); + if (indices.Count == 0) + { + return false; + } + + if (indices[^1] - indices[0] != indices.Count - 1) + { + return false; + } + for (var i = 1; i < indices.Count; i++) + { + if (indices[i] - indices[i - 1] != 1) + { + return false; + } + } + return true; + } + + List GetVisibleItemIndices() + { + var elements = container.FindElements(By.CssSelector(".large-overscan-item")); + var list = new List(elements.Count); + foreach (var el in elements) + { + var text = el.Text; + if (text.StartsWith("Item ", StringComparison.Ordinal)) + { + if (int.TryParse(text.AsSpan(5), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + { + list.Add(value); + } + } + } + return list; + } + } + private string[] GetPeopleNames(IWebElement container) { var peopleElements = container.FindElements(By.CssSelector(".person span")); diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index a3bc250f0634..d7df87adbeed 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -119,6 +119,7 @@ + diff --git a/src/Components/test/testassets/BasicTestApp/VirtualizationLargeOverscan.razor b/src/Components/test/testassets/BasicTestApp/VirtualizationLargeOverscan.razor new file mode 100644 index 000000000000..3beb29cf87c2 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/VirtualizationLargeOverscan.razor @@ -0,0 +1,12 @@ +@* Test component to validate behavior when OverscanCount greatly exceeds MaxItemCount. *@ +@using Microsoft.AspNetCore.Components.Web.Virtualization + +
+ +
Item @context
+
+
+ +@code { + private IList _items = Enumerable.Range(0, 5000).ToList(); +}