// Copyright 2021 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// All tests in this file rely on being about to mount and unmount cgroupfs,
// which isn't expected to work, or be safe on a general linux system.

#include <limits.h>
#include <linux/magic.h>
#include <poll.h>
#include <sys/inotify.h>
#include <sys/mount.h>
#include <sys/statfs.h>
#include <unistd.h>

#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <list>
#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
#include "absl/strings/ascii.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_split.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/mutex.h"
#include "absl/synchronization/notification.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "test/util/cgroup_util.h"
#include "test/util/cleanup.h"
#include "test/util/file_descriptor.h"
#include "test/util/fs_util.h"
#include "test/util/linux_capability_util.h"
#include "test/util/logging.h"
#include "test/util/mount_util.h"
#include "test/util/posix_error.h"
#include "test/util/save_util.h"
#include "test/util/temp_path.h"
#include "test/util/test_util.h"
#include "test/util/thread_util.h"

namespace gvisor {
namespace testing {
namespace {

using ::testing::_;
using ::testing::Contains;
using ::testing::Each;
using ::testing::Eq;
using ::testing::Ge;
using ::testing::Gt;
using ::testing::HasSubstr;
using ::testing::Key;
using ::testing::Not;

std::vector<std::string> known_controllers = {
    "cpu", "cpuset", "cpuacct", "devices", "job", "memory", "pids",
};

bool CgroupsAvailable() {
  return IsRunningOnGvisor() &&
         TEST_CHECK_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN));
}

// NoopThreads spawns a set of threads that do nothing until they're asked to
// exit. Useful for testing functionality that requires a process with multiple
// threads.
class NoopThreads {
 public:
  NoopThreads(int count) {
    auto noop = [this]() { exit_.WaitForNotification(); };

    for (int i = 0; i < count; ++i) {
      threads_.emplace_back(noop);
    }
  }

  ~NoopThreads() { Join(); }

  void Join() {
    if (joined_) {
      return;
    }

    joined_ = true;
    exit_.Notify();
    for (auto& thread : threads_) {
      thread.Join();
    }
  }

 private:
  std::list<ScopedThread> threads_;
  absl::Notification exit_;
  bool joined_ = false;
};

TEST(Cgroup, MountsForAllControllers) {
  SKIP_IF(!CgroupsAvailable());

  for (const auto& ctl : known_controllers) {
    Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/" + ctl);
    EXPECT_NO_ERRNO(c.ContainsCallingProcess());
  }
}

// All supported controllers are mounted by default.
TEST(Cgroup, AllControllersImplicit) {
  SKIP_IF(!CgroupsAvailable());

  absl::flat_hash_map<std::string, CgroupsEntry> cgroups_entries =
      ASSERT_NO_ERRNO_AND_VALUE(ProcCgroupsEntries());
  for (const auto& ctl : known_controllers) {
    EXPECT_TRUE(cgroups_entries.contains(ctl))
        << absl::StreamFormat("ctl=%s", ctl);
  }
  EXPECT_EQ(cgroups_entries.size(), known_controllers.size());
}

TEST(Cgroup, ProcsAndTasks) {
  SKIP_IF(!CgroupsAvailable());

  for (const auto& ctl : known_controllers) {
    Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/" + ctl);
    absl::flat_hash_set<pid_t> pids = ASSERT_NO_ERRNO_AND_VALUE(c.Procs());
    absl::flat_hash_set<pid_t> tids = ASSERT_NO_ERRNO_AND_VALUE(c.Tasks());

    EXPECT_GE(tids.size(), pids.size()) << "Found more processes than threads";

    // Pids should be a strict subset of tids.
    for (auto it = pids.begin(); it != pids.end(); ++it) {
      EXPECT_TRUE(tids.contains(*it))
          << absl::StreamFormat("Have pid %d, but no such tid", *it);
    }
  }
}

TEST(Cgroup, Statfs) {
  SKIP_IF(!CgroupsAvailable());

  for (const auto& ctl : known_controllers) {
    Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/" + ctl);
    struct statfs st;
    EXPECT_THAT(statfs(c.Relpath("cgroup.procs").c_str(), &st),
                SyscallSucceeds());
    EXPECT_EQ(st.f_type, CGROUP_SUPER_MAGIC);

    EXPECT_THAT(statfs(c.Relpath(".").c_str(), &st), SyscallSucceeds());
    EXPECT_EQ(st.f_type, CGROUP_SUPER_MAGIC);
  }
}

TEST(Cgroup, StatfsCgroupDir) {
  SKIP_IF(!CgroupsAvailable());

  struct statfs st;
  EXPECT_THAT(statfs("/sys/fs/cgroup", &st), SyscallSucceeds());
  EXPECT_EQ(st.f_type, TMPFS_MAGIC);
}

TEST(Cgroup, CgroupsCannotMountTwice) {
  SKIP_IF(!CgroupsAvailable());

  Mounter m(ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()));
  // Cgroups are already mounted.
  EXPECT_THAT(m.MountCgroupfs(""), PosixErrorIs(EBUSY, _))
      << "Cgroups are already mounted";
}

TEST(Cgroup, OnlyContainsControllerSpecificFiles) {
  SKIP_IF(!CgroupsAvailable());
  Cgroup mem = Cgroup::RootCgroup("/sys/fs/cgroup/memory");
  EXPECT_THAT(Exists(mem.Relpath("memory.usage_in_bytes")),
              IsPosixErrorOkAndHolds(true));
  // CPU files shouldn't exist in memory cgroups.
  EXPECT_THAT(Exists(mem.Relpath("cpu.cfs_period_us")),
              IsPosixErrorOkAndHolds(false));
  EXPECT_THAT(Exists(mem.Relpath("cpu.cfs_quota_us")),
              IsPosixErrorOkAndHolds(false));
  EXPECT_THAT(Exists(mem.Relpath("cpu.shares")), IsPosixErrorOkAndHolds(false));

  Cgroup cpu = Cgroup::RootCgroup("/sys/fs/cgroup/cpu");
  EXPECT_THAT(Exists(cpu.Relpath("cpu.cfs_period_us")),
              IsPosixErrorOkAndHolds(true));
  EXPECT_THAT(Exists(cpu.Relpath("cpu.cfs_quota_us")),
              IsPosixErrorOkAndHolds(true));
  EXPECT_THAT(Exists(cpu.Relpath("cpu.shares")), IsPosixErrorOkAndHolds(true));
  // Memory files shouldn't exist in cpu cgroups.
  EXPECT_THAT(Exists(cpu.Relpath("memory.usage_in_bytes")),
              IsPosixErrorOkAndHolds(false));
}

