diff --git a/src/uu/env/locales/en-US.ftl b/src/uu/env/locales/en-US.ftl index dd7cf2176b0..a460c69fba4 100644 --- a/src/uu/env/locales/en-US.ftl +++ b/src/uu/env/locales/en-US.ftl @@ -12,6 +12,9 @@ env-help-debug = print verbose information for each processing step env-help-split-string = process and split S into separate arguments; used to pass multiple arguments on shebang lines env-help-argv0 = Override the zeroth argument passed to the command being executed. Without this option a default value of `command` is used. env-help-ignore-signal = set handling of SIG signal(s) to do nothing +env-help-default-signal = reset handling of SIG signal(s) to the default action +env-help-block-signal = block delivery of SIG signal(s) while running COMMAND +env-help-list-signal-handling = list signal handling changes requested by preceding options # Error messages env-error-missing-closing-quote = no terminating quote in -S string at position { $position } for quote '{ $quote }' diff --git a/src/uu/env/locales/fr-FR.ftl b/src/uu/env/locales/fr-FR.ftl index 382568dc0df..2ca1968d230 100644 --- a/src/uu/env/locales/fr-FR.ftl +++ b/src/uu/env/locales/fr-FR.ftl @@ -12,6 +12,9 @@ env-help-debug = afficher des informations détaillées pour chaque étape de tr env-help-split-string = traiter et diviser S en arguments séparés ; utilisé pour passer plusieurs arguments sur les lignes shebang env-help-argv0 = Remplacer le zéroième argument passé à la commande en cours d'exécution. Sans cette option, une valeur par défaut de `command` est utilisée. env-help-ignore-signal = définir la gestion du/des signal/signaux SIG pour ne rien faire +env-help-default-signal = réinitialiser la gestion du/des signal/signaux SIG à l'action par défaut +env-help-block-signal = bloquer la livraison du/des signal/signaux SIG pendant l'exécution de COMMAND +env-help-list-signal-handling = lister les traitements de signaux modifiés par les options précédentes # Messages d'erreur env-error-missing-closing-quote = aucune guillemet de fermeture dans la chaîne -S à la position { $position } pour la guillemet '{ $quote }' diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index da0daf80c81..ab42e1f87e5 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 execvp 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 Sigmask sigprocmask pub mod native_int_str; pub mod split_iterator; @@ -20,10 +20,15 @@ use native_int_str::{ #[cfg(unix)] use nix::libc; #[cfg(unix)] -use nix::sys::signal::{SigHandler::SigIgn, Signal, signal}; +use nix::sys::signal::{ + SigHandler::{SigDfl, SigIgn}, + SigSet, SigmaskHow, Signal, signal, sigprocmask, +}; #[cfg(unix)] use nix::unistd::execvp; use std::borrow::Cow; +#[cfg(unix)] +use std::collections::{BTreeMap, BTreeSet}; use std::env; #[cfg(unix)] use std::ffi::CString; @@ -36,7 +41,7 @@ use uucore::display::Quotable; use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError}; use uucore::line_ending::LineEnding; #[cfg(unix)] -use uucore::signals::signal_by_name_or_value; +use uucore::signals::{ALL_SIGNALS, signal_by_name_or_value, signal_name_by_value}; use uucore::translate; use uucore::{format_usage, show_warning}; @@ -86,6 +91,9 @@ mod options { pub const SPLIT_STRING: &str = "split-string"; pub const ARGV0: &str = "argv0"; pub const IGNORE_SIGNAL: &str = "ignore-signal"; + pub const DEFAULT_SIGNAL: &str = "default-signal"; + pub const BLOCK_SIGNAL: &str = "block-signal"; + pub const LIST_SIGNAL_HANDLING: &str = "list-signal-handling"; } struct Options<'a> { @@ -98,7 +106,13 @@ struct Options<'a> { program: Vec<&'a OsStr>, argv0: Option<&'a OsStr>, #[cfg(unix)] - ignore_signal: Vec, + ignore_signal: SignalRequest, + #[cfg(unix)] + default_signal: SignalRequest, + #[cfg(unix)] + block_signal: SignalRequest, + #[cfg(unix)] + list_signal_handling: bool, } /// print `name=value` env pairs on screen @@ -157,23 +171,21 @@ fn parse_signal_value(signal_name: &str) -> UResult { } #[cfg(unix)] -fn parse_signal_opt<'a>(opts: &mut Options<'a>, opt: &'a OsStr) -> UResult<()> { +fn parse_signal_opt(target: &mut SignalRequest, opt: &OsStr) -> UResult<()> { if opt.is_empty() { return Ok(()); } - let signals: Vec<&'a OsStr> = opt + if opt == "__ALL__" { + target.apply_all = true; + return Ok(()); + } + + for sig in opt .as_bytes() .split(|&b| b == b',') + .filter(|chunk| !chunk.is_empty()) .map(OsStr::from_bytes) - .collect(); - - let mut sig_vec = Vec::with_capacity(signals.len()); - for sig in signals { - if !sig.is_empty() { - sig_vec.push(sig); - } - } - for sig in sig_vec { + { let Some(sig_str) = sig.to_str() else { return Err(USimpleError::new( 1, @@ -181,14 +193,125 @@ fn parse_signal_opt<'a>(opts: &mut Options<'a>, opt: &'a OsStr) -> UResult<()> { )); }; let sig_val = parse_signal_value(sig_str)?; - if !opts.ignore_signal.contains(&sig_val) { - opts.ignore_signal.push(sig_val); - } + target.signals.insert(sig_val); } Ok(()) } +#[cfg(unix)] +#[derive(Default)] +struct SignalRequest { + apply_all: bool, + signals: BTreeSet, +} + +#[cfg(unix)] +impl SignalRequest { + fn is_empty(&self) -> bool { + !self.apply_all && self.signals.is_empty() + } + + fn for_each_signal(&self, mut f: F) -> UResult<()> + where + F: FnMut(usize, bool) -> UResult<()>, + { + if self.is_empty() { + return Ok(()); + } + for &sig in &self.signals { + f(sig, true)?; + } + if self.apply_all { + for sig_value in 1..ALL_SIGNALS.len() { + if self.signals.contains(&sig_value) { + continue; + } + // SIGKILL (9) and SIGSTOP (17 on mac, 19 on linux) cannot be caught or ignored + if sig_value == libc::SIGKILL as usize || sig_value == libc::SIGSTOP as usize { + continue; + } + f(sig_value, false)?; + } + } + Ok(()) + } +} + +#[cfg(unix)] +#[derive(Copy, Clone)] +enum SignalActionKind { + Default, + Ignore, + Block, +} + +#[cfg(unix)] +#[derive(Copy, Clone)] +struct SignalActionRecord { + kind: SignalActionKind, + explicit: bool, +} + +#[cfg(unix)] +#[derive(Default)] +struct SignalActionLog { + records: BTreeMap, +} + +#[cfg(unix)] +impl SignalActionLog { + fn record(&mut self, sig_value: usize, kind: SignalActionKind, explicit: bool) { + self.records + .entry(sig_value) + .and_modify(|entry| { + entry.kind = kind; + if explicit { + entry.explicit = true; + } + }) + .or_insert(SignalActionRecord { kind, explicit }); + } +} + +#[cfg(unix)] +fn build_signal_request(matches: &clap::ArgMatches, option: &str) -> UResult { + let mut request = SignalRequest::default(); + let mut provided_values = 0usize; + + let mut explicit_empty = false; + if let Some(iter) = matches.get_many::(option) { + for opt in iter { + if opt.is_empty() { + explicit_empty = true; + continue; + } + provided_values += 1; + parse_signal_opt(&mut request, opt)?; + } + } + + let present = matches.contains_id(option); + if present && provided_values == 0 && !explicit_empty { + request.apply_all = true; + } + + Ok(request) +} + +#[cfg(unix)] +fn signal_from_value(sig_value: usize) -> UResult { + Signal::try_from(sig_value as i32).map_err(|_| { + USimpleError::new( + 125, + translate!( + "env-error-invalid-signal", + "signal" => sig_value.to_string().quote() + ), + ) + }) +} + fn load_config_file(opts: &mut Options) -> UResult<()> { // NOTE: config files are parsed using an INI parser b/c it's available and compatible with ".env"-style files // ... * but support for actual INI files, although working, is not intended, nor claimed @@ -308,10 +431,41 @@ pub fn uu_app() -> Command { Arg::new(options::IGNORE_SIGNAL) .long(options::IGNORE_SIGNAL) .value_name("SIG") + .num_args(0..=1) + .require_equals(true) .action(ArgAction::Append) + .default_missing_value("") .value_parser(ValueParser::os_string()) .help(translate!("env-help-ignore-signal")), ) + .arg( + Arg::new(options::DEFAULT_SIGNAL) + .long(options::DEFAULT_SIGNAL) + .value_name("SIG") + .num_args(0..=1) + .require_equals(true) + .action(ArgAction::Append) + .default_missing_value("") + .value_parser(ValueParser::os_string()) + .help(translate!("env-help-default-signal")), + ) + .arg( + Arg::new(options::BLOCK_SIGNAL) + .long(options::BLOCK_SIGNAL) + .value_name("SIG") + .num_args(0..=1) + .require_equals(true) + .action(ArgAction::Append) + .default_missing_value("") + .value_parser(ValueParser::os_string()) + .help(translate!("env-help-block-signal")), + ) + .arg( + Arg::new(options::LIST_SIGNAL_HANDLING) + .long(options::LIST_SIGNAL_HANDLING) + .action(ArgAction::SetTrue) + .help(translate!("env-help-list-signal-handling")), + ) } pub fn parse_args_from_str(text: &NativeIntStr) -> UResult> { @@ -414,7 +568,6 @@ impl EnvAppData { options::ARGV0, options::CHDIR, options::FILE, - options::IGNORE_SIGNAL, options::UNSET, ]; let short_flags_with_args = ['a', 'C', 'f', 'u']; @@ -489,7 +642,18 @@ impl EnvAppData { original_args: impl uucore::Args, ) -> Result<(Vec, clap::ArgMatches), Box> { let original_args: Vec = original_args.collect(); - let args = self.process_all_string_arguments(&original_args)?; + let mut args = self.process_all_string_arguments(&original_args)?; + + for arg in &mut args { + if arg == "--ignore-signal" { + *arg = OsString::from("--ignore-signal=__ALL__"); + } else if arg == "--default-signal" { + *arg = OsString::from("--default-signal=__ALL__"); + } else if arg == "--block-signal" { + *arg = OsString::from("--block-signal=__ALL__"); + } + } + let app = uu_app(); let matches = match app.try_get_matches_from(args) { Ok(matches) => matches, @@ -546,7 +710,15 @@ impl EnvAppData { apply_specified_env_vars(&opts); #[cfg(unix)] - apply_ignore_signal(&opts)?; + { + let mut signal_action_log = SignalActionLog::default(); + apply_default_signal(&opts.default_signal, &mut signal_action_log)?; + apply_ignore_signal(&opts.ignore_signal, &mut signal_action_log)?; + apply_block_signal(&opts.block_signal, &mut signal_action_log)?; + if opts.list_signal_handling { + list_signal_handling(&signal_action_log); + } + } if opts.program.is_empty() { // no program provided, so just dump all env vars to stdout @@ -717,6 +889,15 @@ fn make_options(matches: &clap::ArgMatches) -> UResult> { }; let argv0 = matches.get_one::("argv0").map(|s| s.as_os_str()); + #[cfg(unix)] + let ignore_signal = build_signal_request(matches, options::IGNORE_SIGNAL)?; + #[cfg(unix)] + let default_signal = build_signal_request(matches, options::DEFAULT_SIGNAL)?; + #[cfg(unix)] + let block_signal = build_signal_request(matches, options::BLOCK_SIGNAL)?; + #[cfg(unix)] + let list_signal_handling = matches.get_flag(options::LIST_SIGNAL_HANDLING); + let mut opts = Options { ignore_env, line_ending, @@ -727,16 +908,15 @@ fn make_options(matches: &clap::ArgMatches) -> UResult> { program: vec![], argv0, #[cfg(unix)] - ignore_signal: vec![], + ignore_signal, + #[cfg(unix)] + default_signal, + #[cfg(unix)] + block_signal, + #[cfg(unix)] + list_signal_handling, }; - #[cfg(unix)] - if let Some(iter) = matches.get_many::("ignore-signal") { - for opt in iter { - parse_signal_opt(&mut opts, opt)?; - } - } - let mut begin_prog_opts = false; if let Some(mut iter) = matches.get_many::("vars") { // read NAME=VALUE arguments (and up to a single program argument) @@ -842,15 +1022,50 @@ fn apply_specified_env_vars(opts: &Options<'_>) { } #[cfg(unix)] -fn apply_ignore_signal(opts: &Options<'_>) -> UResult<()> { - for &sig_value in &opts.ignore_signal { - let sig: Signal = (sig_value as i32) - .try_into() - .map_err(|e| io::Error::from_raw_os_error(e as i32))?; +fn apply_default_signal(request: &SignalRequest, log: &mut SignalActionLog) -> UResult<()> { + request.for_each_signal(|sig_value, explicit| { + // On some platforms ALL_SIGNALS may contain values that are not valid in libc. + // Skip those invalid ones and continue (GNU env also ignores undefined signals). + let Ok(sig) = signal_from_value(sig_value) else { + return Ok(()); + }; + reset_signal(sig)?; + log.record(sig_value, SignalActionKind::Default, explicit); + // Set environment variable to communicate to Rust child processes + // that SIGPIPE should be default (not ignored) + if sig_value == nix::libc::SIGPIPE as usize { + unsafe { + std::env::set_var("RUST_SIGPIPE", "default"); + } + } + + Ok(()) + }) +} + +#[cfg(unix)] +fn apply_ignore_signal(request: &SignalRequest, log: &mut SignalActionLog) -> UResult<()> { + request.for_each_signal(|sig_value, explicit| { + let Ok(sig) = signal_from_value(sig_value) else { + return Ok(()); + }; ignore_signal(sig)?; - } - Ok(()) + log.record(sig_value, SignalActionKind::Ignore, explicit); + Ok(()) + }) +} + +#[cfg(unix)] +fn apply_block_signal(request: &SignalRequest, log: &mut SignalActionLog) -> UResult<()> { + request.for_each_signal(|sig_value, explicit| { + let Ok(sig) = signal_from_value(sig_value) else { + return Ok(()); + }; + block_signal(sig)?; + log.record(sig_value, SignalActionKind::Block, explicit); + Ok(()) + }) } #[cfg(unix)] @@ -866,6 +1081,51 @@ fn ignore_signal(sig: Signal) -> UResult<()> { Ok(()) } +#[cfg(unix)] +fn reset_signal(sig: Signal) -> UResult<()> { + let result = unsafe { signal(sig, SigDfl) }; + if let Err(err) = result { + return Err(USimpleError::new( + 125, + translate!("env-error-failed-set-signal-action", "signal" => (sig as i32), "error" => err.desc()), + )); + } + Ok(()) +} + +#[cfg(unix)] +fn block_signal(sig: Signal) -> UResult<()> { + let mut set = SigSet::empty(); + set.add(sig); + if let Err(err) = sigprocmask(SigmaskHow::SIG_BLOCK, Some(&set), None) { + return Err(USimpleError::new( + 125, + translate!( + "env-error-failed-set-signal-action", + "signal" => (sig as i32), + "error" => err.desc() + ), + )); + } + Ok(()) +} + +#[cfg(unix)] +fn list_signal_handling(log: &SignalActionLog) { + for (&sig_value, record) in &log.records { + if !record.explicit { + continue; + } + let action = match record.kind { + SignalActionKind::Default => "DEFAULT", + SignalActionKind::Ignore => "IGNORE", + SignalActionKind::Block => "BLOCK", + }; + let signal_name = signal_name_by_value(sig_value).unwrap_or("?"); + eprintln!("{:<10} ({}): {}", signal_name, sig_value as i32, action); + } +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Rust ignores SIGPIPE (see https://github.com/rust-lang/rust/issues/62569). diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 5459c5d5468..bd0fdd26a87 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -186,6 +186,10 @@ macro_rules! bin { pub fn main() { use std::io::Write; use uucore::locale; + + // Preserve inherited SIGPIPE settings (e.g., from env --default-signal=PIPE) + uucore::panic::preserve_inherited_sigpipe(); + // suppress extraneous error output for SIGPIPE failures/panics uucore::panic::mute_sigpipe_panic(); locale::setup_localization(uucore::get_canonical_util_name(stringify!($util))) diff --git a/src/uucore/src/lib/mods/panic.rs b/src/uucore/src/lib/mods/panic.rs index 8c170b3c8cd..2a67a10ba8d 100644 --- a/src/uucore/src/lib/mods/panic.rs +++ b/src/uucore/src/lib/mods/panic.rs @@ -43,3 +43,30 @@ pub fn mute_sigpipe_panic() { } })); } + +/// Preserve inherited SIGPIPE settings from parent process. +/// +/// Rust unconditionally sets SIGPIPE to SIG_IGN on startup. This function +/// checks if the parent process (e.g., `env --default-signal=PIPE`) intended +/// for SIGPIPE to be set to default by checking the RUST_SIGPIPE environment +/// variable. If set to "default", it restores SIGPIPE to SIG_DFL. +#[cfg(unix)] +pub fn preserve_inherited_sigpipe() { + use nix::libc; + + // Check if parent specified that SIGPIPE should be default + if let Ok(val) = std::env::var("RUST_SIGPIPE") { + if val == "default" { + unsafe { + libc::signal(libc::SIGPIPE, libc::SIG_DFL); + // Remove the environment variable so child processes don't inherit it incorrectly + std::env::remove_var("RUST_SIGPIPE"); + } + } + } +} + +#[cfg(not(unix))] +pub fn preserve_inherited_sigpipe() { + // No-op on non-Unix platforms +} diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 68e7e03b506..a3f252accc6 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -2,9 +2,11 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (words) bamf chdir rlimit prlimit COMSPEC cout cerr FFFD winsize xpixel ypixel Secho +// spell-checker:ignore (words) bamf chdir rlimit prlimit COMSPEC cout cerr FFFD winsize xpixel ypixel Secho sighandler #![allow(clippy::missing_errors_doc)] +#[cfg(unix)] +use nix::libc; #[cfg(unix)] use nix::sys::signal::Signal; #[cfg(feature = "echo")] @@ -16,6 +18,8 @@ use std::process::Command; use tempfile::tempdir; use uutests::new_ucmd; #[cfg(unix)] +use uutests::util::PATH; +#[cfg(unix)] use uutests::util::TerminalSimulation; use uutests::util::TestScenario; #[cfg(unix)] @@ -29,13 +33,13 @@ struct Target { #[cfg(unix)] impl Target { fn new(signals: &[&str]) -> Self { - let mut child = new_ucmd!() - .args(&[ - format!("--ignore-signal={}", signals.join(",")).as_str(), - "sleep", - "1000", - ]) - .run_no_wait(); + let mut cmd = new_ucmd!(); + if signals.is_empty() { + cmd.arg("--ignore-signal"); + } else { + cmd.arg(format!("--ignore-signal={}", signals.join(","))); + } + let mut child = cmd.args(&["sleep", "1000"]).run_no_wait(); child.delay(500); Self { child } } @@ -936,6 +940,89 @@ fn test_env_arg_ignore_signal_empty() { .stdout_contains("hello"); } +#[test] +#[cfg(unix)] +fn test_env_arg_ignore_signal_all_signals() { + let mut target = Target::new(&[]); + target.send_signal(Signal::SIGINT); + assert!(target.is_alive()); +} + +#[test] +#[cfg(unix)] +fn test_env_default_signal_pipe() { + let ts = TestScenario::new(util_name!()); + run_sigpipe_script(&ts, &["--default-signal=PIPE"]); +} + +#[test] +#[cfg(unix)] +fn test_env_default_signal_all_signals() { + let ts = TestScenario::new(util_name!()); + run_sigpipe_script(&ts, &["--default-signal"]); +} + +#[test] +#[cfg(unix)] +fn test_env_block_signal_flag() { + new_ucmd!() + .env("PATH", PATH) + .args(&["--block-signal", "true"]) + .succeeds() + .no_stderr(); +} + +#[test] +#[cfg(unix)] +fn test_env_list_signal_handling_reports_ignore() { + let result = new_ucmd!() + .env("PATH", PATH) + .args(&["--ignore-signal=INT", "--list-signal-handling", "true"]) + .succeeds(); + let stderr = result.stderr_str(); + assert!( + stderr.contains("INT") && stderr.contains("IGNORE"), + "unexpected signal listing: {stderr}" + ); +} + +#[cfg(unix)] +fn run_sigpipe_script(ts: &TestScenario, extra_args: &[&str]) { + let shell = env::var("SHELL").unwrap_or_else(|_| String::from("sh")); + let _guard = SigpipeGuard::new(); + let mut cmd = ts.ucmd(); + cmd.env("PATH", PATH); + cmd.args(extra_args); + cmd.arg(shell); + cmd.arg("-c"); + cmd.arg("trap - PIPE; seq 999999 2>err | head -n1 > out"); + cmd.succeeds(); + assert_eq!(ts.fixtures.read("out"), "1\n"); + assert_eq!(ts.fixtures.read("err"), ""); +} + +#[cfg(unix)] +struct SigpipeGuard { + previous: libc::sighandler_t, +} + +#[cfg(unix)] +impl SigpipeGuard { + fn new() -> Self { + let previous = unsafe { libc::signal(libc::SIGPIPE, libc::SIG_IGN) }; + Self { previous } + } +} + +#[cfg(unix)] +impl Drop for SigpipeGuard { + fn drop(&mut self) { + unsafe { + libc::signal(libc::SIGPIPE, self.previous); + } + } +} + #[test] fn disallow_equals_sign_on_short_unset_option() { let ts = TestScenario::new(util_name!());