diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt new file mode 100644 index 0000000..c78971d --- /dev/null +++ b/THIRD-PARTY-NOTICES.txt @@ -0,0 +1,58 @@ +LockCheck uses third-party material as listed below. + +The attached notices are provided for informational purposes only. + + +License for .NET runtime (https://github.com/dotnet/runtime) +------------------------------------------------------------ + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +License for .NET Framework Reference Source (https://github.com/microsoft/referencesource) +------------------------------------------------------------------------------------------ + +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/src/LockCheck/IHasErrorState.cs b/src/LockCheck/IHasErrorState.cs index a0fe4e6..8b6d449 100644 --- a/src/LockCheck/IHasErrorState.cs +++ b/src/LockCheck/IHasErrorState.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace LockCheck { @@ -8,14 +8,19 @@ namespace LockCheck public interface IHasErrorState { /// - /// Get a value that indicates if the entity is errnous. + /// Get a value that indicates if the entity is erroneous. /// bool HasError { get; } /// - /// Set the error state of the entity.. + /// Set the error state of the entity. /// + /// + /// Implementations shall only consider the first call to this method, setting + /// to true. Further calls to this function shall be ignored. + /// /// An optional exception that caused the error. - void SetError(Exception ex = null); + /// An option error code that describes the error. + void SetError(Exception ex = null, int errorCode = 0); } } diff --git a/src/LockCheck/Linux/NativeMethods.cs b/src/LockCheck/Linux/NativeMethods.cs index 5bf5b42..ee4a8c7 100644 --- a/src/LockCheck/Linux/NativeMethods.cs +++ b/src/LockCheck/Linux/NativeMethods.cs @@ -1,9 +1,6 @@ using System; -using System.Buffers; using System.IO; -using System.Reflection.Metadata; using System.Runtime.InteropServices; -using System.Text; #pragma warning disable IDE1006 // Naming Styles - off here, because we want to use native names @@ -91,9 +88,6 @@ private static unsafe bool TryGetUserName(uint uid, byte* buf, int bufLen, out s throw new IOException($"Couldn't get user name for '{uid}' (errno = {error})"); } - // Copied (and modified) from github.com/dotnet/runtime, MIT licensed. - // ---BEGIN--------------------------------------------------------------------------------- - private const string SystemNative = "System.Native"; [StructLayout(LayoutKind.Sequential)] @@ -136,63 +130,5 @@ internal unsafe struct Passwd [LibraryImport(SystemNative, EntryPoint = "SystemNative_GetPwUidR", SetLastError = false)] internal static unsafe partial int GetPwUidR(uint uid, out Passwd pwd, byte* buf, int bufLen); - - [LibraryImport(SystemNative, EntryPoint = "SystemNative_ReadLink", SetLastError = true)] - private static partial int ReadLink(ref byte path, ref byte buffer, int bufferSize); - - internal static string ReadLink(ReadOnlySpan path) - { - const int StackBufferSize = 256; - - Span spanBuffer = stackalloc byte[StackBufferSize]; - byte[] arrayBuffer = null; - - // Convert path to UTF-8 bytes, zero terminated - CLR code used internal ValueUtf8Converter for this. - int maxSize = checked(Encoding.UTF8.GetMaxByteCount(path.Length) + 1); - Span pathBytes = maxSize <= StackBufferSize ? stackalloc byte[maxSize] : new byte[maxSize]; - int pathBytesCount = Encoding.UTF8.GetBytes(path, pathBytes); - pathBytes[pathBytesCount] = 0; - pathBytes = pathBytes.Slice(0, pathBytesCount + 1); - ref byte pathReference = ref MemoryMarshal.GetReference(pathBytes); - - while (true) - { - int error = 0; - try - { - int resultLength = ReadLink(ref pathReference, ref MemoryMarshal.GetReference(spanBuffer), spanBuffer.Length); - - if (resultLength < 0) - { - // error - error = Marshal.GetLastPInvokeError(); - return null; - } - else if (resultLength < spanBuffer.Length) - { - // success - return Encoding.UTF8.GetString(spanBuffer.Slice(0, resultLength)); - } - } - finally - { - if (arrayBuffer != null) - { - ArrayPool.Shared.Return(arrayBuffer); - } - - if (error > 0) - { - Marshal.SetLastPInvokeError(error); - } - } - - // Output buffer was too small, loop around again and try with a larger buffer. - arrayBuffer = ArrayPool.Shared.Rent(spanBuffer.Length * 2); - spanBuffer = arrayBuffer; - } - } - - // ---END----------------------------------------------------------------------------------- } } diff --git a/src/LockCheck/Linux/ProcFileSystem.cs b/src/LockCheck/Linux/ProcFileSystem.cs index 0cb19ae..0683d54 100644 --- a/src/LockCheck/Linux/ProcFileSystem.cs +++ b/src/LockCheck/Linux/ProcFileSystem.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; -using System.IO.Pipes; +using System.Linq; using System.Text; namespace LockCheck.Linux @@ -133,7 +133,7 @@ internal static bool ProcMatchesPidNamespace { get { - // _procMatchesPidNamespace is set to: + // s_procMatchesPidNamespace is set to: // - 0: when uninitialized, // - 1: '/proc' and the process pid namespace match, // - 2: when they don't match. @@ -143,8 +143,9 @@ internal static bool ProcMatchesPidNamespace // We compare it with the pid of the current process to see if the '/proc' and pid namespace match up. int? procSelfPid = null; - if (NativeMethods.ReadLink("/proc/self") is string target && - int.TryParse(target, out int pid)) + + if (Directory.ResolveLinkTarget("/proc/self", false)?.FullName is string target && + int.TryParse(Path.GetFileName(target), out int pid)) { procSelfPid = pid; } @@ -185,11 +186,11 @@ internal static bool TryGetProcPid(int pid, out ProcPid procPid) return false; } - private static string GetProcCmdline(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/cmdline" : string.Create(null, stackalloc char[256], $"/proc/{(int)procPid}/cmdline"); - private static string GetProcExe(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/exe" : string.Create(null, stackalloc char[256], $"/proc/{(int)procPid}/exe"); - private static string GetProcCwd(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/cwd" : string.Create(null, stackalloc char[256], $"/proc/{(int)procPid}/cwd"); - private static string GetProcStat(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/stat" : string.Create(null, stackalloc char[256], $"/proc/{(int)procPid}/stat"); - private static string GetProcDir(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self" : string.Create(null, stackalloc char[256], $"/proc/{(int)procPid}"); + private static string GetProcCmdline(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/cmdline" : string.Create(null, stackalloc char[128], $"/proc/{(int)procPid}/cmdline"); + private static string GetProcExe(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/exe" : string.Create(null, stackalloc char[128], $"/proc/{(int)procPid}/exe"); + private static string GetProcCwd(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/cwd" : string.Create(null, stackalloc char[128], $"/proc/{(int)procPid}/cwd"); + private static string GetProcStat(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/stat" : string.Create(null, stackalloc char[128], $"/proc/{(int)procPid}/stat"); + private static string GetProcDir(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self" : string.Create(null, stackalloc char[128], $"/proc/{(int)procPid}"); internal static bool Exists(int processId) => TryGetProcPid(processId, out var procPid) && Directory.Exists(GetProcDir(procPid)); @@ -213,17 +214,21 @@ internal static string GetProcessOwner(int processId) internal static DateTime GetProcessStartTime(int processId) { - if (TryGetProcPid(processId, out _)) + if (TryGetProcPid(processId, out ProcPid procPid)) { - // Apparently it is impossible to fully recreate the time that Process.StartTime calculates in - // the background. It uses clock_gettime(CLOCK_BOOTTIME) (see https://github.com/dotnet/runtime/pull/83966) - // internally and calculates the start time relative to that using /proc//stat. - // However we shave the yack, we get a different time than what Process.StartTime would return. - // Debugging has it, that this is due to the fact that we get a different "boot time" (later time). - // I'm not sure if that is a bug in the CLR or just a fact of life on Linux. - // In any case, we need to get the exact same Timestamp for our hash keys to work properly. - using var process = Process.GetProcessById(processId); - return process.StartTime; + // Apparently it is currently impossible to fully recreate the time that Process.StartTime is. + // Also see https://github.com/dotnet/runtime/issues/108959. + + if (procPid == ProcPid.Self) + { + using var process = Process.GetCurrentProcess(); + return process.StartTime; + } + else + { + using var process = Process.GetProcessById(processId); + return process.StartTime; + } } return default; @@ -254,7 +259,7 @@ internal static string GetProcessCurrentDirectory(int processId) return null; } - internal static string[] GetProcessCommandLineArgs(int processId) + internal static string[] GetProcessCommandLineArgs(int processId, int maxArgs = -1) { if (TryGetProcPid(processId, out ProcPid procPid)) { @@ -295,7 +300,7 @@ internal static string[] GetProcessCommandLineArgs(int processId) } } - return ConvertToArgs(ref buffer); + return ConvertToArgs(ref buffer, maxArgs); } } catch (IOException) @@ -313,9 +318,9 @@ internal static string[] GetProcessCommandLineArgs(int processId) return null; } - internal static string[] ConvertToArgs(ref Span buffer) + internal static string[] ConvertToArgs(ref Span buffer, int maxArgs = -1) { - if (buffer.IsEmpty) + if (buffer.IsEmpty || maxArgs == 0) { return []; } @@ -333,7 +338,8 @@ internal static string[] ConvertToArgs(ref Span buffer) } // Individual argv elements in the buffer are separated by a null byte. - int count = buffer.Count((byte)'\0') + 1; + int actual = buffer.Count((byte)'\0') + 1; + int count = maxArgs > 0 ? Math.Min(maxArgs, actual) : actual; string[] args = new string[count]; int start = 0; int p = 0; @@ -343,6 +349,11 @@ internal static string[] ConvertToArgs(ref Span buffer) { args[p++] = Encoding.UTF8.GetString(buffer.Slice(start, i - start)); start = i + 1; + + if (maxArgs > 0 && maxArgs == p) + { + return args; + } } } @@ -366,64 +377,11 @@ internal static string GetProcessExecutablePath(int processId) internal static string GetProcessExecutablePathFromCmdLine(int processId) { - if (TryGetProcPid(processId, out ProcPid procPid)) - { - byte[] rentedBuffer = null; - try - { - using (var file = new FileStream(GetProcCmdline(procPid), FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1, useAsync: false)) - { - Span buffer = stackalloc byte[256]; - int bytesRead = 0; - while (true) - { - if (bytesRead == buffer.Length) - { - // Resize buffer - uint newLength = (uint)buffer.Length * 2; - // Store what was read into new buffer - byte[] tmp = ArrayPool.Shared.Rent((int)newLength); - buffer.CopyTo(tmp); - // Remember current "rented" buffer (might be null) - byte[] lastRentedBuffer = rentedBuffer; - // From now on, we did rent a buffer. And it will be used for further reads. - buffer = tmp; - rentedBuffer = tmp; - // Return previously rented buffer, if any. - if (lastRentedBuffer != null) - { - ArrayPool.Shared.Return(lastRentedBuffer); - } - } - - Debug.Assert(bytesRead < buffer.Length); - int n = file.Read(buffer.Slice(bytesRead)); - bytesRead += n; - - // "/proc//cmdline" contains the original argument vector (argv), where each part is separated by a null byte. - // See if we have read enough for argv[0], which contains the process name. - ReadOnlySpan argRemainder = buffer.Slice(0, bytesRead); - int argEnd = argRemainder.IndexOf((byte)'\0'); - if (argEnd != -1) - { - return Encoding.UTF8.GetString(argRemainder.Slice(0, argEnd)); - } - } - } - } - catch (IOException) - { - } - finally - { - if (rentedBuffer != null) - { - ArrayPool.Shared.Return(rentedBuffer); - } - } - } - - return null; + // This is a little more expensive than a specific function only reading up to argv[0] from /proc//cmdline + // would be - GetProcessCommandLineArgs() reads all arguments, but then only converts "maxArgs" of them to an + // actual System.String. On the other hand it saves quite some code duplication. + string[] args = GetProcessCommandLineArgs(processId, maxArgs: 1); + return args?.Length > 0 ? args[0] : null; } private static ReadOnlySpan GetField(ReadOnlySpan content, char delimiter, int index) diff --git a/src/LockCheck/Linux/ProcInfo.cs b/src/LockCheck/Linux/ProcInfo.cs index 0aa6bc6..6486732 100644 --- a/src/LockCheck/Linux/ProcInfo.cs +++ b/src/LockCheck/Linux/ProcInfo.cs @@ -11,6 +11,7 @@ internal class ProcInfo : IHasErrorState #pragma warning disable IDE0052 private string _errorStack; private Exception _errorCause; + private int _errorCode; #pragma warning restore IDE0052 #endif @@ -23,7 +24,7 @@ internal class ProcInfo : IHasErrorState public DateTime StartTime { get; private set; } public bool HasError { get; private set; } - public void SetError(Exception ex = null) + public void SetError(Exception ex = null, int errorCode = 0) { if (!HasError) { @@ -34,6 +35,7 @@ public void SetError(Exception ex = null) // Support manual inspection at a later point _errorStack = Environment.StackTrace; _errorCause = ex; + _errorCode = errorCode; } #endif } @@ -85,7 +87,7 @@ private static string GetProcessOwner(int pid) return null; } - private static string GetCommandLine(int pid, IHasErrorState he) + private static string GetCommandLine(int pid, ProcInfo he) { try { @@ -111,7 +113,7 @@ private static string GetCommandLine(int pid, IHasErrorState he) } } - private static string GetCurrentDirectory(int pid, IHasErrorState he) + private static string GetCurrentDirectory(int pid, ProcInfo he) { try { @@ -135,7 +137,7 @@ private static string GetCurrentDirectory(int pid, IHasErrorState he) } } - private static string GetExecutablePath(int pid, IHasErrorState he) + private static string GetExecutablePath(int pid, ProcInfo he) { try { @@ -165,7 +167,7 @@ private static string GetExecutablePath(int pid, IHasErrorState he) } } - private static int GetSessionId(int pid, IHasErrorState he) + private static int GetSessionId(int pid, ProcInfo he) { try { @@ -190,7 +192,7 @@ private static int GetSessionId(int pid, IHasErrorState he) } } - private static unsafe DateTime GetStartTime(int pid, IHasErrorState he) + private static unsafe DateTime GetStartTime(int pid, ProcInfo he) { try { diff --git a/src/LockCheck/Windows/NativeMethods.cs b/src/LockCheck/Windows/NativeMethods.cs index c1bc14e..da8ae6e 100644 --- a/src/LockCheck/Windows/NativeMethods.cs +++ b/src/LockCheck/Windows/NativeMethods.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security.Principal; using System.Text; @@ -110,7 +111,7 @@ internal static extern int NtQueryInformationProcessWow64(SafeProcessHandle hPro #if NET [LibraryImport(NtDll)] - internal static partial int NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS systemInformationClass, IntPtr dataPtr, int size, out int returnedSize); + internal static unsafe partial uint NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS systemInformationClass, void* dataPtr, uint size, uint* returnedSize); #else [DllImport(NtDll)] internal static extern int NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS systemInformationClass, IntPtr dataPtr, int size, out int returnedSize); @@ -506,7 +507,7 @@ public static PebOffsets Get(bool target64) // native struct defined in ntexapi.h [StructLayout(LayoutKind.Sequential)] - internal class SYSTEM_PROCESS_INFORMATION + internal struct SYSTEM_PROCESS_INFORMATION { internal uint NextEntryOffset; internal uint NumberOfThreads; diff --git a/src/LockCheck/Windows/NtDll.cs b/src/LockCheck/Windows/NtDll.cs index 33a29ba..7e54a60 100644 --- a/src/LockCheck/Windows/NtDll.cs +++ b/src/LockCheck/Windows/NtDll.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Threading; @@ -110,7 +111,7 @@ internal static Win32Exception GetException(uint status) internal static Dictionary<(int, DateTime), ProcessInfo> GetProcessesByWorkingDirectory(List directories) { return EnumerateSystemProcesses(null, directories, - static (dirs, currentPtr, idx, pi) => + static (dirs, idx, pi) => { var peb = new Peb(pi); @@ -130,18 +131,127 @@ internal static Win32Exception GetException(uint status) }); } + // Use a smaller buffer size on debug to ensure we hit the retry path. + private static uint GetDefaultCachedBufferSize() => 1024 * +#if DEBUG + 8; +#else + 1024; +#endif + +#if NET + // + // This implementation with based on dotnet/runtime ProcessManager.Win32.cs does. + // Basically, it doesn't hold on to a "cached buffer" and uses more modern constructs + // which results in "simpler" code. Especially, it does not use GCHandle and also + // doesn't have workarounds for "older" versions of Windows anymore. + // + + private static uint s_mostRecentSize = GetDefaultCachedBufferSize(); + + internal static unsafe Dictionary<(int, DateTime), T> EnumerateSystemProcesses( + HashSet processIds, + TData data, + Func newEntry) + { + // Start with the default buffer size. + uint bufferSize = s_mostRecentSize; + + while (true) + { + // some platforms require the buffer to be 64-bit aligned and NativeLibrary.Alloc guarantees sufficient alignment. + void* bufferPtr = NativeMemory.Alloc(bufferSize); + + try + { + uint actualSize = 0; + uint status = NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS.SystemProcessInformation, bufferPtr, bufferSize, &actualSize); + + if (status != STATUS_INFO_LENGTH_MISMATCH) + { + // see definition of NT_SUCCESS(Status) in SDK + if ((int)status < 0) + { + throw GetException(status); + } + + // Remember last buffer size for next attempt. Note that this may also result in smaller + // buffer sizes for further attempts, as the live processes can also decrease in comparison + // to a previous call. + Debug.Assert(actualSize > 0 && actualSize <= bufferSize, $"actualSize={actualSize} bufferSize={bufferSize} (0x{status:x8})."); + s_mostRecentSize = GetEstimatedBufferSize(actualSize); + + return HandleProcesses(new ReadOnlySpan(bufferPtr, (int)actualSize), processIds, data, newEntry); + } + else + { + // Buffer was too small; retry with a larger buffer. + Debug.Assert(actualSize > bufferSize, $"actualSize={actualSize} bufferSize={bufferSize} (0x{status:x8})."); + bufferSize = GetEstimatedBufferSize(actualSize); + } + } + finally + { + NativeMemory.Free(bufferPtr); + } + } + + // allocating a few more kilo bytes just in case there are some new processes since the last call + static uint GetEstimatedBufferSize(uint actualSize) => actualSize + 1024 * 10; + } + + private static unsafe Dictionary<(int, DateTime), T> HandleProcesses( + ReadOnlySpan current, + HashSet processIds, + TData data, + Func newEntry) + { + var processInfos = new Dictionary<(int, DateTime), T>(); + int processInformationOffset = 0; + int count = 0; + + while (true) + { + ref readonly var pi = ref MemoryMarshal.AsRef(current.Slice(processInformationOffset)); + + int pid = pi.UniqueProcessId.ToInt32(); + if (processIds == null || processIds.Contains(pid)) + { + var entry = newEntry(data, count, pi); + if (entry != null) + { + processInfos.Add((pid, DateTime.FromFileTime(pi.CreateTime)), entry); + } + } + + if (pi.NextEntryOffset == 0) + { + break; + } + processInformationOffset += (int)pi.NextEntryOffset; + count++; + } + + return processInfos; + } + +#else + // + // This implementation with based on .NET Frameworks Process class. + // + private static long[] s_cachedBuffer; internal static Dictionary<(int, DateTime), T> EnumerateSystemProcesses( HashSet processIds, TData data, - Func newEntry) + Func newEntry) { var processInfos = new Dictionary<(int, DateTime), T>(); var bufferHandle = new GCHandle(); - // Start with the default buffer size. - int bufferSize = 4096 * 128; + // Start with the default buffer size (smaller in DEBUG to make sure retry path is hit) + int bufferSize = (int)GetDefaultCachedBufferSize(); // Get the cached buffer. long[] buffer = Interlocked.Exchange(ref s_cachedBuffer, null); @@ -196,15 +306,13 @@ internal static Win32Exception GetException(uint status) while (true) { nint currentPtr = checked((IntPtr)(dataPtr.ToInt64() + totalOffset)); - var pi = new SYSTEM_PROCESS_INFORMATION(); - - Marshal.PtrToStructure(currentPtr, pi); + var pi = Marshal.PtrToStructure(currentPtr); int pid = pi.UniqueProcessId.ToInt32(); if (processIds == null || processIds.Contains(pid)) { var startTime = DateTime.FromFileTime(pi.CreateTime); - var entry = newEntry(data, currentPtr, count, pi); + var entry = newEntry(data, count, pi); if (entry != null) { processInfos.Add((pid, startTime), entry); @@ -260,5 +368,6 @@ static int GetNewBufferSize(int existingBufferSize, int requiredSize) } } } +#endif } } diff --git a/src/LockCheck/Windows/Peb.cs b/src/LockCheck/Windows/Peb.cs index 110d568..5053a39 100644 --- a/src/LockCheck/Windows/Peb.cs +++ b/src/LockCheck/Windows/Peb.cs @@ -15,6 +15,7 @@ internal class Peb : IHasErrorState #pragma warning disable IDE0052 private string _errorStack; private Exception _errorCause; + private int _errorCode; #pragma warning restore IDE0052 #endif @@ -29,7 +30,7 @@ internal class Peb : IHasErrorState public DateTime StartTime { get; private set; } public bool HasError { get; private set; } - public void SetError(Exception ex = null) + public void SetError(Exception ex = null, int errorCode = 0) { if (!HasError) { @@ -40,6 +41,7 @@ public void SetError(Exception ex = null) // Support manual inspection at a later point _errorStack = Environment.StackTrace; _errorCause = ex; + _errorCode = errorCode; } #endif } @@ -47,13 +49,15 @@ public void SetError(Exception ex = null) internal Peb(SYSTEM_PROCESS_INFORMATION pi) { - ProcessId = (int)pi.UniqueProcessId; + // Convert as many members as possible, without actually needing to open the process handle. + // Also, ProcessId/StartTime serve as identity. So it is "useful" to have them. + ProcessId = pi.UniqueProcessId.ToInt32(); + StartTime = DateTime.FromFileTime(pi.CreateTime); using var process = OpenProcessRead(ProcessId); - if (process.IsInvalid) + if (!SUCCEEDED(!process.IsInvalid, this)) { - SetError(); return; } @@ -66,9 +70,8 @@ internal Peb(SYSTEM_PROCESS_INFORMATION pi) if (os64) { - if (!IsWow64Process(process, out bool isWow64Target)) + if (!SUCCEEDED(IsWow64Process(process, out bool isWow64Target), this)) { - SetError(); return; } @@ -111,9 +114,6 @@ internal Peb(SYSTEM_PROCESS_INFORMATION pi) // Owner is not really part of the native PEB, but since we have the process handle // here anyway, and going to need this value later on, we get it here as well. Owner = GetProcessOwner(process); - - // Also not part of native PEB, but easy to get here and needed later on. - StartTime = DateTime.FromFileTime(pi.CreateTime); } private static void InitTargetAnySelfAny(SafeProcessHandle handle, PebOffsets offsets, Peb peb) @@ -179,7 +179,7 @@ private static void InitTarget32SelfAny(SafeProcessHandle handle, PebOffsets off } } - private static int GetInt32Target32(SafeProcessHandle handle, IntPtr pp, int offset, IHasErrorState he) + private static int GetInt32Target32(SafeProcessHandle handle, IntPtr pp, int offset, Peb he) { var ptr = IntPtr.Zero; if (SUCCEEDED(ReadProcessMemory(handle, pp + offset, ref ptr, new IntPtr(sizeof(int)), IntPtr.Zero), he)) @@ -187,11 +187,10 @@ private static int GetInt32Target32(SafeProcessHandle handle, IntPtr pp, int off return ptr.ToInt32(); } - he.SetError(); return default; } - private static string GetStringTarget32(SafeProcessHandle handle, IntPtr pp, int offset, IHasErrorState he) + private static string GetStringTarget32(SafeProcessHandle handle, IntPtr pp, int offset, Peb he) { var us = new UNICODE_STRING_32(); if (SUCCEEDED(ReadProcessMemory(handle, pp + offset, ref us, new IntPtr(Marshal.SizeOf(us)), IntPtr.Zero), he)) @@ -211,11 +210,10 @@ private static string GetStringTarget32(SafeProcessHandle handle, IntPtr pp, int } } - he.SetError(); return null; } - private static int GetInt32Target64(SafeProcessHandle handle, long pp, int offset, IHasErrorState he) + private static int GetInt32Target64(SafeProcessHandle handle, long pp, int offset, Peb he) { var ptr = IntPtr.Zero; uint buf = 0; @@ -225,11 +223,10 @@ private static int GetInt32Target64(SafeProcessHandle handle, long pp, int offse return ptr.ToInt32(); } - he.SetError(); return default; } - private static string GetStringTarget64(SafeProcessHandle handle, long pp, int offset, IHasErrorState he) + private static string GetStringTarget64(SafeProcessHandle handle, long pp, int offset, Peb he) { var us = new UNICODE_STRING_WOW64(); if (SUCCEEDED(NtWow64ReadVirtualMemory64(handle, pp + offset, ref us, Marshal.SizeOf(us), IntPtr.Zero), he)) @@ -249,11 +246,10 @@ private static string GetStringTarget64(SafeProcessHandle handle, long pp, int o } } - he.SetError(); return null; } - private static int GetInt32(SafeProcessHandle handle, IntPtr pp, int offset, IHasErrorState he) + private static int GetInt32(SafeProcessHandle handle, IntPtr pp, int offset, Peb he) { var ptr = IntPtr.Zero; if (SUCCEEDED(ReadProcessMemory(handle, pp + offset, ref ptr, new IntPtr(IntPtr.Size), IntPtr.Zero), he)) @@ -261,11 +257,10 @@ private static int GetInt32(SafeProcessHandle handle, IntPtr pp, int offset, IHa return ptr.ToInt32(); } - he.SetError(); return 0; } - private static string GetString(SafeProcessHandle handle, IntPtr pp, int offset, IHasErrorState he) + private static string GetString(SafeProcessHandle handle, IntPtr pp, int offset, Peb he) { var us = new UNICODE_STRING(); if (SUCCEEDED(ReadProcessMemory(handle, pp + offset, ref us, new IntPtr(Marshal.SizeOf(us)), IntPtr.Zero), he)) @@ -285,37 +280,45 @@ private static string GetString(SafeProcessHandle handle, IntPtr pp, int offset, } } - he.SetError(); return null; } - private static bool SUCCEEDED(uint status, IHasErrorState he, [CallerMemberName] string callerName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = 0) + /// + /// Checks if is success, otherwise sets .SetError(errorCode: ). + /// + private static bool SUCCEEDED(uint status, Peb he, [CallerMemberName] string callerName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = 0) { if (status != STATUS_SUCCESS) { - he.SetError(); + he.SetError(errorCode: (int)status); return false; } return true; } - private static bool SUCCEEDED(int status, IHasErrorState he, [CallerMemberName] string callerName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = 0) + /// + /// Checks if is success, otherwise sets .SetError(errorCode: ). + /// + private static bool SUCCEEDED(int status, Peb he, [CallerMemberName] string callerName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = 0) { if (status != STATUS_SUCCESS) { - he.SetError(); + he.SetError(errorCode: status); return false; } return true; } - private static bool SUCCEEDED(bool result, IHasErrorState he, [CallerMemberName] string callerName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = 0) + /// + /// Checks if is success, otherwise sets .SetError(errorCode: ). + /// + private static bool SUCCEEDED(bool result, Peb he, [CallerMemberName] string callerName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = 0) { if (!result) { - he.SetError(); + he.SetError(errorCode: Marshal.GetLastWin32Error()); return false; } diff --git a/test/LockCheck.Tests/Linux/ProcFileSystemTests.cs b/test/LockCheck.Tests/Linux/ProcFileSystemTests.cs index c3bbb30..c248849 100644 --- a/test/LockCheck.Tests/Linux/ProcFileSystemTests.cs +++ b/test/LockCheck.Tests/Linux/ProcFileSystemTests.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Linq; using System.Text; using LockCheck.Linux; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -7,7 +8,7 @@ namespace LockCheck.Tests.Linux { [SupportedTestClassPlatform("linux")] - public class ProcFileSystemTests + public partial class ProcFileSystemTests { [TestMethod] public void GetProcessExecutablePathFromCmdLine_ShouldReturnExecutableName_WhenNoPermissionToProcess() @@ -33,10 +34,34 @@ public void GetProcessExecutablePathFromCmdLine_ShouldReturnNull_WhenProcessDoes [DataRow("/usr/bin/sh\0\0", new string[] { "/usr/bin/sh" })] [DataRow("/usr/bin/sh\0-c echo \"Hello World\"\0\0", new string[] { "/usr/bin/sh", "-c echo \"Hello World\"" })] [DataRow("/usr/bin/sh\0-c echo \"Hello World\"\0123\0\0", new string[] { "/usr/bin/sh", "-c echo \"Hello World\"", "123" })] - public void ConvertToArgs(string cmdline, string[] expected) + public void ConvertToArgs_ShouldReturnAllArgs_WhenMaxArgsIsNotSet(string cmdline, string[] expected) { var span = Encoding.UTF8.GetBytes(cmdline).AsSpan(); string[] args = ProcFileSystem.ConvertToArgs(ref span); + Assert.AreEqual(expected.Length, args.Length); + CollectionAssert.AreEqual(expected, args); + } + + [DataTestMethod] + [DataRow(0, "", new string[] { })] + [DataRow(1, "", new string[] { })] + [DataRow(1, "\0\0", new string[] { })] + [DataRow(1, "/\0\0", new string[] { "/" })] + [DataRow(0, "/usr/bin/sh\0\0", new string[] { })] + [DataRow(1, "/usr/bin/sh\0\0", new string[] { "/usr/bin/sh" })] + [DataRow(2, "/usr/bin/sh\0\0", new string[] { "/usr/bin/sh" })] + [DataRow(1, "/usr/bin/sh\0-c echo \"Hello World\"\0\0", new string[] { "/usr/bin/sh" })] + [DataRow(2, "/usr/bin/sh\0-c echo \"Hello World\"\0\0", new string[] { "/usr/bin/sh", "-c echo \"Hello World\"" })] + [DataRow(3, "/usr/bin/sh\0-c echo \"Hello World\"\0\0", new string[] { "/usr/bin/sh", "-c echo \"Hello World\"" })] + [DataRow(1, "/usr/bin/sh\0-c echo \"Hello World\"\0123\0\0", new string[] { "/usr/bin/sh" })] + [DataRow(2, "/usr/bin/sh\0-c echo \"Hello World\"\0123\0\0", new string[] { "/usr/bin/sh", "-c echo \"Hello World\"" })] + [DataRow(3, "/usr/bin/sh\0-c echo \"Hello World\"\0123\0\0", new string[] { "/usr/bin/sh", "-c echo \"Hello World\"", "123" })] + [DataRow(4, "/usr/bin/sh\0-c echo \"Hello World\"\0123\0\0", new string[] { "/usr/bin/sh", "-c echo \"Hello World\"", "123" })] + public void ConvertToArgs_ShouldReturnMaxArgs_WhenMaxArgsIsSet(int maxArgs, string cmdline, string[] expected) + { + var span = Encoding.UTF8.GetBytes(cmdline).AsSpan(); + string[] args = ProcFileSystem.ConvertToArgs(ref span, maxArgs); + Assert.AreEqual(expected.Length, args.Length); CollectionAssert.AreEqual(expected, args); } } diff --git a/test/LockCheck.Tests/Windows/NtDllTests.cs b/test/LockCheck.Tests/Windows/NtDllTests.cs index 64f9494..e0a5be9 100644 --- a/test/LockCheck.Tests/Windows/NtDllTests.cs +++ b/test/LockCheck.Tests/Windows/NtDllTests.cs @@ -79,7 +79,7 @@ public void EnumerateSystemProcesses_ShouldContainOnlySpecifiedProcess_WhenFilte using var self = Process.GetCurrentProcess(); bool found = false; int count = 0; - var result = NtDll.EnumerateSystemProcesses([self.Id], self.Id, (mp, currentPtr, idx, pi) => + var result = NtDll.EnumerateSystemProcesses([self.Id], self.Id, (mp, idx, pi) => { if ((int)pi.UniqueProcessId == mp) { @@ -100,7 +100,7 @@ public void EnumerateSystemProcesses_ShouldContainAllProcesses_WhenNoProcessFilt using var self = Process.GetCurrentProcess(); bool found = false; int count = 0; - var result = NtDll.EnumerateSystemProcesses(null, self.Id, (mp, currentPtr, idx, pi) => + var result = NtDll.EnumerateSystemProcesses(null, self.Id, (mp, idx, pi) => { if ((int)pi.UniqueProcessId == mp) {