TEST(Cgroup, InvalidController) {
  SKIP_IF(!CgroupsAvailable());

  TempPath mountpoint = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
  std::string mopts = "this-controller-is-invalid";
  EXPECT_THAT(
      mount("none", mountpoint.path().c_str(), "cgroup", 0, mopts.c_str()),
      SyscallFailsWithErrno(EINVAL));
}

TEST(Cgroup, MoptAllMustBeExclusive) {
  SKIP_IF(!CgroupsAvailable());

  TempPath mountpoint = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
  std::string mopts = "all,cpu";
  EXPECT_THAT(
      mount("none", mountpoint.path().c_str(), "cgroup", 0, mopts.c_str()),
      SyscallFailsWithErrno(EINVAL));
}

TEST(Cgroup, UnmountRepeated) {
  SKIP_IF(!CgroupsAvailable());

  const DisableSave ds;  // Too many syscalls.

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/memory");

  // First unmount should succeed.
  EXPECT_THAT(umount(c.Path().c_str()), SyscallSucceeds());
  EXPECT_THAT(umount(c.Path().c_str()), SyscallFailsWithErrno(EINVAL));
}

TEST(Cgroup, Create) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/memory");
  ASSERT_NO_ERRNO(c.CreateChild("child1"));
  EXPECT_TRUE(ASSERT_NO_ERRNO_AND_VALUE(Exists(c.Path())));
}

TEST(Cgroup, SubcontainerInitiallyEmpty) {
  SKIP_IF(!CgroupsAvailable());
  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/memory");
  Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("child1"));
  auto procs = ASSERT_NO_ERRNO_AND_VALUE(child.Procs());
  EXPECT_TRUE(procs.empty());
}

TEST(Cgroup, SubcontainersHaveIndependentState) {
  SKIP_IF(!CgroupsAvailable());
  // Use the job cgroup as a simple cgroup with state we can modify.
  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/job");

  // Initially job.id should be the default value of 0.
  EXPECT_THAT(c.ReadIntegerControlFile("job.id"), IsPosixErrorOkAndHolds(0));

  // Set id so it is no longer the default.
  ASSERT_NO_ERRNO(c.WriteIntegerControlFile("job.id", 1234));

  // Create a child. The child should inherit the value from the parent, and not
  // the default value of 0.
  Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("child1"));
  EXPECT_THAT(child.ReadIntegerControlFile("job.id"),
              IsPosixErrorOkAndHolds(1234));

  // Setting the parent doesn't change the child.
  ASSERT_NO_ERRNO(c.WriteIntegerControlFile("job.id", 5678));
  EXPECT_THAT(child.ReadIntegerControlFile("job.id"),
              IsPosixErrorOkAndHolds(1234));

  // Likewise, setting the child doesn't change the parent.
  ASSERT_NO_ERRNO(child.WriteIntegerControlFile("job.id", 9012));
  EXPECT_THAT(c.ReadIntegerControlFile("job.id"), IsPosixErrorOkAndHolds(5678));
}

TEST(Cgroup, MigrateToSubcontainer) {
  SKIP_IF(!CgroupsAvailable());
  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/cpuacct");
  Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("child1"));

  // Initially, test process should be in the root cgroup c.
  EXPECT_NO_ERRNO(c.ContainsCallingProcess());

  pid_t pid = getpid();

  EXPECT_NO_ERRNO(child.Enter(pid));

  // After migration, child should contain the test process, and the c should
  // not.
  EXPECT_NO_ERRNO(child.ContainsCallingProcess());
  auto procs = ASSERT_NO_ERRNO_AND_VALUE(c.Procs());
  EXPECT_FALSE(procs.contains(pid));
}

TEST(Cgroup, MigrateToSubcontainerThread) {
  SKIP_IF(!CgroupsAvailable());
  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/cpuacct");
  Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("child1"));

  // Ensure there are some threads for this process.
  NoopThreads threads(1);

  // Initially, test process should be in the root cgroup c.
  EXPECT_NO_ERRNO(c.ContainsCallingThread());

  const pid_t tid = syscall(SYS_gettid);

  EXPECT_NO_ERRNO(child.EnterThread(tid));

  // After migration, child should contain the test process, and the c should
  // not.
  EXPECT_NO_ERRNO(child.ContainsCallingThread());
  auto tasks = ASSERT_NO_ERRNO_AND_VALUE(c.Tasks());
  EXPECT_FALSE(tasks.contains(tid));
}

TEST(Cgroup, MigrateInvalidPID) {
  SKIP_IF(!CgroupsAvailable());
  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/cpuacct");

  EXPECT_THAT(c.WriteControlFile("cgroup.procs", "-1"), PosixErrorIs(EINVAL));
  EXPECT_THAT(c.WriteControlFile("cgroup.procs", "not-a-number"),
              PosixErrorIs(EINVAL));

  EXPECT_THAT(c.WriteControlFile("tasks", "-1"), PosixErrorIs(EINVAL));
  EXPECT_THAT(c.WriteControlFile("tasks", "not-a-number"),
              PosixErrorIs(EINVAL));
}

