1 | /* SPDX-License-Identifier: GPL-2.0 */ |
2 | |
3 | #define _GNU_SOURCE |
4 | #include <linux/limits.h> |
5 | #include <linux/sched.h> |
6 | #include <sys/types.h> |
7 | #include <sys/mman.h> |
8 | #include <sys/wait.h> |
9 | #include <unistd.h> |
10 | #include <fcntl.h> |
11 | #include <sched.h> |
12 | #include <stdio.h> |
13 | #include <errno.h> |
14 | #include <signal.h> |
15 | #include <string.h> |
16 | #include <pthread.h> |
17 | |
18 | #include "../kselftest.h" |
19 | #include "cgroup_util.h" |
20 | |
21 | static int touch_anon(char *buf, size_t size) |
22 | { |
23 | int fd; |
24 | char *pos = buf; |
25 | |
26 | fd = open("/dev/urandom" , O_RDONLY); |
27 | if (fd < 0) |
28 | return -1; |
29 | |
30 | while (size > 0) { |
31 | ssize_t ret = read(fd, pos, size); |
32 | |
33 | if (ret < 0) { |
34 | if (errno != EINTR) { |
35 | close(fd); |
36 | return -1; |
37 | } |
38 | } else { |
39 | pos += ret; |
40 | size -= ret; |
41 | } |
42 | } |
43 | close(fd); |
44 | |
45 | return 0; |
46 | } |
47 | |
48 | static int alloc_and_touch_anon_noexit(const char *cgroup, void *arg) |
49 | { |
50 | int ppid = getppid(); |
51 | size_t size = (size_t)arg; |
52 | void *buf; |
53 | |
54 | buf = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, |
55 | 0, 0); |
56 | if (buf == MAP_FAILED) |
57 | return -1; |
58 | |
59 | if (touch_anon(buf: (char *)buf, size)) { |
60 | munmap(buf, size); |
61 | return -1; |
62 | } |
63 | |
64 | while (getppid() == ppid) |
65 | sleep(1); |
66 | |
67 | munmap(buf, size); |
68 | return 0; |
69 | } |
70 | |
71 | /* |
72 | * Create a child process that allocates and touches 100MB, then waits to be |
73 | * killed. Wait until the child is attached to the cgroup, kill all processes |
74 | * in that cgroup and wait until "cgroup.procs" is empty. At this point try to |
75 | * destroy the empty cgroup. The test helps detect race conditions between |
76 | * dying processes leaving the cgroup and cgroup destruction path. |
77 | */ |
78 | static int test_cgcore_destroy(const char *root) |
79 | { |
80 | int ret = KSFT_FAIL; |
81 | char *cg_test = NULL; |
82 | int child_pid; |
83 | char buf[PAGE_SIZE]; |
84 | |
85 | cg_test = cg_name(root, name: "cg_test" ); |
86 | |
87 | if (!cg_test) |
88 | goto cleanup; |
89 | |
90 | for (int i = 0; i < 10; i++) { |
91 | if (cg_create(cgroup: cg_test)) |
92 | goto cleanup; |
93 | |
94 | child_pid = cg_run_nowait(cgroup: cg_test, fn: alloc_and_touch_anon_noexit, |
95 | arg: (void *) MB(100)); |
96 | |
97 | if (child_pid < 0) |
98 | goto cleanup; |
99 | |
100 | /* wait for the child to enter cgroup */ |
101 | if (cg_wait_for_proc_count(cgroup: cg_test, count: 1)) |
102 | goto cleanup; |
103 | |
104 | if (cg_killall(cgroup: cg_test)) |
105 | goto cleanup; |
106 | |
107 | /* wait for cgroup to be empty */ |
108 | while (1) { |
109 | if (cg_read(cgroup: cg_test, control: "cgroup.procs" , buf, len: sizeof(buf))) |
110 | goto cleanup; |
111 | if (buf[0] == '\0') |
112 | break; |
113 | usleep(1000); |
114 | } |
115 | |
116 | if (rmdir(cg_test)) |
117 | goto cleanup; |
118 | |
119 | if (waitpid(child_pid, NULL, 0) < 0) |
120 | goto cleanup; |
121 | } |
122 | ret = KSFT_PASS; |
123 | cleanup: |
124 | if (cg_test) |
125 | cg_destroy(cgroup: cg_test); |
126 | free(cg_test); |
127 | return ret; |
128 | } |
129 | |
130 | /* |
131 | * A(0) - B(0) - C(1) |
132 | * \ D(0) |
133 | * |
134 | * A, B and C's "populated" fields would be 1 while D's 0. |
135 | * test that after the one process in C is moved to root, |
136 | * A,B and C's "populated" fields would flip to "0" and file |
137 | * modified events will be generated on the |
138 | * "cgroup.events" files of both cgroups. |
139 | */ |
140 | static int test_cgcore_populated(const char *root) |
141 | { |
142 | int ret = KSFT_FAIL; |
143 | int err; |
144 | char *cg_test_a = NULL, *cg_test_b = NULL; |
145 | char *cg_test_c = NULL, *cg_test_d = NULL; |
146 | int cgroup_fd = -EBADF; |
147 | pid_t pid; |
148 | |
149 | cg_test_a = cg_name(root, name: "cg_test_a" ); |
150 | cg_test_b = cg_name(root, name: "cg_test_a/cg_test_b" ); |
151 | cg_test_c = cg_name(root, name: "cg_test_a/cg_test_b/cg_test_c" ); |
152 | cg_test_d = cg_name(root, name: "cg_test_a/cg_test_b/cg_test_d" ); |
153 | |
154 | if (!cg_test_a || !cg_test_b || !cg_test_c || !cg_test_d) |
155 | goto cleanup; |
156 | |
157 | if (cg_create(cgroup: cg_test_a)) |
158 | goto cleanup; |
159 | |
160 | if (cg_create(cgroup: cg_test_b)) |
161 | goto cleanup; |
162 | |
163 | if (cg_create(cgroup: cg_test_c)) |
164 | goto cleanup; |
165 | |
166 | if (cg_create(cgroup: cg_test_d)) |
167 | goto cleanup; |
168 | |
169 | if (cg_enter_current(cgroup: cg_test_c)) |
170 | goto cleanup; |
171 | |
172 | if (cg_read_strcmp(cgroup: cg_test_a, control: "cgroup.events" , expected: "populated 1\n" )) |
173 | goto cleanup; |
174 | |
175 | if (cg_read_strcmp(cgroup: cg_test_b, control: "cgroup.events" , expected: "populated 1\n" )) |
176 | goto cleanup; |
177 | |
178 | if (cg_read_strcmp(cgroup: cg_test_c, control: "cgroup.events" , expected: "populated 1\n" )) |
179 | goto cleanup; |
180 | |
181 | if (cg_read_strcmp(cgroup: cg_test_d, control: "cgroup.events" , expected: "populated 0\n" )) |
182 | goto cleanup; |
183 | |
184 | if (cg_enter_current(cgroup: root)) |
185 | goto cleanup; |
186 | |
187 | if (cg_read_strcmp(cgroup: cg_test_a, control: "cgroup.events" , expected: "populated 0\n" )) |
188 | goto cleanup; |
189 | |
190 | if (cg_read_strcmp(cgroup: cg_test_b, control: "cgroup.events" , expected: "populated 0\n" )) |
191 | goto cleanup; |
192 | |
193 | if (cg_read_strcmp(cgroup: cg_test_c, control: "cgroup.events" , expected: "populated 0\n" )) |
194 | goto cleanup; |
195 | |
196 | if (cg_read_strcmp(cgroup: cg_test_d, control: "cgroup.events" , expected: "populated 0\n" )) |
197 | goto cleanup; |
198 | |
199 | /* Test that we can directly clone into a new cgroup. */ |
200 | cgroup_fd = dirfd_open_opath(dir: cg_test_d); |
201 | if (cgroup_fd < 0) |
202 | goto cleanup; |
203 | |
204 | pid = clone_into_cgroup(cgroup_fd); |
205 | if (pid < 0) { |
206 | if (errno == ENOSYS) |
207 | goto cleanup_pass; |
208 | goto cleanup; |
209 | } |
210 | |
211 | if (pid == 0) { |
212 | if (raise(SIGSTOP)) |
213 | exit(EXIT_FAILURE); |
214 | exit(EXIT_SUCCESS); |
215 | } |
216 | |
217 | err = cg_read_strcmp(cgroup: cg_test_d, control: "cgroup.events" , expected: "populated 1\n" ); |
218 | |
219 | (void)clone_reap(pid, WSTOPPED); |
220 | (void)kill(pid, SIGCONT); |
221 | (void)clone_reap(pid, WEXITED); |
222 | |
223 | if (err) |
224 | goto cleanup; |
225 | |
226 | if (cg_read_strcmp(cgroup: cg_test_d, control: "cgroup.events" , expected: "populated 0\n" )) |
227 | goto cleanup; |
228 | |
229 | /* Remove cgroup. */ |
230 | if (cg_test_d) { |
231 | cg_destroy(cgroup: cg_test_d); |
232 | free(cg_test_d); |
233 | cg_test_d = NULL; |
234 | } |
235 | |
236 | pid = clone_into_cgroup(cgroup_fd); |
237 | if (pid < 0) |
238 | goto cleanup_pass; |
239 | if (pid == 0) |
240 | exit(EXIT_SUCCESS); |
241 | (void)clone_reap(pid, WEXITED); |
242 | goto cleanup; |
243 | |
244 | cleanup_pass: |
245 | ret = KSFT_PASS; |
246 | |
247 | cleanup: |
248 | if (cg_test_d) |
249 | cg_destroy(cgroup: cg_test_d); |
250 | if (cg_test_c) |
251 | cg_destroy(cgroup: cg_test_c); |
252 | if (cg_test_b) |
253 | cg_destroy(cgroup: cg_test_b); |
254 | if (cg_test_a) |
255 | cg_destroy(cgroup: cg_test_a); |
256 | free(cg_test_d); |
257 | free(cg_test_c); |
258 | free(cg_test_b); |
259 | free(cg_test_a); |
260 | if (cgroup_fd >= 0) |
261 | close(cgroup_fd); |
262 | return ret; |
263 | } |
264 | |
265 | /* |
266 | * A (domain threaded) - B (threaded) - C (domain) |
267 | * |
268 | * test that C can't be used until it is turned into a |
269 | * threaded cgroup. "cgroup.type" file will report "domain (invalid)" in |
270 | * these cases. Operations which fail due to invalid topology use |
271 | * EOPNOTSUPP as the errno. |
272 | */ |
273 | static int test_cgcore_invalid_domain(const char *root) |
274 | { |
275 | int ret = KSFT_FAIL; |
276 | char *grandparent = NULL, *parent = NULL, *child = NULL; |
277 | |
278 | grandparent = cg_name(root, name: "cg_test_grandparent" ); |
279 | parent = cg_name(root, name: "cg_test_grandparent/cg_test_parent" ); |
280 | child = cg_name(root, name: "cg_test_grandparent/cg_test_parent/cg_test_child" ); |
281 | if (!parent || !child || !grandparent) |
282 | goto cleanup; |
283 | |
284 | if (cg_create(cgroup: grandparent)) |
285 | goto cleanup; |
286 | |
287 | if (cg_create(cgroup: parent)) |
288 | goto cleanup; |
289 | |
290 | if (cg_create(cgroup: child)) |
291 | goto cleanup; |
292 | |
293 | if (cg_write(cgroup: parent, control: "cgroup.type" , buf: "threaded" )) |
294 | goto cleanup; |
295 | |
296 | if (cg_read_strcmp(cgroup: child, control: "cgroup.type" , expected: "domain invalid\n" )) |
297 | goto cleanup; |
298 | |
299 | if (!cg_enter_current(cgroup: child)) |
300 | goto cleanup; |
301 | |
302 | if (errno != EOPNOTSUPP) |
303 | goto cleanup; |
304 | |
305 | if (!clone_into_cgroup_run_wait(cgroup: child)) |
306 | goto cleanup; |
307 | |
308 | if (errno == ENOSYS) |
309 | goto cleanup_pass; |
310 | |
311 | if (errno != EOPNOTSUPP) |
312 | goto cleanup; |
313 | |
314 | cleanup_pass: |
315 | ret = KSFT_PASS; |
316 | |
317 | cleanup: |
318 | cg_enter_current(cgroup: root); |
319 | if (child) |
320 | cg_destroy(cgroup: child); |
321 | if (parent) |
322 | cg_destroy(cgroup: parent); |
323 | if (grandparent) |
324 | cg_destroy(cgroup: grandparent); |
325 | free(child); |
326 | free(parent); |
327 | free(grandparent); |
328 | return ret; |
329 | } |
330 | |
331 | /* |
332 | * Test that when a child becomes threaded |
333 | * the parent type becomes domain threaded. |
334 | */ |
335 | static int test_cgcore_parent_becomes_threaded(const char *root) |
336 | { |
337 | int ret = KSFT_FAIL; |
338 | char *parent = NULL, *child = NULL; |
339 | |
340 | parent = cg_name(root, name: "cg_test_parent" ); |
341 | child = cg_name(root, name: "cg_test_parent/cg_test_child" ); |
342 | if (!parent || !child) |
343 | goto cleanup; |
344 | |
345 | if (cg_create(cgroup: parent)) |
346 | goto cleanup; |
347 | |
348 | if (cg_create(cgroup: child)) |
349 | goto cleanup; |
350 | |
351 | if (cg_write(cgroup: child, control: "cgroup.type" , buf: "threaded" )) |
352 | goto cleanup; |
353 | |
354 | if (cg_read_strcmp(cgroup: parent, control: "cgroup.type" , expected: "domain threaded\n" )) |
355 | goto cleanup; |
356 | |
357 | ret = KSFT_PASS; |
358 | |
359 | cleanup: |
360 | if (child) |
361 | cg_destroy(cgroup: child); |
362 | if (parent) |
363 | cg_destroy(cgroup: parent); |
364 | free(child); |
365 | free(parent); |
366 | return ret; |
367 | |
368 | } |
369 | |
370 | /* |
371 | * Test that there's no internal process constrain on threaded cgroups. |
372 | * You can add threads/processes on a parent with a controller enabled. |
373 | */ |
374 | static int test_cgcore_no_internal_process_constraint_on_threads(const char *root) |
375 | { |
376 | int ret = KSFT_FAIL; |
377 | char *parent = NULL, *child = NULL; |
378 | |
379 | if (cg_read_strstr(cgroup: root, control: "cgroup.controllers" , needle: "cpu" ) || |
380 | cg_write(cgroup: root, control: "cgroup.subtree_control" , buf: "+cpu" )) { |
381 | ret = KSFT_SKIP; |
382 | goto cleanup; |
383 | } |
384 | |
385 | parent = cg_name(root, name: "cg_test_parent" ); |
386 | child = cg_name(root, name: "cg_test_parent/cg_test_child" ); |
387 | if (!parent || !child) |
388 | goto cleanup; |
389 | |
390 | if (cg_create(cgroup: parent)) |
391 | goto cleanup; |
392 | |
393 | if (cg_create(cgroup: child)) |
394 | goto cleanup; |
395 | |
396 | if (cg_write(cgroup: parent, control: "cgroup.type" , buf: "threaded" )) |
397 | goto cleanup; |
398 | |
399 | if (cg_write(cgroup: child, control: "cgroup.type" , buf: "threaded" )) |
400 | goto cleanup; |
401 | |
402 | if (cg_write(cgroup: parent, control: "cgroup.subtree_control" , buf: "+cpu" )) |
403 | goto cleanup; |
404 | |
405 | if (cg_enter_current(cgroup: parent)) |
406 | goto cleanup; |
407 | |
408 | ret = KSFT_PASS; |
409 | |
410 | cleanup: |
411 | cg_enter_current(cgroup: root); |
412 | cg_enter_current(cgroup: root); |
413 | if (child) |
414 | cg_destroy(cgroup: child); |
415 | if (parent) |
416 | cg_destroy(cgroup: parent); |
417 | free(child); |
418 | free(parent); |
419 | return ret; |
420 | } |
421 | |
422 | /* |
423 | * Test that you can't enable a controller on a child if it's not enabled |
424 | * on the parent. |
425 | */ |
426 | static int test_cgcore_top_down_constraint_enable(const char *root) |
427 | { |
428 | int ret = KSFT_FAIL; |
429 | char *parent = NULL, *child = NULL; |
430 | |
431 | parent = cg_name(root, name: "cg_test_parent" ); |
432 | child = cg_name(root, name: "cg_test_parent/cg_test_child" ); |
433 | if (!parent || !child) |
434 | goto cleanup; |
435 | |
436 | if (cg_create(cgroup: parent)) |
437 | goto cleanup; |
438 | |
439 | if (cg_create(cgroup: child)) |
440 | goto cleanup; |
441 | |
442 | if (!cg_write(cgroup: child, control: "cgroup.subtree_control" , buf: "+memory" )) |
443 | goto cleanup; |
444 | |
445 | ret = KSFT_PASS; |
446 | |
447 | cleanup: |
448 | if (child) |
449 | cg_destroy(cgroup: child); |
450 | if (parent) |
451 | cg_destroy(cgroup: parent); |
452 | free(child); |
453 | free(parent); |
454 | return ret; |
455 | } |
456 | |
457 | /* |
458 | * Test that you can't disable a controller on a parent |
459 | * if it's enabled in a child. |
460 | */ |
461 | static int test_cgcore_top_down_constraint_disable(const char *root) |
462 | { |
463 | int ret = KSFT_FAIL; |
464 | char *parent = NULL, *child = NULL; |
465 | |
466 | parent = cg_name(root, name: "cg_test_parent" ); |
467 | child = cg_name(root, name: "cg_test_parent/cg_test_child" ); |
468 | if (!parent || !child) |
469 | goto cleanup; |
470 | |
471 | if (cg_create(cgroup: parent)) |
472 | goto cleanup; |
473 | |
474 | if (cg_create(cgroup: child)) |
475 | goto cleanup; |
476 | |
477 | if (cg_write(cgroup: parent, control: "cgroup.subtree_control" , buf: "+memory" )) |
478 | goto cleanup; |
479 | |
480 | if (cg_write(cgroup: child, control: "cgroup.subtree_control" , buf: "+memory" )) |
481 | goto cleanup; |
482 | |
483 | if (!cg_write(cgroup: parent, control: "cgroup.subtree_control" , buf: "-memory" )) |
484 | goto cleanup; |
485 | |
486 | ret = KSFT_PASS; |
487 | |
488 | cleanup: |
489 | if (child) |
490 | cg_destroy(cgroup: child); |
491 | if (parent) |
492 | cg_destroy(cgroup: parent); |
493 | free(child); |
494 | free(parent); |
495 | return ret; |
496 | } |
497 | |
498 | /* |
499 | * Test internal process constraint. |
500 | * You can't add a pid to a domain parent if a controller is enabled. |
501 | */ |
502 | static int test_cgcore_internal_process_constraint(const char *root) |
503 | { |
504 | int ret = KSFT_FAIL; |
505 | char *parent = NULL, *child = NULL; |
506 | |
507 | parent = cg_name(root, name: "cg_test_parent" ); |
508 | child = cg_name(root, name: "cg_test_parent/cg_test_child" ); |
509 | if (!parent || !child) |
510 | goto cleanup; |
511 | |
512 | if (cg_create(cgroup: parent)) |
513 | goto cleanup; |
514 | |
515 | if (cg_create(cgroup: child)) |
516 | goto cleanup; |
517 | |
518 | if (cg_write(cgroup: parent, control: "cgroup.subtree_control" , buf: "+memory" )) |
519 | goto cleanup; |
520 | |
521 | if (!cg_enter_current(cgroup: parent)) |
522 | goto cleanup; |
523 | |
524 | if (!clone_into_cgroup_run_wait(cgroup: parent)) |
525 | goto cleanup; |
526 | |
527 | ret = KSFT_PASS; |
528 | |
529 | cleanup: |
530 | if (child) |
531 | cg_destroy(cgroup: child); |
532 | if (parent) |
533 | cg_destroy(cgroup: parent); |
534 | free(child); |
535 | free(parent); |
536 | return ret; |
537 | } |
538 | |
539 | static void *dummy_thread_fn(void *arg) |
540 | { |
541 | return (void *)(size_t)pause(); |
542 | } |
543 | |
544 | /* |
545 | * Test threadgroup migration. |
546 | * All threads of a process are migrated together. |
547 | */ |
548 | static int test_cgcore_proc_migration(const char *root) |
549 | { |
550 | int ret = KSFT_FAIL; |
551 | int t, c_threads = 0, n_threads = 13; |
552 | char *src = NULL, *dst = NULL; |
553 | pthread_t threads[n_threads]; |
554 | |
555 | src = cg_name(root, name: "cg_src" ); |
556 | dst = cg_name(root, name: "cg_dst" ); |
557 | if (!src || !dst) |
558 | goto cleanup; |
559 | |
560 | if (cg_create(cgroup: src)) |
561 | goto cleanup; |
562 | if (cg_create(cgroup: dst)) |
563 | goto cleanup; |
564 | |
565 | if (cg_enter_current(cgroup: src)) |
566 | goto cleanup; |
567 | |
568 | for (c_threads = 0; c_threads < n_threads; ++c_threads) { |
569 | if (pthread_create(&threads[c_threads], NULL, dummy_thread_fn, NULL)) |
570 | goto cleanup; |
571 | } |
572 | |
573 | cg_enter_current(cgroup: dst); |
574 | if (cg_read_lc(cgroup: dst, control: "cgroup.threads" ) != n_threads + 1) |
575 | goto cleanup; |
576 | |
577 | ret = KSFT_PASS; |
578 | |
579 | cleanup: |
580 | for (t = 0; t < c_threads; ++t) { |
581 | pthread_cancel(threads[t]); |
582 | } |
583 | |
584 | for (t = 0; t < c_threads; ++t) { |
585 | pthread_join(threads[t], NULL); |
586 | } |
587 | |
588 | cg_enter_current(cgroup: root); |
589 | |
590 | if (dst) |
591 | cg_destroy(cgroup: dst); |
592 | if (src) |
593 | cg_destroy(cgroup: src); |
594 | free(dst); |
595 | free(src); |
596 | return ret; |
597 | } |
598 | |
599 | static void *migrating_thread_fn(void *arg) |
600 | { |
601 | int g, i, n_iterations = 1000; |
602 | char **grps = arg; |
603 | char lines[3][PATH_MAX]; |
604 | |
605 | for (g = 1; g < 3; ++g) |
606 | snprintf(buf: lines[g], size: sizeof(lines[g]), fmt: "0::%s" , grps[g] + strlen(grps[0])); |
607 | |
608 | for (i = 0; i < n_iterations; ++i) { |
609 | cg_enter_current_thread(cgroup: grps[(i % 2) + 1]); |
610 | |
611 | if (proc_read_strstr(pid: 0, thread: 1, item: "cgroup" , needle: lines[(i % 2) + 1])) |
612 | return (void *)-1; |
613 | } |
614 | return NULL; |
615 | } |
616 | |
617 | /* |
618 | * Test single thread migration. |
619 | * Threaded cgroups allow successful migration of a thread. |
620 | */ |
621 | static int test_cgcore_thread_migration(const char *root) |
622 | { |
623 | int ret = KSFT_FAIL; |
624 | char *dom = NULL; |
625 | char line[PATH_MAX]; |
626 | char *grps[3] = { (char *)root, NULL, NULL }; |
627 | pthread_t thr; |
628 | void *retval; |
629 | |
630 | dom = cg_name(root, name: "cg_dom" ); |
631 | grps[1] = cg_name(root, name: "cg_dom/cg_src" ); |
632 | grps[2] = cg_name(root, name: "cg_dom/cg_dst" ); |
633 | if (!grps[1] || !grps[2] || !dom) |
634 | goto cleanup; |
635 | |
636 | if (cg_create(cgroup: dom)) |
637 | goto cleanup; |
638 | if (cg_create(cgroup: grps[1])) |
639 | goto cleanup; |
640 | if (cg_create(cgroup: grps[2])) |
641 | goto cleanup; |
642 | |
643 | if (cg_write(cgroup: grps[1], control: "cgroup.type" , buf: "threaded" )) |
644 | goto cleanup; |
645 | if (cg_write(cgroup: grps[2], control: "cgroup.type" , buf: "threaded" )) |
646 | goto cleanup; |
647 | |
648 | if (cg_enter_current(cgroup: grps[1])) |
649 | goto cleanup; |
650 | |
651 | if (pthread_create(&thr, NULL, migrating_thread_fn, grps)) |
652 | goto cleanup; |
653 | |
654 | if (pthread_join(thr, &retval)) |
655 | goto cleanup; |
656 | |
657 | if (retval) |
658 | goto cleanup; |
659 | |
660 | snprintf(buf: line, size: sizeof(line), fmt: "0::%s" , grps[1] + strlen(grps[0])); |
661 | if (proc_read_strstr(pid: 0, thread: 1, item: "cgroup" , needle: line)) |
662 | goto cleanup; |
663 | |
664 | ret = KSFT_PASS; |
665 | |
666 | cleanup: |
667 | cg_enter_current(cgroup: root); |
668 | if (grps[2]) |
669 | cg_destroy(cgroup: grps[2]); |
670 | if (grps[1]) |
671 | cg_destroy(cgroup: grps[1]); |
672 | if (dom) |
673 | cg_destroy(cgroup: dom); |
674 | free(grps[2]); |
675 | free(grps[1]); |
676 | free(dom); |
677 | return ret; |
678 | } |
679 | |
680 | /* |
681 | * cgroup migration permission check should be performed based on the |
682 | * credentials at the time of open instead of write. |
683 | */ |
684 | static int test_cgcore_lesser_euid_open(const char *root) |
685 | { |
686 | const uid_t test_euid = TEST_UID; |
687 | int ret = KSFT_FAIL; |
688 | char *cg_test_a = NULL, *cg_test_b = NULL; |
689 | char *cg_test_a_procs = NULL, *cg_test_b_procs = NULL; |
690 | int cg_test_b_procs_fd = -1; |
691 | uid_t saved_uid; |
692 | |
693 | cg_test_a = cg_name(root, name: "cg_test_a" ); |
694 | cg_test_b = cg_name(root, name: "cg_test_b" ); |
695 | |
696 | if (!cg_test_a || !cg_test_b) |
697 | goto cleanup; |
698 | |
699 | cg_test_a_procs = cg_name(root: cg_test_a, name: "cgroup.procs" ); |
700 | cg_test_b_procs = cg_name(root: cg_test_b, name: "cgroup.procs" ); |
701 | |
702 | if (!cg_test_a_procs || !cg_test_b_procs) |
703 | goto cleanup; |
704 | |
705 | if (cg_create(cgroup: cg_test_a) || cg_create(cgroup: cg_test_b)) |
706 | goto cleanup; |
707 | |
708 | if (cg_enter_current(cgroup: cg_test_a)) |
709 | goto cleanup; |
710 | |
711 | if (chown(cg_test_a_procs, test_euid, -1) || |
712 | chown(cg_test_b_procs, test_euid, -1)) |
713 | goto cleanup; |
714 | |
715 | saved_uid = geteuid(); |
716 | if (seteuid(test_euid)) |
717 | goto cleanup; |
718 | |
719 | cg_test_b_procs_fd = open(cg_test_b_procs, O_RDWR); |
720 | |
721 | if (seteuid(saved_uid)) |
722 | goto cleanup; |
723 | |
724 | if (cg_test_b_procs_fd < 0) |
725 | goto cleanup; |
726 | |
727 | if (write(cg_test_b_procs_fd, "0" , 1) >= 0 || errno != EACCES) |
728 | goto cleanup; |
729 | |
730 | ret = KSFT_PASS; |
731 | |
732 | cleanup: |
733 | cg_enter_current(cgroup: root); |
734 | if (cg_test_b_procs_fd >= 0) |
735 | close(cg_test_b_procs_fd); |
736 | if (cg_test_b) |
737 | cg_destroy(cgroup: cg_test_b); |
738 | if (cg_test_a) |
739 | cg_destroy(cgroup: cg_test_a); |
740 | free(cg_test_b_procs); |
741 | free(cg_test_a_procs); |
742 | free(cg_test_b); |
743 | free(cg_test_a); |
744 | return ret; |
745 | } |
746 | |
747 | struct lesser_ns_open_thread_arg { |
748 | const char *path; |
749 | int fd; |
750 | int err; |
751 | }; |
752 | |
753 | static int lesser_ns_open_thread_fn(void *arg) |
754 | { |
755 | struct lesser_ns_open_thread_arg *targ = arg; |
756 | |
757 | targ->fd = open(targ->path, O_RDWR); |
758 | targ->err = errno; |
759 | return 0; |
760 | } |
761 | |
762 | /* |
763 | * cgroup migration permission check should be performed based on the cgroup |
764 | * namespace at the time of open instead of write. |
765 | */ |
766 | static int test_cgcore_lesser_ns_open(const char *root) |
767 | { |
768 | static char stack[65536]; |
769 | const uid_t test_euid = 65534; /* usually nobody, any !root is fine */ |
770 | int ret = KSFT_FAIL; |
771 | char *cg_test_a = NULL, *cg_test_b = NULL; |
772 | char *cg_test_a_procs = NULL, *cg_test_b_procs = NULL; |
773 | int cg_test_b_procs_fd = -1; |
774 | struct lesser_ns_open_thread_arg targ = { .fd = -1 }; |
775 | pid_t pid; |
776 | int status; |
777 | |
778 | cg_test_a = cg_name(root, name: "cg_test_a" ); |
779 | cg_test_b = cg_name(root, name: "cg_test_b" ); |
780 | |
781 | if (!cg_test_a || !cg_test_b) |
782 | goto cleanup; |
783 | |
784 | cg_test_a_procs = cg_name(root: cg_test_a, name: "cgroup.procs" ); |
785 | cg_test_b_procs = cg_name(root: cg_test_b, name: "cgroup.procs" ); |
786 | |
787 | if (!cg_test_a_procs || !cg_test_b_procs) |
788 | goto cleanup; |
789 | |
790 | if (cg_create(cgroup: cg_test_a) || cg_create(cgroup: cg_test_b)) |
791 | goto cleanup; |
792 | |
793 | if (cg_enter_current(cgroup: cg_test_b)) |
794 | goto cleanup; |
795 | |
796 | if (chown(cg_test_a_procs, test_euid, -1) || |
797 | chown(cg_test_b_procs, test_euid, -1)) |
798 | goto cleanup; |
799 | |
800 | targ.path = cg_test_b_procs; |
801 | pid = clone(lesser_ns_open_thread_fn, stack + sizeof(stack), |
802 | CLONE_NEWCGROUP | CLONE_FILES | CLONE_VM | SIGCHLD, |
803 | &targ); |
804 | if (pid < 0) |
805 | goto cleanup; |
806 | |
807 | if (waitpid(pid, &status, 0) < 0) |
808 | goto cleanup; |
809 | |
810 | if (!WIFEXITED(status)) |
811 | goto cleanup; |
812 | |
813 | cg_test_b_procs_fd = targ.fd; |
814 | if (cg_test_b_procs_fd < 0) |
815 | goto cleanup; |
816 | |
817 | if (cg_enter_current(cgroup: cg_test_a)) |
818 | goto cleanup; |
819 | |
820 | if ((status = write(cg_test_b_procs_fd, "0" , 1)) >= 0 || errno != ENOENT) |
821 | goto cleanup; |
822 | |
823 | ret = KSFT_PASS; |
824 | |
825 | cleanup: |
826 | cg_enter_current(cgroup: root); |
827 | if (cg_test_b_procs_fd >= 0) |
828 | close(cg_test_b_procs_fd); |
829 | if (cg_test_b) |
830 | cg_destroy(cgroup: cg_test_b); |
831 | if (cg_test_a) |
832 | cg_destroy(cgroup: cg_test_a); |
833 | free(cg_test_b_procs); |
834 | free(cg_test_a_procs); |
835 | free(cg_test_b); |
836 | free(cg_test_a); |
837 | return ret; |
838 | } |
839 | |
840 | #define T(x) { x, #x } |
841 | struct corecg_test { |
842 | int (*fn)(const char *root); |
843 | const char *name; |
844 | } tests[] = { |
845 | T(test_cgcore_internal_process_constraint), |
846 | T(test_cgcore_top_down_constraint_enable), |
847 | T(test_cgcore_top_down_constraint_disable), |
848 | T(test_cgcore_no_internal_process_constraint_on_threads), |
849 | T(test_cgcore_parent_becomes_threaded), |
850 | T(test_cgcore_invalid_domain), |
851 | T(test_cgcore_populated), |
852 | T(test_cgcore_proc_migration), |
853 | T(test_cgcore_thread_migration), |
854 | T(test_cgcore_destroy), |
855 | T(test_cgcore_lesser_euid_open), |
856 | T(test_cgcore_lesser_ns_open), |
857 | }; |
858 | #undef T |
859 | |
860 | int main(int argc, char *argv[]) |
861 | { |
862 | char root[PATH_MAX]; |
863 | int i, ret = EXIT_SUCCESS; |
864 | |
865 | if (cg_find_unified_root(root, len: sizeof(root))) |
866 | ksft_exit_skip(msg: "cgroup v2 isn't mounted\n" ); |
867 | |
868 | if (cg_read_strstr(cgroup: root, control: "cgroup.subtree_control" , needle: "memory" )) |
869 | if (cg_write(cgroup: root, control: "cgroup.subtree_control" , buf: "+memory" )) |
870 | ksft_exit_skip(msg: "Failed to set memory controller\n" ); |
871 | |
872 | for (i = 0; i < ARRAY_SIZE(tests); i++) { |
873 | switch (tests[i].fn(root)) { |
874 | case KSFT_PASS: |
875 | ksft_test_result_pass(msg: "%s\n" , tests[i].name); |
876 | break; |
877 | case KSFT_SKIP: |
878 | ksft_test_result_skip(msg: "%s\n" , tests[i].name); |
879 | break; |
880 | default: |
881 | ret = EXIT_FAILURE; |
882 | ksft_test_result_fail(msg: "%s\n" , tests[i].name); |
883 | break; |
884 | } |
885 | } |
886 | |
887 | return ret; |
888 | } |
889 | |