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)
{