// Regression test for b/222278194.
TEST(Cgroup, DuplicateUnlinkOnDirFD) {
  SKIP_IF(!CgroupsAvailable());
  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/cpuset");
  Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("child"));

  // Orphan child directory by opening FD to it then deleting it.
  const FileDescriptor dirfd =
      ASSERT_NO_ERRNO_AND_VALUE(Open(child.Path(), 0, 0));
  ASSERT_NO_ERRNO(child.Delete());

  // Replace orphan with new directory of same name, so path resolution
  // succeeds.
  Cgroup child_new = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("child"));

  // Attempt to delete orphaned child again through dirfd.
  EXPECT_THAT(UnlinkAt(dirfd, ".", AT_REMOVEDIR), PosixErrorIs(EINVAL));
}

TEST(Cgroup, MkdirWithPermissions) {
  SKIP_IF(!CgroupsAvailable());
  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/cpuset");

  std::string child1_path = JoinPath(c.Path(), "child1");
  std::string child2_path = JoinPath(c.Path(), "child2");

  ASSERT_NO_ERRNO(Mkdir(child1_path, 0444));
  const struct stat s1 = ASSERT_NO_ERRNO_AND_VALUE(Stat(child1_path));
  EXPECT_THAT(s1.st_mode, PermissionIs(0444));
  EXPECT_TRUE(S_ISDIR(s1.st_mode));

  ASSERT_NO_ERRNO(Mkdir(child2_path, 0));
  const struct stat s2 = ASSERT_NO_ERRNO_AND_VALUE(Stat(child2_path));
  EXPECT_THAT(s2.st_mode, PermissionIs(0000));
  EXPECT_TRUE(S_ISDIR(s2.st_mode));
}

TEST(Cgroup, CantRenameControlFile) {
  SKIP_IF(!CgroupsAvailable());
  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/cpu");

  const std::string control_file_path = c.Relpath("cgroup.procs");
  EXPECT_THAT(
      rename(c.Relpath("cgroup.procs").c_str(), c.Relpath("foo").c_str()),
      SyscallFailsWithErrno(ENOTDIR));
}

TEST(Cgroup, CrossDirRenameNotAllowed) {
  SKIP_IF(!CgroupsAvailable());
  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/cpu");

  Cgroup dir1 = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("dir1"));
  Cgroup dir2 = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("dir2"));

  Cgroup target = ASSERT_NO_ERRNO_AND_VALUE(dir1.CreateChild("target"));
  // Move to sibling directory.
  EXPECT_THAT(rename(target.Path().c_str(), dir2.Relpath("target").c_str()),
              SyscallFailsWithErrno(EIO));
  // Move to parent directory.
  EXPECT_THAT(rename(target.Path().c_str(), c.Relpath("target").c_str()),
              SyscallFailsWithErrno(EIO));

  // Original directory unaffected.
  EXPECT_THAT(Exists(target.Path()), IsPosixErrorOkAndHolds(true));
}

TEST(Cgroup, RenameNameCollision) {
  SKIP_IF(!CgroupsAvailable());
  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/cpu");

  Cgroup dir1 = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("dir1"));
  Cgroup dir2 = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("dir2"));

  // Collision with dir.
  EXPECT_THAT(rename(dir1.Path().c_str(), dir2.Path().c_str()),
              SyscallFailsWithErrno(EEXIST));
  // Collision with control file.
  EXPECT_THAT(rename(dir1.Path().c_str(), c.Relpath("cgroup.procs").c_str()),
              SyscallFailsWithErrno(EEXIST));
}

TEST(Cgroup, Rename) {
  SKIP_IF(!CgroupsAvailable());
  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/cpu");
  Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("child"));
  Cgroup target = ASSERT_NO_ERRNO_AND_VALUE(child.CreateChild("oldname"));
  ASSERT_THAT(rename(target.Path().c_str(), child.Relpath("newname").c_str()),
              SyscallSucceeds());
  EXPECT_THAT(Exists(child.Relpath("newname")), IsPosixErrorOkAndHolds(true));
  EXPECT_THAT(Exists(child.Relpath("oldname")), IsPosixErrorOkAndHolds(false));
}

TEST(Cgroup, PIDZeroMovesSelf) {
  SKIP_IF(!CgroupsAvailable());
  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/memory");
  Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("child"));

  // Source contains this process.
  EXPECT_NO_ERRNO(c.ContainsCallingProcess());

  // Move to child by writing PID 0.
  ASSERT_NO_ERRNO(child.WriteIntegerControlFile("cgroup.procs", 0));

  // Destination now contains this process, and source does not.
  EXPECT_NO_ERRNO(child.ContainsCallingProcess());
  auto procs = ASSERT_NO_ERRNO_AND_VALUE(c.Procs());
  EXPECT_FALSE(procs.contains(getpid()));
}

TEST(Cgroup, TIDZeroMovesSelf) {
  SKIP_IF(!CgroupsAvailable());
  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/memory");
  Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("child"));

  // Source contains this thread.
  EXPECT_NO_ERRNO(c.ContainsCallingThread());

  // Move to child by writing TID 0.
  ASSERT_NO_ERRNO(child.WriteIntegerControlFile("tasks", 0));

  // Destination now contains this thread, and source does not.
  EXPECT_NO_ERRNO(child.ContainsCallingThread());
  auto tasks = ASSERT_NO_ERRNO_AND_VALUE(c.Tasks());
  EXPECT_FALSE(tasks.contains(syscall(SYS_gettid)));
}

TEST(Cgroup, NamedHierarchies) {
  SKIP_IF(!CgroupsAvailable());

  Mounter m(ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()));
  Cgroup c1 = ASSERT_NO_ERRNO_AND_VALUE(m.MountCgroupfs("none,name=h1"));
  Cgroup c2 = ASSERT_NO_ERRNO_AND_VALUE(m.MountCgroupfs("none,name=h2"));

  // Check that /proc/<pid>/cgroup contains an entry for this task.
  absl::flat_hash_map<std::string, PIDCgroupEntry> entries =
      ASSERT_NO_ERRNO_AND_VALUE(ProcPIDCgroupEntries(getpid()));
  EXPECT_TRUE(entries.contains("name=h1"));
  EXPECT_TRUE(entries.contains("name=h2"));
  EXPECT_NO_ERRNO(c1.ContainsCallingProcess());
  EXPECT_NO_ERRNO(c2.ContainsCallingProcess());
}

