diff options
Diffstat (limited to 'tools/testing/selftests/cgroup')
-rw-r--r-- | tools/testing/selftests/cgroup/.gitignore | 2 | ||||
-rw-r--r-- | tools/testing/selftests/cgroup/Makefile | 12 | ||||
-rw-r--r-- | tools/testing/selftests/cgroup/cgroup_util.c | 370 | ||||
-rw-r--r-- | tools/testing/selftests/cgroup/cgroup_util.h | 43 | ||||
-rw-r--r-- | tools/testing/selftests/cgroup/test_core.c | 567 | ||||
-rw-r--r-- | tools/testing/selftests/cgroup/test_memcontrol.c | 1228 |
6 files changed, 2222 insertions, 0 deletions
diff --git a/tools/testing/selftests/cgroup/.gitignore b/tools/testing/selftests/cgroup/.gitignore new file mode 100644 index 000000000..adacda50a --- /dev/null +++ b/tools/testing/selftests/cgroup/.gitignore @@ -0,0 +1,2 @@ +test_memcontrol +test_core diff --git a/tools/testing/selftests/cgroup/Makefile b/tools/testing/selftests/cgroup/Makefile new file mode 100644 index 000000000..23fbaa4a9 --- /dev/null +++ b/tools/testing/selftests/cgroup/Makefile @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: GPL-2.0 +CFLAGS += -Wall + +all: + +TEST_GEN_PROGS = test_memcontrol +TEST_GEN_PROGS += test_core + +include ../lib.mk + +$(OUTPUT)/test_memcontrol: cgroup_util.c +$(OUTPUT)/test_core: cgroup_util.c diff --git a/tools/testing/selftests/cgroup/cgroup_util.c b/tools/testing/selftests/cgroup/cgroup_util.c new file mode 100644 index 000000000..a516d01f0 --- /dev/null +++ b/tools/testing/selftests/cgroup/cgroup_util.c @@ -0,0 +1,370 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#define _GNU_SOURCE + +#include <errno.h> +#include <fcntl.h> +#include <linux/limits.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "cgroup_util.h" + +static ssize_t read_text(const char *path, char *buf, size_t max_len) +{ + ssize_t len; + int fd; + + fd = open(path, O_RDONLY); + if (fd < 0) + return fd; + + len = read(fd, buf, max_len - 1); + if (len < 0) + goto out; + + buf[len] = 0; +out: + close(fd); + return len; +} + +static ssize_t write_text(const char *path, char *buf, ssize_t len) +{ + int fd; + + fd = open(path, O_WRONLY | O_APPEND); + if (fd < 0) + return fd; + + len = write(fd, buf, len); + if (len < 0) { + close(fd); + return len; + } + + close(fd); + + return len; +} + +char *cg_name(const char *root, const char *name) +{ + size_t len = strlen(root) + strlen(name) + 2; + char *ret = malloc(len); + + snprintf(ret, len, "%s/%s", root, name); + + return ret; +} + +char *cg_name_indexed(const char *root, const char *name, int index) +{ + size_t len = strlen(root) + strlen(name) + 10; + char *ret = malloc(len); + + snprintf(ret, len, "%s/%s_%d", root, name, index); + + return ret; +} + +int cg_read(const char *cgroup, const char *control, char *buf, size_t len) +{ + char path[PATH_MAX]; + + snprintf(path, sizeof(path), "%s/%s", cgroup, control); + + if (read_text(path, buf, len) >= 0) + return 0; + + return -1; +} + +int cg_read_strcmp(const char *cgroup, const char *control, + const char *expected) +{ + size_t size; + char *buf; + int ret; + + /* Handle the case of comparing against empty string */ + if (!expected) + return -1; + else + size = strlen(expected) + 1; + + buf = malloc(size); + if (!buf) + return -1; + + if (cg_read(cgroup, control, buf, size)) { + free(buf); + return -1; + } + + ret = strcmp(expected, buf); + free(buf); + return ret; +} + +int cg_read_strstr(const char *cgroup, const char *control, const char *needle) +{ + char buf[PAGE_SIZE]; + + if (cg_read(cgroup, control, buf, sizeof(buf))) + return -1; + + return strstr(buf, needle) ? 0 : -1; +} + +long cg_read_long(const char *cgroup, const char *control) +{ + char buf[128]; + + if (cg_read(cgroup, control, buf, sizeof(buf))) + return -1; + + return atol(buf); +} + +long cg_read_key_long(const char *cgroup, const char *control, const char *key) +{ + char buf[PAGE_SIZE]; + char *ptr; + + if (cg_read(cgroup, control, buf, sizeof(buf))) + return -1; + + ptr = strstr(buf, key); + if (!ptr) + return -1; + + return atol(ptr + strlen(key)); +} + +int cg_write(const char *cgroup, const char *control, char *buf) +{ + char path[PATH_MAX]; + ssize_t len = strlen(buf); + + snprintf(path, sizeof(path), "%s/%s", cgroup, control); + + if (write_text(path, buf, len) == len) + return 0; + + return -1; +} + +int cg_find_unified_root(char *root, size_t len) +{ + char buf[10 * PAGE_SIZE]; + char *fs, *mount, *type; + const char delim[] = "\n\t "; + + if (read_text("/proc/self/mounts", buf, sizeof(buf)) <= 0) + return -1; + + /* + * Example: + * cgroup /sys/fs/cgroup cgroup2 rw,seclabel,noexec,relatime 0 0 + */ + for (fs = strtok(buf, delim); fs; fs = strtok(NULL, delim)) { + mount = strtok(NULL, delim); + type = strtok(NULL, delim); + strtok(NULL, delim); + strtok(NULL, delim); + strtok(NULL, delim); + + if (strcmp(type, "cgroup2") == 0) { + strncpy(root, mount, len); + return 0; + } + } + + return -1; +} + +int cg_create(const char *cgroup) +{ + return mkdir(cgroup, 0755); +} + +static int cg_killall(const char *cgroup) +{ + char buf[PAGE_SIZE]; + char *ptr = buf; + + if (cg_read(cgroup, "cgroup.procs", buf, sizeof(buf))) + return -1; + + while (ptr < buf + sizeof(buf)) { + int pid = strtol(ptr, &ptr, 10); + + if (pid == 0) + break; + if (*ptr) + ptr++; + else + break; + if (kill(pid, SIGKILL)) + return -1; + } + + return 0; +} + +int cg_destroy(const char *cgroup) +{ + int ret; + +retry: + ret = rmdir(cgroup); + if (ret && errno == EBUSY) { + ret = cg_killall(cgroup); + if (ret) + return ret; + usleep(100); + goto retry; + } + + if (ret && errno == ENOENT) + ret = 0; + + return ret; +} + +int cg_enter_current(const char *cgroup) +{ + char pidbuf[64]; + + snprintf(pidbuf, sizeof(pidbuf), "%d", getpid()); + return cg_write(cgroup, "cgroup.procs", pidbuf); +} + +int cg_run(const char *cgroup, + int (*fn)(const char *cgroup, void *arg), + void *arg) +{ + int pid, retcode; + + pid = fork(); + if (pid < 0) { + return pid; + } else if (pid == 0) { + char buf[64]; + + snprintf(buf, sizeof(buf), "%d", getpid()); + if (cg_write(cgroup, "cgroup.procs", buf)) + exit(EXIT_FAILURE); + exit(fn(cgroup, arg)); + } else { + waitpid(pid, &retcode, 0); + if (WIFEXITED(retcode)) + return WEXITSTATUS(retcode); + else + return -1; + } +} + +int cg_run_nowait(const char *cgroup, + int (*fn)(const char *cgroup, void *arg), + void *arg) +{ + int pid; + + pid = fork(); + if (pid == 0) { + char buf[64]; + + snprintf(buf, sizeof(buf), "%d", getpid()); + if (cg_write(cgroup, "cgroup.procs", buf)) + exit(EXIT_FAILURE); + exit(fn(cgroup, arg)); + } + + return pid; +} + +int get_temp_fd(void) +{ + return open(".", O_TMPFILE | O_RDWR | O_EXCL); +} + +int alloc_pagecache(int fd, size_t size) +{ + char buf[PAGE_SIZE]; + struct stat st; + int i; + + if (fstat(fd, &st)) + goto cleanup; + + size += st.st_size; + + if (ftruncate(fd, size)) + goto cleanup; + + for (i = 0; i < size; i += sizeof(buf)) + read(fd, buf, sizeof(buf)); + + return 0; + +cleanup: + return -1; +} + +int alloc_anon(const char *cgroup, void *arg) +{ + size_t size = (unsigned long)arg; + char *buf, *ptr; + + buf = malloc(size); + for (ptr = buf; ptr < buf + size; ptr += PAGE_SIZE) + *ptr = 0; + + free(buf); + return 0; +} + +int is_swap_enabled(void) +{ + char buf[PAGE_SIZE]; + const char delim[] = "\n"; + int cnt = 0; + char *line; + + if (read_text("/proc/swaps", buf, sizeof(buf)) <= 0) + return -1; + + for (line = strtok(buf, delim); line; line = strtok(NULL, delim)) + cnt++; + + return cnt > 1; +} + +int set_oom_adj_score(int pid, int score) +{ + char path[PATH_MAX]; + int fd, len; + + sprintf(path, "/proc/%d/oom_score_adj", pid); + + fd = open(path, O_WRONLY | O_APPEND); + if (fd < 0) + return fd; + + len = dprintf(fd, "%d", score); + if (len < 0) { + close(fd); + return len; + } + + close(fd); + return 0; +} diff --git a/tools/testing/selftests/cgroup/cgroup_util.h b/tools/testing/selftests/cgroup/cgroup_util.h new file mode 100644 index 000000000..9ac8b7958 --- /dev/null +++ b/tools/testing/selftests/cgroup/cgroup_util.h @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#include <stdlib.h> + +#define PAGE_SIZE 4096 + +#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) + +#define MB(x) (x << 20) + +/* + * Checks if two given values differ by less than err% of their sum. + */ +static inline int values_close(long a, long b, int err) +{ + return abs(a - b) <= (a + b) / 100 * err; +} + +extern int cg_find_unified_root(char *root, size_t len); +extern char *cg_name(const char *root, const char *name); +extern char *cg_name_indexed(const char *root, const char *name, int index); +extern int cg_create(const char *cgroup); +extern int cg_destroy(const char *cgroup); +extern int cg_read(const char *cgroup, const char *control, + char *buf, size_t len); +extern int cg_read_strcmp(const char *cgroup, const char *control, + const char *expected); +extern int cg_read_strstr(const char *cgroup, const char *control, + const char *needle); +extern long cg_read_long(const char *cgroup, const char *control); +long cg_read_key_long(const char *cgroup, const char *control, const char *key); +extern int cg_write(const char *cgroup, const char *control, char *buf); +extern int cg_run(const char *cgroup, + int (*fn)(const char *cgroup, void *arg), + void *arg); +extern int cg_enter_current(const char *cgroup); +extern int cg_run_nowait(const char *cgroup, + int (*fn)(const char *cgroup, void *arg), + void *arg); +extern int get_temp_fd(void); +extern int alloc_pagecache(int fd, size_t size); +extern int alloc_anon(const char *cgroup, void *arg); +extern int is_swap_enabled(void); +extern int set_oom_adj_score(int pid, int score); diff --git a/tools/testing/selftests/cgroup/test_core.c b/tools/testing/selftests/cgroup/test_core.c new file mode 100644 index 000000000..599234c5e --- /dev/null +++ b/tools/testing/selftests/cgroup/test_core.c @@ -0,0 +1,567 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#define _GNU_SOURCE +#include <linux/limits.h> +#include <linux/sched.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> +#include <fcntl.h> +#include <sched.h> +#include <stdio.h> +#include <errno.h> + +#include "../kselftest.h" +#include "cgroup_util.h" + +/* + * A(0) - B(0) - C(1) + * \ D(0) + * + * A, B and C's "populated" fields would be 1 while D's 0. + * test that after the one process in C is moved to root, + * A,B and C's "populated" fields would flip to "0" and file + * modified events will be generated on the + * "cgroup.events" files of both cgroups. + */ +static int test_cgcore_populated(const char *root) +{ + int ret = KSFT_FAIL; + char *cg_test_a = NULL, *cg_test_b = NULL; + char *cg_test_c = NULL, *cg_test_d = NULL; + + cg_test_a = cg_name(root, "cg_test_a"); + cg_test_b = cg_name(root, "cg_test_a/cg_test_b"); + cg_test_c = cg_name(root, "cg_test_a/cg_test_b/cg_test_c"); + cg_test_d = cg_name(root, "cg_test_a/cg_test_b/cg_test_d"); + + if (!cg_test_a || !cg_test_b || !cg_test_c || !cg_test_d) + goto cleanup; + + if (cg_create(cg_test_a)) + goto cleanup; + + if (cg_create(cg_test_b)) + goto cleanup; + + if (cg_create(cg_test_c)) + goto cleanup; + + if (cg_create(cg_test_d)) + goto cleanup; + + if (cg_enter_current(cg_test_c)) + goto cleanup; + + if (cg_read_strcmp(cg_test_a, "cgroup.events", "populated 1\n")) + goto cleanup; + + if (cg_read_strcmp(cg_test_b, "cgroup.events", "populated 1\n")) + goto cleanup; + + if (cg_read_strcmp(cg_test_c, "cgroup.events", "populated 1\n")) + goto cleanup; + + if (cg_read_strcmp(cg_test_d, "cgroup.events", "populated 0\n")) + goto cleanup; + + if (cg_enter_current(root)) + goto cleanup; + + if (cg_read_strcmp(cg_test_a, "cgroup.events", "populated 0\n")) + goto cleanup; + + if (cg_read_strcmp(cg_test_b, "cgroup.events", "populated 0\n")) + goto cleanup; + + if (cg_read_strcmp(cg_test_c, "cgroup.events", "populated 0\n")) + goto cleanup; + + if (cg_read_strcmp(cg_test_d, "cgroup.events", "populated 0\n")) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + if (cg_test_d) + cg_destroy(cg_test_d); + if (cg_test_c) + cg_destroy(cg_test_c); + if (cg_test_b) + cg_destroy(cg_test_b); + if (cg_test_a) + cg_destroy(cg_test_a); + free(cg_test_d); + free(cg_test_c); + free(cg_test_b); + free(cg_test_a); + return ret; +} + +/* + * A (domain threaded) - B (threaded) - C (domain) + * + * test that C can't be used until it is turned into a + * threaded cgroup. "cgroup.type" file will report "domain (invalid)" in + * these cases. Operations which fail due to invalid topology use + * EOPNOTSUPP as the errno. + */ +static int test_cgcore_invalid_domain(const char *root) +{ + int ret = KSFT_FAIL; + char *grandparent = NULL, *parent = NULL, *child = NULL; + + grandparent = cg_name(root, "cg_test_grandparent"); + parent = cg_name(root, "cg_test_grandparent/cg_test_parent"); + child = cg_name(root, "cg_test_grandparent/cg_test_parent/cg_test_child"); + if (!parent || !child || !grandparent) + goto cleanup; + + if (cg_create(grandparent)) + goto cleanup; + + if (cg_create(parent)) + goto cleanup; + + if (cg_create(child)) + goto cleanup; + + if (cg_write(parent, "cgroup.type", "threaded")) + goto cleanup; + + if (cg_read_strcmp(child, "cgroup.type", "domain invalid\n")) + goto cleanup; + + if (!cg_enter_current(child)) + goto cleanup; + + if (errno != EOPNOTSUPP) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + cg_enter_current(root); + if (child) + cg_destroy(child); + if (parent) + cg_destroy(parent); + if (grandparent) + cg_destroy(grandparent); + free(child); + free(parent); + free(grandparent); + return ret; +} + +/* + * Test that when a child becomes threaded + * the parent type becomes domain threaded. + */ +static int test_cgcore_parent_becomes_threaded(const char *root) +{ + int ret = KSFT_FAIL; + char *parent = NULL, *child = NULL; + + parent = cg_name(root, "cg_test_parent"); + child = cg_name(root, "cg_test_parent/cg_test_child"); + if (!parent || !child) + goto cleanup; + + if (cg_create(parent)) + goto cleanup; + + if (cg_create(child)) + goto cleanup; + + if (cg_write(child, "cgroup.type", "threaded")) + goto cleanup; + + if (cg_read_strcmp(parent, "cgroup.type", "domain threaded\n")) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + if (child) + cg_destroy(child); + if (parent) + cg_destroy(parent); + free(child); + free(parent); + return ret; + +} + +/* + * Test that there's no internal process constrain on threaded cgroups. + * You can add threads/processes on a parent with a controller enabled. + */ +static int test_cgcore_no_internal_process_constraint_on_threads(const char *root) +{ + int ret = KSFT_FAIL; + char *parent = NULL, *child = NULL; + + if (cg_read_strstr(root, "cgroup.controllers", "cpu") || + cg_write(root, "cgroup.subtree_control", "+cpu")) { + ret = KSFT_SKIP; + goto cleanup; + } + + parent = cg_name(root, "cg_test_parent"); + child = cg_name(root, "cg_test_parent/cg_test_child"); + if (!parent || !child) + goto cleanup; + + if (cg_create(parent)) + goto cleanup; + + if (cg_create(child)) + goto cleanup; + + if (cg_write(parent, "cgroup.type", "threaded")) + goto cleanup; + + if (cg_write(child, "cgroup.type", "threaded")) + goto cleanup; + + if (cg_write(parent, "cgroup.subtree_control", "+cpu")) + goto cleanup; + + if (cg_enter_current(parent)) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + cg_enter_current(root); + cg_enter_current(root); + if (child) + cg_destroy(child); + if (parent) + cg_destroy(parent); + free(child); + free(parent); + return ret; +} + +/* + * Test that you can't enable a controller on a child if it's not enabled + * on the parent. + */ +static int test_cgcore_top_down_constraint_enable(const char *root) +{ + int ret = KSFT_FAIL; + char *parent = NULL, *child = NULL; + + parent = cg_name(root, "cg_test_parent"); + child = cg_name(root, "cg_test_parent/cg_test_child"); + if (!parent || !child) + goto cleanup; + + if (cg_create(parent)) + goto cleanup; + + if (cg_create(child)) + goto cleanup; + + if (!cg_write(child, "cgroup.subtree_control", "+memory")) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + if (child) + cg_destroy(child); + if (parent) + cg_destroy(parent); + free(child); + free(parent); + return ret; +} + +/* + * Test that you can't disable a controller on a parent + * if it's enabled in a child. + */ +static int test_cgcore_top_down_constraint_disable(const char *root) +{ + int ret = KSFT_FAIL; + char *parent = NULL, *child = NULL; + + parent = cg_name(root, "cg_test_parent"); + child = cg_name(root, "cg_test_parent/cg_test_child"); + if (!parent || !child) + goto cleanup; + + if (cg_create(parent)) + goto cleanup; + + if (cg_create(child)) + goto cleanup; + + if (cg_write(parent, "cgroup.subtree_control", "+memory")) + goto cleanup; + + if (cg_write(child, "cgroup.subtree_control", "+memory")) + goto cleanup; + + if (!cg_write(parent, "cgroup.subtree_control", "-memory")) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + if (child) + cg_destroy(child); + if (parent) + cg_destroy(parent); + free(child); + free(parent); + return ret; +} + +/* + * Test internal process constraint. + * You can't add a pid to a domain parent if a controller is enabled. + */ +static int test_cgcore_internal_process_constraint(const char *root) +{ + int ret = KSFT_FAIL; + char *parent = NULL, *child = NULL; + + parent = cg_name(root, "cg_test_parent"); + child = cg_name(root, "cg_test_parent/cg_test_child"); + if (!parent || !child) + goto cleanup; + + if (cg_create(parent)) + goto cleanup; + + if (cg_create(child)) + goto cleanup; + + if (cg_write(parent, "cgroup.subtree_control", "+memory")) + goto cleanup; + + if (!cg_enter_current(parent)) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + if (child) + cg_destroy(child); + if (parent) + cg_destroy(parent); + free(child); + free(parent); + return ret; +} + +/* + * cgroup migration permission check should be performed based on the + * credentials at the time of open instead of write. + */ +static int test_cgcore_lesser_euid_open(const char *root) +{ + const uid_t test_euid = 65534; /* usually nobody, any !root is fine */ + int ret = KSFT_FAIL; + char *cg_test_a = NULL, *cg_test_b = NULL; + char *cg_test_a_procs = NULL, *cg_test_b_procs = NULL; + int cg_test_b_procs_fd = -1; + uid_t saved_uid; + + cg_test_a = cg_name(root, "cg_test_a"); + cg_test_b = cg_name(root, "cg_test_b"); + + if (!cg_test_a || !cg_test_b) + goto cleanup; + + cg_test_a_procs = cg_name(cg_test_a, "cgroup.procs"); + cg_test_b_procs = cg_name(cg_test_b, "cgroup.procs"); + + if (!cg_test_a_procs || !cg_test_b_procs) + goto cleanup; + + if (cg_create(cg_test_a) || cg_create(cg_test_b)) + goto cleanup; + + if (cg_enter_current(cg_test_a)) + goto cleanup; + + if (chown(cg_test_a_procs, test_euid, -1) || + chown(cg_test_b_procs, test_euid, -1)) + goto cleanup; + + saved_uid = geteuid(); + if (seteuid(test_euid)) + goto cleanup; + + cg_test_b_procs_fd = open(cg_test_b_procs, O_RDWR); + + if (seteuid(saved_uid)) + goto cleanup; + + if (cg_test_b_procs_fd < 0) + goto cleanup; + + if (write(cg_test_b_procs_fd, "0", 1) >= 0 || errno != EACCES) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + cg_enter_current(root); + if (cg_test_b_procs_fd >= 0) + close(cg_test_b_procs_fd); + if (cg_test_b) + cg_destroy(cg_test_b); + if (cg_test_a) + cg_destroy(cg_test_a); + free(cg_test_b_procs); + free(cg_test_a_procs); + free(cg_test_b); + free(cg_test_a); + return ret; +} + +struct lesser_ns_open_thread_arg { + const char *path; + int fd; + int err; +}; + +static int lesser_ns_open_thread_fn(void *arg) +{ + struct lesser_ns_open_thread_arg *targ = arg; + + targ->fd = open(targ->path, O_RDWR); + targ->err = errno; + return 0; +} + +/* + * cgroup migration permission check should be performed based on the cgroup + * namespace at the time of open instead of write. + */ +static int test_cgcore_lesser_ns_open(const char *root) +{ + static char stack[65536]; + const uid_t test_euid = 65534; /* usually nobody, any !root is fine */ + int ret = KSFT_FAIL; + char *cg_test_a = NULL, *cg_test_b = NULL; + char *cg_test_a_procs = NULL, *cg_test_b_procs = NULL; + int cg_test_b_procs_fd = -1; + struct lesser_ns_open_thread_arg targ = { .fd = -1 }; + pid_t pid; + int status; + + cg_test_a = cg_name(root, "cg_test_a"); + cg_test_b = cg_name(root, "cg_test_b"); + + if (!cg_test_a || !cg_test_b) + goto cleanup; + + cg_test_a_procs = cg_name(cg_test_a, "cgroup.procs"); + cg_test_b_procs = cg_name(cg_test_b, "cgroup.procs"); + + if (!cg_test_a_procs || !cg_test_b_procs) + goto cleanup; + + if (cg_create(cg_test_a) || cg_create(cg_test_b)) + goto cleanup; + + if (cg_enter_current(cg_test_b)) + goto cleanup; + + if (chown(cg_test_a_procs, test_euid, -1) || + chown(cg_test_b_procs, test_euid, -1)) + goto cleanup; + + targ.path = cg_test_b_procs; + pid = clone(lesser_ns_open_thread_fn, stack + sizeof(stack), + CLONE_NEWCGROUP | CLONE_FILES | CLONE_VM | SIGCHLD, + &targ); + if (pid < 0) + goto cleanup; + + if (waitpid(pid, &status, 0) < 0) + goto cleanup; + + if (!WIFEXITED(status)) + goto cleanup; + + cg_test_b_procs_fd = targ.fd; + if (cg_test_b_procs_fd < 0) + goto cleanup; + + if (cg_enter_current(cg_test_a)) + goto cleanup; + + if ((status = write(cg_test_b_procs_fd, "0", 1)) >= 0 || errno != ENOENT) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + cg_enter_current(root); + if (cg_test_b_procs_fd >= 0) + close(cg_test_b_procs_fd); + if (cg_test_b) + cg_destroy(cg_test_b); + if (cg_test_a) + cg_destroy(cg_test_a); + free(cg_test_b_procs); + free(cg_test_a_procs); + free(cg_test_b); + free(cg_test_a); + return ret; +} + +#define T(x) { x, #x } +struct corecg_test { + int (*fn)(const char *root); + const char *name; +} tests[] = { + T(test_cgcore_internal_process_constraint), + T(test_cgcore_top_down_constraint_enable), + T(test_cgcore_top_down_constraint_disable), + T(test_cgcore_no_internal_process_constraint_on_threads), + T(test_cgcore_parent_becomes_threaded), + T(test_cgcore_invalid_domain), + T(test_cgcore_populated), + T(test_cgcore_lesser_euid_open), + T(test_cgcore_lesser_ns_open), +}; +#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", "memory")) + if (cg_write(root, "cgroup.subtree_control", "+memory")) + ksft_exit_skip("Failed to set memory 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; +} diff --git a/tools/testing/selftests/cgroup/test_memcontrol.c b/tools/testing/selftests/cgroup/test_memcontrol.c new file mode 100644 index 000000000..c19a97dd0 --- /dev/null +++ b/tools/testing/selftests/cgroup/test_memcontrol.c @@ -0,0 +1,1228 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#define _GNU_SOURCE + +#include <linux/limits.h> +#include <linux/oom.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> +#include <sys/socket.h> +#include <sys/wait.h> +#include <arpa/inet.h> +#include <netinet/in.h> +#include <netdb.h> +#include <errno.h> + +#include "../kselftest.h" +#include "cgroup_util.h" + +/* + * This test creates two nested cgroups with and without enabling + * the memory controller. + */ +static int test_memcg_subtree_control(const char *root) +{ + char *parent, *child, *parent2 = NULL, *child2 = NULL; + int ret = KSFT_FAIL; + char buf[PAGE_SIZE]; + + /* Create two nested cgroups with the memory controller enabled */ + parent = cg_name(root, "memcg_test_0"); + child = cg_name(root, "memcg_test_0/memcg_test_1"); + if (!parent || !child) + goto cleanup_free; + + if (cg_create(parent)) + goto cleanup_free; + + if (cg_write(parent, "cgroup.subtree_control", "+memory")) + goto cleanup_parent; + + if (cg_create(child)) + goto cleanup_parent; + + if (cg_read_strstr(child, "cgroup.controllers", "memory")) + goto cleanup_child; + + /* Create two nested cgroups without enabling memory controller */ + parent2 = cg_name(root, "memcg_test_1"); + child2 = cg_name(root, "memcg_test_1/memcg_test_1"); + if (!parent2 || !child2) + goto cleanup_free2; + + if (cg_create(parent2)) + goto cleanup_free2; + + if (cg_create(child2)) + goto cleanup_parent2; + + if (cg_read(child2, "cgroup.controllers", buf, sizeof(buf))) + goto cleanup_all; + + if (!cg_read_strstr(child2, "cgroup.controllers", "memory")) + goto cleanup_all; + + ret = KSFT_PASS; + +cleanup_all: + cg_destroy(child2); +cleanup_parent2: + cg_destroy(parent2); +cleanup_free2: + free(parent2); + free(child2); +cleanup_child: + cg_destroy(child); +cleanup_parent: + cg_destroy(parent); +cleanup_free: + free(parent); + free(child); + + return ret; +} + +static int alloc_anon_50M_check(const char *cgroup, void *arg) +{ + size_t size = MB(50); + char *buf, *ptr; + long anon, current; + int ret = -1; + + buf = malloc(size); + for (ptr = buf; ptr < buf + size; ptr += PAGE_SIZE) + *ptr = 0; + + current = cg_read_long(cgroup, "memory.current"); + if (current < size) + goto cleanup; + + if (!values_close(size, current, 3)) + goto cleanup; + + anon = cg_read_key_long(cgroup, "memory.stat", "anon "); + if (anon < 0) + goto cleanup; + + if (!values_close(anon, current, 3)) + goto cleanup; + + ret = 0; +cleanup: + free(buf); + return ret; +} + +static int alloc_pagecache_50M_check(const char *cgroup, void *arg) +{ + size_t size = MB(50); + int ret = -1; + long current, file; + int fd; + + fd = get_temp_fd(); + if (fd < 0) + return -1; + + if (alloc_pagecache(fd, size)) + goto cleanup; + + current = cg_read_long(cgroup, "memory.current"); + if (current < size) + goto cleanup; + + file = cg_read_key_long(cgroup, "memory.stat", "file "); + if (file < 0) + goto cleanup; + + if (!values_close(file, current, 10)) + goto cleanup; + + ret = 0; + +cleanup: + close(fd); + return ret; +} + +/* + * This test create a memory cgroup, allocates + * some anonymous memory and some pagecache + * and check memory.current and some memory.stat values. + */ +static int test_memcg_current(const char *root) +{ + int ret = KSFT_FAIL; + long current; + char *memcg; + + memcg = cg_name(root, "memcg_test"); + if (!memcg) + goto cleanup; + + if (cg_create(memcg)) + goto cleanup; + + current = cg_read_long(memcg, "memory.current"); + if (current != 0) + goto cleanup; + + if (cg_run(memcg, alloc_anon_50M_check, NULL)) + goto cleanup; + + if (cg_run(memcg, alloc_pagecache_50M_check, NULL)) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + cg_destroy(memcg); + free(memcg); + + return ret; +} + +static int alloc_pagecache_50M(const char *cgroup, void *arg) +{ + int fd = (long)arg; + + return alloc_pagecache(fd, MB(50)); +} + +static int alloc_pagecache_50M_noexit(const char *cgroup, void *arg) +{ + int fd = (long)arg; + int ppid = getppid(); + + if (alloc_pagecache(fd, MB(50))) + return -1; + + while (getppid() == ppid) + sleep(1); + + return 0; +} + +static int alloc_anon_noexit(const char *cgroup, void *arg) +{ + int ppid = getppid(); + + if (alloc_anon(cgroup, arg)) + return -1; + + while (getppid() == ppid) + sleep(1); + + return 0; +} + +/* + * Wait until processes are killed asynchronously by the OOM killer + * If we exceed a timeout, fail. + */ +static int cg_test_proc_killed(const char *cgroup) +{ + int limit; + + for (limit = 10; limit > 0; limit--) { + if (cg_read_strcmp(cgroup, "cgroup.procs", "") == 0) + return 0; + + usleep(100000); + } + return -1; +} + +/* + * First, this test creates the following hierarchy: + * A memory.min = 50M, memory.max = 200M + * A/B memory.min = 50M, memory.current = 50M + * A/B/C memory.min = 75M, memory.current = 50M + * A/B/D memory.min = 25M, memory.current = 50M + * A/B/E memory.min = 500M, memory.current = 0 + * A/B/F memory.min = 0, memory.current = 50M + * + * Usages are pagecache, but the test keeps a running + * process in every leaf cgroup. + * Then it creates A/G and creates a significant + * memory pressure in it. + * + * A/B memory.current ~= 50M + * A/B/C memory.current ~= 33M + * A/B/D memory.current ~= 17M + * A/B/E memory.current ~= 0 + * + * After that it tries to allocate more than there is + * unprotected memory in A available, and checks + * checks that memory.min protects pagecache even + * in this case. + */ +static int test_memcg_min(const char *root) +{ + int ret = KSFT_FAIL; + char *parent[3] = {NULL}; + char *children[4] = {NULL}; + long c[4]; + int i, attempts; + int fd; + + fd = get_temp_fd(); + if (fd < 0) + goto cleanup; + + parent[0] = cg_name(root, "memcg_test_0"); + if (!parent[0]) + goto cleanup; + + parent[1] = cg_name(parent[0], "memcg_test_1"); + if (!parent[1]) + goto cleanup; + + parent[2] = cg_name(parent[0], "memcg_test_2"); + if (!parent[2]) + goto cleanup; + + if (cg_create(parent[0])) + goto cleanup; + + if (cg_read_long(parent[0], "memory.min")) { + ret = KSFT_SKIP; + goto cleanup; + } + + if (cg_write(parent[0], "cgroup.subtree_control", "+memory")) + goto cleanup; + + if (cg_write(parent[0], "memory.max", "200M")) + goto cleanup; + + if (cg_write(parent[0], "memory.swap.max", "0")) + goto cleanup; + + if (cg_create(parent[1])) + goto cleanup; + + if (cg_write(parent[1], "cgroup.subtree_control", "+memory")) + goto cleanup; + + if (cg_create(parent[2])) + goto cleanup; + + for (i = 0; i < ARRAY_SIZE(children); i++) { + children[i] = cg_name_indexed(parent[1], "child_memcg", i); + if (!children[i]) + goto cleanup; + + if (cg_create(children[i])) + goto cleanup; + + if (i == 2) + continue; + + cg_run_nowait(children[i], alloc_pagecache_50M_noexit, + (void *)(long)fd); + } + + if (cg_write(parent[0], "memory.min", "50M")) + goto cleanup; + if (cg_write(parent[1], "memory.min", "50M")) + goto cleanup; + if (cg_write(children[0], "memory.min", "75M")) + goto cleanup; + if (cg_write(children[1], "memory.min", "25M")) + goto cleanup; + if (cg_write(children[2], "memory.min", "500M")) + goto cleanup; + if (cg_write(children[3], "memory.min", "0")) + goto cleanup; + + attempts = 0; + while (!values_close(cg_read_long(parent[1], "memory.current"), + MB(150), 3)) { + if (attempts++ > 5) + break; + sleep(1); + } + + if (cg_run(parent[2], alloc_anon, (void *)MB(148))) + goto cleanup; + + if (!values_close(cg_read_long(parent[1], "memory.current"), MB(50), 3)) + goto cleanup; + + for (i = 0; i < ARRAY_SIZE(children); i++) + c[i] = cg_read_long(children[i], "memory.current"); + + if (!values_close(c[0], MB(33), 10)) + goto cleanup; + + if (!values_close(c[1], MB(17), 10)) + goto cleanup; + + if (!values_close(c[2], 0, 1)) + goto cleanup; + + if (!cg_run(parent[2], alloc_anon, (void *)MB(170))) + goto cleanup; + + if (!values_close(cg_read_long(parent[1], "memory.current"), MB(50), 3)) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + for (i = ARRAY_SIZE(children) - 1; i >= 0; i--) { + if (!children[i]) + continue; + + cg_destroy(children[i]); + free(children[i]); + } + + for (i = ARRAY_SIZE(parent) - 1; i >= 0; i--) { + if (!parent[i]) + continue; + + cg_destroy(parent[i]); + free(parent[i]); + } + close(fd); + return ret; +} + +/* + * First, this test creates the following hierarchy: + * A memory.low = 50M, memory.max = 200M + * A/B memory.low = 50M, memory.current = 50M + * A/B/C memory.low = 75M, memory.current = 50M + * A/B/D memory.low = 25M, memory.current = 50M + * A/B/E memory.low = 500M, memory.current = 0 + * A/B/F memory.low = 0, memory.current = 50M + * + * Usages are pagecache. + * Then it creates A/G an creates a significant + * memory pressure in it. + * + * Then it checks actual memory usages and expects that: + * A/B memory.current ~= 50M + * A/B/ memory.current ~= 33M + * A/B/D memory.current ~= 17M + * A/B/E memory.current ~= 0 + * + * After that it tries to allocate more than there is + * unprotected memory in A available, + * and checks low and oom events in memory.events. + */ +static int test_memcg_low(const char *root) +{ + int ret = KSFT_FAIL; + char *parent[3] = {NULL}; + char *children[4] = {NULL}; + long low, oom; + long c[4]; + int i; + int fd; + + fd = get_temp_fd(); + if (fd < 0) + goto cleanup; + + parent[0] = cg_name(root, "memcg_test_0"); + if (!parent[0]) + goto cleanup; + + parent[1] = cg_name(parent[0], "memcg_test_1"); + if (!parent[1]) + goto cleanup; + + parent[2] = cg_name(parent[0], "memcg_test_2"); + if (!parent[2]) + goto cleanup; + + if (cg_create(parent[0])) + goto cleanup; + + if (cg_read_long(parent[0], "memory.low")) + goto cleanup; + + if (cg_write(parent[0], "cgroup.subtree_control", "+memory")) + goto cleanup; + + if (cg_write(parent[0], "memory.max", "200M")) + goto cleanup; + + if (cg_write(parent[0], "memory.swap.max", "0")) + goto cleanup; + + if (cg_create(parent[1])) + goto cleanup; + + if (cg_write(parent[1], "cgroup.subtree_control", "+memory")) + goto cleanup; + + if (cg_create(parent[2])) + goto cleanup; + + for (i = 0; i < ARRAY_SIZE(children); i++) { + children[i] = cg_name_indexed(parent[1], "child_memcg", i); + if (!children[i]) + goto cleanup; + + if (cg_create(children[i])) + goto cleanup; + + if (i == 2) + continue; + + if (cg_run(children[i], alloc_pagecache_50M, (void *)(long)fd)) + goto cleanup; + } + + if (cg_write(parent[0], "memory.low", "50M")) + goto cleanup; + if (cg_write(parent[1], "memory.low", "50M")) + goto cleanup; + if (cg_write(children[0], "memory.low", "75M")) + goto cleanup; + if (cg_write(children[1], "memory.low", "25M")) + goto cleanup; + if (cg_write(children[2], "memory.low", "500M")) + goto cleanup; + if (cg_write(children[3], "memory.low", "0")) + goto cleanup; + + if (cg_run(parent[2], alloc_anon, (void *)MB(148))) + goto cleanup; + + if (!values_close(cg_read_long(parent[1], "memory.current"), MB(50), 3)) + goto cleanup; + + for (i = 0; i < ARRAY_SIZE(children); i++) + c[i] = cg_read_long(children[i], "memory.current"); + + if (!values_close(c[0], MB(33), 10)) + goto cleanup; + + if (!values_close(c[1], MB(17), 10)) + goto cleanup; + + if (!values_close(c[2], 0, 1)) + goto cleanup; + + if (cg_run(parent[2], alloc_anon, (void *)MB(166))) { + fprintf(stderr, + "memory.low prevents from allocating anon memory\n"); + goto cleanup; + } + + for (i = 0; i < ARRAY_SIZE(children); i++) { + oom = cg_read_key_long(children[i], "memory.events", "oom "); + low = cg_read_key_long(children[i], "memory.events", "low "); + + if (oom) + goto cleanup; + if (i < 2 && low <= 0) + goto cleanup; + if (i >= 2 && low) + goto cleanup; + } + + ret = KSFT_PASS; + +cleanup: + for (i = ARRAY_SIZE(children) - 1; i >= 0; i--) { + if (!children[i]) + continue; + + cg_destroy(children[i]); + free(children[i]); + } + + for (i = ARRAY_SIZE(parent) - 1; i >= 0; i--) { + if (!parent[i]) + continue; + + cg_destroy(parent[i]); + free(parent[i]); + } + close(fd); + return ret; +} + +static int alloc_pagecache_max_30M(const char *cgroup, void *arg) +{ + size_t size = MB(50); + int ret = -1; + long current; + int fd; + + fd = get_temp_fd(); + if (fd < 0) + return -1; + + if (alloc_pagecache(fd, size)) + goto cleanup; + + current = cg_read_long(cgroup, "memory.current"); + if (current <= MB(29) || current > MB(30)) + goto cleanup; + + ret = 0; + +cleanup: + close(fd); + return ret; + +} + +/* + * This test checks that memory.high limits the amount of + * memory which can be consumed by either anonymous memory + * or pagecache. + */ +static int test_memcg_high(const char *root) +{ + int ret = KSFT_FAIL; + char *memcg; + long high; + + memcg = cg_name(root, "memcg_test"); + if (!memcg) + goto cleanup; + + if (cg_create(memcg)) + goto cleanup; + + if (cg_read_strcmp(memcg, "memory.high", "max\n")) + goto cleanup; + + if (cg_write(memcg, "memory.swap.max", "0")) + goto cleanup; + + if (cg_write(memcg, "memory.high", "30M")) + goto cleanup; + + if (cg_run(memcg, alloc_anon, (void *)MB(100))) + goto cleanup; + + if (!cg_run(memcg, alloc_pagecache_50M_check, NULL)) + goto cleanup; + + if (cg_run(memcg, alloc_pagecache_max_30M, NULL)) + goto cleanup; + + high = cg_read_key_long(memcg, "memory.events", "high "); + if (high <= 0) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + cg_destroy(memcg); + free(memcg); + + return ret; +} + +/* + * This test checks that memory.max limits the amount of + * memory which can be consumed by either anonymous memory + * or pagecache. + */ +static int test_memcg_max(const char *root) +{ + int ret = KSFT_FAIL; + char *memcg; + long current, max; + + memcg = cg_name(root, "memcg_test"); + if (!memcg) + goto cleanup; + + if (cg_create(memcg)) + goto cleanup; + + if (cg_read_strcmp(memcg, "memory.max", "max\n")) + goto cleanup; + + if (cg_write(memcg, "memory.swap.max", "0")) + goto cleanup; + + if (cg_write(memcg, "memory.max", "30M")) + goto cleanup; + + /* Should be killed by OOM killer */ + if (!cg_run(memcg, alloc_anon, (void *)MB(100))) + goto cleanup; + + if (cg_run(memcg, alloc_pagecache_max_30M, NULL)) + goto cleanup; + + current = cg_read_long(memcg, "memory.current"); + if (current > MB(30) || !current) + goto cleanup; + + max = cg_read_key_long(memcg, "memory.events", "max "); + if (max <= 0) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + cg_destroy(memcg); + free(memcg); + + return ret; +} + +static int alloc_anon_50M_check_swap(const char *cgroup, void *arg) +{ + long mem_max = (long)arg; + size_t size = MB(50); + char *buf, *ptr; + long mem_current, swap_current; + int ret = -1; + + buf = malloc(size); + for (ptr = buf; ptr < buf + size; ptr += PAGE_SIZE) + *ptr = 0; + + mem_current = cg_read_long(cgroup, "memory.current"); + if (!mem_current || !values_close(mem_current, mem_max, 3)) + goto cleanup; + + swap_current = cg_read_long(cgroup, "memory.swap.current"); + if (!swap_current || + !values_close(mem_current + swap_current, size, 3)) + goto cleanup; + + ret = 0; +cleanup: + free(buf); + return ret; +} + +/* + * This test checks that memory.swap.max limits the amount of + * anonymous memory which can be swapped out. + */ +static int test_memcg_swap_max(const char *root) +{ + int ret = KSFT_FAIL; + char *memcg; + long max; + + if (!is_swap_enabled()) + return KSFT_SKIP; + + memcg = cg_name(root, "memcg_test"); + if (!memcg) + goto cleanup; + + if (cg_create(memcg)) + goto cleanup; + + if (cg_read_long(memcg, "memory.swap.current")) { + ret = KSFT_SKIP; + goto cleanup; + } + + if (cg_read_strcmp(memcg, "memory.max", "max\n")) + goto cleanup; + + if (cg_read_strcmp(memcg, "memory.swap.max", "max\n")) + goto cleanup; + + if (cg_write(memcg, "memory.swap.max", "30M")) + goto cleanup; + + if (cg_write(memcg, "memory.max", "30M")) + goto cleanup; + + /* Should be killed by OOM killer */ + if (!cg_run(memcg, alloc_anon, (void *)MB(100))) + goto cleanup; + + if (cg_read_key_long(memcg, "memory.events", "oom ") != 1) + goto cleanup; + + if (cg_read_key_long(memcg, "memory.events", "oom_kill ") != 1) + goto cleanup; + + if (cg_run(memcg, alloc_anon_50M_check_swap, (void *)MB(30))) + goto cleanup; + + max = cg_read_key_long(memcg, "memory.events", "max "); + if (max <= 0) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + cg_destroy(memcg); + free(memcg); + + return ret; +} + +/* + * This test disables swapping and tries to allocate anonymous memory + * up to OOM. Then it checks for oom and oom_kill events in + * memory.events. + */ +static int test_memcg_oom_events(const char *root) +{ + int ret = KSFT_FAIL; + char *memcg; + + memcg = cg_name(root, "memcg_test"); + if (!memcg) + goto cleanup; + + if (cg_create(memcg)) + goto cleanup; + + if (cg_write(memcg, "memory.max", "30M")) + goto cleanup; + + if (cg_write(memcg, "memory.swap.max", "0")) + goto cleanup; + + if (!cg_run(memcg, alloc_anon, (void *)MB(100))) + goto cleanup; + + if (cg_read_strcmp(memcg, "cgroup.procs", "")) + goto cleanup; + + if (cg_read_key_long(memcg, "memory.events", "oom ") != 1) + goto cleanup; + + if (cg_read_key_long(memcg, "memory.events", "oom_kill ") != 1) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + cg_destroy(memcg); + free(memcg); + + return ret; +} + +struct tcp_server_args { + unsigned short port; + int ctl[2]; +}; + +static int tcp_server(const char *cgroup, void *arg) +{ + struct tcp_server_args *srv_args = arg; + struct sockaddr_in6 saddr = { 0 }; + socklen_t slen = sizeof(saddr); + int sk, client_sk, ctl_fd, yes = 1, ret = -1; + + close(srv_args->ctl[0]); + ctl_fd = srv_args->ctl[1]; + + saddr.sin6_family = AF_INET6; + saddr.sin6_addr = in6addr_any; + saddr.sin6_port = htons(srv_args->port); + + sk = socket(AF_INET6, SOCK_STREAM, 0); + if (sk < 0) + return ret; + + if (setsockopt(sk, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) + goto cleanup; + + if (bind(sk, (struct sockaddr *)&saddr, slen)) { + write(ctl_fd, &errno, sizeof(errno)); + goto cleanup; + } + + if (listen(sk, 1)) + goto cleanup; + + ret = 0; + if (write(ctl_fd, &ret, sizeof(ret)) != sizeof(ret)) { + ret = -1; + goto cleanup; + } + + client_sk = accept(sk, NULL, NULL); + if (client_sk < 0) + goto cleanup; + + ret = -1; + for (;;) { + uint8_t buf[0x100000]; + + if (write(client_sk, buf, sizeof(buf)) <= 0) { + if (errno == ECONNRESET) + ret = 0; + break; + } + } + + close(client_sk); + +cleanup: + close(sk); + return ret; +} + +static int tcp_client(const char *cgroup, unsigned short port) +{ + const char server[] = "localhost"; + struct addrinfo *ai; + char servport[6]; + int retries = 0x10; /* nice round number */ + int sk, ret; + + snprintf(servport, sizeof(servport), "%hd", port); + ret = getaddrinfo(server, servport, NULL, &ai); + if (ret) + return ret; + + sk = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (sk < 0) + goto free_ainfo; + + ret = connect(sk, ai->ai_addr, ai->ai_addrlen); + if (ret < 0) + goto close_sk; + + ret = KSFT_FAIL; + while (retries--) { + uint8_t buf[0x100000]; + long current, sock; + + if (read(sk, buf, sizeof(buf)) <= 0) + goto close_sk; + + current = cg_read_long(cgroup, "memory.current"); + sock = cg_read_key_long(cgroup, "memory.stat", "sock "); + + if (current < 0 || sock < 0) + goto close_sk; + + if (current < sock) + goto close_sk; + + if (values_close(current, sock, 10)) { + ret = KSFT_PASS; + break; + } + } + +close_sk: + close(sk); +free_ainfo: + freeaddrinfo(ai); + return ret; +} + +/* + * This test checks socket memory accounting. + * The test forks a TCP server listens on a random port between 1000 + * and 61000. Once it gets a client connection, it starts writing to + * its socket. + * The TCP client interleaves reads from the socket with check whether + * memory.current and memory.stat.sock are similar. + */ +static int test_memcg_sock(const char *root) +{ + int bind_retries = 5, ret = KSFT_FAIL, pid, err; + unsigned short port; + char *memcg; + + memcg = cg_name(root, "memcg_test"); + if (!memcg) + goto cleanup; + + if (cg_create(memcg)) + goto cleanup; + + while (bind_retries--) { + struct tcp_server_args args; + + if (pipe(args.ctl)) + goto cleanup; + + port = args.port = 1000 + rand() % 60000; + + pid = cg_run_nowait(memcg, tcp_server, &args); + if (pid < 0) + goto cleanup; + + close(args.ctl[1]); + if (read(args.ctl[0], &err, sizeof(err)) != sizeof(err)) + goto cleanup; + close(args.ctl[0]); + + if (!err) + break; + if (err != EADDRINUSE) + goto cleanup; + + waitpid(pid, NULL, 0); + } + + if (err == EADDRINUSE) { + ret = KSFT_SKIP; + goto cleanup; + } + + if (tcp_client(memcg, port) != KSFT_PASS) + goto cleanup; + + waitpid(pid, &err, 0); + if (WEXITSTATUS(err)) + goto cleanup; + + if (cg_read_long(memcg, "memory.current") < 0) + goto cleanup; + + if (cg_read_key_long(memcg, "memory.stat", "sock ")) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + cg_destroy(memcg); + free(memcg); + + return ret; +} + +/* + * This test disables swapping and tries to allocate anonymous memory + * up to OOM with memory.group.oom set. Then it checks that all + * processes in the leaf (but not the parent) were killed. + */ +static int test_memcg_oom_group_leaf_events(const char *root) +{ + int ret = KSFT_FAIL; + char *parent, *child; + + parent = cg_name(root, "memcg_test_0"); + child = cg_name(root, "memcg_test_0/memcg_test_1"); + + if (!parent || !child) + goto cleanup; + + if (cg_create(parent)) + goto cleanup; + + if (cg_create(child)) + goto cleanup; + + if (cg_write(parent, "cgroup.subtree_control", "+memory")) + goto cleanup; + + if (cg_write(child, "memory.max", "50M")) + goto cleanup; + + if (cg_write(child, "memory.swap.max", "0")) + goto cleanup; + + if (cg_write(child, "memory.oom.group", "1")) + goto cleanup; + + cg_run_nowait(parent, alloc_anon_noexit, (void *) MB(60)); + cg_run_nowait(child, alloc_anon_noexit, (void *) MB(1)); + cg_run_nowait(child, alloc_anon_noexit, (void *) MB(1)); + if (!cg_run(child, alloc_anon, (void *)MB(100))) + goto cleanup; + + if (cg_test_proc_killed(child)) + goto cleanup; + + if (cg_read_key_long(child, "memory.events", "oom_kill ") <= 0) + goto cleanup; + + if (cg_read_key_long(parent, "memory.events", "oom_kill ") != 0) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + if (child) + cg_destroy(child); + if (parent) + cg_destroy(parent); + free(child); + free(parent); + + return ret; +} + +/* + * This test disables swapping and tries to allocate anonymous memory + * up to OOM with memory.group.oom set. Then it checks that all + * processes in the parent and leaf were killed. + */ +static int test_memcg_oom_group_parent_events(const char *root) +{ + int ret = KSFT_FAIL; + char *parent, *child; + + parent = cg_name(root, "memcg_test_0"); + child = cg_name(root, "memcg_test_0/memcg_test_1"); + + if (!parent || !child) + goto cleanup; + + if (cg_create(parent)) + goto cleanup; + + if (cg_create(child)) + goto cleanup; + + if (cg_write(parent, "memory.max", "80M")) + goto cleanup; + + if (cg_write(parent, "memory.swap.max", "0")) + goto cleanup; + + if (cg_write(parent, "memory.oom.group", "1")) + goto cleanup; + + cg_run_nowait(parent, alloc_anon_noexit, (void *) MB(60)); + cg_run_nowait(child, alloc_anon_noexit, (void *) MB(1)); + cg_run_nowait(child, alloc_anon_noexit, (void *) MB(1)); + + if (!cg_run(child, alloc_anon, (void *)MB(100))) + goto cleanup; + + if (cg_test_proc_killed(child)) + goto cleanup; + if (cg_test_proc_killed(parent)) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + if (child) + cg_destroy(child); + if (parent) + cg_destroy(parent); + free(child); + free(parent); + + return ret; +} + +/* + * This test disables swapping and tries to allocate anonymous memory + * up to OOM with memory.group.oom set. Then it checks that all + * processes were killed except those set with OOM_SCORE_ADJ_MIN + */ +static int test_memcg_oom_group_score_events(const char *root) +{ + int ret = KSFT_FAIL; + char *memcg; + int safe_pid; + + memcg = cg_name(root, "memcg_test_0"); + + if (!memcg) + goto cleanup; + + if (cg_create(memcg)) + goto cleanup; + + if (cg_write(memcg, "memory.max", "50M")) + goto cleanup; + + if (cg_write(memcg, "memory.swap.max", "0")) + goto cleanup; + + if (cg_write(memcg, "memory.oom.group", "1")) + goto cleanup; + + safe_pid = cg_run_nowait(memcg, alloc_anon_noexit, (void *) MB(1)); + if (set_oom_adj_score(safe_pid, OOM_SCORE_ADJ_MIN)) + goto cleanup; + + cg_run_nowait(memcg, alloc_anon_noexit, (void *) MB(1)); + if (!cg_run(memcg, alloc_anon, (void *)MB(100))) + goto cleanup; + + if (cg_read_key_long(memcg, "memory.events", "oom_kill ") != 3) + goto cleanup; + + if (kill(safe_pid, SIGKILL)) + goto cleanup; + + ret = KSFT_PASS; + +cleanup: + if (memcg) + cg_destroy(memcg); + free(memcg); + + return ret; +} + + +#define T(x) { x, #x } +struct memcg_test { + int (*fn)(const char *root); + const char *name; +} tests[] = { + T(test_memcg_subtree_control), + T(test_memcg_current), + T(test_memcg_min), + T(test_memcg_low), + T(test_memcg_high), + T(test_memcg_max), + T(test_memcg_oom_events), + T(test_memcg_swap_max), + T(test_memcg_sock), + T(test_memcg_oom_group_leaf_events), + T(test_memcg_oom_group_parent_events), + T(test_memcg_oom_group_score_events), +}; +#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"); + + /* + * Check that memory controller is available: + * memory is listed in cgroup.controllers + */ + if (cg_read_strstr(root, "cgroup.controllers", "memory")) + ksft_exit_skip("memory controller isn't available\n"); + + if (cg_read_strstr(root, "cgroup.subtree_control", "memory")) + if (cg_write(root, "cgroup.subtree_control", "+memory")) + ksft_exit_skip("Failed to set memory 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; +} |