From f7af9e9ae8d21e938fd9f8723d43d913ae3763c4 Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Thu, 8 Oct 2026 01:41:16 -0400 Subject: [PATCH] Fix macOS Process hangs caused by stopped children (#135323) Fixes https://github.com/dotnet/runtime/issues/135294. On macOS, `waitid(WEXITED | WNOHANG | WNOWAIT)` can report a stopped child even though `waitpid(WNOHANG)` cannot reap it. The SIGCHLD handler repeatedly observes the same child while holding shared process-management locks, blocking other Process operations. Fall back to scanning all tracked children when the reported child cannot be reaped, rather than continuing the no-progress loop. This guard lives in the shared Unix implementation; successful reaping retains the existing fast path, and the scan reuses the existing nonblocking fallback for unknown children. - Add isolated regression coverage for synchronous and asynchronous exit waits while another child is stopped. - Restore class-level parallelism for Process tests on macOS by removing the temporary serialization safeguard. - Preserve the existing RemoteExecutor isolation changes and timeouts. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Diagnostics/ProcessWaitState.Unix.cs | 16 ++--- .../tests/AssemblyInfo.cs | 6 -- .../tests/ProcessTests.Unix.cs | 58 +++++++++++++++++++ .../System.Diagnostics.Process.Tests.csproj | 3 +- 4 files changed, 68 insertions(+), 15 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs index b2c8445a3b9006..9161b91dfaf0f3 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs @@ -622,18 +622,18 @@ internal static void CheckChildren(bool reapAll, bool configureConsole) pid = Interop.Sys.WaitIdAnyExitedNoHangNoWait(); if (pid > 0) { - if (s_childProcessWaitStates.TryGetValue(pid, out ProcessWaitState? pws)) + if (s_childProcessWaitStates.TryGetValue(pid, out ProcessWaitState? pws) && + pws.TryReapChild(configureConsole)) { - // Known Process. - if (pws.TryReapChild(configureConsole)) - { - pws.ReleaseRef(); - } + pws.ReleaseRef(); } else { - // unlikely: This is not a managed Process, so we are not responsible for reaping. - // Fall back to checking all Processes. + // The child may be unmanaged, or stopped rather than exited. On macOS, + // waitid can report stopped children despite omitting WSTOPPED; + // the SIGCHLD handler is also registered with SA_NOCLDSTOP. + // Scan all managed children instead of repeatedly observing + // the same child without reaping it. checkAll = true; break; } diff --git a/src/libraries/System.Diagnostics.Process/tests/AssemblyInfo.cs b/src/libraries/System.Diagnostics.Process/tests/AssemblyInfo.cs index f15a1cb605513a..41a9e3ed1e5ef3 100644 --- a/src/libraries/System.Diagnostics.Process/tests/AssemblyInfo.cs +++ b/src/libraries/System.Diagnostics.Process/tests/AssemblyInfo.cs @@ -3,10 +3,4 @@ using Xunit; -#if TARGET_OSX -// Retain serialization until process-management hangs under concurrent test classes on macOS are resolved. -// https://github.com/dotnet/runtime/issues/135294 -[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] -#endif - [assembly: SkipOnPlatform(TestPlatforms.Browser, "System.Diagnostics.Process is not supported on Browser.")] diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs index f916cedb732608..161e4daaf1532d 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs @@ -769,6 +769,64 @@ public async Task TestChildProcessCleanupAfterDispose(bool shortProcess, bool en Assert.True(processReaped); } + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [PlatformSpecific(TestPlatforms.OSX)] + [InlineData(false)] + [InlineData(true)] + public void WaitForExit_StoppedChild_DoesNotPreventReapingOtherChildren(bool useAsync) + { + RemoteExecutor.Invoke(async useAsyncString => + { + const uint StoppedProcessStatus = 4; // SSTOP in sys/proc.h. + using Process stopped = Process.Start("/bin/sleep", "300"); + using Process exiting = Process.Start("/bin/sleep", "300"); + int stoppedPid = stopped.Id; + int exitingPid = exiting.Id; + int killSignal = Interop.Sys.GetPlatformSignalNumber(PosixSignal.SIGKILL); + using var sigChildReceived = new ManualResetEventSlim(); + using PosixSignalRegistration registration = PosixSignalRegistration.Create(PosixSignal.SIGCHLD, context => + { + context.Cancel = true; + sigChildReceived.Set(); + }); + + try + { + Assert.Equal(0, Interop.Sys.Kill(stoppedPid, Interop.Sys.GetPlatformSIGSTOP())); + + Assert.True(SpinWait.SpinUntil( + () => Interop.libproc.GetProcessInfoById(stoppedPid) is { } info && + info.pbsd.pbi_status == StoppedProcessStatus, WaitInMS)); + + sigChildReceived.Reset(); + Assert.Equal(0, Interop.Sys.Kill(Environment.ProcessId, Interop.Sys.GetPlatformSignalNumber(PosixSignal.SIGCHLD))); + Assert.True(sigChildReceived.Wait(WaitInMS)); + + Assert.Equal(0, Interop.Sys.Kill(exitingPid, killSignal)); + if (bool.Parse(useAsyncString)) + { + using var cts = new CancellationTokenSource(WaitInMS); + await exiting.WaitForExitAsync(cts.Token); + } + else + { + Assert.True(exiting.WaitForExit(WaitInMS)); + } + Assert.True(exiting.HasExited); + } + finally + { + // Bypass managed process locks so cleanup also works if the reaper is stuck. + Assert.Equal(0, Interop.Sys.Kill(stoppedPid, killSignal)); + Assert.True(stopped.WaitForExit(WaitInMS)); + exiting.Kill(); + Assert.True(exiting.WaitForExit(WaitInMS)); + } + + return RemoteExecutor.SuccessExitCode; + }, useAsync.ToString()).Dispose(); + } + private static Process CreateShortProcess() { Process process = new Process(); diff --git a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj index 8a00172f4ddee7..446be53722769d 100644 --- a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj +++ b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj @@ -10,7 +10,6 @@ $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) $(DefineConstants);TargetsWindows - $(DefineConstants);TARGET_OSX +