TEST(Cgroup, NoneExclusiveWithAnyController) {
  SKIP_IF(!CgroupsAvailable());

  Mounter m(ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()));
  EXPECT_THAT(m.MountCgroupfs("none,cpu"), PosixErrorIs(EINVAL, _));
}

TEST(Cgroup, EmptyHierarchyMustHaveName) {
  SKIP_IF(!CgroupsAvailable());

  Mounter m(ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()));
  // This will fail since it is an empty hierarchy with no name.
  EXPECT_THAT(m.MountCgroupfs("none"), PosixErrorIs(EINVAL, _));
}

TEST(Cgroup, NameMatchButControllersDont) {
  SKIP_IF(!CgroupsAvailable());

  Mounter m(ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()));
  Cgroup c1 = ASSERT_NO_ERRNO_AND_VALUE(m.MountCgroupfs("none,name=h1"));
  EXPECT_THAT(m.MountCgroupfs("name=h2,memory"), PosixErrorIs(EBUSY, _));
  EXPECT_THAT(m.MountCgroupfs("name=h1,memory"), PosixErrorIs(EBUSY, _));
  EXPECT_THAT(m.MountCgroupfs("name=h2,cpu"), PosixErrorIs(EBUSY, _));
}

TEST(MemoryCgroup, MemoryUsageInBytes) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/memory");
  const uint64_t usage = ASSERT_NO_ERRNO_AND_VALUE(
      c.ReadIntegerControlFile("memory.usage_in_bytes"));
  EXPECT_GE(usage, 0);
}

TEST(CPUCgroup, ControlFilesHaveDefaultValues) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/cpu");
  EXPECT_THAT(c.ReadIntegerControlFile("cpu.cfs_quota_us"),
              IsPosixErrorOkAndHolds(-1));
  EXPECT_THAT(c.ReadIntegerControlFile("cpu.cfs_period_us"),
              IsPosixErrorOkAndHolds(100000));
  EXPECT_THAT(c.ReadIntegerControlFile("cpu.shares"),
              IsPosixErrorOkAndHolds(1024));
}

TEST(CPUAcctCgroup, CPUAcctUsage) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/cpuacct");
  const int64_t usage =
      ASSERT_NO_ERRNO_AND_VALUE(c.ReadIntegerControlFile("cpuacct.usage"));
  const int64_t usage_user =
      ASSERT_NO_ERRNO_AND_VALUE(c.ReadIntegerControlFile("cpuacct.usage_user"));
  const int64_t usage_sys =
      ASSERT_NO_ERRNO_AND_VALUE(c.ReadIntegerControlFile("cpuacct.usage_sys"));

  EXPECT_GE(usage, 0);
  EXPECT_GE(usage_user, 0);
  EXPECT_GE(usage_sys, 0);

  EXPECT_GE(usage_user + usage_sys, usage);
}

TEST(CPUAcctCgroup, CPUAcctStat) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/cpuacct");
  std::string stat =
      ASSERT_NO_ERRNO_AND_VALUE(c.ReadControlFile("cpuacct.stat"));

  // We're expecting the contents of "cpuacct.stat" to look similar to this:
  //
  // user 377986
  // system 220662

  std::vector<absl::string_view> lines =
      absl::StrSplit(stat, '\n', absl::SkipEmpty());
  ASSERT_EQ(lines.size(), 2);

  std::vector<absl::string_view> user_tokens =
      StrSplit(lines[0], absl::ByChar(' '));
  EXPECT_EQ(user_tokens[0], "user");
  EXPECT_THAT(Atoi<int64_t>(user_tokens[1]), IsPosixErrorOkAndHolds(Ge(0)));

  std::vector<absl::string_view> sys_tokens =
      StrSplit(lines[1], absl::ByChar(' '));
  EXPECT_EQ(sys_tokens[0], "system");
  EXPECT_THAT(Atoi<int64_t>(sys_tokens[1]), IsPosixErrorOkAndHolds(Ge(0)));
}

TEST(CPUAcctCgroup, HierarchicalAccounting) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup root = Cgroup::RootCgroup("/sys/fs/cgroup/cpuacct");
  Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(root.CreateChild("child1"));

  // The test starts in the root cgroup, so its CPU usage should be accounted
  // there. Since the granularity of cpuacct.usage is unspecified and the test
  // may not have run for very long yet, wait for it to be accounted.
  ASSERT_NO_ERRNO(
      root.PollControlFileForChange("cpuacct.usage", absl::Seconds(5)));
  EXPECT_THAT(root.ReadIntegerControlFile("cpuacct.usage"),
              IsPosixErrorOkAndHolds(Gt(0)));

  // Child should have zero usage since it is initially empty.
  EXPECT_THAT(child.ReadIntegerControlFile("cpuacct.usage"),
              IsPosixErrorOkAndHolds(Eq(0)));

  // Move test into child and confirm child starts incurring usage.
  const int64_t before_move =
      ASSERT_NO_ERRNO_AND_VALUE(root.ReadIntegerControlFile("cpuacct.usage"));
  ASSERT_NO_ERRNO(child.Enter(getpid()));
  ASSERT_NO_ERRNO(
      child.PollControlFileForChange("cpuacct.usage", absl::Seconds(5)));

  EXPECT_THAT(child.ReadIntegerControlFile("cpuacct.usage"),
              IsPosixErrorOkAndHolds(Gt(0)));

  // Root shouldn't lose usage due to the migration.
  const int64_t after_move =
      ASSERT_NO_ERRNO_AND_VALUE(root.ReadIntegerControlFile("cpuacct.usage"));
  EXPECT_GE(after_move, before_move);

  // Root should continue to gain usage after the move since child is a
  // subcgroup.
  ASSERT_NO_ERRNO(
      child.PollControlFileForChange("cpuacct.usage", absl::Seconds(5)));
  EXPECT_THAT(root.ReadIntegerControlFile("cpuacct.usage"),
              IsPosixErrorOkAndHolds(Ge(after_move)));
}

