Skip to content

Commit 7504645

Browse files
committed
fix(stat): block ignored signals to imitate GNU
Also added a test case for it.
1 parent 0af5542 commit 7504645

File tree

3 files changed

+76
-7
lines changed

3 files changed

+76
-7
lines changed

src/uu/timeout/src/timeout.rs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ use uucore::process::{ChildExt, CommandExt, SelfPipe, WaitOrTimeoutRet};
2020
use uucore::translate;
2121

2222
#[cfg(unix)]
23-
use uucore::signals::enable_pipe_errors;
23+
use ::{
24+
nix::sys::signal::{SigSet, SigmaskHow, pthread_sigmask},
25+
uucore::signals::{enable_pipe_errors, is_ignored},
26+
};
2427

2528
use uucore::{
2629
format_usage, show_error,
@@ -280,6 +283,33 @@ fn preserve_signal_info(signal: libc::c_int) -> libc::c_int {
280283
signal
281284
}
282285

286+
#[cfg(unix)]
287+
fn block_ignored_signals() -> nix::Result<()> {
288+
let mut set = SigSet::empty();
289+
let rt_signals = if cfg!(target_os = "linux") {
290+
libc::SIGRTMIN()..=libc::SIGRTMAX()
291+
} else {
292+
std::iter::empty()
293+
};
294+
for s in Signal::iterator()
295+
.filter_map(|s| {
296+
if matches!(s, Signal::SIGSTOP | Signal::SIGKILL | Signal::SIGTERM) {
297+
None
298+
} else {
299+
Some(s as i32)
300+
}
301+
})
302+
.chain(rt_signals)
303+
{
304+
if is_ignored(s)? {
305+
// We use raw libc bindings because [`nix`] does not support RT signals.
306+
// SAFETY: SigSet is repr(transparent) over sigset_t.
307+
unsafe { libc::sigaddset((&mut set as *mut SigSet).cast(), s) };
308+
}
309+
}
310+
pthread_sigmask(SigmaskHow::SIG_BLOCK, Some(&set), None)
311+
}
312+
283313
fn timeout(
284314
cmd: &[String],
285315
duration: Duration,
@@ -293,14 +323,19 @@ fn timeout(
293323
unsafe { libc::setpgid(0, 0) };
294324
}
295325
#[cfg(unix)]
296-
enable_pipe_errors()?;
326+
// We keep the inherited SIGPIPE disposition if ignored.
327+
if !is_ignored(Signal::SIGPIPE as _)? {
328+
enable_pipe_errors()?;
329+
}
297330

298331
let mut command = process::Command::new(&cmd[0]);
299332
command
300333
.args(&cmd[1..])
301334
.stdin(Stdio::inherit())
302335
.stdout(Stdio::inherit())
303336
.stderr(Stdio::inherit());
337+
#[cfg(unix)]
338+
block_ignored_signals()?;
304339
let mut self_pipe = command.set_up_timeout(Some(Signal::SIGTERM))?;
305340
let process = &mut command.spawn().map_err(|err| {
306341
let status_code = match err.kind() {

src/uucore/src/lib/features/signals.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
//! It provides a way to convert signal names to their corresponding values and vice versa.
1111
//! It also provides a way to ignore the SIGINT signal and enable pipe errors.
1212
13+
use std::mem::MaybeUninit;
14+
use std::ptr::null;
15+
1316
#[cfg(unix)]
1417
use nix::errno::Errno;
1518
#[cfg(unix)]
@@ -410,6 +413,17 @@ pub fn signal_name_by_value(signal_value: usize) -> Option<&'static str> {
410413
ALL_SIGNALS.get(signal_value).copied()
411414
}
412415

416+
/// Returns whether signal disposition is to ignore. We use raw i32 because [`nix`] does not currently
417+
/// support RT signals.
418+
#[cfg(unix)]
419+
pub fn is_ignored(signal: i32) -> Result<bool, Errno> {
420+
let mut prev_handler = MaybeUninit::uninit();
421+
// We use libc functions here because nix does not properly
422+
// support real-time signals nor null sigaction handlers.
423+
Errno::result(unsafe { libc::sigaction(signal, null(), prev_handler.as_mut_ptr()) })?;
424+
Ok(unsafe { prev_handler.assume_init() }.sa_sigaction == libc::SIG_IGN)
425+
}
426+
413427
/// Returns the default signal value.
414428
#[cfg(unix)]
415429
pub fn enable_pipe_errors() -> Result<(), Errno> {

tests/by-util/test_timeout.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::process::Command;
12
use std::time::{Duration, Instant};
23
use uutests::util::TestScenario;
34

@@ -255,11 +256,7 @@ fn test_command_cannot_invoke() {
255256
#[test]
256257
fn test_cascaded_timeout_with_bash_trap() {
257258
// Use bash if available, otherwise skip
258-
if std::process::Command::new("bash")
259-
.arg("--version")
260-
.output()
261-
.is_err()
262-
{
259+
if Command::new("bash").arg("--version").output().is_err() {
263260
// Skip test if bash is not available
264261
return;
265262
}
@@ -291,3 +288,26 @@ fn test_cascaded_timeout_with_bash_trap() {
291288
.fails_with_code(124)
292289
.stdout_contains("bash_trap_fired");
293290
}
291+
292+
#[test]
293+
fn test_signal_block_on_ignore() {
294+
let ts = TestScenario::new("timeout");
295+
let res = ts
296+
.cmd("/bin/sh")
297+
.arg("-c")
298+
.arg(format!(
299+
"(trap '' PIPE; {} timeout -v 10 yes | :)",
300+
ts.bin_path.to_str().unwrap()
301+
))
302+
.timeout(Duration::from_secs(2))
303+
.succeeds();
304+
// If the signal disposition is correct, instead of being silently killed
305+
// by SIGPIPE, `yes` receives an EPIPE error and outputs it.
306+
assert_eq!(
307+
res.stderr_str()
308+
.to_string()
309+
.trim_end_matches('\n')
310+
.trim_end_matches('\r'),
311+
"yes: standard output: Broken pipe"
312+
);
313+
}

0 commit comments

Comments
 (0)