Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ feat_os_unix_musl = [
"feat_require_unix_utmpx",
]
# "feat_os_windows" == set of utilities which can be built/run on modern windows platforms
feat_os_windows = ["feat_Tier1", "stdbuf", "timeout"]
feat_os_windows = ["feat_Tier1", "kill", "stdbuf", "timeout"]
## (secondary platforms) feature sets
# "feat_os_unix_gnueabihf" == set of utilities which can be built/run on the "arm-unknown-linux-gnueabihf" target (ARMv6 Linux [hardfloat])
feat_os_unix_gnueabihf = [
Expand Down
2 changes: 1 addition & 1 deletion src/uu/kill/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ doctest = false
[dependencies]
clap = { workspace = true }
thiserror = { workspace = true }
uucore = { workspace = true, features = ["signals"] }
uucore = { workspace = true, features = ["process", "signals"] }
fluent = { workspace = true }

[target.'cfg(unix)'.dependencies]
Expand Down
20 changes: 20 additions & 0 deletions src/uu/kill/locales/en-US.ftl
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
kill-about = Send signal to processes or list information about signals.
kill-usage = kill [OPTIONS]... PID...
kill-after-help-windows = Windows notes:
Signalled processes are force-terminated (Windows has no signal delivery);
their exit status is 128 plus the signal number. Negative PIDs (another
process's group) and STOP are not supported. Permissions come from your
current token, with SeDebugPrivilege enabled when it is held, so run
elevated to reach processes a standard token cannot signal. Protected
(anti-malware) processes cannot be terminated at all.

PID 0 targets the Job object kill runs in, the closest Windows analog of a
process group. Every process in that job and in its child jobs is signalled,
kill itself last, so kill dies with the group. Outside a job, PID 0 signals
only kill itself.

Beware: a Job object is usually not yours. Terminals, IDEs, Docker, CI
agents and Windows' own Program Compatibility Assistant all run what they
launch inside a job, and a job captures every descendant from creation
onward. Under a CI agent, kill 0 signals the agent and every sibling step.
The blast radius can be far wider than a POSIX process group.

# Help messages
kill-help-list = Lists signals
Expand All @@ -13,3 +31,5 @@ kill-error-invalid-signal = { $signal }: invalid signal
kill-error-parse-argument = failed to parse argument { $argument }: { $error }
kill-error-sending-signal = sending signal to { $pid } failed
kill-error-write = write error: { $error }
kill-error-unsupported-signal = unsupported signal on Windows
kill-error-negative-pid-unsupported = a negative PID (another process's group) is not supported on Windows
23 changes: 23 additions & 0 deletions src/uu/kill/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
kill-about = Envoyer un signal aux processus ou lister les informations sur les signaux.
kill-usage = kill [OPTIONS]... PID...
kill-after-help-windows = Notes pour Windows :
Les processus signalés sont terminés de force (Windows ne délivre pas de
signaux) ; leur code de sortie est 128 plus le numéro du signal. Les PID
négatifs (le groupe d'un autre processus) et STOP ne sont pas pris en charge.
Les permissions proviennent de votre jeton actuel, avec SeDebugPrivilege
activé lorsqu'il est détenu ; exécutez kill en tant qu'administrateur pour
atteindre les processus qu'un jeton standard ne peut pas signaler. Les
processus protégés (anti-programmes malveillants) ne peuvent jamais être
terminés.

Le PID 0 cible l'objet Job dans lequel kill s'exécute, l'équivalent Windows
le plus proche d'un groupe de processus. Tous les processus de ce Job et de
ses Jobs enfants sont signalés, kill lui-même en dernier, de sorte que kill
meurt avec le groupe. Hors d'un Job, le PID 0 ne signale que kill lui-même.

Attention : un objet Job ne vous appartient généralement pas. Les terminaux,
les IDE, Docker, les agents d'intégration continue et l'Assistant de
compatibilité des programmes de Windows exécutent tous dans un Job ce qu'ils
lancent, et un Job capture chaque descendant dès sa création. Sous un agent
d'intégration continue, kill 0 signale l'agent et toutes les étapes voisines.
La portée peut être bien plus large que celle d'un groupe de processus POSIX.

# Messages d'aide
kill-help-list = Liste les signaux
Expand All @@ -13,3 +34,5 @@ kill-error-invalid-signal = { $signal } : signal invalide
kill-error-parse-argument = échec de l'analyse de l'argument { $argument } : { $error }
kill-error-sending-signal = échec de l'envoi du signal au processus { $pid }
kill-error-write = erreur d'écriture : { $error }
kill-error-unsupported-signal = signal non pris en charge sur Windows
kill-error-negative-pid-unsupported = un PID négatif (le groupe d'un autre processus) n'est pas pris en charge sur Windows
64 changes: 8 additions & 56 deletions src/uu/kill/src/kill.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,6 @@
// spell-checker:ignore (ToDO) signalname pids killpg NOPESIG

use clap::{Arg, ArgAction, Command};
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::{self, BufWriter, Write};
use thiserror::Error;
use uucore::display::Quotable;
Expand All @@ -23,6 +18,8 @@ use uucore::signals::{
};
use uucore::{format_usage, show};

mod platform;

// When the -l option is selected, the program displays the type of signal related to a certain
// value or string. In case of a value, the program should control the lower 8 bits, but there is
// a particular case in which if the value is in range [128, 159], it is translated to a signal
Expand Down Expand Up @@ -95,7 +92,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
}

pub fn uu_app() -> Command {
Command::new("kill")
let cmd = Command::new("kill")
.version(uucore::crate_version!())
.help_template(uucore::localized_help_template("kill"))
.about(translate!("kill-about"))
Expand Down Expand Up @@ -131,7 +128,10 @@ pub fn uu_app() -> Command {
Arg::new(options::PIDS_OR_SIGNALS)
.hide(true)
.action(ArgAction::Append),
)
);
#[cfg(windows)]
let cmd = cmd.after_help(translate!("kill-after-help-windows"));
cmd
}

fn handle_obsolete(args: &mut Vec<String>) -> UResult<Option<usize>> {
Expand Down Expand Up @@ -244,18 +244,6 @@ fn list(signals: &Vec<String>) -> UResult<()> {
Ok(())
}

// rustix's `Signal` rejects libc-reserved realtime signals, so fall back to a
// raw `libc::kill` for any value its safe constructor doesn't recognize.
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))?;
// SAFETY: plain FFI call; `kill` has no memory-safety preconditions.
if unsafe { libc::kill(pid as libc::pid_t, sig) } == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}

fn parse_signal_value(signal_name: &str) -> UResult<usize> {
let optional_signal_value = signal_by_name_or_value(signal_name);
match optional_signal_value {
Expand All @@ -281,44 +269,8 @@ fn parse_pids(pids: &[String]) -> UResult<Vec<i32>> {
}

fn kill(sig: usize, pids: &[i32]) {
// Standard named signals use rustix's typed API; anything its safe
// constructor doesn't recognize (realtime/reserved) falls back to libc.
let named = (sig != 0)
.then(|| i32::try_from(sig).ok().and_then(Signal::from_named_raw))
.flatten();
for &pid in pids {
let result = match pid.cmp(&0) {
Ordering::Equal => match named {
_ if sig == 0 => test_kill_current_process_group().map_err(io::Error::from),
Some(s) => kill_current_process_group(s).map_err(io::Error::from),
None => raw_kill(0, sig),
},
Ordering::Greater => {
let pid = Pid::from_raw(pid).expect("pid > 0 guaranteed by Ordering::Greater");
match named {
_ if sig == 0 => test_kill_process(pid).map_err(io::Error::from),
Some(s) => kill_process(pid, s).map_err(io::Error::from),
None => raw_kill(pid.as_raw_nonzero().get(), 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");
match named {
_ if sig == 0 => test_kill_process_group(pid).map_err(io::Error::from),
Some(s) => kill_process_group(pid, s).map_err(io::Error::from),
None => raw_kill(-pid.as_raw_nonzero().get(), sig),
}
}
};
if let Err(e) = result {
if let Err(e) = platform::send_signal(pid, sig) {
show!(e.map_err_context(|| translate!("kill-error-sending-signal", "pid" => pid)));
}
}
Expand Down
18 changes: 18 additions & 0 deletions src/uu/kill/src/platform/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

//! Platform-specific piece of `kill`: delivering one signal to one pid via
//! the [`send_signal`] facade, provided by both submodules with an identical
//! signature. The shared control flow stays in `kill.rs`.

#[cfg(unix)]
mod unix;
#[cfg(unix)]
pub(crate) use unix::*;

#[cfg(windows)]
mod windows;
#[cfg(windows)]
pub(crate) use windows::*;
64 changes: 64 additions & 0 deletions src/uu/kill/src/platform/unix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

// spell-checker:ignore pids ESRCH

use std::cmp::Ordering;
use std::io;

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,
};

// rustix's `Signal` rejects libc-reserved realtime signals, so fall back to a
// raw `libc::kill` for any value its safe constructor doesn't recognize.
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))?;
// SAFETY: plain FFI call; `kill` has no memory-safety preconditions.
if unsafe { libc::kill(pid as libc::pid_t, sig) } == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}

/// Deliver `sig` to `pid` with kill(2) semantics: positive pids target one
/// process, 0 the current process group, negative pids the group `-pid`.
pub(crate) fn send_signal(pid: i32, sig: usize) -> io::Result<()> {
// Standard named signals use rustix's typed API; anything its safe
// constructor doesn't recognize (realtime/reserved) falls back to libc.
let named = (sig != 0)
.then(|| i32::try_from(sig).ok().and_then(Signal::from_named_raw))
.flatten();
match pid.cmp(&0) {
Ordering::Equal => match named {
_ if sig == 0 => test_kill_current_process_group().map_err(io::Error::from),
Some(s) => kill_current_process_group(s).map_err(io::Error::from),
None => raw_kill(0, sig),
},
Ordering::Greater => {
let pid = Pid::from_raw(pid).expect("pid > 0 guaranteed by Ordering::Greater");
match named {
_ if sig == 0 => test_kill_process(pid).map_err(io::Error::from),
Some(s) => kill_process(pid, s).map_err(io::Error::from),
None => raw_kill(pid.as_raw_nonzero().get(), sig),
}
}
Ordering::Less => {
// i32::MIN cannot be negated, so no such process group can exist.
let Some(abs_pid) = pid.checked_neg() else {
return Err(io::Error::from_raw_os_error(libc::ESRCH));
};
let pid =
Pid::from_raw(abs_pid).expect("abs_pid > 0 since pid < 0 and pid != i32::MIN");
match named {
_ if sig == 0 => test_kill_process_group(pid).map_err(io::Error::from),
Some(s) => kill_process_group(pid, s).map_err(io::Error::from),
None => raw_kill(-pid.as_raw_nonzero().get(), sig),
}
}
}
}
67 changes: 67 additions & 0 deletions src/uu/kill/src/platform/windows.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

// spell-checker:ignore pids

//! Windows implementation of `kill`'s platform facade, built on the signal
//! emulation in [`uucore::process`]. PID 0 targets the Job object `kill` runs
//! in, the closest Windows analog of a process group. STOP (no process-suspend
//! API) and negative pids (no way to name another process's group) are
//! rejected.

use std::io;

use uucore::process::{enable_debug_privilege, send_signal_to_own_group, send_signal_to_pid};
use uucore::translate;

const SIGNAL_STOP: usize = 19;

fn unsupported(message: String) -> io::Error {
io::Error::new(io::ErrorKind::Unsupported, message)
}

pub(crate) fn send_signal(pid: i32, sig: usize) -> io::Result<()> {
if sig == SIGNAL_STOP {
return Err(unsupported(translate!("kill-error-unsupported-signal")));
}
match u32::try_from(pid) {
// Fails exactly for pid < 0.
Err(_) => Err(unsupported(translate!(
"kill-error-negative-pid-unsupported"
))),
Ok(pid) => {
// Group members need the same rights as a single target.
enable_debug_privilege();
if pid == 0 {
send_signal_to_own_group(sig)
} else {
send_signal_to_pid(pid, sig)
}
}
}
}

#[cfg(test)]
mod tests {
use super::send_signal;

#[test]
fn negative_pids_are_rejected_without_touching_any_process() {
for pid in [-1, -2, i32::MIN] {
assert_eq!(
send_signal(pid, 9).unwrap_err().kind(),
std::io::ErrorKind::Unsupported
);
}
}

#[test]
fn stop_is_rejected_for_every_pid_including_zero() {
assert_eq!(
send_signal(0, 19).unwrap_err().kind(),
std::io::ErrorKind::Unsupported
);
}
}
1 change: 1 addition & 0 deletions src/uucore/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ help-flag-version = Print version information
error-io = I/O error
error-permission-denied = Permission denied
error-file-not-found = No such file or directory
error-no-such-process = No such process
error-invalid-argument = Invalid argument
error-is-a-directory = { $file }: Is a directory

Expand Down
1 change: 1 addition & 0 deletions src/uucore/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ help-flag-version = Afficher les informations de version
error-io = Erreur E/S
error-permission-denied = Permission refusée
error-file-not-found = Aucun fichier ou répertoire de ce type
error-no-such-process = Aucun processus de ce type
error-invalid-argument = Argument invalide
error-is-a-directory = { $file }: Est un répertoire

Expand Down
Loading
Loading