TEST(CPUAcctCgroup, IndirectCharge) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup root = Cgroup::RootCgroup("/sys/fs/cgroup/cpuacct");
  Cgroup child1 = ASSERT_NO_ERRNO_AND_VALUE(root.CreateChild("child1"));
  Cgroup child2 = ASSERT_NO_ERRNO_AND_VALUE(root.CreateChild("child2"));
  Cgroup child2a = ASSERT_NO_ERRNO_AND_VALUE(child2.CreateChild("child2a"));

  ASSERT_NO_ERRNO(child1.Enter(getpid()));
  ASSERT_NO_ERRNO(
      child1.PollControlFileForChange("cpuacct.usage", absl::Seconds(5)));

  // Only root and child1 should have usage.
  for (auto const& cg : {root, child1}) {
    EXPECT_THAT(cg.ReadIntegerControlFile("cpuacct.usage"),
                IsPosixErrorOkAndHolds(Gt(0)));
  }
  for (auto const& cg : {child2, child2a}) {
    EXPECT_THAT(cg.ReadIntegerControlFile("cpuacct.usage"),
                IsPosixErrorOkAndHolds(Eq(0)));
  }

  ASSERT_NO_ERRNO(child2a.Enter(getpid()));
  ASSERT_NO_ERRNO(
      child2a.PollControlFileForChange("cpuacct.usage", absl::Seconds(5)));

  const int64_t snapshot_root =
      ASSERT_NO_ERRNO_AND_VALUE(root.ReadIntegerControlFile("cpuacct.usage"));
  const int64_t snapshot_child1 =
      ASSERT_NO_ERRNO_AND_VALUE(child1.ReadIntegerControlFile("cpuacct.usage"));
  const int64_t snapshot_child2 =
      ASSERT_NO_ERRNO_AND_VALUE(child2.ReadIntegerControlFile("cpuacct.usage"));
  const int64_t snapshot_child2a = ASSERT_NO_ERRNO_AND_VALUE(
      child2a.ReadIntegerControlFile("cpuacct.usage"));

  ASSERT_NO_ERRNO(
      child2a.PollControlFileForChange("cpuacct.usage", absl::Seconds(5)));

  // Root, child2 and child2a should've accumulated new usage. Child1 should
  // not.
  const int64_t now_root =
      ASSERT_NO_ERRNO_AND_VALUE(root.ReadIntegerControlFile("cpuacct.usage"));
  const int64_t now_child1 =
      ASSERT_NO_ERRNO_AND_VALUE(child1.ReadIntegerControlFile("cpuacct.usage"));
  const int64_t now_child2 =
      ASSERT_NO_ERRNO_AND_VALUE(child2.ReadIntegerControlFile("cpuacct.usage"));
  const int64_t now_child2a = ASSERT_NO_ERRNO_AND_VALUE(
      child2a.ReadIntegerControlFile("cpuacct.usage"));

  EXPECT_GT(now_root, snapshot_root);
  EXPECT_GT(now_child2, snapshot_child2);
  EXPECT_GT(now_child2a, snapshot_child2a);
  EXPECT_EQ(now_child1, snapshot_child1);
}

TEST(CPUAcctCgroup, NoDoubleAccounting) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup root = Cgroup::RootCgroup("/sys/fs/cgroup/cpuacct");
  Cgroup parent = ASSERT_NO_ERRNO_AND_VALUE(root.CreateChild("parent"));
  Cgroup a = ASSERT_NO_ERRNO_AND_VALUE(parent.CreateChild("a"));
  Cgroup b = ASSERT_NO_ERRNO_AND_VALUE(parent.CreateChild("b"));

  ASSERT_NO_ERRNO(a.Enter(getpid()));
  ASSERT_NO_ERRNO(
      a.PollControlFileForChange("cpuacct.usage", absl::Seconds(5)));

  ASSERT_NO_ERRNO(b.Enter(getpid()));
  ASSERT_NO_ERRNO(
      b.PollControlFileForChange("cpuacct.usage", absl::Seconds(5)));

  ASSERT_NO_ERRNO(root.Enter(getpid()));
  ASSERT_NO_ERRNO(
      root.PollControlFileForChange("cpuacct.usage", absl::Seconds(5)));

  // The usage for parent, a & b should now be frozen, since they no longer have
  // any tasks. Root will continue to accumulate usage.
  const int64_t usage_root =
      ASSERT_NO_ERRNO_AND_VALUE(root.ReadIntegerControlFile("cpuacct.usage"));
  const int64_t usage_parent =
      ASSERT_NO_ERRNO_AND_VALUE(parent.ReadIntegerControlFile("cpuacct.usage"));
  const int64_t usage_a =
      ASSERT_NO_ERRNO_AND_VALUE(a.ReadIntegerControlFile("cpuacct.usage"));
  const int64_t usage_b =
      ASSERT_NO_ERRNO_AND_VALUE(b.ReadIntegerControlFile("cpuacct.usage"));

  EXPECT_GT(usage_root, 0);
  EXPECT_GT(usage_parent, 0);
  EXPECT_GT(usage_a, 0);
  EXPECT_GT(usage_b, 0);
  EXPECT_EQ(usage_parent, usage_a + usage_b);
  EXPECT_GE(usage_parent, usage_a);
  EXPECT_GE(usage_parent, usage_b);
  EXPECT_GE(usage_root, usage_parent);
}

// WriteAndVerifyControlValue attempts to write val to a cgroup file at path,
// and verify the value by reading it afterwards.
PosixError WriteAndVerifyControlValue(const Cgroup& c, absl::string_view path,
                                      int64_t val) {
  RETURN_IF_ERRNO(c.WriteIntegerControlFile(path, val));
  ASSIGN_OR_RETURN_ERRNO(int64_t newval, c.ReadIntegerControlFile(path));
  if (newval != val) {
    return PosixError(
        EINVAL,
        absl::StrFormat(
            "Unexpected value for control file '%s': expected %d, got %d", path,
            val, newval));
  }
  return NoError();
}

