Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8d4df78
Register CtrlHandler only once on Windows to prevent AB/BA deadlock b…
mrek-msft Feb 26, 2026
346a77b
Reregister in case of Free/Attach/AllocConsole directly used.
mrek-msft Feb 26, 2026
0bcf834
Rework locking mechanism to handle possible races
mrek-msft Mar 6, 2026
adfcccb
Undo s_oneOfUnregisterExecuteLock
mrek-msft Mar 9, 2026
d9ab110
Whitespace changes cleanup
mrek-msft Mar 9, 2026
1a9751f
Update src/libraries/System.Private.CoreLib/src/System/Runtime/Intero…
jkotas Mar 9, 2026
7f9a9de
Merge branch 'main' into dev/mrek/meh-sig-hndlr-deadlock
mrek-msft Mar 12, 2026
e2184ec
Remove register/unregister lock and rename
mrek-msft Mar 12, 2026
44e6763
Capture and use fixed address of HandlerRoutine
mrek-msft Mar 12, 2026
d27d330
Adding test for testing that deadlock do not happen whne unregisterin…
mrek-msft Mar 12, 2026
b369a3c
Dispose handlers immidiately, do not wait for finialize time
mrek-msft Mar 11, 2026
6ed26b2
Merge remote-tracking branch 'remotes/mrek/dev/mrek/meh-sig-hndlr-dea…
mrek-msft Mar 12, 2026
c4250f3
Fix typo in comment
mrek-msft Mar 13, 2026
43b751f
Fix misleading comment on lock object.
mrek-msft Mar 13, 2026
481abee
Move unsafe from partial class to members needing it.
mrek-msft Mar 13, 2026
a7ed82c
Fix typo in comment
mrek-msft Mar 13, 2026
5af264a
revert introduced using static.
mrek-msft Mar 13, 2026
5e59198
Improve comment grammar.
mrek-msft Mar 13, 2026
41eb483
Merge branch 'dev/mrek/meh-sig-hndlr-deadlock' of https://github.com/…
mrek-msft Mar 13, 2026
2082289
Fix wrong const message
mrek-msft Mar 13, 2026
781813f
Improve comment grammar
mrek-msft Mar 13, 2026
54be37c
Fix wrong naming convetion
mrek-msft Mar 13, 2026
23a28b4
Merge branch 'dev/mrek/meh-sig-hndlr-deadlock' of https://github.com/…
mrek-msft Mar 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,7 @@ private void HandleWindowsShutdown(PosixSignalContext context)
// for SIGTERM on Windows we must block this thread until the application is finished
// otherwise the process will be killed immediately on return from this handler

// don't allow Dispose to unregister handlers, since Windows has a lock that prevents the unregistration while this handler is running
// just leak these, since the process is exiting
_sigIntRegistration = null;
_sigQuitRegistration = null;
_sigTermRegistration = null;
UnregisterShutdownHandlers();

ApplicationLifetime.StopApplication();

Expand Down
92 changes: 92 additions & 0 deletions src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,98 @@ public void TestCreateNewProcessGroup_HandlerReceivesExpectedSignal(PosixSignal
}
}

[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[MemberData(nameof(SignalTestData))]
public void SignalHandler_CanDisposeInHandler(PosixSignal signal)
Comment on lines +164 to +166
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
{
const string PosixSignalRegistrationCreatedMessage = "PosixSignalRegistration created...";
const string PosixSignalHandlerStartedMessage = "PosixSignalHandlerStartedMessage created...";
const string PosixSignalHandlerDisposedMessage = "PosixSignalHandlerDisposedMessage created...";
Comment on lines +164 to +170

var remoteInvokeOptions = new RemoteInvokeOptions { CheckExitCode = false };
remoteInvokeOptions.StartInfo.RedirectStandardOutput = true;
if (OperatingSystem.IsWindows())
{
remoteInvokeOptions.StartInfo.CreateNewProcessGroup = true;
}

using RemoteInvokeHandle remoteHandle = RemoteExecutor.Invoke(
(signalStr) =>
{
PosixSignal expectedSignal = Enum.Parse<PosixSignal>(signalStr);
using ManualResetEvent receivedSignalEvent = new ManualResetEvent(false);
ReEnableCtrlCHandlerIfNeeded(expectedSignal);

PosixSignalRegistration? p = null;
p = PosixSignalRegistration.Create(expectedSignal, (ctx) =>
{
Console.WriteLine(PosixSignalHandlerStartedMessage);
Assert.Equal(expectedSignal, ctx.Signal);

Assert.NotNull(p);
p?.Dispose();

// Used for checking that Dispose returned and did not get stuck
Console.WriteLine(PosixSignalHandlerDisposedMessage);

receivedSignalEvent.Set();
ctx.Cancel = true;
});

Console.WriteLine(PosixSignalRegistrationCreatedMessage);

// Wait for signal which unregisters itself
Assert.True(receivedSignalEvent.WaitOne(WaitInMS));

// Wait for second signal which should terminate process by default system handler
Thread.Sleep(1000);

// If we did not terminated yet, return failure exit code
return -1;
},
arg: $"{signal}",
remoteInvokeOptions);

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);
}
Comment on lines +225 to +228

