711 lines · c
1// SPDX-License-Identifier: GPL-2.02#define _GNU_SOURCE3#include <sched.h>4#include <sys/mount.h>5#include <sys/stat.h>6#include <sys/types.h>7#include <linux/limits.h>8#include <stdio.h>9#include <stdlib.h>10#include <linux/sched.h>11#include <fcntl.h>12#include <unistd.h>13#include <ftw.h>14 15#include "cgroup_helpers.h"16#include "bpf_util.h"17 18/*19 * To avoid relying on the system setup, when setup_cgroup_env is called20 * we create a new mount namespace, and cgroup namespace. The cgroupv221 * root is mounted at CGROUP_MOUNT_PATH. Unfortunately, most people don't22 * have cgroupv2 enabled at this point in time. It's easier to create our23 * own mount namespace and manage it ourselves. We assume /mnt exists.24 *25 * Related cgroupv1 helpers are named *classid*(), since we only use the26 * net_cls controller for tagging net_cls.classid. We assume the default27 * mount under /sys/fs/cgroup/net_cls, which should be the case for the28 * vast majority of users.29 */30 31#define WALK_FD_LIMIT 1632 33#define CGROUP_MOUNT_PATH "/mnt"34#define CGROUP_MOUNT_DFLT "/sys/fs/cgroup"35#define NETCLS_MOUNT_PATH CGROUP_MOUNT_DFLT "/net_cls"36#define CGROUP_WORK_DIR "/cgroup-test-work-dir"37 38#define format_cgroup_path_pid(buf, path, pid) \39 snprintf(buf, sizeof(buf), "%s%s%d%s", CGROUP_MOUNT_PATH, \40 CGROUP_WORK_DIR, pid, path)41 42#define format_cgroup_path(buf, path) \43 format_cgroup_path_pid(buf, path, getpid())44 45#define format_parent_cgroup_path(buf, path) \46 format_cgroup_path_pid(buf, path, getppid())47 48#define format_classid_path_pid(buf, pid) \49 snprintf(buf, sizeof(buf), "%s%s%d", NETCLS_MOUNT_PATH, \50 CGROUP_WORK_DIR, pid)51 52#define format_classid_path(buf) \53 format_classid_path_pid(buf, getpid())54 55static __thread bool cgroup_workdir_mounted;56 57static void __cleanup_cgroup_environment(void);58 59static int __enable_controllers(const char *cgroup_path, const char *controllers)60{61 char path[PATH_MAX + 1];62 char enable[PATH_MAX + 1];63 char *c, *c2;64 int fd, cfd;65 ssize_t len;66 67 /* If not controllers are passed, enable all available controllers */68 if (!controllers) {69 snprintf(path, sizeof(path), "%s/cgroup.controllers",70 cgroup_path);71 fd = open(path, O_RDONLY);72 if (fd < 0) {73 log_err("Opening cgroup.controllers: %s", path);74 return 1;75 }76 len = read(fd, enable, sizeof(enable) - 1);77 if (len < 0) {78 close(fd);79 log_err("Reading cgroup.controllers: %s", path);80 return 1;81 } else if (len == 0) { /* No controllers to enable */82 close(fd);83 return 0;84 }85 enable[len] = 0;86 close(fd);87 } else {88 bpf_strlcpy(enable, controllers, sizeof(enable));89 }90 91 snprintf(path, sizeof(path), "%s/cgroup.subtree_control", cgroup_path);92 cfd = open(path, O_RDWR);93 if (cfd < 0) {94 log_err("Opening cgroup.subtree_control: %s", path);95 return 1;96 }97 98 for (c = strtok_r(enable, " ", &c2); c; c = strtok_r(NULL, " ", &c2)) {99 if (dprintf(cfd, "+%s\n", c) <= 0) {100 log_err("Enabling controller %s: %s", c, path);101 close(cfd);102 return 1;103 }104 }105 close(cfd);106 return 0;107}108 109/**110 * enable_controllers() - Enable cgroup v2 controllers111 * @relative_path: The cgroup path, relative to the workdir112 * @controllers: List of controllers to enable in cgroup.controllers format113 *114 *115 * Enable given cgroup v2 controllers, if @controllers is NULL, enable all116 * available controllers.117 *118 * If successful, 0 is returned.119 */120int enable_controllers(const char *relative_path, const char *controllers)121{122 char cgroup_path[PATH_MAX + 1];123 124 format_cgroup_path(cgroup_path, relative_path);125 return __enable_controllers(cgroup_path, controllers);126}127 128static int __write_cgroup_file(const char *cgroup_path, const char *file,129 const char *buf)130{131 char file_path[PATH_MAX + 1];132 int fd;133 134 snprintf(file_path, sizeof(file_path), "%s/%s", cgroup_path, file);135 fd = open(file_path, O_RDWR);136 if (fd < 0) {137 log_err("Opening %s", file_path);138 return 1;139 }140 141 if (dprintf(fd, "%s", buf) <= 0) {142 log_err("Writing to %s", file_path);143 close(fd);144 return 1;145 }146 close(fd);147 return 0;148}149 150/**151 * write_cgroup_file() - Write to a cgroup file152 * @relative_path: The cgroup path, relative to the workdir153 * @file: The name of the file in cgroupfs to write to154 * @buf: Buffer to write to the file155 *156 * Write to a file in the given cgroup's directory.157 *158 * If successful, 0 is returned.159 */160int write_cgroup_file(const char *relative_path, const char *file,161 const char *buf)162{163 char cgroup_path[PATH_MAX - 24];164 165 format_cgroup_path(cgroup_path, relative_path);166 return __write_cgroup_file(cgroup_path, file, buf);167}168 169/**170 * write_cgroup_file_parent() - Write to a cgroup file in the parent process171 * workdir172 * @relative_path: The cgroup path, relative to the parent process workdir173 * @file: The name of the file in cgroupfs to write to174 * @buf: Buffer to write to the file175 *176 * Write to a file in the given cgroup's directory under the parent process177 * workdir.178 *179 * If successful, 0 is returned.180 */181int write_cgroup_file_parent(const char *relative_path, const char *file,182 const char *buf)183{184 char cgroup_path[PATH_MAX - 24];185 186 format_parent_cgroup_path(cgroup_path, relative_path);187 return __write_cgroup_file(cgroup_path, file, buf);188}189 190/**191 * setup_cgroup_environment() - Setup the cgroup environment192 *193 * After calling this function, cleanup_cgroup_environment should be called194 * once testing is complete.195 *196 * This function will print an error to stderr and return 1 if it is unable197 * to setup the cgroup environment. If setup is successful, 0 is returned.198 */199int setup_cgroup_environment(void)200{201 char cgroup_workdir[PATH_MAX - 24];202 203 format_cgroup_path(cgroup_workdir, "");204 205 if (mkdir(CGROUP_MOUNT_PATH, 0777) && errno != EEXIST) {206 log_err("mkdir mount");207 return 1;208 }209 210 if (unshare(CLONE_NEWNS)) {211 log_err("unshare");212 return 1;213 }214 215 if (mount("none", "/", NULL, MS_REC | MS_PRIVATE, NULL)) {216 log_err("mount fakeroot");217 return 1;218 }219 220 if (mount("none", CGROUP_MOUNT_PATH, "cgroup2", 0, NULL) && errno != EBUSY) {221 log_err("mount cgroup2");222 return 1;223 }224 cgroup_workdir_mounted = true;225 226 /* Cleanup existing failed runs, now that the environment is setup */227 __cleanup_cgroup_environment();228 229 if (mkdir(cgroup_workdir, 0777) && errno != EEXIST) {230 log_err("mkdir cgroup work dir");231 return 1;232 }233 234 /* Enable all available controllers to increase test coverage */235 if (__enable_controllers(CGROUP_MOUNT_PATH, NULL) ||236 __enable_controllers(cgroup_workdir, NULL))237 return 1;238 239 return 0;240}241 242static int nftwfunc(const char *filename, const struct stat *statptr,243 int fileflags, struct FTW *pfwt)244{245 if ((fileflags & FTW_D) && rmdir(filename))246 log_err("Removing cgroup: %s", filename);247 return 0;248}249 250static int join_cgroup_from_top(const char *cgroup_path)251{252 char cgroup_procs_path[PATH_MAX + 1];253 pid_t pid = getpid();254 int fd, rc = 0;255 256 snprintf(cgroup_procs_path, sizeof(cgroup_procs_path),257 "%s/cgroup.procs", cgroup_path);258 259 fd = open(cgroup_procs_path, O_WRONLY);260 if (fd < 0) {261 log_err("Opening Cgroup Procs: %s", cgroup_procs_path);262 return 1;263 }264 265 if (dprintf(fd, "%d\n", pid) < 0) {266 log_err("Joining Cgroup");267 rc = 1;268 }269 270 close(fd);271 return rc;272}273 274/**275 * join_cgroup() - Join a cgroup276 * @relative_path: The cgroup path, relative to the workdir, to join277 *278 * This function expects a cgroup to already be created, relative to the cgroup279 * work dir, and it joins it. For example, passing "/my-cgroup" as the path280 * would actually put the calling process into the cgroup281 * "/cgroup-test-work-dir/my-cgroup"282 *283 * On success, it returns 0, otherwise on failure it returns 1.284 */285int join_cgroup(const char *relative_path)286{287 char cgroup_path[PATH_MAX + 1];288 289 format_cgroup_path(cgroup_path, relative_path);290 return join_cgroup_from_top(cgroup_path);291}292 293/**294 * join_root_cgroup() - Join the root cgroup295 *296 * This function joins the root cgroup.297 *298 * On success, it returns 0, otherwise on failure it returns 1.299 */300int join_root_cgroup(void)301{302 return join_cgroup_from_top(CGROUP_MOUNT_PATH);303}304 305/**306 * join_parent_cgroup() - Join a cgroup in the parent process workdir307 * @relative_path: The cgroup path, relative to parent process workdir, to join308 *309 * See join_cgroup().310 *311 * On success, it returns 0, otherwise on failure it returns 1.312 */313int join_parent_cgroup(const char *relative_path)314{315 char cgroup_path[PATH_MAX + 1];316 317 format_parent_cgroup_path(cgroup_path, relative_path);318 return join_cgroup_from_top(cgroup_path);319}320 321/**322 * __cleanup_cgroup_environment() - Delete temporary cgroups323 *324 * This is a helper for cleanup_cgroup_environment() that is responsible for325 * deletion of all temporary cgroups that have been created during the test.326 */327static void __cleanup_cgroup_environment(void)328{329 char cgroup_workdir[PATH_MAX + 1];330 331 format_cgroup_path(cgroup_workdir, "");332 join_cgroup_from_top(CGROUP_MOUNT_PATH);333 nftw(cgroup_workdir, nftwfunc, WALK_FD_LIMIT, FTW_DEPTH | FTW_MOUNT);334}335 336/**337 * cleanup_cgroup_environment() - Cleanup Cgroup Testing Environment338 *339 * This is an idempotent function to delete all temporary cgroups that340 * have been created during the test and unmount the cgroup testing work341 * directory.342 *343 * At call time, it moves the calling process to the root cgroup, and then344 * runs the deletion process. It is idempotent, and should not fail, unless345 * a process is lingering.346 *347 * On failure, it will print an error to stderr, and try to continue.348 */349void cleanup_cgroup_environment(void)350{351 __cleanup_cgroup_environment();352 if (cgroup_workdir_mounted && umount(CGROUP_MOUNT_PATH))353 log_err("umount cgroup2");354 cgroup_workdir_mounted = false;355}356 357/**358 * get_root_cgroup() - Get the FD of the root cgroup359 *360 * On success, it returns the file descriptor. On failure, it returns -1.361 * If there is a failure, it prints the error to stderr.362 */363int get_root_cgroup(void)364{365 int fd;366 367 fd = open(CGROUP_MOUNT_PATH, O_RDONLY);368 if (fd < 0) {369 log_err("Opening root cgroup");370 return -1;371 }372 return fd;373}374 375/*376 * remove_cgroup() - Remove a cgroup377 * @relative_path: The cgroup path, relative to the workdir, to remove378 *379 * This function expects a cgroup to already be created, relative to the cgroup380 * work dir. It also expects the cgroup doesn't have any children or live381 * processes and it removes the cgroup.382 *383 * On failure, it will print an error to stderr.384 */385void remove_cgroup(const char *relative_path)386{387 char cgroup_path[PATH_MAX + 1];388 389 format_cgroup_path(cgroup_path, relative_path);390 if (rmdir(cgroup_path))391 log_err("rmdiring cgroup %s .. %s", relative_path, cgroup_path);392}393 394/**395 * create_and_get_cgroup() - Create a cgroup, relative to workdir, and get the FD396 * @relative_path: The cgroup path, relative to the workdir, to join397 *398 * This function creates a cgroup under the top level workdir and returns the399 * file descriptor. It is idempotent.400 *401 * On success, it returns the file descriptor. On failure it returns -1.402 * If there is a failure, it prints the error to stderr.403 */404int create_and_get_cgroup(const char *relative_path)405{406 char cgroup_path[PATH_MAX + 1];407 int fd;408 409 format_cgroup_path(cgroup_path, relative_path);410 if (mkdir(cgroup_path, 0777) && errno != EEXIST) {411 log_err("mkdiring cgroup %s .. %s", relative_path, cgroup_path);412 return -1;413 }414 415 fd = open(cgroup_path, O_RDONLY);416 if (fd < 0) {417 log_err("Opening Cgroup");418 return -1;419 }420 421 return fd;422}423 424/**425 * get_cgroup_id_from_path - Get cgroup id for a particular cgroup path426 * @cgroup_workdir: The absolute cgroup path427 *428 * On success, it returns the cgroup id. On failure it returns 0,429 * which is an invalid cgroup id.430 * If there is a failure, it prints the error to stderr.431 */432static unsigned long long get_cgroup_id_from_path(const char *cgroup_workdir)433{434 int dirfd, err, flags, mount_id, fhsize;435 union {436 unsigned long long cgid;437 unsigned char raw_bytes[8];438 } id;439 struct file_handle *fhp, *fhp2;440 unsigned long long ret = 0;441 442 dirfd = AT_FDCWD;443 flags = 0;444 fhsize = sizeof(*fhp);445 fhp = calloc(1, fhsize);446 if (!fhp) {447 log_err("calloc");448 return 0;449 }450 err = name_to_handle_at(dirfd, cgroup_workdir, fhp, &mount_id, flags);451 if (err >= 0 || fhp->handle_bytes != 8) {452 log_err("name_to_handle_at");453 goto free_mem;454 }455 456 fhsize = sizeof(struct file_handle) + fhp->handle_bytes;457 fhp2 = realloc(fhp, fhsize);458 if (!fhp2) {459 log_err("realloc");460 goto free_mem;461 }462 err = name_to_handle_at(dirfd, cgroup_workdir, fhp2, &mount_id, flags);463 fhp = fhp2;464 if (err < 0) {465 log_err("name_to_handle_at");466 goto free_mem;467 }468 469 memcpy(id.raw_bytes, fhp->f_handle, 8);470 ret = id.cgid;471 472free_mem:473 free(fhp);474 return ret;475}476 477unsigned long long get_cgroup_id(const char *relative_path)478{479 char cgroup_workdir[PATH_MAX + 1];480 481 format_cgroup_path(cgroup_workdir, relative_path);482 return get_cgroup_id_from_path(cgroup_workdir);483}484 485int cgroup_setup_and_join(const char *path) {486 int cg_fd;487 488 if (setup_cgroup_environment()) {489 fprintf(stderr, "Failed to setup cgroup environment\n");490 return -EINVAL;491 }492 493 cg_fd = create_and_get_cgroup(path);494 if (cg_fd < 0) {495 fprintf(stderr, "Failed to create test cgroup\n");496 cleanup_cgroup_environment();497 return cg_fd;498 }499 500 if (join_cgroup(path)) {501 fprintf(stderr, "Failed to join cgroup\n");502 cleanup_cgroup_environment();503 return -EINVAL;504 }505 return cg_fd;506}507 508/**509 * setup_classid_environment() - Setup the cgroupv1 net_cls environment510 *511 * This function should only be called in a custom mount namespace, e.g.512 * created by running setup_cgroup_environment.513 *514 * After calling this function, cleanup_classid_environment should be called515 * once testing is complete.516 *517 * This function will print an error to stderr and return 1 if it is unable518 * to setup the cgroup environment. If setup is successful, 0 is returned.519 */520int setup_classid_environment(void)521{522 char cgroup_workdir[PATH_MAX + 1];523 524 format_classid_path(cgroup_workdir);525 526 if (mount("tmpfs", CGROUP_MOUNT_DFLT, "tmpfs", 0, NULL) &&527 errno != EBUSY) {528 log_err("mount cgroup base");529 return 1;530 }531 532 if (mkdir(NETCLS_MOUNT_PATH, 0777) && errno != EEXIST) {533 log_err("mkdir cgroup net_cls");534 return 1;535 }536 537 if (mount("net_cls", NETCLS_MOUNT_PATH, "cgroup", 0, "net_cls")) {538 if (errno != EBUSY) {539 log_err("mount cgroup net_cls");540 return 1;541 }542 543 if (rmdir(NETCLS_MOUNT_PATH)) {544 log_err("rmdir cgroup net_cls");545 return 1;546 }547 if (umount(CGROUP_MOUNT_DFLT)) {548 log_err("umount cgroup base");549 return 1;550 }551 }552 553 cleanup_classid_environment();554 555 if (mkdir(cgroup_workdir, 0777) && errno != EEXIST) {556 log_err("mkdir cgroup work dir");557 return 1;558 }559 560 return 0;561}562 563/**564 * set_classid() - Set a cgroupv1 net_cls classid565 *566 * Writes the classid into the cgroup work dir's net_cls.classid567 * file in order to later on trigger socket tagging.568 *569 * We leverage the current pid as the classid, ensuring unique identification.570 *571 * On success, it returns 0, otherwise on failure it returns 1. If there572 * is a failure, it prints the error to stderr.573 */574int set_classid(void)575{576 char cgroup_workdir[PATH_MAX - 42];577 char cgroup_classid_path[PATH_MAX + 1];578 int fd, rc = 0;579 580 format_classid_path(cgroup_workdir);581 snprintf(cgroup_classid_path, sizeof(cgroup_classid_path),582 "%s/net_cls.classid", cgroup_workdir);583 584 fd = open(cgroup_classid_path, O_WRONLY);585 if (fd < 0) {586 log_err("Opening cgroup classid: %s", cgroup_classid_path);587 return 1;588 }589 590 if (dprintf(fd, "%u\n", getpid()) < 0) {591 log_err("Setting cgroup classid");592 rc = 1;593 }594 595 close(fd);596 return rc;597}598 599/**600 * join_classid() - Join a cgroupv1 net_cls classid601 *602 * This function expects the cgroup work dir to be already created, as we603 * join it here. This causes the process sockets to be tagged with the given604 * net_cls classid.605 *606 * On success, it returns 0, otherwise on failure it returns 1.607 */608int join_classid(void)609{610 char cgroup_workdir[PATH_MAX + 1];611 612 format_classid_path(cgroup_workdir);613 return join_cgroup_from_top(cgroup_workdir);614}615 616/**617 * cleanup_classid_environment() - Cleanup the cgroupv1 net_cls environment618 *619 * At call time, it moves the calling process to the root cgroup, and then620 * runs the deletion process.621 *622 * On failure, it will print an error to stderr, and try to continue.623 */624void cleanup_classid_environment(void)625{626 char cgroup_workdir[PATH_MAX + 1];627 628 format_classid_path(cgroup_workdir);629 join_cgroup_from_top(NETCLS_MOUNT_PATH);630 nftw(cgroup_workdir, nftwfunc, WALK_FD_LIMIT, FTW_DEPTH | FTW_MOUNT);631}632 633/**634 * get_classid_cgroup_id - Get the cgroup id of a net_cls cgroup635 */636unsigned long long get_classid_cgroup_id(void)637{638 char cgroup_workdir[PATH_MAX + 1];639 640 format_classid_path(cgroup_workdir);641 return get_cgroup_id_from_path(cgroup_workdir);642}643 644/**645 * get_cgroup1_hierarchy_id - Retrieves the ID of a cgroup1 hierarchy from the cgroup1 subsys name.646 * @subsys_name: The cgroup1 subsys name, which can be retrieved from /proc/self/cgroup. It can be647 * a named cgroup like "name=systemd", a controller name like "net_cls", or multi-controllers like648 * "net_cls,net_prio".649 */650int get_cgroup1_hierarchy_id(const char *subsys_name)651{652 char *c, *c2, *c3, *c4;653 bool found = false;654 char line[1024];655 FILE *file;656 int i, id;657 658 if (!subsys_name)659 return -1;660 661 file = fopen("/proc/self/cgroup", "r");662 if (!file) {663 log_err("fopen /proc/self/cgroup");664 return -1;665 }666 667 while (fgets(line, 1024, file)) {668 i = 0;669 for (c = strtok_r(line, ":", &c2); c && i < 2; c = strtok_r(NULL, ":", &c2)) {670 if (i == 0) {671 id = strtol(c, NULL, 10);672 } else if (i == 1) {673 if (!strcmp(c, subsys_name)) {674 found = true;675 break;676 }677 678 /* Multiple subsystems may share one single mount point */679 for (c3 = strtok_r(c, ",", &c4); c3;680 c3 = strtok_r(NULL, ",", &c4)) {681 if (!strcmp(c, subsys_name)) {682 found = true;683 break;684 }685 }686 }687 i++;688 }689 if (found)690 break;691 }692 fclose(file);693 return found ? id : -1;694}695 696/**697 * open_classid() - Open a cgroupv1 net_cls classid698 *699 * This function expects the cgroup work dir to be already created, as we700 * open it here.701 *702 * On success, it returns the file descriptor. On failure it returns -1.703 */704int open_classid(void)705{706 char cgroup_workdir[PATH_MAX + 1];707 708 format_classid_path(cgroup_workdir);709 return open(cgroup_workdir, O_RDONLY);710}711