Skip to content

GHSA-qwv4-3gwc-w5g8 on CTRL-OS 26.05

Aliases: GHSA-qwv4-3gwc-w5g8, CVE-2026-15060

Packages: systemd

Status: Plausible

Advisory Information

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 (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.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.cvl_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.cmachine_kill(), KILL_SUPERVISOR branch: return pidref_kill(&m->supervisor, signo); (runs as root). machine_pidref() only refuses pid == 1.

  • src/shared/bus-polkit.cvarlink_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.

Updates

2026-08-12 21:29 CEST

Metadata changes:

  • Status for package systemd: “Plausible

2026-08-11 03:33 CEST

Metadata changes:

  • Status for package systemd: “New