Skip to content

Commit

Permalink
Support blocking and unblocking signals for child processes on Unix
Browse files Browse the repository at this point in the history
This commit implements basic support for setting signal masks during process spawning.

* With the `posix_spawn` code path, this means setting `posix_spawnattr_setsigmask`.
* With the `fork/exec` path, this means calling `pthread_sigmask` in the child before executing it.

This also fixes a bug: previously, we were always setting the signal mask to
the empty set, breaking tools like `nohup`. With this change, we inherit
the signal mask from the parent by default.
  • Loading branch information
sunshowers committed Aug 27, 2022
1 parent 7b9651f commit ac8fac2
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 17 deletions.
115 changes: 115 additions & 0 deletions library/std/src/os/unix/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,107 @@ pub trait CommandExt: Sealed {
/// ```
#[stable(feature = "process_set_process_group", since = "1.64.0")]
fn process_group(&mut self, pgroup: i32) -> &mut process::Command;

/// Blocks the given signal for the child process at the time it is started.
///
/// The set of blocked signals for a process is known as its signal mask.
/// Use this method to block some signals.
///
/// This method corresponds to calling [`sigaddset`] with the given signal.
///
/// # Notes
///
/// Rust's current default is to not block any signals in child processes. This may change in
/// the future to inheriting the current process's signal mask.
///
/// This method is idempotent: blocking a signal that's already blocked results in
/// success and has no effect.
///
/// Blocking some signals like `SIGSEGV` can lead to undefined behavior in
/// the child. See the [`pthread_sigmask`] man page for more information.
///
/// # Errors
///
/// Returns an `InvalidInput` error if the signal is invalid.
///
/// # Examples
///
/// Start a process with `SIGINT` blocked:
///
/// ```no_run
/// #![feature(process_sigmask)]
/// #
/// use std::process::Command;
/// use std::os::unix::process::CommandExt;
///
/// Command::new("sleep")
/// .arg("10")
/// // On most platforms, SIGINT is signal 2.
/// .block_signal(2)?
/// .spawn()?;
/// #
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
///
/// [`sigaddset`]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/sigaddset.html
/// [`pthread_sigmask`]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_sigmask.html
#[unstable(feature = "process_sigmask", issue = "none")]
fn block_signal(&mut self, signal: i32) -> io::Result<&mut process::Command>;

/// Unblocks the given signal for the child process at the time it is started.
///
/// The set of blocked signals for a process is known as its signal mask.
/// Use this method to unblock a signal.
///
/// This method corresponds to calling [`sigdelset`] with the given signal.
///
/// # Notes
///
/// Rust's current default is to not block any signals in child processes. This may change in
/// the future to inheriting the current process's signal mask.
///
/// This method is idempotent: unblocking a signal that's already unblocked results in
/// success and has no effect.
///
/// # Errors
///
/// Returns an `InvalidInput` error if the signal is invalid.
///
/// # Examples
///
/// Start a process with `SIGHUP` unblocked:
///
/// ```no_run
/// #![feature(process_sigmask)]
/// #
/// use std::process::Command;
/// use std::os::unix::process::CommandExt;
///
/// Command::new("sleep")
/// .arg("10")
/// // On most platforms, SIGHUP is signal 1.
/// .unblock_signal(1)?
/// .spawn()?;
/// #
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
///
/// [`sigdelset`]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/sigdelset.html
/// [`pthread_sigmask`]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_sigmask.html
#[unstable(feature = "process_sigmask", issue = "none")]
fn unblock_signal(&mut self, signal: i32) -> io::Result<&mut process::Command>;

/// Returns true if a signal will be blocked in the child process at the time it is started.
///
/// This method corresponds to calling [`sigismember`] with the given signal.
///
/// # Errors
///
/// Returns an `InvalidInput` error if the signal is invalid.
///
/// [`sigismember`]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/sigismember.html
#[unstable(feature = "process_sigmask", issue = "none")]
fn will_block_signal(&self, signal: i32) -> io::Result<bool>;
}

