add api to kill subcgroups of units (#38102)

This commit is contained in:
Lennart Poettering
2025-07-08 06:33:32 +02:00
committed by GitHub
15 changed files with 289 additions and 58 deletions
+11
View File
@@ -831,6 +831,12 @@ static int method_kill_unit(sd_bus_message *message, void *userdata, sd_bus_erro
return method_generic_unit_operation(message, userdata, error, bus_unit_method_kill, 0);
}
static int method_kill_unit_subgroup(sd_bus_message *message, void *userdata, sd_bus_error *error) {
/* We don't bother with GENERIC_UNIT_LOAD nor GENERIC_UNIT_VALIDATE_LOADED here, as it shouldn't
* matter whether a unit is loaded for killing any processes possibly in the unit's cgroup. */
return method_generic_unit_operation(message, userdata, error, bus_unit_method_kill_subgroup, 0);
}
static int method_clean_unit(sd_bus_message *message, void *userdata, sd_bus_error *error) {
/* Load the unit if necessary, in order to load it, and insist on the unit being loaded to be
* cleaned */
@@ -3025,6 +3031,11 @@ const sd_bus_vtable bus_manager_vtable[] = {
SD_BUS_NO_RESULT,
method_kill_unit,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("KillUnitSubgroup",
SD_BUS_ARGS("s", name, "s", whom, "s", subgroup, "i", signal),
SD_BUS_NO_RESULT,
method_kill_unit_subgroup,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("QueueSignalUnit",
SD_BUS_ARGS("s", name, "s", whom, "i", signal, "i", value),
SD_BUS_NO_RESULT,
+59 -1
View File
@@ -572,7 +572,60 @@ int bus_unit_method_kill(sd_bus_message *message, void *userdata, sd_bus_error *
if (r == 0)
return 1; /* No authorization for now, but the async polkit stuff will call us again when it has it */
r = unit_kill(u, whom, signo, code, value, error);
r = unit_kill(u, whom, /* subgroup= */ NULL, signo, code, value, error);
if (r < 0)
return r;
return sd_bus_reply_method_return(message, NULL);
}
int bus_unit_method_kill_subgroup(sd_bus_message *message, void *userdata, sd_bus_error *error) {
Unit *u = ASSERT_PTR(userdata);
int r;
assert(message);
r = mac_selinux_unit_access_check(u, message, "stop", error);
if (r < 0)
return r;
const char *swhom, *subgroup;
int32_t signo;
r = sd_bus_message_read(message, "ssi", &swhom, &subgroup, &signo);
if (r < 0)
return r;
KillWhom whom;
if (isempty(swhom))
whom = KILL_CGROUP;
else {
whom = kill_whom_from_string(swhom);
if (whom < 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid whom argument: %s", swhom);
}
if (isempty(subgroup))
subgroup = NULL;
else if (!path_is_normalized(subgroup))
return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Specified cgroup sub-path is not valid.");
else if (!IN_SET(whom, KILL_CGROUP, KILL_CGROUP_FAIL))
return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Subgroup can only be specified in combination with 'cgroup' or 'cgroup-fail'.");
if (!SIGNAL_VALID(signo))
return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Signal number out of range.");
r = bus_verify_manage_units_async_full(
u,
"kill-subgroup",
N_("Authentication is required to send a UNIX signal to the processes of subgroup of '$(unit)'."),
message,
error);
if (r < 0)
return r;
if (r == 0)
return 1; /* No authorization for now, but the async polkit stuff will call us again when it has it */
r = unit_kill(u, whom, subgroup, signo, SI_USER, /* value= */ 0, error);
if (r < 0)
return r;
@@ -986,6 +1039,11 @@ const sd_bus_vtable bus_unit_vtable[] = {
SD_BUS_NO_RESULT,
bus_unit_method_kill,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("KillSubgroup",
SD_BUS_ARGS("s", subgroup, "i", signal),
SD_BUS_NO_RESULT,
bus_unit_method_kill_subgroup,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("QueueSignal",
SD_BUS_ARGS("s", whom, "i", signal, "i", value),
SD_BUS_NO_RESULT,
+1
View File
@@ -16,6 +16,7 @@ void bus_unit_send_removed_signal(Unit *u);
int bus_unit_method_start_generic(sd_bus_message *message, Unit *u, JobType job_type, bool reload_if_possible, sd_bus_error *error);
int bus_unit_method_enqueue_job(sd_bus_message *message, void *userdata, sd_bus_error *error);
int bus_unit_method_kill(sd_bus_message *message, void *userdata, sd_bus_error *error);
int bus_unit_method_kill_subgroup(sd_bus_message *message, void *userdata, sd_bus_error *error);
int bus_unit_method_reset_failed(sd_bus_message *message, void *userdata, sd_bus_error *error);
int bus_unit_set_properties(Unit *u, sd_bus_message *message, UnitWriteFlags flags, bool commit, sd_bus_error *error);
+2
View File
@@ -54,6 +54,8 @@ static const char* const kill_whom_table[_KILL_WHOM_MAX] = {
[KILL_MAIN_FAIL] = "main-fail",
[KILL_CONTROL_FAIL] = "control-fail",
[KILL_ALL_FAIL] = "all-fail",
[KILL_CGROUP] = "cgroup",
[KILL_CGROUP_FAIL] = "cgroup-fail",
};
DEFINE_STRING_TABLE_LOOKUP(kill_whom, KillWhom);
+2
View File
@@ -31,6 +31,8 @@ typedef enum KillWhom {
KILL_MAIN_FAIL,
KILL_CONTROL_FAIL,
KILL_ALL_FAIL,
KILL_CGROUP,
KILL_CGROUP_FAIL,
_KILL_WHOM_MAX,
_KILL_WHOM_INVALID = -EINVAL,
} KillWhom;
+8
View File
@@ -246,6 +246,10 @@
send_interface="org.freedesktop.systemd1.Manager"
send_member="KillUnit"/>
<allow send_destination="org.freedesktop.systemd1"
send_interface="org.freedesktop.systemd1.Manager"
send_member="KillUnitSubgroup"/>
<allow send_destination="org.freedesktop.systemd1"
send_interface="org.freedesktop.systemd1.Manager"
send_member="QueueSignalUnit"/>
@@ -410,6 +414,10 @@
send_interface="org.freedesktop.systemd1.Unit"
send_member="Kill"/>
<allow send_destination="org.freedesktop.systemd1"
send_interface="org.freedesktop.systemd1.Unit"
send_member="KillSubgroup"/>
<allow send_destination="org.freedesktop.systemd1"
send_interface="org.freedesktop.systemd1.Unit"
send_member="QueueSignal"/>
+36 -15
View File
@@ -4041,6 +4041,7 @@ static int unit_kill_one(
int unit_kill(
Unit *u,
KillWhom whom,
const char *subgroup,
int signo,
int code,
int value,
@@ -4060,11 +4061,19 @@ int unit_kill(
assert(SIGNAL_VALID(signo));
assert(IN_SET(code, SI_USER, SI_QUEUE));
if (subgroup) {
if (!IN_SET(whom, KILL_CGROUP, KILL_CGROUP_FAIL))
return sd_bus_error_set(ret_error, SD_BUS_ERROR_NOT_SUPPORTED, "Killing by subgroup is only supported for 'cgroup' or 'cgroup-kill' modes.");
if (!unit_cgroup_delegate(u))
return sd_bus_error_set(ret_error, SD_BUS_ERROR_NOT_SUPPORTED, "Killing by subgroup is only available for units with control group delegation enabled.");
}
main_pid = unit_main_pid(u);
control_pid = unit_control_pid(u);
if (!UNIT_HAS_CGROUP_CONTEXT(u) && !main_pid && !control_pid)
return sd_bus_error_setf(ret_error, SD_BUS_ERROR_NOT_SUPPORTED, "Unit type does not support process killing.");
return sd_bus_error_set(ret_error, SD_BUS_ERROR_NOT_SUPPORTED, "Unit type does not support process killing.");
if (IN_SET(whom, KILL_MAIN, KILL_MAIN_FAIL)) {
if (!main_pid)
@@ -4095,54 +4104,66 @@ int unit_kill(
/* Note: if we shall enqueue rather than kill we won't do this via the cgroup mechanism, since it
* doesn't really make much sense (and given that enqueued values are a relatively expensive
* resource, and we shouldn't allow us to be subjects for such allocation sprees) */
if (IN_SET(whom, KILL_ALL, KILL_ALL_FAIL) && code == SI_USER) {
if (IN_SET(whom, KILL_ALL, KILL_ALL_FAIL, KILL_CGROUP, KILL_CGROUP_FAIL) && code == SI_USER) {
CGroupRuntime *crt = unit_get_cgroup_runtime(u);
if (crt && crt->cgroup_path) {
_cleanup_set_free_ Set *pid_set = NULL;
_cleanup_free_ char *joined = NULL;
const char *p;
if (empty_or_root(subgroup))
p = crt->cgroup_path;
else {
joined = path_join(crt->cgroup_path, subgroup);
if (!joined)
return -ENOMEM;
p = joined;
}
if (signo == SIGKILL) {
r = cg_kill_kernel_sigkill(crt->cgroup_path);
r = cg_kill_kernel_sigkill(p);
if (r >= 0) {
killed = true;
log_unit_info(u, "Killed unit cgroup with SIGKILL on client request.");
log_unit_info(u, "Killed unit cgroup '%s' with SIGKILL on client request.", p);
goto finish;
}
if (r != -EOPNOTSUPP) {
if (ret >= 0)
sd_bus_error_set_errnof(ret_error, r,
"Failed to kill unit cgroup: %m");
RET_GATHER(ret, log_unit_warning_errno(u, r, "Failed to kill unit cgroup: %m"));
RET_GATHER(ret, log_unit_warning_errno(u, r, "Failed to kill unit cgroup '%s': %m", p));
goto finish;
}
/* Fall back to manual enumeration */
} else {
/* Exclude the main/control pids from being killed via the cgroup if
* not SIGKILL */
} else if (IN_SET(whom, KILL_ALL, KILL_ALL_FAIL)) {
/* Exclude the main/control pids from being killed via the cgroup if not
* SIGKILL */
r = unit_pid_set(u, &pid_set);
if (r < 0)
return log_oom();
}
r = cg_kill_recursive(crt->cgroup_path, signo, 0, pid_set, kill_common_log, u);
r = cg_kill_recursive(p, signo, /* flags= */ 0, pid_set, kill_common_log, u);
if (r < 0 && !IN_SET(r, -ESRCH, -ENOENT)) {
if (ret >= 0)
sd_bus_error_set_errnof(
ret_error, r,
"Failed to send signal SIG%s to auxiliary processes: %m",
signal_to_string(signo));
"Failed to send signal SIG%s to processes in unit cgroup '%s': %m",
signal_to_string(signo), p);
RET_GATHER(ret, log_unit_warning_errno(
u, r,
"Failed to send signal SIG%s to auxiliary processes on client request: %m",
signal_to_string(signo)));
"Failed to send signal SIG%s to processes in unit cgroup '%s' on client request: %m",
signal_to_string(signo), p));
}
killed = killed || r >= 0;
killed = killed || r > 0;
}
}
finish:
/* If the "fail" versions of the operation are requested, then complain if the set of processes we killed is empty */
if (ret >= 0 && !killed && IN_SET(whom, KILL_ALL_FAIL, KILL_CONTROL_FAIL, KILL_MAIN_FAIL))
if (ret >= 0 && !killed && IN_SET(whom, KILL_ALL_FAIL, KILL_CONTROL_FAIL, KILL_MAIN_FAIL, KILL_CGROUP_FAIL))
return sd_bus_error_set_const(ret_error, BUS_ERROR_NO_SUCH_PROCESS, "No matching processes to kill");
return ret;
+1 -1
View File
@@ -886,7 +886,7 @@ int unit_start(Unit *u, ActivationDetails *details);
int unit_stop(Unit *u);
int unit_reload(Unit *u);
int unit_kill(Unit *u, KillWhom w, int signo, int code, int value, sd_bus_error *ret_error);
int unit_kill(Unit *u, KillWhom w, const char *subgroup, int signo, int code, int value, sd_bus_error *ret_error);
void unit_notify_cgroup_oom(Unit *u, bool managed_oom);
+12 -1
View File
@@ -20,6 +20,9 @@ int verb_kill(int argc, char *argv[], void *userdata) {
sd_bus *bus;
int r, q;
if (arg_kill_subgroup && arg_kill_value_set)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--kill-subgroup= and --kill-value= may not be combined.");
r = acquire_bus(BUS_MANAGER, &bus);
if (r < 0)
return r;
@@ -32,7 +35,7 @@ int verb_kill(int argc, char *argv[], void *userdata) {
polkit_agent_open_maybe();
kill_whom = arg_kill_whom ?: "all";
kill_whom = arg_kill_whom ?: arg_kill_subgroup ? "cgroup" : "all";
/* --fail was specified */
if (streq(arg_job_mode(), "fail"))
@@ -53,6 +56,14 @@ int verb_kill(int argc, char *argv[], void *userdata) {
&error,
NULL,
"ssii", *name, kill_whom, arg_signal, arg_kill_value);
else if (arg_kill_subgroup)
q = bus_call_method(
bus,
bus_systemd_mgr,
"KillUnitSubgroup",
&error,
NULL,
"sssi", *name, kill_whom, arg_kill_subgroup, arg_signal);
else
q = bus_call_method(
bus,
+25 -1
View File
@@ -16,6 +16,7 @@
#include "pager.h"
#include "parse-argument.h"
#include "parse-util.h"
#include "path-util.h"
#include "pretty-print.h"
#include "static-destruct.h"
#include "string-table.h"
@@ -88,6 +89,7 @@ bool arg_mkdir = false;
bool arg_marked = false;
const char *arg_drop_in = NULL;
ImagePolicy *arg_image_policy = NULL;
char *arg_kill_subgroup = NULL;
STATIC_DESTRUCTOR_REGISTER(arg_types, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_states, strv_freep);
@@ -103,6 +105,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_boot_loader_entry, unsetp);
STATIC_DESTRUCTOR_REGISTER(arg_clean_what, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_drop_in, unsetp);
STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
STATIC_DESTRUCTOR_REGISTER(arg_kill_subgroup, freep);
static int systemctl_help(void) {
_cleanup_free_ char *link = NULL;
@@ -253,9 +256,11 @@ static int systemctl_help(void) {
" Whether to check inhibitors before shutting down,\n"
" sleeping, or hibernating\n"
" -i Shortcut for --check-inhibitors=no\n"
" -s --signal=SIGNAL Which signal to send\n"
" --kill-whom=WHOM Whom to send signal to\n"
" --kill-value=INT Signal value to enqueue\n"
" -s --signal=SIGNAL Which signal to send\n"
" --kill-subgroup=PATH\n"
" Send signal to sub-control-group only\n"
" --what=RESOURCES Which types of resources to remove\n"
" --now Start or stop unit after enabling or disabling it\n"
" --dry-run Only print what would be done\n"
@@ -438,6 +443,7 @@ static int systemctl_parse_argv(int argc, char *argv[]) {
ARG_DROP_IN,
ARG_WHEN,
ARG_STDIN,
ARG_KILL_SUBGROUP,
};
static const struct option options[] = {
@@ -507,6 +513,7 @@ static int systemctl_parse_argv(int argc, char *argv[]) {
{ "drop-in", required_argument, NULL, ARG_DROP_IN },
{ "when", required_argument, NULL, ARG_WHEN },
{ "stdin", no_argument, NULL, ARG_STDIN },
{ "kill-subgroup", required_argument, NULL, ARG_KILL_SUBGROUP },
{}
};
@@ -1021,6 +1028,23 @@ static int systemctl_parse_argv(int argc, char *argv[]) {
arg_stdin = true;
break;
case ARG_KILL_SUBGROUP: {
if (empty_or_root(optarg)) {
arg_kill_subgroup = mfree(arg_kill_subgroup);
break;
}
_cleanup_free_ char *p = NULL;
if (path_simplify_alloc(optarg, &p) < 0)
return log_oom();
if (!path_is_safe(p))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Control group sub-path '%s' is not valid.", p);
free_and_replace(arg_kill_subgroup, p);
break;
}
case '.':
/* Output an error mimicking getopt, and print a hint afterwards */
log_error("%s: invalid option -- '.'", program_invocation_name);
+1
View File
@@ -100,6 +100,7 @@ extern bool arg_mkdir;
extern bool arg_marked;
extern const char *arg_drop_in;
extern ImagePolicy *arg_image_policy;
extern char *arg_kill_subgroup;
static inline const char* arg_job_mode(void) {
return _arg_job_mode ?: "replace";
+1 -1
View File
@@ -63,7 +63,7 @@ static int time_handler(sd_event_source *s, uint64_t usec, void *userdata) {
int r;
log_error("Test timeout when testing %s", unit->id);
r = unit_kill(unit, KILL_ALL, SIGKILL, SI_USER, 0, NULL);
r = unit_kill(unit, KILL_ALL, /* subgroup= */ NULL, SIGKILL, SI_USER, /* value= */ 0, /* ret_error= */ NULL);
if (r < 0)
log_error_errno(r, "Failed to kill %s, ignoring: %m", unit->id);