From 9a136155b31909c4fd7766ced79e50839cb9e897 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 06:08:52 +0000 Subject: [PATCH 1/6] Initial plan From 6531bc272891bb54a21564f8f74c811356a7d325 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 06:53:54 +0000 Subject: [PATCH 2/6] Fix fork/exec race in GetUntruncatedProcessName by using /proc/pid/exe basename as fallback When Process.Start() is called on Linux, there's a rare race condition: if pipe2() fails to create the synchronization pipe, the parent process may read /proc//stat before execve() updates the comm field. In this case, GetUntruncatedProcessName falls back to stat.comm which still shows the parent's name (e.g. '.NET Long Runni') rather than the started process name (e.g. 'sleep'). The fix: when the cmdline-matching approach fails to find the process name, try /proc//exe basename instead of stat.comm. The /proc//exe symlink is updated earlier in the exec path (during exec_mmap/flush_old_exec) than stat.comm (updated in setup_new_exec), so it reflects the new binary even during the race window. Fixes: dotnet/runtime#111431 (regression of dotnet/runtime#106595) Co-authored-by: danmoseley <6385855+danmoseley@users.noreply.github.com> Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/a3bcb4da-7804-45c5-84cc-99b1f18f7178 --- .../src/System/Diagnostics/Process.Linux.cs | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) 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 93c7950b453571..4f0806c19e6788 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 @@ -319,20 +319,20 @@ internal static string GetUntruncatedProcessName(Interop.procfs.ProcPid procPid, if (argEnd != -1) { name = GetUntruncatedNameFromArg(argRemainder.Slice(0, argEnd), prefix: stat.comm); - return name ?? stat.comm; + return name ?? GetExeBasenameOrComm(procPid, stat.comm); } } if (n == 0) { - return stat.comm; + return GetExeBasenameOrComm(procPid, stat.comm); } } } } catch (IOException) { - return stat.comm; + return GetExeBasenameOrComm(procPid, stat.comm); } finally { @@ -359,6 +359,28 @@ internal static string GetUntruncatedProcessName(Interop.procfs.ProcPid procPid, } } + /// + /// Gets the process name from /proc/pid/exe basename, falling back to the stat comm field. + /// Using /proc/pid/exe is more reliable than stat.comm during the fork/exec race window: + /// the exe symlink is updated early in execve (when the new memory map is created), before + /// stat.comm is updated, so it reflects the new binary even if stat.comm still shows the + /// parent's name. + /// + private static string GetExeBasenameOrComm(Interop.procfs.ProcPid procPid, string comm) + { + string? exePath = GetExePath(procPid); + if (exePath is not null) + { + string exeBasename = Path.GetFileName(exePath); + if (exeBasename.Length > 0) + { + return exeBasename; + } + } + + return comm; + } + // ---------------------------------- // ---- Unix PAL layer ends here ---- // ---------------------------------- From 98ed1ddcb5754c366655f9d990b8b32e45f7cd8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 00:54:38 +0000 Subject: [PATCH 3/6] Clarify GetExeBasenameOrComm comment: race is fully eliminated on glibc/Linux (vfork) Co-authored-by: danmoseley <6385855+danmoseley@users.noreply.github.com> Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d0f62e57-e643-4876-987b-7bb5c994bf3c --- .../src/System/Diagnostics/Process.Linux.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 4f0806c19e6788..d415ad67a32998 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 @@ -361,10 +361,12 @@ internal static string GetUntruncatedProcessName(Interop.procfs.ProcPid procPid, /// /// Gets the process name from /proc/pid/exe basename, falling back to the stat comm field. - /// Using /proc/pid/exe is more reliable than stat.comm during the fork/exec race window: - /// the exe symlink is updated early in execve (when the new memory map is created), before - /// stat.comm is updated, so it reflects the new binary even if stat.comm still shows the - /// parent's name. + /// This eliminates the fork/exec race on glibc Linux (where vfork() is used): vfork() keeps + /// the parent suspended until exec_mmap() runs in the child, so /proc/pid/exe is already + /// updated to the new binary by the time the parent can call this method. stat.comm may + /// still lag behind (it is updated by setup_new_exec(), which runs after exec_mmap()), which + /// is why reading /proc/pid/exe is preferred here. The race is fully closed for the cases + /// where it was observed to fail in practice. /// private static string GetExeBasenameOrComm(Interop.procfs.ProcPid procPid, string comm) { From cfb29b0825fb9190cd8da7a96a2a867ed64e65d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 01:17:05 +0000 Subject: [PATCH 4/6] Apply style nit: use pattern matching in GetExeBasenameOrComm Co-authored-by: danmoseley <6385855+danmoseley@users.noreply.github.com> Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/b0fc4ddc-5970-4e6e-9d76-af313d11690f --- .../src/System/Diagnostics/Process.Linux.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) 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 d415ad67a32998..33c2e20ac7601c 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 @@ -370,14 +370,10 @@ internal static string GetUntruncatedProcessName(Interop.procfs.ProcPid procPid, /// private static string GetExeBasenameOrComm(Interop.procfs.ProcPid procPid, string comm) { - string? exePath = GetExePath(procPid); - if (exePath is not null) + if (GetExePath(procPid) is string exePath && + Path.GetFileName(exePath) is { Length: > 0 } exeBasename) { - string exeBasename = Path.GetFileName(exePath); - if (exeBasename.Length > 0) - { - return exeBasename; - } + return exeBasename; } return comm; From 583db8e94a57ed8a880a34ee0d8cdc1aa508716e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 06:09:51 +0000 Subject: [PATCH 5/6] Fix flaky tests by polling ProcessName instead of patching production code Revert the GetUntruncatedProcessName/GetExeBasenameOrComm production code changes. Instead, fix the two flaky tests by polling ProcessName every 50ms for up to 1 second (20 iterations) until it matches the expected program name, tolerating the transient fork/exec race window without changing library behavior. Co-authored-by: danmoseley <6385855+danmoseley@users.noreply.github.com> Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/12051142-4a27-4b38-85b5-dc3cd7dd437b --- .../src/System/Diagnostics/Process.Linux.cs | 26 +++---------------- .../tests/ProcessTests.Unix.cs | 22 ++++++++++++++-- 2 files changed, 23 insertions(+), 25 deletions(-) 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 33c2e20ac7601c..93c7950b453571 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 @@ -319,20 +319,20 @@ internal static string GetUntruncatedProcessName(Interop.procfs.ProcPid procPid, if (argEnd != -1) { name = GetUntruncatedNameFromArg(argRemainder.Slice(0, argEnd), prefix: stat.comm); - return name ?? GetExeBasenameOrComm(procPid, stat.comm); + return name ?? stat.comm; } } if (n == 0) { - return GetExeBasenameOrComm(procPid, stat.comm); + return stat.comm; } } } } catch (IOException) { - return GetExeBasenameOrComm(procPid, stat.comm); + return stat.comm; } finally { @@ -359,26 +359,6 @@ internal static string GetUntruncatedProcessName(Interop.procfs.ProcPid procPid, } } - /// - /// Gets the process name from /proc/pid/exe basename, falling back to the stat comm field. - /// This eliminates the fork/exec race on glibc Linux (where vfork() is used): vfork() keeps - /// the parent suspended until exec_mmap() runs in the child, so /proc/pid/exe is already - /// updated to the new binary by the time the parent can call this method. stat.comm may - /// still lag behind (it is updated by setup_new_exec(), which runs after exec_mmap()), which - /// is why reading /proc/pid/exe is preferred here. The race is fully closed for the cases - /// where it was observed to fail in practice. - /// - private static string GetExeBasenameOrComm(Interop.procfs.ProcPid procPid, string comm) - { - if (GetExePath(procPid) is string exePath && - Path.GetFileName(exePath) is { Length: > 0 } exeBasename) - { - return exeBasename; - } - - return comm; - } - // ---------------------------------- // ---- Unix PAL layer ends here ---- // ---------------------------------- diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs index 4cc913b32ea6cf..3f50ef64f9ab5e 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs @@ -351,7 +351,16 @@ public void ProcessStart_OnLinux_UsesSpecifiedProgram() { try { - Assert.Equal(Program, px.ProcessName); + // ProcessName may transiently reflect the parent process name immediately after + // fork() if execve() hasn't completed yet, so poll briefly until it stabilizes. + string processName = px.ProcessName; + for (int i = 0; processName != Program && i < 20; i++) + { + Thread.Sleep(50); + px.Refresh(); + processName = px.ProcessName; + } + Assert.Equal(Program, processName); } finally { @@ -374,7 +383,16 @@ public void ProcessStart_OnLinux_UsesSpecifiedProgramUsingArgumentList() { try { - Assert.Equal(Program, px.ProcessName); + // ProcessName may transiently reflect the parent process name immediately after + // fork() if execve() hasn't completed yet, so poll briefly until it stabilizes. + string processName = px.ProcessName; + for (int i = 0; processName != Program && i < 20; i++) + { + Thread.Sleep(50); + px.Refresh(); + processName = px.ProcessName; + } + Assert.Equal(Program, processName); } finally { From a6324ca167fb6dd84d5a3b7d22dea3abec1b2c85 Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Wed, 25 Mar 2026 21:00:15 -0600 Subject: [PATCH 6/6] Simplify polling loop to use RetryHelper; fix comment to say thread name Address tmds feedback: replace hand-rolled poll loop with RetryHelper.Execute which is the standard test retry pattern. Fix comment to correctly say 'parent thread name' (the comm field reflects the thread name set by .NET runtime, not the process executable name). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/ProcessTests.Unix.cs | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs index 3f50ef64f9ab5e..8869950c3a44aa 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs @@ -351,16 +351,13 @@ public void ProcessStart_OnLinux_UsesSpecifiedProgram() { try { - // ProcessName may transiently reflect the parent process name immediately after - // fork() if execve() hasn't completed yet, so poll briefly until it stabilizes. - string processName = px.ProcessName; - for (int i = 0; processName != Program && i < 20; i++) + // ProcessName may transiently reflect the parent's thread name immediately + // after fork() if execve() hasn't completed yet in the child, so retry briefly. + RetryHelper.Execute(() => { - Thread.Sleep(50); px.Refresh(); - processName = px.ProcessName; - } - Assert.Equal(Program, processName); + Assert.Equal(Program, px.ProcessName); + }); } finally { @@ -383,16 +380,13 @@ public void ProcessStart_OnLinux_UsesSpecifiedProgramUsingArgumentList() { try { - // ProcessName may transiently reflect the parent process name immediately after - // fork() if execve() hasn't completed yet, so poll briefly until it stabilizes. - string processName = px.ProcessName; - for (int i = 0; processName != Program && i < 20; i++) + // ProcessName may transiently reflect the parent's thread name immediately + // after fork() if execve() hasn't completed yet in the child, so retry briefly. + RetryHelper.Execute(() => { - Thread.Sleep(50); px.Refresh(); - processName = px.ProcessName; - } - Assert.Equal(Program, processName); + Assert.Equal(Program, px.ProcessName); + }); } finally {