Skip to content

Commit 293ded3

Browse files
committed
timeout: fix signal handling, SIGPIPE preservation, and GNU test compatibility
1 parent 376d5b2 commit 293ded3

6 files changed

Lines changed: 214 additions & 89 deletions

File tree

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,10 @@ noxfer
114114
ofile
115115
oflag
116116
oflags
117+
pdeathsig
117118
peekable
118119
performant
120+
prctl
119121
precompiled
120122
precompute
121123
preload
@@ -140,8 +142,17 @@ SETFL
140142
setlocale
141143
shortcode
142144
shortcodes
145+
setpgid
143146
sigaction
147+
CHLD
148+
chld
149+
SIGCHLD
150+
sigchld
144151
siginfo
152+
SIGTTIN
153+
sigttin
154+
SIGTTOU
155+
sigttou
145156
sigusr
146157
strcasecmp
147158
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/uu/yes/src/yes.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
2727

2828
match exec(&buffer) {
2929
Ok(()) => Ok(()),
30-
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => Ok(()),
3130
Err(err) => Err(USimpleError::new(
3231
1,
3332
translate!("yes-error-standard-output", "error" => 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)