#[stable(feature = "rust1", since = "1.0.0")]
Expand Down Expand Up @@ -224,6 +325,20 @@ impl CommandExt for process::Command {
self.as_inner_mut().pgroup(pgroup);
self
}

fn block_signal(&mut self, signal: i32) -> io::Result<&mut process::Command> {
self.as_inner_mut().signal_mask()?.insert(signal)?;
Ok(self)
}

fn unblock_signal(&mut self, signal: i32) -> io::Result<&mut process::Command> {
self.as_inner_mut().signal_mask()?.remove(signal)?;
Ok(self)
}

fn will_block_signal(&self, signal: i32) -> io::Result<bool> {
self.as_inner().get_signal_mask()?.contains(signal)
}
}

/// Unix-specific extensions to [`process::ExitStatus`] and
Expand Down
2 changes: 1 addition & 1 deletion library/std/src/sys/unix/process/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub use self::process_common::{Command, CommandArgs, ExitCode, Stdio, StdioPipes};
pub use self::process_common::{Command, CommandArgs, ExitCode, SignalSet, Stdio, StdioPipes};
pub use self::process_inner::{ExitStatus, ExitStatusError, Process};
pub use crate::ffi::OsString as EnvKey;
pub use crate::sys_common::process::CommandEnvs;
Expand Down
56 changes: 56 additions & 0 deletions library/std/src/sys/unix/process/process_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ use crate::fmt;
use crate::io;
use crate::path::Path;
use crate::ptr;
use crate::sync::OnceLock;
use crate::sys::fd::FileDesc;
use crate::sys::fs::File;
use crate::sys::pipe::{self, AnonPipe};
use crate::sys::{cvt, cvt_nz};
use crate::sys_common::process::{CommandEnv, CommandEnvs};
use crate::sys_common::IntoInner;

Expand Down Expand Up @@ -152,6 +154,7 @@ pub struct Command {
#[cfg(target_os = "linux")]
create_pidfd: bool,
pgroup: Option<pid_t>,
signal_mask: OnceLock<SignalSet>,
}

// Create a new type for argv, so that we can make it `Send` and `Sync`
Expand Down Expand Up @@ -196,6 +199,49 @@ pub enum Stdio {
Fd(FileDesc),
}

pub struct SignalSet {
pub sigset: libc::sigset_t,
}

impl SignalSet {
#[allow(dead_code)]
pub fn new_from_current() -> io::Result<Self> {
let mut set = crate::mem::MaybeUninit::<libc::sigset_t>::uninit();
// pthread_sigmask returns the errno rather than setting it directly.
unsafe {
cvt_nz(libc::pthread_sigmask(libc::SIG_BLOCK, ptr::null(), set.as_mut_ptr()))?;
Ok(Self { sigset: set.assume_init() })
}
}

pub fn empty() -> io::Result<Self> {
let mut set = crate::mem::MaybeUninit::<libc::sigset_t>::uninit();
unsafe {
cvt(sigemptyset(set.as_mut_ptr()))?;
Ok(Self { sigset: set.assume_init() })
}
}

pub fn insert(&mut self, signal: i32) -> io::Result<&mut Self> {
unsafe {
cvt(sigaddset(&mut self.sigset, signal))?;
}
Ok(self)
}

pub fn remove(&mut self, signal: i32) -> io::Result<&mut Self> {
unsafe {
cvt(sigdelset(&mut self.sigset, signal))?;
}
Ok(self)
}

pub fn contains(&self, signal: i32) -> io::Result<bool> {
let contains = unsafe { cvt(sigismember(&self.sigset, signal))? };
Ok(contains != 0)
}
}

