glove/tests/container/linux_resource_lifecycle_test.cpp
trvon a8137db76b
feat(control): neutral guest channels, quarantine recovery, capability sealing, local-service proxy
Introduce the framed guest_channel_transport with shared deadlines and
std::jthread ownership, metadata-only retired-schema quarantine with
fail-closed integrity, stateless frozen-catalog validators with
capability pinned to zero, and the generic local-service proxy with
descriptor-pinned endpoints, alias-only guest sockets, audit-gated
delivery, and a Linux managed runtime capability derived only from a
registry-bound operational factory. Sage vocabulary is confined to
src/adapters/sage.
2026-08-30 20:28:41 -06:00

435 lines
16 KiB
C++

#include "glove/container/profile.hpp"
#include "glove/supervisor/linux_session_filesystem.hpp"
#include "cgroup_v2.hpp"
#include "linux_resource_lifecycle.hpp"
#include <fcntl.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <algorithm>
#include <array>
#include <cerrno>
#include <chrono>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <expected>
#include <filesystem>
#include <optional>
#include <string>
#include <thread>
#include <vector>
#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)
using glove::container::resource_limits;
using glove::container::resource_termination_cause;
using glove::container::linux_detail::cgroup_v2_root;
using glove::container::linux_detail::linux_resource_lifecycle;
using glove::supervisor::linux_detail::linux_session_filesystem;
class temporary_tree {
public:
temporary_tree() {
std::string pattern = "/tmp/glove-resource-lifecycle-test-XXXXXX";
char* created = ::mkdtemp(pattern.data());
if (created != nullptr) {
root_ = created;
}
}
temporary_tree(const temporary_tree&) = delete;
auto operator=(const temporary_tree&) -> temporary_tree& = delete;
~temporary_tree() {
std::error_code error;
std::filesystem::remove_all(root_, error);
}
[[nodiscard]] auto materialization_root() const -> std::filesystem::path {
return root_ / "materializations";
}
[[nodiscard]] auto prepare() const -> bool {
const auto root = materialization_root();
return !root_.empty() && std::filesystem::create_directory(root) &&
::chmod(root.c_str(), 0700) == 0;
}
private:
std::filesystem::path root_;
};
auto epoch_ms() -> std::uint64_t {
const auto now = std::chrono::system_clock::now().time_since_epoch();
return static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(now).count()
);
}
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 wait_bounded(::pid_t child) -> std::expected<int, std::string> {
for (unsigned int attempt = 0; attempt < 5'000; ++attempt) {
int status = 0;
const ::pid_t result = ::waitpid(child, &status, WNOHANG);
if (result == child) {
return status;
}
if (result < 0 && errno != EINTR) {
return std::unexpected(std::string{"waitpid failed"});
}
std::this_thread::sleep_for(std::chrono::milliseconds{1});
}
static_cast<void>(::kill(child, SIGKILL));
int status = 0;
while (::waitpid(child, &status, 0) < 0 && errno == EINTR) {}
return std::unexpected(std::string{"child did not terminate within test bound"});
}
auto fill_until_quota(int directory_fd, std::uint64_t quota_bytes) -> bool {
const int fd = ::openat(
directory_fd, "pressure", O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0600
);
if (fd < 0) {
return errno == ENOSPC || errno == EDQUOT;
}
std::array<char, 4096> block{};
bool denied = false;
for (std::uint64_t attempt = 0; attempt < quota_bytes / block.size() + 32; ++attempt) {
if (::write(fd, block.data(), block.size()) < 0) {
denied = errno == ENOSPC || errno == EDQUOT;
break;
}
}
::close(fd);
return denied;
}
auto limits_with(std::uint64_t wall_time_ms, std::uint64_t output_bytes) -> resource_limits {
return {
.cpu_time_ms = 10'000,
.memory_bytes = 256U * 1024U * 1024U,
.pids = 8,
.wall_time_ms = wall_time_ms,
.disk_bytes = 16U * 1024U * 1024U,
.terminal_output_bytes = output_bytes,
};
}
auto make_lifecycle(
cgroup_v2_root& root,
const std::filesystem::path& materialization_root,
std::string_view session_id,
const resource_limits& limits,
std::uint64_t started_at_ms
) -> std::expected<std::unique_ptr<linux_resource_lifecycle>, std::string> {
auto cgroup = root.create_session(session_id, limits);
if (!cgroup) {
return std::unexpected(cgroup.error());
}
auto filesystem = linux_session_filesystem::create(
materialization_root.string(), session_id, limits.disk_bytes, {}
);
if (!filesystem) {
return std::unexpected(filesystem.error());
}
return linux_resource_lifecycle::create(
std::move(*cgroup), std::move(*filesystem), limits, started_at_ms
);
}
auto natural_exit_terminal_test(
cgroup_v2_root& root, const std::filesystem::path& materialization_root
) -> int {
const auto limits = limits_with(10'000, 1024);
const auto started = epoch_ms();
auto child = stopped_probe("exit", "7");
REQUIRE(child > 0);
REQUIRE(wait_stopped(child));
auto lifecycle = make_lifecycle(root, materialization_root, "lifecycle-exit", limits, started);
REQUIRE(lifecycle.has_value());
temporary_tree lease_tree;
REQUIRE(lease_tree.prepare());
const auto lease_path = lease_tree.materialization_root() / "terminal-secret-lease";
const int lease_lock =
::open(lease_path.c_str(), O_RDWR | O_CREAT | O_CLOEXEC | O_NOFOLLOW, 0600);
REQUIRE(lease_lock >= 0);
REQUIRE(::flock(lease_lock, LOCK_EX | LOCK_NB) == 0);
const int mount_descriptor = ::dup(lease_lock);
REQUIRE(mount_descriptor >= 0);
REQUIRE((*lifecycle)
->install_secret_mounts(
{{
.descriptor_fd = mount_descriptor,
.target_path = "/home/agent/.codex/auth.json",
.alias = "secret:codex-auth",
.quota_partition = {},
.quota_bytes = 0,
.source_identity = std::nullopt,
.source_content_digest = std::nullopt,
.projection_id = std::nullopt,
.projection_destination_alias = std::nullopt,
.runtime_adapter_id = std::nullopt,
.runtime_context_digest = std::nullopt,
.runtime_adoption_manifest_digest = std::nullopt,
.runtime_adoption_snapshot_digest = std::nullopt,
.secret_handle = "codex-auth",
.secret_runtime_id = "codex",
.service_proxy_manifest_digest = std::nullopt,
.writable = true,
.directory = false,
}},
{lease_lock}
)
.has_value());
const int contender = ::open(lease_path.c_str(), O_RDWR | O_CLOEXEC | O_NOFOLLOW);
REQUIRE(contender >= 0);
REQUIRE(::flock(contender, LOCK_EX | LOCK_NB) != 0);
REQUIRE(errno == EWOULDBLOCK || errno == EAGAIN);
REQUIRE((*lifecycle)->attach(child).has_value());
REQUIRE(::kill(child, SIGCONT) == 0);
auto status = wait_bounded(child);
REQUIRE(status.has_value());
auto terminal = (*lifecycle)->finish(*status, epoch_ms());
REQUIRE(terminal.has_value());
REQUIRE(terminal->termination_cause == resource_termination_cause::exited);
REQUIRE(terminal->exit_code == 7);
REQUIRE(terminal->observed.disk_bytes <= limits.disk_bytes);
REQUIRE(::flock(contender, LOCK_EX | LOCK_NB) == 0);
REQUIRE(::close(contender) == 0);
auto repeated = (*lifecycle)->finish(*status, epoch_ms());
REQUIRE(repeated == terminal);
return 0;
}
auto expired_before_attach_test(
cgroup_v2_root& root, const std::filesystem::path& materialization_root
) -> int {
const auto limits = limits_with(1, 1024);
auto child = stopped_probe("sleep");
REQUIRE(child > 0);
REQUIRE(wait_stopped(child));
auto lifecycle =
make_lifecycle(root, materialization_root, "lifecycle-expired", limits, epoch_ms());
REQUIRE(lifecycle.has_value());
std::this_thread::sleep_for(std::chrono::milliseconds{25});
auto attached = (*lifecycle)->attach(child);
static_cast<void>(::kill(child, SIGKILL));
auto status = wait_bounded(child);
REQUIRE(status.has_value());
REQUIRE(!attached.has_value());
return 0;
}
auto wall_limit_terminal_test(
cgroup_v2_root& root, const std::filesystem::path& materialization_root
) -> int {
const auto limits = limits_with(25, 1024);
auto child = stopped_probe("sleep");
REQUIRE(child > 0);
REQUIRE(wait_stopped(child));
auto lifecycle =
make_lifecycle(root, materialization_root, "lifecycle-wall", limits, epoch_ms());
REQUIRE(lifecycle.has_value());
REQUIRE((*lifecycle)->attach(child).has_value());
REQUIRE(::kill(child, SIGCONT) == 0);
auto status = wait_bounded(child);
REQUIRE(status.has_value());
REQUIRE(WIFSIGNALED(*status));
auto terminal = (*lifecycle)->finish(*status, epoch_ms());
REQUIRE(terminal.has_value());
REQUIRE(terminal->termination_cause == resource_termination_cause::wall_time_limit);
REQUIRE(!terminal->exit_code.has_value());
REQUIRE(terminal->observed.wall_time_ms >= limits.wall_time_ms);
return 0;
}
auto output_limit_terminal_test(
cgroup_v2_root& root, const std::filesystem::path& materialization_root
) -> int {
const auto limits = limits_with(10'000, 10);
auto child = stopped_probe("sleep");
REQUIRE(child > 0);
REQUIRE(wait_stopped(child));
auto lifecycle =
make_lifecycle(root, materialization_root, "lifecycle-output", limits, epoch_ms());
REQUIRE(lifecycle.has_value());
REQUIRE((*lifecycle)->attach(child).has_value());
REQUIRE(::kill(child, SIGCONT) == 0);
REQUIRE(!(*lifecycle)->monitor()->account_terminal_output(11));
auto status = wait_bounded(child);
REQUIRE(status.has_value());
auto terminal = (*lifecycle)->finish(*status, epoch_ms());
REQUIRE(terminal.has_value());
REQUIRE(terminal->termination_cause == resource_termination_cause::terminal_output_limit);
REQUIRE(terminal->observed.terminal_output_bytes == 11);
return 0;
}
auto disk_limit_terminal_test(
cgroup_v2_root& root, const std::filesystem::path& materialization_root
) -> int {
auto limits = limits_with(10'000, 1024);
const long page_size = ::sysconf(_SC_PAGESIZE);
REQUIRE(page_size > 0);
limits.disk_bytes = static_cast<std::uint64_t>(page_size) * 16U;
auto child = stopped_probe("sleep");
REQUIRE(child > 0);
REQUIRE(wait_stopped(child));
auto lifecycle =
make_lifecycle(root, materialization_root, "lifecycle-disk", limits, epoch_ms());
REQUIRE(lifecycle.has_value());
const auto mounts = (*lifecycle)->mounts();
const auto tmp = std::ranges::find(
mounts, std::string{"/tmp"}, &glove::supervisor::linux_detail::session_mount::target_path
);
REQUIRE(tmp != mounts.end());
REQUIRE((*lifecycle)->attach(child).has_value());
REQUIRE(::kill(child, SIGCONT) == 0);
REQUIRE(fill_until_quota(tmp->descriptor_fd, limits.disk_bytes));
auto status = wait_bounded(child);
REQUIRE(status.has_value());
REQUIRE(WIFSIGNALED(*status));
auto terminal = (*lifecycle)->finish(*status, epoch_ms());
REQUIRE(terminal.has_value());
REQUIRE(terminal->termination_cause == resource_termination_cause::disk_limit);
REQUIRE(terminal->observed.disk_bytes > 0);
REQUIRE(terminal->observed.disk_bytes <= limits.disk_bytes);
return 0;
}
auto cpu_limit_terminal_test(
cgroup_v2_root& root, const std::filesystem::path& materialization_root
) -> int {
auto limits = limits_with(10'000, 1024);
limits.cpu_time_ms = 25;
auto child = stopped_probe("cpu");
REQUIRE(child > 0);
REQUIRE(wait_stopped(child));
auto lifecycle =
make_lifecycle(root, materialization_root, "lifecycle-cpu", limits, epoch_ms());
REQUIRE(lifecycle.has_value());
REQUIRE((*lifecycle)->attach(child).has_value());
REQUIRE(::kill(child, SIGCONT) == 0);
auto status = wait_bounded(child);
REQUIRE(status.has_value());
auto terminal = (*lifecycle)->finish(*status, epoch_ms());
REQUIRE(terminal.has_value());
REQUIRE(terminal->termination_cause == resource_termination_cause::cpu_time_limit);
REQUIRE(terminal->observed.cpu_time_ms >= limits.cpu_time_ms);
return 0;
}
auto memory_limit_terminal_test(
cgroup_v2_root& root, const std::filesystem::path& materialization_root
) -> int {
auto limits = limits_with(10'000, 1024);
limits.memory_bytes = 96U * 1024U * 1024U;
auto child = stopped_probe("memory", "402653184");
REQUIRE(child > 0);
REQUIRE(wait_stopped(child));
auto lifecycle =
make_lifecycle(root, materialization_root, "lifecycle-memory", limits, epoch_ms());
REQUIRE(lifecycle.has_value());
REQUIRE((*lifecycle)->attach(child).has_value());
REQUIRE(::kill(child, SIGCONT) == 0);
auto status = wait_bounded(child);
REQUIRE(status.has_value());
auto terminal = (*lifecycle)->finish(*status, epoch_ms());
REQUIRE(terminal.has_value());
REQUIRE(terminal->termination_cause == resource_termination_cause::memory_limit);
REQUIRE(terminal->observed.peak_memory_bytes > 0);
return 0;
}
auto pid_limit_terminal_test(
cgroup_v2_root& root, const std::filesystem::path& materialization_root
) -> int {
auto limits = limits_with(10'000, 1024);
limits.pids = 2;
auto child = stopped_probe("pids");
REQUIRE(child > 0);
REQUIRE(wait_stopped(child));
auto lifecycle =
make_lifecycle(root, materialization_root, "lifecycle-pids", limits, epoch_ms());
REQUIRE(lifecycle.has_value());
REQUIRE((*lifecycle)->attach(child).has_value());
REQUIRE(::kill(child, SIGCONT) == 0);
auto status = wait_bounded(child);
REQUIRE(status.has_value());
auto terminal = (*lifecycle)->finish(*status, epoch_ms());
REQUIRE(terminal.has_value());
REQUIRE(terminal->termination_cause == resource_termination_cause::pid_limit);
REQUIRE(terminal->observed.peak_pids == limits.pids);
return 0;
}
auto run() -> int {
temporary_tree tree;
REQUIRE(tree.prepare());
const auto materialization_root = tree.materialization_root();
auto root = cgroup_v2_root::prepare_for_current_process();
if (!root) {
std::fprintf(
stderr, "cgroup v2 lifecycle topology unavailable: %s\n", root.error().c_str()
);
return 77;
}
REQUIRE(expired_before_attach_test(*root, materialization_root) == 0);
REQUIRE(natural_exit_terminal_test(*root, materialization_root) == 0);
REQUIRE(wall_limit_terminal_test(*root, materialization_root) == 0);
REQUIRE(output_limit_terminal_test(*root, materialization_root) == 0);
REQUIRE(disk_limit_terminal_test(*root, materialization_root) == 0);
REQUIRE(cpu_limit_terminal_test(*root, materialization_root) == 0);
REQUIRE(memory_limit_terminal_test(*root, materialization_root) == 0);
REQUIRE(pid_limit_terminal_test(*root, materialization_root) == 0);
REQUIRE(std::filesystem::is_empty(materialization_root));
return 0;
}
} // namespace
auto main() -> int {
return run();
}