diff --git a/Cargo.lock b/Cargo.lock index 6a52897506a..7c3bb6b0b05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3512,7 +3512,7 @@ version = "0.9.0" dependencies = [ "clap", "fluent", - "nix", + "libc", "rust-ini", "thiserror 2.0.18", "uucore", diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 8b2870c66f3..f587891347e 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -1995,7 +1995,7 @@ version = "0.9.0" dependencies = [ "clap", "fluent", - "nix", + "libc", "rust-ini", "thiserror", "uucore", diff --git a/src/uu/env/Cargo.toml b/src/uu/env/Cargo.toml index 46aa1be7c99..3181ab645c8 100644 --- a/src/uu/env/Cargo.toml +++ b/src/uu/env/Cargo.toml @@ -26,7 +26,7 @@ uucore = { workspace = true, features = ["signals"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["signal"] } +libc = { workspace = true } [[bin]] name = "env" diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 6c2ab71dddd..0385094f994 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 ptrs pub mod native_int_str; pub mod split_iterator; @@ -18,15 +18,6 @@ use native_int_str::{ Convert, NCvt, NativeIntStr, NativeIntString, NativeStr, from_native_int_representation, from_native_int_representation_owned, get_single_native_int_value, }; -#[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)] use std::collections::{BTreeMap, BTreeSet}; @@ -44,7 +35,9 @@ use uucore::display::{Quotable, print_all_env_vars}; use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError}; use uucore::line_ending::LineEnding; #[cfg(unix)] -use uucore::signals::{signal_by_name_or_value, signal_name_by_value, signal_number_upper_bound}; +use uucore::signals::{ + signal_by_name_or_value, signal_list_name_by_value, signal_number_upper_bound, +}; use uucore::translate; use uucore::{format_usage, show_warning}; @@ -293,19 +286,6 @@ fn build_signal_request( 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 @@ -786,13 +766,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, @@ -891,12 +871,21 @@ impl EnvAppData { argv.push(arg_cstring); } - // 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) => { + // Execute the program using execvp(3). This replaces the current + // process and only returns on error. Build the NULL-terminated argv + // array of pointers that libc expects. + let mut argv_ptrs: Vec<*const libc::c_char> = + argv.iter().map(|arg| arg.as_ptr()).collect(); + argv_ptrs.push(std::ptr::null()); + + // SAFETY: `prog_cstring` and every `argv` entry are valid NUL-terminated + // C strings that outlive the call, and `argv_ptrs` is NULL-terminated. + unsafe { libc::execvp(prog_cstring.as_ptr(), argv_ptrs.as_ptr()) }; + + // execvp only returns on failure; errno tells us why. + match io::Error::last_os_error().raw_os_error() { + Some(libc::ENOENT) => Err(self.make_error_no_such_file_or_dir(&prog)), + Some(libc::EACCES) => { uucore::show_error!( "{}", translate!( @@ -906,7 +895,7 @@ impl EnvAppData { ); Err(126.into()) } - Err(_) => { + _ => { uucore::show_error!( "{}", translate!( @@ -916,9 +905,6 @@ impl EnvAppData { ); Err(126.into()) } - Ok(_) => { - unreachable!("execvp should never return on success") - } } } @@ -1128,12 +1114,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) = uucore::signals::signal_from_raw(sig_value) else { return Ok(()); }; signal_fn(sig)?; @@ -1151,46 +1137,36 @@ where }) } +/// Set `sig`'s disposition to `SIG_IGN`/`SIG_DFL`, wrapping failures in env's +/// translated error. The disposition is applied via [`uucore::signals::set_disposition`], +/// which handles real-time signals too. #[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()), - )); - } - Ok(()) +fn set_signal_disposition(sig: i32, handler: libc::sighandler_t) -> UResult<()> { + uucore::signals::set_disposition(sig, handler).map_err(|_| signal_action_error(sig)) } #[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" => uucore::error::strip_errno(&io::Error::last_os_error()) + ), + ) } #[cfg(unix)] @@ -1204,7 +1180,9 @@ fn list_signal_handling(log: &SignalActionLog) { SignalActionKind::Ignore => "IGNORE", SignalActionKind::Block => "BLOCK", }; - let signal_name = signal_name_by_value(sig_value).unwrap_or("?"); + // Use the list-style lookup so real-time signals (SIGRTMIN..=SIGRTMAX) + // print as "RTMIN"/"RTMAX" rather than "?", matching GNU env. + let signal_name = signal_list_name_by_value(sig_value).unwrap_or_else(|| "?".to_string()); eprintln!("{signal_name:<10} ({}): {action}", sig_value as i32); } } diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 37a14830b4d..caa11c836c7 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", @@ -102,7 +108,6 @@ selinux = { workspace = true, optional = true } nix = { workspace = true, features = [ "dir", "fs", - "poll", "signal", "uio", "user", @@ -183,7 +188,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..e752e79d2ea 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,42 @@ pub trait ChildExt { ) -> io::Result>; } +/// Build a rustix [`Signal`] from a raw number, including real-time signals +/// (`SIGRTMIN..=SIGRTMAX`). Real-time signals are not "named", so +/// [`Signal::from_named_raw`] rejects them and we build them from the raw value. +/// +/// Validation (named signals plus the real-time range) is shared with `env` via +/// [`crate::signals::signal_from_raw`] when the `signals` feature is enabled — +/// which the signal-sending callers (`kill`, `timeout`) always do. The +/// `process`-only utilities (`id`, `whoami`, …) never send signals, so they fall +/// back to a named-signal-only converter rather than pull in the whole module. +#[cfg(feature = "signals")] +fn signal_from_value(signal: usize) -> io::Result { + let raw = crate::signals::signal_from_raw(signal) + .ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))?; + // SAFETY: `signal_from_raw` only returns named or real-time signal numbers, + // both of which are valid `Signal` values on this platform. + Ok(Signal::from_named_raw(raw).unwrap_or_else(|| unsafe { Signal::from_raw_unchecked(raw) })) +} + +#[cfg(not(feature = "signals"))] +fn signal_from_value(signal: usize) -> io::Result { + i32::try_from(signal) + .ok() + .filter(|&s| s > 0) + .and_then(Signal::from_named_raw) + .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 +138,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 0402c2ffa4a..74a0b0045c1 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,66 @@ pub fn signal_list_value_by_name_or_number(spec: &str) -> Option { }) } +/// Convert a raw signal number to a validated `c_int`, real-time signals included. +/// +/// Returns `None` for values that are not usable signals on this platform: zero +/// or negative numbers, out-of-range numbers, and platform gaps such as the +/// glibc-reserved 32/33 on Linux. Named signals are validated through rustix so +/// those gaps are rejected (matching GNU, which silently skips undefined +/// signals); real-time signals (`SIGRTMIN..=SIGRTMAX`) are not "named", so they +/// are accepted by range instead. +#[cfg(unix)] +pub fn signal_from_raw(signal: usize) -> Option { + let raw = i32::try_from(signal).ok().filter(|&s| s > 0)?; + if rustix::process::Signal::from_named_raw(raw).is_some() { + return Some(raw); + } + if let Some((rtmin, rtmax)) = realtime_signal_bounds() { + if (rtmin..=rtmax).contains(&(raw as usize)) { + return Some(raw); + } + } + None +} + +/// Set a signal's disposition to `SIG_DFL` or `SIG_IGN`. +/// +/// rustix deliberately does not wrap signal disposition (see its +/// `not_implemented::libc_internals`), leaving it to libc in a process that +/// links libc, so this goes straight to `libc::sigaction`. Using `sigaction` +/// with an empty mask (rather than `signal`) also lets callers target real-time +/// signals and gives consistent semantics across platforms. +#[cfg(unix)] +pub fn set_disposition(sig: libc::c_int, disposition: libc::sighandler_t) -> std::io::Result<()> { + // SAFETY: a zeroed sigaction with an empty mask and SIG_DFL/SIG_IGN is a + // valid disposition for any signal. + let mut action: libc::sigaction = unsafe { std::mem::zeroed() }; + action.sa_sigaction = disposition; + 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(()) +} + /// 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 +540,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 +584,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,43 +686,36 @@ 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; let out = stdout(); // First, check that stdout is a fifo and return true if it's not the case - let stat = fstat(&out)?; - if !SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFIFO) { + let stat = fstat(out.as_fd())?; + 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 ab9f188b92d..dfcc86216d3 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() } @@ -998,6 +1005,48 @@ 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(); + // The number, the IGNORE disposition, and the "RTMIN" name must all appear: + // env must print the real-time signal name like GNU, not "?". + assert!( + stderr.contains(&format!("({rtmin})")) + && stderr.contains("IGNORE") + && stderr.contains("RTMIN"), + "unexpected signal listing: {stderr}" + ); + assert!( + !stderr.contains('?'), + "real-time signal name should not be '?': {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!()