Skip to content

Commit ac8fac2

Browse files
committed
Support blocking and unblocking signals for child processes on Unix
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.
1 parent 7b9651f commit ac8fac2

File tree

4 files changed

+181
-17
lines changed

4 files changed

+181
-17
lines changed

library/std/src/os/unix/process.rs

+115
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,107 @@ pub trait CommandExt: Sealed {
179179
/// ```
180180
#[stable(feature = "process_set_process_group", since = "1.64.0")]
181181
fn process_group(&mut self, pgroup: i32) -> &mut process::Command;
182+
183+
/// Blocks the given signal for the child process at the time it is started.
184+
///
185+
/// The set of blocked signals for a process is known as its signal mask.
186+
/// Use this method to block some signals.
187+
///
188+
/// This method corresponds to calling [`sigaddset`] with the given signal.
189+
///
190+
/// # Notes
191+
///
192+
/// Rust's current default is to not block any signals in child processes. This may change in
193+
/// the future to inheriting the current process's signal mask.
194+
///
195+
/// This method is idempotent: blocking a signal that's already blocked results in
196+
/// success and has no effect.
197+
///
198+
/// Blocking some signals like `SIGSEGV` can lead to undefined behavior in
199+
/// the child. See the [`pthread_sigmask`] man page for more information.
200+
///
201+
/// # Errors
202+
///
203+
/// Returns an `InvalidInput` error if the signal is invalid.
204+
///
205+
/// # Examples
206+
///
207+
/// Start a process with `SIGINT` blocked:
208+
///
209+
/// ```no_run
210+
/// #![feature(process_sigmask)]
211+
/// #
212+
/// use std::process::Command;
213+
/// use std::os::unix::process::CommandExt;
214+
///
215+
/// Command::new("sleep")
216+
/// .arg("10")
217+
/// // On most platforms, SIGINT is signal 2.
218+
/// .block_signal(2)?
219+
/// .spawn()?;
220+
/// #
221+
/// # Ok::<_, Box<dyn std::error::Error>>(())
222+
/// ```
223+
///
224+
/// [`sigaddset`]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/sigaddset.html
225+
/// [`pthread_sigmask`]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_sigmask.html
226+
#[unstable(feature = "process_sigmask", issue = "none")]
227+
fn block_signal(&mut self, signal: i32) -> io::Result<&mut process::Command>;
228+
229+
/// Unblocks the given signal for the child process at the time it is started.
230+
///
231+
/// The set of blocked signals for a process is known as its signal mask.
232+
/// Use this method to unblock a signal.
233+
///
234+
/// This method corresponds to calling [`sigdelset`] with the given signal.
235+
///
236+
/// # Notes
237+
///
238+
/// Rust's current default is to not block any signals in child processes. This may change in
239+
/// the future to inheriting the current process's signal mask.
240+
///
241+
/// This method is idempotent: unblocking a signal that's already unblocked results in
242+
/// success and has no effect.
243+
///
244+
/// # Errors
245+
///
246+
/// Returns an `InvalidInput` error if the signal is invalid.
247+
///
248+
/// # Examples
249+
///
250+
/// Start a process with `SIGHUP` unblocked:
251+
///
252+
/// ```no_run
253+
/// #![feature(process_sigmask)]
254+
/// #
255+
/// use std::process::Command;
256+
/// use std::os::unix::process::CommandExt;
257+
///
258+
/// Command::new("sleep")
259+
/// .arg("10")
260+
/// // On most platforms, SIGHUP is signal 1.
261+
/// .unblock_signal(1)?
262+
/// .spawn()?;
263+
/// #
264+
/// # Ok::<_, Box<dyn std::error::Error>>(())
265+
/// ```
266+
///
267+
/// [`sigdelset`]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/sigdelset.html
268+
/// [`pthread_sigmask`]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_sigmask.html
269+
#[unstable(feature = "process_sigmask", issue = "none")]
270+
fn unblock_signal(&mut self, signal: i32) -> io::Result<&mut process::Command>;
271+
272+
/// Returns true if a signal will be blocked in the child process at the time it is started.
273+
///
274+
/// This method corresponds to calling [`sigismember`] with the given signal.
275+
///
276+
/// # Errors
277+
///
278+
/// Returns an `InvalidInput` error if the signal is invalid.
279+
///
280+
/// [`sigismember`]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/sigismember.html
281+
#[unstable(feature = "process_sigmask", issue = "none")]
282+
fn will_block_signal(&self, signal: i32) -> io::Result<bool>;
182283
}
183284

