at master 9.3 kB view raw
1{ lib, ... }: 2 3{ 4 name = "rtkit"; 5 6 meta.maintainers = with lib.maintainers; [ rvl ]; 7 8 nodes.machine = 9 { config, pkgs, ... }: 10 { 11 assertions = [ 12 { 13 assertion = config.security.polkit.enable; 14 message = "rtkit needs polkit to handle authorization"; 15 } 16 ]; 17 18 imports = [ ./common/user-account.nix ]; 19 services.getty.autologinUser = "alice"; 20 21 security.rtkit.enable = true; 22 23 # Modified configuration with higher maximum realtime priority. 24 specialisation.withHigherPrio.configuration = { 25 security.rtkit.args = [ 26 "--max-realtime-priority=89" 27 "--our-realtime-priority=90" 28 ]; 29 }; 30 31 # Target process for testing - belongs to a logind session. 32 systemd.user.services.sleeper = { 33 description = "Guinea pig service"; 34 serviceConfig = { 35 ExecStart = "@${pkgs.coreutils}/bin/sleep sleep inf"; 36 # rtkit-daemon won't grant real-time to threads unless they have a rttime limit. 37 LimitRTTIME = 200000; 38 }; 39 wantedBy = [ "default.target" ]; 40 }; 41 42 # Target process for testing - doesn't belong to a session. 43 systemd.services."sleeper@" = { 44 description = "Guinea pig system service for %I"; 45 serviceConfig = { 46 ExecStart = "@${pkgs.coreutils}/bin/sleep sleep inf"; 47 LimitRTTIME = 200000; 48 User = "%I"; 49 }; 50 }; 51 systemd.targets.multi-user.wants = [ "sleeper@alice.service" ]; 52 53 # Install chrt to check outcomes of rtkit calls 54 environment.systemPackages = [ pkgs.util-linux ]; 55 56 # Provide a little logging of polkit checks - otherwise it's 57 # impossible to know what's going on. 58 security.polkit.debug = true; 59 security.polkit.extraConfig = '' 60 polkit.addRule(function(action, subject) { 61 const ns = "org.freedesktop.RealtimeKit1."; 62 const acquireHighPrio = ns + "acquire-high-priority"; 63 const acquireRT = ns + "acquire-real-time"; 64 if (action.id == acquireHighPrio || action.id == acquireRT) { 65 polkit.log("rtkit: Checking " + action.id + " for " + subject.user + "\n " + subject); 66 } 67 }); 68 ''; 69 }; 70 71 interactive.nodes.machine = 72 { pkgs, ... }: 73 { 74 security.rtkit.args = [ "--debug" ]; 75 systemd.services.strace-rtkit = 76 let 77 target = "rtkit-daemon.service"; 78 in 79 { 80 bindsTo = [ target ]; 81 after = [ target ]; 82 scriptArgs = target; 83 script = '' 84 pid=$(systemctl show -P MainPID $1) 85 strace -tt -s 100 -e trace=all -p $pid 86 ''; 87 path = [ pkgs.strace ]; 88 }; 89 }; 90 91 testScript = 92 { nodes, ... }: 93 let 94 specialisations = "${nodes.machine.system.build.toplevel}/specialisation"; 95 uid = toString nodes.machine.users.users.alice.uid; 96 in 97 '' 98 import json 99 import shlex 100 from collections import namedtuple 101 from typing import Any, Optional 102 103 Result = namedtuple("Result", ["command", "machine", "status", "out", "value"]) 104 Value = namedtuple("Value", ["type", "data"]) 105 106 def busctl(node: Machine, *args: Any, user: Optional[str] = None) -> Result: 107 command = f"busctl --json=short {shlex.join(map(str, args))}" 108 if user is not None: 109 command = f"su - {user} -c {shlex.quote(command)}" 110 (status, out) = node.execute(command) 111 out = out.strip() 112 value = json.loads(out, object_hook=lambda x: Value(**x)) if status == 0 and out else None 113 return Result(command, node, status, out, value) 114 115 def assert_result_success(result: Result): 116 if result.status != 0: 117 result.machine.log(f"output: {result.out}") 118 raise Exception(f"command `{result.command}` failed (exit code {result.status})") 119 120 def assert_result_fail(result: Result): 121 if result.status == 0: 122 raise Exception(f"command `{result.command}` unexpectedly succeeded") 123 124 def rtkit_make_process_realtime(node: Machine, pid: int, priority: int, user: Optional[str] = None) -> Result: 125 return busctl(node, "call", "org.freedesktop.RealtimeKit1", "/org/freedesktop/RealtimeKit1", "org.freedesktop.RealtimeKit1", "MakeThreadRealtimeWithPID", "ttu", pid, 0, priority, user=user) 126 127 def get_max_realtime_priority() -> int: 128 result = busctl(machine, "get-property", "org.freedesktop.RealtimeKit1", "/org/freedesktop/RealtimeKit1", "org.freedesktop.RealtimeKit1", "MaxRealtimePriority") 129 assert_result_success(result) 130 assert result.value.type == "i", f"""Unexpected MaxRealtimePriority property type ({result.value})""" 131 return int(result.value.data) 132 133 def parse_chrt(out: str, field: str) -> str: 134 return next(map(lambda l: l.split(": ")[1], filter(lambda l: field in l, out.splitlines()))) 135 136 def get_pid(node: Machine, unit: str, user: Optional[str] = None) -> int: 137 node.wait_for_unit(unit, user=user) 138 (status, out) = node.systemctl(f"show -P MainPID {unit}", user=user) 139 if status == 0: 140 return int(out.strip()) 141 else: 142 node.log(out) 143 raise Exception(f"unable to determine MainPID of {unit} (systemctl exit code {status})") 144 145 def assert_sched(node: Machine, pid: int, policy: Optional[str] = None, priority: Optional[int] = None): 146 out = node.succeed(f"chrt -p {pid}") 147 node.log(out) 148 if policy is not None: 149 thread_policy = parse_chrt(out, "policy") 150 assert policy in thread_policy, f"Expected {policy} scheduling policy, but got: {thread_policy}" 151 if priority is not None: 152 thread_priority = parse_chrt(out, "priority") 153 assert str(priority) == thread_priority, f"Expected scheduling priority {priority}, but got: {thread_priority}" 154 155 machine.wait_for_unit("basic.target") 156 157 rtprio = 20 158 higher_rtprio = 42 159 max_rtprio = get_max_realtime_priority() 160 161 with subtest("maximum sched_rr priority"): 162 assert max_rtprio >= rtprio, f"""MaxRealtimePriority ({max_rtprio}) too low""" 163 assert higher_rtprio > max_rtprio, f"""Test value higher_rtprio ({higher_rtprio}) insufficient compared to MaxRealtimePriority ({max_rtprio})""" 164 165 # wait for autologin and systemd user service manager 166 machine.wait_for_unit("multi-user.target") 167 machine.wait_for_unit("user@${uid}.service") 168 169 with subtest("polkit sanity check"): 170 pid = get_pid(machine, "sleeper.service", user="alice") 171 machine.succeed(f"pkcheck -p {pid} -a org.freedesktop.RealtimeKit1.acquire-real-time") 172 173 with subtest("chrt sanity check"): 174 print(machine.succeed("chrt --rr --max")) 175 pid = get_pid(machine, "sleeper.service", user="alice") 176 machine.succeed(f"chrt --rr --pid {rtprio} {pid}") 177 assert_sched(machine, pid, policy="SCHED_RR", priority=rtprio) 178 machine.stop_job("sleeper.service", user="alice") 179 machine.start_job("sleeper.service", user="alice") 180 181 # Permission granted by policy from rtkit package. 182 with subtest("local user process can acquire real-time scheduling"): 183 pid = get_pid(machine, "sleeper.service", user="alice") 184 result = rtkit_make_process_realtime(machine, pid, rtprio, user="alice") 185 assert_result_success(result) 186 assert_sched(machine, pid, policy="SCHED_RR", priority=rtprio) 187 188 # User must not get higher priority than the maximum 189 with subtest("real-time scheduling priority is limited"): 190 machine.stop_job("sleeper.service", user="alice") 191 machine.start_job("sleeper.service", user="alice") 192 pid = get_pid(machine, "sleeper.service", user="alice") 193 with machine.nested("rtkit call must fail"): 194 result = rtkit_make_process_realtime(machine, pid, max_rtprio + 1, user="alice") 195 assert_result_fail(result) 196 assert_sched(machine, pid, policy="SCHED_OTHER") 197 198 # This is a local shop for local people - we'll have no trouble here. 199 # In this test, the target process belongs to alice, but doesn't 200 # have a user session, so it's considered non-local. 201 with subtest("non-local user process cannot acquire real-time scheduling"): 202 pid = get_pid(machine, "sleeper@alice.service") 203 with machine.nested("rtkit call must fail"): 204 result = rtkit_make_process_realtime(machine, pid, rtprio, "alice") 205 assert_result_fail(result) 206 assert_sched(machine, pid, policy="SCHED_OTHER") 207 208 # Switch to alternate configuration then ask for higher priority. 209 with subtest("command-line arguments allow increasing maximum rtprio"): 210 machine.succeed("${specialisations}/withHigherPrio/bin/switch-to-configuration test") 211 pid = get_pid(machine, "sleeper.service", user="alice") 212 result = rtkit_make_process_realtime(machine, pid, higher_rtprio, user="alice") 213 assert_result_success(result) 214 assert_sched(machine, pid, policy="SCHED_RR", priority=higher_rtprio) 215 ''; 216}