GHSA-qwv4-3gwc-w5g8
CVE Information
Impact
When systemd-machined >= v259 (or v258 with a custom
polkitpolicy that allowsregister-machineaccess) is running on a desktop system, an unprivileged user logged in a desktop graphical session can kill arbitrary processes, even privileged ones.
- versions older than v259 are not affected, unless unprivileged access is granted for the
register-machinepolkit action via a local, custom policy config file- versions older than v258 are not affected
- unrelated to the systemd service manager (pid 1 or user session managers)
- systemd-machined is not typically installed by default, and is typically in an optional, separate package (e.g.: systemd-container)
- terminal-only or remote sessions (e.g.: ssh) are not affected
Patches
v262 (https://github.com/systemd/systemd/commit/b7769aa34eee5abcdb0ede535459cb9d42fc4376) v261.2 (https://github.com/systemd/systemd/commit/8eb162df81b4f684c9d444e458dbf22674f964fb) v260.4 (https://github.com/systemd/systemd/commit/51f9f9f6d7c482532fe027bca1c5aa774dbdb9ea) v259.8 (https://github.com/systemd/systemd/commit/93bf2fd1f5d818872aab1b03d876599d001f60e2) v258.10 (https://github.com/systemd/systemd/commit/cb4907c284f70843b04de2c13e325563f76dfe06)
Workarounds
Restricting access to privileged users only via a Polkit rule blocks the escalation path. Create
/etc/polkit-1/rules.d/machined-register.ruleswith content:polkit.addRule(function(action, subject) { if (action.id == "org.freedesktop.machine1.register-machine" && subject.user != "root") { return polkit.Result.AUTH_ADMIN_KEEP; } });References
Original report follows
Claim.
systemd-machinedlets a local unprivileged, active-session user deliver an arbitrary signal, as root, to any non-PID-1 process it does not own.vl_method_register()ownership-checks the machine's leader pidref but never the attacker-supplied supervisor pidref;io.systemd.Machine.Killwithwhom:"supervisor"then funnelsmachine->supervisorintopidref_kill()from root context, and themanage-machinespolkit gate on Kill is short-circuited byvarlink_check_good_user()becausemachine->uidequals the registering caller's own uid.Sink (file:line at HEAD
5a80137a).
src/machine/machine-varlink.c—vl_method_register(). The leader is ownership-checked; the check is gated atmachine-varlink.c:203:if (manager->runtime_scope != RUNTIME_SCOPE_USER && machine->uid != 0 && !sender_is_admin) { r = process_is_owned_by_uid(&machine->leader, machine->uid); if (r < 0) return r; if (r == 0) return sd_varlink_error(link, SD_VARLINK_ERROR_PERMISSION_DENIED, NULL); }The supervisor pidref is taken in (dispatched via
supervisorProcessId,machine->supervisor = TAKE_PIDREF(client_pidref)) with no correspondingprocess_is_owned_by_uid(&machine->supervisor, machine->uid). Note the upstream-addedsender_is_adminadmin fast-path in this same conjunction is an admin short-circuit — it does not close the gap for the unprivileged attacker, who reaches this branch withsender_is_adminfalse and is still only leader-checked.
src/machine/machine.c—machine_kill(),KILL_SUPERVISORbranch:return pidref_kill(&m->supervisor, signo);(runs as root).machine_pidref()only refusespid == 1.
src/shared/bus-polkit.c—varlink_verify_polkit_async_full()→varlink_check_good_user(link, machine->uid)returns 1 when the peer uid equalsmachine->uid, so themanage-machinespolkit check on Kill is skipped for the machine's own registrant.Attacker + trust boundary. Local unprivileged active-session user. Boundary:
local_unpriv_to_root_daemon(systemddocs/SECURITY.md). The attacker's own session legitimately satisfies theorg.freedesktop.machine1.register-machineaction (upstream policyimplicit active: yes).Exact observable. No crash — an authorization boundary crossing. A root-owned, non-PID-1 process receives the attacker's signal (e.g. SIGKILL) and is reaped ("Killed"). The discriminator is the asymmetry: the identical root-owned PID is rejected when supplied as a leader (ownership check intact) but accepted when supplied as a supervisor (ownership check missing), and the subsequent root-context
Killsucceeds.Minimal reproduction. With a real
systemd-machined(system scope,dbus+polkitenabled) owningorg.freedesktop.machine1, acting as an unprivileged user (uid 1001) viavarlinkctl:
- Negative control: Register a machine whose leader is a root-owned PID → machined returns
Permission denied(leader ownership check fires).- Attack, step A: Register a machine with
leader= one of the attacker's own PIDs andsupervisor= a root-owned target PID (e.g. a root-ownedsetsid sleep 9000) → Register succeeds (exit 0); the supervisor pidref is accepted with no ownership check.- Attack, step B: Call
io.systemd.Machine.Killwithwhom:"supervisor",signal:9→ returns exit 0, and the OS reaps the root-owned target ("<pid> Killed").The root-owned PID is killed by the unprivileged user through root machined. (Honest caveat, carried from the underlying proof: the distro used to fire this end-to-end shipped polkit 0.105, which does not understand the modern
unix-processpidfdsubject field systemd sends, so machined's live register-machine polkit query errors on a tooling-version mismatch.pkcheck --action-id org.freedesktop.machine1.register-machineindependently returns exit 0 for the active session, confirming the precondition is genuinely granted; a POC-only shim modeled that grant to run end-to-end. The shim touches only the register precondition gate — the supervisor/leader/Kill/varlink_check_good_userlogic is byte-identical to HEAD, and the negative control proves leader ownership enforcement was not weakened.)Concrete fix. Add
process_is_owned_by_uid(&machine->supervisor, machine->uid)mirroring the leader check (reject onr == 0withSD_VARLINK_ERROR_PERMISSION_DENIED), and do not letvarlink_check_good_user()waive themanage-machinesgate for the Kill path.