Skip to content

Commit ad74ceb

Browse files
committed
yes,timeout: fix SIGPIPE handling when trapped (fixes #7252)
1 parent 0d403a4 commit ad74ceb

5 files changed

Lines changed: 98 additions & 24 deletions

File tree

src/uu/timeout/src/timeout.rs

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ 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::{
2623
format_usage, show_error,
2724
signals::{signal_by_name_or_value, signal_name_by_value},
@@ -105,8 +102,16 @@ impl Config {
105102
}
106103
}
107104

105+
#[cfg(unix)]
106+
uucore::init_sigpipe_capture!();
107+
108108
#[uucore::main]
109109
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
110+
#[cfg(unix)]
111+
if !uucore::signals::sigpipe_was_ignored() {
112+
uucore::signals::enable_pipe_errors()?;
113+
}
114+
110115
let matches =
111116
uucore::clap_localization::handle_clap_result_with_exit_code(uu_app(), args, 125)?;
112117

@@ -320,26 +325,28 @@ fn timeout(
320325
if !foreground {
321326
let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0));
322327
}
323-
#[cfg(unix)]
324-
enable_pipe_errors()?;
325328

326-
let process = &mut process::Command::new(&cmd[0])
329+
let mut cmd_builder = process::Command::new(&cmd[0]);
330+
cmd_builder
327331
.args(&cmd[1..])
328332
.stdin(Stdio::inherit())
329333
.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-
})?;
334+
.stderr(Stdio::inherit());
335+
336+
#[cfg(unix)]
337+
uucore::signals::preserve_sigpipe_for_child(&mut cmd_builder);
338+
339+
let process = &mut cmd_builder.spawn().map_err(|err| {
340+
let status_code = match err.kind() {
341+
ErrorKind::NotFound => ExitStatus::CommandNotFound.into(),
342+
ErrorKind::PermissionDenied => ExitStatus::CannotInvoke.into(),
343+
_ => ExitStatus::CannotInvoke.into(),
344+
};
345+
USimpleError::new(
346+
status_code,
347+
translate!("timeout-error-failed-to-execute-process", "error" => err),
348+
)
349+
})?;
343350
unblock_sigchld();
344351
catch_sigterm();
345352
// Wait for the child process for the specified time period.

src/uu/yes/src/yes.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,22 @@ use std::ffi::OsString;
1111
use std::io::{self, Write};
1212
use uucore::error::{UResult, USimpleError};
1313
use uucore::format_usage;
14-
#[cfg(unix)]
15-
use uucore::signals::enable_pipe_errors;
1614
use uucore::translate;
1715

1816
// it's possible that using a smaller or larger buffer might provide better performance on some
1917
// systems, but honestly this is good enough
2018
const BUF_SIZE: usize = 16 * 1024;
2119

20+
#[cfg(unix)]
21+
uucore::init_sigpipe_capture!();
22+
2223
#[uucore::main]
2324
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
25+
#[cfg(unix)]
26+
if !uucore::signals::sigpipe_was_ignored() {
27+
let _ = uucore::signals::enable_pipe_errors();
28+
}
29+
2430
let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
2531

2632
let mut buffer = Vec::with_capacity(BUF_SIZE);
@@ -29,7 +35,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
2935

3036
match exec(&buffer) {
3137
Ok(()) => Ok(()),
32-
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => Ok(()),
38+
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => {
39+
#[cfg(unix)]
40+
if uucore::signals::sigpipe_was_ignored() {
41+
uucore::show_error!(
42+
"{}",
43+
translate!("yes-error-standard-output", "error" => err)
44+
);
45+
}
46+
Ok(())
47+
}
3348
Err(err) => Err(USimpleError::new(
3449
1,
3550
translate!("yes-error-standard-output", "error" => err),
@@ -113,8 +128,6 @@ fn prepare_buffer(buf: &mut Vec<u8>) {
113128
pub fn exec(bytes: &[u8]) -> io::Result<()> {
114129
let stdout = io::stdout();
115130
let mut stdout = stdout.lock();
116-
#[cfg(unix)]
117-
enable_pipe_errors()?;
118131

119132
loop {
120133
stdout.write_all(bytes)?;

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,25 @@ pub const fn sigpipe_was_ignored() -> bool {
488488
false
489489
}
490490

491+
/// Configures a Command to preserve SIGPIPE disposition for child processes.
492+
/// When the parent had SIGPIPE ignored, this ensures the child also has it ignored.
493+
#[cfg(unix)]
494+
pub fn preserve_sigpipe_for_child(cmd: &mut std::process::Command) {
495+
use std::os::unix::process::CommandExt;
496+
if sigpipe_was_ignored() {
497+
unsafe {
498+
cmd.pre_exec(|| {
499+
nix::sys::signal::signal(
500+
nix::sys::signal::Signal::SIGPIPE,
501+
nix::sys::signal::SigHandler::SigIgn,
502+
)
503+
.map_err(std::io::Error::other)?;
504+
Ok(())
505+
});
506+
}
507+
}
508+
}
509+
491510
#[cfg(target_os = "linux")]
492511
pub fn ensure_stdout_not_broken() -> std::io::Result<bool> {
493512
use nix::{

tests/by-util/test_timeout.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ use rstest::rstest;
99

1010
use uucore::display::Quotable;
1111
use uutests::new_ucmd;
12+
#[cfg(all(unix, feature = "yes"))]
13+
use uutests::util::TestScenario;
14+
#[cfg(all(unix, feature = "yes"))]
15+
use uutests::util_name;
1216

1317
#[test]
1418
fn test_invalid_arg() {
@@ -235,3 +239,17 @@ fn test_command_cannot_invoke() {
235239
// Try to execute a directory (should give permission denied or similar)
236240
new_ucmd!().args(&["1", "/"]).fails_with_code(126);
237241
}
242+
243+
/// Test for https://github.com/uutils/coreutils/issues/7252
244+
#[test]
245+
#[cfg(unix)]
246+
#[cfg(feature = "yes")]
247+
fn test_sigpipe_ignored_preserves_for_child() {
248+
let ts = TestScenario::new(util_name!());
249+
let bin = ts.bin_path.clone().into_os_string();
250+
let result = ts
251+
.cmd_shell("trap '' PIPE; \"$BIN\" timeout 10 \"$BIN\" yes 2>err |:; cat err")
252+
.env("BIN", &bin)
253+
.succeeds();
254+
assert!(result.stdout_str().contains("Broken pipe"));
255+
}

tests/by-util/test_yes.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ use std::process::ExitStatus;
99
use std::os::unix::process::ExitStatusExt;
1010

1111
use uutests::new_ucmd;
12+
#[cfg(unix)]
13+
use uutests::util::TestScenario;
14+
#[cfg(unix)]
15+
use uutests::util_name;
1216

1317
#[cfg(unix)]
1418
fn check_termination(result: ExitStatus) {
@@ -111,3 +115,16 @@ fn test_non_utf8() {
111115
&b"\xbf\xff\xee bar\n".repeat(5000),
112116
);
113117
}
118+
119+
/// Test for https://github.com/uutils/coreutils/issues/7252
120+
#[test]
121+
#[cfg(unix)]
122+
fn test_sigpipe_ignored_prints_error() {
123+
let ts = TestScenario::new(util_name!());
124+
let bin = ts.bin_path.clone().into_os_string();
125+
let result = ts
126+
.cmd_shell("trap '' PIPE; \"$BIN\" yes 2>err |:; cat err")
127+
.env("BIN", &bin)
128+
.succeeds();
129+
assert!(result.stdout_str().contains("Broken pipe"));
130+
}

0 commit comments

Comments
 (0)