| // SPDX-License-Identifier: GPL-2.0 |
| |
| #define _GNU_SOURCE |
| #include <assert.h> |
| #include <linux/limits.h> |
| #include <pthread.h> |
| #include <sched.h> |
| #include <signal.h> |
| #include <sys/syscall.h> |
| #include <unistd.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; |
| } |
| |
| static int get_cpu_affinity(cpu_set_t *mask) |
| { |
| CPU_ZERO(mask); |
| return sched_getaffinity(0, sizeof(*mask), mask); |
| } |
| |
| static int cpu_set_equal(cpu_set_t *dst, unsigned long mask) |
| { |
| cpu_set_t expected; |
| |
| CPU_ZERO(&expected); |
| assert(sizeof(mask) < CPU_SETSIZE); |
| |
| for (int cpu = 0; cpu < sizeof(mask) * 8; ++cpu) |
| if ((1UL << cpu) & mask) |
| CPU_SET(cpu, &expected); |
| |
| return CPU_EQUAL(&expected, dst); |
| } |
| |
| enum test_phase { |
| AFFINITY_SETUP, |
| AFFINITY_CONTROLLER_DISABLED, |
| AFFINITY_COMPLETE, |
| AFFINITY_ERROR |
| }; |
| |
| struct thread_args { |
| const char *cgroup; |
| cpu_set_t *affinity_before; |
| cpu_set_t *affinity_after; |
| int affinity_before_ready; |
| }; |
| |
| static pthread_mutex_t test_mutex = PTHREAD_MUTEX_INITIALIZER; |
| static pthread_cond_t test_cond = PTHREAD_COND_INITIALIZER; |
| static enum test_phase test_phase; |
| |
| static void *affinity_thread_fn(void *arg) |
| { |
| struct thread_args *args = (struct thread_args *)arg; |
| |
| if (cg_enter_current_thread(args->cgroup)) |
| goto fail; |
| |
| if (get_cpu_affinity(args->affinity_before) != 0) |
| goto fail; |
| |
| pthread_mutex_lock(&test_mutex); |
| args->affinity_before_ready = 1; |
| pthread_cond_broadcast(&test_cond); |
| |
| while (test_phase < AFFINITY_CONTROLLER_DISABLED) |
| pthread_cond_wait(&test_cond, &test_mutex); |
| pthread_mutex_unlock(&test_mutex); |
| |
| if (get_cpu_affinity(args->affinity_after) != 0) |
| goto fail; |
| |
| |
| return NULL; |
| |
| fail: |
| pthread_mutex_lock(&test_mutex); |
| test_phase = AFFINITY_ERROR; |
| pthread_cond_broadcast(&test_cond); |
| pthread_mutex_unlock(&test_mutex); |
| return NULL; |
| } |
| |
| /* |
| * Test that disabling cpuset controller properly updates thread affinity. |
| * |
| * This test exposes a bug in cpuset_attach() where threads in child cgroups |
| * don't get their affinity updated when the cpuset controller is disabled. |
| * |
| * Setup: |
| * - Create parent cgroup with cpuset.cpus=0-1 |
| * - Create child A with cpuset.cpus=0-1 |
| * - Create child B with cpuset.cpus=1 |
| * - Place multithreaded process: group leader + thread_a in A, thread_b in B |
| * - Disable cpuset controller on parent |
| * |
| * Expected: thread_b's affinity should expand from {1} to {0-1} |
| * Buggy: thread_b's affinity remains {1} |
| */ |
| static int test_cpuset_affinity_on_controller_disable(const char *root) |
| { |
| char *parent = NULL, *child_a = NULL, *child_b = NULL; |
| pthread_t thread_a, thread_b; |
| int thread_a_created = 0, thread_b_created = 0; |
| cpu_set_t affinity_a_before, affinity_a_after; |
| cpu_set_t affinity_b_before, affinity_b_after; |
| int ret = KSFT_FAIL; |
| |
| parent = cg_name(root, "cpuset_affinity_test"); |
| if (!parent) |
| goto cleanup; |
| if (cg_create(parent)) |
| goto cleanup; |
| if (cg_write(parent, "cgroup.type", "threaded")) |
| goto cleanup; |
| |
| child_a = cg_name(parent, "A"); |
| if (!child_a) |
| goto cleanup; |
| if (cg_create(child_a)) |
| goto cleanup; |
| if (cg_write(child_a, "cgroup.type", "threaded")) |
| goto cleanup; |
| |
| child_b = cg_name(parent, "B"); |
| if (!child_b) |
| goto cleanup; |
| if (cg_create(child_b)) |
| goto cleanup; |
| if (cg_write(child_b, "cgroup.type", "threaded")) |
| goto cleanup; |
| |
| /* Now enable cpuset controller in parent */ |
| if (cg_write(parent, "cgroup.subtree_control", "+cpuset")) |
| goto skip; |
| |
| /* |
| * Set CPU affinity constraints |
| * Skip the test if the setting of "cpuset.cpus" fails as the test |
| * system may not have CPU 1. |
| */ |
| if (cg_write(parent, "cpuset.cpus", "0-1")) |
| goto skip; |
| if (cg_write(child_a, "cpuset.cpus", "0-1")) |
| goto skip; |
| if (cg_write(child_b, "cpuset.cpus", "1")) |
| goto skip; |
| |
| /* Move group leader (main thread) to child A */ |
| if (cg_enter_current(child_a)) |
| goto cleanup; |
| |
| /* Create threads - they will move themselves to their respective cgroups */ |
| test_phase = AFFINITY_SETUP; |
| |
| struct thread_args args_a = { |
| .cgroup = child_a, |
| .affinity_before = &affinity_a_before, |
| .affinity_after = &affinity_a_after, |
| .affinity_before_ready = 0, |
| }; |
| if (pthread_create(&thread_a, NULL, affinity_thread_fn, &args_a)) |
| goto cleanup; |
| thread_a_created = 1; |
| |
| struct thread_args args_b = { |
| .cgroup = child_b, |
| .affinity_before = &affinity_b_before, |
| .affinity_after = &affinity_b_after, |
| .affinity_before_ready = 0, |
| }; |
| if (pthread_create(&thread_b, NULL, affinity_thread_fn, &args_b)) |
| goto cleanup_threads; |
| thread_b_created = 1; |
| |
| pthread_mutex_lock(&test_mutex); |
| while ((test_phase < AFFINITY_ERROR) && |
| (args_a.affinity_before_ready + args_b.affinity_before_ready < 2)) |
| pthread_cond_wait(&test_cond, &test_mutex); |
| |
| /* If a thread failed during setup, bail out */ |
| if (test_phase == AFFINITY_ERROR) { |
| pthread_mutex_unlock(&test_mutex); |
| goto cleanup_threads; |
| } |
| pthread_mutex_unlock(&test_mutex); |
| |
| if (!cpu_set_equal(&affinity_a_before, 0x3)) { |
| ksft_print_msg("FAIL: thread_a initial affinity incorrect\n"); |
| goto cleanup_threads; |
| } |
| |
| if (!cpu_set_equal(&affinity_b_before, 0x2)) { |
| ksft_print_msg("FAIL: thread_b initial affinity incorrect\n"); |
| goto cleanup_threads; |
| } |
| |
| /* Disable cpuset controller - this should trigger affinity update */ |
| if (cg_write(parent, "cgroup.subtree_control", "-cpuset")) |
| goto cleanup_threads; |
| |
| /* Signal threads to save their final affinity and exit */ |
| pthread_mutex_lock(&test_mutex); |
| test_phase = AFFINITY_CONTROLLER_DISABLED; |
| pthread_cond_broadcast(&test_cond); |
| pthread_mutex_unlock(&test_mutex); |
| |
| pthread_join(thread_a, NULL); |
| pthread_join(thread_b, NULL); |
| |
| /* Verify thread affinities AFTER disabling controller */ |
| if (!cpu_set_equal(&affinity_a_after, 0x3)) { |
| ksft_print_msg("FAIL: thread_a final affinity incorrect\n"); |
| goto cleanup; |
| } |
| |
| if (!cpu_set_equal(&affinity_b_after, 0x3)) { |
| ksft_print_msg("FAIL: thread_b affinity did not expand to {0-1}\n"); |
| goto cleanup; |
| } |
| |
| ret = KSFT_PASS; |
| goto cleanup; |
| |
| skip: |
| ret = KSFT_SKIP; |
| goto cleanup; |
| |
| cleanup_threads: |
| pthread_mutex_lock(&test_mutex); |
| test_phase = AFFINITY_COMPLETE; |
| pthread_cond_broadcast(&test_cond); |
| pthread_mutex_unlock(&test_mutex); |
| |
| if (thread_a_created) |
| pthread_join(thread_a, NULL); |
| if (thread_b_created) |
| pthread_join(thread_b, NULL); |
| |
| cleanup: |
| /* Move back to root before cleanup */ |
| cg_enter_current(root); |
| |
| cg_destroy(child_b); |
| free(child_b); |
| cg_destroy(child_a); |
| free(child_a); |
| cg_destroy(parent); |
| 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), |
| T(test_cpuset_affinity_on_controller_disable), |
| }; |
| #undef T |
| |
| int main(int argc, char *argv[]) |
| { |
| char root[PATH_MAX]; |
| int i; |
| |
| ksft_print_header(); |
| ksft_set_plan(ARRAY_SIZE(tests)); |
| if (cg_find_unified_root(root, sizeof(root), NULL)) |
| 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: |
| ksft_test_result_fail("%s\n", tests[i].name); |
| break; |
| } |
| } |
| |
| ksft_finished(); |
| } |