Summary
Follow-up to #140. The action-timeout fix contains non-privileged commands in a process group and signals the group on timeout. But the daemon runs as User=sysknife and elevates through sudo, and an unprivileged process cannot signal a process running as root. So for every privileged action the real work runs as a root child that the daemon's SIGTERM/SIGKILL cannot touch.
The #140 fix handles this honestly rather than pretending: when the process group cannot be confirmed gone (killpg returns EPERM, meaning members remain but are unsignalable), the executor returns ExecutorError::ActionNotStopped, the dispatcher skips the automatic rollback, and it holds the exclusion slot so no second mutating action can race the first. That is the safe response, but it is detection, not termination: a hung privileged action still runs to completion or until its own tool gives up.
What real termination needs
A privileged mechanism the daemon is allowed to invoke:
- systemd transient scope — run each privileged action under
systemd-run --scope --collect --unit=sysknife-action-<id>, so systemd owns the cgroup and can systemctl kill it. This is the cleanest: cgroup kill catches every descendant regardless of uid, and it works for rpm-ostree too (a systemctl stop rpm-ostreed-adjacent story). Requires the daemon to have a sudoers grant for systemd-run/systemctl kill scoped to sysknife-action-* units.
- A sudoers-authorised kill helper —
sysknife-kill <pgid> allowed via /etc/sudoers.d/, scoped so it can only signal groups the daemon spawned. More surface than option 1.
rpm-ostree needs its own care regardless: it is a thin D-Bus client to rpm-ostreed, so the transaction lives in a different cgroup and the correct cancel is rpm-ostree cancel, not a signal.
Acceptance
- A privileged action that ignores signals is actually stopped on timeout, verified by a live-VM test (the unit tests can only cover the same-uid path).
ActionNotStopped becomes the genuine last resort (mechanism failed), not the expected path for every sudo action.
Provenance
Surfaced by the review of #141 (two independent reviewers) and a defensive red-team pass. The detection-and-veto half shipped in #141; this issue is the termination half.
Summary
Follow-up to #140. The action-timeout fix contains non-privileged commands in a process group and signals the group on timeout. But the daemon runs as
User=sysknifeand elevates throughsudo, and an unprivileged process cannot signal a process running as root. So for every privileged action the real work runs as a root child that the daemon's SIGTERM/SIGKILL cannot touch.The #140 fix handles this honestly rather than pretending: when the process group cannot be confirmed gone (
killpgreturnsEPERM, meaning members remain but are unsignalable), the executor returnsExecutorError::ActionNotStopped, the dispatcher skips the automatic rollback, and it holds the exclusion slot so no second mutating action can race the first. That is the safe response, but it is detection, not termination: a hung privileged action still runs to completion or until its own tool gives up.What real termination needs
A privileged mechanism the daemon is allowed to invoke:
systemd-run --scope --collect --unit=sysknife-action-<id>, so systemd owns the cgroup and cansystemctl killit. This is the cleanest: cgroup kill catches every descendant regardless of uid, and it works forrpm-ostreetoo (asystemctl stop rpm-ostreed-adjacent story). Requires the daemon to have a sudoers grant forsystemd-run/systemctl killscoped tosysknife-action-*units.sysknife-kill <pgid>allowed via/etc/sudoers.d/, scoped so it can only signal groups the daemon spawned. More surface than option 1.rpm-ostreeneeds its own care regardless: it is a thin D-Bus client torpm-ostreed, so the transaction lives in a different cgroup and the correct cancel isrpm-ostree cancel, not a signal.Acceptance
ActionNotStoppedbecomes the genuine last resort (mechanism failed), not the expected path for every sudo action.Provenance
Surfaced by the review of #141 (two independent reviewers) and a defensive red-team pass. The detection-and-veto half shipped in #141; this issue is the termination half.