| // SPDX-License-Identifier: GPL-2.0 |
| |
| #include <linux/limits.h> |
| #include <signal.h> |
| |
| #include "../kselftest.h" |
| #include "cgroup_util.h" |
| |
| static int idle_process_fn(const char *cgroup, void *arg) |
| { |
| (void)pause(); |
| return 0; |
| } |
| |
| static int do_migration_fn(const char *cgroup, void *arg) |
| { |
| int object_pid = (int)(size_t)arg; |
| |
| if (setuid(TEST_UID)) |
| return EXIT_FAILURE; |
| |
| // XXX checking /proc/$pid/cgroup would be quicker than wait |
| if (cg_enter(cgroup, object_pid) || |
| cg_wait_for_proc_count(cgroup, 1)) |
| return EXIT_FAILURE; |
| |
| return EXIT_SUCCESS; |
| } |
| |
| static int do_controller_fn(const char *cgroup, void *arg) |
| { |
| const char *child = cgroup; |
| const char *parent = arg; |
| |
| if (setuid(TEST_UID)) |
| return EXIT_FAILURE; |
| |
| if (!cg_read_strstr(child, "cgroup.controllers", "cpuset")) |
| return EXIT_FAILURE; |
| |
| if (cg_write(parent, "cgroup.subtree_control", "+cpuset")) |
| return EXIT_FAILURE; |
| |
| if (cg_read_strstr(child, "cgroup.controllers", "cpuset")) |
| return EXIT_FAILURE; |
| |
| if (cg_write(parent, "cgroup.subtree_control", "-cpuset")) |
| return EXIT_FAILURE; |
| |
| if (!cg_read_strstr(child, "cgroup.controllers", "cpuset")) |
| return EXIT_FAILURE; |
| |
| return EXIT_SUCCESS; |
| } |
| |
| /* |
| * Migrate a process between two sibling cgroups. |
| * The success should only depend on the parent cgroup permissions and not the |
| * migrated process itself (cpuset controller is in place because it uses |
| * security_task_setscheduler() in cgroup v1). |
| * |
| * Deliberately don't set cpuset.cpus in children to avoid definining migration |
| * permissions between two different cpusets. |
| */ |
| static int test_cpuset_perms_object(const char *root, bool allow) |
| { |
| char *parent = NULL, *child_src = NULL, *child_dst = NULL; |
| char *parent_procs = NULL, *child_src_procs = NULL, *child_dst_procs = NULL; |
| const uid_t test_euid = TEST_UID; |
| int object_pid = 0; |
| int ret = KSFT_FAIL; |
| |
| parent = cg_name(root, "cpuset_test_0"); |
| if (!parent) |
| goto cleanup; |
| parent_procs = cg_name(parent, "cgroup.procs"); |
| if (!parent_procs) |
| goto cleanup; |
| if (cg_create(parent)) |
| goto cleanup; |
| |
| child_src = cg_name(parent, "cpuset_test_1"); |
| if (!child_src) |
| goto cleanup; |
| child_src_procs = cg_name(child_src, "cgroup.procs"); |
| if (!child_src_procs) |
| goto cleanup; |
| if (cg_create(child_src)) |
| goto cleanup; |
| |
| child_dst = cg_name(parent, "cpuset_test_2"); |
| if (!child_dst) |
| goto cleanup; |
| child_dst_procs = cg_name(child_dst, "cgroup.procs"); |
| if (!child_dst_procs) |
| goto cleanup; |
| if (cg_create(child_dst)) |
| goto cleanup; |
| |
| if (cg_write(parent, "cgroup.subtree_control", "+cpuset")) |
| goto cleanup; |
| |
| if (cg_read_strstr(child_src, "cgroup.controllers", "cpuset") || |
| cg_read_strstr(child_dst, "cgroup.controllers", "cpuset")) |
| goto cleanup; |
| |
| /* Enable permissions along src->dst tree path */ |
| if (chown(child_src_procs, test_euid, -1) || |
| chown(child_dst_procs, test_euid, -1)) |
| goto cleanup; |
| |
| if (allow && chown(parent_procs, test_euid, -1)) |
| goto cleanup; |
| |
| /* Fork a privileged child as a test object */ |
| object_pid = cg_run_nowait(child_src, idle_process_fn, NULL); |
| if (object_pid < 0) |
| goto cleanup; |
| |
| /* Carry out migration in a child process that can drop all privileges |
| * (including capabilities), the main process must remain privileged for |
| * cleanup. |
| * Child process's cgroup is irrelevant but we place it into child_dst |
| * as hacky way to pass information about migration target to the child. |
| */ |
| if (allow ^ (cg_run(child_dst, do_migration_fn, (void *)(size_t)object_pid) == EXIT_SUCCESS)) |
| goto cleanup; |
| |
| ret = KSFT_PASS; |
| |
| cleanup: |
| if (object_pid > 0) { |
| (void)kill(object_pid, SIGTERM); |
| (void)clone_reap(object_pid, WEXITED); |
| } |
| |
| cg_destroy(child_dst); |
| free(child_dst_procs); |
| free(child_dst); |
| |
| cg_destroy(child_src); |
| free(child_src_procs); |
| free(child_src); |
| |
| cg_destroy(parent); |
| free(parent_procs); |
| free(parent); |
| |
| return ret; |
| } |
| |
| static int test_cpuset_perms_object_allow(const char *root) |
| { |
| return test_cpuset_perms_object(root, true); |
| } |
| |
| static int test_cpuset_perms_object_deny(const char *root) |
| { |
| return test_cpuset_perms_object(root, false); |
| } |
| |
| /* |
| * Migrate a process between parent and child implicitely |
| * Implicit migration happens when a controller is enabled/disabled. |
| * |
| */ |
| static int test_cpuset_perms_subtree(const char *root) |
| { |
| char *parent = NULL, *child = NULL; |
| char *parent_procs = NULL, *parent_subctl = NULL, *child_procs = NULL; |
| const uid_t test_euid = TEST_UID; |
| int object_pid = 0; |
| int ret = KSFT_FAIL; |
| |
| parent = cg_name(root, "cpuset_test_0"); |
| if (!parent) |
| goto cleanup; |
| parent_procs = cg_name(parent, "cgroup.procs"); |
| if (!parent_procs) |
| goto cleanup; |
| parent_subctl = cg_name(parent, "cgroup.subtree_control"); |
| if (!parent_subctl) |
| goto cleanup; |
| if (cg_create(parent)) |
| goto cleanup; |
| |
| child = cg_name(parent, "cpuset_test_1"); |
| if (!child) |
| goto cleanup; |
| child_procs = cg_name(child, "cgroup.procs"); |
| if (!child_procs) |
| goto cleanup; |
| if (cg_create(child)) |
| goto cleanup; |
| |
| /* Enable permissions as in a delegated subtree */ |
| if (chown(parent_procs, test_euid, -1) || |
| chown(parent_subctl, test_euid, -1) || |
| chown(child_procs, test_euid, -1)) |
| goto cleanup; |
| |
| /* Put a privileged child in the subtree and modify controller state |
| * from an unprivileged process, the main process remains privileged |
| * for cleanup. |
| * The unprivileged child runs in subtree too to avoid parent and |
| * internal-node constraing violation. |
| */ |
| object_pid = cg_run_nowait(child, idle_process_fn, NULL); |
| if (object_pid < 0) |
| goto cleanup; |
| |
| if (cg_run(child, do_controller_fn, parent) != EXIT_SUCCESS) |
| goto cleanup; |
| |
| ret = KSFT_PASS; |
| |
| cleanup: |
| if (object_pid > 0) { |
| (void)kill(object_pid, SIGTERM); |
| (void)clone_reap(object_pid, WEXITED); |
| } |
| |
| cg_destroy(child); |
| free(child_procs); |
| free(child); |
| |
| cg_destroy(parent); |
| free(parent_subctl); |
| free(parent_procs); |
| free(parent); |
| |
| return ret; |
| } |
| |
| |
| #define T(x) { x, #x } |
| struct cpuset_test { |
| int (*fn)(const char *root); |
| const char *name; |
| } tests[] = { |
| T(test_cpuset_perms_object_allow), |
| T(test_cpuset_perms_object_deny), |
| T(test_cpuset_perms_subtree), |
| }; |
| #undef T |
| |
| int main(int argc, char *argv[]) |
| { |
| char root[PATH_MAX]; |
| int i, ret = EXIT_SUCCESS; |
| |
| if (cg_find_unified_root(root, sizeof(root))) |
| ksft_exit_skip("cgroup v2 isn't mounted\n"); |
| |
| if (cg_read_strstr(root, "cgroup.subtree_control", "cpuset")) |
| if (cg_write(root, "cgroup.subtree_control", "+cpuset")) |
| ksft_exit_skip("Failed to set cpuset controller\n"); |
| |
| for (i = 0; i < ARRAY_SIZE(tests); i++) { |
| switch (tests[i].fn(root)) { |
| case KSFT_PASS: |
| ksft_test_result_pass("%s\n", tests[i].name); |
| break; |
| case KSFT_SKIP: |
| ksft_test_result_skip("%s\n", tests[i].name); |
| break; |
| default: |
| ret = EXIT_FAILURE; |
| ksft_test_result_fail("%s\n", tests[i].name); |
| break; |
| } |
| } |
| |
| return ret; |
| } |