// SPDX-License-Identifier: GPL-2.0 #define _GNU_SOURCE #include <sched.h> #include <sys/mount.h> #include <sys/stat.h> #include <sys/types.h> #include <linux/limits.h> #include <stdio.h> #include <stdlib.h> #include <linux/sched.h> #include <fcntl.h> #include <unistd.h> #include <ftw.h> #include "cgroup_helpers.h" /* * To avoid relying on the system setup, when setup_cgroup_env is called * we create a new mount namespace, and cgroup namespace. The cgroup2 * root is mounted at CGROUP_MOUNT_PATH * * Unfortunately, most people don't have cgroupv2 enabled at this point in time. * It's easier to create our own mount namespace and manage it ourselves. * * We assume /mnt exists. */ #define WALK_FD_LIMIT 16 #define CGROUP_MOUNT_PATH "/mnt" #define CGROUP_WORK_DIR "/cgroup-test-work-dir" #define format_cgroup_path(buf, path) \ snprintf(buf, sizeof(buf), "%s%s%s", CGROUP_MOUNT_PATH, \ CGROUP_WORK_DIR, path) /** * setup_cgroup_environment() - Setup the cgroup environment * * After calling this function, cleanup_cgroup_environment should be called * once testing is complete. * * This function will print an error to stderr and return 1 if it is unable * to setup the cgroup environment. If setup is successful, 0 is returned. */ int setup_cgroup_environment(void) { char cgroup_workdir[PATH_MAX + 1]; format_cgroup_path(cgroup_workdir, ""); if (unshare(CLONE_NEWNS)) { log_err("unshare"); return 1; } if (mount("none", "/", NULL, MS_REC | MS_PRIVATE, NULL)) { log_err("mount fakeroot"); return 1; } if (mount("none", CGROUP_MOUNT_PATH, "cgroup2", 0, NULL) && errno != EBUSY) { log_err("mount cgroup2"); return 1; } /* Cleanup existing failed runs, now that the environment is setup */ cleanup_cgroup_environment(); if (mkdir(cgroup_workdir, 0777) && errno != EEXIST) { log_err("mkdir cgroup work dir"); return 1; } return 0; } static int nftwfunc(const char *filename, const struct stat *statptr, int fileflags, struct FTW *pfwt) { if ((fileflags & FTW_D) && rmdir(filename)) log_err("Removing cgroup: %s", filename); return 0; } static int join_cgroup_from_top(char *cgroup_path) { char cgroup_procs_path[PATH_MAX + 1]; pid_t pid = getpid(); int fd, rc = 0; snprintf(cgroup_procs_path, sizeof(cgroup_procs_path), "%s/cgroup.procs", cgroup_path); fd = open(cgroup_procs_path, O_WRONLY); if (fd < 0) { log_err("Opening Cgroup Procs: %s", cgroup_procs_path); return 1; } if (dprintf(fd, "%d\n", pid) < 0) { log_err("Joining Cgroup"); rc = 1; } close(fd); return rc; } /** * join_cgroup() - Join a cgroup * @path: The cgroup path, relative to the workdir, to join * * This function expects a cgroup to already be created, relative to the cgroup * work dir, and it joins it. For example, passing "/my-cgroup" as the path * would actually put the calling process into the cgroup * "/cgroup-test-work-dir/my-cgroup" * * On success, it returns 0, otherwise on failure it returns 1. */ int join_cgroup(const char *path) { char cgroup_path[PATH_MAX + 1]; format_cgroup_path(cgroup_path, path); return join_cgroup_from_top(cgroup_path); } /** * cleanup_cgroup_environment() - Cleanup Cgroup Testing Environment * * This is an idempotent function to delete all temporary cgroups that * have been created during the test, including the cgroup testing work * directory. * * At call time, it moves the calling process to the root cgroup, and then * runs the deletion process. It is idempotent, and should not fail, unless * a process is lingering. * * On failure, it will print an error to stderr, and try to continue. */ void cleanup_cgroup_environment(void) { char cgroup_workdir[PATH_MAX + 1]; format_cgroup_path(cgroup_workdir, ""); join_cgroup_from_top(CGROUP_MOUNT_PATH); nftw(cgroup_workdir, nftwfunc, WALK_FD_LIMIT, FTW_DEPTH | FTW_MOUNT); } /** * create_and_get_cgroup() - Create a cgroup, relative to workdir, and get the FD * @path: The cgroup path, relative to the workdir, to join * * This function creates a cgroup under the top level workdir and returns the * file descriptor. It is idempotent. * * On success, it returns the file descriptor. On failure it returns 0. * If there is a failure, it prints the error to stderr. */ int create_and_get_cgroup(const char *path) { char cgroup_path[PATH_MAX + 1]; int fd; format_cgroup_path(cgroup_path, path); if (mkdir(cgroup_path, 0777) && errno != EEXIST) { log_err("mkdiring cgroup %s .. %s", path, cgroup_path); return 0; } fd = open(cgroup_path, O_RDONLY); if (fd < 0) { log_err("Opening Cgroup"); return 0; } return fd; } /** * get_cgroup_id() - Get cgroup id for a particular cgroup path * @path: The cgroup path, relative to the workdir, to join * * On success, it returns the cgroup id. On failure it returns 0, * which is an invalid cgroup id. * If there is a failure, it prints the error to stderr. */ unsigned long long get_cgroup_id(const char *path) { int dirfd, err, flags, mount_id, fhsize; union { unsigned long long cgid; unsigned char raw_bytes[8]; } id; char cgroup_workdir[PATH_MAX + 1]; struct file_handle *fhp, *fhp2; unsigned long long ret = 0; format_cgroup_path(cgroup_workdir, path); dirfd = AT_FDCWD; flags = 0; fhsize = sizeof(*fhp); fhp = calloc(1, fhsize); if (!fhp) { log_err("calloc"); return 0; } err = name_to_handle_at(dirfd, cgroup_workdir, fhp, &mount_id, flags); if (err >= 0 || fhp->handle_bytes != 8) { log_err("name_to_handle_at"); goto free_mem; } fhsize = sizeof(struct file_handle) + fhp->handle_bytes; fhp2 = realloc(fhp, fhsize); if (!fhp2) { log_err("realloc"); goto free_mem; } err = name_to_handle_at(dirfd, cgroup_workdir, fhp2, &mount_id, flags); fhp = fhp2; if (err < 0) { log_err("name_to_handle_at"); goto free_mem; } memcpy(id.raw_bytes, fhp->f_handle, 8); ret = id.cgid; free_mem: free(fhp); return ret; }