276 lines
6.6 KiB
C
276 lines
6.6 KiB
C
// 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;
|
|
}
|