Skip to content

Commit

Permalink
Improve signal handling for service processes
Browse files Browse the repository at this point in the history
Keep the original signal mask that is set when dinit starts and apply it
(minus a few specific signals) to service processes. Mask all signals
that can be invoked via the terminal by default; add an option,
"unmask-intr", to unmask SIGINT.

In addition, mask all signals after starting if dinit is running in
system manager mode, to avoid the risk of death of suspension by an
unhandled signal.
  • Loading branch information
davmac314 committed Aug 13, 2023
1 parent b8ba599 commit 6eaaa3b
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 28 deletions.
36 changes: 29 additions & 7 deletions doc/manpages/dinit-service.5.m4
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@ These options are specified via the \fBoptions\fR parameter.
.TP
\fBruns\-on\-console\fR
Specifies that this service uses the console; its input and output should be
directed to the console (or precisely, to the device to which Dinit's standard
directed to the console (or precisely, to the device to which \fBdinit\fR's standard
output stream is connected).
A service running on the console prevents other services from running on the
console (they will queue for the console).
Expand All @@ -524,10 +524,10 @@ Proper operation of this option (and related options) assumes that \fBdinit\fR
is itself attached correctly to the console device (or a terminal, in which case
that terminal will be used as the "console").
.sp
The \fIinterrupt\fR key (normally control-C) will be active for process / scripted
services that run on the console.
Handling of an interrupt is determined by the service process, but typically will
cause it to terminate.
The \fIinterrupt\fR key (normally control-C) may be active for process / scripted
services that run on the console, depending on terminal configuration and operating-system
specifics.
The interrupt signal (SIGINT), however, is masked by default (but see \fBunmask\-intr\fR).
.TP
\fBstarts\-on\-console\fR
Specifies that this service uses the console during service startup.
Expand All @@ -549,6 +549,27 @@ This is mutually exclusive with both \fBstarts\-on\-console\fR and \fBruns\-on\-
setting this option unsets both those options, and setting either of those options unsets
this option.
.TP
\fBunmask\-intr\fR
For services that run or start on the console, specifies that the terminal interrupt signal
(SIGINT, normally invoked by control-C) should be unmasked.
Handling of an interrupt is determined by the service process, but typically will
cause it to terminate.
This option may therefore be used to allow a service to be terminated by the user via
a keypress combination.
In combination with \fBskippable\fR, it may allow service startup to be skipped.
.sp
A service with this option will typically also have the \fBstart\-interruptible\fR option
set.
.sp
Note that whether an interrupt can be generated, and the key combination required to do so,
depends on the operating system's handling of the console device and, if it is a terminal,
how the terminal is configured; see \fBstty\fR(1).
.sp
Note also that a process may choose to mask or unmask the interrupt signal of its own accord,
once it has started.
Shells, in particular, may unmask the signal; it might not be possible to reliably run a shell
script on the console without allowing a user to interrupt it.
.TP
\fBstarts\-rwfs\fR
This service mounts the root filesystem read/write (or at least mounts the
normal writable filesystems for the system).
Expand Down Expand Up @@ -576,8 +597,9 @@ You should not use this option unless the service is designed to receive a Dinit
control socket.
.TP
\fBstart\-interruptible\fR
This service can have its startup interrupted (cancelled) if it becomes inactive
while still starting, by sending it the SIGINT signal.
Indicates that this service can have its startup interrupted (cancelled), by sending it the SIGINT signal.
If service state changes such that this service will stop, but it is currently starting, and this option
is set, then Dinit will attempt to interrupt it rather than waiting for its startup to complete.
This is meaningful only for \fBbgprocess\fR and \fBscripted\fR services.
.TP
\fBskippable\fR
Expand Down
1 change: 1 addition & 0 deletions src/baseproc-service.cc
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ bool base_process_service::start_ps_process(const std::vector<const char *> &cmd
run_proc_params run_params{cmd.data(), working_dir_c, logfile, pipefd[1], run_as_uid, run_as_gid, rlimits};
run_params.on_console = on_console;
run_params.in_foreground = !onstart_flags.shares_console;
run_params.unmask_sigint = onstart_flags.unmask_intr;
run_params.csfd = control_socket[1];
run_params.socket_fd = socket_fd;
run_params.notify_fd = notify_pipe[1];
Expand Down
20 changes: 14 additions & 6 deletions src/dinit.cc
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ bool external_log_open = false;
int active_control_conns = 0;
int socket_ready_fd = -1;

sigset_t orig_signal_mask; // signal mask when started

// Control socket path. We maintain a string (control_socket_str) in case we need
// to allocate storage, but control_socket_path is the authoritative value.
static const char *control_socket_path = SYSCONTROLSOCKET;
Expand Down Expand Up @@ -470,12 +472,18 @@ int dinit_main(int argc, char **argv)

/* Set up signal handlers etc */
sigset_t sigwait_set;
sigemptyset(&sigwait_set);
sigaddset(&sigwait_set, SIGCHLD);
sigaddset(&sigwait_set, SIGINT);
sigaddset(&sigwait_set, SIGTERM);
if (am_system_mgr) sigaddset(&sigwait_set, SIGQUIT);
sigprocmask(SIG_BLOCK, &sigwait_set, NULL);
if (am_system_mgr) {
// Block all signals in system manager mode - don't want to chance provoking a signal that
// will suspend or terminate the process
sigfillset(&sigwait_set);
}
else {
sigemptyset(&sigwait_set);
sigaddset(&sigwait_set, SIGCHLD);
sigaddset(&sigwait_set, SIGINT);
sigaddset(&sigwait_set, SIGTERM);
}
sigprocmask(SIG_BLOCK, &sigwait_set, &orig_signal_mask);

// Terminal access control signals - we ignore these so that dinit can't be
// suspended if it writes to the terminal after some other process has claimed
Expand Down
6 changes: 5 additions & 1 deletion src/includes/load-service.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ struct service_flags_t
bool runs_on_console : 1; // run "in the foreground"
bool starts_on_console : 1; // starts in the foreground
bool shares_console : 1; // run on console, but not exclusively
bool unmask_intr : 1; // (if runs/starts on console) unmask SIGINTR
bool pass_cs_fd : 1; // pass this service a control socket connection via fd
bool start_interruptible : 1; // the startup of this service process is ok to interrupt with SIGINT
bool skippable : 1; // if interrupted the service is skipped (scripted services)
Expand All @@ -39,7 +40,7 @@ struct service_flags_t
bool kill_all_on_stop : 1; // kill all other processes before stopping this service

service_flags_t() noexcept : rw_ready(false), log_ready(false),
runs_on_console(false), starts_on_console(false), shares_console(false),
runs_on_console(false), starts_on_console(false), shares_console(false), unmask_intr(false),
pass_cs_fd(false), start_interruptible(false), skippable(false), signal_process_only(false),
always_chain(false), kill_all_on_stop(false)
{
Expand Down Expand Up @@ -1229,6 +1230,9 @@ void process_service_line(settings_wrapper &settings, const char *name, string &
settings.onstart_flags.runs_on_console = false;
settings.onstart_flags.starts_on_console = false;
}
else if (option_txt == "unmask-intr") {
settings.onstart_flags.unmask_intr = true;
}
else if (option_txt == "pass-cs-fd") {
settings.onstart_flags.pass_cs_fd = true;
}
Expand Down
1 change: 1 addition & 0 deletions src/includes/proc-service.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ struct run_proc_params
#endif
bool on_console; // whether to run on console
bool in_foreground; // if on console: whether to run in foreground
bool unmask_sigint = false; // if in foreground: whether to unmask SIGINT
int wpipefd; // pipe to which error status will be sent (if error occurs)
int csfd; // control socket fd (or -1); may be moved
int socket_fd; // pre-opened socket fd (or -1); may be moved
Expand Down
41 changes: 27 additions & 14 deletions src/run-child-proc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ extern bool have_cgroups_path;
#include <grp.h>
#endif

extern sigset_t orig_signal_mask;

// Move an fd, if necessary, to another fd. The original destination fd will be closed.
// if fd is specified as -1, returns -1 immediately. Returns 0 on success.
static int move_fd(int fd, int dest)
Expand Down Expand Up @@ -70,19 +72,14 @@ void base_process_service::run_child_proc(run_proc_params params) noexcept

// If the console already has a session leader, presumably it is us. On the other hand
// if it has no session leader, and we don't create one, then control inputs such as
// ^C will have no effect.
bool do_set_ctty = (tcgetsid(0) == -1);
// ^C will have no effect. (We check here, before we potentially re-assign STDIN).
bool do_set_ctty = on_console && (tcgetsid(0) == -1);

// Copy signal mask, but unmask signals that we masked on startup. For the moment, we'll
// also block all signals, since apparently dup() can be interrupted (!!! really, POSIX??).
sigset_t sigwait_set;
// For the moment, we'll block all signals, since apparently even dup() can be interrupted
// (thanks, POSIX...).
sigset_t sigall_set;
sigfillset(&sigall_set);
sigprocmask(SIG_SETMASK, &sigall_set, &sigwait_set);
sigdelset(&sigwait_set, SIGCHLD);
sigdelset(&sigwait_set, SIGINT);
sigdelset(&sigwait_set, SIGTERM);
sigdelset(&sigwait_set, SIGQUIT);
sigprocmask(SIG_SETMASK, &sigall_set, nullptr);

constexpr int bufsz = 11 + ((CHAR_BIT * sizeof(pid_t) + 2) / 3) + 1;
// "LISTEN_PID=" - 11 characters; the expression above gives a conservative estimate
Expand Down Expand Up @@ -261,9 +258,6 @@ void base_process_service::run_child_proc(run_proc_params params) noexcept
// will not see control signals from ^C etc.

if (do_set_ctty) {
// Disable suspend (^Z) (and on some systems, delayed suspend / ^Y)
signal(SIGTSTP, SIG_IGN);

// Become session leader
setsid();
ioctl(0, TIOCSCTTY, 0);
Expand Down Expand Up @@ -376,7 +370,26 @@ void base_process_service::run_child_proc(run_proc_params params) noexcept
if (setreuid(uid, uid) != 0) goto failure_out;
}

sigprocmask(SIG_SETMASK, &sigwait_set, nullptr);
// Restore signal mask. If running on the console, we'll keep various control signals that can
// be invoked from the terminal masked, with the exception of SIGHUP and possibly SIGINT.
{
sigset_t sigwait_set = orig_signal_mask;
sigdelset(&sigwait_set, SIGCHLD);
sigdelset(&sigwait_set, SIGTERM);
if (on_console && params.in_foreground) {
if (params.unmask_sigint) {
sigdelset(&sigwait_set, SIGINT);
}
else {
sigaddset(&sigwait_set, SIGINT);
}
sigaddset(&sigwait_set, SIGQUIT);
sigaddset(&sigwait_set, SIGTSTP);
sigaddset(&sigwait_set, SIGTTIN);
sigaddset(&sigwait_set, SIGTTOU);
}
sigprocmask(SIG_SETMASK, &sigwait_set, nullptr);
}

err.stage = exec_stage::DO_EXEC;
// (on linux we could use execvpe, but it's not POSIX and not in eg FreeBSD).
Expand Down

0 comments on commit 6eaaa3b

Please sign in to comment.