Skip to content

Commit ca63ce6

Browse files
authored
Deliver Posix signals in reverse order of their registration (#116557)
Fixes #116556
1 parent 8a35055 commit ca63ce6

File tree

4 files changed

+95
-66
lines changed

4 files changed

+95
-66
lines changed

src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Unix.cs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ namespace System.Runtime.InteropServices
99
{
1010
public sealed partial class PosixSignalRegistration
1111
{
12-
private static readonly Dictionary<int, HashSet<Token>> s_registrations = Initialize();
12+
private static readonly Dictionary<int, List<Token>> s_registrations = Initialize();
1313

14-
private static unsafe Dictionary<int, HashSet<Token>> Initialize()
14+
private static unsafe Dictionary<int, List<Token>> Initialize()
1515
{
1616
if (!Interop.Sys.InitializeTerminalAndSignalHandling())
1717
{
@@ -20,7 +20,7 @@ private static unsafe Dictionary<int, HashSet<Token>> Initialize()
2020

2121
Interop.Sys.SetPosixSignalHandler(&OnPosixSignal);
2222

23-
return new Dictionary<int, HashSet<Token>>();
23+
return new Dictionary<int, List<Token>>();
2424
}
2525

2626
private static PosixSignalRegistration Register(PosixSignal signal, Action<PosixSignalContext> handler)
@@ -36,9 +36,9 @@ private static PosixSignalRegistration Register(PosixSignal signal, Action<Posix
3636

3737
lock (s_registrations)
3838
{
39-
if (!s_registrations.TryGetValue(signo, out HashSet<Token>? tokens))
39+
if (!s_registrations.TryGetValue(signo, out List<Token>? tokens))
4040
{
41-
s_registrations[signo] = tokens = new HashSet<Token>();
41+
s_registrations[signo] = tokens = new List<Token>();
4242
}
4343

4444
if (tokens.Count == 0 &&
@@ -61,7 +61,7 @@ private void Unregister()
6161
{
6262
_token = null;
6363

64-
if (s_registrations.TryGetValue(token.SigNo, out HashSet<Token>? tokens))
64+
if (s_registrations.TryGetValue(token.SigNo, out List<Token>? tokens))
6565
{
6666
tokens.Remove(token);
6767
if (tokens.Count == 0)
@@ -81,7 +81,7 @@ private static int OnPosixSignal(int signo, PosixSignal signal)
8181

8282
lock (s_registrations)
8383
{
84-
if (s_registrations.TryGetValue(signo, out HashSet<Token>? registrations))
84+
if (s_registrations.TryGetValue(signo, out List<Token>? registrations))
8585
{
8686
tokens = new Token[registrations.Count];
8787
registrations.CopyTo(tokens);
@@ -122,16 +122,19 @@ static void HandleSignal(object? state)
122122
{
123123
(int signo, Token[] tokens) = ((int, Token[]))state!;
124124

125-
PosixSignalContext ctx = new(0);
126-
foreach (Token token in tokens)
125+
PosixSignalContext context = new(0);
126+
127+
// Iterate through the tokens in reverse order to match the order of registration.
128+
for (int i = tokens.Length - 1; i >= 0; i--)
127129
{
130+
Token token = tokens[i];
128131
// Different values for PosixSignal map to the same signo.
129132
// Match the PosixSignal value used when registering.
130-
ctx.Signal = token.Signal;
131-
token.Handler(ctx);
133+
context.Signal = token.Signal;
134+
token.Handler(context);
132135
}
133136

134-
if (!ctx.Cancel)
137+
if (!context.Cancel)
135138
{
136139
Interop.Sys.HandleNonCanceledPosixSignal(signo);
137140
}

src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.Windows.cs

Lines changed: 44 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,20 @@ namespace System.Runtime.InteropServices
88
{
99
public sealed partial class PosixSignalRegistration
1010
{
11-
private static readonly HashSet<Token> s_registrations = new();
11+
private static readonly Dictionary<int, List<Token>> s_registrations = new();
1212

1313
private static unsafe PosixSignalRegistration Register(PosixSignal signal, Action<PosixSignalContext> handler)
1414
{
15-
switch (signal)
15+
int signo = signal switch
1616
{
17-
case PosixSignal.SIGINT:
18-
case PosixSignal.SIGQUIT:
19-
case PosixSignal.SIGTERM:
20-
case PosixSignal.SIGHUP:
21-
break;
22-
23-
default:
24-
throw new PlatformNotSupportedException();
25-
}
26-
27-
var token = new Token(signal, handler);
17+
PosixSignal.SIGINT => Interop.Kernel32.CTRL_C_EVENT,
18+
PosixSignal.SIGQUIT => Interop.Kernel32.CTRL_BREAK_EVENT,
19+
PosixSignal.SIGTERM => Interop.Kernel32.CTRL_SHUTDOWN_EVENT,
20+
PosixSignal.SIGHUP => Interop.Kernel32.CTRL_CLOSE_EVENT,
21+
_ => throw new PlatformNotSupportedException()
22+
};
23+
24+
var token = new Token(signal, signo, handler);
2825
var registration = new PosixSignalRegistration(token);
2926

3027
lock (s_registrations)
@@ -35,7 +32,12 @@ private static unsafe PosixSignalRegistration Register(PosixSignal signal, Actio
3532
throw Win32Marshal.GetExceptionForLastWin32Error();
3633
}
3734

38-
s_registrations.Add(token);
35+
if (!s_registrations.TryGetValue(signo, out List<Token>? tokens))
36+
{
37+
s_registrations[signo] = tokens = new List<Token>();
38+
}
39+
40+
tokens.Add(token);
3941
}
4042

4143
return registration;
@@ -49,17 +51,25 @@ private unsafe void Unregister()
4951
{
5052
_token = null;
5153

52-
s_registrations.Remove(token);
53-
if (s_registrations.Count == 0 &&
54-
!Interop.Kernel32.SetConsoleCtrlHandler(&HandlerRoutine, Add: false))
54+
if (s_registrations.TryGetValue(token.SigNo, out List<Token>? tokens))
5555
{
56-
// Ignore errors due to the handler no longer being registered; this can happen, for example, with
57-
// direct use of Alloc/Attach/FreeConsole which result in the table of control handlers being reset.
58-
// Throw for everything else.
59-
int error = Marshal.GetLastPInvokeError();
60-
if (error != Interop.Errors.ERROR_INVALID_PARAMETER)
56+
tokens.Remove(token);
57+
if (tokens.Count == 0)
6158
{
62-
throw Win32Marshal.GetExceptionForWin32Error(error);
59+
s_registrations.Remove(token.SigNo);
60+
}
61+
62+
if (s_registrations.Count == 0 &&
63+
!Interop.Kernel32.SetConsoleCtrlHandler(&HandlerRoutine, Add: false))
64+
{
65+
// Ignore errors due to the handler no longer being registered; this can happen, for example, with
66+
// direct use of Alloc/Attach/FreeConsole which result in the table of control handlers being reset.
67+
// Throw for everything else.
68+
int error = Marshal.GetLastPInvokeError();
69+
if (error != Interop.Errors.ERROR_INVALID_PARAMETER)
70+
{
71+
throw Win32Marshal.GetExceptionForWin32Error(error);
72+
}
6373
}
6474
}
6575
}
@@ -69,38 +79,14 @@ private unsafe void Unregister()
6979
[UnmanagedCallersOnly]
7080
private static Interop.BOOL HandlerRoutine(int dwCtrlType)
7181
{
72-
PosixSignal signal;
73-
switch (dwCtrlType)
74-
{
75-
case Interop.Kernel32.CTRL_C_EVENT:
76-
signal = PosixSignal.SIGINT;
77-
break;
82+
Token[]? tokens = null;
7883

79-
case Interop.Kernel32.CTRL_BREAK_EVENT:
80-
signal = PosixSignal.SIGQUIT;
81-
break;
82-
83-
case Interop.Kernel32.CTRL_SHUTDOWN_EVENT:
84-
signal = PosixSignal.SIGTERM;
85-
break;
86-
87-
case Interop.Kernel32.CTRL_CLOSE_EVENT:
88-
signal = PosixSignal.SIGHUP;
89-
break;
90-
91-
default:
92-
return Interop.BOOL.FALSE;
93-
}
94-
95-
List<Token>? tokens = null;
9684
lock (s_registrations)
9785
{
98-
foreach (Token token in s_registrations)
86+
if (s_registrations.TryGetValue(dwCtrlType, out List<Token>? registrations))
9987
{
100-
if (token.Signal == signal)
101-
{
102-
(tokens ??= new List<Token>()).Add(token);
103-
}
88+
tokens = new Token[registrations.Count];
89+
registrations.CopyTo(tokens);
10490
}
10591
}
10692

@@ -109,10 +95,14 @@ private static Interop.BOOL HandlerRoutine(int dwCtrlType)
10995
return Interop.BOOL.FALSE;
11096
}
11197

112-
var context = new PosixSignalContext(signal);
113-
foreach (Token handler in tokens)
98+
var context = new PosixSignalContext(0);
99+
100+
// Iterate through the tokens in reverse order to match the order of registration.
101+
for (int i = tokens.Length - 1; i >= 0; i--)
114102
{
115-
handler.Handler(context);
103+
Token token = tokens[i];
104+
context.Signal = token.Signal;
105+
token.Handler(context);
116106
}
117107

118108
return context.Cancel ? Interop.BOOL.TRUE : Interop.BOOL.FALSE;

src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRegistration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public sealed partial class PosixSignalRegistration : IDisposable
2424
/// <exception cref="PlatformNotSupportedException"><paramref name="signal"/> is not supported by the platform.</exception>
2525
/// <exception cref="IOException">An error occurred while setting up the signal handling or while installing the handler for the specified signal.</exception>
2626
/// <remarks>
27+
/// The handlers are executed in reverse order of their registration.
2728
/// Raw values can be provided for <paramref name="signal"/> on Unix by casting them to <see cref="PosixSignal"/>.
2829
/// Default handling of the signal can be canceled through <see cref="PosixSignalContext.Cancel"/>.
2930
/// <see cref="PosixSignal.SIGINT"/> and <see cref="PosixSignal.SIGQUIT"/> can be canceled on both

src/libraries/System.Runtime.InteropServices/tests/System.Runtime.InteropServices.UnitTests/System/Runtime/InteropServices/PosixSignalRegistrationTests.Unix.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,41 @@ public void SignalHandlerWorksForSecondRegistration()
125125
}
126126
}
127127

128+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile))]
129+
public void SignalHandlersCalledInReverseOrder()
130+
{
131+
PosixSignal signal = PosixSignal.SIGCONT;
132+
bool secondHandlerCalled = false;
133+
134+
using SemaphoreSlim semaphore = new(0);
135+
using var first = PosixSignalRegistration.Create(signal, ctx =>
136+
{
137+
Assert.Equal(signal, ctx.Signal);
138+
139+
// Ensure signal doesn't cause the process to terminate.
140+
ctx.Cancel = true;
141+
142+
Assert.True(secondHandlerCalled);
143+
144+
semaphore.Release();
145+
});
146+
147+
using var second = PosixSignalRegistration.Create(signal, ctx =>
148+
{
149+
Assert.Equal(signal, ctx.Signal);
150+
151+
// Ensure signal doesn't cause the process to terminate.
152+
ctx.Cancel = true;
153+
154+
Assert.False(secondHandlerCalled);
155+
secondHandlerCalled = true;
156+
});
157+
158+
kill(signal);
159+
bool entered = semaphore.Wait(SuccessTimeout);
160+
Assert.True(entered);
161+
}
162+
128163
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile))]
129164
public void SignalHandlerNotCalledWhenDisposed()
130165
{

0 commit comments

Comments
 (0)