Skip to content

Commit 1fcff0d

Browse files
committed
Implement cgroup resource limit management and add tests for invocation cgroup
1 parent c88c91e commit 1fcff0d

7 files changed

Lines changed: 288 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,14 @@ if(UNIX)
132132
target_include_directories(cgroups_test PRIVATE src)
133133
add_test(NAME cgroups_test COMMAND cgroups_test)
134134
set_tests_properties(cgroups_test PROPERTIES LABELS "smoke;cgroups")
135+
add_executable(cgroups_limits_test tests/cgroups_limits_test.cpp src/sandbox/cgroups.cpp)
136+
target_include_directories(cgroups_limits_test PRIVATE src)
137+
add_test(NAME cgroups_limits_test COMMAND cgroups_limits_test)
138+
set_tests_properties(cgroups_limits_test PROPERTIES LABELS "smoke;cgroups;limits")
139+
add_executable(invocation_cgroup_test tests/invocation_cgroup_test.cpp src/sandbox/invocation_cgroup.cpp src/sandbox/cgroups.cpp)
140+
target_include_directories(invocation_cgroup_test PRIVATE src)
141+
add_test(NAME invocation_cgroup_test COMMAND invocation_cgroup_test)
142+
set_tests_properties(invocation_cgroup_test PROPERTIES LABELS "smoke;cgroups;invocation")
135143
endif()
136144

137145

src/sandbox/cgroups.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,39 @@ bool remove_transient_cgroup(const std::string& cgroup_path) {
6060
return true;
6161
}
6262

63+
bool set_cgroup_cpu_max(const std::string& cgroup_path, const std::string& cpu_max) {
64+
std::string file = cgroup_path + "/cpu.max";
65+
if (!write_file(file, cpu_max + "\n")) {
66+
std::cerr << "[cgroups] failed to write cpu.max" << std::endl;
67+
return false;
68+
}
69+
return true;
70+
}
71+
72+
bool set_cgroup_memory_max(const std::string& cgroup_path, const std::string& memory_max) {
73+
std::string file = cgroup_path + "/memory.max";
74+
if (!write_file(file, memory_max + "\n")) {
75+
std::cerr << "[cgroups] failed to write memory.max" << std::endl;
76+
return false;
77+
}
78+
return true;
79+
}
80+
81+
bool set_cgroup_pids_max(const std::string& cgroup_path, const std::string& pids_max) {
82+
std::string file = cgroup_path + "/pids.max";
83+
if (!write_file(file, pids_max + "\n")) {
84+
std::cerr << "[cgroups] failed to write pids.max" << std::endl;
85+
return false;
86+
}
87+
return true;
88+
}
89+
90+
std::string read_cgroup_file(const std::string& path) {
91+
std::ifstream ifs(path);
92+
if (!ifs.is_open()) return std::string();
93+
std::string content;
94+
std::getline(ifs, content);
95+
return content;
96+
}
97+
6398
} // namespace sandbox

src/sandbox/cgroups.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,16 @@ bool add_pid_to_cgroup(const std::string& cgroup_path, pid_t pid = 0);
1818
// Remove the transient cgroup; best effort.
1919
bool remove_transient_cgroup(const std::string& cgroup_path);
2020

21+
// Resource limits (cgroup v2 controller files)
22+
// Set CPU max (value format as in cgroup v2, e.g. "100000 100000" or "max")
23+
bool set_cgroup_cpu_max(const std::string& cgroup_path, const std::string& cpu_max);
24+
25+
// Set memory max in bytes (or "max" for no limit)
26+
bool set_cgroup_memory_max(const std::string& cgroup_path, const std::string& memory_max);
27+
28+
// Set pids max (integer or "max")
29+
bool set_cgroup_pids_max(const std::string& cgroup_path, const std::string& pids_max);
30+
31+
// Read back controller files for verification
32+
std::string read_cgroup_file(const std::string& path);
2133
} // namespace sandbox

