Skip to content

Commit dd2dc6a

Browse files
committed
flatpak-spawn: Make signal handling async-signal-safe
signal(2) is annoying to use because the signal handler is restricted to a limited set of async-signal-safe functions (see signal-safety(7)). For example, the signal handler might be called while the same thread is halfway through a call to malloc(), so it is not necessarily safe for it to allocate memory - which is a problem, because g_idle_add() almost certainly allocates memory. Solving this portably would require tricky Unix code like the implementation that's behind g_unix_signal_source_new() (which we can't use directly here, because it is documented not to support SIGQUIT, SIGCONT or SIGTSTP). However, Flatpak is Linux-specific, and Linux since at least 2.6.27 (2008) implements signalfd(), which delivers signals into a poll()-based main loop - exactly what we want here. Resolves: #29 Signed-off-by: Simon McVittie <[email protected]>
1 parent 4fdd2e2 commit dd2dc6a

File tree

3 files changed

+130
-15
lines changed

3 files changed

+130
-15
lines changed

meson.build

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ conf.set('_GNU_SOURCE', 1)
2828
config_h = configure_file(output: 'config.h', configuration: conf)
2929

3030
gio_unix = dependency('gio-unix-2.0')
31+
threads = dependency('threads')
3132

3233
srcinc = include_directories('src')
3334

src/flatpak-spawn.c

+128-14
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,22 @@
2323
#include <stdio.h>
2424
#include <stdlib.h>
2525
#include <string.h>
26+
#include <sys/signalfd.h>
2627
#include <sys/stat.h>
2728
#include <sys/types.h>
2829
#include <sys/wait.h>
30+
#include <glib-unix.h>
2931
#include <gio/gio.h>
3032
#include <gio/gunixfdlist.h>
3133

3234
#include "backport-autoptr.h"
3335

