Skip to content

Commit cffd508

Browse files
committed
env: fix "--ignore-signal=PIPE"
- revert #9614 because `Command::exec()` resets the default signal handler for SIGPIPE, interfering with the option `--ignore-signal=PIPE` - add regression-test for --ignore-signal=PIPE - add FIXME comment about execvp() Signed-off-by: Etienne Cordonnier <[email protected]>
1 parent 666c6df commit cffd508

File tree

2 files changed

+134
-20
lines changed

2 files changed

+134
-20
lines changed

src/uu/env/src/env.rs

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,18 @@ use nix::sys::signal::{
2424
SigHandler::{SigDfl, SigIgn},
2525
SigSet, SigmaskHow, Signal, signal, sigprocmask,
2626
};
27+
#[cfg(unix)]
28+
use nix::unistd::execvp;
2729
use std::borrow::Cow;
2830
#[cfg(unix)]
2931
use std::collections::{BTreeMap, BTreeSet};
3032
use std::env;
33+
#[cfg(unix)]
34+
use std::ffi::CString;
3135
use std::ffi::{OsStr, OsString};
3236
use std::io;
3337
#[cfg(unix)]
3438
use std::os::unix::ffi::OsStrExt;
35-
#[cfg(unix)]
36-
use std::os::unix::process::CommandExt;
3739

