diff --git a/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.ParseMapModules.cs b/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.ParseMapModules.cs index e622a9df0a456..c9b308fbabbc7 100644 --- a/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.ParseMapModules.cs +++ b/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.ParseMapModules.cs @@ -15,9 +15,11 @@ internal static partial class @procfs { private const string MapsFileName = "/maps"; - private static string GetMapsFilePathForProcess(int pid) => string.Create(null, stackalloc char[256], $"{RootPath}{(uint)pid}{MapsFileName}"); + private static string GetMapsFilePathForProcess(ProcPid pid) => + pid == ProcPid.Self ? $"{RootPath}{Self}{MapsFileName}" : + string.Create(null, stackalloc char[256], $"{RootPath}{(uint)pid}{MapsFileName}"); - internal static ProcessModuleCollection? ParseMapsModules(int pid) + internal static ProcessModuleCollection? ParseMapsModules(ProcPid pid) { try { diff --git a/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.TryReadStatusFile.cs b/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.TryReadStatusFile.cs index 7663b4ab15d39..7ffdbb4d7d2e6 100644 --- a/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.TryReadStatusFile.cs +++ b/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.TryReadStatusFile.cs @@ -15,8 +15,28 @@ internal static partial class Interop internal static partial class @procfs { internal const string RootPath = "/proc/"; + internal const string Self = "self"; private const string StatusFileName = "/status"; + // Normally the '/proc' filesystem uses the same pid namespace as the process. + // With rootless containers, it may happen that these pid namespaces do not match + // because the container doesn't have permissions to change '/proc' but it can + // create a new pid namespace. + // + // When that happens, the numeric ids used by the '/proc' filesystem no longer match with + // the process pid namespace. We can still access information for the current process + // using '/proc/self'. For other processes, we can't map pids to the proc pids so we musn't + // use '/proc' as that would return information for non-existing/wrong/inaccessible processes. + // + // The 'ProcPid' type represents a pid used by the '/proc' filesystem. + // This type provides a type-safe way to distingish the proc pids from the process pid namespace pids, + // which are passed as regular 'int's. + internal enum ProcPid : int + { + Invalid = -1, + Self = 0, // Information for the current process, accessible through '/proc/self'. + } + internal struct ParsedStatus { #if DEBUG @@ -30,13 +50,15 @@ internal struct ParsedStatus internal ulong VmPeak; } - internal static string GetStatusFilePathForProcess(int pid) => string.Create(null, stackalloc char[256], $"{RootPath}{(uint)pid}{StatusFileName}"); + internal static string GetStatusFilePathForProcess(ProcPid pid) => + pid == ProcPid.Self ? $"{RootPath}{Self}{StatusFileName}" : + string.Create(null, stackalloc char[256], $"{RootPath}{(uint)pid}{StatusFileName}"); - internal static bool TryReadStatusFile(int pid, out ParsedStatus result) + internal static bool TryReadStatusFile(ProcPid pid, out ParsedStatus result) { bool b = TryParseStatusFile(GetStatusFilePathForProcess(pid), out result); #if DEBUG - Debug.Assert(!b || result.Pid == pid, "Expected process ID from status file to match supplied pid"); + Debug.Assert(!b || (ProcPid)result.Pid == pid || pid == ProcPid.Self, "Expected process ID from status file to match supplied pid"); #endif return b; } diff --git a/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.cs b/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.cs index ee9c2b7597188..7c2ff54520bd4 100644 --- a/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.cs +++ b/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.cs @@ -72,26 +72,38 @@ internal struct ParsedStat //internal long cguest_time; } - internal static string GetExeFilePathForProcess(int pid) => string.Create(null, stackalloc char[256], $"{RootPath}{(uint)pid}{ExeFileName}"); + internal static string GetExeFilePathForProcess(ProcPid pid) => + pid == ProcPid.Self ? $"{RootPath}{Self}{ExeFileName}" : + string.Create(null, stackalloc char[256], $"{RootPath}{(uint)pid}{ExeFileName}"); - internal static string GetCmdLinePathForProcess(int pid) => string.Create(null, stackalloc char[256], $"{RootPath}{(uint)pid}{CmdLineFileName}"); + internal static string GetCmdLinePathForProcess(ProcPid pid) => + pid == ProcPid.Self ? $"{RootPath}{Self}{CmdLineFileName}" : + string.Create(null, stackalloc char[256], $"{RootPath}{(uint)pid}{CmdLineFileName}"); - internal static string GetStatFilePathForProcess(int pid) => string.Create(null, stackalloc char[256], $"{RootPath}{(uint)pid}{StatFileName}"); + internal static string GetStatFilePathForProcess(ProcPid pid) => + pid == ProcPid.Self ? $"{RootPath}{Self}{StatFileName}" : + string.Create(null, stackalloc char[256], $"{RootPath}{(uint)pid}{StatFileName}"); - internal static string GetTaskDirectoryPathForProcess(int pid) => string.Create(null, stackalloc char[256], $"{RootPath}{(uint)pid}{TaskDirectoryName}"); + internal static string GetTaskDirectoryPathForProcess(ProcPid pid) => + pid == ProcPid.Self ? $"{RootPath}{Self}{TaskDirectoryName}" : + string.Create(null, stackalloc char[256], $"{RootPath}{(uint)pid}{TaskDirectoryName}"); - internal static string GetFileDescriptorDirectoryPathForProcess(int pid) => string.Create(null, stackalloc char[256], $"{RootPath}{(uint)pid}{FileDescriptorDirectoryName}"); + internal static string GetFileDescriptorDirectoryPathForProcess(ProcPid pid) => + pid == ProcPid.Self ? $"{RootPath}{Self}{FileDescriptorDirectoryName}" : + string.Create(null, stackalloc char[256], $"{RootPath}{(uint)pid}{FileDescriptorDirectoryName}"); - private static string GetStatFilePathForThread(int pid, int tid) => string.Create(null, stackalloc char[256], $"{RootPath}{(uint)pid}{TaskDirectoryName}{(uint)tid}{StatFileName}"); + private static string GetStatFilePathForThread(ProcPid pid, int tid) => + pid == ProcPid.Self ? string.Create(null, stackalloc char[256], $"{RootPath}{Self}{TaskDirectoryName}{(uint)tid}{StatFileName}") : + string.Create(null, stackalloc char[256], $"{RootPath}{(uint)pid}{TaskDirectoryName}{(uint)tid}{StatFileName}"); - internal static bool TryReadStatFile(int pid, out ParsedStat result) + internal static bool TryReadStatFile(ProcPid pid, out ParsedStat result) { bool b = TryParseStatFile(GetStatFilePathForProcess(pid), out result); - Debug.Assert(!b || result.pid == pid, "Expected process ID from stat file to match supplied pid"); + Debug.Assert(!b || pid == ProcPid.Self|| (ProcPid)result.pid == pid, "Expected process ID from stat file to match supplied pid"); return b; } - internal static bool TryReadStatFile(int pid, int tid, out ParsedStat result) + internal static bool TryReadStatFile(ProcPid pid, int tid, out ParsedStat result) { bool b = TryParseStatFile(GetStatFilePathForThread(pid, tid), out result); Debug.Assert(!b || result.pid == tid, "Expected thread ID from stat file to match supplied tid"); diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs index ca59d944dc970..df370e3b487fa 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs @@ -31,13 +31,14 @@ public static Process[] GetProcessesByName(string? processName, string machineNa ArrayBuilder processes = default; foreach (int pid in ProcessManager.EnumerateProcessIds()) { - if (Interop.procfs.TryReadStatFile(pid, out Interop.procfs.ParsedStat parsedStat)) + if (ProcessManager.TryGetProcPid(pid, out Interop.procfs.ProcPid procPid) && + Interop.procfs.TryReadStatFile(procPid, out Interop.procfs.ParsedStat parsedStat)) { - string actualProcessName = GetUntruncatedProcessName(ref parsedStat); + string actualProcessName = GetUntruncatedProcessName(procPid, ref parsedStat); if ((processName == "" || string.Equals(processName, actualProcessName, StringComparison.OrdinalIgnoreCase)) && - Interop.procfs.TryReadStatusFile(pid, out Interop.procfs.ParsedStatus parsedStatus)) + Interop.procfs.TryReadStatusFile(procPid, out Interop.procfs.ParsedStatus parsedStatus)) { - ProcessInfo processInfo = ProcessManager.CreateProcessInfo(ref parsedStat, ref parsedStatus, actualProcessName); + ProcessInfo processInfo = ProcessManager.CreateProcessInfo(procPid, ref parsedStat, ref parsedStatus, actualProcessName); processes.Add(new Process(machineName, isRemoteMachine: false, pid, processInfo)); } } @@ -160,7 +161,11 @@ partial void EnsureHandleCountPopulated() { return; } - string path = Interop.procfs.GetFileDescriptorDirectoryPathForProcess(_processId); + if (!ProcessManager.TryGetProcPid(_processId, out Interop.procfs.ProcPid procPid)) + { + return; + } + string path = Interop.procfs.GetFileDescriptorDirectoryPathForProcess(procPid); if (Directory.Exists(path)) { try @@ -250,18 +255,19 @@ private static void SetWorkingSetLimitsCore(IntPtr? newMin, IntPtr? newMax, out #pragma warning restore IDE0060 /// Gets the path to the executable for the process, or null if it could not be retrieved. - /// The pid for the target process, or -1 for the current process. - internal static string? GetExePath(int processId = -1) + /// The pid for the target process. + internal static string? GetExePath(Interop.procfs.ProcPid procPid) { - return processId == -1 ? Environment.ProcessPath : - Interop.Sys.ReadLink(Interop.procfs.GetExeFilePathForProcess(processId)); + return procPid == Interop.procfs.ProcPid.Self ? Environment.ProcessPath : + Interop.Sys.ReadLink(Interop.procfs.GetExeFilePathForProcess(procPid)); } /// Gets the name that was used to start the process, or null if it could not be retrieved. + /// The pid for the target process. /// The stat for the target process. - internal static string GetUntruncatedProcessName(ref Interop.procfs.ParsedStat stat) + internal static string GetUntruncatedProcessName(Interop.procfs.ProcPid procPid, ref Interop.procfs.ParsedStat stat) { - string cmdLineFilePath = Interop.procfs.GetCmdLinePathForProcess(stat.pid); + string cmdLineFilePath = Interop.procfs.GetCmdLinePathForProcess(procPid); byte[]? rentedArray = null; try @@ -362,7 +368,7 @@ private Interop.procfs.ParsedStat GetStat() { EnsureState(State.HaveNonExitedId); Interop.procfs.ParsedStat stat; - if (!Interop.procfs.TryReadStatFile(_processId, out stat)) + if (!ProcessManager.TryReadStatFile(_processId, out stat)) { throw new Win32Exception(SR.ProcessInformationUnavailable); } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Linux.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Linux.cs index 34088e34bdac3..b578bc241d843 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Linux.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Linux.cs @@ -9,6 +9,8 @@ namespace System.Diagnostics { internal static partial class ProcessManager { + private static volatile int _procMatchesPidNamespace; + /// Gets the IDs of all processes on the current machine. public static int[] GetProcessIds() => new List(EnumerateProcessIds()).ToArray(); @@ -41,28 +43,31 @@ public static ProcessInfo[] GetProcessInfos(string? processNameFilter, string ma /// The array of modules. internal static ProcessModuleCollection GetModules(int processId) { - ProcessModuleCollection modules = Interop.procfs.ParseMapsModules(processId) ?? new(capacity: 0); - - // Move the main executable module to be the first in the list if it's not already - if (Process.GetExePath(processId) is string exePath) + ProcessModuleCollection? modules = null; + if (TryGetProcPid(processId, out Interop.procfs.ProcPid procPid)) { - for (int i = 0; i < modules.Count; i++) + modules = Interop.procfs.ParseMapsModules(procPid); + + // Move the main executable module to be the first in the list if it's not already + if (modules is not null && Process.GetExePath(procPid) is string exePath) { - ProcessModule module = modules[i]; - if (module.FileName == exePath) + for (int i = 0; i < modules.Count; i++) { - if (i > 0) + ProcessModule module = modules[i]; + if (module.FileName == exePath) { - modules.RemoveAt(i); - modules.Insert(0, module); + if (i > 0) + { + modules.RemoveAt(i); + modules.Insert(0, module); + } + break; } - break; } } } - // Return the set of modules found - return modules; + return modules ?? new(capacity: 0); } /// @@ -70,10 +75,11 @@ internal static ProcessModuleCollection GetModules(int processId) /// internal static ProcessInfo? CreateProcessInfo(int pid) { - if (Interop.procfs.TryReadStatFile(pid, out Interop.procfs.ParsedStat stat)) + if (TryGetProcPid(pid, out Interop.procfs.ProcPid procPid) && + Interop.procfs.TryReadStatFile(procPid, out Interop.procfs.ParsedStat stat)) { - Interop.procfs.TryReadStatusFile(pid, out Interop.procfs.ParsedStatus status); - return CreateProcessInfo(ref stat, ref status); + Interop.procfs.TryReadStatusFile(procPid, out Interop.procfs.ParsedStatus status); + return CreateProcessInfo(procPid, ref stat, ref status); } return null; } @@ -81,14 +87,14 @@ internal static ProcessModuleCollection GetModules(int processId) /// /// Creates a ProcessInfo from the data parsed from a /proc/pid/stat file and the associated tasks directory. /// - internal static ProcessInfo CreateProcessInfo(ref Interop.procfs.ParsedStat procFsStat, ref Interop.procfs.ParsedStatus procFsStatus, string? processName = null) + internal static ProcessInfo CreateProcessInfo(Interop.procfs.ProcPid procPid, ref Interop.procfs.ParsedStat procFsStat, ref Interop.procfs.ParsedStatus procFsStatus, string? processName = null) { int pid = procFsStat.pid; var pi = new ProcessInfo() { ProcessId = pid, - ProcessName = processName ?? Process.GetUntruncatedProcessName(ref procFsStat) ?? string.Empty, + ProcessName = processName ?? Process.GetUntruncatedProcessName(procPid, ref procFsStat) ?? string.Empty, BasePriority = (int)procFsStat.nice, SessionId = procFsStat.session, PoolPagedBytes = (long)procFsStatus.VmSwap, @@ -105,7 +111,7 @@ internal static ProcessInfo CreateProcessInfo(ref Interop.procfs.ParsedStat proc }; // Then read through /proc/pid/task/ to find each thread in the process... - string tasksDir = Interop.procfs.GetTaskDirectoryPathForProcess(pid); + string tasksDir = Interop.procfs.GetTaskDirectoryPathForProcess(procPid); try { foreach (string taskDir in Directory.EnumerateDirectories(tasksDir)) @@ -115,7 +121,7 @@ internal static ProcessInfo CreateProcessInfo(ref Interop.procfs.ParsedStat proc int tid; Interop.procfs.ParsedStat stat; if (int.TryParse(dirName, NumberStyles.Integer, CultureInfo.InvariantCulture, out tid) && - Interop.procfs.TryReadStatFile(pid, tid, out stat)) + Interop.procfs.TryReadStatFile(procPid, tid, out stat)) { pi._threadInfoList.Add(new ThreadInfo() { @@ -147,18 +153,26 @@ internal static ProcessInfo CreateProcessInfo(ref Interop.procfs.ParsedStat proc /// Enumerates the IDs of all processes on the current machine. internal static IEnumerable EnumerateProcessIds() { - // Parse /proc for any directory that's named with a number. Each such - // directory represents a process. - foreach (string procDir in Directory.EnumerateDirectories(Interop.procfs.RootPath)) + if (ProcMatchesPidNamespace) { - string dirName = Path.GetFileName(procDir); - int pid; - if (int.TryParse(dirName, NumberStyles.Integer, CultureInfo.InvariantCulture, out pid)) + // Parse /proc for any directory that's named with a number. Each such + // directory represents a process. + foreach (string procDir in Directory.EnumerateDirectories(Interop.procfs.RootPath)) { - Debug.Assert(pid >= 0); - yield return pid; + string dirName = Path.GetFileName(procDir); + int pid; + if (int.TryParse(dirName, NumberStyles.Integer, CultureInfo.InvariantCulture, out pid)) + { + Debug.Assert(pid >= 0); + yield return pid; + } } } + else + { + // Limit to our own process. For other processes, the pids from /proc don't match with those in the process namespace. + yield return Environment.ProcessId; + } } /// Gets a ThreadState to represent the value returned from the status field of /proc/pid/stat. @@ -198,5 +212,80 @@ private static ThreadState ProcFsStateToThreadState(char c) } } + internal static bool TryReadStatFile(int pid, out Interop.procfs.ParsedStat stat) + { + if (!TryGetProcPid(pid, out Interop.procfs.ProcPid procPid)) + { + stat = default; + return false; + } + return Interop.procfs.TryReadStatFile(procPid, out stat); + } + + internal static bool TryReadStatusFile(int pid, out Interop.procfs.ParsedStatus status) + { + if (!TryGetProcPid(pid, out Interop.procfs.ProcPid procPid)) + { + status = default; + return false; + } + return Interop.procfs.TryReadStatusFile(procPid, out status); + } + + internal static bool TryReadStatFile(int pid, int tid, out Interop.procfs.ParsedStat stat) + { + if (!TryGetProcPid(pid, out Interop.procfs.ProcPid procPid)) + { + stat = default; + return false; + } + return Interop.procfs.TryReadStatFile(procPid, tid, out stat); + } + + internal static bool TryGetProcPid(int pid, out Interop.procfs.ProcPid procPid) + { + // Use '/proc/self' for the current process. + if (pid == Environment.ProcessId) + { + procPid = Interop.procfs.ProcPid.Self; + return true; + } + + if (ProcMatchesPidNamespace) + { + procPid = (Interop.procfs.ProcPid)pid; + return true; + } + + // We can't map a process namespace pid to a procfs pid. + procPid = Interop.procfs.ProcPid.Invalid; + return false; + } + + internal static bool ProcMatchesPidNamespace + { + get + { + // _procMatchesPidNamespace is set to: + // - 0: when uninitialized, + // - 1: '/proc' and the process pid namespace match, + // - 2: when they don't match. + if (_procMatchesPidNamespace == 0) + { + // '/proc/self' is a symlink to the pid used by '/proc' for the current process. + // We compare it with the pid of the current process to see if the '/proc' and pid namespace match up. + int? procSelfPid = null; + if (Interop.Sys.ReadLink($"{Interop.procfs.RootPath}{Interop.procfs.Self}") is string target && + int.TryParse(target, out int pid)) + { + procSelfPid = pid; + } + Debug.Assert(procSelfPid.HasValue); + + _procMatchesPidNamespace = !procSelfPid.HasValue || procSelfPid == Environment.ProcessId ? 1 : 2; + } + return _procMatchesPidNamespace == 1; + } + } } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.Linux.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.Linux.cs index e9a8bd92e04a9..c25f9d9de582f 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.Linux.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.Linux.cs @@ -83,7 +83,7 @@ public TimeSpan UserProcessorTime private Interop.procfs.ParsedStat GetStat() { Interop.procfs.ParsedStat stat; - if (!Interop.procfs.TryReadStatFile(pid: _processId, tid: Id, result: out stat)) + if (!ProcessManager.TryReadStatFile(_processId, tid: Id, out stat)) { throw new InvalidOperationException(SR.Format(SR.ThreadExited, Id)); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.Linux.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.Linux.cs index 4f488f43a84ae..765ef5ab64e22 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Environment.Linux.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Environment.Linux.cs @@ -7,6 +7,6 @@ namespace System { public static partial class Environment { - public static long WorkingSet => (long)(Interop.procfs.TryReadStatusFile(ProcessId, out Interop.procfs.ParsedStatus status) ? status.VmRSS : 0); + public static long WorkingSet => (long)(Interop.procfs.TryReadStatusFile(Interop.procfs.ProcPid.Self, out Interop.procfs.ParsedStatus status) ? status.VmRSS : 0); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.SunOS.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.SunOS.cs index fb7ff75cb2e6c..84199a3d306d2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Environment.SunOS.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Environment.SunOS.cs @@ -8,6 +8,6 @@ namespace System public static partial class Environment { public static long WorkingSet => - (long)(Interop.procfs.TryReadProcessStatusInfo(ProcessId, out Interop.procfs.ProcessStatusInfo status) ? status.ResidentSetSize : 0); + (long)(Interop.procfs.TryReadProcessStatusInfo(Interop.procfs.ProcPid.Self, out Interop.procfs.ProcessStatusInfo status) ? status.ResidentSetSize : 0); } }