PosixErrorOr<std::vector<bool>> ParseBitmap(std::string s) {
  std::vector<bool> bitmap;
  bitmap.reserve(64);
  for (const absl::string_view& t : absl::StrSplit(s, ',')) {
    std::vector<std::string> parts = absl::StrSplit(t, absl::MaxSplits('-', 2));
    if (parts.size() == 2) {
      ASSIGN_OR_RETURN_ERRNO(uint64_t start, Atoi<uint64_t>(parts[0]));
      ASSIGN_OR_RETURN_ERRNO(uint64_t end, Atoi<uint64_t>(parts[1]));
      // Note: start and end are indices into bitmap.
      if (end >= bitmap.size()) {
        bitmap.resize(end + 1, false);
      }
      for (uint64_t i = start; i <= end; ++i) {
        bitmap[i] = true;
      }
    } else {  // parts.size() == 1, 0 not possible.
      ASSIGN_OR_RETURN_ERRNO(uint64_t i, Atoi<uint64_t>(parts[0]));
      if (i >= bitmap.size()) {
        bitmap.resize(i + 1, false);
      }
      bitmap[i] = true;
    }
  }
  return bitmap;
}

TEST(JobCgroup, ReadWriteRead) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/job");

  EXPECT_THAT(c.ReadIntegerControlFile("job.id"), IsPosixErrorOkAndHolds(0));
  EXPECT_NO_ERRNO(WriteAndVerifyControlValue(c, "job.id", 1234));
  EXPECT_NO_ERRNO(WriteAndVerifyControlValue(c, "job.id", -1));
  EXPECT_NO_ERRNO(WriteAndVerifyControlValue(c, "job.id", LLONG_MIN));
  EXPECT_NO_ERRNO(WriteAndVerifyControlValue(c, "job.id", LLONG_MAX));
}

TEST(CpusetCgroup, Defaults) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/cpuset");
  std::string cpus =
      ASSERT_NO_ERRNO_AND_VALUE(c.ReadControlFile("cpuset.cpus"));
  std::vector<bool> cpus_bitmap = ASSERT_NO_ERRNO_AND_VALUE(ParseBitmap(cpus));
  EXPECT_GT(cpus_bitmap.size(), 0);
  EXPECT_THAT(cpus_bitmap, Each(Eq(true)));

  std::string mems =
      ASSERT_NO_ERRNO_AND_VALUE(c.ReadControlFile("cpuset.mems"));
  std::vector<bool> mems_bitmap = ASSERT_NO_ERRNO_AND_VALUE(ParseBitmap(mems));
  EXPECT_GT(mems_bitmap.size(), 0);
  EXPECT_THAT(mems_bitmap, Each(Eq(true)));
}

TEST(CpusetCgroup, SetMask) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/cpuset");
  std::string cpus =
      ASSERT_NO_ERRNO_AND_VALUE(c.ReadControlFile("cpuset.cpus"));
  std::vector<bool> cpus_bitmap = ASSERT_NO_ERRNO_AND_VALUE(ParseBitmap(cpus));

  SKIP_IF(cpus_bitmap.size() <= 1);  // "Not enough CPUs"

  int max_cpu = cpus_bitmap.size() - 1;
  ASSERT_NO_ERRNO(
      c.WriteControlFile("cpuset.cpus", absl::StrCat("1-", max_cpu)));
  cpus_bitmap[0] = false;
  cpus = ASSERT_NO_ERRNO_AND_VALUE(c.ReadControlFile("cpuset.cpus"));
  std::vector<bool> cpus_bitmap_after =
      ASSERT_NO_ERRNO_AND_VALUE(ParseBitmap(cpus));
  EXPECT_EQ(cpus_bitmap_after, cpus_bitmap);
}

TEST(CpusetCgroup, SetEmptyMask) {
  SKIP_IF(!CgroupsAvailable());
  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/cpuset");
  ASSERT_NO_ERRNO(c.WriteControlFile("cpuset.cpus", ""));
  std::string cpus = std::string(absl::StripAsciiWhitespace(
      ASSERT_NO_ERRNO_AND_VALUE(c.ReadControlFile("cpuset.cpus"))));
  EXPECT_EQ(cpus, "");
  ASSERT_NO_ERRNO(c.WriteControlFile("cpuset.mems", ""));
  std::string mems = std::string(absl::StripAsciiWhitespace(
      ASSERT_NO_ERRNO_AND_VALUE(c.ReadControlFile("cpuset.mems"))));
  EXPECT_EQ(mems, "");
}

TEST(ProcCgroups, Empty) {
  SKIP_IF(!CgroupsAvailable());

  absl::flat_hash_map<std::string, CgroupsEntry> entries =
      ASSERT_NO_ERRNO_AND_VALUE(ProcCgroupsEntries());
  // Cgroups are mounted, we should have entries.
  EXPECT_FALSE(entries.empty());
}

TEST(ProcCgroups, ProcCgroupsEntries) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup mem = Cgroup::RootCgroup("/sys/fs/cgroup/memory");
  absl::flat_hash_map<std::string, CgroupsEntry> entries =
      ASSERT_NO_ERRNO_AND_VALUE(ProcCgroupsEntries());
  EXPECT_EQ(entries.size(), 7);
  ASSERT_TRUE(entries.contains("memory"));
  CgroupsEntry mem_e = entries["memory"];
  EXPECT_EQ(mem_e.subsys_name, "memory");
  EXPECT_GE(mem_e.hierarchy, 1);
  // Expect a single root cgroup.
  EXPECT_EQ(mem_e.num_cgroups, 2);
  // Cgroups are currently always enabled when mounted.
  EXPECT_TRUE(mem_e.enabled);

  // Add a second cgroup, and check for new entry.

  Cgroup cpu = Cgroup::RootCgroup("/sys/fs/cgroup/cpu");
  entries = ASSERT_NO_ERRNO_AND_VALUE(ProcCgroupsEntries());
  EXPECT_EQ(entries.size(), 7);
  EXPECT_TRUE(entries.contains("memory"));  // Still have memory entry.
  ASSERT_TRUE(entries.contains("cpu"));
  CgroupsEntry cpu_e = entries["cpu"];
  EXPECT_EQ(cpu_e.subsys_name, "cpu");
  EXPECT_GE(cpu_e.hierarchy, 1);
  EXPECT_EQ(cpu_e.num_cgroups, 2);
  EXPECT_TRUE(cpu_e.enabled);

  // Separate hierarchies, since controllers were mounted separately.
  EXPECT_NE(mem_e.hierarchy, cpu_e.hierarchy);
}