3840
use uucore::display::{Quotable, print_all_env_vars};
3941
use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError};
@@ -701,6 +703,24 @@ impl EnvAppData {
701703
#[cfg(unix)]
702704
{
703705
let mut signal_action_log = SignalActionLog::default();
706+
707+
// Rust ignores SIGPIPE (see https://github.com/rust-lang/rust/issues/62569).
708+
// We restore its default action here, but only if the user hasn't explicitly
709+
// specified signal handling for SIGPIPE via --ignore-signal or --block-signal.
710+
let sigpipe_value = nix::libc::SIGPIPE as usize;
711+
let user_handled_sigpipe = opts.ignore_signal.signals.contains(&sigpipe_value)
712+
|| opts.block_signal.signals.contains(&sigpipe_value);
713+
714+
if !user_handled_sigpipe
715+
&& !opts.ignore_signal.apply_all
716+
&& !opts.block_signal.apply_all
717+
{
718+
// Only restore SIGPIPE to default if user hasn't explicitly handled it
719+
unsafe {
720+
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
721+
}
722+
}
723+
704724
apply_signal_action(
705725
&opts.default_signal,
706726
&mut signal_action_log,
@@ -782,16 +802,37 @@ impl EnvAppData {
782802

783803
#[cfg(unix)]
784804
{
785-
// Execute the program using exec, which replaces the current process.
786-
let err = std::process::Command::new(&*prog)
787-
.arg0(&*arg0)
788-
.args(args)
789-
.exec();
790-
791-
// exec() only returns if there was an error
792-
match err.kind() {
793-
io::ErrorKind::NotFound => Err(self.make_error_no_such_file_or_dir(&prog)),
794-
io::ErrorKind::PermissionDenied => {
805+
// Convert program name to CString.
806+
let prog_os: &OsStr = prog.as_ref();
807+
let Ok(prog_cstring) = CString::new(prog_os.as_bytes()) else {
808+
return Err(self.make_error_no_such_file_or_dir(&prog));
809+
};
810+
811+
// Prepare arguments for execvp.
812+
let mut argv = Vec::new();
813+
814+
// Convert arg0 to CString.
815+
let arg0_os: &OsStr = arg0.as_ref();
816+
let Ok(arg0_cstring) = CString::new(arg0_os.as_bytes()) else {
817+
return Err(self.make_error_no_such_file_or_dir(&prog));
818+
};
819+
argv.push(arg0_cstring);
820+
821+
// Convert remaining arguments to CString.
822+
for arg in args {
823+
let arg_os = arg;
824+
let Ok(arg_cstring) = CString::new(arg_os.as_bytes()) else {
825+
return Err(self.make_error_no_such_file_or_dir(&prog));
826+
};
827+
argv.push(arg_cstring);
828+
}
829+
830+
// Execute the program using execvp. this replaces the current
831+
// process. The execvp function takes care of appending a NULL
832+
// argument to the argument list so that we don't have to.
833+
match execvp(&prog_cstring, &argv) {
834+
Err(nix::errno::Errno::ENOENT) => Err(self.make_error_no_such_file_or_dir(&prog)),
835+
Err(nix::errno::Errno::EACCES) => {
795836
uucore::show_error!(
796837
"{}",
797838
translate!(
@@ -801,16 +842,19 @@ impl EnvAppData {
801842
);
802843
Err(126.into())
803844
}
804-
_ => {
845+
Err(_) => {
805846
uucore::show_error!(
806847
"{}",
807848
translate!(
808849
"env-error-unknown",
809-
"error" => err
850+
"error" => "execvp failed"
810851
)
811852
);
812853
Err(126.into())
813854
}
855+
Ok(_) => {
856+
unreachable!("execvp should never return on success")
857+
}
814858
}
815859
}
816860

@@ -1097,12 +1141,6 @@ fn list_signal_handling(log: &SignalActionLog) {
10971141

10981142
#[uucore::main]
10991143
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
1100-
// Rust ignores SIGPIPE (see https://github.com/rust-lang/rust/issues/62569).
1101-
// We restore its default action here.
1102-
#[cfg(unix)]
1103-
unsafe {
1104-
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
1105-
}
11061144
EnvAppData::default().run_env(args)
11071145
}
11081146

tests/by-util/test_env.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1962,3 +1962,79 @@ fn test_non_utf8_env_vars() {
19621962
.succeeds()
19631963
.stdout_contains_bytes(b"NON_UTF8_VAR=hello\x80world");
19641964
}
1965+
1966+
#[test]
1967+
#[cfg(unix)]
1968+
fn test_ignore_signal_pipe_broken_pipe_regression() {
1969+
// Test that --ignore-signal=PIPE properly ignores SIGPIPE in child processes.
1970+
// When SIGPIPE is ignored, processes should handle broken pipes gracefully
1971+
// instead of being terminated by the signal.
1972+
//
1973+
// Regression test for: https://github.com/uutils/coreutils/issues/9617
1974+
1975+
use std::io::{BufRead, BufReader};
1976+
use std::process::{Command, Stdio};
1977+
1978+
let scene = TestScenario::new(util_name!());
1979+
1980+
// Helper function to simulate a broken pipe scenario (like "yes | head -n1")
1981+
let test_sigpipe_behavior = |use_ignore_signal: bool| -> i32 {
1982+
let mut cmd = Command::new(&scene.bin_path);
1983+
cmd.arg("env");
1984+
1985+
if use_ignore_signal {
1986+
cmd.arg("--ignore-signal=PIPE");
1987+
}
1988+
1989+
cmd.arg("yes").stdout(Stdio::piped()).stderr(Stdio::null());
1990+
1991+
let mut child = cmd.spawn().expect("Failed to spawn env process");
1992+
1993+
// Read exactly one line then close the pipe to trigger SIGPIPE
1994+
if let Some(stdout) = child.stdout.take() {
1995+
let mut reader = BufReader::new(stdout);
1996+
let mut line = String::new();
1997+
let _ = reader.read_line(&mut line);
1998+
// Pipe closes when reader is dropped, sending SIGPIPE to writing process
1999+
}
2000+
2001+
match child.wait() {
2002+
Ok(status) => {
2003+
// Process terminated by signal (likely SIGPIPE = 13)
2004+
// Unix convention: signal death = 128 + signal_number
2005+
status.code().unwrap_or(141) // 128 + 13
2006+
}
2007+
Err(_) => 141,
2008+
}
2009+
};
2010+
2011+
// Test without signal ignoring - should be killed by SIGPIPE
2012+
let normal_exit_code = test_sigpipe_behavior(false);
2013+
println!("Normal 'env yes' exit code: {normal_exit_code}");
2014+
2015+
// Test with --ignore-signal=PIPE - should handle broken pipe gracefully
2016+
let ignore_signal_exit_code = test_sigpipe_behavior(true);
2017+
println!("With --ignore-signal=PIPE exit code: {ignore_signal_exit_code}");
2018+
2019+
// Verify the --ignore-signal=PIPE flag changes the behavior
2020+
assert!(
2021+
ignore_signal_exit_code != 141,
2022+
"--ignore-signal=PIPE had no effect! Process was still killed by SIGPIPE (exit code 141). Normal: {normal_exit_code}, --ignore-signal: {ignore_signal_exit_code}"
2023+
);
2024+
2025+
// Expected behavior:
2026+
assert_eq!(
2027+
normal_exit_code, 141,
2028+
"Without --ignore-signal, process should be killed by SIGPIPE"
2029+
);
2030+
assert_ne!(
2031+
ignore_signal_exit_code, 141,
2032+
"With --ignore-signal=PIPE, process should NOT be killed by SIGPIPE"
2033+
);
2034+
2035+
// Process should exit gracefully when SIGPIPE is ignored
2036+
assert!(
2037+
ignore_signal_exit_code == 0 || ignore_signal_exit_code == 1,
2038+
"With --ignore-signal=PIPE, process should exit gracefully (0 or 1), got: {ignore_signal_exit_code}"
2039+
);
2040+
}

0 commit comments

Comments
 (0)