using System.ComponentModel; using System.Runtime.InteropServices; using System.Runtime.Versioning; using Microsoft.Win32.SafeHandles; /// Byte-for-byte the Windows descriptor observer of Sdk2LifecycleWindowsDescriptors.cs (lmxopcua#799), hosted for the SDK1 comparator. internal static class WindowsDescriptors { [SupportedOSPlatform("windows")] internal static Task<(int Total, int Pki)> CountAsync(CancellationToken ct, IReadOnlyList roots) { var result = new TaskCompletionSource<(int Total, int Pki)>(TaskCreationOptions.RunContinuationsAsynchronously); var thread = new Thread(() => { try { result.TrySetResult(WindowsHandleCounts(roots)); } catch (Exception error) { result.TrySetException(error); } }) { IsBackground = true, Name = "soak-descriptor-probe" }; thread.Start(); return BoundAsync(); async Task<(int Total, int Pki)> BoundAsync() { try { return await result.Task.WaitAsync(TimeSpan.FromSeconds(5), ct).ConfigureAwait(false); } catch (TimeoutException) { throw new TimeoutException("Descriptor probe exceeded its handle-enumeration deadline."); } } } [SupportedOSPlatform("windows")] private static (int Total, int Pki) WindowsHandleCounts(IReadOnlyList roots) { // Canonicalise the roots first and close those directory handles before the snapshot, so they are not counted. var canonicalRoots = roots.Select(WindowsCanonicalRoot).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); var self = (nuint)Environment.ProcessId; var length = 1 << 20; while (true) { var buffer = Marshal.AllocHGlobal(length); try { var status = NtQuerySystemInformation(SystemExtendedHandleInformation, buffer, length, out var needed); if (status == StatusInfoLengthMismatch) { length = Math.Max(length * 2, needed + (1 << 16)); continue; } if (status != 0) throw new Win32Exception(RtlNtStatusToDosError(status), $"NtQuerySystemInformation failed (0x{status:X8})."); var count = (long)Marshal.ReadIntPtr(buffer); var entries = buffer + 2 * IntPtr.Size; var entrySize = Marshal.SizeOf(); int total = 0, pki = 0; var path = new char[32768]; for (long i = 0; i < count; i++) { var entry = Marshal.PtrToStructure(entries + (nint)(i * entrySize)); if (entry.UniqueProcessId != self) continue; total++; var handle = (nint)entry.HandleValue; // GetFileType is answered by the I/O manager without the file-object lock; only disk files and // directories go on to the name query (pipes, sockets and consoles cannot hold PKI material). if (GetFileType(handle) != FileTypeDisk) continue; var name = FinalPath(handle, path); if (name is not null && WithinWindowsRoots(name, canonicalRoots)) pki++; } return (total, pki); } finally { Marshal.FreeHGlobal(buffer); } } } [SupportedOSPlatform("windows")] private static string WindowsCanonicalRoot(string root) { var full = Path.TrimEndingDirectorySeparator(Path.GetFullPath(root)); if (!Directory.Exists(full)) return BaselineMetrics.CanonicalPath(full); using var directory = CreateFileW(full, 0, FileShareAll, 0, OpenExisting, FileFlagBackupSemantics, 0); if (directory.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error(), "Cannot open a PKI root for canonicalisation."); return FinalPath(directory.DangerousGetHandle(), new char[32768]) ?? throw new IOException("A PKI root has no final path."); } private static string? FinalPath(nint handle, char[] buffer) { var written = GetFinalPathNameByHandleW(handle, buffer, buffer.Length, 0); if (written == 0 || written >= buffer.Length) return null; // Closed/reused between snapshot and query, or not a named file. var name = new string(buffer, 0, (int)written); if (name.StartsWith(@"\\?\UNC\", StringComparison.Ordinal)) return @"\\" + name[8..]; return name.StartsWith(@"\\?\", StringComparison.Ordinal) ? name[4..] : name; } private static bool WithinWindowsRoots(string path, IReadOnlyList roots) { path = Path.TrimEndingDirectorySeparator(path); return roots.Any(root => path.Equals(root, StringComparison.OrdinalIgnoreCase) || path.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)); } private const int SystemExtendedHandleInformation = 64; private const int StatusInfoLengthMismatch = unchecked((int)0xC0000004); private const uint FileTypeDisk = 1; private const uint FileShareAll = 7; private const uint OpenExisting = 3; private const uint FileFlagBackupSemantics = 0x02000000; [StructLayout(LayoutKind.Sequential)] private struct HandleEntry { public nint Object; public nuint UniqueProcessId; public nuint HandleValue; public uint GrantedAccess; public ushort CreatorBackTraceIndex; public ushort ObjectTypeIndex; public uint HandleAttributes; public uint Reserved; } [DllImport("ntdll.dll")] private static extern int NtQuerySystemInformation(int informationClass, nint information, int length, out int returnLength); [DllImport("ntdll.dll")] private static extern int RtlNtStatusToDosError(int status); [DllImport("kernel32.dll", SetLastError = true)] private static extern uint GetFileType(nint handle); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern uint GetFinalPathNameByHandleW(nint handle, [Out] char[] path, int length, uint flags); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern SafeFileHandle CreateFileW(string name, uint access, uint share, nint security, uint disposition, uint flags, nint template); }