Skip to content

Commit bf9f733

Browse files
committed
timeout: fix signal handling, SIGPIPE preservation, and signal 0 behavior
1 parent 450e7cf commit bf9f733

2 files changed

Lines changed: 118 additions & 105 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,5 @@ VMULL
219219
vmull
220220
SETFL
221221
tmpfs
222+
pdeathsig
223+
prctl

src/uu/timeout/src/timeout.rs

Lines changed: 116 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ mod status;
88

99
use crate::status::ExitStatus;
1010
use clap::{Arg, ArgAction, Command};
11-
use std::io::ErrorKind;
11+
use std::io::{ErrorKind, Write};
1212
use std::os::unix::process::ExitStatusExt;
1313
use std::process::{self, Child, Stdio};
1414
use std::sync::atomic::{self, AtomicBool};
@@ -19,16 +19,15 @@ use uucore::parser::parse_time;
1919
use uucore::process::ChildExt;
2020
use uucore::translate;
2121

22-
#[cfg(unix)]
23-
use uucore::signals::enable_pipe_errors;
24-
2522
use uucore::{
26-
format_usage, show_error,
23+
format_usage,
2724
signals::{signal_by_name_or_value, signal_name_by_value},
2825
};
2926

30-
use nix::sys::signal::{Signal, kill};
27+
use nix::sys::signal::{SigHandler, Signal, kill};
3128
use nix::unistd::{Pid, getpid, setpgid};
29+
#[cfg(unix)]
30+
use std::os::unix::process::CommandExt;
3231

3332
pub mod options {
3433
pub static FOREGROUND: &str = "foreground";
@@ -105,8 +104,16 @@ impl Config {
105104
}
106105
}
107106

107+
#[cfg(unix)]
108+
uucore::init_sigpipe_capture!();
109+
108110
#[uucore::main]
109111
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
112+
#[cfg(unix)]
113+
if !uucore::signals::sigpipe_was_ignored() {
114+
uucore::signals::enable_pipe_errors()?;
115+
}
116+
110117
let matches =
111118
uucore::clap_localization::handle_clap_result_with_exit_code(uu_app(), args, 125)?;
112119

@@ -179,58 +186,62 @@ pub fn uu_app() -> Command {
179186
.after_help(translate!("timeout-after-help"))
180187
}
181188

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

193-
/// We should terminate child process when receiving TERM signal.
195+
/// We should terminate child process when receiving termination signals.
194196
static SIGNALED: AtomicBool = AtomicBool::new(false);
197+
/// Track which signal was received (0 = none/timeout expired naturally).
198+
static RECEIVED_SIGNAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
195199