184285
#[stable(feature = "rust1", since = "1.0.0")]
@@ -224,6 +325,20 @@ impl CommandExt for process::Command {
224325
self.as_inner_mut().pgroup(pgroup);
225326
self
226327
}
328+
329+
fn block_signal(&mut self, signal: i32) -> io::Result<&mut process::Command> {
330+
self.as_inner_mut().signal_mask()?.insert(signal)?;
331+
Ok(self)
332+
}
333+
334+
fn unblock_signal(&mut self, signal: i32) -> io::Result<&mut process::Command> {
335+
self.as_inner_mut().signal_mask()?.remove(signal)?;
336+
Ok(self)
337+
}
338+
339+
fn will_block_signal(&self, signal: i32) -> io::Result<bool> {
340+
self.as_inner().get_signal_mask()?.contains(signal)
341+
}
227342
}
228343

229344
/// Unix-specific extensions to [`process::ExitStatus`] and

library/std/src/sys/unix/process/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
pub use self::process_common::{Command, CommandArgs, ExitCode, Stdio, StdioPipes};
1+
pub use self::process_common::{Command, CommandArgs, ExitCode, SignalSet, Stdio, StdioPipes};
22
pub use self::process_inner::{ExitStatus, ExitStatusError, Process};
33
pub use crate::ffi::OsString as EnvKey;
44
pub use crate::sys_common::process::CommandEnvs;

library/std/src/sys/unix/process/process_common.rs

+56
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ use crate::fmt;
99
use crate::io;
1010
use crate::path::Path;
1111
use crate::ptr;
12+
use crate::sync::OnceLock;
1213
use crate::sys::fd::FileDesc;
1314
use crate::sys::fs::File;
1415
use crate::sys::pipe::{self, AnonPipe};
16+
use crate::sys::{cvt, cvt_nz};
1517
use crate::sys_common::process::{CommandEnv, CommandEnvs};
1618
use crate::sys_common::IntoInner;
1719

@@ -152,6 +154,7 @@ pub struct Command {
152154
#[cfg(target_os = "linux")]
153155
create_pidfd: bool,
154156
pgroup: Option<pid_t>,
157+
signal_mask: OnceLock<SignalSet>,
155158
}
156159

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

202+
pub struct SignalSet {
203+
pub sigset: libc::sigset_t,
204+
}
205+
206+
impl SignalSet {
207+
#[allow(dead_code)]
208+
pub fn new_from_current() -> io::Result<Self> {
209+
let mut set = crate::mem::MaybeUninit::<libc::sigset_t>::uninit();
210+
// pthread_sigmask returns the errno rather than setting it directly.
211+
unsafe {
212+
cvt_nz(libc::pthread_sigmask(libc::SIG_BLOCK, ptr::null(), set.as_mut_ptr()))?;
213+
Ok(Self { sigset: set.assume_init() })
214+
}
215+
}
216+
217+
pub fn empty() -> io::Result<Self> {
218+
let mut set = crate::mem::MaybeUninit::<libc::sigset_t>::uninit();
219+
unsafe {
220+
cvt(sigemptyset(set.as_mut_ptr()))?;
221+
Ok(Self { sigset: set.assume_init() })
222+
}
223+
}
224+
225+
pub fn insert(&mut self, signal: i32) -> io::Result<&mut Self> {
226+
unsafe {
227+
cvt(sigaddset(&mut self.sigset, signal))?;
228+
}
229+
Ok(self)
230+
}
231+
232+
pub fn remove(&mut self, signal: i32) -> io::Result<&mut Self> {
233+
unsafe {
234+
cvt(sigdelset(&mut self.sigset, signal))?;
235+
}
236+
Ok(self)
237+
}
238+
239+
pub fn contains(&self, signal: i32) -> io::Result<bool> {
240+
let contains = unsafe { cvt(sigismember(&self.sigset, signal))? };
241+
Ok(contains != 0)
242+
}
243+
}
244+
199245
impl Command {
200246
#[cfg(not(target_os = "linux"))]
201247
pub fn new(program: &OsStr) -> Command {
@@ -216,6 +262,7 @@ impl Command {
216262
stdout: None,
217263
stderr: None,
218264
pgroup: None,
265+
mask: None,
219266
}
220267
}
221268

@@ -239,6 +286,7 @@ impl Command {
239286
stderr: None,
240287
create_pidfd: false,
241288
pgroup: None,
289+
signal_mask: OnceLock::new(),
242290
}
243291
}
244292