impl Command {
#[cfg(not(target_os = "linux"))]
pub fn new(program: &OsStr) -> Command {
Expand All @@ -216,6 +262,7 @@ impl Command {
stdout: None,
stderr: None,
pgroup: None,
mask: None,
}
}

Expand All @@ -239,6 +286,7 @@ impl Command {
stderr: None,
create_pidfd: false,
pgroup: None,
signal_mask: OnceLock::new(),
}
}

Expand Down Expand Up @@ -277,6 +325,10 @@ impl Command {
pub fn pgroup(&mut self, pgroup: pid_t) {
self.pgroup = Some(pgroup);
}
pub fn signal_mask(&mut self) -> io::Result<&mut SignalSet> {
self.signal_mask.get_or_try_init(SignalSet::empty)?;
Ok(self.signal_mask.get_mut().unwrap())
}

#[cfg(target_os = "linux")]
pub fn create_pidfd(&mut self, val: bool) {
Expand Down Expand Up @@ -344,6 +396,10 @@ impl Command {
pub fn get_pgroup(&self) -> Option<pid_t> {
self.pgroup
}
#[allow(dead_code)]
pub fn get_signal_mask(&self) -> io::Result<&SignalSet> {
self.signal_mask.get_or_try_init(SignalSet::empty)
}

pub fn get_closures(&mut self) -> &mut Vec<Box<dyn FnMut() -> io::Result<()> + Send + Sync>> {
&mut self.closures
Expand Down
25 changes: 9 additions & 16 deletions library/std/src/sys/unix/process/process_unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,18 +326,10 @@ impl Command {
// emscripten has no signal support.
#[cfg(not(target_os = "emscripten"))]
{
use crate::mem::MaybeUninit;
use crate::sys::cvt_nz;
// Reset signal handling so the child process starts in a
// standardized state. libstd ignores SIGPIPE, and signal-handling
// libraries often set a mask. Child processes inherit ignored
// signals and the signal mask from their parent, but most
// UNIX programs do not reset these things on their own, so we
// need to clean things up now to avoid confusing the program
// we're about to run.
let mut set = MaybeUninit::<libc::sigset_t>::uninit();
cvt(sigemptyset(set.as_mut_ptr()))?;
cvt_nz(libc::pthread_sigmask(libc::SIG_SETMASK, set.as_ptr(), ptr::null_mut()))?;
// Set the signal mask.
let signal_mask = self.get_signal_mask()?;
cvt_nz(libc::pthread_sigmask(libc::SIG_SETMASK, &signal_mask.sigset, ptr::null_mut()))?;

#[cfg(target_os = "android")] // see issue #88585
{
Expand Down Expand Up @@ -529,11 +521,12 @@ impl Command {
cvt_nz(libc::posix_spawnattr_setpgroup(attrs.0.as_mut_ptr(), pgroup))?;
}

let mut set = MaybeUninit::<libc::sigset_t>::uninit();
cvt(sigemptyset(set.as_mut_ptr()))?;
cvt_nz(libc::posix_spawnattr_setsigmask(attrs.0.as_mut_ptr(), set.as_ptr()))?;
cvt(sigaddset(set.as_mut_ptr(), libc::SIGPIPE))?;
cvt_nz(libc::posix_spawnattr_setsigdefault(attrs.0.as_mut_ptr(), set.as_ptr()))?;
let signal_mask = self.get_signal_mask()?;
cvt_nz(libc::posix_spawnattr_setsigmask(attrs.0.as_mut_ptr(), &signal_mask.sigset))?;

let mut sig_default = SignalSet::empty()?;
sig_default.insert(libc::SIGPIPE)?;
cvt_nz(libc::posix_spawnattr_setsigdefault(attrs.0.as_mut_ptr(), &sig_default.sigset))?;

flags |= libc::POSIX_SPAWN_SETSIGDEF | libc::POSIX_SPAWN_SETSIGMASK;
cvt_nz(libc::posix_spawnattr_setflags(attrs.0.as_mut_ptr(), flags as _))?;
Expand Down

0 comments on commit ac8fac2

Please sign in to comment.