From e5a5656b55725b3674419b67a3f0287f37781860 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Sat, 28 Feb 2026 00:46:21 +0000 Subject: [PATCH] machined: do not allow unprivileged users to shell into the root namespace via varlink Forbid non-root from shelling into a machine that is running in the root user namespace. Follow-up for adaff8eb35d9c471af81fddaa4403bc5843a256f --- src/machine/machine-varlink.c | 18 ++++++++++++++++++ test/units/TEST-13-NSPAWN.unpriv.sh | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/machine/machine-varlink.c b/src/machine/machine-varlink.c index a499dbc3be..e8a8933251 100644 --- a/src/machine/machine-varlink.c +++ b/src/machine/machine-varlink.c @@ -552,6 +552,24 @@ int vl_method_open(sd_varlink *link, sd_json_variant *parameters, sd_varlink_met return r; if (manager->runtime_scope != RUNTIME_SCOPE_USER) { + /* Ensure only root can shell into the root namespace, unless it's specifically the host machine, + * which is owned by uid 0 anyway and cannot be self-registered. This is to avoid unprivileged + * users registering a process they own in the root user namespace, and then shelling in as root + * or another user. Note that the shell operation is privileged and requires 'auth_admin', so we + * do not need to check the caller's uid, as that will be checked by polkit, and if they machine's + * and the caller's do not match, authorization will be required. It's only the case where the + * caller owns the machine that will be shortcut and needs to be checked here. */ + if (machine->uid != 0 && machine->class != MACHINE_HOST) { + r = pidref_in_same_namespace(&PIDREF_MAKE_FROM_PID(1), &machine->leader, NAMESPACE_USER); + if (r < 0) + return log_debug_errno( + r, + "Failed to check if machine '%s' is running in the root user namespace: %m", + machine->name); + if (r > 0) + return sd_varlink_error(link, SD_VARLINK_ERROR_PERMISSION_DENIED, NULL); + } + _cleanup_strv_free_ char **polkit_details = NULL; polkit_details = machine_open_polkit_details(p.mode, machine->name, user, path, command_line); diff --git a/test/units/TEST-13-NSPAWN.unpriv.sh b/test/units/TEST-13-NSPAWN.unpriv.sh index cbf332aa22..25867de707 100755 --- a/test/units/TEST-13-NSPAWN.unpriv.sh +++ b/test/units/TEST-13-NSPAWN.unpriv.sh @@ -31,6 +31,7 @@ at_exit() { rm -f /usr/share/polkit-1/rules.d/registermachinetest.rules rm -rf /var/tmp/mangletest rm -f /var/tmp/mangletest.tar.gz + rm -f /shouldnotwork } trap at_exit EXIT @@ -104,6 +105,24 @@ run0 -u testuser \ (! run0 -u testuser machinectl shell 0@shouldnotwork2 /usr/bin/id -u) (! run0 -u testuser machinectl shell testuser@shouldnotwork2 /usr/bin/id -u) +run0 -u testuser \ + systemd-run --unit sleep.service --user sleep infinity +sleep_pid="$(run0 -u testuser systemctl show --user -P MainPID sleep.service)" +run0 -u testuser \ + varlinkctl \ + call \ + /run/systemd/machine/io.systemd.Machine \ + io.systemd.Machine.Register \ + "{\"name\":\"shouldnotwork3\", \"class\":\"container\", \"leader\": $sleep_pid}" +(! run0 -u testuser \ + varlinkctl \ + call \ + /run/systemd/machine/io.systemd.Machine \ + io.systemd.Machine.Open \ + '{"name":"shouldnotwork3", "mode": "shell", "user":"root","path":"/usr/bin/bash","args":["bash","-c","''touch /shouldnotwork; sleep 20''"]}') +systemctl --user --machine testuser@ stop sleep.service +test ! -f /shouldnotwork + run0 -u testuser mkdir /var/tmp/image-tar run0 -u testuser importctl --user export-tar zurps /var/tmp/image-tar/kurps.tar.gz -m run0 -u testuser importctl --user import-tar /var/tmp/image-tar/kurps.tar.gz -m