TEST(ProcPIDCgroup, Entries) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/memory");
  absl::flat_hash_map<std::string, PIDCgroupEntry> entries =
      ASSERT_NO_ERRNO_AND_VALUE(ProcPIDCgroupEntries(getpid()));
  // All controllers are mounted.
  EXPECT_EQ(entries.size(), 7);
  PIDCgroupEntry mem_e = entries["memory"];
  EXPECT_GE(mem_e.hierarchy, 1);
  EXPECT_EQ(mem_e.controllers, "memory");
  // The path is /<container-id>.
  EXPECT_NE(mem_e.path, "/");

  Cgroup c1 = Cgroup::RootCgroup("/sys/fs/cgroup/cpu");
  entries = ASSERT_NO_ERRNO_AND_VALUE(ProcPIDCgroupEntries(getpid()));
  // All controllers are mounted.
  EXPECT_EQ(entries.size(), 7);
  EXPECT_TRUE(entries.contains("memory"));  // Still have memory entry.
  PIDCgroupEntry cpu_e = entries["cpu"];
  EXPECT_GE(cpu_e.hierarchy, 1);
  EXPECT_EQ(cpu_e.controllers, "cpu");
  // The path is /<container-id>.
  EXPECT_NE(cpu_e.path, "/");

  // Separate hierarchies, since controllers were mounted separately.
  EXPECT_NE(mem_e.hierarchy, cpu_e.hierarchy);
}

TEST(ProcCgroup, PIDCgroupMatchesCgroups) {
  SKIP_IF(!CgroupsAvailable());

  Mounter m(ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()));
  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/memory");
  Cgroup c1 = Cgroup::RootCgroup("/sys/fs/cgroup/cpu");

  absl::flat_hash_map<std::string, CgroupsEntry> cgroups_entries =
      ASSERT_NO_ERRNO_AND_VALUE(ProcCgroupsEntries());
  absl::flat_hash_map<std::string, PIDCgroupEntry> pid_entries =
      ASSERT_NO_ERRNO_AND_VALUE(ProcPIDCgroupEntries(getpid()));

  CgroupsEntry cgroup_mem = cgroups_entries["memory"];
  PIDCgroupEntry pid_mem = pid_entries["memory"];

  EXPECT_EQ(cgroup_mem.hierarchy, pid_mem.hierarchy);

  CgroupsEntry cgroup_cpu = cgroups_entries["cpu"];
  PIDCgroupEntry pid_cpu = pid_entries["cpu"];

  EXPECT_EQ(cgroup_cpu.hierarchy, pid_cpu.hierarchy);
  EXPECT_NE(cgroup_mem.hierarchy, cgroup_cpu.hierarchy);
  EXPECT_NE(pid_mem.hierarchy, pid_cpu.hierarchy);
}

TEST(PIDsCgroup, ControlFilesExist) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/pids");

  const std::string root_limit =
      ASSERT_NO_ERRNO_AND_VALUE(c.ReadControlFile("pids.max"));
  EXPECT_EQ(root_limit, "max\n");

  // There should be at least one PID in use in the root controller, since the
  // test process is running in the root controller.
  const int64_t current =
      ASSERT_NO_ERRNO_AND_VALUE(c.ReadIntegerControlFile("pids.current"));
  EXPECT_GE(current, 1);

  Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("child"));

  // The limit file should exist for any child cgroups, and should be unlimited
  // by default.
  const std::string child_limit =
      ASSERT_NO_ERRNO_AND_VALUE(child.ReadControlFile("pids.max"));
  EXPECT_EQ(child_limit, "max\n");

  // The child cgroup should have no tasks, and thus no pids usage.
  const int64_t current_child =
      ASSERT_NO_ERRNO_AND_VALUE(child.ReadIntegerControlFile("pids.current"));
  EXPECT_EQ(current_child, 0);
}

TEST(PIDsCgroup, ChargeMigration) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/pids");
  const int64_t root_start =
      ASSERT_NO_ERRNO_AND_VALUE(c.ReadIntegerControlFile("pids.current"));
  // Root should have at least one task.
  ASSERT_GE(root_start, 1);

  Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("child"));

  // Child initially has no charge.
  EXPECT_THAT(child.ReadIntegerControlFile("pids.current"),
              IsPosixErrorOkAndHolds(0));

  // Move the test process. The root cgroup should lose charges equal to the
  // number of tasks moved to the child.
  ASSERT_NO_ERRNO(child.Enter(getpid()));

  const int64_t child_after =
      ASSERT_NO_ERRNO_AND_VALUE(child.ReadIntegerControlFile("pids.current"));
  EXPECT_GE(child_after, 1);

  const int64_t root_after =
      ASSERT_NO_ERRNO_AND_VALUE(c.ReadIntegerControlFile("pids.current"));
  EXPECT_EQ(root_start - root_after, child_after);
}

TEST(PIDsCgroup, MigrationCanExceedLimit) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/pids");
  Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("child"));

  // Set child limit to 0, and try move tasks into it. This should be allowed,
  // as the limit isn't enforced on migration.
  ASSERT_NO_ERRNO(child.WriteIntegerControlFile("pids.max", 0));
  ASSERT_NO_ERRNO(child.Enter(getpid()));
  EXPECT_THAT(child.ReadIntegerControlFile("pids.current"),
              IsPosixErrorOkAndHolds(Gt(0)));
}

