diff --git a/src/uu/env/locales/en-US.ftl b/src/uu/env/locales/en-US.ftl index dd7cf2176b0..234eefa0d7c 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 = set handling of SIG signal(s) to the default +env-help-block-signal = block delivery of SIG signal(s) to COMMAND +env-help-list-signal-handling = list non default signal handling to stderr # 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/src/env.rs b/src/uu/env/src/env.rs index 72f5aa7923e..cf4c8d04fb9 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -17,10 +17,6 @@ use ini::Ini; use native_int_str::{ Convert, NCvt, NativeIntStr, NativeIntString, NativeStr, from_native_int_representation_owned, }; -#[cfg(unix)] -use nix::libc; -#[cfg(unix)] -use nix::sys::signal::{SigHandler::SigIgn, Signal, signal}; use std::borrow::Cow; use std::env; use std::ffi::{OsStr, OsString}; @@ -29,12 +25,15 @@ use std::io::{self, Write}; use std::os::unix::ffi::OsStrExt; #[cfg(unix)] use std::os::unix::process::CommandExt; +#[cfg(unix)] +use uucore::signals::{ + SIGKILL, SIGSTOP, SignalStatus, block_signals, get_signal_status, ignore_signal, reset_signal, + signal_by_name_or_value, signal_name_by_value, +}; 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::translate; use uucore::{format_usage, show_warning}; @@ -84,6 +83,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> { @@ -97,6 +99,12 @@ struct Options<'a> { argv0: Option<&'a OsStr>, #[cfg(unix)] ignore_signal: Vec, + #[cfg(unix)] + default_signal: Vec, + #[cfg(unix)] + block_signal: Vec, + #[cfg(unix)] + list_signal_handling: bool, } /// print `name=value` env pairs on screen @@ -155,11 +163,11 @@ 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(signal_vec: &mut Vec, opt: &OsStr) -> UResult<()> { if opt.is_empty() { return Ok(()); } - let signals: Vec<&'a OsStr> = opt + let signals: Vec<&OsStr> = opt .as_bytes() .split(|&b| b == b',') .map(OsStr::from_bytes) @@ -179,14 +187,35 @@ 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); + if !signal_vec.contains(&sig_val) { + signal_vec.push(sig_val); } } Ok(()) } +/// Parse signal option that can be empty (meaning all signals) +#[cfg(unix)] +fn parse_signal_opt_or_all(signal_vec: &mut Vec, opt: &OsStr) -> UResult<()> { + if opt.is_empty() { + // Empty means all signals - add all valid signal numbers (1-31 typically) + // Skip SIGKILL and SIGSTOP which cannot be caught or ignored + let sigkill = SIGKILL as usize; + let sigstop = SIGSTOP as usize; + for sig_val in 1..32 { + if sig_val == sigkill || sig_val == sigstop { + continue; // SIGKILL and SIGSTOP cannot be modified + } + if !signal_vec.contains(&sig_val) { + signal_vec.push(sig_val); + } + } + return Ok(()); + } + parse_signal_opt(signal_vec, opt) +} + 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 @@ -307,9 +336,40 @@ pub fn uu_app() -> Command { .long(options::IGNORE_SIGNAL) .value_name("SIG") .action(ArgAction::Append) + .num_args(0..=1) + .default_missing_value("") + .require_equals(true) .value_parser(ValueParser::os_string()) .help(translate!("env-help-ignore-signal")), ) + .arg( + Arg::new(options::DEFAULT_SIGNAL) + .long(options::DEFAULT_SIGNAL) + .value_name("SIG") + .action(ArgAction::Append) + .num_args(0..=1) + .default_missing_value("") + .require_equals(true) + .value_parser(ValueParser::os_string()) + .help(translate!("env-help-default-signal")), + ) + .arg( + Arg::new(options::BLOCK_SIGNAL) + .long(options::BLOCK_SIGNAL) + .value_name("SIG") + .action(ArgAction::Append) + .num_args(0..=1) + .default_missing_value("") + .require_equals(true) + .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> { @@ -543,9 +603,23 @@ impl EnvAppData { apply_specified_env_vars(&opts); + // Apply signal handling in the correct order: + // 1. Reset signals to default + // 2. Set signals to ignore + // 3. Block signals + // 4. List signal handling (if requested) + #[cfg(unix)] + apply_default_signal(&opts)?; + #[cfg(unix)] apply_ignore_signal(&opts)?; + #[cfg(unix)] + apply_block_signal(&opts)?; + + #[cfg(unix)] + list_signal_handling(&opts)?; + if opts.program.is_empty() { // no program provided, so just dump all env vars to stdout print_env(opts.line_ending); @@ -705,12 +779,32 @@ fn make_options(matches: &clap::ArgMatches) -> UResult> { argv0, #[cfg(unix)] ignore_signal: vec![], + #[cfg(unix)] + default_signal: vec![], + #[cfg(unix)] + block_signal: vec![], + #[cfg(unix)] + list_signal_handling: matches.get_flag(options::LIST_SIGNAL_HANDLING), }; #[cfg(unix)] - if let Some(iter) = matches.get_many::("ignore-signal") { + if let Some(iter) = matches.get_many::(options::IGNORE_SIGNAL) { + for opt in iter { + parse_signal_opt_or_all(&mut opts.ignore_signal, opt)?; + } + } + + #[cfg(unix)] + if let Some(iter) = matches.get_many::(options::DEFAULT_SIGNAL) { for opt in iter { - parse_signal_opt(&mut opts, opt)?; + parse_signal_opt_or_all(&mut opts.default_signal, opt)?; + } + } + + #[cfg(unix)] + if let Some(iter) = matches.get_many::(options::BLOCK_SIGNAL) { + for opt in iter { + parse_signal_opt_or_all(&mut opts.block_signal, opt)?; } } @@ -821,25 +915,58 @@ 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))?; + ignore_signal(sig_value).map_err(|err| { + USimpleError::new( + 125, + translate!("env-error-failed-set-signal-action", "signal" => sig_value, "error" => err.desc()), + ) + })?; + } + Ok(()) +} - ignore_signal(sig)?; +#[cfg(unix)] +fn apply_default_signal(opts: &Options<'_>) -> UResult<()> { + for &sig_value in &opts.default_signal { + reset_signal(sig_value).map_err(|err| { + USimpleError::new( + 125, + translate!("env-error-failed-set-signal-action", "signal" => sig_value, "error" => err.desc()), + ) + })?; } Ok(()) } #[cfg(unix)] -fn ignore_signal(sig: Signal) -> UResult<()> { - // SAFETY: This is safe because we write the handler for each signal only once, and therefore "the current handler is the default", as the documentation requires it. - let result = unsafe { signal(sig, SigIgn) }; - if let Err(err) = result { - return Err(USimpleError::new( - 125, - translate!("env-error-failed-set-signal-action", "signal" => (sig as i32), "error" => err.desc()), - )); +fn apply_block_signal(opts: &Options<'_>) -> UResult<()> { + block_signals(&opts.block_signal) + .map_err(|err| USimpleError::new(125, format!("failed to block signals: {}", err.desc()))) +} + +#[cfg(unix)] +fn list_signal_handling(opts: &Options<'_>) -> UResult<()> { + if !opts.list_signal_handling { + return Ok(()); } + + let stderr = io::stderr(); + let mut stderr = stderr.lock(); + + // Check each signal that was modified + for &sig_value in &opts.ignore_signal { + if let Some(name) = signal_name_by_value(sig_value) { + if let Ok(status) = get_signal_status(sig_value) { + let handler_str = match status { + SignalStatus::Ignore => "IGNORE", + SignalStatus::Default => "DEFAULT", + SignalStatus::Custom => "HANDLER", + }; + writeln!(stderr, "{name:<10} (): {handler_str}").ok(); + } + } + } + Ok(()) } @@ -848,9 +975,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Rust ignores SIGPIPE (see https://github.com/rust-lang/rust/issues/62569). // We restore its default action here. #[cfg(unix)] - unsafe { - libc::signal(libc::SIGPIPE, libc::SIG_DFL); - } + uucore::signals::enable_pipe_errors().ok(); EnvAppData::default().run_env(args) } diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs index 0bccb2173f6..07619cd52c8 100644 --- a/src/uucore/src/lib/features/signals.rs +++ b/src/uucore/src/lib/features/signals.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 (vars/api) fcntl setrlimit setitimer rubout pollable sysconf pgrp +// spell-checker:ignore (vars/api) fcntl setrlimit setitimer rubout pollable sysconf pgrp sigset sigprocmask sigaction Sigmask // spell-checker:ignore (vars/signals) ABRT ALRM CHLD SEGV SIGABRT SIGALRM SIGBUS SIGCHLD SIGCONT SIGDANGER SIGEMT SIGFPE SIGHUP SIGILL SIGINFO SIGINT SIGIO SIGIOT SIGKILL SIGMIGRATE SIGMSG SIGPIPE SIGPRE SIGPROF SIGPWR SIGQUIT SIGSEGV SIGSTOP SIGSYS SIGTALRM SIGTERM SIGTRAP SIGTSTP SIGTHR SIGTTIN SIGTTOU SIGURG SIGUSR SIGVIRT SIGVTALRM SIGWINCH SIGXCPU SIGXFSZ STKFLT PWR THR TSTP TTIN TTOU VIRT VTALRM XCPU XFSZ SIGCLD SIGPOLL SIGWAITING SIGAIOCANCEL SIGLWP SIGFREEZE SIGTHAW SIGCANCEL SIGLOST SIGXRES SIGJVM SIGRTMIN SIGRT SIGRTMAX TALRM AIOCANCEL XRES RTMIN RTMAX LTOSTOP //! This module provides a way to handle signals in a platform-independent way. @@ -13,10 +13,16 @@ #[cfg(unix)] use nix::errno::Errno; #[cfg(unix)] +use nix::libc; +#[cfg(unix)] use nix::sys::signal::{ - SigHandler::SigDfl, SigHandler::SigIgn, Signal::SIGINT, Signal::SIGPIPE, signal, + SigHandler::SigDfl, SigHandler::SigIgn, SigSet, SigmaskHow, Signal, Signal::SIGINT, + Signal::SIGPIPE, signal, sigprocmask, }; +#[cfg(unix)] +pub use nix::libc::{SIGKILL, SIGSTOP}; + /// The default signal value. pub static DEFAULT_SIGNAL: usize = 15; @@ -426,6 +432,65 @@ pub fn ignore_interrupts() -> Result<(), Errno> { unsafe { signal(SIGINT, SigIgn) }.map(|_| ()) } +/// Ignore a signal by its number. +#[cfg(unix)] +pub fn ignore_signal(sig: usize) -> Result<(), Errno> { + let sig = Signal::try_from(sig as i32).map_err(|_| Errno::EINVAL)?; + // SAFETY: Setting to SigIgn is safe - no custom handler. + unsafe { signal(sig, SigIgn) }.map(|_| ()) +} + +/// Reset a signal to its default handler. +#[cfg(unix)] +pub fn reset_signal(sig: usize) -> Result<(), Errno> { + let sig = Signal::try_from(sig as i32).map_err(|_| Errno::EINVAL)?; + // SAFETY: Setting to SigDfl is safe - no custom handler. + unsafe { signal(sig, SigDfl) }.map(|_| ()) +} + +/// Block delivery of a set of signals. +#[cfg(unix)] +pub fn block_signals(signals: &[usize]) -> Result<(), Errno> { + if signals.is_empty() { + return Ok(()); + } + let mut sigset = SigSet::empty(); + for &sig in signals { + let sig = Signal::try_from(sig as i32).map_err(|_| Errno::EINVAL)?; + sigset.add(sig); + } + sigprocmask(SigmaskHow::SIG_BLOCK, Some(&sigset), None) +} + +/// Status of a signal handler. +#[cfg(unix)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SignalStatus { + /// Default signal handler. + Default, + /// Signal is ignored. + Ignore, + /// Custom signal handler installed. + Custom, +} + +/// Query the current handler status for a signal. +#[cfg(unix)] +pub fn get_signal_status(sig: usize) -> Result { + let mut current = std::mem::MaybeUninit::::uninit(); + // SAFETY: We pass null for new action, so this only queries the current handler. + if unsafe { libc::sigaction(sig as i32, std::ptr::null(), current.as_mut_ptr()) } != 0 { + return Err(Errno::last()); + } + // SAFETY: sigaction succeeded, so current is initialized. + let handler = unsafe { current.assume_init() }.sa_sigaction; + Ok(match handler { + h if h == libc::SIG_IGN => SignalStatus::Ignore, + h if h == libc::SIG_DFL => SignalStatus::Default, + _ => SignalStatus::Custom, + }) +} + #[test] fn signal_by_value() { assert_eq!(signal_by_name_or_value("0"), Some(0)); diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 68e7e03b506..f77d5fd08b6 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -936,6 +936,106 @@ fn test_env_arg_ignore_signal_empty() { .stdout_contains("hello"); } +#[test] +#[cfg(unix)] +fn test_env_arg_default_signal_accepted() { + // Verify --default-signal is accepted and processes correctly + new_ucmd!() + .args(&["--default-signal=int", "echo", "hello"]) + .succeeds() + .stdout_contains("hello"); +} + +#[test] +#[cfg(unix)] +fn test_env_arg_default_signal_multiple() { + // Verify multiple signals can be specified + new_ucmd!() + .args(&["--default-signal=int,usr1", "echo", "hello"]) + .succeeds() + .stdout_contains("hello"); +} + +#[test] +#[cfg(unix)] +fn test_env_arg_default_signal_invalid() { + new_ucmd!() + .args(&["--default-signal=banana"]) + .fails() + .stderr_contains("invalid signal"); +} + +#[test] +#[cfg(unix)] +fn test_env_arg_default_signal_empty_resets_all() { + // Empty --default-signal= should be accepted (resets all signals) + new_ucmd!() + .args(&["--default-signal=", "echo", "hello"]) + .succeeds() + .stdout_contains("hello"); +} + +#[test] +#[cfg(unix)] +fn test_env_arg_block_signal_accepted() { + // Verify --block-signal is accepted + new_ucmd!() + .args(&["--block-signal=int", "echo", "hello"]) + .succeeds() + .stdout_contains("hello"); +} + +#[test] +#[cfg(unix)] +fn test_env_arg_block_signal_invalid() { + new_ucmd!() + .args(&["--block-signal=banana"]) + .fails() + .stderr_contains("invalid signal"); +} + +#[test] +#[cfg(unix)] +fn test_env_arg_block_signal_empty() { + // Empty --block-signal= should be accepted (blocks all signals) + new_ucmd!() + .args(&["--block-signal=", "echo", "hello"]) + .succeeds() + .stdout_contains("hello"); +} + +#[test] +#[cfg(unix)] +fn test_env_arg_list_signal_handling() { + // --list-signal-handling should output to stderr + new_ucmd!() + .args(&[ + "--ignore-signal=int", + "--list-signal-handling", + "echo", + "hello", + ]) + .succeeds() + .stdout_contains("hello") + .stderr_contains("INT"); +} + +#[test] +#[cfg(unix)] +fn test_env_arg_list_signal_handling_shows_ignore() { + // When signal is ignored, should show IGNORE + new_ucmd!() + .args(&[ + "--ignore-signal=usr1", + "--list-signal-handling", + "echo", + "hello", + ]) + .succeeds() + .stdout_contains("hello") + .stderr_contains("IGNORE"); +} + #[test] fn disallow_equals_sign_on_short_unset_option() { let ts = TestScenario::new(util_name!());