@@ -277,6 +325,10 @@ impl Command {
277325
pub fn pgroup(&mut self, pgroup: pid_t) {
278326
self.pgroup = Some(pgroup);
279327
}
328+
pub fn signal_mask(&mut self) -> io::Result<&mut SignalSet> {
329+
self.signal_mask.get_or_try_init(SignalSet::empty)?;
330+
Ok(self.signal_mask.get_mut().unwrap())
331+
}
280332

281333
#[cfg(target_os = "linux")]
282334
pub fn create_pidfd(&mut self, val: bool) {
@@ -344,6 +396,10 @@ impl Command {
344396
pub fn get_pgroup(&self) -> Option<pid_t> {
345397
self.pgroup
346398
}
399+
#[allow(dead_code)]
400+
pub fn get_signal_mask(&self) -> io::Result<&SignalSet> {
401+
self.signal_mask.get_or_try_init(SignalSet::empty)
402+
}
347403

348404
pub fn get_closures(&mut self) -> &mut Vec<Box<dyn FnMut() -> io::Result<()> + Send + Sync>> {
349405
&mut self.closures

library/std/src/sys/unix/process/process_unix.rs

+9-16
Original file line numberDiff line numberDiff line change
@@ -326,18 +326,10 @@ impl Command {
326326
// emscripten has no signal support.
327327
#[cfg(not(target_os = "emscripten"))]
328328
{
329-
use crate::mem::MaybeUninit;
330329
use crate::sys::cvt_nz;
331-
// Reset signal handling so the child process starts in a
332-
// standardized state. libstd ignores SIGPIPE, and signal-handling
333-
// libraries often set a mask. Child processes inherit ignored
334-
// signals and the signal mask from their parent, but most
335-
// UNIX programs do not reset these things on their own, so we
336-
// need to clean things up now to avoid confusing the program
337-
// we're about to run.
338-
let mut set = MaybeUninit::<libc::sigset_t>::uninit();
339-
cvt(sigemptyset(set.as_mut_ptr()))?;
340-
cvt_nz(libc::pthread_sigmask(libc::SIG_SETMASK, set.as_ptr(), ptr::null_mut()))?;
330+
// Set the signal mask.
331+
let signal_mask = self.get_signal_mask()?;
332+
cvt_nz(libc::pthread_sigmask(libc::SIG_SETMASK, &signal_mask.sigset, ptr::null_mut()))?;
341333

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

532-
let mut set = MaybeUninit::<libc::sigset_t>::uninit();
533-
cvt(sigemptyset(set.as_mut_ptr()))?;
534-
cvt_nz(libc::posix_spawnattr_setsigmask(attrs.0.as_mut_ptr(), set.as_ptr()))?;
535-
cvt(sigaddset(set.as_mut_ptr(), libc::SIGPIPE))?;
536-
cvt_nz(libc::posix_spawnattr_setsigdefault(attrs.0.as_mut_ptr(), set.as_ptr()))?;
524+
let signal_mask = self.get_signal_mask()?;
525+
cvt_nz(libc::posix_spawnattr_setsigmask(attrs.0.as_mut_ptr(), &signal_mask.sigset))?;
526+
527+
let mut sig_default = SignalSet::empty()?;
528+
sig_default.insert(libc::SIGPIPE)?;
529+
cvt_nz(libc::posix_spawnattr_setsigdefault(attrs.0.as_mut_ptr(), &sig_default.sigset))?;
537530

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

0 commit comments

Comments
 (0)