src/sandbox/invocation_cgroup.cpp

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#include "invocation_cgroup.h"
2+
#include "cgroups.h"
3+
#include <unistd.h>
4+
#include <chrono>
5+
#include <sstream>
6+
#include <random>
7+
#include <iostream>
8+
9+
namespace sandbox {
10+
11+
static std::string make_unique_name() {
12+
pid_t pid = getpid();
13+
auto now = std::chrono::steady_clock::now().time_since_epoch().count();
14+
std::mt19937_64 rng(static_cast<unsigned long>(now ^ pid));
15+
uint64_t r = rng();
16+
std::ostringstream ss;
17+
ss << "native_node_inv_" << pid << "_" << now << "_" << (r & 0xffffffffULL);
18+
return ss.str();
19+
}
20+
21+
InvocationCgroup::InvocationCgroup(const CgroupLimits& limits) {
22+
if (!is_cgroup_v2_available()) {
23+
created_ = false;
24+
return;
25+
}
26+
27+
std::string name = make_unique_name();
28+
std::string p = create_transient_cgroup(name);
29+
if (p.empty()) {
30+
created_ = false;
31+
return;
32+
}
33+
// Apply limits if provided
34+
if (!limits.cpu_max.empty()) set_cgroup_cpu_max(p, limits.cpu_max);
35+
if (!limits.memory_max.empty()) set_cgroup_memory_max(p, limits.memory_max);
36+
if (!limits.pids_max.empty()) set_cgroup_pids_max(p, limits.pids_max);
37+
38+
path_ = p;
39+
created_ = true;
40+
}
41+
42+
InvocationCgroup::~InvocationCgroup() {
43+
if (created_) {
44+
// best-effort: try to remove
45+
remove_transient_cgroup(path_);
46+
}
47+
}
48+
49+
InvocationCgroup::InvocationCgroup(InvocationCgroup&& o) noexcept {
50+
path_ = std::move(o.path_);
51+
created_ = o.created_;
52+
o.created_ = false;
53+
}
54+
55+
InvocationCgroup& InvocationCgroup::operator=(InvocationCgroup&& o) noexcept {
56+
if (this != &o) {
57+
if (created_) remove_transient_cgroup(path_);
58+
path_ = std::move(o.path_);
59+
created_ = o.created_;
60+
o.created_ = false;
61+
}
62+
return *this;
63+
}
64+
65+
bool InvocationCgroup::valid() const { return created_ && !path_.empty(); }
66+
67+
const std::string& InvocationCgroup::path() const { return path_; }
68+
69+
bool InvocationCgroup::add_pid(pid_t pid) {
70+
if (!created_) return false;
71+
return add_pid_to_cgroup(path_, pid);
72+
}
73+
74+
} // namespace sandbox

src/sandbox/invocation_cgroup.h

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#pragma once
2+
3+
#include <string>
4+
#include <optional>
5+
6+
namespace sandbox {
7+
8+
struct CgroupLimits {
9+
std::string cpu_max; // e.g., "100000 100000" or "max"
10+
std::string memory_max; // bytes or "max"
11+
std::string pids_max; // integer or "max"
12+
};
13+
14+
// RAII helper for per-invocation transient cgroups.
15+
// Creates a uniquely named cgroup and applies optional resource limits.
16+
class InvocationCgroup {
17+
public:
18+
explicit InvocationCgroup(const CgroupLimits& limits);
19+
~InvocationCgroup();
20+
21+
// Non-copyable
22+
InvocationCgroup(const InvocationCgroup&) = delete;
23+
InvocationCgroup& operator=(const InvocationCgroup&) = delete;
24+
25+
// Moveable
26+
InvocationCgroup(InvocationCgroup&&) noexcept;
27+
InvocationCgroup& operator=(InvocationCgroup&&) noexcept;
28+
29+
bool valid() const;
30+
const std::string& path() const;
31+
32+
// Add a pid to this invocation cgroup (defaults to current process)
33+
bool add_pid(pid_t pid = 0);
34+
35+
private:
36+
std::string path_;
37+
bool created_ = false;
38+
};
39+
40+
} // namespace sandbox

