1use anyhow::Context;
2use log::{debug, warn};
3use nix::unistd;
4use std::collections::HashMap;
5use std::fs::File;
6use std::io::Write;
7use std::io::{BufRead, BufReader};
8use std::path::PathBuf;
9
10use crate::procfs;
11use crate::result::Result;
12
13trait CgroupManager {
15 fn move_to(&self, pid: unistd::Pid, target_pid: unistd::Pid) -> Result<()>;
17}
18
19struct CgroupV1Manager {
21 procfs_path: PathBuf,
22}
23
24struct CgroupV2Manager {
26 mount_path: PathBuf,
27 procfs_path: PathBuf,
28}
29
30struct HybridCgroupManager {
32 v1: CgroupV1Manager,
33 v2: CgroupV2Manager,
34}
35
36struct NullCgroupManager;
38
39fn get_subsystems() -> Result<Vec<String>> {
42 let path = "/proc/cgroups";
43 let f = File::open(path).context("failed to open /proc/cgroups")?;
44 let reader = BufReader::new(f);
45 let mut subsystems: Vec<String> = Vec::new();
46 for l in reader.lines() {
47 let line = l.context("failed to read line from /proc/cgroups")?;
48 if line.starts_with('#') {
49 continue;
50 }
51 let fields: Vec<&str> = line.split('\t').collect();
52 if fields.len() >= 4 && fields[3] != "0" {
53 subsystems.push(fields[0].to_string());
54 }
55 }
56 Ok(subsystems)
57}
58
59fn get_mounts() -> Result<HashMap<String, String>> {
60 let subsystems =
61 get_subsystems().context("failed to obtain cgroup subsystems from /proc/cgroups")?;
62 let path = "/proc/self/mountinfo";
63 let f = File::open(path).context("failed to open /proc/self/mountinfo")?;
68 let reader = BufReader::new(f);
69 let mut mountpoints: HashMap<String, String> = HashMap::new();
70 for l in reader.lines() {
71 let line = l.with_context(|| format!("failed to read line from {}", path))?;
72 let fields: Vec<&str> = line.split(' ').collect();
73 if fields.len() < 11 || fields[9] != "cgroup" {
74 continue;
75 }
76 for option in fields[10].split(',') {
77 let name = option.strip_prefix("name=").unwrap_or(option).to_string();
78 if subsystems.contains(&name) {
80 mountpoints.insert(name, fields[4].to_string());
81 }
82 }
83 }
84 Ok(mountpoints)
85}
86
87fn cgroup_v1_path(cgroup: &str, mountpoints: &HashMap<String, String>) -> Option<PathBuf> {
88 for c in cgroup.split(',') {
89 let m = mountpoints.get(c);
90 if let Some(path) = m {
91 let mut tasks_path = PathBuf::from(path);
92 tasks_path.push(cgroup);
93 tasks_path.push("tasks");
94 return Some(tasks_path);
95 }
96 }
97 None
98}
99
100impl CgroupV1Manager {
102 fn get_cgroups(&self, pid: unistd::Pid) -> Result<Vec<String>> {
103 let path = self.procfs_path.join(format!("{}/cgroup", pid));
104 let f = File::open(&path)
105 .with_context(|| format!("failed to open cgroup file {}", path.display()))?;
106 let reader = BufReader::new(f);
107 let mut cgroups: Vec<String> = Vec::new();
108 for l in reader.lines() {
109 let line = l.with_context(|| format!("failed to read line from {}", path.display()))?;
110 let fields: Vec<&str> = line.split(":/").collect();
111 if fields.len() >= 2 {
112 cgroups.push(fields[1].to_string());
113 }
114 }
115 Ok(cgroups)
116 }
117}
118
119impl CgroupManager for CgroupV1Manager {
120 fn move_to(&self, pid: unistd::Pid, target_pid: unistd::Pid) -> Result<()> {
121 let cgroups = self
122 .get_cgroups(target_pid)
123 .with_context(|| format!("failed to get cgroups for PID {}", target_pid))?;
124 let mountpoints = get_mounts().context("failed to get cgroup mountpoints")?;
125
126 for cgroup in cgroups {
127 let p = cgroup_v1_path(&cgroup, &mountpoints);
128 if let Some(path) = p {
129 match File::create(&path) {
130 Ok(mut buffer) => {
131 write!(buffer, "{}", pid)
132 .with_context(|| format!("failed to write PID to cgroup {}", cgroup))?;
133 }
134 Err(err) => {
135 warn!("failed to enter {} cgroup: {}", cgroup, err);
136 }
137 }
138 }
139 }
140 Ok(())
141 }
142}
143
144impl CgroupV2Manager {
146 fn get_cgroup_path(&self, pid: unistd::Pid) -> Result<Option<String>> {
147 let path = self.procfs_path.join(format!("{}/cgroup", pid));
148 let f = File::open(&path)
149 .with_context(|| format!("failed to open cgroup file {}", path.display()))?;
150 let reader = BufReader::new(f);
151
152 for l in reader.lines() {
153 let line = l.with_context(|| format!("failed to read line from {}", path.display()))?;
154 if let Some(stripped) = line.strip_prefix("0::") {
156 return Ok(Some(stripped.to_string()));
157 }
158 }
159 Ok(None)
160 }
161}
162
163impl CgroupManager for CgroupV2Manager {
164 fn move_to(&self, pid: unistd::Pid, target_pid: unistd::Pid) -> Result<()> {
165 let target_cgroup = self
166 .get_cgroup_path(target_pid)
167 .with_context(|| format!("failed to get cgroup v2 path for PID {}", target_pid))?;
168
169 let Some(cgroup_path) = target_cgroup else {
170 warn!(
171 "PID {} not in a cgroup v2, skipping cgroup migration",
172 target_pid
173 );
174 return Ok(());
175 };
176
177 let mut procs_path = self.mount_path.clone();
179 procs_path.push(cgroup_path.trim_start_matches('/'));
180 procs_path.push("cgroup.procs");
181
182 match File::options().append(true).open(&procs_path) {
183 Ok(mut file) => {
184 if let Err(err) = write!(file, "{}", pid) {
185 warn!(
188 "failed to write PID to cgroup.procs at {}: {} (try running as root or with CAP_SYS_ADMIN)",
189 procs_path.display(),
190 err
191 );
192 }
193 }
194 Err(err) => {
195 warn!(
196 "failed to open cgroup.procs at {}: {}",
197 procs_path.display(),
198 err
199 );
200 }
201 }
202
203 Ok(())
204 }
205}
206
207impl CgroupManager for HybridCgroupManager {
209 fn move_to(&self, pid: unistd::Pid, target_pid: unistd::Pid) -> Result<()> {
210 if let Err(e) = self.v2.move_to(pid, target_pid) {
212 warn!("cgroup v2 migration failed: {}, trying v1", e);
213 self.v1.move_to(pid, target_pid)?;
214 }
215 Ok(())
216 }
217}
218
219impl CgroupManager for NullCgroupManager {
221 fn move_to(&self, _pid: unistd::Pid, _target_pid: unistd::Pid) -> Result<()> {
222 debug!("cgroup support not detected, skipping cgroup migration");
223 Ok(())
224 }
225}
226
227fn create_manager() -> Result<Box<dyn CgroupManager>> {
229 let path = "/proc/self/mountinfo";
230 let f = File::open(path).context("failed to open /proc/self/mountinfo")?;
231 let reader = BufReader::new(f);
232
233 let mut has_v1 = false;
234 let mut v2_mount: Option<PathBuf> = None;
235
236 for l in reader.lines() {
237 let line = l.with_context(|| format!("failed to read line from {}", path))?;
238 let fields: Vec<&str> = line.split(' ').collect();
239 if fields.len() < 10 {
240 continue;
241 }
242 if fields[9] == "cgroup" {
243 has_v1 = true;
244 } else if fields[9] == "cgroup2" {
245 v2_mount = Some(PathBuf::from(fields[4]));
246 }
247 }
248
249 let procfs_path = procfs::get_path();
250
251 match (has_v1, v2_mount) {
252 (true, Some(mount_path)) => {
253 Ok(Box::new(HybridCgroupManager {
255 v1: CgroupV1Manager {
256 procfs_path: procfs_path.clone(),
257 },
258 v2: CgroupV2Manager {
259 mount_path,
260 procfs_path,
261 },
262 }))
263 }
264 (true, None) => {
265 Ok(Box::new(CgroupV1Manager { procfs_path }))
267 }
268 (false, Some(mount_path)) => {
269 Ok(Box::new(CgroupV2Manager {
271 mount_path,
272 procfs_path,
273 }))
274 }
275 (false, None) => {
276 Ok(Box::new(NullCgroupManager))
278 }
279 }
280}
281
282pub(crate) fn move_to(pid: unistd::Pid, target_pid: unistd::Pid) -> Result<()> {
284 let manager = create_manager().context("failed to create cgroup manager")?;
285 manager.move_to(pid, target_pid)
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use std::fs;
292 use std::io::Write as IoWrite;
293
294 #[test]
295 fn test_cgroup_v1_path_construction() {
296 let mut mountpoints = HashMap::new();
297 mountpoints.insert("cpu".to_string(), "/sys/fs/cgroup/cpu".to_string());
298 mountpoints.insert("memory".to_string(), "/sys/fs/cgroup/memory".to_string());
299
300 let result = cgroup_v1_path("cpu", &mountpoints);
302 assert_eq!(result, Some(PathBuf::from("/sys/fs/cgroup/cpu/cpu/tasks")));
303
304 let result = cgroup_v1_path("blkio", &mountpoints);
306 assert_eq!(result, None);
307 }
308
309 #[test]
310 fn test_cgroup_v2_path_parses_correctly() {
311 let temp_dir = std::env::temp_dir().join(format!("cntr_test_{}", std::process::id()));
313 let pid_dir = temp_dir.join("12345");
314 fs::create_dir_all(&pid_dir).unwrap();
315
316 let cgroup_file = pid_dir.join("cgroup");
318 let mut file = fs::File::create(&cgroup_file).unwrap();
319 writeln!(file, "0::/user.slice/user-1000.slice/session-3.scope").unwrap();
320
321 let manager = CgroupV2Manager {
323 mount_path: PathBuf::from("/sys/fs/cgroup"),
324 procfs_path: temp_dir.clone(),
325 };
326
327 let result = manager
328 .get_cgroup_path(unistd::Pid::from_raw(12345))
329 .unwrap();
330
331 fs::remove_dir_all(&temp_dir).unwrap();
333
334 assert_eq!(
335 result,
336 Some("/user.slice/user-1000.slice/session-3.scope".to_string())
337 );
338 }
339
340 #[test]
341 fn test_cgroup_v2_path_returns_none_for_v1() {
342 let temp_dir = std::env::temp_dir().join(format!("cntr_test_v1_{}", std::process::id()));
344 let pid_dir = temp_dir.join("12346");
345 fs::create_dir_all(&pid_dir).unwrap();
346
347 let cgroup_file = pid_dir.join("cgroup");
349 let mut file = fs::File::create(&cgroup_file).unwrap();
350 writeln!(file, "1:name=systemd:/user.slice").unwrap();
351 writeln!(file, "2:cpu,cpuacct:/user.slice").unwrap();
352
353 let manager = CgroupV2Manager {
354 mount_path: PathBuf::from("/sys/fs/cgroup"),
355 procfs_path: temp_dir.clone(),
356 };
357
358 let result = manager
359 .get_cgroup_path(unistd::Pid::from_raw(12346))
360 .unwrap();
361
362 fs::remove_dir_all(&temp_dir).unwrap();
363
364 assert_eq!(result, None);
365 }
366}