Conversation
On macOS, waitid(P_ALL, WEXITED | WNOHANG | WNOWAIT) also reports children that have stopped (SIGSTOP) or continued (SIGCONT), not just exited ones. Because WNOWAIT does not consume the notification, SystemNative_WaitIdAnyExitedNoHangNoWait kept returning the same stopped PID, causing the SIGCHLD reaper (CheckChildren) to spin forever while holding s_childProcessWaitStates / s_processStartLock. Process.Kill(entireProcessTree: true) SIGSTOPs the whole tree before killing it (the two-phase StopTree added in dotnet#128598), so a concurrent kill would leave a direct child stopped long enough for the reaper to wedge on it, deadlocking every concurrent Kill/Start. This is why the SDK dotnet-watch tests hung on osx.15.arm64. Only report children whose si_code indicates an actual exit (CLD_EXITED / CLD_KILLED / CLD_DUMPED). When a stopped/continued child is reported, consume that notification and keep looking for a real exit, which also unmasks any exited child that macOS was hiding behind the stopped one. On platforms that honor WEXITED (e.g. Linux) the new branch is never taken. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @dotnet/area-system-diagnostics-process |
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate issues remain in test synchronization, cleanup, grandchild verification, and native notification-drain scoping.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This pull request fixes a macOS deadlock during concurrent process-tree termination.
Changes:
- Filters
waitidresults and drains stopped/continued notifications. - Adds a macOS concurrent process-tree kill regression test.
- Addresses moderate follow-ups for grandchild readiness and verification, failure cleanup, and limiting drains to tracked children.
File summaries
| File | Summary |
|---|---|
src/native/libs/System.Native/pal_process.c |
Updates exit detection and notification draining; draining should be limited to managed children. |
src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs |
Adds the regression test; it needs reliable grandchild startup, termination assertions, and timeout cleanup. |
Review details
Suppressed comments (2)
src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs:1201
- The fixed 500 ms delay does not establish that each
/bin/sleepgrandchild has started before the roots are stopped. On a slow or loaded macOS runner, tree enumeration can miss a grandchild, making this test pass without exercising the recursive path and leaving that orphaned sleep behind. Use a readiness handshake (or otherwise wait for each grandchild PID) before starting the concurrent kills.
// Give the grandchildren time to start so the trees are fully formed.
Thread.Sleep(500);
src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs:1211
- If
Task.WaitAlltimes out, it only returnsfalse; the subsequentAssert.Truethrows while all eightKilltasks remain blocked. This method has nofinally, andProcessTestBase.Disposeonly callsKill()on the tracked roots, so each remote root's/bin/sleepgrandchild is orphaned and left running (and cleanup can encounter the same reaper deadlock). Please add failure-path cleanup that can terminate both the roots and the spawned grandchildren without calling the hanging tree-kill operation.
bool completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(60));
Assert.True(completed, $"Kill(entireProcessTree: true) hung on iteration {iteration}.");
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Lite
| foreach (Process root in roots) | ||
| { | ||
| Assert.True(root.WaitForExit(WaitInMS)); |
| siginfo_t drain; | ||
| memset(&drain, 0, sizeof(drain)); | ||
| while (CheckInterrupted(result = waitid(P_PID, (id_t)siginfo.si_pid, &drain, WSTOPPED | WCONTINUED | WNOHANG))); |
There was a problem hiding this comment.
This is valid feedback. I wish SA_NOCLDSTOP would just prevent us from getting here in the first place.
adamsitnik
left a comment
There was a problem hiding this comment.
@jozkee Big thanks for helping me with this!
| // another Kill) then blocks on that lock, so the stopped child is never SIGKILL'd -> deadlock. | ||
| [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] | ||
| [PlatformSpecific(TestPlatforms.OSX)] | ||
| public void Kill_EntireProcessTree_Concurrent_DoesNotHang() |
There was a problem hiding this comment.
I was able to confirm that this test reproduces the problem: adamsitnik/macosrepro#2
| return 0; | ||
| } | ||
|
|
||
| // We requested WEXITED only, but some platforms (notably macOS) also report |
There was a problem hiding this comment.
| const int TreeCount = 8; | ||
| const int Iterations = 30; |
There was a problem hiding this comment.
Hmm this test spawns a LOT of processes. I think it's reasonable to keep it, but we should most likely consider moving it to Outerloop.
| siginfo_t drain; | ||
| memset(&drain, 0, sizeof(drain)); | ||
| while (CheckInterrupted(result = waitid(P_PID, (id_t)siginfo.si_pid, &drain, WSTOPPED | WCONTINUED | WNOHANG))); |
There was a problem hiding this comment.
This is valid feedback. I wish SA_NOCLDSTOP would just prevent us from getting here in the first place.
Summary
Fixes a deadlock where
Process.Kill(entireProcessTree: true)can hang indefinitely on macOS (#131944), observed as the SDKdotnet-watch.Testshanging onosx.15.arm64.Root cause
The hang is a bad interaction between a macOS
waitidquirk and the two-phase tree kill.1. macOS
waitidreports stopped children underWEXITED. The SIGCHLD reaper peeks for exited children with:On macOS this also returns children that are merely stopped (
SIGSTOP) —si_code == CLD_STOPPED— even though onlyWEXITEDwas requested. BecauseWNOWAITnever consumes the notification, the reaper loop inCheckChildrengets the same stopped PID forever:This spin runs while holding
s_childProcessWaitStatesand thes_processStartLockwrite lock.2. The two-phase
StopTree(from #128598) holds the tree stopped.Kill(entireProcessTree: true)nowSIGSTOPs the entire tree up front and defers allSIGKILLs to the end:So a direct child sits in the stopped state across the whole traversal. If a concurrent kill/exit fires
SIGCHLDduring that window, the reaper wedges on the stopped child. The killing thread's next step (StopTree->GetChildProcesses-> constructing aProcess->AddRef) needss_childProcessWaitStates, which the spinning reaper holds — so the tree is never killed and the stop never ends:That circular wait is the reported hang ("nine calls entering Kill, only three returned").
Why it's a regression
The macOS
waitidquirk is long-standing, but in .NET 10KillTreewas one-phase — it stopped a node andSIGKILLed it immediately, before recursing — so a direct child was stopped only for a microscopic window and killed without needing the lock again. Confirmed on .NET 10.0.12: the repro below runs 30/30 clean. #128598's two-phase design widened that window to the entire tree traversal, turning an essentially-unhittable race into a reliable hang.Fix
In
SystemNative_WaitIdAnyExitedNoHangNoWait, only report children whosesi_codeindicates an actual exit (CLD_EXITED/CLD_KILLED/CLD_DUMPED). When macOS reports a stopped/continued child, consume that notification with a targetedwaitid(P_PID, ..., WSTOPPED | WCONTINUED | WNOHANG)and keep looking for a real exit.The consuming
waitidis essential (not just cosmetic): macOS returns the stopped child in preference to an already-exited sibling and hides the exit behind it. Draining the stop unmasks the exited child so it's still reaped — verified with a standalone probe. On platforms that honorWEXITED(e.g. Linux) the new branch is never taken, so behavior there is unchanged.Testing
Kill_EntireProcessTree_Concurrent_DoesNotHang(macOS): concurrently kills 8 process trees with a 60s watchdog. Fails (hangs on iteration 1) without the fix; passes with it.System.Diagnostics.Processsuite: green.Resolves #131944
Note
This PR description was generated with the assistance of GitHub Copilot.