machined: make registration of unpriv user's VMs/containers work (#37855)

This adds missing glue to reasonably allow unpriv users VMs/containers
to register with the system machined.

This primarily adds two things:

1. machined can now properly track VMs/containers residing in subcgroups
of units, because that's effectively what happens for per-user
VMs/containers: they are placed below the system unit `user@….service`
in some user unit.

2. machines registered with machined now have an owning UID: users can
operate on their own machines withour re-authentication, but not on
others.

Note that this is only a first step regarding machined's hookup of
nspawn/vmspawn in the long run for unpriv operation.

I think eventually we should make it so that there's both a per-user and
a per-system machined instance (so far, and even with this PR there's
still one per-system instance), and per-user containers/VMs would
registering with *both*. Having two instances makes sense I think,
because it would mean we can make machined reasonably manage the
per-user image discovery, and also do the per-system network/hostname
handling.
This commit is contained in:
Lennart Poettering
2025-07-11 21:10:08 +02:00
committed by GitHub
38 changed files with 1492 additions and 336 deletions
+108 -58
View File
@@ -12,7 +12,6 @@
#include <sys/prctl.h>
#include <sys/wait.h>
#include <unistd.h>
#include "constants.h"
#if HAVE_SELINUX
#include <selinux/selinux.h>
@@ -38,8 +37,10 @@
#include "cgroup-util.h"
#include "chase.h"
#include "common-signal.h"
#include "constants.h"
#include "copy.h"
#include "cpu-set-util.h"
#include "daemon-util.h"
#include "dev-setup.h"
#include "devnum-util.h"
#include "discover-image.h"
@@ -1667,11 +1668,6 @@ static int verify_arguments(void) {
if (has_custom_root_mount(arg_custom_mounts, arg_n_custom_mounts))
arg_read_only = true;
if (arg_keep_unit && arg_register && cg_pid_get_owner_uid(0, NULL) >= 0)
/* Save the user from accidentally registering either user-$SESSION.scope or user@.service.
* The latter is not technically a user session, but we don't need to labour the point. */
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--keep-unit --register=yes may not be used when invoked from a user session.");
if (arg_directory && arg_image)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--directory= and --image= may not be combined.");
@@ -2960,13 +2956,19 @@ static int wait_for_container(PidRef *pid, ContainerStatus *container) {
static int on_orderly_shutdown(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
PidRef *pid = ASSERT_PTR(userdata);
assert(si);
if (pidref_is_set(pid))
if (pidref_kill(pid, arg_kill_signal) >= 0) {
log_info("Trying to halt container. Send SIGTERM again to trigger immediate termination.");
log_info("Trying to halt container by sending %s to container PID 1. Send SIGTERM again to trigger immediate termination.",
signal_to_string(si->ssi_signo));
sd_event_source_set_userdata(s, NULL);
sd_notify(/* unset_environment= */ false, NOTIFY_STOPPING_MESSAGE);
return 0;
}
log_debug("Got %s, exiting.", signal_to_string(si->ssi_signo));
sd_event_exit(sd_event_source_get_event(s), 0);
return 0;
}
@@ -4617,15 +4619,18 @@ static int nspawn_dispatch_notify_fd(sd_event_source *source, int fd, uint32_t r
log_debug("Got sd_notify() message: %s", strnull(joined));
}
char *status = strv_find_startswith(tags, "STATUS=");
if (status)
(void) sd_notifyf(/* unset_environment= */ false, "STATUS=Container running: %s", status);
if (strv_contains(tags, "READY=1")) {
r = sd_notify(false, "READY=1\n");
r = sd_notify(/* unset_environment= */ false, "READY=1\n");
if (r < 0)
log_warning_errno(r, "Failed to send readiness notification, ignoring: %m");
}
char *p = strv_find_startswith(tags, "STATUS=");
if (p)
(void) sd_notifyf(false, "STATUS=Container running: %s", p);
if (!status)
(void) sd_notifyf(/* unset_environment= */ false, "STATUS=Container running.");
}
return 0;
}
@@ -5136,7 +5141,6 @@ static int run_container(
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
_cleanup_(pty_forward_freep) PTYForward *forward = NULL;
_cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_free_ uid_t *bind_user_uid = NULL;
size_t n_bind_user_uid = 0;
ContainerStatus container_status = 0;
@@ -5459,19 +5463,35 @@ static int run_container(
return r;
}
if (arg_register || !arg_keep_unit) {
if (arg_privileged || arg_register)
r = sd_bus_default_system(&bus);
else
r = sd_bus_default_user(&bus);
/* Registration always happens on the system bus */
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *system_bus = NULL;
if (arg_register || arg_privileged) {
r = sd_bus_default_system(&system_bus);
if (r < 0)
return log_error_errno(r, "Failed to open bus: %m");
return log_error_errno(r, "Failed to open system bus: %m");
r = sd_bus_set_close_on_exit(bus, false);
r = sd_bus_set_close_on_exit(system_bus, false);
if (r < 0)
return log_error_errno(r, "Failed to disable close-on-exit behaviour: %m");
(void) sd_bus_set_allow_interactive_authorization(bus, arg_ask_password);
(void) sd_bus_set_allow_interactive_authorization(system_bus, arg_ask_password);
}
/* Scope allocation happens on the user bus if we are unpriv, otherwise system bus. */
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *user_bus = NULL;
_cleanup_(sd_bus_unrefp) sd_bus *runtime_bus = NULL;
if (arg_privileged)
runtime_bus = sd_bus_ref(system_bus);
else {
r = sd_bus_default_user(&user_bus);
if (r < 0)
return log_error_errno(r, "Failed to open user bus: %m");
r = sd_bus_set_close_on_exit(user_bus, false);
if (r < 0)
return log_error_errno(r, "Failed to disable close-on-exit behaviour: %m");
runtime_bus = sd_bus_ref(user_bus);
}
if (!arg_keep_unit) {
@@ -5480,7 +5500,7 @@ static int run_container(
* scope. Let's hook into that, and cleanly shut down the container, and print a friendly message. */
r = sd_bus_match_signal_async(
bus,
runtime_bus,
/* ret= */ NULL,
"org.freedesktop.systemd1",
/* path= */ NULL,
@@ -5493,11 +5513,46 @@ static int run_container(
return log_error_errno(r, "Failed to request RequestStop match: %m");
}
if (arg_keep_unit) {
/* If we are not supposed to allocate a unit, then let's move the process now, so that we can
* register things while being in the right cgroup location already. Otherwise, let's move
* the process later, once we have unit and hence cgroup. */
r = create_subcgroup(
pid,
arg_keep_unit,
arg_uid_shift,
userns_fd,
arg_userns_mode);
if (r < 0)
return r;
}
bool scope_allocated = false;
if (!arg_keep_unit && (!arg_register || !arg_privileged)) {
AllocateScopeFlags flags = ALLOCATE_SCOPE_ALLOW_PIDFD;
r = allocate_scope(
runtime_bus,
arg_machine,
pid,
arg_slice,
arg_custom_mounts, arg_n_custom_mounts,
arg_kill_signal,
arg_property,
arg_property_message,
arg_start_mode,
flags);
if (r < 0)
return r;
scope_allocated = true;
}
bool registered = false;
if (arg_register) {
RegisterMachineFlags flags = 0;
SET_FLAG(flags, REGISTER_MACHINE_KEEP_UNIT, arg_keep_unit);
SET_FLAG(flags, REGISTER_MACHINE_KEEP_UNIT, arg_keep_unit || !arg_privileged);
r = register_machine(
bus,
system_bus,
arg_machine,
pid,
arg_directory,
@@ -5514,33 +5569,22 @@ static int run_container(
if (r < 0)
return r;
} else if (!arg_keep_unit) {
AllocateScopeFlags flags = ALLOCATE_SCOPE_ALLOW_PIDFD;
r = allocate_scope(
bus,
arg_machine,
pid,
arg_slice,
arg_custom_mounts, arg_n_custom_mounts,
arg_kill_signal,
arg_property,
arg_property_message,
arg_start_mode,
flags);
if (r < 0)
return r;
registered = true;
}
} else if (arg_slice || arg_property)
if (arg_keep_unit && (arg_slice || arg_property))
log_notice("Machine and scope registration turned off, --slice= and --property= settings will have no effect.");
r = create_subcgroup(
pid,
arg_keep_unit,
arg_uid_shift,
userns_fd,
arg_userns_mode);
if (r < 0)
return r;
if (!arg_keep_unit) {
r = create_subcgroup(
pid,
arg_keep_unit,
arg_uid_shift,
userns_fd,
arg_userns_mode);
if (r < 0)
return r;
}
/* Notify the child that the parent is ready with all its setup (including cgroup-ification), and
* that the child can now hand over control to the code to run inside the container. */
@@ -5561,10 +5605,16 @@ static int run_container(
(void) sd_event_set_watchdog(event, true);
if (bus) {
r = sd_bus_attach_event(bus, event, 0);
if (system_bus) {
r = sd_bus_attach_event(system_bus, event, 0);
if (r < 0)
return log_error_errno(r, "Failed to attach bus to event loop: %m");
return log_error_errno(r, "Failed to attach system bus to event loop: %m");
}
if (user_bus) {
r = sd_bus_attach_event(user_bus, event, 0);
if (r < 0)
return log_error_errno(r, "Failed to attach user bus to event loop: %m");
}
r = setup_notify_parent(event, notify_socket, pid, &notify_event_source);
@@ -5592,11 +5642,11 @@ static int run_container(
* will make them appear in getpwuid(), thus we can release the /etc/passwd lock. */
etc_passwd_lock = safe_close(etc_passwd_lock);
(void) sd_notifyf(false,
"STATUS=Container running.\n"
(void) sd_notifyf(/* unset_environment= */ false,
"STATUS=Container started.\n"
"X_NSPAWN_LEADER_PID=" PID_FMT, pid->pid);
if (!arg_notify_ready) {
r = sd_notify(false, "READY=1\n");
r = sd_notify(/* unset_environment= */ false, "READY=1\n");
if (r < 0)
log_warning_errno(r, "Failed to send readiness notification, ignoring: %m");
}
@@ -5710,8 +5760,8 @@ static int run_container(
return log_error_errno(r, "Failed to run event loop: %m");
/* Kill if it is not dead yet anyway */
if (!arg_register && !arg_keep_unit && bus)
terminate_scope(bus, arg_machine);
if (scope_allocated)
terminate_scope(runtime_bus, arg_machine);
/* Normally redundant, but better safe than sorry */
(void) pidref_kill(pid, SIGKILL);
@@ -5731,8 +5781,8 @@ static int run_container(
r = wait_for_container(pid, &container_status);
/* Tell machined that we are gone. */
if (arg_register && bus)
(void) unregister_machine(bus, arg_machine);
if (registered)
(void) unregister_machine(system_bus, arg_machine);
if (r < 0)
/* We failed to wait for the container, or the container exited abnormally. */