cntr/
cgroup.rs

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
13/// Trait for cgroup operations, supporting both v1 and v2
14trait CgroupManager {
15    /// Move a process into the cgroup of another process
16    fn move_to(&self, pid: unistd::Pid, target_pid: unistd::Pid) -> Result<()>;
17}
18
19/// Cgroup v1 (legacy) manager
20struct CgroupV1Manager {
21    procfs_path: PathBuf,
22}
23
24/// Cgroup v2 (unified) manager
25struct CgroupV2Manager {
26    mount_path: PathBuf,
27    procfs_path: PathBuf,
28}
29
30/// Hybrid manager that supports both v1 and v2
31struct HybridCgroupManager {
32    v1: CgroupV1Manager,
33    v2: CgroupV2Manager,
34}
35
36/// Null manager for systems without cgroup support
37struct NullCgroupManager;
38
39// Helper functions for cgroup v1
40
41fn 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    // example:
64    //
65    // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
66    // (1)(2)(3)   (4)   (5)      (6)      (7)   (8) (9)   (10)         (11)
67    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            // Fixed: only insert if name IS a valid subsystem
79            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
100// Cgroup v1 implementation
101impl 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
144// Cgroup v2 implementation
145impl 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            // cgroup v2 format: "0::/path/to/cgroup"
155            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        // Build path: /sys/fs/cgroup/<cgroup_path>/cgroup.procs
178        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                    // Writing to cgroup.procs requires CAP_SYS_ADMIN or root.
186                    // For unprivileged users (e.g., rootless podman), warn and continue.
187                    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
207// Hybrid implementation - tries v2 first, falls back to v1
208impl CgroupManager for HybridCgroupManager {
209    fn move_to(&self, pid: unistd::Pid, target_pid: unistd::Pid) -> Result<()> {
210        // Try v2 first
211        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
219// Null implementation - no-op when cgroups are unavailable
220impl 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
227/// Factory function to create the appropriate CgroupManager
228fn 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            // Hybrid: both v1 and v2
254            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            // Only v1
266            Ok(Box::new(CgroupV1Manager { procfs_path }))
267        }
268        (false, Some(mount_path)) => {
269            // Only v2
270            Ok(Box::new(CgroupV2Manager {
271                mount_path,
272                procfs_path,
273            }))
274        }
275        (false, None) => {
276            // No cgroups found, use null manager
277            Ok(Box::new(NullCgroupManager))
278        }
279    }
280}
281
282/// Move a process into the cgroup of another process
283pub(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        // Test single controller
301        let result = cgroup_v1_path("cpu", &mountpoints);
302        assert_eq!(result, Some(PathBuf::from("/sys/fs/cgroup/cpu/cpu/tasks")));
303
304        // Test non-existent controller
305        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        // Create a temporary proc directory structure
312        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        // Write a test cgroup file with v2 format
317        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        // Create manager with mock procfs path
322        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        // Clean up
332        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        // Create a temporary proc directory structure
343        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        // Write a test cgroup file with v1 format (no "0::" prefix)
348        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}