Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 41 additions & 15 deletions src/uu/env/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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};
Expand Down Expand Up @@ -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!(
Expand All @@ -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")
}
}
}

Expand Down
76 changes: 76 additions & 0 deletions tests/by-util/test_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
);
}
Loading