diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 72f5aa7923e..a09de7b9463 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) chdir progname subcommand subcommands unsets setenv putenv spawnp SIGSEGV SIGBUS sigaction +// spell-checker:ignore (ToDO) chdir execvp progname subcommand subcommands unsets setenv putenv spawnp SIGSEGV SIGBUS sigaction pub mod native_int_str; pub mod split_iterator; @@ -21,14 +21,16 @@ use native_int_str::{ use nix::libc; #[cfg(unix)] use nix::sys::signal::{SigHandler::SigIgn, Signal, signal}; +#[cfg(unix)] +use nix::unistd::execvp; use std::borrow::Cow; use std::env; +#[cfg(unix)] +use std::ffi::CString; use std::ffi::{OsStr, OsString}; use std::io::{self, Write}; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; -#[cfg(unix)] -use std::os::unix::process::CommandExt; use uucore::display::Quotable; use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError}; @@ -604,16 +606,37 @@ impl EnvAppData { #[cfg(unix)] { - // Execute the program using exec, which replaces the current process. - let err = std::process::Command::new(&*prog) - .arg0(&*arg0) - .args(args) - .exec(); - - // exec() only returns if there was an error - match err.kind() { - io::ErrorKind::NotFound => Err(self.make_error_no_such_file_or_dir(&prog)), - io::ErrorKind::PermissionDenied => { + // Convert program name to CString. + let Ok(prog_cstring) = CString::new(prog.as_bytes()) else { + return Err(self.make_error_no_such_file_or_dir(&prog)); + }; + + // Prepare arguments for execvp. + let mut argv = Vec::new(); + + // Convert arg0 to CString. + let Ok(arg0_cstring) = CString::new(arg0.as_bytes()) else { + return Err(self.make_error_no_such_file_or_dir(&prog)); + }; + argv.push(arg0_cstring); + + // Convert remaining arguments to CString. + for arg in args { + let Ok(arg_cstring) = CString::new(arg.as_bytes()) else { + return Err(self.make_error_no_such_file_or_dir(&prog)); + }; + argv.push(arg_cstring); + } + + // FIXME: libc::execvp() can be replaced with Command::exec() once https://github.com/rust-lang/rust/issues/97889#issuecomment-2007391597 + // is merged into rust stable. + + // Execute the program using execvp. this replaces the current + // process. The execvp function takes care of appending a NULL + // argument to the argument list so that we don't have to. + match execvp(&prog_cstring, &argv) { + Err(nix::errno::Errno::ENOENT) => Err(self.make_error_no_such_file_or_dir(&prog)), + Err(nix::errno::Errno::EACCES) => { uucore::show_error!( "{}", translate!( @@ -623,16 +646,19 @@ impl EnvAppData { ); Err(126.into()) } - _ => { + Err(_) => { uucore::show_error!( "{}", translate!( "env-error-unknown", - "error" => err + "error" => "execvp failed" ) ); Err(126.into()) } + Ok(_) => { + unreachable!("execvp should never return on success") + } } } diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 68e7e03b506..8705166d173 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -1862,3 +1862,79 @@ fn test_braced_variable_error_unexpected_character() { .fails_with_code(125) .stderr_contains("Unexpected character: '?'"); } + +#[test] +#[cfg(unix)] +fn test_ignore_signal_pipe_broken_pipe_regression() { + // Test that --ignore-signal=PIPE properly ignores SIGPIPE in child processes. + // When SIGPIPE is ignored, processes should handle broken pipes gracefully + // instead of being terminated by the signal. + // + // Regression test for: https://github.com/uutils/coreutils/issues/9617 + + use std::io::{BufRead, BufReader}; + use std::process::{Command, Stdio}; + + let scene = TestScenario::new(util_name!()); + + // Helper function to simulate a broken pipe scenario (like "yes | head -n1") + let test_sigpipe_behavior = |use_ignore_signal: bool| -> i32 { + let mut cmd = Command::new(&scene.bin_path); + cmd.arg("env"); + + if use_ignore_signal { + cmd.arg("--ignore-signal=PIPE"); + } + + cmd.arg("yes").stdout(Stdio::piped()).stderr(Stdio::null()); + + let mut child = cmd.spawn().expect("Failed to spawn env process"); + + // Read exactly one line then close the pipe to trigger SIGPIPE + if let Some(stdout) = child.stdout.take() { + let mut reader = BufReader::new(stdout); + let mut line = String::new(); + let _ = reader.read_line(&mut line); + // Pipe closes when reader is dropped, sending SIGPIPE to writing process + } + + match child.wait() { + Ok(status) => { + // Process terminated by signal (likely SIGPIPE = 13) + // Unix convention: signal death = 128 + signal_number + status.code().unwrap_or(141) // 128 + 13 + } + Err(_) => 141, + } + }; + + // Test without signal ignoring - should be killed by SIGPIPE + let normal_exit_code = test_sigpipe_behavior(false); + println!("Normal 'env yes' exit code: {normal_exit_code}"); + + // Test with --ignore-signal=PIPE - should handle broken pipe gracefully + let ignore_signal_exit_code = test_sigpipe_behavior(true); + println!("With --ignore-signal=PIPE exit code: {ignore_signal_exit_code}"); + + // Verify the --ignore-signal=PIPE flag changes the behavior + assert!( + ignore_signal_exit_code != 141, + "--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}" + ); + + // Expected behavior: + assert_eq!( + normal_exit_code, 141, + "Without --ignore-signal, process should be killed by SIGPIPE" + ); + assert_ne!( + ignore_signal_exit_code, 141, + "With --ignore-signal=PIPE, process should NOT be killed by SIGPIPE" + ); + + // Process should exit gracefully when SIGPIPE is ignored + assert!( + ignore_signal_exit_code == 0 || ignore_signal_exit_code == 1, + "With --ignore-signal=PIPE, process should exit gracefully (0 or 1), got: {ignore_signal_exit_code}" + ); +}