Skip to content

systemd-machined: unprivileged users can terminate arbitrary processes

Moderate
bluca published GHSA-qwv4-3gwc-w5g8 Aug 10, 2026

Package

systemd-machined (systemd)

Affected versions

>= 259

Patched versions

262 261.2 260.4 259.8 258.10

Description

Impact

When systemd-machined >= v259 (or v258 with a custom polkit policy that allows register-machine access) 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-machine polkit 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 (b7769aa)
v261.2 (8eb162d)
v260.4 (51f9f9f)
v259.8 (93bf2fd)
v258.10 (cb4907c)

Workarounds

Restricting access to privileged users only via a Polkit rule blocks the escalation path. Create /etc/polkit-1/rules.d/machined-register.rules with 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-machined lets 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.Kill with whom:"supervisor" then funnels machine->supervisor into pidref_kill() from root context, and the manage-machines polkit gate on Kill is short-circuited by varlink_check_good_user() because machine->uid equals 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 at machine-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 corresponding process_is_owned_by_uid(&machine->supervisor, machine->uid). Note the upstream-added sender_is_admin admin 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 with sender_is_admin false and is still only leader-checked.

  • src/machine/machine.c — machine_kill(), KILL_SUPERVISOR branch: return pidref_kill(&m->supervisor, signo); (runs as root). machine_pidref() only refuses pid == 1.

  • src/shared/bus-polkit.c — varlink_verify_polkit_async_full() → varlink_check_good_user(link, machine->uid) returns 1 when the peer uid equals machine->uid, so the manage-machines polkit 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 (systemd docs/SECURITY.md). The attacker's own session legitimately satisfies the org.freedesktop.machine1.register-machine action (upstream policy implicit 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 Kill succeeds.

Minimal reproduction. With a real systemd-machined (system scope, dbus+polkit enabled) owning org.freedesktop.machine1, acting as an unprivileged user (uid 1001) via varlinkctl:

  1. Negative control: Register a machine whose leader is a root-owned PID → machined returns Permission denied (leader ownership check fires).
  2. Attack, step A: Register a machine with leader = one of the attacker's own PIDs and supervisor = a root-owned target PID (e.g. a root-owned setsid sleep 9000) → Register succeeds (exit 0); the supervisor pidref is accepted with no ownership check.
  3. Attack, step B: Call io.systemd.Machine.Kill with whom:"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-process pidfd subject field systemd sends, so machined's live register-machine polkit query errors on a tooling-version mismatch. pkcheck --action-id org.freedesktop.machine1.register-machine independently 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_user logic 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 on r == 0 with SD_VARLINK_ERROR_PERMISSION_DENIED), and do not let varlink_check_good_user() waive the manage-machines gate for the Kill path.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
High
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H

CVE ID

CVE-2026-15060

Weaknesses

Improper Access Control

The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor. Learn more on MITRE.

Missing Authorization

The product does not perform an authorization check when an actor attempts to access a resource or perform an action. Learn more on MITRE.

Credits