Fix possible deadlocks on PosixSignalRegistration disposal on Windows#124893
Fix possible deadlocks on PosixSignalRegistration disposal on Windows#124893mrek-msft wants to merge 23 commits intodotnet:mainfrom
Conversation
…etween runtime and kernelbase.
|
Tagging subscribers to this area: @dotnet/area-system-runtime |
There was a problem hiding this comment.
Pull request overview
Updates Windows PosixSignalRegistration to reduce shutdown deadlock risk by ensuring the console control handler is only registered once and by removing handler unregistration.
Changes:
- Introduces
s_isCtrlHandlerRegisteredOnceto prevent repeatedSetConsoleCtrlHandlerregistrations. - Moves
SetConsoleCtrlHandler(Add: true)outside thes_registrationslock to avoid A→B / B→A deadlock scenarios. - Removes
SetConsoleCtrlHandler(Add: false)unregistration when the last registration is disposed.
...System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs
Outdated
Show resolved
Hide resolved
...System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs:77
- The token is added to s_registrations even when the SetConsoleCtrlHandler registration failed or was skipped. If registerCtrlHandler is false (another thread is registering), the current thread will proceed to add its token to s_registrations before the other thread's SetConsoleCtrlHandler call completes. If that call fails, this registration will be in s_registrations but the handler won't be registered with Windows, causing signals to be silently ignored.
Consider adding the token to s_registrations only after successful handler registration, or checking s_isCtrlHandlerRegistering again and verifying the handler was successfully registered.
lock (s_registrations)
{
s_isCtrlHandlerRegistering = false;
if (!s_registrations.TryGetValue(signo, out List<Token>? tokens))
{
s_registrations[signo] = tokens = new List<Token>();
}
tokens.Add(token);
}
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs:12
- The variable name s_isCtrlHandlerRegistering uses the present continuous tense (-ing form) suggesting an ongoing action, but it's set to true before registration starts and false after it completes (or fails), making it a state flag. Consider renaming to s_ctrlHandlerRegistrationInProgress or s_isRegisteringCtrlHandler for clarity, or use a simpler name like s_handlerRegistrationInProgress that better conveys it's a guard flag for the registration operation.
private static bool s_isCtrlHandlerRegistering;
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs:77
- The concurrent registration scenario where one thread's SetConsoleCtrlHandler call fails while another thread is waiting is not covered by tests. This is a critical scenario because it can lead to registrations being added to s_registrations without the handler actually being registered with Windows, causing signals to be silently ignored. Consider adding a test that simulates concurrent registration with one failing, or at least document this edge case if it's considered too difficult to test reliably.
lock (s_registrations)
{
s_isCtrlHandlerRegistering = false;
if (!s_registrations.TryGetValue(signo, out List<Token>? tokens))
{
s_registrations[signo] = tokens = new List<Token>();
}
tokens.Add(token);
}
...System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.
You can also share your feedback on Copilot code review. Take the survey.
...System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs
Outdated
Show resolved
Hide resolved
...System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs
Show resolved
Hide resolved
…pServices/PosixSignalRegistration.Windows.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
...System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs
Outdated
Show resolved
Hide resolved
...System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs
Outdated
Show resolved
Hide resolved
…dlock' into dev/mrek/meh-sig-hndlr-deadlock
|
I pushed also changes to Hosting which was the second issue mentioned in #117753 Potential shutdown deadlocks in Microsoft.Extensions.Hosting on Windows. I originally planed to fix them in separate PRs, but since it is simple and related, I added them here. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 12 comments.
You can also share your feedback on Copilot code review. Take the survey.
...System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs
Outdated
Show resolved
Hide resolved
| [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] | ||
| [MemberData(nameof(SignalTestData))] | ||
| public void SignalHandler_CanDisposeInHandler(PosixSignal signal) |
There was a problem hiding this comment.
This test uses SignalTestData, which on Unix includes signals like SIGCHLD/SIGWINCH (default: ignored) and SIGTSTP/SIGTTIN/SIGTTOU (default: stop, not terminate). The test expects the second signal to terminate the process, so it will fail or hang/return -1 for those signals. Limit this test’s data to signals whose default action is process termination (e.g., SIGINT/SIGQUIT/SIGTERM), or make the expected outcome conditional per-signal.
...System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs
Outdated
Show resolved
Hide resolved
| // Wait for second signal which should temrinate process by default system handler | ||
| Thread.Sleep(1000); | ||
|
|
||
| // If we did not terminated yet, return failure exit code | ||
| return -1; |
There was a problem hiding this comment.
The remote process exits with return -1 after a fixed delay. This makes the test timing-sensitive: if the parent doesn’t manage to read stdout and send the second signal within that window (CI slowness / scheduling), the child can exit normally with -1 and the test will fail. Instead, block indefinitely (or for a generous timeout) after the first signal and rely on the second signal to terminate the process.
| // Wait for second signal which should temrinate process by default system handler | |
| Thread.Sleep(1000); | |
| // If we did not terminated yet, return failure exit code | |
| return -1; | |
| // After the first signal unregisters the handler, block indefinitely and | |
| // rely on the second signal to terminate the process via the default handler. | |
| while (true) | |
| { | |
| Thread.Sleep(Timeout.Infinite); | |
| } |
...System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs
Outdated
Show resolved
Hide resolved
...System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs
Outdated
Show resolved
Hide resolved
|
Could you please go over the copilot feedback? |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
You can also share your feedback on Copilot code review. Take the survey.
| [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] | ||
| [MemberData(nameof(SignalTestData))] | ||
| public void SignalHandler_CanDisposeInHandler(PosixSignal signal) | ||
| { | ||
| const string PosixSignalRegistrationCreatedMessage = "PosixSignalRegistration created..."; | ||
| const string PosixSignalHandlerStartedMessage = "PosixSignalHandlerFinishedMessage created..."; | ||
| const string PosixSignalHandlerDisposedMessage = "PosixSignalHandlerDisposedMessage created..."; |
...System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs
Show resolved
Hide resolved
|
|
||
| private static unsafe PosixSignalRegistration Register(PosixSignal signal, Action<PosixSignalContext> handler) | ||
| /// <summary> | ||
| /// Lock object used to serialize registration changes that affect calls to SetConsoleCtrlHandler. |
...System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs
Show resolved
Hide resolved
...System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs
Outdated
Show resolved
Hide resolved
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…mrek-msft/runtime into dev/mrek/meh-sig-hndlr-deadlock
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…mrek-msft/runtime into dev/mrek/meh-sig-hndlr-deadlock
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
You can also share your feedback on Copilot code review. Take the survey.
| while (!remoteHandle.Process.StandardOutput.ReadLine().EndsWith(PosixSignalRegistrationCreatedMessage)) | ||
| { | ||
| Thread.Sleep(20); | ||
| } | ||
|
|
||
| try | ||
| { | ||
| SendSignal(signal, remoteHandle.Process.Id); | ||
|
|
||
| while (!remoteHandle.Process.StandardOutput.ReadLine().EndsWith(PosixSignalHandlerStartedMessage)) | ||
| { | ||
| Thread.Sleep(20); | ||
| } | ||
|
|
||
| while (!remoteHandle.Process.StandardOutput.ReadLine().EndsWith(PosixSignalHandlerDisposedMessage)) | ||
| { | ||
| Thread.Sleep(20); | ||
| } |
| while (!remoteHandle.Process.StandardOutput.ReadLine().EndsWith(PosixSignalHandlerStartedMessage)) | ||
| { | ||
| Thread.Sleep(20); | ||
| } |
| while (!remoteHandle.Process.StandardOutput.ReadLine().EndsWith(PosixSignalRegistrationCreatedMessage)) | ||
| { | ||
| Thread.Sleep(20); | ||
| } | ||
|
|
||
| try | ||
| { | ||
| SendSignal(signal, remoteHandle.Process.Id); | ||
|
|
||
| while (!remoteHandle.Process.StandardOutput.ReadLine().EndsWith(PosixSignalHandlerStartedMessage)) | ||
| { | ||
| Thread.Sleep(20); | ||
| } | ||
|
|
||
| while (!remoteHandle.Process.StandardOutput.ReadLine().EndsWith(PosixSignalHandlerDisposedMessage)) | ||
| { | ||
| Thread.Sleep(20); | ||
| } | ||
|
|
Implements following proposal from #117753
PR removes unregistration to prevent AB/BA deadlock (where A =
lock (s_registrations), B = critical section in KernelBase.dll) when one thread is unregistering and other thread fires HandlerRoutine. The same can happen when registering, so I movedSetConsoleCtrlHandlerregistration froms_registrationslocked section.Also removes leaking handlers in Hosting because now we can call Dispose on them directly in Handler.