196-
fn catch_sigterm() {
197-
use nix::sys::signal;
200+
/// Install signal handlers for termination signals.
201+
fn install_signal_handlers(term_signal: usize) {
202+
extern "C" fn handle_signal(sig: libc::c_int) {
203+
SIGNALED.store(true, atomic::Ordering::Relaxed);
204+
RECEIVED_SIGNAL.store(sig, atomic::Ordering::Relaxed);
205+
}
198206

199-
extern "C" fn handle_sigterm(signal: libc::c_int) {
200-
let signal = signal::Signal::try_from(signal).unwrap();
201-
if signal == signal::Signal::SIGTERM {
202-
SIGNALED.store(true, atomic::Ordering::Relaxed);
207+
let handler = SigHandler::Handler(handle_signal);
208+
let sigpipe_ignored = uucore::signals::sigpipe_was_ignored();
209+
210+
for sig in [
211+
Signal::SIGALRM, Signal::SIGINT, Signal::SIGQUIT, Signal::SIGHUP,
212+
Signal::SIGTERM, Signal::SIGPIPE, Signal::SIGUSR1, Signal::SIGUSR2,
213+
] {
214+
if sig != Signal::SIGPIPE || !sigpipe_ignored {
215+
let _ = unsafe { nix::sys::signal::signal(sig, handler) };
203216
}
204217
}
205218

206-
let handler = signal::SigHandler::Handler(handle_sigterm);
207-
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+
}
208222
}
209223

210224
/// Report that a signal is being sent if the verbose flag is set.
211225
fn report_if_verbose(signal: usize, cmd: &str, verbose: bool) {
212226
if verbose {
213-
let s = signal_name_by_value(signal).unwrap();
214-
show_error!(
215-
"{}",
216-
translate!("timeout-verbose-sending-signal", "signal" => s, "command" => cmd.quote())
217-
);
227+
let s = if signal == 0 { "0".to_string() } else { signal_name_by_value(signal).unwrap().to_string() };
228+
let _ = writeln!(std::io::stderr(), "timeout: sending signal {} to command {}", s, cmd.quote());
218229
}
219230
}
220231

221232
fn send_signal(process: &mut Child, signal: usize, foreground: bool) {
222233
// NOTE: GNU timeout doesn't check for errors of signal.
223234
// The subprocess might have exited just after the timeout.
224-
// Sending a signal now would return "No such process", but we should still try to kill the children.
225-
if foreground {
226-
let _ = process.send_signal(signal);
227-
} else {
228-
let _ = process.send_signal_group(signal);
229-
let kill_signal = signal_by_name_or_value("KILL").unwrap();
230-
let continued_signal = signal_by_name_or_value("CONT").unwrap();
231-
if signal != kill_signal && signal != continued_signal {
232-
_ = process.send_signal_group(continued_signal);
233-
}
235+
let _ = process.send_signal(signal);
236+
if signal == 0 || foreground {
237+
return;
238+
}
239+
let _ = process.send_signal_group(signal);
240+
let kill_sig = signal_by_name_or_value("KILL").unwrap();
241+
let cont_sig = signal_by_name_or_value("CONT").unwrap();
242+
if signal != kill_sig && signal != cont_sig {
243+
let _ = process.send_signal(cont_sig);
244+
let _ = process.send_signal_group(cont_sig);
234245
}
235246
}
236247

@@ -320,85 +331,85 @@ fn timeout(
320331
if !foreground {
321332
let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0));
322333
}
323-
#[cfg(unix)]
324-
enable_pipe_errors()?;
325334

326-
let process = &mut process::Command::new(&cmd[0])
335+
let mut cmd_builder = process::Command::new(&cmd[0]);
336+
cmd_builder
327337
.args(&cmd[1..])
328338
.stdin(Stdio::inherit())
329339
.stdout(Stdio::inherit())
330-
.stderr(Stdio::inherit())
331-
.spawn()
332-
.map_err(|err| {
333-
let status_code = match err.kind() {
334-
ErrorKind::NotFound => ExitStatus::CommandNotFound.into(),
335-
ErrorKind::PermissionDenied => ExitStatus::CannotInvoke.into(),
336-
_ => ExitStatus::CannotInvoke.into(),
337-
};
338-
USimpleError::new(
339-
status_code,
340-
translate!("timeout-error-failed-to-execute-process", "error" => err),
341-
)
342-
})?;
343-
unblock_sigchld();
344-
catch_sigterm();
345-
// Wait for the child process for the specified time period.
346-
//
347-
// If the process exits within the specified time period (the
348-
// `Ok(Some(_))` arm), then return the appropriate status code.
349-
//
350-
// If the process does not exit within that time (the `Ok(None)`
351-
// arm) and `kill_after` is specified, then try sending `SIGKILL`.
352-
//
353-
// TODO The structure of this block is extremely similar to the
354-
// structure of `wait_or_kill_process()`. They can probably be
355-
// refactored into some common function.
340+
.stderr(Stdio::inherit());
341+
342+
#[cfg(unix)]
343+
{
344+
#[cfg(target_os = "linux")]
345+
let death_sig = Signal::try_from(signal as i32).ok();
346+
let sigpipe_was_ignored = uucore::signals::sigpipe_was_ignored();
347+
348+
unsafe {
349+
cmd_builder.pre_exec(move || {
350+
let _ = nix::sys::signal::signal(Signal::SIGTTIN, SigHandler::SigDfl);
351+
let _ = nix::sys::signal::signal(Signal::SIGTTOU, SigHandler::SigDfl);
352+
if sigpipe_was_ignored {
353+
let _ = nix::sys::signal::signal(Signal::SIGPIPE, SigHandler::SigIgn);
354+
}
355+
#[cfg(target_os = "linux")]
356+
if let Some(sig) = death_sig {
357+
let _ = nix::sys::prctl::set_pdeathsig(sig);
358+
}
359+
Ok(())
360+
});
361+
}
362+
}
363+
364+
let process = &mut cmd_builder.spawn().map_err(|err| {
365+
let status_code = match err.kind() {
366+
ErrorKind::NotFound => ExitStatus::CommandNotFound.into(),
367+
ErrorKind::PermissionDenied => ExitStatus::CannotInvoke.into(),
368+
_ => ExitStatus::CannotInvoke.into(),
369+
};
370+
USimpleError::new(
371+
status_code,
372+
translate!("timeout-error-failed-to-execute-process", "error" => err),
373+
)
374+
})?;
375+
install_sigchld();
376+
install_signal_handlers(signal);
377+
356378
match process.wait_or_timeout(duration, Some(&SIGNALED)) {
357379
Ok(Some(status)) => Err(status
358380
.code()
359381
.unwrap_or_else(|| preserve_signal_info(status.signal().unwrap()))
360382
.into()),
361383
Ok(None) => {
362-
report_if_verbose(signal, &cmd[0], verbose);
363-
send_signal(process, signal, foreground);
364-
match kill_after {
365-
None => {
366-
let status = process.wait()?;
367-
if SIGNALED.load(atomic::Ordering::Relaxed) {
368-
Err(ExitStatus::Terminated.into())
369-
} else if preserve_status {
370-
if let Some(ec) = status.code() {
371-
Err(ec.into())
372-
} else if let Some(sc) = status.signal() {
373-
Err(ExitStatus::SignalSent(sc.try_into().unwrap()).into())
374-
} else {
375-
Err(ExitStatus::CommandTimedOut.into())
376-
}
377-
} else {
378-
Err(ExitStatus::CommandTimedOut.into())
379-
}
380-
}
381-
Some(kill_after) => {
382-
match wait_or_kill_process(
383-
process,
384-
&cmd[0],
385-
kill_after,
386-
preserve_status,
387-
foreground,
388-
verbose,
389-
) {
390-
Ok(status) => Err(status.into()),
391-
Err(e) => Err(USimpleError::new(
392-
ExitStatus::TimeoutFailed.into(),
393-
e.to_string(),
394-
)),
395-
}
396-
}
384+
let received_sig = RECEIVED_SIGNAL.load(atomic::Ordering::Relaxed);
385+
let is_external_signal = received_sig > 0 && received_sig != libc::SIGALRM;
386+
let signal_to_send = if is_external_signal { received_sig as usize } else { signal };
387+
388+
report_if_verbose(signal_to_send, &cmd[0], verbose);
389+
send_signal(process, signal_to_send, foreground);
390+
391+
if let Some(kill_after) = kill_after {
392+
return match wait_or_kill_process(process, &cmd[0], kill_after, preserve_status, foreground, verbose) {
393+
Ok(status) => Err(status.into()),
394+
Err(e) => Err(USimpleError::new(ExitStatus::TimeoutFailed.into(), e.to_string())),
395+
};
396+
}
397+
398+
let status = process.wait()?;
399+
if is_external_signal {
400+
Err(ExitStatus::SignalSent(received_sig as usize).into())
401+
} else if SIGNALED.load(atomic::Ordering::Relaxed) {
402+
Err(ExitStatus::CommandTimedOut.into())
403+
} else if preserve_status {
404+
Err(status.code()
405+
.or_else(|| status.signal().map(|s| ExitStatus::SignalSent(s as usize).into()))
406+
.unwrap_or(ExitStatus::CommandTimedOut.into())
407+
.into())
408+
} else {
409+
Err(ExitStatus::CommandTimedOut.into())
397410
}
398411
}
399412
Err(_) => {
400-
// We're going to return ERR_EXIT_STATUS regardless of
401-
// whether `send_signal()` succeeds or fails
402413
send_signal(process, signal, foreground);
403414
Err(ExitStatus::TimeoutFailed.into())
404415
}

0 commit comments

Comments
 (0)