diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 3343544253e..a4e13ad62fe 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 Sigmask sigprocmask elidable +// spell-checker:ignore (ToDO) chdir progname subcommand subcommands unsets setenv putenv spawnp SIGSEGV SIGBUS sigaction Sigmask sigprocmask elidable sigset sigemptyset sigaddset sighandler pub mod native_int_str; pub mod split_iterator; @@ -20,11 +20,6 @@ use native_int_str::{ #[cfg(unix)] use nix::libc; #[cfg(unix)] -use nix::sys::signal::{ - SigHandler::{SigDfl, SigIgn}, - SigSet, SigmaskHow, Signal, signal, sigprocmask, -}; -#[cfg(unix)] use nix::unistd::execvp; use std::borrow::Cow; #[cfg(unix)] @@ -294,17 +289,21 @@ fn build_signal_request( Ok(request) } +/// Validate a signal number and return it as a raw `c_int`. +/// +/// Returns `None` for numbers that are not usable signals (e.g. the +/// glibc-reserved 32/33 on Linux). Real-time signals (`SIGRTMIN..=SIGRTMAX`) +/// are accepted directly since nix's `Signal` enum cannot represent them. #[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 signal_from_value(sig_value: usize) -> Option { + let sig = i32::try_from(sig_value).ok()?; + #[cfg(any(target_os = "linux", target_os = "android"))] + if sig >= libc::SIGRTMIN() && sig <= libc::SIGRTMAX() { + return Some(sig); + } + // For standard signals, let nix reject platform gaps (matches GNU, which + // silently skips undefined signals). + nix::sys::signal::Signal::try_from(sig).ok().map(|_| sig) } fn load_config_file(opts: &mut Options) -> UResult<()> { @@ -731,13 +730,13 @@ impl EnvAppData { &opts.default_signal, &mut signal_action_log, SignalActionKind::Default, - reset_signal, + |sig| set_signal_disposition(sig, libc::SIG_DFL), )?; apply_signal_action( &opts.ignore_signal, &mut signal_action_log, SignalActionKind::Ignore, - ignore_signal, + |sig| set_signal_disposition(sig, libc::SIG_IGN), )?; apply_signal_action( &opts.block_signal, @@ -1072,12 +1071,12 @@ fn apply_signal_action( signal_fn: F, ) -> UResult<()> where - F: Fn(Signal) -> UResult<()>, + F: Fn(i32) -> 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 { + let Some(sig) = signal_from_value(sig_value) else { return Ok(()); }; signal_fn(sig)?; @@ -1095,46 +1094,47 @@ where }) } +/// Set `sig`'s disposition to `SIG_IGN`/`SIG_DFL`. +/// +/// We use `libc::sigaction` directly rather than nix so that real-time signals +/// (whose numbers are not in nix's `Signal` enum) are handled too. rustix +/// deliberately does not wrap sigaction (see its not_implemented::libc_internals), +/// leaving signal disposition to libc, so libc is the intended tool here. #[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 set_signal_disposition(sig: i32, handler: libc::sighandler_t) -> UResult<()> { + // SAFETY: a zeroed sigaction with an empty mask and SIG_IGN/SIG_DFL is a valid + // disposition; we only set the handler for each signal once. + let mut action: libc::sigaction = unsafe { std::mem::zeroed() }; + action.sa_sigaction = handler; + unsafe { libc::sigemptyset(&raw mut action.sa_mask) }; + if unsafe { libc::sigaction(sig, &raw const action, std::ptr::null_mut()) } == -1 { + return Err(signal_action_error(sig)); } 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()), - )); +fn block_signal(sig: i32) -> UResult<()> { + // SAFETY: build a set containing only `sig` and add it to the process mask. + let mut set: libc::sigset_t = unsafe { std::mem::zeroed() }; + unsafe { libc::sigemptyset(&raw mut set) }; + unsafe { libc::sigaddset(&raw mut set, sig) }; + if unsafe { libc::sigprocmask(libc::SIG_BLOCK, &raw const set, std::ptr::null_mut()) } == -1 { + return Err(signal_action_error(sig)); } 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(()) +fn signal_action_error(sig: i32) -> Box { + USimpleError::new( + 125, + translate!( + "env-error-failed-set-signal-action", + "signal" => sig, + "error" => io::Error::last_os_error().to_string() + ), + ) } #[cfg(unix)] diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 37a14830b4d..3d1c1d5a1fd 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -36,7 +36,13 @@ jiff = { workspace = true, optional = true, features = [ "tzdb-concatenated", ] } rustc-hash = { workspace = true } -rustix = { workspace = true, features = ["fs", "net", "pipe", "process"] } +rustix = { workspace = true, features = [ + "event", + "fs", + "net", + "pipe", + "process", +] } time = { workspace = true, optional = true, features = [ "formatting", "local-offset", @@ -183,7 +189,7 @@ safe-copy = [] safe-traversal = ["libc"] selinux = ["dep:selinux"] smack = ["xattr"] -signals = [] +signals = ["libc"] sum = [ "digest", "hex", diff --git a/src/uucore/src/lib/features/process.rs b/src/uucore/src/lib/features/process.rs index 06274eb59f9..9d5a137e9a5 100644 --- a/src/uucore/src/lib/features/process.rs +++ b/src/uucore/src/lib/features/process.rs @@ -5,13 +5,13 @@ // spell-checker:ignore (vars) cvar exitstatus cmdline kworker getsid getpid // spell-checker:ignore (sys/unix) WIFSIGNALED ESRCH -// spell-checker:ignore pgrep pwait snice getpgrp +// spell-checker:ignore pgrep pwait snice getpgrp SRCH use libc::{gid_t, pid_t, uid_t}; -#[cfg(not(target_os = "redox"))] -use nix::errno::Errno; -use nix::sys::signal::{self as nix_signal, SigHandler, Signal}; -use nix::unistd::Pid; +use rustix::process::{ + Pid, Signal, kill_current_process_group, kill_process, test_kill_current_process_group, + test_kill_process, +}; use std::io; use std::process::Child; use std::process::ExitStatus; @@ -22,23 +22,22 @@ use std::time::{Duration, Instant}; /// `geteuid()` returns the effective user ID of the calling process. pub fn geteuid() -> uid_t { - nix::unistd::geteuid().as_raw() + rustix::process::geteuid().as_raw() } /// `getpgrp()` returns the process group ID of the calling process. -/// It is a trivial wrapper over nix::unistd::getpgrp. pub fn getpgrp() -> pid_t { - nix::unistd::getpgrp().as_raw() + rustix::process::getpgrp().as_raw_pid() } /// `getegid()` returns the effective group ID of the calling process. pub fn getegid() -> gid_t { - nix::unistd::getegid().as_raw() + rustix::process::getegid().as_raw() } /// `getgid()` returns the real group ID of the calling process. pub fn getgid() -> gid_t { - nix::unistd::getgid().as_raw() + rustix::process::getgid().as_raw() } /// `getuid()` returns the real user ID of the calling process. @@ -48,7 +47,7 @@ pub fn getuid() -> uid_t { /// `getpid()` returns the pid of the calling process. pub fn getpid() -> pid_t { - nix::unistd::getpid().as_raw() + rustix::process::getpid().as_raw_pid() } /// `getsid()` returns the session ID of the process with process ID pid. @@ -57,8 +56,8 @@ pub fn getpid() -> pid_t { /// /// # Error /// -/// - [Errno::EPERM] A process with process ID pid exists, but it is not in the same session as the calling process, and the implementation considers this an error. -/// - [Errno::ESRCH] No process with process ID pid was found. +/// - `EPERM` A process with process ID pid exists, but it is not in the same session as the calling process, and the implementation considers this an error. +/// - `ESRCH` No process with process ID pid was found. /// /// /// # Platform @@ -66,13 +65,12 @@ pub fn getpid() -> pid_t { /// This function only support standard POSIX implementation platform, /// so some system such as redox doesn't supported. #[cfg(not(target_os = "redox"))] -pub fn getsid(pid: i32) -> Result { - let pid = if pid == 0 { - None - } else { - Some(Pid::from_raw(pid)) +pub fn getsid(pid: i32) -> Result { + let pid = match pid { + 0 => None, + _ => Some(Pid::from_raw(pid).ok_or(rustix::io::Errno::SRCH)?), }; - nix::unistd::getsid(pid).map(Pid::as_raw) + rustix::process::getsid(pid).map(Pid::as_raw_pid) } /// Missing methods for Child objects @@ -95,17 +93,34 @@ pub trait ChildExt { ) -> io::Result>; } +/// Build a rustix [`Signal`] from a raw number, including real-time signals +/// (`SIGRTMIN..=SIGRTMAX`). Those are not "named" signals, so +/// [`Signal::from_named_raw`] rejects them; build them from the raw value. +fn signal_from_value(signal: usize) -> io::Result { + let raw = i32::try_from(signal).ok().filter(|&s| s > 0); + raw.and_then(|raw| { + if let Some(sig) = Signal::from_named_raw(raw) { + return Some(sig); + } + #[cfg(any(target_os = "linux", target_os = "android"))] + if (libc::SIGRTMIN()..=libc::SIGRTMAX()).contains(&raw) { + // SAFETY: `raw` is within the real-time signal range. + return Some(unsafe { Signal::from_raw_unchecked(raw) }); + } + None + }) + .ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL)) +} + impl ChildExt for Child { fn send_signal(&mut self, signal: usize) -> io::Result<()> { - let pid = Pid::from_raw(self.id() as pid_t); - let result = if signal == 0 { - nix_signal::kill(pid, None) - } else { - let signal = Signal::try_from(signal as i32) - .map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?; - nix_signal::kill(pid, Some(signal)) - }; - result.map_err(|e| io::Error::from_raw_os_error(e as i32)) + let pid = Pid::from_raw(self.id() as pid_t) + .ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))?; + // signal == 0 only probes whether the pid is still alive. + if signal == 0 { + return test_kill_process(pid).map_err(io::Error::from); + } + kill_process(pid, signal_from_value(signal)?).map_err(io::Error::from) } fn send_signal_group(&mut self, signal: usize) -> io::Result<()> { @@ -115,23 +130,31 @@ impl ChildExt for Child { // in the group. If the child has created its own process group (via setpgid), // it won't receive this group signal, but will have received the direct signal. - // Signal 0 is special - it just checks if process exists, doesn't send anything. + // Signal 0 is special - it just checks if the group exists, doesn't send anything. // No need to manipulate signal handlers for it. if signal == 0 { - return nix_signal::kill(Pid::from_raw(0), None) - .map_err(|e| io::Error::from_raw_os_error(e as i32)); + return test_kill_current_process_group().map_err(io::Error::from); } - let signal = Signal::try_from(signal as i32) - .map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?; - - // Ignore the signal temporarily so we don't receive it ourselves. - let old_handler = unsafe { nix_signal::signal(signal, SigHandler::SigIgn) } - .map_err(|e| io::Error::from_raw_os_error(e as i32))?; - let result = nix_signal::kill(Pid::from_raw(0), Some(signal)); - // Restore the old handler - let _ = unsafe { nix_signal::signal(signal, old_handler) }; - result.map_err(|e| io::Error::from_raw_os_error(e as i32)) + let sig = signal_from_value(signal)?; + let sig_raw = sig.as_raw(); + + // Ignore the signal temporarily so we don't receive it ourselves. rustix + // deliberately does not wrap sigaction (see its not_implemented::libc_internals); + // its only equivalent is the experimental `runtime` module, which is UB in a + // process that links libc. Signal disposition is left to libc, so use it here. + // SAFETY: a zeroed sigaction with SIG_IGN is a valid disposition; we restore the + // previous one right after sending to our own process group. + let mut ignore: libc::sigaction = unsafe { std::mem::zeroed() }; + ignore.sa_sigaction = libc::SIG_IGN; + let mut old: libc::sigaction = unsafe { std::mem::zeroed() }; + if unsafe { libc::sigaction(sig_raw, &raw const ignore, &raw mut old) } == -1 { + return Err(io::Error::last_os_error()); + } + let res = kill_current_process_group(sig); + // Restore the previous disposition. + unsafe { libc::sigaction(sig_raw, &raw const old, std::ptr::null_mut()) }; + res.map_err(io::Error::from) } fn wait_or_timeout( diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs index 0d423a6e5b1..064eddd0b77 100644 --- a/src/uucore/src/lib/features/signals.rs +++ b/src/uucore/src/lib/features/signals.rs @@ -3,23 +3,13 @@ // 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 GETFD pfds revents POLLRDBAND POLLERR +// spell-checker:ignore (vars/api) fcntl setrlimit setitimer rubout pollable sysconf pgrp GETFD pfds revents POLLRDBAND POLLERR RDBAND sigemptyset sighandler sigaction // 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. //! It provides a way to convert signal names to their corresponding values and vice versa. //! It also provides a way to ignore the SIGINT signal and enable pipe errors. -#[cfg(unix)] -use nix::errno::Errno; -#[cfg(any(target_os = "linux", target_os = "android"))] -use nix::libc; -#[cfg(unix)] -use nix::sys::signal::{ - SaFlags, SigAction, SigHandler, SigHandler::SigDfl, SigHandler::SigIgn, SigSet, Signal, - Signal::SIGINT, Signal::SIGPIPE, sigaction, signal, -}; - /// The default signal value. pub static DEFAULT_SIGNAL: usize = 15; @@ -483,29 +473,36 @@ pub fn signal_list_value_by_name_or_number(spec: &str) -> Option { }) } +/// Set a signal's disposition to `SIG_DFL` or `SIG_IGN`. rustix does not wrap +/// signal disposition (it leaves that to libc in a process that links libc), so +/// this goes straight to `libc::signal`. +#[cfg(unix)] +fn set_disposition(sig: libc::c_int, disposition: libc::sighandler_t) -> std::io::Result<()> { + // SAFETY: SIG_DFL/SIG_IGN are always valid dispositions for any signal. + if unsafe { libc::signal(sig, disposition) } == libc::SIG_ERR { + return Err(std::io::Error::last_os_error()); + } + Ok(()) +} + /// Restores SIGPIPE to default behavior (process terminates on broken pipe). #[cfg(unix)] -pub fn enable_pipe_errors() -> Result<(), Errno> { - // We pass the error as is, the return value would just be Ok(SigDfl), so we can safely ignore it. - // SAFETY: this function is safe as long as we do not use a custom SigHandler -- we use the default one. - unsafe { signal(SIGPIPE, SigDfl) }.map(|_| ()) +pub fn enable_pipe_errors() -> std::io::Result<()> { + set_disposition(libc::SIGPIPE, libc::SIG_DFL) } /// Ignores SIGPIPE signal (broken pipe errors are returned instead of terminating). /// Use this to override the default SIGPIPE handling when you need to handle /// broken pipe errors gracefully (e.g., tee with --output-error). #[cfg(unix)] -pub fn disable_pipe_errors() -> Result<(), Errno> { - // SAFETY: this function is safe as long as we do not use a custom SigHandler -- we use the default one. - unsafe { signal(SIGPIPE, SigIgn) }.map(|_| ()) +pub fn disable_pipe_errors() -> std::io::Result<()> { + set_disposition(libc::SIGPIPE, libc::SIG_IGN) } /// Ignores the SIGINT signal. #[cfg(unix)] -pub fn ignore_interrupts() -> Result<(), Errno> { - // We pass the error as is, the return value would just be Ok(SigIgn), so we can safely ignore it. - // SAFETY: this function is safe as long as we do not use a custom SigHandler -- we use the default one. - unsafe { signal(SIGINT, SigIgn) }.map(|_| ()) +pub fn ignore_interrupts() -> std::io::Result<()> { + set_disposition(libc::SIGINT, libc::SIG_IGN) } /// Installs a signal handler. The handler must be async-signal-safe. @@ -513,14 +510,22 @@ pub fn ignore_interrupts() -> Result<(), Errno> { pub fn install_signal_handler( sig: i32, handler: extern "C" fn(std::os::raw::c_int), -) -> Result<(), Errno> { - let signal = Signal::try_from(sig).map_err(|_| Errno::EINVAL)?; - let action = SigAction::new( - SigHandler::Handler(handler), - SaFlags::SA_RESTART, - SigSet::empty(), - ); - unsafe { sigaction(signal, &action) }?; +) -> std::io::Result<()> { + // Build a sigaction with SA_RESTART and an empty mask, then install it via libc + // directly. We go straight to libc (not rustix's `Signal`) so that real-time + // signals (SIGRTMIN..=SIGRTMAX) can be handled as well. rustix is used for the + // kills, but it deliberately does not wrap sigaction (see its + // not_implemented::libc_internals): signal disposition is left to libc, since + // libc expects to own signal handling in a process that links it. + // SAFETY: the sigaction is fully initialized below; the handler is async-signal-safe + // per this function's contract. + let mut action: libc::sigaction = unsafe { std::mem::zeroed() }; + action.sa_sigaction = handler as libc::sighandler_t; + action.sa_flags = libc::SA_RESTART; + unsafe { libc::sigemptyset(&raw mut action.sa_mask) }; + if unsafe { libc::sigaction(sig, &raw const action, std::ptr::null_mut()) } == -1 { + return Err(std::io::Error::last_os_error()); + } Ok(()) } @@ -549,7 +554,6 @@ static STARTUP_STATE_WAS_CAPTURED: AtomicBool = AtomicBool::new(false); #[cfg(unix)] #[allow(clippy::missing_safety_doc)] pub unsafe extern "C" fn capture_startup_state() { - use nix::libc; use std::mem::MaybeUninit; use std::ptr; @@ -652,10 +656,8 @@ pub const fn sigpipe_was_ignored() -> bool { #[cfg(target_os = "linux")] pub fn ensure_stdout_not_broken() -> std::io::Result { - use nix::{ - poll::{PollFd, PollFlags, PollTimeout, poll}, - sys::stat::{SFlag, fstat}, - }; + use rustix::event::{PollFd, PollFlags, Timespec, poll}; + use rustix::fs::{FileType, fstat}; use std::io::stdout; use std::os::fd::AsFd; @@ -663,32 +665,27 @@ pub fn ensure_stdout_not_broken() -> std::io::Result { // First, check that stdout is a fifo and return true if it's not the case let stat = fstat(out.as_fd())?; - if !SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFIFO) { + if FileType::from_raw_mode(stat.st_mode) != FileType::Fifo { return Ok(true); } // POLLRDBAND is the flag used by GNU tee. - let mut pfds = [PollFd::new(out.as_fd(), PollFlags::POLLRDBAND)]; + let mut pfds = [PollFd::new(&out, PollFlags::RDBAND)]; // Then, ensure that the pipe is not broken. - // Use ZERO timeout to return immediately - we just want to check the current state. - let res = poll(&mut pfds, PollTimeout::ZERO)?; + // Use a zero timeout to return immediately - we just want to check the current state. + let res = poll(&mut pfds, Some(&Timespec::default()))?; if res > 0 { // poll returned with events ready - check if POLLERR is set (pipe broken) - let error = pfds.iter().any(|pfd| { - if let Some(revents) = pfd.revents() { - revents.contains(PollFlags::POLLERR) - } else { - true - } - }); + let error = pfds + .iter() + .any(|pfd| pfd.revents().contains(PollFlags::ERR)); return Ok(!error); } - // res == 0 means no events ready (timeout reached immediately with ZERO timeout). - // This means the pipe is healthy (not broken). - // res < 0 would be an error, but nix returns Err in that case. + // res == 0 means no events ready (zero timeout reached immediately). + // This means the pipe is healthy (not broken). An error returns Err above. Ok(true) } diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 1a1d504a13a..bf02cc43fc3 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -58,6 +58,13 @@ impl Target { #[cfg(not(target_os = "macos"))] self.child.delay(100); } + #[cfg(any(target_os = "linux", target_os = "android"))] + fn send_raw_signal(&mut self, signal: i32) { + // nix's `Signal` enum cannot represent real-time signals, so send the raw number. + // SAFETY: kill(2) with the child's pid and a valid signal number. + unsafe { libc::kill(self.child.id() as libc::pid_t, signal) }; + self.child.delay(100); + } fn is_alive(&mut self) -> bool { self.child.is_alive() } @@ -902,6 +909,40 @@ fn test_env_arg_ignore_signal_valid_signals() { } } +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_env_ignore_signal_realtime() { + // Real-time signals (SIGRTMIN..=SIGRTMAX) are not in nix's Signal enum; make sure + // env still applies the action to them. Regression for env-signal-handler.sh. + let rtmin = libc::SIGRTMIN(); + { + let mut target = Target::new(&["RTMIN"]); + target.send_raw_signal(rtmin); + assert!(target.is_alive(), "env should ignore SIGRTMIN"); + } + { + // Control: a signal env does not ignore still terminates by default. + let mut target = Target::new(&["int"]); + target.send_raw_signal(rtmin); + assert!(!target.is_alive(), "SIGRTMIN should terminate by default"); + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_env_list_signal_handling_realtime() { + let rtmin = libc::SIGRTMIN(); + let result = new_ucmd!() + .env("PATH", PATH) + .args(&["--ignore-signal=RTMIN", "--list-signal-handling", "true"]) + .succeeds(); + let stderr = result.stderr_str(); + assert!( + stderr.contains(&format!("({rtmin})")) && stderr.contains("IGNORE"), + "unexpected signal listing: {stderr}" + ); +} + #[test] #[cfg(unix)] fn test_env_arg_ignore_signal_empty() { diff --git a/tests/by-util/test_timeout.rs b/tests/by-util/test_timeout.rs index 5e24fdb1095..3c7138d190e 100644 --- a/tests/by-util/test_timeout.rs +++ b/tests/by-util/test_timeout.rs @@ -63,6 +63,18 @@ fn test_verbose() { } } +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_signal_realtime() { + // A real-time signal must be forwarded to the child so it terminates on its own; + // otherwise timeout falls back to SIGKILL (exit 137 + a KILL line). Regression for + // tests/env/env-signal-handler.sh. + new_ucmd!() + .args(&["--verbose", "-k.1", "--signal=RTMIN", ".1", "sleep", "10"]) + .fails_with_code(124) + .stderr_only("timeout: sending signal RTMIN to command 'sleep'\n"); +} + #[test] fn test_zero_timeout() { new_ucmd!()