Skip to content

Commit 67046f5

Browse files
authored
timeout: add all signal handlers, pass on signals to child, add ignored signal handling (uutils#10254)
There was quite a bunch of different features missing in timeout, to start off, now that we have a mechanism to read the SIGPIPE handlers before they are overwritten by the rust runtime, it means that we can not propagate this signal down to the child processes if the signal is set to ignore. This also includes all of the latest changes since 9.9 where the specific signal sent to timeout will be propagated instead of just defaulting to a TERM signal.
1 parent fec6cdf commit 67046f5

File tree

5 files changed

+214
-88
lines changed

5 files changed

+214
-88
lines changed

.vscode/cspell.dictionaries/jargon.wordlist.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,10 @@ noxfer
117117
ofile
118118
oflag
119119
oflags
120+
pdeathsig
120121
peekable
121122
performant
123+
prctl
122124
precompiled
123125
precompute
124126
preload
@@ -143,8 +145,17 @@ SETFL
143145
setlocale
144146
shortcode
145147
shortcodes
148+
setpgid
146149
sigaction
150+
CHLD
151+
chld
152+
SIGCHLD
153+
sigchld
147154
siginfo
155+
SIGTTIN
156+
sigttin
157+
SIGTTOU
158+
sigttou
148159
sigusr
149160
strcasecmp
150161
subcommand

src/uu/timeout/src/status.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@ pub(crate) enum ExitStatus {
3333

3434
/// When a signal is sent to the child process or `timeout` itself.
3535
SignalSent(usize),
36-
37-
/// When `SIGTERM` signal received.
38-
Terminated,
3936
}
4037

4138
impl From<ExitStatus> for i32 {
@@ -46,7 +43,6 @@ impl From<ExitStatus> for i32 {
4643
ExitStatus::CannotInvoke => 126,
4744
ExitStatus::CommandNotFound => 127,
4845
ExitStatus::SignalSent(s) => 128 + s as Self,
49-
ExitStatus::Terminated => 143,
5046
}
5147
}
5248
}

src/uu/timeout/src/timeout.rs

Lines changed: 129 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ mod status;
99

1010
use crate::status::ExitStatus;
1111
use clap::{Arg, ArgAction, Command};
12-
use std::io::ErrorKind;
13-
use std::os::unix::process::{CommandExt, ExitStatusExt};
12+
use std::io::{ErrorKind, Write};
13+
use std::os::unix::process::ExitStatusExt;
1414
use std::process::{self, Child, Stdio};
1515
use std::sync::atomic::{self, AtomicBool};
1616
use std::time::Duration;
@@ -21,12 +21,14 @@ use uucore::process::ChildExt;
2121
use uucore::translate;
2222

2323
use uucore::{
24-
format_usage, show_error,
24+
format_usage,
2525
signals::{signal_by_name_or_value, signal_name_by_value},
2626
};
2727

28-
use nix::sys::signal::{Signal, kill};
28+
use nix::sys::signal::{SigHandler, Signal, kill};
2929
use nix::unistd::{Pid, getpid, setpgid};
30+
#[cfg(unix)]
31+
use std::os::unix::process::CommandExt;
3032

