kill: replace nix::sys::signal with rustix::process (#12326)

This commit is contained in:
mattsu
2026-06-05 13:09:15 +02:00
committed by GitHub
parent abcdcdca53
commit 533edad992
4 changed files with 143 additions and 25 deletions
Generated
+2 -1
View File
@@ -3664,7 +3664,8 @@ version = "0.9.0"
dependencies = [
"clap",
"fluent",
"nix",
"libc",
"rustix",
"uucore",
]
+2 -1
View File
@@ -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"
+93 -23
View File
@@ -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<Signal> = 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<String>) {
}
}
/// 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<usize> {
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<Vec<i32>> {
.collect()
}
fn kill(sig: Option<Signal>, 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)));
}
}
}
+46
View File
@@ -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()));
}