Skip to content

Commit 1402c4a

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

File tree

3 files changed

+82
-9
lines changed

3 files changed

+82
-9
lines changed

src/uu/timeout/src/timeout.rs

Lines changed: 43 additions & 4 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,
@@ -233,7 +236,7 @@ fn wait_or_kill_process(
233236
self_pipe: &mut SelfPipe,
234237
) -> std::io::Result<i32> {
235238
// ignore `SIGTERM` here
236-
self_pipe.unset_other()?;
239+
self_pipe.unset_other(Signal::SIGTERM)?;
237240

238241
match process.wait_or_timeout(duration, self_pipe) {
239242
Ok(WaitOrTimeoutRet::InTime(status)) => {
@@ -280,6 +283,34 @@ fn preserve_signal_info(signal: libc::c_int) -> libc::c_int {
280283
signal
281284
}
282285

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

298332
let mut command = process::Command::new(&cmd[0]);
299333
command
300334
.args(&cmd[1..])
301335
.stdin(Stdio::inherit())
302336
.stdout(Stdio::inherit())
303337
.stderr(Stdio::inherit());
304-
let mut self_pipe = command.set_up_timeout(Some(Signal::SIGTERM))?;
338+
#[cfg(unix)]
339+
block_ignored_signals()?;
340+
let mut set = SigSet::empty();
341+
set.add(Signal::SIGTERM);
342+
set.add(Signal::SIGUSR1);
343+
let mut self_pipe = command.set_up_timeout(set)?;
305344
let process = &mut command.spawn().map_err(|err| {
306345
let status_code = match err.kind() {
307346
ErrorKind::NotFound => ExitStatus::CommandNotFound.into(),

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)