TEST(PIDsCgroup, SetInvalidLimit) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/pids");
  Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("child"));

  // Set a valid limit, so we can verify it doesn't change after invalid writes.
  ASSERT_NO_ERRNO(child.WriteIntegerControlFile("pids.max", 1234));

  EXPECT_THAT(child.WriteControlFile("pids.max", "m a x"),
              PosixErrorIs(EINVAL, _));
  EXPECT_THAT(child.WriteControlFile("pids.max", "some-invalid-string"),
              PosixErrorIs(EINVAL, _));
  EXPECT_THAT(child.WriteControlFile("pids.max", "-1"),
              PosixErrorIs(EINVAL, _));
  EXPECT_THAT(child.WriteControlFile("pids.max", "-3894732"),
              PosixErrorIs(EINVAL, _));
  // This value is much larger than the maximum allowed value of ~ 1<<22.
  EXPECT_THAT(child.WriteIntegerControlFile("pids.max", LLONG_MAX - 1),
              PosixErrorIs(EINVAL, _));

  // The initial valid limit should remain unchanged.
  EXPECT_THAT(child.ReadIntegerControlFile("pids.max"),
              IsPosixErrorOkAndHolds(1234));
}

TEST(PIDsCgroup, CanLowerLimitBelowCurrentCharge) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/pids");
  Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("child"));
  ASSERT_NO_ERRNO(child.Enter(getpid()));
  // Confirm current charge is non-zero.
  ASSERT_THAT(child.ReadIntegerControlFile("pids.current"),
              IsPosixErrorOkAndHolds(Gt(0)));
  // Try set limit to zero.
  EXPECT_NO_ERRNO(child.WriteIntegerControlFile("pids.max", 0));
}

TEST(DevicesCgroup, ControlFilesExist) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/devices");

  // The root group starts with allowing rwm to all.
  EXPECT_THAT(c.ReadControlFile("devices.allow"), IsPosixErrorOkAndHolds(""));
  EXPECT_THAT(c.ReadControlFile("devices.deny"), IsPosixErrorOkAndHolds(""));
  EXPECT_THAT(c.ReadControlFile("devices.list"),
              IsPosixErrorOkAndHolds("a *:* rwm"));
}

TEST(DevicesCgroup, DenyAll) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/devices");

  ASSERT_NO_ERRNO(c.WriteControlFile("devices.allow", "b *:* rw\n"));
  EXPECT_THAT(c.ReadControlFile("devices.list"),
              IsPosixErrorOkAndHolds("b *:* rw\n"));

  ASSERT_NO_ERRNO(c.WriteControlFile("devices.deny", "a"));
  EXPECT_THAT(c.ReadControlFile("devices.list"), IsPosixErrorOkAndHolds(""));
}

TEST(DevicesCgroup, AddDeviceRule) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/devices");

  ASSERT_THAT(c.ReadControlFile("devices.list"),
              IsPosixErrorOkAndHolds("a *:* rwm"));
  // Gives character devices with major device number 7 read and write
  // permission.
  ASSERT_NO_ERRNO(c.WriteControlFile("devices.allow", "c 7:* rw\n"));
  EXPECT_THAT(c.ReadControlFile("devices.list"),
              IsPosixErrorOkAndHolds("c 7:* rw\n"));

  // Diasllows all devices.
  ASSERT_NO_ERRNO(c.WriteControlFile("devices.deny", "a"));
  EXPECT_THAT(c.ReadControlFile("devices.list"), IsPosixErrorOkAndHolds(""));

  // Adds one more rule.
  ASSERT_NO_ERRNO(c.WriteControlFile("devices.allow", "b *:* rw\n"));
  EXPECT_THAT(c.ReadControlFile("devices.list"),
              IsPosixErrorOkAndHolds("b *:* rw\n"));
}

TEST(DevicesCgroup, RemoveDeviceRule) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/devices");
  // The root group starts with allowing rwm to all.
  ASSERT_THAT(c.ReadControlFile("devices.list"),
              IsPosixErrorOkAndHolds("a *:* rwm"));
  // Gives character devices with the major device number 7 read and write
  // permission.
  ASSERT_NO_ERRNO(c.WriteControlFile("devices.allow", "c 7:* rw"));
  EXPECT_THAT(c.ReadControlFile("devices.list"),
              IsPosixErrorOkAndHolds("c 7:* rw\n"));

  // Removes the write permission from the character devices with the major
  // device number 7.
  ASSERT_NO_ERRNO(c.WriteControlFile("devices.deny", "c 7:* w"));
  EXPECT_THAT(c.ReadControlFile("devices.list"),
              IsPosixErrorOkAndHolds("c 7:* r\n"));
}

TEST(DevicesCgroup, IgnorePartialMatchRule) {
  SKIP_IF(!CgroupsAvailable());

  Cgroup c = Cgroup::RootCgroup("/sys/fs/cgroup/devices");

  // Gives character devices with the major device number 7 read and write
  // permission.
  ASSERT_NO_ERRNO(c.WriteControlFile("devices.allow", "c 7:* rw"));
  EXPECT_THAT(c.ReadControlFile("devices.list"),
              IsPosixErrorOkAndHolds("c 7:* rw\n"));

  // Expect no change to the allow list since minor device matches partially a
  // existing rule for character devices 7:*.
  ASSERT_NO_ERRNO(c.WriteControlFile("devices.deny", "c 7:0 w"));
  EXPECT_THAT(c.ReadControlFile("devices.list"),
              IsPosixErrorOkAndHolds("c 7:* rw\n"));
}

TEST(Cgroup, ProcSelfCgroupNoV2LineIfUnmounted) {
  SKIP_IF(!CgroupsAvailable());

  std::string content;
  ASSERT_NO_ERRNO(GetContents("/proc/self/cgroup", &content));

  // The cgroup v2 entry "0::/..." should only appear if cgroup v2 has been
  // mounted at least once. Since cgroup v2 has not been mounted in this test
  // environment, the string "0::" should not appear in /proc/self/cgroup.
  EXPECT_FALSE(absl::StrContains(content, "0::"));
}

}  // namespace
}  // namespace testing
}  // namespace gvisor
