1use crate::error::{NucleusError, Result, StateTransition};
2use crate::resources::{CgroupState, ResourceLimits};
3use nix::sys::signal::{kill, Signal};
4use nix::unistd::Pid;
5use std::ffi::{CString, OsString};
6use std::fs;
7use std::io::Write;
8use std::mem::MaybeUninit;
9use std::os::unix::ffi::OsStrExt;
10use std::os::unix::fs::OpenOptionsExt;
11use std::os::unix::io::{AsRawFd, RawFd};
12use std::path::{Component, Path, PathBuf};
13use std::thread;
14use std::time::Duration;
15use tracing::{debug, info, warn};
16
17const CGROUP_V2_ROOT: &str = "/sys/fs/cgroup";
18const CGROUP2_SUPER_MAGIC: libc::c_long = 0x6367_7270;
19const NUCLEUS_CGROUP_ROOT_ENV: &str = "NUCLEUS_CGROUP_ROOT";
20const CGROUP_CLEANUP_RETRIES: usize = 50;
21const CGROUP_CLEANUP_SLEEP: Duration = Duration::from_millis(20);
22const CGROUP_FREEZE_RETRIES: usize = 100;
23const CGROUP_FREEZE_SLEEP: Duration = Duration::from_millis(10);
24
25pub struct Cgroup {
30 path: PathBuf,
31 state: CgroupState,
32}
33
34pub struct CgroupFreezeGuard {
36 path: PathBuf,
37 active: bool,
38}
39
40impl Cgroup {
41 pub fn create(name: &str) -> Result<Self> {
45 let root = Self::root_path()?;
46 Self::create_in_root(name, &root)
47 }
48
49 fn create_in_root(name: &str, root: &Path) -> Result<Self> {
50 Self::validate_cgroup_name(name)?;
51 Self::validate_root_path(root)?;
52
53 let state = CgroupState::Nonexistent.transition(CgroupState::Created)?;
54 let path = root.join(name);
55
56 info!("Creating cgroup at {:?}", path);
57
58 fs::create_dir_all(&path).map_err(|e| {
60 NucleusError::CgroupError(format!("Failed to create cgroup directory: {}", e))
61 })?;
62
63 Self::validate_cgroup_directory(&path)?;
64
65 Ok(Self { path, state })
66 }
67
68 fn root_path() -> Result<PathBuf> {
69 let path = Self::root_path_from_override(std::env::var_os(NUCLEUS_CGROUP_ROOT_ENV))?;
70 Self::validate_root_path(&path)?;
71 Ok(path)
72 }
73
74 fn root_path_from_override(raw: Option<OsString>) -> Result<PathBuf> {
75 match raw {
76 Some(raw) if !raw.as_os_str().is_empty() => {
77 let path = PathBuf::from(raw);
78 if !path.is_absolute() {
79 return Err(NucleusError::CgroupError(format!(
80 "{} must be an absolute path",
81 NUCLEUS_CGROUP_ROOT_ENV
82 )));
83 }
84 Ok(path)
85 }
86 _ => Ok(PathBuf::from(CGROUP_V2_ROOT)),
87 }
88 }
89
90 fn validate_cgroup_name(name: &str) -> Result<()> {
91 if name.is_empty() || name.as_bytes().contains(&0) {
92 return Err(NucleusError::CgroupError(
93 "cgroup name must be a non-empty path component".to_string(),
94 ));
95 }
96
97 let mut components = Path::new(name).components();
98 match (components.next(), components.next()) {
99 (Some(Component::Normal(_)), None) => Ok(()),
100 _ => Err(NucleusError::CgroupError(
101 "cgroup name must be a single relative path component".to_string(),
102 )),
103 }
104 }
105
106 fn validate_root_path(path: &Path) -> Result<()> {
107 if !path.is_absolute() {
108 return Err(NucleusError::CgroupError(format!(
109 "{} must be an absolute path",
110 NUCLEUS_CGROUP_ROOT_ENV
111 )));
112 }
113
114 Self::validate_directory_not_symlink(path, "cgroup root")?;
115 let canonical = fs::canonicalize(path).map_err(|e| {
116 NucleusError::CgroupError(format!(
117 "Failed to canonicalize cgroup root {:?}: {}",
118 path, e
119 ))
120 })?;
121 let canonical_cgroup_root = fs::canonicalize(CGROUP_V2_ROOT).map_err(|e| {
122 NucleusError::CgroupError(format!(
123 "Failed to canonicalize {} while validating cgroup root {:?}: {}",
124 CGROUP_V2_ROOT, path, e
125 ))
126 })?;
127
128 if !canonical.starts_with(&canonical_cgroup_root) {
129 return Err(NucleusError::CgroupError(format!(
130 "cgroup root {:?} must be inside {}",
131 path, CGROUP_V2_ROOT
132 )));
133 }
134
135 Self::ensure_path_on_cgroup2_fs(&canonical)?;
136 Self::require_cgroup_control_file(&canonical.join("cgroup.controllers"))?;
137 Self::require_cgroup_control_file(&canonical.join("cgroup.subtree_control"))?;
138 Self::require_cgroup_control_file(&canonical.join("cgroup.procs"))?;
139 Ok(())
140 }
141
142 fn validate_cgroup_directory(path: &Path) -> Result<()> {
143 Self::validate_directory_not_symlink(path, "cgroup directory")?;
144 Self::ensure_path_on_cgroup2_fs(path)?;
145 Self::require_cgroup_control_file(&path.join("cgroup.procs"))?;
146 Ok(())
147 }
148
149 fn validate_directory_not_symlink(path: &Path, description: &str) -> Result<()> {
150 let metadata = fs::symlink_metadata(path).map_err(|e| {
151 NucleusError::CgroupError(format!(
152 "Failed to inspect {} {:?}: {}",
153 description, path, e
154 ))
155 })?;
156 let file_type = metadata.file_type();
157 if file_type.is_symlink() {
158 return Err(NucleusError::CgroupError(format!(
159 "{} {:?} must not be a symlink",
160 description, path
161 )));
162 }
163 if !file_type.is_dir() {
164 return Err(NucleusError::CgroupError(format!(
165 "{} {:?} must be a directory",
166 description, path
167 )));
168 }
169 Ok(())
170 }
171
172 fn require_cgroup_control_file(path: &Path) -> Result<()> {
173 let metadata = fs::symlink_metadata(path).map_err(|e| {
174 NucleusError::CgroupError(format!(
175 "Required cgroup control file {:?} is missing or inaccessible: {}",
176 path, e
177 ))
178 })?;
179 let file_type = metadata.file_type();
180 if file_type.is_symlink() {
181 return Err(NucleusError::CgroupError(format!(
182 "cgroup control file {:?} must not be a symlink",
183 path
184 )));
185 }
186 if !file_type.is_file() {
187 return Err(NucleusError::CgroupError(format!(
188 "{:?} is not a cgroup control file",
189 path
190 )));
191 }
192 Self::ensure_path_on_cgroup2_fs(path)
193 }
194
195 fn ensure_path_on_cgroup2_fs(path: &Path) -> Result<()> {
196 let statfs = Self::statfs_path(path)?;
197 Self::ensure_cgroup2_magic(statfs.f_type, path)
198 }
199
200 fn ensure_fd_on_cgroup2_fs(fd: RawFd, path: &Path) -> Result<()> {
201 let mut statfs = MaybeUninit::<libc::statfs>::uninit();
202 let rc = unsafe { libc::fstatfs(fd, statfs.as_mut_ptr()) };
205 if rc != 0 {
206 return Err(NucleusError::CgroupError(format!(
207 "Failed to statfs opened cgroup control file {:?}: {}",
208 path,
209 std::io::Error::last_os_error()
210 )));
211 }
212 let statfs = unsafe { statfs.assume_init() };
215 Self::ensure_cgroup2_magic(statfs.f_type, path)
216 }
217
218 fn statfs_path(path: &Path) -> Result<libc::statfs> {
219 let c_path = CString::new(path.as_os_str().as_bytes()).map_err(|_| {
220 NucleusError::CgroupError(format!("cgroup path {:?} contains an interior NUL", path))
221 })?;
222 let mut statfs = MaybeUninit::<libc::statfs>::uninit();
223 let rc = unsafe { libc::statfs(c_path.as_ptr(), statfs.as_mut_ptr()) };
226 if rc != 0 {
227 return Err(NucleusError::CgroupError(format!(
228 "Failed to statfs cgroup path {:?}: {}",
229 path,
230 std::io::Error::last_os_error()
231 )));
232 }
233 Ok(unsafe { statfs.assume_init() })
236 }
237
238 fn ensure_cgroup2_magic(fs_type: libc::c_long, path: &Path) -> Result<()> {
239 if fs_type != CGROUP2_SUPER_MAGIC {
240 return Err(NucleusError::CgroupError(format!(
241 "{:?} is not on a cgroup v2 filesystem",
242 path
243 )));
244 }
245 Ok(())
246 }
247
248 pub fn set_limits(&mut self, limits: &ResourceLimits) -> Result<()> {
252 self.state = self.state.transition(CgroupState::Configured)?;
253
254 info!("Configuring cgroup limits: {:?}", limits);
255
256 if let Some(memory_bytes) = limits.memory_bytes {
258 self.write_value("memory.max", &memory_bytes.to_string())?;
259 debug!("Set memory.max = {}", memory_bytes);
260 }
261
262 if let Some(memory_high) = limits.memory_high {
264 self.write_value("memory.high", &memory_high.to_string())?;
265 debug!("Set memory.high = {}", memory_high);
266 }
267
268 if let Some(swap_max) = limits.memory_swap_max {
270 self.write_value("memory.swap.max", &swap_max.to_string())?;
271 debug!("Set memory.swap.max = {}", swap_max);
272 }
273 if limits.memory_bytes.is_some()
274 || limits.memory_high.is_some()
275 || limits.memory_swap_max.is_some()
276 {
277 self.write_value("memory.oom.group", "1")?;
278 debug!("Set memory.oom.group = 1");
279 }
280
281 if let Some(cpu_quota_us) = limits.cpu_quota_us {
283 let cpu_max = format!("{} {}", cpu_quota_us, limits.cpu_period_us);
284 self.write_value("cpu.max", &cpu_max)?;
285 debug!("Set cpu.max = {}", cpu_max);
286 }
287
288 if let Some(cpu_weight) = limits.cpu_weight {
290 self.write_value("cpu.weight", &cpu_weight.to_string())?;
291 debug!("Set cpu.weight = {}", cpu_weight);
292 }
293
294 if let Some(pids_max) = limits.pids_max {
296 self.write_value("pids.max", &pids_max.to_string())?;
297 debug!("Set pids.max = {}", pids_max);
298 }
299
300 for io_limit in &limits.io_limits {
302 let line = io_limit.to_io_max_line();
303 self.write_value("io.max", &line)?;
304 debug!("Set io.max: {}", line);
305 }
306
307 info!("Successfully configured cgroup limits");
308
309 Ok(())
310 }
311
312 pub fn attach_process(&mut self, pid: u32) -> Result<()> {
316 self.state = self.state.transition(CgroupState::Attached)?;
317
318 info!("Attaching process {} to cgroup", pid);
319
320 self.write_value("cgroup.procs", &pid.to_string())?;
321
322 info!("Successfully attached process to cgroup");
323
324 Ok(())
325 }
326
327 fn write_value(&self, file: &str, value: &str) -> Result<()> {
329 Self::write_value_at(&self.path, file, value)
330 }
331
332 fn write_value_at(path: &Path, file: &str, value: &str) -> Result<()> {
333 Self::validate_cgroup_name(file)?;
334 let file_path = path.join(file);
335 let mut control_file = fs::OpenOptions::new()
336 .write(true)
337 .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC)
338 .open(&file_path)
339 .map_err(|e| {
340 NucleusError::CgroupError(format!(
341 "Failed to open cgroup control file {:?}: {}",
342 file_path, e
343 ))
344 })?;
345 Self::ensure_fd_on_cgroup2_fs(control_file.as_raw_fd(), &file_path)?;
346 control_file.write_all(value.as_bytes()).map_err(|e| {
347 NucleusError::CgroupError(format!(
348 "Failed to write {} to {:?}: {}",
349 value, file_path, e
350 ))
351 })?;
352 Ok(())
353 }
354
355 fn read_value(&self, file: &str) -> Result<String> {
357 Self::read_value_at(&self.path, file)
358 }
359
360 fn read_value_at(path: &Path, file: &str) -> Result<String> {
361 Self::validate_cgroup_name(file)?;
362 let file_path = path.join(file);
363 fs::read_to_string(&file_path).map_err(|e| {
364 NucleusError::CgroupError(format!("Failed to read {:?}: {}", file_path, e))
365 })
366 }
367
368 fn set_frozen(&self, frozen: bool) -> Result<bool> {
369 let freeze_path = self.path.join("cgroup.freeze");
370 if !freeze_path.exists() {
371 return Ok(false);
372 }
373 self.write_value("cgroup.freeze", if frozen { "1" } else { "0" })?;
374 debug!("Set cgroup.freeze = {}", if frozen { 1 } else { 0 });
375 Ok(true)
376 }
377
378 fn parse_cgroup_events_bool(events: &str, key: &str) -> Result<bool> {
379 for line in events.lines() {
380 if let Some(value) = line
381 .strip_prefix(key)
382 .and_then(|rest| rest.strip_prefix(' '))
383 {
384 return match value.trim() {
385 "0" => Ok(false),
386 "1" => Ok(true),
387 other => Err(NucleusError::CgroupError(format!(
388 "Unexpected {} value in cgroup.events: {}",
389 key, other
390 ))),
391 };
392 }
393 }
394 Err(NucleusError::CgroupError(format!(
395 "Missing {} entry in cgroup.events",
396 key
397 )))
398 }
399
400 fn parse_cgroup_events_populated(events: &str) -> Result<bool> {
401 Self::parse_cgroup_events_bool(events, "populated")
402 }
403
404 fn read_cgroup_events_frozen(path: &Path) -> Result<bool> {
405 let events = Self::read_value_at(path, "cgroup.events")?;
406 Self::parse_cgroup_events_bool(&events, "frozen")
407 }
408
409 fn wait_for_frozen_state(path: &Path, frozen: bool) -> Result<()> {
410 let events_path = path.join("cgroup.events");
411 if !events_path.exists() {
412 return Ok(());
413 }
414
415 for attempt in 0..CGROUP_FREEZE_RETRIES {
416 if Self::read_cgroup_events_frozen(path)? == frozen {
417 return Ok(());
418 }
419 if attempt + 1 < CGROUP_FREEZE_RETRIES {
420 thread::sleep(CGROUP_FREEZE_SLEEP);
421 }
422 }
423
424 Err(NucleusError::CgroupError(format!(
425 "Timed out waiting for cgroup {:?} to become {}",
426 path,
427 if frozen { "frozen" } else { "thawed" }
428 )))
429 }
430
431 pub fn freeze_existing(path: &Path) -> Result<CgroupFreezeGuard> {
433 Self::validate_cgroup_directory(path)?;
434 let freeze_path = path.join("cgroup.freeze");
435 if !freeze_path.exists() {
436 return Err(NucleusError::CgroupError(format!(
437 "Cgroup {:?} does not support cgroup.freeze",
438 path
439 )));
440 }
441
442 Self::write_value_at(path, "cgroup.freeze", "1")?;
443 Self::wait_for_frozen_state(path, true)?;
444 debug!("Frozen cgroup {:?}", path);
445 Ok(CgroupFreezeGuard {
446 path: path.to_path_buf(),
447 active: true,
448 })
449 }
450
451 fn read_pids(&self) -> Result<Vec<Pid>> {
452 let file_path = self.path.join("cgroup.procs");
453 if !file_path.exists() {
454 return Ok(Vec::new());
455 }
456 let content = fs::read_to_string(&file_path).map_err(|e| {
457 NucleusError::CgroupError(format!("Failed to read {:?}: {}", file_path, e))
458 })?;
459 content
460 .lines()
461 .filter(|line| !line.trim().is_empty())
462 .map(|line| {
463 line.trim().parse::<i32>().map(Pid::from_raw).map_err(|e| {
464 NucleusError::CgroupError(format!(
465 "Failed to parse pid '{}' from {:?}: {}",
466 line.trim(),
467 file_path,
468 e
469 ))
470 })
471 })
472 .collect()
473 }
474
475 fn is_populated(&self) -> Result<bool> {
476 let events_path = self.path.join("cgroup.events");
477 if events_path.exists() {
478 let events = fs::read_to_string(&events_path).map_err(|e| {
479 NucleusError::CgroupError(format!("Failed to read {:?}: {}", events_path, e))
480 })?;
481 return Self::parse_cgroup_events_populated(&events);
482 }
483 Ok(!self.read_pids()?.is_empty())
484 }
485
486 fn kill_visible_processes(&self) -> Result<()> {
487 for pid in self.read_pids()? {
488 match kill(pid, Signal::SIGKILL) {
489 Ok(()) => {}
490 Err(nix::errno::Errno::ESRCH) => {}
491 Err(e) => {
492 return Err(NucleusError::CgroupError(format!(
493 "Failed to SIGKILL pid {} in {:?}: {}",
494 pid, self.path, e
495 )))
496 }
497 }
498 }
499 Ok(())
500 }
501
502 fn kill_all_processes(&self) -> Result<()> {
503 let kill_path = self.path.join("cgroup.kill");
504 if kill_path.exists() {
505 self.write_value("cgroup.kill", "1")?;
506 debug!("Triggered cgroup.kill for {:?}", self.path);
507 }
508 self.kill_visible_processes()
509 }
510
511 fn wait_until_empty(&self) -> Result<()> {
512 for attempt in 0..CGROUP_CLEANUP_RETRIES {
513 if !self.is_populated()? {
514 return Ok(());
515 }
516 if attempt + 1 < CGROUP_CLEANUP_RETRIES {
517 self.kill_visible_processes()?;
518 thread::sleep(CGROUP_CLEANUP_SLEEP);
519 }
520 }
521
522 let remaining = self
523 .read_pids()?
524 .into_iter()
525 .map(|pid| pid.to_string())
526 .collect::<Vec<_>>();
527 Err(NucleusError::CgroupError(format!(
528 "Timed out waiting for cgroup {:?} to drain (remaining pids: {})",
529 self.path,
530 if remaining.is_empty() {
531 "<unknown>".to_string()
532 } else {
533 remaining.join(", ")
534 }
535 )))
536 }
537
538 pub fn memory_current(&self) -> Result<u64> {
540 let value = self.read_value("memory.current")?;
541 value.trim().parse().map_err(|e| {
542 NucleusError::CgroupError(format!("Failed to parse memory.current: {}", e))
543 })
544 }
545
546 pub fn install_gpu_device_allowlist(
557 &self,
558 gpu_specs: &[crate::filesystem::DeviceNodeSpec],
559 include_tty: bool,
560 best_effort: bool,
561 ) -> Result<bool> {
562 use crate::resources::{base_device_specs, install_device_allowlist, DeviceAllowSpec};
563 let mut specs: Vec<DeviceAllowSpec> = base_device_specs(include_tty);
564 for dev in gpu_specs {
565 specs.push(DeviceAllowSpec {
566 is_block: dev.is_block,
567 major: dev.major,
568 minor: dev.minor,
569 });
570 }
571 install_device_allowlist(&self.path, &specs, best_effort)
572 }
573
574 pub fn path(&self) -> &Path {
576 &self.path
577 }
578
579 pub fn state(&self) -> CgroupState {
581 self.state
582 }
583
584 pub fn cleanup(mut self) -> Result<()> {
588 info!("Cleaning up cgroup {:?}", self.path);
589
590 if self.path.exists() {
591 let froze = self.set_frozen(true)?;
592 let cleanup_result: Result<()> = (|| {
593 self.kill_all_processes()?;
594 self.wait_until_empty()?;
595 fs::remove_dir(&self.path).map_err(|e| {
596 NucleusError::CgroupError(format!("Failed to remove cgroup: {}", e))
599 })?;
600 Ok(())
601 })();
602 if cleanup_result.is_err() && froze {
603 if let Err(e) = self.set_frozen(false) {
604 warn!(
605 "Failed to unfreeze cgroup {:?} after cleanup error: {}",
606 self.path, e
607 );
608 }
609 }
610 cleanup_result?;
611 }
612
613 self.state = CgroupState::Removed;
615 info!("Successfully cleaned up cgroup");
616
617 Ok(())
618 }
619}
620
621impl Drop for CgroupFreezeGuard {
622 fn drop(&mut self) {
623 if self.active && self.path.exists() {
624 if let Err(e) = Cgroup::write_value_at(&self.path, "cgroup.freeze", "0")
625 .and_then(|_| Cgroup::wait_for_frozen_state(&self.path, false))
626 {
627 warn!("Failed to thaw cgroup {:?}: {}", self.path, e);
628 }
629 self.active = false;
630 }
631 }
632}
633
634impl Drop for Cgroup {
635 fn drop(&mut self) {
636 if !self.state.is_terminal() && self.path.exists() {
637 let froze = self.set_frozen(true).unwrap_or(false);
638 let _ = self.kill_all_processes();
639 let _ = self.wait_until_empty();
640 let _ = fs::remove_dir(&self.path);
641 if self.path.exists() && froze {
642 let _ = self.set_frozen(false);
643 }
644 }
645 }
646}
647
648#[cfg(test)]
649mod tests {
650 use super::*;
651 use std::ffi::OsString;
652 use std::os::unix::fs::symlink;
653 use std::sync::Mutex;
654
655 static CGROUP_ENV_LOCK: Mutex<()> = Mutex::new(());
656
657 #[test]
658 fn test_resource_limits_unlimited() {
659 let limits = ResourceLimits::unlimited();
660 assert!(limits.memory_bytes.is_none());
661 assert!(limits.memory_high.is_none());
662 assert!(limits.memory_swap_max.is_none());
663 assert!(limits.cpu_quota_us.is_none());
664 assert!(limits.cpu_weight.is_none());
665 assert!(limits.pids_max.is_none());
666 assert!(limits.io_limits.is_empty());
667 }
668
669 #[test]
670 fn test_cgroup_root_override_requires_absolute_path() {
671 assert_eq!(
672 Cgroup::root_path_from_override(None).unwrap(),
673 PathBuf::from(CGROUP_V2_ROOT)
674 );
675 assert_eq!(
676 Cgroup::root_path_from_override(Some(OsString::from(""))).unwrap(),
677 PathBuf::from(CGROUP_V2_ROOT)
678 );
679 assert_eq!(
680 Cgroup::root_path_from_override(Some(OsString::from("/sys/fs/cgroup/example.service")))
681 .unwrap(),
682 PathBuf::from("/sys/fs/cgroup/example.service")
683 );
684 assert!(Cgroup::root_path_from_override(Some(OsString::from("relative"))).is_err());
685 }
686
687 #[test]
688 fn test_cgroup_name_must_be_single_path_component() {
689 assert!(Cgroup::validate_cgroup_name("nucleus-abc123").is_ok());
690 assert!(Cgroup::validate_cgroup_name("").is_err());
691 assert!(Cgroup::validate_cgroup_name("../escape").is_err());
692 assert!(Cgroup::validate_cgroup_name("/sys/fs/cgroup/escape").is_err());
693 assert!(Cgroup::validate_cgroup_name("parent/child").is_err());
694 }
695
696 #[test]
697 fn test_parse_cgroup_events_bool_reads_frozen_state() {
698 let events = "populated 1\nfrozen 0\n";
699 assert!(!Cgroup::parse_cgroup_events_bool(events, "frozen").unwrap());
700 assert!(Cgroup::parse_cgroup_events_bool("frozen 1\n", "frozen").unwrap());
701 assert!(Cgroup::parse_cgroup_events_bool("frozen 2\n", "frozen").is_err());
702 assert!(Cgroup::parse_cgroup_events_bool("populated 1\n", "frozen").is_err());
703 }
704
705 #[test]
706 fn test_cgroup_root_validation_rejects_regular_filesystem() {
707 let temp = tempfile::tempdir().unwrap();
708
709 assert!(Cgroup::validate_root_path(temp.path()).is_err());
710 }
711
712 #[test]
713 fn test_create_rejects_regular_filesystem_root_before_child_creation() {
714 let temp = tempfile::tempdir().unwrap();
715 let child = temp.path().join("nucleus-bypass");
716
717 assert!(Cgroup::create_in_root("nucleus-bypass", temp.path()).is_err());
718 assert!(
719 !child.exists(),
720 "regular filesystem root must be rejected before creating a fake cgroup"
721 );
722 }
723
724 #[test]
725 fn test_cgroup_root_env_rejects_regular_filesystem() {
726 let _guard = CGROUP_ENV_LOCK.lock().unwrap();
727 let previous = std::env::var_os(NUCLEUS_CGROUP_ROOT_ENV);
728 let temp = tempfile::tempdir().unwrap();
729 let child = temp.path().join("nucleus-bypass");
730
731 std::env::set_var(NUCLEUS_CGROUP_ROOT_ENV, temp.path().as_os_str());
732 let result = Cgroup::create("nucleus-bypass");
733 match previous {
734 Some(value) => std::env::set_var(NUCLEUS_CGROUP_ROOT_ENV, value),
735 None => std::env::remove_var(NUCLEUS_CGROUP_ROOT_ENV),
736 }
737
738 assert!(result.is_err());
739 assert!(
740 !child.exists(),
741 "regular filesystem override must be rejected before creating a fake cgroup"
742 );
743 }
744
745 #[test]
746 fn test_write_value_rejects_preexisting_regular_file() {
747 let temp = tempfile::tempdir().unwrap();
748 let path = temp.path().join("fake-cgroup");
749 fs::create_dir(&path).unwrap();
750 let control_file = path.join("memory.max");
751 fs::write(&control_file, "old").unwrap();
752
753 let cgroup = Cgroup {
754 path,
755 state: CgroupState::Removed,
756 };
757 assert!(cgroup.write_value("memory.max", "123").is_err());
758 assert_eq!(fs::read_to_string(control_file).unwrap(), "old");
759 }
760
761 #[test]
762 fn test_write_value_rejects_symlink_control_file() {
763 let temp = tempfile::tempdir().unwrap();
764 let path = temp.path().join("fake-cgroup");
765 let target = temp.path().join("target");
766 fs::create_dir(&path).unwrap();
767 fs::write(&target, "old").unwrap();
768 symlink(&target, path.join("memory.max")).unwrap();
769
770 let cgroup = Cgroup {
771 path,
772 state: CgroupState::Removed,
773 };
774 assert!(cgroup.write_value("memory.max", "123").is_err());
775 assert_eq!(fs::read_to_string(target).unwrap(), "old");
776 }
777
778 #[test]
782 fn test_cleanup_sets_removed_only_after_success() {
783 let source = include_str!("cgroup.rs");
787 let fn_start = source.find("pub fn cleanup").unwrap();
788 let after = &source[fn_start..];
789 let open = after.find('{').unwrap();
790 let mut depth = 0u32;
791 let mut fn_end = open;
792 for (i, ch) in after[open..].char_indices() {
793 match ch {
794 '{' => depth += 1,
795 '}' => {
796 depth -= 1;
797 if depth == 0 {
798 fn_end = open + i + 1;
799 break;
800 }
801 }
802 _ => {}
803 }
804 }
805 let cleanup_body = &after[..fn_end];
806 let removed_pos = cleanup_body
807 .find("Removed")
808 .expect("must reference Removed state");
809 let remove_dir_pos = cleanup_body
810 .find("remove_dir")
811 .expect("must call remove_dir");
812 assert!(
813 removed_pos > remove_dir_pos,
814 "CgroupState::Removed must be set AFTER remove_dir succeeds, not before"
815 );
816 }
817
818 #[test]
819 fn test_parse_cgroup_events_populated() {
820 assert!(Cgroup::parse_cgroup_events_populated("populated 1\nfrozen 0\n").unwrap());
821 assert!(!Cgroup::parse_cgroup_events_populated("frozen 0\npopulated 0\n").unwrap());
822 }
823
824 #[test]
825 fn test_set_limits_source_enables_memory_oom_group() {
826 let source = include_str!("cgroup.rs");
827 let fn_start = source.find("pub fn set_limits").unwrap();
828 let after = &source[fn_start..];
829 let open = after.find('{').unwrap();
830 let mut depth = 0u32;
831 let mut fn_end = open;
832 for (i, ch) in after[open..].char_indices() {
833 match ch {
834 '{' => depth += 1,
835 '}' => {
836 depth -= 1;
837 if depth == 0 {
838 fn_end = open + i + 1;
839 break;
840 }
841 }
842 _ => {}
843 }
844 }
845 let body = &after[..fn_end];
846 assert!(
847 body.contains("memory.oom.group"),
848 "set_limits must enable memory.oom.group when memory controls are configured"
849 );
850 }
851
852 #[test]
853 fn test_cleanup_source_kills_processes_before_remove_dir() {
854 let source = include_str!("cgroup.rs");
855 let fn_start = source.find("pub fn cleanup").unwrap();
856 let after = &source[fn_start..];
857 let open = after.find('{').unwrap();
858 let mut depth = 0u32;
859 let mut fn_end = open;
860 for (i, ch) in after[open..].char_indices() {
861 match ch {
862 '{' => depth += 1,
863 '}' => {
864 depth -= 1;
865 if depth == 0 {
866 fn_end = open + i + 1;
867 break;
868 }
869 }
870 _ => {}
871 }
872 }
873 let body = &after[..fn_end];
874 let freeze_pos = body.find("set_frozen(true)").unwrap();
875 let kill_pos = body.find("kill_all_processes").unwrap();
876 let remove_dir_pos = body.find("remove_dir").unwrap();
877 assert!(
878 freeze_pos < kill_pos && kill_pos < remove_dir_pos,
879 "cleanup must freeze and kill the cgroup before attempting remove_dir"
880 );
881 }
882}