tests/cgroups_limits_test.cpp

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#include <iostream>
2+
#include "sandbox/cgroups.h"
3+
4+
int main() {
5+
std::cout << "cgroups_limits_test: starting" << std::endl;
6+
if (!sandbox::is_cgroup_v2_available()) {
7+
std::cout << "cgroup v2 not available; skipping test" << std::endl;
8+
return 0;
9+
}
10+
11+
const std::string name = "native_node_limits_test_12345";
12+
std::string path = sandbox::create_transient_cgroup(name);
13+
if (path.empty()) {
14+
std::cerr << "failed to create transient cgroup" << std::endl;
15+
return 2;
16+
}
17+
18+
// Set limits
19+
if (!sandbox::set_cgroup_cpu_max(path, "100000 100000")) {
20+
std::cerr << "failed to set cpu.max" << std::endl;
21+
sandbox::remove_transient_cgroup(path);
22+
return 2;
23+
}
24+
25+
if (!sandbox::set_cgroup_memory_max(path, "33554432")) { // 32MB
26+
std::cerr << "failed to set memory.max" << std::endl;
27+
sandbox::remove_transient_cgroup(path);
28+
return 2;
29+
}
30+
31+
if (!sandbox::set_cgroup_pids_max(path, "10")) {
32+
std::cerr << "failed to set pids.max" << std::endl;
33+
sandbox::remove_transient_cgroup(path);
34+
return 2;
35+
}
36+
37+
// Read back and verify (best-effort)
38+
auto cpu = sandbox::read_cgroup_file(path + "/cpu.max");
39+
auto mem = sandbox::read_cgroup_file(path + "/memory.max");
40+
auto pids = sandbox::read_cgroup_file(path + "/pids.max");
41+
42+
std::cout << "cpu.max='" << cpu << "' memory.max='" << mem << "' pids.max='" << pids << "'" << std::endl;
43+
44+
if (cpu.empty() || mem.empty() || pids.empty()) {
45+
std::cerr << "failed to read back one or more limits" << std::endl;
46+
sandbox::remove_transient_cgroup(path);
47+
return 2;
48+
}
49+
50+
// Cleanup
51+
if (!sandbox::remove_transient_cgroup(path)) {
52+
std::cerr << "failed to remove cgroup" << std::endl;
53+
return 2;
54+
}
55+
56+
std::cout << "cgroups_limits_test: succeeded" << std::endl;
57+
return 0;
58+
}

tests/invocation_cgroup_test.cpp

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#include <iostream>
2+
#include <sys/types.h>
3+
#include <sys/wait.h>
4+
#include <unistd.h>
5+
#include <string>
6+
#include "sandbox/invocation_cgroup.h"
7+
#include "sandbox/cgroups.h"
8+
9+
int main() {
10+
std::cout << "invocation_cgroup_test: starting" << std::endl;
11+
if (!sandbox::is_cgroup_v2_available()) {
12+
std::cout << "cgroup v2 not available; skipping test" << std::endl;
13+
return 0;
14+
}
15+
16+
sandbox::CgroupLimits limits;
17+
limits.cpu_max = "100000 100000";
18+
limits.memory_max = "33554432"; // 32MB
19+
limits.pids_max = "10";
20+
21+
sandbox::InvocationCgroup ig(limits);
22+
if (!ig.valid()) {
23+
std::cerr << "failed to create invocation cgroup (privileges?)" << std::endl;
24+
return 2;
25+
}
26+
27+
pid_t pid = fork();
28+
if (pid < 0) {
29+
std::cerr << "fork failed" << std::endl;
30+
return 2;
31+
}
32+
if (pid == 0) {
33+
// child: sleep a bit and exit; pause to allow parent to add pid
34+
sleep(2);
35+
_exit(0);
36+
}
37+
38+
// Parent: add child pid to cgroup
39+
if (!ig.add_pid(pid)) {
40+
std::cerr << "failed to add pid to invocation cgroup" << std::endl;
41+
kill(pid, SIGKILL);
42+
waitpid(pid, nullptr, 0);
43+
return 2;
44+
}
45+
46+
// Read cgroup.procs and check presence
47+
std::string procs_path = ig.path() + "/cgroup.procs";
48+
std::string content = sandbox::read_cgroup_file(procs_path);
49+
if (content.find(std::to_string(pid)) == std::string::npos) {
50+
std::cerr << "pid not found in cgroup.procs: '" << content << "'" << std::endl;
51+
kill(pid, SIGKILL);
52+
waitpid(pid, nullptr, 0);
53+
return 2;
54+
}
55+
56+
// cleanup: wait for child
57+
waitpid(pid, nullptr, 0);
58+
59+
std::cout << "invocation_cgroup_test: succeeded" << std::endl;
60+
return 0;
61+
}

0 commit comments

Comments
 (0)