36+
/* Change to #if 1 to check backwards-compatibility code paths */
37+
#if 0
38+
#undef GLIB_CHECK_VERSION
39+
#define GLIB_CHECK_VERSION(x, y, z) (0)
40+
#endif
41+
3442
typedef enum {
3543
FLATPAK_SPAWN_FLAGS_CLEAR_ENV = 1 << 0,
3644
FLATPAK_SPAWN_FLAGS_LATEST_VERSION = 1 << 1,
@@ -136,14 +144,37 @@ message_handler (G_GNUC_UNUSED const gchar *log_domain,
136144
g_printerr ("%s: %s\n", g_get_prgname (), message);
137145
}
138146

139-
static gboolean
140-
forward_signal_idle_cb (gpointer user_data)
147+
static void
148+
forward_signal (int sig)
141149
{
142-
int sig = GPOINTER_TO_INT(user_data);
143150
g_autoptr(GVariant) reply = NULL;
144151
gboolean to_process_group = FALSE;
145152
g_autoptr(GError) error = NULL;
146153

154+
if (child_pid == 0)
155+
{
156+
/* We are not monitoring a child yet, so let the signal act on
157+
* this main process instead */
158+
if (sig == SIGTSTP || sig == SIGTSTP)
159+
{
160+
raise (SIGSTOP);
161+
}
162+
else if (sig != SIGCONT)
163+
{
164+
sigset_t mask;
165+
166+
sigemptyset (&mask);
167+
sigaddset (&mask, sig);
168+
/* Unblock it, so that it will be delivered properly this time.
169+
* Use pthread_sigmask instead of sigprocmask because the latter
170+
* has unspecified behaviour in a multi-threaded process. */
171+
pthread_sigmask (SIG_UNBLOCK, &mask, NULL);
172+
raise (sig);
173+
}
174+
175+
return;
176+
}
177+
147178
g_debug ("Forwarding signal: %d", sig);
148179

149180
/* We forward stop requests as real stop, because the default doesn't
@@ -174,26 +205,97 @@ forward_signal_idle_cb (gpointer user_data)
174205
g_debug ("SIGSTOP:ing flatpak-spawn");
175206
raise (SIGSTOP);
176207
}
177-
178-
return G_SOURCE_REMOVE;
179208
}
180209

181-
static void
182-
forward_signal_handler (int sig)
210+
static gboolean
211+
forward_signal_handler (
212+
#if GLIB_CHECK_VERSION (2, 36, 0)
213+
int sfd,
214+
#else
215+
GIOChannel *source,
216+
#endif
217+
G_GNUC_UNUSED GIOCondition condition,
218+
G_GNUC_UNUSED gpointer data)
183219
{
184-
g_idle_add (forward_signal_idle_cb, GINT_TO_POINTER(sig));
220+
struct signalfd_siginfo info;
221+
ssize_t size;
222+
223+
#if !GLIB_CHECK_VERSION (2, 36, 0)
224+
int sfd;
225+
226+
sfd = g_io_channel_unix_get_fd (source);
227+
g_return_val_if_fail (sfd >= 0, G_SOURCE_CONTINUE);
228+
#endif
229+
230+
size = read (sfd, &info, sizeof (info));
231+
232+
if (size < 0)
233+
{
234+
if (errno != EINTR && errno != EAGAIN)
235+
g_warning ("Unable to read struct signalfd_siginfo: %s",
236+
g_strerror (errno));
237+
}
238+
else if (size != sizeof (info))
239+
{
240+
g_warning ("Expected struct signalfd_siginfo of size %"
241+
G_GSIZE_FORMAT ", got %" G_GSSIZE_FORMAT,
242+
sizeof (info), size);
243+
}
244+
else
245+
{
246+
forward_signal (info.ssi_signo);
247+
}
248+
249+
return G_SOURCE_CONTINUE;
185250
}
186251

187-
static void
252+
static guint
188253
forward_signals (void)
189254
{
190-
int forward[] = {
255+
static int forward[] = {
191256
SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGCONT, SIGTSTP, SIGUSR1, SIGUSR2
192257
};
258+
sigset_t mask;
193259
guint i;
260+
int sfd;
261+
262+
sigemptyset (&mask);
263+
264+
for (i = 0; i < G_N_ELEMENTS (forward); i++)
265+
sigaddset (&mask, forward[i]);
266+
267+
sfd = signalfd (-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
194268

195-
for (i = 0; i < G_N_ELEMENTS(forward); i++)
196-
signal (forward[i], forward_signal_handler);
269+
if (sfd < 0)
270+
{
271+
g_warning ("Unable to watch signals: %s", g_strerror (errno));
272+
return 0;
273+
}
274+
275+
/*
276+
* We have to block the signals, for two reasons:
277+
* - If we didn't, most of them would kill our process.
278+
* Listening for a signal with a signalfd does not prevent the signal's
279+
* default disposition from being acted on.
280+
* - Reading from a signalfd only returns information about the signals
281+
* that are still pending for the process. If we ignored them instead
282+
* of blocking them, they would no longer be pending by the time the
283+
* main loop wakes up and reads from the signalfd.
284+
*/
285+
pthread_sigmask (SIG_BLOCK, &mask, NULL);
286+
287+
#if GLIB_CHECK_VERSION (2, 36, 0)
288+
return g_unix_fd_add (sfd, G_IO_IN, forward_signal_handler, NULL);
289+
#else
290+
GIOChannel *channel = g_io_channel_unix_new (sfd);
291+
guint ret;
292+
293+
/* Disable text recoding, treat it as a bytestream */
294+
g_io_channel_set_encoding (channel, NULL, NULL);
295+
ret = g_io_add_watch (channel, G_IO_IN, forward_signal_handler, NULL);
296+
g_io_channel_unref (channel);
297+
return ret;
298+
#endif
197299
}
198300

199301
static void
@@ -473,6 +575,7 @@ main (int argc,
473575
{ "directory", 0, 0, G_OPTION_ARG_FILENAME, &opt_directory, "Working directory in which to run the command", "DIR" },
474576
{ NULL }
475577
};
578+
guint signal_source = 0;
476579

477580
setlocale (LC_ALL, "");
478581

@@ -517,6 +620,16 @@ main (int argc,
517620
if (verbose)
518621
g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, message_handler, NULL);
519622

623+
/* We have to block the signals we want to forward before we start any
624+
* other thread, and in particular the GDBus worker thread, because
625+
* the signal mask is per-thread. We need all threads to have the same
626+
* mask, otherwise a thread that doesn't have the mask will receive
627+
* process-directed signals, causing the whole process to exit. */
628+
signal_source = forward_signals ();
629+
630+
if (signal_source == 0)
631+
return 1;
632+
520633
session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
521634
if (session_bus == NULL)
522635
{
@@ -820,13 +933,14 @@ main (int argc,
820933

821934
g_debug ("child_pid: %d", child_pid);
822935

823-
forward_signals ();
824-
825936
loop = g_main_loop_new (NULL, FALSE);
826937

827938
g_signal_connect (session_bus, "closed", G_CALLBACK (session_bus_closed_cb), loop);
828939

829940
g_main_loop_run (loop);
830941

942+
if (signal_source != 0)
943+
g_source_remove (signal_source);
944+
831945
return 0;
832946
}

src/meson.build

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
flatpak_spawn = executable(
22
'flatpak-spawn',
33
sources: 'flatpak-spawn.c',
4-
dependencies: [gio_unix],
4+
dependencies: [gio_unix, threads],
55
c_args: ['-include', '@0@'.format(config_h)],
66
install: true,
77
)

0 commit comments

Comments
 (0)