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
9static int idle_process_fn(const char *cgroup, void *arg)
10{
11 (void)pause();
12 return 0;
13}
14
15static 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
30static 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 */
65static 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
131cleanup:
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
152static int test_cpuset_perms_object_allow(const char *root)
153{
154 return test_cpuset_perms_object(root, allow: true);
155}
156
157static 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 */
167static 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
217cleanup:
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 }
237struct 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
247int 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

source code of linux/tools/testing/selftests/cgroup/test_cpuset.c