Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 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
25cdb3c
Improve remote proces emmited messages
mrek-msft Mar 13, 2026
cc720e3
Comments improvements.
mrek-msft Mar 13, 2026
af28e8f
Move waiting for console message to separate method, add timeout. Cha…
mrek-msft Mar 16, 2026
8132a94
Await canceled read task in AssertRemoteProcessStandardOutputLine
mrek-msft Mar 16, 2026
dad39de
Fix typo
mrek-msft Mar 16, 2026
c48b00b
Fix adding negative signal number
mrek-msft Mar 16, 2026
f19e405
Rework unix return code checks once more.
mrek-msft Mar 16, 2026
525f3b8
Rework timeout to CTS with timeout + OperationCanceledException catching
mrek-msft Mar 16, 2026
6fef7a4
Do not kill process if already exited in finally.
mrek-msft Mar 16, 2026
73957d1
Better handling of Kill() failures
mrek-msft Mar 16, 2026
190d349
Remove ifdef for calling Process.Kill on old .NET Core.
mrek-msft Mar 17, 2026
f5b9924
Terminate by SIGTERM instead of SIGQUIT on mono on unix systems.
mrek-msft Mar 18, 2026
b0fd4b5
Merge branch 'main' into dev/mrek/meh-sig-hndlr-deadlock
mrek-msft Mar 18, 2026
02b45b5
Merge branch 'main' into dev/mrek/meh-sig-hndlr-deadlock
jkotas Mar 19, 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
124 changes: 120 additions & 4 deletions src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,31 @@ private void AssertNonZeroAllZeroDarwin(long value)
}
}

private static void AssertRemoteProcessStandardOutputLine(RemoteInvokeHandle remoteHandle, string expectedMessage, int timeout)
{
using CancellationTokenSource cts = new(timeout);

Task<string?> readTask = remoteHandle.Process.StandardOutput.ReadLineAsync(cts.Token).AsTask();
string? line;
try
{
line = readTask.ConfigureAwait(false).GetAwaiter().GetResult();
}
catch (OperationCanceledException)
{
throw new XunitException($"Expected message '{expectedMessage}' was not observed on remote process within specified time.");
}

if (line != null)
{
Assert.Equal(expectedMessage, line);
}
else
{
throw new XunitException($"StandardOutput was closed before observing expected message '{expectedMessage}'");
}
}

public static IEnumerable<object[]> SignalTestData()
{
if (OperatingSystem.IsWindows())
Expand Down Expand Up @@ -141,10 +166,7 @@ public void TestCreateNewProcessGroup_HandlerReceivesExpectedSignal(PosixSignal
arg: $"{signal}",
remoteInvokeOptions);

while (!remoteHandle.Process.StandardOutput.ReadLine().EndsWith(PosixSignalRegistrationCreatedMessage))
{
Thread.Sleep(20);
}
AssertRemoteProcessStandardOutputLine(remoteHandle, PosixSignalRegistrationCreatedMessage, WaitInMS);

try
{
Expand All @@ -161,6 +183,100 @@ public void TestCreateNewProcessGroup_HandlerReceivesExpectedSignal(PosixSignal
}
}

[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
// Limited only to signals which terminates process by default, and are supported on all platforms by SendSignal
[InlineData(PosixSignal.SIGINT)]
[InlineData(PosixSignal.SIGQUIT)]
public void SignalHandler_CanDisposeInHandler(PosixSignal signal)
{
const string PosixSignalRegistrationCreatedMessage = "PosixSignalRegistration created.";
const string PosixSignalHandlerStartedMessage = "PosixSignalRegistration handler started.";
const string PosixSignalHandlerDisposedMessage = "PosixSignalRegistration disposed.";
const int UnterminatedExitCode = -1;

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(WaitInMS);

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


try
{
AssertRemoteProcessStandardOutputLine(remoteHandle, PosixSignalRegistrationCreatedMessage, WaitInMS);

SendSignal(signal, remoteHandle.Process.Id);

AssertRemoteProcessStandardOutputLine(remoteHandle, PosixSignalHandlerStartedMessage, WaitInMS);
AssertRemoteProcessStandardOutputLine(remoteHandle, PosixSignalHandlerDisposedMessage, WaitInMS);

SendSignal(signal, remoteHandle.Process.Id);

// https://github.com/dotnet/runtime/issues/125733
if (PlatformDetection.IsMonoRuntime && signal == PosixSignal.SIGQUIT && !PlatformDetection.IsWindows)
{
SendSignal(PosixSignal.SIGTERM, 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
{
// Signal numbers are platform dependent, so we can't check exact exit code
Assert.NotEqual(0, remoteHandle.Process.ExitCode);
Assert.NotEqual(UnterminatedExitCode, remoteHandle.Process.ExitCode);
}
}
finally
{
// If sending the signal fails or process did not exit on its own, 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,18 @@ public sealed partial class PosixSignalRegistration
{
private static readonly Dictionary<int, List<Token>> s_registrations = new();

/// <summary>
/// Serializes concurrent <see cref="Register"/> calls to make the emptiness check and
/// the subsequent token insertion atomic.
/// </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 +36,56 @@ 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))
// All SetConsoleCtrlHandler calls must happen outside s_registrations locked section
// otherwise we risk AB/BA deadlock between it and internal critical section in OS.

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 +100,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