while (!remoteHandle.Process.StandardOutput.ReadLine().EndsWith(PosixSignalHandlerDisposedMessage))
{
Thread.Sleep(20);
}
Comment on lines +216 to +233

Comment on lines +216 to +234
SendSignal(signal, remoteHandle.Process.Id);

Assert.True(remoteHandle.Process.WaitForExit(WaitInMS));
Assert.True(remoteHandle.Process.StandardOutput.EndOfStream);
if (OperatingSystem.IsWindows())
{
Assert.Equal(unchecked((int)0xC000013A), remoteHandle.Process.ExitCode); // STATUS_CONTROL_C_EXIT
}
else
{
Assert.Equal(0, remoteHandle.Process.ExitCode);
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On non-Windows platforms, letting a terminating signal take the default action typically ends the process with a signal-based exit code (e.g., 128+signo; SIGTERM commonly yields 143), not 0. The Unix branch Assert.Equal(0, remoteHandle.Process.ExitCode) is therefore incorrect for SIGINT/SIGQUIT/SIGTERM in this test; assert the signal-based exit code (or restrict the test to platforms/signals where 0 is expected).

Suggested change
Assert.Equal(0, remoteHandle.Process.ExitCode);
int expectedExitCode = 128 + (int)signal;
Assert.Equal(expectedExitCode, remoteHandle.Process.ExitCode);

Copilot uses AI. Check for mistakes.
}
}
finally
{
// If sending the signal fails, we want to kill the process ASAP
// to prevent RemoteExecutor's timeout from hiding it.
remoteHandle.Process.Kill();
}
}

[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[InlineData(-2)]
[InlineData((long)int.MaxValue + 1)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ public sealed partial class PosixSignalRegistration
{
private static readonly Dictionary<int, List<Token>> s_registrations = new();

/// <summary>
/// Lock object used to serialize registration changes that affect calls to SetConsoleCtrlHandler.
/// </summary>
private static readonly object s_registerLock = new();

/// <summary>
/// Runtime can generate multiple addresses to the same function. To ensure that registering and unregistering always
/// use the same instance, we capture it in this static field.
/// </summary>
private static readonly unsafe delegate* unmanaged<int, Interop.BOOL> s_handlerRoutineAddr = &HandlerRoutine;

private static unsafe PosixSignalRegistration Register(PosixSignal signal, Action<PosixSignalContext> handler)
{
int signo = signal switch
Expand All @@ -24,26 +35,53 @@ private static unsafe PosixSignalRegistration Register(PosixSignal signal, Actio
var token = new Token(signal, signo, handler);
var registration = new PosixSignalRegistration(token);

lock (s_registrations)
lock (s_registerLock)
{
if (s_registrations.Count == 0 &&
!Interop.Kernel32.SetConsoleCtrlHandler(&HandlerRoutine, Add: true))
bool registerCtrlHandler = false;
lock (s_registrations)
{
throw Win32Marshal.GetExceptionForLastWin32Error();
if (s_registrations.Count == 0)
{
registerCtrlHandler = true;
}
}

if (!s_registrations.TryGetValue(signo, out List<Token>? tokens))
if (registerCtrlHandler)
{
s_registrations[signo] = tokens = new List<Token>();
// User may reset registrations externally by direct calls of Free/Attach/AllocConsole.
// We do not know if it is currently registered or not. To prevent duplicate
// registration, we try unregister existing one first.
if (!Interop.Kernel32.SetConsoleCtrlHandler(s_handlerRoutineAddr, Add: false))
{
// Returns ERROR_INVALID_PARAMETER if it was not registered. Throw for everything else.
int error = Marshal.GetLastPInvokeError();
if (error != Interop.Errors.ERROR_INVALID_PARAMETER)
{
throw Win32Marshal.GetExceptionForWin32Error(error);
}
}

if (!Interop.Kernel32.SetConsoleCtrlHandler(s_handlerRoutineAddr, Add: true))
{
throw Win32Marshal.GetExceptionForLastWin32Error();
}
}

tokens.Add(token);
lock (s_registrations)
{
if (!s_registrations.TryGetValue(signo, out List<Token>? tokens))
{
s_registrations[signo] = tokens = new List<Token>();
}

tokens.Add(token);
}
}

return registration;
}

private unsafe void Unregister()
private void Unregister()
{
lock (s_registrations)
{
Expand All @@ -58,19 +96,6 @@ private unsafe void Unregister()
{
s_registrations.Remove(token.SigNo);
}

if (s_registrations.Count == 0 &&
!Interop.Kernel32.SetConsoleCtrlHandler(&HandlerRoutine, Add: false))
{
// Ignore errors due to the handler no longer being registered; this can happen, for example, with
// direct use of Alloc/Attach/FreeConsole which result in the table of control handlers being reset.
// Throw for everything else.
int error = Marshal.GetLastPInvokeError();
if (error != Interop.Errors.ERROR_INVALID_PARAMETER)
{
throw Win32Marshal.GetExceptionForWin32Error(error);
}
}
}
}
}
Expand Down
Loading