3133
pub mod options {
3234
pub static FOREGROUND: &str = "foreground";
@@ -177,32 +179,46 @@ pub fn uu_app() -> Command {
177179
.after_help(translate!("timeout-after-help"))
178180
}
179181

180-
/// Remove pre-existing SIGCHLD handlers that would make waiting for the child's exit code fail.
181-
fn unblock_sigchld() {
182-
unsafe {
183-
nix::sys::signal::signal(
184-
nix::sys::signal::Signal::SIGCHLD,
185-
nix::sys::signal::SigHandler::SigDfl,
186-
)
187-
.unwrap();
188-
}
182+
/// Install SIGCHLD handler to ensure waiting for child works even if parent ignored SIGCHLD.
183+
fn install_sigchld() {
184+
extern "C" fn chld(_: libc::c_int) {}
185+
let _ = unsafe { nix::sys::signal::signal(Signal::SIGCHLD, SigHandler::Handler(chld)) };
189186
}
190187

191-
/// We should terminate child process when receiving TERM signal.
188+
/// We should terminate child process when receiving termination signals.
192189
static SIGNALED: AtomicBool = AtomicBool::new(false);
190+
/// Track which signal was received (0 = none/timeout expired naturally).
191+
static RECEIVED_SIGNAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
192+
193+
/// Install signal handlers for termination signals.
194+
fn install_signal_handlers(term_signal: usize) {
195+
extern "C" fn handle_signal(sig: libc::c_int) {
196+
SIGNALED.store(true, atomic::Ordering::Relaxed);
197+
RECEIVED_SIGNAL.store(sig, atomic::Ordering::Relaxed);
198+
}
193199

194-
fn catch_sigterm() {
195-
use nix::sys::signal;
196-
197-
extern "C" fn handle_sigterm(signal: libc::c_int) {
198-
let signal = signal::Signal::try_from(signal).unwrap();
199-
if signal == signal::Signal::SIGTERM {
200-
SIGNALED.store(true, atomic::Ordering::Relaxed);
200+
let handler = SigHandler::Handler(handle_signal);
201+
let sigpipe_ignored = uucore::signals::sigpipe_was_ignored();
202+
203+
for sig in [
204+
Signal::SIGALRM,
205+
Signal::SIGINT,
206+
Signal::SIGQUIT,
207+
Signal::SIGHUP,
208+
Signal::SIGTERM,
209+
Signal::SIGPIPE,
210+
Signal::SIGUSR1,
211+
Signal::SIGUSR2,
212+
] {
213+
if sig == Signal::SIGPIPE && sigpipe_ignored {
214+
continue; // Skip SIGPIPE if it was ignored by parent
201215
}
216+
let _ = unsafe { nix::sys::signal::signal(sig, handler) };
202217
}
203218

204-
let handler = signal::SigHandler::Handler(handle_sigterm);
205-
unsafe { signal::signal(signal::Signal::SIGTERM, handler) }.unwrap();
219+
if let Ok(sig) = Signal::try_from(term_signal as i32) {
220+
let _ = unsafe { nix::sys::signal::signal(sig, handler) };
221+
}
206222
}
207223

208224
/// Report that a signal is being sent if the verbose flag is set.
@@ -213,26 +229,29 @@ fn report_if_verbose(signal: usize, cmd: &str, verbose: bool) {
213229
} else {
214230
signal_name_by_value(signal).unwrap().to_string()
215231
};
216-
show_error!(
217-
"{}",
232+
let mut stderr = std::io::stderr();
233+
let _ = writeln!(
234+
stderr,
235+
"timeout: {}",
218236
translate!("timeout-verbose-sending-signal", "signal" => s, "command" => cmd.quote())
219237
);
238+
let _ = stderr.flush();
220239
}
221240
}
222241

223242
fn send_signal(process: &mut Child, signal: usize, foreground: bool) {
224243
// NOTE: GNU timeout doesn't check for errors of signal.
225244
// The subprocess might have exited just after the timeout.
226-
// Sending a signal now would return "No such process", but we should still try to kill the children.
227-
if foreground {
228-
let _ = process.send_signal(signal);
229-
} else {
230-
let _ = process.send_signal_group(signal);
231-
let kill_signal = signal_by_name_or_value("KILL").unwrap();
232-
let continued_signal = signal_by_name_or_value("CONT").unwrap();
233-
if signal != kill_signal && signal != continued_signal {
234-
_ = process.send_signal_group(continued_signal);
235-
}
245+
let _ = process.send_signal(signal);
246+
if signal == 0 || foreground {
247+
return;
248+
}
249+
let _ = process.send_signal_group(signal);
250+
let kill_signal = signal_by_name_or_value("KILL").unwrap();
251+
let continued_signal = signal_by_name_or_value("CONT").unwrap();
252+
if signal != kill_signal && signal != continued_signal {
253+
let _ = process.send_signal(continued_signal);
254+
let _ = process.send_signal_group(continued_signal);
236255
}
237256
}
238257

@@ -330,24 +349,46 @@ fn timeout(
330349
let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0));
331350
}
332351

333-
let mut command = process::Command::new(&cmd[0]);
334-
command
352+
let mut cmd_builder = process::Command::new(&cmd[0]);
353+
cmd_builder
335354
.args(&cmd[1..])
336355
.stdin(Stdio::inherit())
337356
.stdout(Stdio::inherit())
338357
.stderr(Stdio::inherit());
339358

340-
// If stdin was closed before Rust reopened it as /dev/null, close it in child
341-
if uucore::signals::stdin_was_closed() {
359+
#[cfg(unix)]
360+
{
361+
#[cfg(target_os = "linux")]
362+
let death_sig = Signal::try_from(signal as i32).ok();
363+
let sigpipe_was_ignored = uucore::signals::sigpipe_was_ignored();
364+
let stdin_was_closed = uucore::signals::stdin_was_closed();
365+
342366
unsafe {
343-
command.pre_exec(|| {
344-
libc::close(libc::STDIN_FILENO);
367+
cmd_builder.pre_exec(move || {
368+
// Reset terminal signals to default
369+
let _ = nix::sys::signal::signal(Signal::SIGTTIN, SigHandler::SigDfl);
370+
let _ = nix::sys::signal::signal(Signal::SIGTTOU, SigHandler::SigDfl);
371+
// Preserve SIGPIPE ignore status if parent had it ignored
372+
if sigpipe_was_ignored {
373+
let _ = nix::sys::signal::signal(Signal::SIGPIPE, SigHandler::SigIgn);
374+
}
375+
// If stdin was closed before Rust reopened it as /dev/null, close it in child
376+
if stdin_was_closed {
377+
libc::close(libc::STDIN_FILENO);
378+
}
379+
#[cfg(target_os = "linux")]
380+
if let Some(sig) = death_sig {
381+
let _ = nix::sys::prctl::set_pdeathsig(sig);
382+
}
345383
Ok(())
346384
});
347385
}
348386
}
349387

350-
let process = &mut command.spawn().map_err(|err| {
388+
install_sigchld();
389+
install_signal_handlers(signal);
390+
391+
let process = &mut cmd_builder.spawn().map_err(|err| {
351392
let status_code = match err.kind() {
352393
ErrorKind::NotFound => ExitStatus::CommandNotFound.into(),
353394
ErrorKind::PermissionDenied => ExitStatus::CannotInvoke.into(),
@@ -358,8 +399,7 @@ fn timeout(
358399
translate!("timeout-error-failed-to-execute-process", "error" => err),
359400
)
360401
})?;
361-
unblock_sigchld();
362-
catch_sigterm();
402+
363403
// Wait for the child process for the specified time period.
364404
//
365405
// If the process exits within the specified time period (the
@@ -381,41 +421,51 @@ fn timeout(
381421
Err(exit_code.into())
382422
}
383423
Ok(None) => {
384-
report_if_verbose(signal, &cmd[0], verbose);
385-
send_signal(process, signal, foreground);
386-
match kill_after {
387-
None => {
388-
let status = process.wait()?;
389-
if SIGNALED.load(atomic::Ordering::Relaxed) {
390-
Err(ExitStatus::Terminated.into())
391-
} else if preserve_status {
392-
if let Some(ec) = status.code() {
393-
Err(ec.into())
394-
} else if let Some(sc) = status.signal() {
395-
Err(ExitStatus::SignalSent(sc.try_into().unwrap()).into())
396-
} else {
397-
Err(ExitStatus::CommandTimedOut.into())
398-
}
399-
} else {
400-
Err(ExitStatus::CommandTimedOut.into())
401-
}
402-
}
403-
Some(kill_after) => {
404-
match wait_or_kill_process(
405-
process,
406-
&cmd[0],
407-
kill_after,
408-
preserve_status,
409-
foreground,
410-
verbose,
411-
) {
412-
Ok(status) => Err(status.into()),
413-
Err(e) => Err(USimpleError::new(
414-
ExitStatus::TimeoutFailed.into(),
415-
e.to_string(),
416-
)),
417-
}
418-
}
424+
let received_sig = RECEIVED_SIGNAL.load(atomic::Ordering::Relaxed);
425+
let is_external_signal = received_sig > 0 && received_sig != libc::SIGALRM;
426+
let signal_to_send = if is_external_signal {
427+
received_sig as usize
428+
} else {
429+
signal
430+
};
431+
432+
report_if_verbose(signal_to_send, &cmd[0], verbose);
433+
send_signal(process, signal_to_send, foreground);
434+
435+
if let Some(kill_after) = kill_after {
436+
return match wait_or_kill_process(
437+
process,
438+
&cmd[0],
439+
kill_after,
440+
preserve_status,
441+
foreground,
442+
verbose,
443+
) {
444+
Ok(status) => Err(status.into()),
445+
Err(e) => Err(USimpleError::new(
446+
ExitStatus::TimeoutFailed.into(),
447+
e.to_string(),
448+
)),
449+
};
450+
}
451+
452+
let status = process.wait()?;
453+
if is_external_signal {
454+
Err(ExitStatus::SignalSent(received_sig as usize).into())
455+
} else if SIGNALED.load(atomic::Ordering::Relaxed) {
456+
Err(ExitStatus::CommandTimedOut.into())
457+
} else if preserve_status {
458+
Err(status
459+
.code()
460+
.or_else(|| {
461+
status
462+
.signal()
463+
.map(|s| ExitStatus::SignalSent(s as usize).into())
464+
})
465+
.unwrap_or(ExitStatus::CommandTimedOut.into())
466+
.into())
467+
} else {
468+
Err(ExitStatus::CommandTimedOut.into())
419469
}
420470
}
421471
Err(_) => {

src/uucore/src/lib/features/process.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,29 @@ impl ChildExt for Child {
105105
}
106106

107107
fn send_signal_group(&mut self, signal: usize) -> io::Result<()> {
108-
// Ignore the signal, so we don't go into a signal loop.
109-
if unsafe { libc::signal(signal as i32, libc::SIG_IGN) } == usize::MAX {
110-
return Err(io::Error::last_os_error());
108+
// Send signal to our process group (group 0 = caller's group).
109+
// This matches GNU coreutils behavior: if the child has remained in our
110+
// process group, it will receive this signal along with all other processes
111+
// in the group. If the child has created its own process group (via setpgid),
112+
// it won't receive this group signal, but will have received the direct signal.
113+
114+
// Signal 0 is special - it just checks if process exists, doesn't send anything.
115+
// No need to manipulate signal handlers for it.
116+
if signal == 0 {
117+
let result = unsafe { libc::kill(0, 0) };
118+
return if result == 0 {
119+
Ok(())
120+
} else {
121+
Err(io::Error::last_os_error())
122+
};
111123
}
112-
if unsafe { libc::kill(0, signal as i32) } == 0 {
124+
125+
// Ignore the signal temporarily so we don't receive it ourselves.
126+
let old_handler = unsafe { libc::signal(signal as i32, libc::SIG_IGN) };
127+
let result = unsafe { libc::kill(0, signal as i32) };
128+
// Restore the old handler
129+
unsafe { libc::signal(signal as i32, old_handler) };
130+
if result == 0 {
113131
Ok(())
114132
} else {
115133
Err(io::Error::last_os_error())

0 commit comments

Comments
 (0)