diff --git a/Cargo.lock b/Cargo.lock index 10b9a7553d5..3b14f2a7f50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3662,7 +3662,8 @@ version = "0.9.0" dependencies = [ "clap", "fluent", - "nix", + "libc", + "rustix", "uucore", ] diff --git a/src/uu/kill/Cargo.toml b/src/uu/kill/Cargo.toml index 9f36aba5074..5af87bc3ec6 100644 --- a/src/uu/kill/Cargo.toml +++ b/src/uu/kill/Cargo.toml @@ -25,7 +25,8 @@ uucore = { workspace = true, features = ["signals"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { workspace = true, features = ["signal"] } +libc = { workspace = true } +rustix = { workspace = true, features = ["process"] } [[bin]] name = "kill" diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index e92a47da3b4..74c67dac26e 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -3,19 +3,22 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) signalname pids killpg +// spell-checker:ignore (ToDO) signalname pids killpg NSIG use clap::{Arg, ArgAction, Command}; -use nix::sys::signal::{self, Signal}; -use nix::unistd::Pid; -use std::io::Error; +use rustix::process::{ + Pid, Signal, kill_current_process_group, kill_process, kill_process_group, + test_kill_current_process_group, test_kill_process, test_kill_process_group, +}; +use std::cmp::Ordering; +use std::io; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::translate; use uucore::signals::{ signal_by_name_or_value, signal_list_name_by_value, signal_list_value_by_name_or_number, - signal_name_by_value, signal_number_upper_bound, + signal_number_upper_bound, }; use uucore::{format_usage, show}; @@ -68,18 +71,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { 15_usize //SIGTERM }; - let sig_name = signal_name_by_value(sig); - // Signal does not support converting from EXIT - // Instead, nix::signal::kill expects Option::None to properly handle EXIT - let sig: Option = if sig_name.is_some_and(|name| name == "EXIT") { - None - } else { - let sig = (sig as i32) - .try_into() - .map_err(|e| Error::from_raw_os_error(e as i32))?; - Some(sig) - }; - let pids = parse_pids(&pids_or_signals)?; if pids.is_empty() { Err(USimpleError::new(1, translate!("kill-error-no-process-id"))) @@ -226,6 +217,55 @@ fn list(signals: &Vec) { } } +/// Convert a validated non-realtime signal number to a rustix [`Signal`]. +/// +/// # Safety (justification) +/// +/// The caller must guarantee `sig > 0`. In this module that invariant is +/// upheld by the control flow in [`kill()`], which routes `sig == 0` to the +/// `test_kill_*` functions before reaching this helper. +/// +/// Signal validity is ensured by [`signal_by_name_or_value`], which accepts: +/// named and numeric signals in the supported platform range. Realtime signals +/// are handled by [`raw_kill`] before this helper is called, because rustix +/// does not permit libc-reserved realtime [`Signal`] values to be used for +/// sending signals. +fn sig_from_usize(sig: usize) -> Signal { + debug_assert!( + sig > 0, + "signal 0 must be handled before calling this function" + ); + debug_assert!(!is_realtime_signal(sig)); + // SAFETY: See function-level safety comment above. + unsafe { Signal::from_raw_unchecked(sig as i32) } +} + +fn rustix_to_io(result: rustix::io::Result<()>) -> io::Result<()> { + result.map_err(io::Error::from) +} + +fn raw_kill(pid: i32, sig: usize) -> io::Result<()> { + let sig = i32::try_from(sig).map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?; + if unsafe { libc::kill(pid as libc::pid_t, sig) } == 0 { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +fn is_realtime_signal(sig: usize) -> bool { + let Ok(sig) = i32::try_from(sig) else { + return false; + }; + (libc::SIGRTMIN()..=libc::SIGRTMAX()).contains(&sig) +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +fn is_realtime_signal(_: usize) -> bool { + false +} + fn parse_signal_value(signal_name: &str) -> UResult { let optional_signal_value = signal_by_name_or_value(signal_name); match optional_signal_value { @@ -250,13 +290,43 @@ fn parse_pids(pids: &[String]) -> UResult> { .collect() } -fn kill(sig: Option, pids: &[i32]) { +fn kill(sig: usize, pids: &[i32]) { for &pid in pids { - if let Err(e) = signal::kill(Pid::from_raw(pid), sig) { - show!( - Error::from_raw_os_error(e as i32) - .map_err_context(|| { translate!("kill-error-sending-signal", "pid" => pid) }) - ); + let result = match pid.cmp(&0) { + Ordering::Equal if sig == 0 => rustix_to_io(test_kill_current_process_group()), + Ordering::Equal if is_realtime_signal(sig) => raw_kill(0, sig), + Ordering::Equal => rustix_to_io(kill_current_process_group(sig_from_usize(sig))), + Ordering::Greater => { + let pid = Pid::from_raw(pid).expect("pid > 0 guaranteed by Ordering::Greater"); + if sig == 0 { + rustix_to_io(test_kill_process(pid)) + } else if is_realtime_signal(sig) { + raw_kill(pid.as_raw_nonzero().get(), sig) + } else { + rustix_to_io(kill_process(pid, sig_from_usize(sig))) + } + } + Ordering::Less => { + let Some(abs_pid) = pid.checked_neg() else { + show!(USimpleError::new( + 1, + translate!("kill-error-sending-signal", "pid" => pid), + )); + continue; + }; + let pid = + Pid::from_raw(abs_pid).expect("abs_pid > 0 since pid < 0 and pid != i32::MIN"); + if sig == 0 { + rustix_to_io(test_kill_process_group(pid)) + } else if is_realtime_signal(sig) { + raw_kill(-pid.as_raw_nonzero().get(), sig) + } else { + rustix_to_io(kill_process_group(pid, sig_from_usize(sig))) + } + } + }; + if let Err(e) = result { + show!(e.map_err_context(|| translate!("kill-error-sending-signal", "pid" => pid))); } } } diff --git a/tests/by-util/test_kill.rs b/tests/by-util/test_kill.rs index c666db3714d..8fa8a2778b4 100644 --- a/tests/by-util/test_kill.rs +++ b/tests/by-util/test_kill.rs @@ -493,3 +493,49 @@ fn test_kill_signal_only_no_pid() { .fails() .stderr_contains("no process ID specified"); } + +#[test] +fn test_kill_signal_zero_process() { + let target = Target::new(); + // kill -0 should succeed for a running process (signal 0 = existence check) + new_ucmd!() + .arg("-0") + .arg(format!("{}", target.pid())) + .succeeds(); +} + +#[test] +fn test_kill_signal_zero_new_form() { + let target = Target::new(); + // kill -s 0 should also work + new_ucmd!() + .arg("-s") + .arg("0") + .arg(format!("{}", target.pid())) + .succeeds(); +} + +#[test] +fn test_kill_signal_zero_nonexistent() { + // kill -0 with a nonexistent PID should fail + new_ucmd!().arg("-0").arg("999999999").fails(); +} + +#[test] +fn test_kill_signal_zero_current_process_group() { + // kill -0 0 should succeed (checks current process group) + new_ucmd!().arg("-0").arg("0").succeeds(); +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_kill_realtime_signal() { + let mut target = Target::new(); + // kill -s RTMIN should send SIGRTMIN and terminate the process + new_ucmd!() + .arg("-s") + .arg("RTMIN") + .arg(format!("{}", target.pid())) + .succeeds(); + assert_eq!(target.wait_for_signal(), Some(libc::SIGRTMIN())); +}