1 | // SPDX-License-Identifier: GPL-2.0 |
2 | |
3 | #include <linux/limits.h> |
4 | #include <signal.h> |
5 | |
6 | #include "../kselftest.h" |
7 | #include "cgroup_util.h" |
8 | |
9 | static int idle_process_fn(const char *cgroup, void *arg) |
10 | { |
11 | (void)pause(); |
12 | return 0; |
13 | } |
14 | |
15 | static int do_migration_fn(const char *cgroup, void *arg) |
16 | { |
17 | int object_pid = (int)(size_t)arg; |
18 | |
19 | if (setuid(TEST_UID)) |
20 | return EXIT_FAILURE; |
21 | |
22 | // XXX checking /proc/$pid/cgroup would be quicker than wait |
23 | if (cg_enter(cgroup, pid: object_pid) || |
24 | cg_wait_for_proc_count(cgroup, count: 1)) |
25 | return EXIT_FAILURE; |
26 | |
27 | return EXIT_SUCCESS; |
28 | } |
29 | |
30 | static int do_controller_fn(const char *cgroup, void *arg) |
31 | { |
32 | const char *child = cgroup; |
33 | const char *parent = arg; |
34 | |
35 | if (setuid(TEST_UID)) |
36 | return EXIT_FAILURE; |
37 | |
38 | if (!cg_read_strstr(cgroup: child, control: "cgroup.controllers" , needle: "cpuset" )) |
39 | return EXIT_FAILURE; |
40 | |
41 | if (cg_write(cgroup: parent, control: "cgroup.subtree_control" , buf: "+cpuset" )) |
42 | return EXIT_FAILURE; |
43 | |
44 | if (cg_read_strstr(cgroup: child, control: "cgroup.controllers" , needle: "cpuset" )) |
45 | return EXIT_FAILURE; |
46 | |
47 | if (cg_write(cgroup: parent, control: "cgroup.subtree_control" , buf: "-cpuset" )) |
48 | return EXIT_FAILURE; |
49 | |
50 | if (!cg_read_strstr(cgroup: child, control: "cgroup.controllers" , needle: "cpuset" )) |
51 | return EXIT_FAILURE; |
52 | |
53 | return EXIT_SUCCESS; |
54 | } |
55 | |
56 | /* |
57 | * Migrate a process between two sibling cgroups. |
58 | * The success should only depend on the parent cgroup permissions and not the |
59 | * migrated process itself (cpuset controller is in place because it uses |
60 | * security_task_setscheduler() in cgroup v1). |
61 | * |
62 | * Deliberately don't set cpuset.cpus in children to avoid definining migration |
63 | * permissions between two different cpusets. |
64 | */ |
65 | static int test_cpuset_perms_object(const char *root, bool allow) |
66 | { |
67 | char *parent = NULL, *child_src = NULL, *child_dst = NULL; |
68 | char *parent_procs = NULL, *child_src_procs = NULL, *child_dst_procs = NULL; |
69 | const uid_t test_euid = TEST_UID; |
70 | int object_pid = 0; |
71 | int ret = KSFT_FAIL; |
72 | |
73 | parent = cg_name(root, name: "cpuset_test_0" ); |
74 | if (!parent) |
75 | goto cleanup; |
76 | parent_procs = cg_name(root: parent, name: "cgroup.procs" ); |
77 | if (!parent_procs) |
78 | goto cleanup; |
79 | if (cg_create(cgroup: parent)) |
80 | goto cleanup; |
81 | |
82 | child_src = cg_name(root: parent, name: "cpuset_test_1" ); |
83 | if (!child_src) |
84 | goto cleanup; |
85 | child_src_procs = cg_name(root: child_src, name: "cgroup.procs" ); |
86 | if (!child_src_procs) |
87 | goto cleanup; |
88 | if (cg_create(cgroup: child_src)) |
89 | goto cleanup; |
90 | |
91 | child_dst = cg_name(root: parent, name: "cpuset_test_2" ); |
92 | if (!child_dst) |
93 | goto cleanup; |
94 | child_dst_procs = cg_name(root: child_dst, name: "cgroup.procs" ); |
95 | if (!child_dst_procs) |
96 | goto cleanup; |
97 | if (cg_create(cgroup: child_dst)) |
98 | goto cleanup; |
99 | |
100 | if (cg_write(cgroup: parent, control: "cgroup.subtree_control" , buf: "+cpuset" )) |
101 | goto cleanup; |
102 | |
103 | if (cg_read_strstr(cgroup: child_src, control: "cgroup.controllers" , needle: "cpuset" ) || |
104 | cg_read_strstr(cgroup: child_dst, control: "cgroup.controllers" , needle: "cpuset" )) |
105 | goto cleanup; |
106 | |
107 | /* Enable permissions along src->dst tree path */ |
108 | if (chown(child_src_procs, test_euid, -1) || |
109 | chown(child_dst_procs, test_euid, -1)) |
110 | goto cleanup; |
111 | |
112 | if (allow && chown(parent_procs, test_euid, -1)) |
113 | goto cleanup; |
114 | |
115 | /* Fork a privileged child as a test object */ |
116 | object_pid = cg_run_nowait(cgroup: child_src, fn: idle_process_fn, NULL); |
117 | if (object_pid < 0) |
118 | goto cleanup; |
119 | |
120 | /* Carry out migration in a child process that can drop all privileges |
121 | * (including capabilities), the main process must remain privileged for |
122 | * cleanup. |
123 | * Child process's cgroup is irrelevant but we place it into child_dst |
124 | * as hacky way to pass information about migration target to the child. |
125 | */ |
126 | if (allow ^ (cg_run(child_dst, do_migration_fn, (void *)(size_t)object_pid) == EXIT_SUCCESS)) |
127 | goto cleanup; |
128 | |
129 | ret = KSFT_PASS; |
130 | |
131 | cleanup: |
132 | if (object_pid > 0) { |
133 | (void)kill(object_pid, SIGTERM); |
134 | (void)clone_reap(object_pid, WEXITED); |
135 | } |
136 | |
137 | cg_destroy(cgroup: child_dst); |
138 | free(child_dst_procs); |
139 | free(child_dst); |
140 | |
141 | cg_destroy(cgroup: child_src); |
142 | free(child_src_procs); |
143 | free(child_src); |
144 | |
145 | cg_destroy(cgroup: parent); |
146 | free(parent_procs); |
147 | free(parent); |
148 | |
149 | return ret; |
150 | } |
151 | |
152 | static int test_cpuset_perms_object_allow(const char *root) |
153 | { |
154 | return test_cpuset_perms_object(root, allow: true); |
155 | } |
156 | |
157 | static int test_cpuset_perms_object_deny(const char *root) |
158 | { |
159 | return test_cpuset_perms_object(root, allow: false); |
160 | } |
161 | |
162 | /* |
163 | * Migrate a process between parent and child implicitely |
164 | * Implicit migration happens when a controller is enabled/disabled. |
165 | * |
166 | */ |
167 | static int test_cpuset_perms_subtree(const char *root) |
168 | { |
169 | char *parent = NULL, *child = NULL; |
170 | char *parent_procs = NULL, *parent_subctl = NULL, *child_procs = NULL; |
171 | const uid_t test_euid = TEST_UID; |
172 | int object_pid = 0; |
173 | int ret = KSFT_FAIL; |
174 | |
175 | parent = cg_name(root, name: "cpuset_test_0" ); |
176 | if (!parent) |
177 | goto cleanup; |
178 | parent_procs = cg_name(root: parent, name: "cgroup.procs" ); |
179 | if (!parent_procs) |
180 | goto cleanup; |
181 | parent_subctl = cg_name(root: parent, name: "cgroup.subtree_control" ); |
182 | if (!parent_subctl) |
183 | goto cleanup; |
184 | if (cg_create(cgroup: parent)) |
185 | goto cleanup; |
186 | |
187 | child = cg_name(root: parent, name: "cpuset_test_1" ); |
188 | if (!child) |
189 | goto cleanup; |
190 | child_procs = cg_name(root: child, name: "cgroup.procs" ); |
191 | if (!child_procs) |
192 | goto cleanup; |
193 | if (cg_create(cgroup: child)) |
194 | goto cleanup; |
195 | |
196 | /* Enable permissions as in a delegated subtree */ |
197 | if (chown(parent_procs, test_euid, -1) || |
198 | chown(parent_subctl, test_euid, -1) || |
199 | chown(child_procs, test_euid, -1)) |
200 | goto cleanup; |
201 | |
202 | /* Put a privileged child in the subtree and modify controller state |
203 | * from an unprivileged process, the main process remains privileged |
204 | * for cleanup. |
205 | * The unprivileged child runs in subtree too to avoid parent and |
206 | * internal-node constraing violation. |
207 | */ |
208 | object_pid = cg_run_nowait(cgroup: child, fn: idle_process_fn, NULL); |
209 | if (object_pid < 0) |
210 | goto cleanup; |
211 | |
212 | if (cg_run(cgroup: child, fn: do_controller_fn, arg: parent) != EXIT_SUCCESS) |
213 | goto cleanup; |
214 | |
215 | ret = KSFT_PASS; |
216 | |
217 | cleanup: |
218 | if (object_pid > 0) { |
219 | (void)kill(object_pid, SIGTERM); |
220 | (void)clone_reap(object_pid, WEXITED); |
221 | } |
222 | |
223 | cg_destroy(cgroup: child); |
224 | free(child_procs); |
225 | free(child); |
226 | |
227 | cg_destroy(cgroup: parent); |
228 | free(parent_subctl); |
229 | free(parent_procs); |
230 | free(parent); |
231 | |
232 | return ret; |
233 | } |
234 | |
235 | |
236 | #define T(x) { x, #x } |
237 | struct cpuset_test { |
238 | int (*fn)(const char *root); |
239 | const char *name; |
240 | } tests[] = { |
241 | T(test_cpuset_perms_object_allow), |
242 | T(test_cpuset_perms_object_deny), |
243 | T(test_cpuset_perms_subtree), |
244 | }; |
245 | #undef T |
246 | |
247 | int main(int argc, char *argv[]) |
248 | { |
249 | char root[PATH_MAX]; |
250 | int i, ret = EXIT_SUCCESS; |
251 | |
252 | if (cg_find_unified_root(root, len: sizeof(root))) |
253 | ksft_exit_skip(msg: "cgroup v2 isn't mounted\n" ); |
254 | |
255 | if (cg_read_strstr(cgroup: root, control: "cgroup.subtree_control" , needle: "cpuset" )) |
256 | if (cg_write(cgroup: root, control: "cgroup.subtree_control" , buf: "+cpuset" )) |
257 | ksft_exit_skip(msg: "Failed to set cpuset controller\n" ); |
258 | |
259 | for (i = 0; i < ARRAY_SIZE(tests); i++) { |
260 | switch (tests[i].fn(root)) { |
261 | case KSFT_PASS: |
262 | ksft_test_result_pass(msg: "%s\n" , tests[i].name); |
263 | break; |
264 | case KSFT_SKIP: |
265 | ksft_test_result_skip(msg: "%s\n" , tests[i].name); |
266 | break; |
267 | default: |
268 | ret = EXIT_FAILURE; |
269 | ksft_test_result_fail(msg: "%s\n" , tests[i].name); |
270 | break; |
271 | } |
272 | } |
273 | |
274 | return ret; |
275 | } |
276 | |