< Summary - Results for net481, Release

Information
Class: LockCheck.Windows.NtDll
Assembly: LockCheck
File(s): D:\a\LockCheck\LockCheck\src\LockCheck\Windows\NtDll.cs
Tag: 117_11660770947
Line coverage
83%
Covered lines: 147
Uncovered lines: 29
Coverable lines: 176
Total lines: 493
Line coverage: 83.5%
Branch coverage
75%
Covered branches: 63
Total branches: 84
Branch coverage: 75%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

D:\a\LockCheck\LockCheck\src\LockCheck\Windows\NtDll.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.ComponentModel;
 4using System.Diagnostics;
 5using System.Diagnostics.CodeAnalysis;
 6using System.IO;
 7using System.Linq;
 8using System.Runtime.CompilerServices;
 9using System.Runtime.InteropServices;
 10using System.Security.Principal;
 11using System.Threading;
 12
 13using static LockCheck.Windows.NativeMethods;
 14
 15namespace LockCheck.Windows;
 16
 17internal static class NtDll
 18{
 19    internal class PseudoPeb
 20    {
 221        public PseudoPeb(SYSTEM_PROCESS_INFORMATION pi, string? executable, string? systemAccount, string? processName =
 22        {
 223            ProcessId = pi.UniqueProcessId.ToInt32();
 224            ParentProcessId = pi.InheritedFromUniqueProcessId.ToInt32();
 225            ExecutableFullPath = executable;
 226            ProcessName = pi.NamePtr != IntPtr.Zero ? Marshal.PtrToStringUni(pi.NamePtr) : processName;
 227            Owner = systemAccount;
 228            ProcessSequenceNumber = processSequenceNumber;
 229            StartTime = DateTime.FromFileTime(pi.CreateTime);
 230        }
 31
 232        public int ProcessId { get; private set; }
 233        public int? ParentProcessId { get; private set; }
 234        public string? ProcessName { get; private set; }
 235        public string? ExecutableFullPath { get; private set; }
 236        public string? Owner { get; private set; }
 237        public ulong? ProcessSequenceNumber { get; }
 238        public DateTime StartTime { get; private set; }
 239        public int SessionId => 0;
 240        public bool? IsCritical => true;
 241        public bool IsPseudoProcess => true;
 42    }
 43
 44    // Pseudo processes never change during w/o rebooting. So we can cache them up front.
 245    private static readonly Lazy<Dictionary<int, PseudoPeb>> s_systemPseudoProcesses = new(() =>
 246    {
 247        string? systemAccount = GetSystemAccountName();
 248        var result = new Dictionary<int, PseudoPeb>();
 249
 250        EnumerateSystemProcesses(null, result, (res, _, pi, processSequenceNumber) =>
 251        {
 252            if ((int)pi.UniqueProcessId == 0)
 253            {
 254                // "System Idle Process" always PID 0, does not have a name, even in SYSTEM_PROCESS_INFORMATION.NamePtr
 255                res![0] = new PseudoPeb(pi, null, systemAccount, "System Idle Process", processSequenceNumber);
 256            }
 257            else if (pi.NamePtr != IntPtr.Zero)
 258            {
 259                string? name = Marshal.PtrToStringUni(pi.NamePtr);
 260                if (name != null && IsSystemPseudoProcessByName(name, out var executable))
 261                {
 262                    var pseudoPeb = new PseudoPeb(pi, executable, systemAccount, processSequenceNumber: processSequenceN
 263                    res![pseudoPeb.ProcessId] = pseudoPeb;
 264                }
 265            }
 266            return 0;
 267        });
 268
 269        return result;
 270    }, LazyThreadSafetyMode.ExecutionAndPublication);
 71
 72    private static bool IsSystemPseudoProcessByName(string? processName, out string? executablePath)
 73    {
 274        executablePath = null;
 275        if (processName != null)
 76        {
 77            switch (processName)
 78            {
 79                case "System Idle Process":
 80                    // Process Explorer and others also return no executable name here,
 81                    // even though this *is* a pseudo process. Technically, we should
 82                    // never get here, because this process (PID 0) doesn't have a name
 83                    // set in SYSTEM_PROCESS_INFORMATION.NamePtr, but we're playing it
 84                    // safe.
 085                    return true;
 86                case "System":
 87                case "Secure System":
 88                case "Registry":
 89                case "Memory Compression":
 90                    // Regardless of whether the current process is WOW64, 64 bit or 32 bit, always return
 91                    // the "actual" system directory here. This is compatible to what NativeMethods.GetProcessImagePath(
 92                    // does.
 293                    executablePath = Environment.ExpandEnvironmentVariables("%windir%\\System32\\ntoskrnl.exe");
 294                    return true;
 95            }
 96        }
 97
 298        return false;
 99    }
 100
 101    private static string? GetSystemAccountName()
 102    {
 103        try
 104        {
 2105            var sid = new SecurityIdentifier("S-1-5-18");
 2106            return sid.Translate(typeof(NTAccount)).Value;
 107        }
 0108        catch
 109        {
 0110        }
 111
 0112        return null;
 2113    }
 114
 115    internal static bool TryGetSystemPseudoProcess(int processId, [NotNullWhen(true)] out PseudoPeb? info)
 2116        => s_systemPseudoProcesses.Value.TryGetValue(processId, out info);
 117
 118    public static HashSet<IWin32ProcessDetails> GetAllProcesses()
 119    {
 0120        return EnumerateSystemProcesses(null, (object?)null,
 0121            static (_, idx, pi, seq) => (IWin32ProcessDetails)new Peb(pi, seq))
 0122            .Select(v => v.Value)
 0123            .ToHashSet();
 124    }
 125
 126    public static HashSet<ProcessInfo> GetLockingProcessInfos(string[] paths, [NotNullIfNotNull(nameof(directories))] re
 127    {
 2128        if (paths == null)
 129        {
 2130            throw new ArgumentNullException(nameof(paths));
 131        }
 132
 2133        var result = new HashSet<ProcessInfo>();
 134
 2135        foreach (string path in paths)
 136        {
 2137            if (Directory.Exists(path))
 138            {
 2139                directories?.Add(path);
 2140                continue;
 141            }
 142
 2143            GetLockingProcessInfo(path, result);
 144        }
 145
 2146        return result;
 147    }
 148
 149    private static void GetLockingProcessInfo(string path, HashSet<ProcessInfo> result)
 150    {
 2151        if (path == null)
 152        {
 2153            throw new ArgumentNullException(nameof(path));
 154        }
 155
 2156        if (result == null)
 157        {
 0158            throw new ArgumentNullException(nameof(result));
 159        }
 160
 2161        var statusBlock = new IO_STATUS_BLOCK();
 162
 2163        using (var handle = GetFileHandle(path))
 164        {
 2165            if (handle.IsInvalid)
 166            {
 167                // The file does not exist or is gone already. Could be a race condition. There is nothing we can contri
 168                // Doing this, exhibits the same behavior as the RestartManager implementation.
 0169                return;
 170            }
 171
 172            // The resulting FILE_PROCESS_IDS_USING_FILE_INFORMATION structure is rather small (on 64 bit Windows,
 173            // 12 byte + padding). Additionally, we assume that there aren't a whole lot of processes locking the
 174            // same path. Thus choosing our initial buffer size rather conservative for about 8 processes.
 2175            int bufferSize = (IntPtr.Size + sizeof(int)) * 8;
 176#if NET
 177            using (var mem = new ScopedNativeMemory(stackalloc byte[bufferSize]))
 178#else
 2179            using (var mem = new ScopedNativeMemory(bufferSize))
 180#endif
 181            {
 182                uint status;
 2183                while ((status = NtQueryInformationFile(handle, ref statusBlock, (IntPtr)mem, (uint)mem.Size,
 2184                    FILE_INFORMATION_CLASS.FileProcessIdsUsingFileInformation)) == STATUS_INFO_LENGTH_MISMATCH)
 185                {
 0186                    mem.Resize(mem.Size * 2);
 187                }
 188
 2189                if (status != STATUS_SUCCESS)
 190                {
 0191                    throw GetException(status);
 192                }
 193
 194                // Buffer contains:
 195                //    struct FILE_PROCESS_IDS_USING_FILE_INFORMATION
 196                //    {
 197                //        ULONG NumberOfProcessIdsInList;
 198                //        ULONG_PTR ProcessIdList[1];
 199                //    }
 200
 2201                IntPtr readBuffer = (IntPtr)mem;
 2202                int numEntries = Marshal.ReadInt32(readBuffer); // NumberOfProcessIdsInList
 2203                readBuffer += IntPtr.Size;
 204
 2205                for (int i = 0; i < numEntries; i++)
 206                {
 2207                    int processId = Marshal.ReadIntPtr(readBuffer).ToInt32(); // A single ProcessIdList[] element
 2208                    var entry = ProcessInfoWindows.Create(processId);
 2209                    if (entry != null)
 210                    {
 2211                        result.Add(entry);
 212                    }
 2213                    readBuffer += IntPtr.Size;
 214                }
 2215            }
 216        }
 2217    }
 218
 219    internal static Win32Exception GetException(uint status)
 220    {
 0221        int res = RtlNtStatusToDosError(status);
 0222        return new Win32Exception(res, GetMessage(res));
 223    }
 224
 225    internal static Dictionary<(int, DateTime), ProcessInfo> GetProcessesByWorkingDirectory(List<string> directories)
 226    {
 2227        if (directories == null)
 228        {
 0229            throw new ArgumentNullException(nameof(directories));
 230        }
 231
 2232        return EnumerateSystemProcesses(null, directories,
 2233            static (dirs, idx, pi, seq) =>
 2234            {
 2235                var peb = new Peb(pi, seq);
 2236
 2237                if (!peb.HasError && !string.IsNullOrEmpty(peb.CurrentDirectory))
 2238                {
 2239                    // If the process' current directory is the search path itself, or it is somewhere nested below it,
 2240                    // we have to take it into account. This will also account for differences in the two when the
 2241                    // search path does not end with a '\', but the PEB's current directory does (which is always the
 2242                    // case).
 2243                    if (dirs!.FindIndex(d => peb.CurrentDirectory.StartsWith(d, StringComparison.OrdinalIgnoreCase)) != 
 2244                    {
 2245                        return (ProcessInfo)new ProcessInfoWindows(peb);
 2246                    }
 2247                }
 2248
 2249                return null;
 2250            });
 251    }
 252
 253    // Use a smaller buffer size on debug to ensure we hit the retry path.
 2254    private static uint GetDefaultCachedBufferSize() => 1024 *
 2255#if DEBUG
 2256        8;
 2257#else
 2258        1024;
 259#endif
 260
 261#if NET
 262    //
 263    // This implementation with based on dotnet/runtime ProcessManager.Win32.cs does.
 264    // Basically, it doesn't hold on to a "cached buffer" and uses more modern constructs
 265    // which results in "simpler" code. Especially, it does not use GCHandle and also
 266    // doesn't have workarounds for "older" versions of Windows anymore.
 267    //
 268
 269    private static uint s_mostRecentSize = GetDefaultCachedBufferSize();
 270
 271    internal static unsafe Dictionary<(int, DateTime), T> EnumerateSystemProcesses<T, TData>(
 272        HashSet<int>? processIds,
 273        TData? data,
 274        Func<TData?, int, SYSTEM_PROCESS_INFORMATION, ulong?, T?> newEntry)
 275    {
 276        // Start with the default buffer size.
 277        uint bufferSize = s_mostRecentSize;
 278        var infoClass = SYSTEM_INFORMATION_CLASS.SystemProcessInformation;
 279
 280        while (true)
 281        {
 282            // Some platforms require the buffer to be 64-bit aligned. ScopedNativeMemory guarantees sufficient alignmen
 283            using var mem = new ScopedNativeMemory((int)bufferSize);
 284
 285            uint actualSize = 0;
 286            uint status = NtQuerySystemInformation(infoClass, (void*)mem, bufferSize, &actualSize);
 287
 288            if (status != STATUS_INFO_LENGTH_MISMATCH)
 289            {
 290                // see definition of NT_SUCCESS(Status) in SDK
 291                if ((int)status < 0)
 292                {
 293                    throw GetException(status);
 294                }
 295
 296                // Remember last buffer size for next attempt. Note that this may also result in smaller
 297                // buffer sizes for further attempts, as the live processes can also decrease in comparison
 298                // to a previous call.
 299                Debug.Assert(actualSize > 0 && actualSize <= bufferSize, $"actualSize={actualSize} bufferSize={bufferSiz
 300                s_mostRecentSize = GetEstimatedBufferSize(actualSize);
 301
 302                return HandleProcesses(new ReadOnlySpan<byte>((void*)mem, (int)actualSize), processIds, data, newEntry);
 303            }
 304            else
 305            {
 306                // Buffer was too small; retry with a larger buffer.
 307                Debug.Assert(actualSize > bufferSize, $"actualSize={actualSize} bufferSize={bufferSize} (0x{status:x8}).
 308                bufferSize = GetEstimatedBufferSize(actualSize);
 309            }
 310        }
 311
 312        // allocating a few more kilo bytes just in case there are some new processes since the last call
 313        static uint GetEstimatedBufferSize(uint actualSize) => actualSize + 1024 * 10;
 314    }
 315
 316    private static unsafe Dictionary<(int, DateTime), T> HandleProcesses<T, TData>(
 317        ReadOnlySpan<byte> current,
 318        HashSet<int>? processIds,
 319        TData? data,
 320        Func<TData?, int, SYSTEM_PROCESS_INFORMATION, ulong?, T?> newEntry)
 321    {
 322        var processInfos = new Dictionary<(int, DateTime), T>();
 323        int processInformationOffset = 0;
 324        int count = 0;
 325
 326        while (true)
 327        {
 328            ref readonly var pi = ref MemoryMarshal.AsRef<SYSTEM_PROCESS_INFORMATION>(current.Slice(processInformationOf
 329            int pid = pi.UniqueProcessId.ToInt32();
 330
 331            ulong? seq = null;
 332            if (SupportsProcessSequenceNumber)
 333            {
 334                ref readonly var pix = ref MemoryMarshal.AsRef<SYSTEM_PROCESS_INFORMATION_EXTENSION>(current.Slice(proce
 335                seq = pix.ProcessSequenceNumber;
 336            }
 337
 338            if (processIds == null || processIds.Contains(pid))
 339            {
 340                var entry = newEntry(data, count, pi, seq);
 341                if (entry != null)
 342                {
 343                    processInfos.Add((pid, DateTime.FromFileTime(pi.CreateTime)), entry);
 344                }
 345            }
 346
 347            if (pi.NextEntryOffset == 0)
 348            {
 349                break;
 350            }
 351            processInformationOffset += (int)pi.NextEntryOffset;
 352            count++;
 353        }
 354
 355        return processInfos;
 356    }
 357
 358#else
 359    //
 360    // This implementation with based on .NET Frameworks Process class. It does not provide all the features as the
 361    // .NET version. For example, it does not access SYSTEM_PROCESS_INFORMATION_EXTENSION to get the ProcessSequenceNumb
 362    //
 363
 364    private static long[]? s_cachedBuffer;
 365
 366    internal static Dictionary<(int, DateTime), T> EnumerateSystemProcesses<T, TData>(
 367        HashSet<int>? processIds,
 368        TData? data,
 369        Func<TData?, int, SYSTEM_PROCESS_INFORMATION, ulong?, T?> newEntry)
 370    {
 2371        var processInfos = new Dictionary<(int, DateTime), T>();
 2372        var bufferHandle = new GCHandle();
 373
 374        // Start with the default buffer size (smaller in DEBUG to make sure retry path is hit)
 2375        int bufferSize = (int)GetDefaultCachedBufferSize();
 376
 377        // Get the cached buffer.
 2378        long[]? buffer = Interlocked.Exchange(ref s_cachedBuffer, null);
 379
 380        try
 381        {
 382            // Retry until we get all the data
 383            int status;
 384            do
 385            {
 2386                if (buffer == null)
 387                {
 388                    // Allocate buffer of longs since some platforms require the buffer to be 64-bit aligned.
 2389                    buffer = new long[(bufferSize + 7) / 8];
 390                }
 391                else
 392                {
 393                    // If we have cached buffer, set the size properly.
 2394                    bufferSize = buffer.Length * sizeof(long);
 395                }
 2396                bufferHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
 397
 2398                status = NtQuerySystemInformation(
 2399                    SYSTEM_INFORMATION_CLASS.SystemProcessInformation,
 2400                    bufferHandle.AddrOfPinnedObject(),
 2401                    bufferSize,
 2402                    out int requiredSize);
 403
 2404                if ((uint)status == STATUS_INFO_LENGTH_MISMATCH)
 405                {
 0406                    if (bufferHandle.IsAllocated)
 407                    {
 0408                        bufferHandle.Free();
 409                    }
 410
 0411                    buffer = null;
 0412                    bufferSize = GetNewBufferSize(bufferSize, requiredSize);
 413                }
 2414            } while ((uint)status == STATUS_INFO_LENGTH_MISMATCH);
 415
 2416            if (status < 0)
 417            {
 0418                throw GetException((uint)status);
 419            }
 420
 421            // Parse the data block to get process information
 2422            IntPtr dataPtr = bufferHandle.AddrOfPinnedObject();
 423
 2424            long totalOffset = 0;
 2425            int count = 0;
 426
 2427            while (true)
 428            {
 2429                nint currentPtr = checked((IntPtr)(dataPtr.ToInt64() + totalOffset));
 2430                var pi = Marshal.PtrToStructure<SYSTEM_PROCESS_INFORMATION>(currentPtr);
 431
 2432                int pid = pi.UniqueProcessId.ToInt32();
 2433                if (processIds == null || processIds.Contains(pid))
 434                {
 2435                    var startTime = DateTime.FromFileTime(pi.CreateTime);
 2436                    var entry = newEntry(data, count, pi, null);
 2437                    if (entry != null)
 438                    {
 2439                        processInfos.Add((pid, startTime), entry);
 440                    }
 441                }
 442
 2443                if (pi.NextEntryOffset == 0)
 444                {
 2445                    break;
 446                }
 2447                totalOffset += pi.NextEntryOffset;
 2448                count++;
 449            }
 450        }
 451        finally
 452        {
 453            // Cache the final buffer for use on the next call.
 2454            Interlocked.Exchange(ref s_cachedBuffer, buffer);
 455
 2456            if (bufferHandle.IsAllocated)
 457            {
 2458                bufferHandle.Free();
 459            }
 2460        }
 461
 2462        return processInfos;
 463
 464        static int GetNewBufferSize(int existingBufferSize, int requiredSize)
 465        {
 0466            if (requiredSize == 0)
 467            {
 468                //
 469                // On some old OS like win2000, requiredSize will not be set if the buffer
 470                // passed to NtQuerySystemInformation is not enough.
 471                //
 0472                int newSize = existingBufferSize * 2;
 0473                if (newSize < existingBufferSize)
 474                {
 0475                    throw new OutOfMemoryException($"Existing buffer size {existingBufferSize:N0} bytes, attempting to a
 476                }
 0477                return newSize;
 478            }
 479            else
 480            {
 481                // allocating a few more kilo bytes just in case there are some new process
 482                // kicked in since new call to NtQuerySystemInformation
 0483                int newSize = requiredSize + (1024 * 10);
 0484                if (newSize < requiredSize)
 485                {
 0486                    throw new OutOfMemoryException($"Required buffer size {requiredSize:N0} bytes, attempting to allocat
 487                }
 0488                return newSize;
 489            }
 490        }
 491    }
 492#endif
 493}