251 lines
8.7 KiB
C++
251 lines
8.7 KiB
C++
#include "cgroup_v2.hpp"
|
|
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include <cerrno>
|
|
#include <csignal>
|
|
#include <cstddef>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <string>
|
|
|
|
#ifndef GLOVE_CGROUP_PROBE_AGENT_BIN
|
|
# error "GLOVE_CGROUP_PROBE_AGENT_BIN must point at the cgroup probe agent"
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
#define REQUIRE(cond) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
std::fprintf(stderr, "REQUIRE failed: %s @ %s:%d\n", #cond, __FILE__, __LINE__); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
auto stopped_probe(const char* mode, const char* argument = nullptr) -> ::pid_t {
|
|
const ::pid_t child = ::fork();
|
|
if (child == 0) {
|
|
::raise(SIGSTOP);
|
|
if (argument == nullptr) {
|
|
::execl(GLOVE_CGROUP_PROBE_AGENT_BIN, GLOVE_CGROUP_PROBE_AGENT_BIN, mode, nullptr);
|
|
} else {
|
|
::execl(
|
|
GLOVE_CGROUP_PROBE_AGENT_BIN, GLOVE_CGROUP_PROBE_AGENT_BIN, mode, argument, nullptr
|
|
);
|
|
}
|
|
std::_Exit(50);
|
|
}
|
|
return child;
|
|
}
|
|
|
|
auto wait_stopped(::pid_t child) -> bool {
|
|
int status = 0;
|
|
while (::waitpid(child, &status, WUNTRACED) < 0) {
|
|
if (errno != EINTR) {
|
|
return false;
|
|
}
|
|
}
|
|
return WIFSTOPPED(status);
|
|
}
|
|
|
|
auto read_trimmed(const std::filesystem::path& path) -> std::string {
|
|
std::ifstream input{path};
|
|
std::string value;
|
|
std::getline(input, value);
|
|
return value;
|
|
}
|
|
|
|
auto run_pid_limit_test(glove::container::linux_detail::cgroup_v2_root& root) -> int {
|
|
glove::container::resource_limits limits{
|
|
.cpu_time_ms = 10'000,
|
|
.memory_bytes = 256U * 1024U * 1024U,
|
|
.pids = 2,
|
|
.wall_time_ms = 10'000,
|
|
.disk_bytes = 16U * 1024U * 1024U,
|
|
.terminal_output_bytes = 1024U * 1024U,
|
|
};
|
|
auto session = root.create_session("pid-limit", limits);
|
|
REQUIRE(session.has_value());
|
|
REQUIRE(read_trimmed(session->path() / "memory.max") == std::to_string(limits.memory_bytes));
|
|
REQUIRE(read_trimmed(session->path() / "memory.swap.max") == "0");
|
|
REQUIRE(read_trimmed(session->path() / "pids.max") == "2");
|
|
|
|
const ::pid_t child = stopped_probe("pids");
|
|
REQUIRE(child > 0);
|
|
REQUIRE(wait_stopped(child));
|
|
REQUIRE(session->attach(child).has_value());
|
|
REQUIRE(::kill(child, SIGCONT) == 0);
|
|
int status = 0;
|
|
REQUIRE(::waitpid(child, &status, 0) == child);
|
|
REQUIRE(WIFEXITED(status));
|
|
REQUIRE(WEXITSTATUS(status) == 0);
|
|
auto observation = session->observe();
|
|
REQUIRE(observation.has_value());
|
|
REQUIRE(observation->peak_pids == 2);
|
|
REQUIRE(observation->pid_limit_hit);
|
|
REQUIRE(session->cleanup().has_value());
|
|
return 0;
|
|
}
|
|
|
|
auto run_memory_limit_test(glove::container::linux_detail::cgroup_v2_root& root) -> int {
|
|
constexpr std::size_t memory_limit = 96U * 1024U * 1024U;
|
|
constexpr const char* allocation_size = "402653184";
|
|
glove::container::resource_limits limits{
|
|
.cpu_time_ms = 10'000,
|
|
.memory_bytes = memory_limit,
|
|
.pids = 2,
|
|
.wall_time_ms = 10'000,
|
|
.disk_bytes = 16U * 1024U * 1024U,
|
|
.terminal_output_bytes = 1024U * 1024U,
|
|
};
|
|
auto oom_session = root.create_session("memory-oom", limits);
|
|
REQUIRE(oom_session.has_value());
|
|
const ::pid_t allocator = stopped_probe("memory", allocation_size);
|
|
REQUIRE(allocator > 0);
|
|
REQUIRE(wait_stopped(allocator));
|
|
REQUIRE(oom_session->attach(allocator).has_value());
|
|
REQUIRE(::kill(allocator, SIGCONT) == 0);
|
|
int status = 0;
|
|
REQUIRE(::waitpid(allocator, &status, 0) == allocator);
|
|
REQUIRE(WIFSIGNALED(status));
|
|
REQUIRE(WTERMSIG(status) == SIGKILL);
|
|
auto observation = oom_session->observe();
|
|
REQUIRE(observation.has_value());
|
|
REQUIRE(observation->peak_memory_bytes > 0);
|
|
REQUIRE(observation->memory_limit_hit);
|
|
REQUIRE(oom_session->cleanup().has_value());
|
|
return 0;
|
|
}
|
|
|
|
auto run_cpu_limit_test(glove::container::linux_detail::cgroup_v2_root& root) -> int {
|
|
glove::container::resource_limits limits{
|
|
.cpu_time_ms = 25,
|
|
.memory_bytes = 256U * 1024U * 1024U,
|
|
.pids = 1,
|
|
.wall_time_ms = 10'000,
|
|
.disk_bytes = 16U * 1024U * 1024U,
|
|
.terminal_output_bytes = 1024U * 1024U,
|
|
};
|
|
auto session = root.create_session("cpu-limit", limits);
|
|
REQUIRE(session.has_value());
|
|
const ::pid_t child = stopped_probe("cpu");
|
|
REQUIRE(child > 0);
|
|
REQUIRE(wait_stopped(child));
|
|
REQUIRE(session->attach(child).has_value());
|
|
REQUIRE(::kill(child, SIGCONT) == 0);
|
|
bool triggered = false;
|
|
for (unsigned int attempt = 0; attempt < 2'000; ++attempt) {
|
|
auto event = session->triggered_limit(limits);
|
|
REQUIRE(event.has_value());
|
|
if (*event == glove::container::linux_detail::cgroup_limit_event::cpu_time) {
|
|
triggered = true;
|
|
break;
|
|
}
|
|
::usleep(1'000);
|
|
}
|
|
REQUIRE(triggered);
|
|
REQUIRE(session->kill_all().has_value());
|
|
int status = 0;
|
|
REQUIRE(::waitpid(child, &status, 0) == child);
|
|
REQUIRE(WIFSIGNALED(status));
|
|
REQUIRE(WTERMSIG(status) == SIGKILL);
|
|
auto observation = session->observe();
|
|
REQUIRE(observation.has_value());
|
|
REQUIRE(observation->cpu_time_ms >= limits.cpu_time_ms);
|
|
REQUIRE(session->cleanup().has_value());
|
|
return 0;
|
|
}
|
|
|
|
auto run_crash_adoption_test(glove::container::linux_detail::cgroup_v2_root& root) -> int {
|
|
int identity_pipe[2]{};
|
|
REQUIRE(::pipe(identity_pipe) == 0);
|
|
const ::pid_t owner = ::fork();
|
|
REQUIRE(owner >= 0);
|
|
if (owner == 0) {
|
|
::close(identity_pipe[0]);
|
|
const glove::container::resource_limits limits{
|
|
.cpu_time_ms = 10'000,
|
|
.memory_bytes = 64U * 1024U * 1024U,
|
|
.pids = 2,
|
|
.wall_time_ms = 10'000,
|
|
.disk_bytes = 16U * 1024U * 1024U,
|
|
.terminal_output_bytes = 1024U * 1024U,
|
|
};
|
|
auto session = root.create_session("crash-adoption", limits);
|
|
|
|
struct stat status{};
|
|
|
|
if (!session || ::fstat(session->directory_fd(), &status) != 0 || status.st_dev == 0 ||
|
|
status.st_ino == 0) {
|
|
std::_Exit(40);
|
|
}
|
|
const std::uint64_t identity[] = {
|
|
static_cast<std::uint64_t>(status.st_dev),
|
|
static_cast<std::uint64_t>(status.st_ino),
|
|
};
|
|
if (::write(identity_pipe[1], identity, sizeof(identity)) != sizeof(identity)) {
|
|
std::_Exit(41);
|
|
}
|
|
std::_Exit(0);
|
|
}
|
|
::close(identity_pipe[1]);
|
|
std::uint64_t identity[2]{};
|
|
REQUIRE(::read(identity_pipe[0], identity, sizeof(identity)) == sizeof(identity));
|
|
::close(identity_pipe[0]);
|
|
int owner_status = 0;
|
|
REQUIRE(::waitpid(owner, &owner_status, 0) == owner);
|
|
REQUIRE(WIFEXITED(owner_status));
|
|
REQUIRE(WEXITSTATUS(owner_status) == 0);
|
|
|
|
REQUIRE(!root.adopt_session("crash-adoption", identity[0], identity[1] + 1U).has_value());
|
|
auto adopted = root.adopt_session("crash-adoption", identity[0], identity[1]);
|
|
REQUIRE(adopted.has_value());
|
|
|
|
struct stat adopted_status{};
|
|
|
|
REQUIRE(::fstat(adopted->directory_fd(), &adopted_status) == 0);
|
|
REQUIRE(static_cast<std::uint64_t>(adopted_status.st_dev) == identity[0]);
|
|
REQUIRE(static_cast<std::uint64_t>(adopted_status.st_ino) == identity[1]);
|
|
REQUIRE(adopted->cleanup().has_value());
|
|
REQUIRE(!root.adopt_session("crash-adoption", identity[0], identity[1]).has_value());
|
|
REQUIRE(
|
|
root.cleanup_session_if_matches("crash-adoption", identity[0], identity[1]).has_value()
|
|
);
|
|
return 0;
|
|
}
|
|
|
|
auto run() -> int {
|
|
auto root = glove::container::linux_detail::cgroup_v2_root::prepare_for_current_process();
|
|
if (!root) {
|
|
std::fprintf(
|
|
stderr,
|
|
"cgroup v2 delegation unavailable in this test topology: %s\n",
|
|
root.error().c_str()
|
|
);
|
|
return 77;
|
|
}
|
|
glove::container::resource_limits limits{
|
|
.cpu_time_ms = 1,
|
|
.memory_bytes = 1,
|
|
.pids = 1,
|
|
.wall_time_ms = 1,
|
|
.disk_bytes = 1,
|
|
.terminal_output_bytes = 1,
|
|
};
|
|
REQUIRE(!root->create_session("../escape", limits).has_value());
|
|
REQUIRE(run_pid_limit_test(*root) == 0);
|
|
REQUIRE(run_memory_limit_test(*root) == 0);
|
|
REQUIRE(run_cpu_limit_test(*root) == 0);
|
|
REQUIRE(run_crash_adoption_test(*root) == 0);
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
auto main() -> int {
|
|
return run();
|
|
}
|