blob: 0795dee20a6194cd739a55858c1907bc423e728a [file] [log] [blame]
// Copyright 2024 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <dirent.h>
#include <fcntl.h>
#include <poll.h>
#include <sys/mount.h>
#include <unistd.h>
#include <algorithm>
#include <cerrno>
#include <fstream>
#include <string>
#include <gtest/gtest.h>
#include "src/starnix/tests/syscalls/cpp/syscall_matchers.h"
#include "src/starnix/tests/syscalls/cpp/test_helper.h"
constexpr char CONTROLLERS_FILE[] = "cgroup.controllers";
constexpr char PROCS_FILE[] = "cgroup.procs";
constexpr char FREEZE_FILE[] = "cgroup.freeze";
constexpr char EVENTS_FILE[] = "cgroup.events";
constexpr char KILL_FILE[] = "cgroup.kill";
constexpr char EVENTS_POPULATED[] = "populated 1";
constexpr char EVENTS_NOT_POPULATED[] = "populated 0";
constexpr char PROC_CGROUP_PREFIX[] = "0::";
// Mounts cgroup2 in a temporary directory for each test case, and deletes all cgroups created by
// `CreateCgroup` at the end of each test, and all mountpoints of the cgroup.
class CgroupTest : public ::testing::Test {
public:
void SetUp() override {
if (!test_helper::HasSysAdmin()) {
// From https://docs.kernel.org/admin-guide/cgroup-v2.html#interaction-with-other-namespaces
// mounting cgroup requires CAP_SYS_ADMIN.
GTEST_SKIP() << "requires CAP_SYS_ADMIN to mount cgroup";
}
MountCgroup2();
}
void TearDown() override {
if (!test_helper::HasSysAdmin()) {
// `TearDown` is still called for skipped tests, and the below assertions can fail.
return;
}
// Remove paths created by the test in reverse creation order.
// cgroup2 filesystem persists on the system after umounting, and lingering subdirectories can
// cause subsequent tests to fail.
for (auto path = cgroup_paths_.rbegin(); path != cgroup_paths_.rend(); path++) {
ASSERT_THAT(rmdir(path->c_str()), SyscallSucceeds()) << "Could not delete " << *path << "";
}
for (auto mountpoint = cgroup_mountpoints_.rbegin(); mountpoint != cgroup_mountpoints_.rend();
mountpoint++) {
ASSERT_THAT(umount((mountpoint->path()).c_str()), SyscallSucceeds());
}
}
// Returns the path of the first mountpoint.
std::string root_path() { return cgroup_mountpoints_[0].path(); }
// Creates a temp directory and mount cgroup2 on it. Returns the mountpoint path.
std::string MountCgroup2() {
auto& mountpoint = cgroup_mountpoints_.emplace_back();
EXPECT_THAT(mount(nullptr, mountpoint.path().c_str(), "cgroup2", 0, nullptr),
SyscallSucceeds());
return mountpoint.path();
}
static void CheckInterfaceFilesExist(const std::string& path, bool is_root) {
std::string controllers_path = path + "/" + CONTROLLERS_FILE;
std::string procs_path = path + "/" + PROCS_FILE;
std::string freeze_path = path + "/" + FREEZE_FILE;
std::string events_path = path + "/" + EVENTS_FILE;
struct stat buffer;
ASSERT_THAT(stat(controllers_path.c_str(), &buffer), SyscallSucceeds());
ASSERT_THAT(stat(procs_path.c_str(), &buffer), SyscallSucceeds());
if (is_root) {
ASSERT_THAT(stat(freeze_path.c_str(), &buffer), SyscallFailsWithErrno(ENOENT));
ASSERT_THAT(stat(events_path.c_str(), &buffer), SyscallFailsWithErrno(ENOENT));
} else {
ASSERT_THAT(stat(freeze_path.c_str(), &buffer), SyscallSucceeds());
ASSERT_THAT(stat(events_path.c_str(), &buffer), SyscallSucceeds());
}
}
struct ExpectedEntry {
std::string name;
unsigned char type;
};
static void CheckDirectoryIncludes(const std::string& path,
const std::vector<ExpectedEntry>& expected) {
DIR* dir = opendir(path.c_str());
ASSERT_TRUE(dir);
std::unordered_map<std::string, unsigned char> entry_types;
while (struct dirent* entry = readdir(dir)) {
entry_types.emplace(std::string(entry->d_name), entry->d_type);
}
closedir(dir);
for (const ExpectedEntry& entry : expected) {
auto found = entry_types.find(entry.name);
ASSERT_NE(found, entry_types.end()) << entry.name << " not found in directory";
EXPECT_EQ(found->second, entry.type);
}
}
static testing::AssertionResult CheckFileForLine(const std::string& path, const std::string& line,
const bool should_exist) {
std::ifstream file(path);
if (!file.is_open()) {
return testing::AssertionFailure() << "Unable to open " << path;
}
std::string file_line;
while (std::getline(file, file_line)) {
if (line == file_line) {
if (should_exist) {
return testing::AssertionSuccess();
}
return testing::AssertionFailure() << "Unexpectedly found " << line << " in " << path;
}
}
if (should_exist) {
return testing::AssertionFailure() << "Could not find " << line << " in " << path;
}
return testing::AssertionSuccess();
}
static testing::AssertionResult CheckFileHasLine(const std::string& path,
const std::string& line) {
return CheckFileForLine(path, line, true);
}
static testing::AssertionResult CheckFileDoesNotHaveLine(const std::string& path,
const std::string& line) {
return CheckFileForLine(path, line, false);
}
void CreateCgroup(std::string path) {
ASSERT_THAT(mkdir(path.c_str(), 0777), SyscallSucceeds()) << "Could not create " << path;
cgroup_paths_.push_back(std::move(path));
}
void DeleteCgroup(const std::string& path) {
auto it = std::ranges::find(cgroup_paths_, path);
ASSERT_NE(it, cgroup_paths_.end()) << path << " not found";
ASSERT_THAT(rmdir(path.c_str()), SyscallSucceeds()) << "Could not delete " << path;
cgroup_paths_.erase(it);
}
private:
// Paths to be removed after a test has completed.
std::vector<std::string> cgroup_paths_;
// Mountpoints to be unmounted after a test has completed.
std::vector<test_helper::ScopedTempDir> cgroup_mountpoints_;
};
TEST_F(CgroupTest, InterfaceFilesForRoot) { CheckInterfaceFilesExist(root_path(), true); }
// This test checks that nodes created as part of cgroups have the same inode each time it is
// accessed, which is seen on Linux.
TEST_F(CgroupTest, InodeNumbersAreConsistent) {
std::string controllers_path = root_path() + "/" + CONTROLLERS_FILE;
struct stat buffer1, buffer2;
ASSERT_THAT(stat(controllers_path.c_str(), &buffer1), SyscallSucceeds());
ASSERT_THAT(stat(controllers_path.c_str(), &buffer2), SyscallSucceeds());
EXPECT_EQ(buffer1.st_ino, buffer2.st_ino);
}
TEST_F(CgroupTest, ReadDir) {
CheckDirectoryIncludes(root_path(), {
{.name = PROCS_FILE, .type = DT_REG},
{.name = CONTROLLERS_FILE, .type = DT_REG},
});
std::string child1 = "child1";
CreateCgroup(root_path() + "/" + child1);
CheckDirectoryIncludes(root_path(), {
{.name = PROCS_FILE, .type = DT_REG},
{.name = CONTROLLERS_FILE, .type = DT_REG},
{.name = child1, .type = DT_DIR},
});
std::string child2 = "child2";
CreateCgroup(root_path() + "/" + child2);
CheckDirectoryIncludes(root_path(), {
{.name = PROCS_FILE, .type = DT_REG},
{.name = CONTROLLERS_FILE, .type = DT_REG},
{.name = child1, .type = DT_DIR},
{.name = child2, .type = DT_DIR},
});
}
TEST_F(CgroupTest, CreateSubgroups) {
std::string child1_path = root_path() + "/child1";
CreateCgroup(child1_path);
CheckInterfaceFilesExist(child1_path, false);
std::string child2_path = root_path() + "/child2";
CreateCgroup(child2_path);
CheckInterfaceFilesExist(child2_path, false);
std::string grandchild_path = root_path() + "/child2/grandchild";
CreateCgroup(grandchild_path);
CheckInterfaceFilesExist(grandchild_path, false);
}
TEST_F(CgroupTest, CreateSubgroupAlreadyExists) {
std::string child_path = root_path() + "/child";
CreateCgroup(child_path);
ASSERT_THAT(mkdir(child_path.c_str(), 0777), SyscallFailsWithErrno(EEXIST));
}
TEST_F(CgroupTest, WriteToInterfaceFileAfterCgroupIsDeleted) {
std::string child_path = root_path() + "/child";
std::string child_procs_path = child_path + "/" + PROCS_FILE;
CreateCgroup(child_path);
fbl::unique_fd child_procs_fd(open(child_procs_path.c_str(), O_WRONLY));
ASSERT_TRUE(child_procs_fd.is_valid());
DeleteCgroup(child_path);
std::string pid_string = std::to_string(getpid());
EXPECT_THAT(write(child_procs_fd.get(), pid_string.c_str(), pid_string.length()),
SyscallFailsWithErrno(ENODEV));
}
TEST_F(CgroupTest, MoveProcessToCgroup) {
std::string root_procs_path = root_path() + "/" + PROCS_FILE;
std::string child_path = root_path() + "/child";
std::string child_procs_path = child_path + "/" + PROCS_FILE;
std::string child_events_path = child_path + "/" + EVENTS_FILE;
std::string pid_string = std::to_string(getpid());
CreateCgroup(child_path);
ASSERT_TRUE(CheckFileHasLine(child_events_path, EVENTS_NOT_POPULATED));
{
// Write pid to /child/cgroup.procs
fbl::unique_fd child_procs_fd(open(child_procs_path.c_str(), O_WRONLY));
ASSERT_TRUE(child_procs_fd.is_valid());
EXPECT_THAT(write(child_procs_fd.get(), pid_string.c_str(), pid_string.length()),
SyscallSucceeds());
}
ASSERT_TRUE(CheckFileDoesNotHaveLine(root_procs_path, pid_string));
ASSERT_TRUE(CheckFileHasLine(child_procs_path, pid_string));
ASSERT_TRUE(CheckFileHasLine(child_events_path, EVENTS_POPULATED));
{
// Write pid to /cgroup.procs
fbl::unique_fd procs_fd(open(root_procs_path.c_str(), O_WRONLY));
ASSERT_TRUE(procs_fd.is_valid());
EXPECT_THAT(write(procs_fd.get(), pid_string.c_str(), pid_string.length()), SyscallSucceeds());
}
ASSERT_TRUE(CheckFileDoesNotHaveLine(child_procs_path, pid_string));
ASSERT_TRUE(CheckFileHasLine(root_procs_path, pid_string));
ASSERT_TRUE(CheckFileHasLine(child_events_path, EVENTS_NOT_POPULATED));
}
TEST_F(CgroupTest, EventsWithPopulatedChild) {
std::string root_procs_path = root_path() + "/" + PROCS_FILE;
std::string child_path = root_path() + "/child";
std::string child_events_path = child_path + "/" + EVENTS_FILE;
std::string grandchild_path = child_path + "/grandchild";
std::string grandchild_procs_path = grandchild_path + "/" + PROCS_FILE;
std::string grandchild_events_path = grandchild_path + "/" + EVENTS_FILE;
std::string pid_string = std::to_string(getpid());
CreateCgroup(child_path);
CreateCgroup(grandchild_path);
ASSERT_TRUE(CheckFileHasLine(child_events_path, EVENTS_NOT_POPULATED));
ASSERT_TRUE(CheckFileHasLine(grandchild_events_path, EVENTS_NOT_POPULATED));
{
// Write pid to /child/grandchild/cgroup.procs
fbl::unique_fd child_procs_fd(open(grandchild_procs_path.c_str(), O_WRONLY));
ASSERT_TRUE(child_procs_fd.is_valid());
EXPECT_THAT(write(child_procs_fd.get(), pid_string.c_str(), pid_string.length()),
SyscallSucceeds());
}
ASSERT_TRUE(CheckFileHasLine(child_events_path, EVENTS_POPULATED));
ASSERT_TRUE(CheckFileHasLine(grandchild_events_path, EVENTS_POPULATED));
{
// Write pid to /cgroup.procs
fbl::unique_fd procs_fd(open(root_procs_path.c_str(), O_WRONLY));
ASSERT_TRUE(procs_fd.is_valid());
EXPECT_THAT(write(procs_fd.get(), pid_string.c_str(), pid_string.length()), SyscallSucceeds());
}
ASSERT_TRUE(CheckFileHasLine(child_events_path, EVENTS_NOT_POPULATED));
ASSERT_TRUE(CheckFileHasLine(grandchild_events_path, EVENTS_NOT_POPULATED));
}
TEST_F(CgroupTest, PollEvents) {
std::string child_path = root_path() + "/child";
std::string child_events_path = child_path + "/" + EVENTS_FILE;
std::string child_procs_path = child_path + "/" + PROCS_FILE;
std::string pid_string = std::to_string(getpid());
CreateCgroup(child_path);
fbl::unique_fd events_fd(open(child_events_path.c_str(), O_RDONLY));
ASSERT_TRUE(events_fd.is_valid());
// Initially, the cgroup should not be populated.
ASSERT_TRUE(CheckFileHasLine(child_events_path, EVENTS_NOT_POPULATED));
struct pollfd pfd = {.fd = events_fd.get(), .events = POLLPRI};
fbl::unique_fd procs_fd(open(child_procs_path.c_str(), O_WRONLY));
ASSERT_TRUE(procs_fd.is_valid());
EXPECT_THAT(write(procs_fd.get(), pid_string.c_str(), pid_string.length()), SyscallSucceeds());
// After adding the process, poll should return with POLLPRI as populated changes to true.
EXPECT_THAT(poll(&pfd, 1, -1), SyscallSucceedsWithValue(1));
EXPECT_TRUE(pfd.revents & (POLLPRI | POLLERR));
// Verify the populated state has changed.
ASSERT_TRUE(CheckFileHasLine(child_events_path, EVENTS_POPULATED));
// Now remove the process from the cgroup.
std::string root_procs_path = root_path() + "/" + PROCS_FILE;
procs_fd.reset(open(root_procs_path.c_str(), O_WRONLY));
ASSERT_TRUE(procs_fd.is_valid());
EXPECT_THAT(write(procs_fd.get(), pid_string.c_str(), pid_string.length()), SyscallSucceeds());
// Poll should return with POLLPRI as populated changes back to false.
EXPECT_THAT(poll(&pfd, 1, -1), SyscallSucceedsWithValue(1));
EXPECT_TRUE(pfd.revents & (POLLPRI | POLLERR));
// Verify the populated state has changed.
ASSERT_TRUE(CheckFileHasLine(child_events_path, EVENTS_NOT_POPULATED));
}
TEST_F(CgroupTest, UnlinkCgroupWithProcess) {
std::string root_procs_path = root_path() + "/" + PROCS_FILE;
std::string child_path = root_path() + "/child";
std::string child_procs_path = child_path + "/" + PROCS_FILE;
std::string pid_string = std::to_string(getpid());
CreateCgroup(child_path);
{
fbl::unique_fd child_procs_fd(open(child_procs_path.c_str(), O_WRONLY));
ASSERT_TRUE(child_procs_fd.is_valid());
EXPECT_THAT(write(child_procs_fd.get(), pid_string.c_str(), pid_string.length()),
SyscallSucceeds());
}
ASSERT_THAT(rmdir(child_path.c_str()), SyscallFailsWithErrno(EBUSY));
{
fbl::unique_fd procs_fd(open(root_procs_path.c_str(), O_WRONLY));
ASSERT_TRUE(procs_fd.is_valid());
EXPECT_THAT(write(procs_fd.get(), pid_string.c_str(), pid_string.length()), SyscallSucceeds());
}
}
TEST_F(CgroupTest, UnlinkCgroupWithChildren) {
std::string child_path = root_path() + "/child";
std::string grandchild_path = child_path + "/grandchild";
CreateCgroup(child_path);
CreateCgroup(grandchild_path);
ASSERT_THAT(rmdir(child_path.c_str()), SyscallFailsWithErrno(EBUSY));
}
TEST_F(CgroupTest, EventsFileSeekable) {
std::string child_path = root_path() + "/child";
std::string events_path = child_path + "/" + EVENTS_FILE;
CreateCgroup(child_path);
fbl::unique_fd events_fd(open(events_path.c_str(), O_RDONLY));
ASSERT_TRUE(events_fd.is_valid());
// Seek exactly 10 bytes over, skipping "populated ". The next byte read should be 1 or 0
// indicating whether the cgroup is populated or not, respectively.
EXPECT_THAT(lseek(events_fd.get(), 10, SEEK_SET), SyscallSucceeds());
char buffer;
EXPECT_THAT(read(events_fd.get(), &buffer, 1), SyscallSucceeds());
EXPECT_EQ(buffer, '0');
}
TEST_F(CgroupTest, KillEmptyCgroup) {
std::string child_path = root_path() + "/child";
std::string child_kill_path = child_path + "/" + KILL_FILE;
CreateCgroup(child_path);
{
fbl::unique_fd child_kill_fd(open(child_kill_path.c_str(), O_WRONLY));
ASSERT_TRUE(child_kill_fd.is_valid());
EXPECT_THAT(write(child_kill_fd.get(), "1", 1), SyscallSucceeds());
}
}
TEST_F(CgroupTest, KillCgroupWithProcess) {
std::string child_path = root_path() + "/child";
std::string child_procs_path = child_path + "/" + PROCS_FILE;
std::string child_events_path = child_path + "/" + EVENTS_FILE;
std::string child_kill_path = child_path + "/" + KILL_FILE;
CreateCgroup(child_path);
test_helper::ForkHelper fork_helper;
fork_helper.OnlyWaitForForkedChildren();
fork_helper.ExpectSignal(SIGKILL);
pid_t child_pid = fork_helper.RunInForkedProcess([]() {
// Child process blocks forever.
while (true) {
pause();
}
});
// Move forked child to /child/cgroup.procs
{
std::string pid_string = std::to_string(child_pid);
fbl::unique_fd child_procs_fd(open(child_procs_path.c_str(), O_WRONLY));
ASSERT_TRUE(child_procs_fd.is_valid());
EXPECT_THAT(write(child_procs_fd.get(), pid_string.c_str(), pid_string.length()),
SyscallSucceeds());
}
ASSERT_TRUE(CheckFileHasLine(child_events_path, EVENTS_POPULATED));
{
fbl::unique_fd child_kill_fd(open(child_kill_path.c_str(), O_WRONLY));
ASSERT_TRUE(child_kill_fd.is_valid());
EXPECT_THAT(write(child_kill_fd.get(), "1", 1), SyscallSucceeds());
}
EXPECT_TRUE(fork_helper.WaitForChildren());
ASSERT_TRUE(CheckFileHasLine(child_events_path, EVENTS_NOT_POPULATED));
}
TEST_F(CgroupTest, KillCgroupWithDescendant) {
std::string child_path = root_path() + "/child";
std::string grandchild_path = child_path + "/grandchild";
std::string grandchild_procs_path = grandchild_path + "/" + PROCS_FILE;
std::string grandchild_events_path = grandchild_path + "/" + EVENTS_FILE;
std::string grandchild_kill_path = grandchild_path + "/" + KILL_FILE;
CreateCgroup(child_path);
CreateCgroup(grandchild_path);
test_helper::ForkHelper fork_helper;
fork_helper.OnlyWaitForForkedChildren();
fork_helper.ExpectSignal(SIGKILL);
pid_t child_pid = fork_helper.RunInForkedProcess([]() {
// Child process blocks forever.
while (true) {
pause();
}
});
// Move forked child to /child/grandchild/cgroup.procs
{
std::string pid_string = std::to_string(child_pid);
fbl::unique_fd child_procs_fd(open(grandchild_procs_path.c_str(), O_WRONLY));
ASSERT_TRUE(child_procs_fd.is_valid());
EXPECT_THAT(write(child_procs_fd.get(), pid_string.c_str(), pid_string.length()),
SyscallSucceeds());
}
ASSERT_TRUE(CheckFileHasLine(grandchild_events_path, EVENTS_POPULATED));
{
fbl::unique_fd child_kill_fd(open(grandchild_kill_path.c_str(), O_WRONLY));
ASSERT_TRUE(child_kill_fd.is_valid());
EXPECT_THAT(write(child_kill_fd.get(), "1", 1), SyscallSucceeds());
}
EXPECT_TRUE(fork_helper.WaitForChildren());
ASSERT_TRUE(CheckFileHasLine(grandchild_events_path, EVENTS_NOT_POPULATED));
}
TEST_F(CgroupTest, ProcfsCgroup) {
std::string root_procs_path = root_path() + "/" + PROCS_FILE;
std::string child_path_from_root = "/child";
std::string child_path = root_path() + child_path_from_root;
std::string child_procs_path = child_path + "/" + PROCS_FILE;
std::string grandchild_path_from_root = child_path_from_root + "/grandchild";
std::string grandchild_path = root_path() + grandchild_path_from_root;
std::string grandchild_procs_path = grandchild_path + "/" + PROCS_FILE;
std::string procfs_cgroup_path = "/proc/self/cgroup";
std::string pid_string = std::to_string(getpid());
ASSERT_TRUE(CheckFileHasLine(procfs_cgroup_path, PROC_CGROUP_PREFIX + std::string("/")));
CreateCgroup(child_path);
CreateCgroup(grandchild_path);
{
fbl::unique_fd child_procs_fd(open(child_procs_path.c_str(), O_WRONLY));
ASSERT_TRUE(child_procs_fd.is_valid());
EXPECT_THAT(write(child_procs_fd.get(), pid_string.c_str(), pid_string.length()),
SyscallSucceeds());
}
ASSERT_TRUE(CheckFileHasLine(procfs_cgroup_path, PROC_CGROUP_PREFIX + child_path_from_root));
{
fbl::unique_fd grandchild_procs_fd(open(grandchild_procs_path.c_str(), O_WRONLY));
ASSERT_TRUE(grandchild_procs_fd.is_valid());
EXPECT_THAT(write(grandchild_procs_fd.get(), pid_string.c_str(), pid_string.length()),
SyscallSucceeds());
}
ASSERT_TRUE(CheckFileHasLine(procfs_cgroup_path, PROC_CGROUP_PREFIX + grandchild_path_from_root));
{
fbl::unique_fd procs_fd(open(root_procs_path.c_str(), O_WRONLY));
ASSERT_TRUE(procs_fd.is_valid());
EXPECT_THAT(write(procs_fd.get(), pid_string.c_str(), pid_string.length()), SyscallSucceeds());
}
}
// `CgroupTest` mounts a cgroup2 during `SetUp()`. This test case mounts cgroup2 filesystem again at
// another mountpoint, and expects that operations are reflected in both mounts.
TEST_F(CgroupTest, MountCgroup2Twice) {
const std::string mountpoint = MountCgroup2();
CheckInterfaceFilesExist(mountpoint, true);
// Create /child in the first mount and observe from second mount.
const std::string child = "child";
const std::string child_path = root_path() + "/" + child;
const std::string child_path_mirrored = mountpoint + "/" + child;
CreateCgroup(child_path);
CheckDirectoryIncludes(mountpoint, {{.name = child, .type = DT_DIR}});
CheckInterfaceFilesExist(child_path_mirrored, false);
// Create /child/grandchild in the second mount and observe from first mount.
const std::string grandchild = "grandchild";
const std::string grandchild_path = child_path + "/" + grandchild;
const std::string grandchild_path_mirrored = child_path_mirrored + "/" + grandchild;
CreateCgroup(grandchild_path_mirrored);
CheckDirectoryIncludes(child_path, {{.name = grandchild, .type = DT_DIR}});
CheckInterfaceFilesExist(grandchild_path, false);
}
TEST_F(CgroupTest, ForkedProcessInheritsCgroup) {
// Create child cgroup and put the current pid into it. Fork a new process which should be
// automatically added the cgroup
std::string child_str = "/child";
std::string child_path = root_path() + child_str;
std::string child_procs_path = child_path + "/" + PROCS_FILE;
std::string procfs_cgroup_path = "/proc/self/cgroup";
std::string procfs_cgroup_str = PROC_CGROUP_PREFIX + child_str;
std::string pid_string = std::to_string(getpid());
CreateCgroup(child_path);
// Move current process to the child cgroup.
{
fbl::unique_fd child_procs_fd(open(child_procs_path.c_str(), O_WRONLY));
ASSERT_TRUE(child_procs_fd.is_valid());
EXPECT_THAT(write(child_procs_fd.get(), pid_string.c_str(), pid_string.length()),
SyscallSucceeds());
}
ASSERT_TRUE(CheckFileHasLine(procfs_cgroup_path, procfs_cgroup_str));
test_helper::ForkHelper fork_helper;
fork_helper.OnlyWaitForForkedChildren();
fork_helper.RunInForkedProcess([procfs_cgroup_path, procfs_cgroup_str]() {
// Child process should be in same cgroup as parent.
EXPECT_TRUE(CheckFileHasLine(procfs_cgroup_path, procfs_cgroup_str));
});
EXPECT_TRUE(fork_helper.WaitForChildren());
{
// Move current process back to the root cgroup.
fbl::unique_fd procs_fd(open((root_path() + "/" + PROCS_FILE).c_str(), O_WRONLY));
ASSERT_TRUE(procs_fd.is_valid());
EXPECT_THAT(write(procs_fd.get(), pid_string.c_str(), pid_string.length()), SyscallSucceeds());
}
}