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);
22
23pub struct Cgroup {
28 path: PathBuf,
29 state: CgroupState,
30}
31
32impl Cgroup {
33 pub fn create(name: &str) -> Result<Self> {
37 let root = Self::root_path()?;
38 Self::create_in_root(name, &root)
39 }
40
41 fn create_in_root(name: &str, root: &Path) -> Result<Self> {
42 Self::validate_cgroup_name(name)?;
43 Self::validate_root_path(root)?;
44
45 let state = CgroupState::Nonexistent.transition(CgroupState::Created)?;
46 let path = root.join(name);
47
48 info!("Creating cgroup at {:?}", path);
49
50 fs::create_dir_all(&path).map_err(|e| {
52 NucleusError::CgroupError(format!("Failed to create cgroup directory: {}", e))
53 })?;
54
55 Self::validate_cgroup_directory(&path)?;
56
57 Ok(Self { path, state })
58 }
59
60 fn root_path() -> Result<PathBuf> {
61 let path = Self::root_path_from_override(std::env::var_os(NUCLEUS_CGROUP_ROOT_ENV))?;
62 Self::validate_root_path(&path)?;
63 Ok(path)
64 }
65
66 fn root_path_from_override(raw: Option<OsString>) -> Result<PathBuf> {
67 match raw {
68 Some(raw) if !raw.as_os_str().is_empty() => {
69 let path = PathBuf::from(raw);
70 if !path.is_absolute() {
71 return Err(NucleusError::CgroupError(format!(
72 "{} must be an absolute path",
73 NUCLEUS_CGROUP_ROOT_ENV
74 )));
75 }
76 Ok(path)
77 }
78 _ => Ok(PathBuf::from(CGROUP_V2_ROOT)),
79 }
80 }
81
82 fn validate_cgroup_name(name: &str) -> Result<()> {
83 if name.is_empty() || name.as_bytes().contains(&0) {
84 return Err(NucleusError::CgroupError(
85 "cgroup name must be a non-empty path component".to_string(),
86 ));
87 }
88
89 let mut components = Path::new(name).components();
90 match (components.next(), components.next()) {
91 (Some(Component::Normal(_)), None) => Ok(()),
92 _ => Err(NucleusError::CgroupError(
93 "cgroup name must be a single relative path component".to_string(),
94 )),
95 }
96 }
97
98 fn validate_root_path(path: &Path) -> Result<()> {
99 if !path.is_absolute() {
100 return Err(NucleusError::CgroupError(format!(
101 "{} must be an absolute path",
102 NUCLEUS_CGROUP_ROOT_ENV
103 )));
104 }
105
106 Self::validate_directory_not_symlink(path, "cgroup root")?;
107 let canonical = fs::canonicalize(path).map_err(|e| {
108 NucleusError::CgroupError(format!(
109 "Failed to canonicalize cgroup root {:?}: {}",
110 path, e
111 ))
112 })?;
113 let canonical_cgroup_root = fs::canonicalize(CGROUP_V2_ROOT).map_err(|e| {
114 NucleusError::CgroupError(format!(
115 "Failed to canonicalize {} while validating cgroup root {:?}: {}",
116 CGROUP_V2_ROOT, path, e
117 ))
118 })?;
119
120 if !canonical.starts_with(&canonical_cgroup_root) {
121 return Err(NucleusError::CgroupError(format!(
122 "cgroup root {:?} must be inside {}",
123 path, CGROUP_V2_ROOT
124 )));
125 }
126
127 Self::ensure_path_on_cgroup2_fs(&canonical)?;
128 Self::require_cgroup_control_file(&canonical.join("cgroup.controllers"))?;
129 Self::require_cgroup_control_file(&canonical.join("cgroup.subtree_control"))?;
130 Self::require_cgroup_control_file(&canonical.join("cgroup.procs"))?;
131 Ok(())
132 }
133
134 fn validate_cgroup_directory(path: &Path) -> Result<()> {
135 Self::validate_directory_not_symlink(path, "cgroup directory")?;
136 Self::ensure_path_on_cgroup2_fs(path)?;
137 Self::require_cgroup_control_file(&path.join("cgroup.procs"))?;
138 Ok(())
139 }
140
141 fn validate_directory_not_symlink(path: &Path, description: &str) -> Result<()> {
142 let metadata = fs::symlink_metadata(path).map_err(|e| {
143 NucleusError::CgroupError(format!(
144 "Failed to inspect {} {:?}: {}",
145 description, path, e
146 ))
147 })?;
148 let file_type = metadata.file_type();
149 if file_type.is_symlink() {
150 return Err(NucleusError::CgroupError(format!(
151 "{} {:?} must not be a symlink",
152 description, path
153 )));
154 }
155 if !file_type.is_dir() {
156 return Err(NucleusError::CgroupError(format!(
157 "{} {:?} must be a directory",
158 description, path
159 )));
160 }
161 Ok(())
162 }
163
164 fn require_cgroup_control_file(path: &Path) -> Result<()> {
165 let metadata = fs::symlink_metadata(path).map_err(|e| {
166 NucleusError::CgroupError(format!(
167 "Required cgroup control file {:?} is missing or inaccessible: {}",
168 path, e
169 ))
170 })?;
171 let file_type = metadata.file_type();
172 if file_type.is_symlink() {
173 return Err(NucleusError::CgroupError(format!(
174 "cgroup control file {:?} must not be a symlink",
175 path
176 )));
177 }
178 if !file_type.is_file() {
179 return Err(NucleusError::CgroupError(format!(
180 "{:?} is not a cgroup control file",
181 path
182 )));
183 }
184 Self::ensure_path_on_cgroup2_fs(path)
185 }
186
187 fn ensure_path_on_cgroup2_fs(path: &Path) -> Result<()> {
188 let statfs = Self::statfs_path(path)?;
189 Self::ensure_cgroup2_magic(statfs.f_type, path)
190 }
191
192 fn ensure_fd_on_cgroup2_fs(fd: RawFd, path: &Path) -> Result<()> {
193 let mut statfs = MaybeUninit::<libc::statfs>::uninit();
194 let rc = unsafe { libc::fstatfs(fd, statfs.as_mut_ptr()) };
197 if rc != 0 {
198 return Err(NucleusError::CgroupError(format!(
199 "Failed to statfs opened cgroup control file {:?}: {}",
200 path,
201 std::io::Error::last_os_error()
202 )));
203 }
204 let statfs = unsafe { statfs.assume_init() };
207 Self::ensure_cgroup2_magic(statfs.f_type, path)
208 }
209
210 fn statfs_path(path: &Path) -> Result<libc::statfs> {
211 let c_path = CString::new(path.as_os_str().as_bytes()).map_err(|_| {
212 NucleusError::CgroupError(format!("cgroup path {:?} contains an interior NUL", path))
213 })?;
214 let mut statfs = MaybeUninit::<libc::statfs>::uninit();
215 let rc = unsafe { libc::statfs(c_path.as_ptr(), statfs.as_mut_ptr()) };
218 if rc != 0 {
219 return Err(NucleusError::CgroupError(format!(
220 "Failed to statfs cgroup path {:?}: {}",
221 path,
222 std::io::Error::last_os_error()
223 )));
224 }
225 Ok(unsafe { statfs.assume_init() })
228 }
229
230 fn ensure_cgroup2_magic(fs_type: libc::c_long, path: &Path) -> Result<()> {
231 if fs_type != CGROUP2_SUPER_MAGIC {
232 return Err(NucleusError::CgroupError(format!(
233 "{:?} is not on a cgroup v2 filesystem",
234 path
235 )));
236 }
237 Ok(())
238 }
239
240 pub fn set_limits(&mut self, limits: &ResourceLimits) -> Result<()> {
244 self.state = self.state.transition(CgroupState::Configured)?;
245
246 info!("Configuring cgroup limits: {:?}", limits);
247
248 if let Some(memory_bytes) = limits.memory_bytes {
250 self.write_value("memory.max", &memory_bytes.to_string())?;
251 debug!("Set memory.max = {}", memory_bytes);
252 }
253
254 if let Some(memory_high) = limits.memory_high {
256 self.write_value("memory.high", &memory_high.to_string())?;
257 debug!("Set memory.high = {}", memory_high);
258 }
259
260 if let Some(swap_max) = limits.memory_swap_max {
262 self.write_value("memory.swap.max", &swap_max.to_string())?;
263 debug!("Set memory.swap.max = {}", swap_max);
264 }
265 if limits.memory_bytes.is_some()
266 || limits.memory_high.is_some()
267 || limits.memory_swap_max.is_some()
268 {
269 self.write_value("memory.oom.group", "1")?;
270 debug!("Set memory.oom.group = 1");
271 }
272
273 if let Some(cpu_quota_us) = limits.cpu_quota_us {
275 let cpu_max = format!("{} {}", cpu_quota_us, limits.cpu_period_us);
276 self.write_value("cpu.max", &cpu_max)?;
277 debug!("Set cpu.max = {}", cpu_max);
278 }
279
280 if let Some(cpu_weight) = limits.cpu_weight {
282 self.write_value("cpu.weight", &cpu_weight.to_string())?;
283 debug!("Set cpu.weight = {}", cpu_weight);
284 }
285
286 if let Some(pids_max) = limits.pids_max {
288 self.write_value("pids.max", &pids_max.to_string())?;
289 debug!("Set pids.max = {}", pids_max);
290 }
291
292 for io_limit in &limits.io_limits {
294 let line = io_limit.to_io_max_line();
295 self.write_value("io.max", &line)?;
296 debug!("Set io.max: {}", line);
297 }
298
299 info!("Successfully configured cgroup limits");
300
301 Ok(())
302 }
303
304 pub fn attach_process(&mut self, pid: u32) -> Result<()> {
308 self.state = self.state.transition(CgroupState::Attached)?;
309
310 info!("Attaching process {} to cgroup", pid);
311
312 self.write_value("cgroup.procs", &pid.to_string())?;
313
314 info!("Successfully attached process to cgroup");
315
316 Ok(())
317 }
318
319 fn write_value(&self, file: &str, value: &str) -> Result<()> {
321 Self::validate_cgroup_name(file)?;
322 let file_path = self.path.join(file);
323 let mut control_file = fs::OpenOptions::new()
324 .write(true)
325 .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC)
326 .open(&file_path)
327 .map_err(|e| {
328 NucleusError::CgroupError(format!(
329 "Failed to open cgroup control file {:?}: {}",
330 file_path, e
331 ))
332 })?;
333 Self::ensure_fd_on_cgroup2_fs(control_file.as_raw_fd(), &file_path)?;
334 control_file.write_all(value.as_bytes()).map_err(|e| {
335 NucleusError::CgroupError(format!(
336 "Failed to write {} to {:?}: {}",
337 value, file_path, e
338 ))
339 })?;
340 Ok(())
341 }
342
343 fn read_value(&self, file: &str) -> Result<String> {
345 let file_path = self.path.join(file);
346 fs::read_to_string(&file_path).map_err(|e| {
347 NucleusError::CgroupError(format!("Failed to read {:?}: {}", file_path, e))
348 })
349 }
350
351 fn set_frozen(&self, frozen: bool) -> Result<bool> {
352 let freeze_path = self.path.join("cgroup.freeze");
353 if !freeze_path.exists() {
354 return Ok(false);
355 }
356 self.write_value("cgroup.freeze", if frozen { "1" } else { "0" })?;
357 debug!("Set cgroup.freeze = {}", if frozen { 1 } else { 0 });
358 Ok(true)
359 }
360
361 fn parse_cgroup_events_populated(events: &str) -> Result<bool> {
362 for line in events.lines() {
363 if let Some(value) = line.strip_prefix("populated ") {
364 return match value.trim() {
365 "0" => Ok(false),
366 "1" => Ok(true),
367 other => Err(NucleusError::CgroupError(format!(
368 "Unexpected populated value in cgroup.events: {}",
369 other
370 ))),
371 };
372 }
373 }
374 Err(NucleusError::CgroupError(
375 "Missing populated entry in cgroup.events".to_string(),
376 ))
377 }
378
379 fn read_pids(&self) -> Result<Vec<Pid>> {
380 let file_path = self.path.join("cgroup.procs");
381 if !file_path.exists() {
382 return Ok(Vec::new());
383 }
384 let content = fs::read_to_string(&file_path).map_err(|e| {
385 NucleusError::CgroupError(format!("Failed to read {:?}: {}", file_path, e))
386 })?;
387 content
388 .lines()
389 .filter(|line| !line.trim().is_empty())
390 .map(|line| {
391 line.trim().parse::<i32>().map(Pid::from_raw).map_err(|e| {
392 NucleusError::CgroupError(format!(
393 "Failed to parse pid '{}' from {:?}: {}",
394 line.trim(),
395 file_path,
396 e
397 ))
398 })
399 })
400 .collect()
401 }
402
403 fn is_populated(&self) -> Result<bool> {
404 let events_path = self.path.join("cgroup.events");
405 if events_path.exists() {
406 let events = fs::read_to_string(&events_path).map_err(|e| {
407 NucleusError::CgroupError(format!("Failed to read {:?}: {}", events_path, e))
408 })?;
409 return Self::parse_cgroup_events_populated(&events);
410 }
411 Ok(!self.read_pids()?.is_empty())
412 }
413
414 fn kill_visible_processes(&self) -> Result<()> {
415 for pid in self.read_pids()? {
416 match kill(pid, Signal::SIGKILL) {
417 Ok(()) => {}
418 Err(nix::errno::Errno::ESRCH) => {}
419 Err(e) => {
420 return Err(NucleusError::CgroupError(format!(
421 "Failed to SIGKILL pid {} in {:?}: {}",
422 pid, self.path, e
423 )))
424 }
425 }
426 }
427 Ok(())
428 }
429
430 fn kill_all_processes(&self) -> Result<()> {
431 let kill_path = self.path.join("cgroup.kill");
432 if kill_path.exists() {
433 self.write_value("cgroup.kill", "1")?;
434 debug!("Triggered cgroup.kill for {:?}", self.path);
435 }
436 self.kill_visible_processes()
437 }
438
439 fn wait_until_empty(&self) -> Result<()> {
440 for attempt in 0..CGROUP_CLEANUP_RETRIES {
441 if !self.is_populated()? {
442 return Ok(());
443 }
444 if attempt + 1 < CGROUP_CLEANUP_RETRIES {
445 self.kill_visible_processes()?;
446 thread::sleep(CGROUP_CLEANUP_SLEEP);
447 }
448 }
449
450 let remaining = self
451 .read_pids()?
452 .into_iter()
453 .map(|pid| pid.to_string())
454 .collect::<Vec<_>>();
455 Err(NucleusError::CgroupError(format!(
456 "Timed out waiting for cgroup {:?} to drain (remaining pids: {})",
457 self.path,
458 if remaining.is_empty() {
459 "<unknown>".to_string()
460 } else {
461 remaining.join(", ")
462 }
463 )))
464 }
465
466 pub fn memory_current(&self) -> Result<u64> {
468 let value = self.read_value("memory.current")?;
469 value.trim().parse().map_err(|e| {
470 NucleusError::CgroupError(format!("Failed to parse memory.current: {}", e))
471 })
472 }
473
474 pub fn path(&self) -> &Path {
476 &self.path
477 }
478
479 pub fn state(&self) -> CgroupState {
481 self.state
482 }
483
484 pub fn cleanup(mut self) -> Result<()> {
488 info!("Cleaning up cgroup {:?}", self.path);
489
490 if self.path.exists() {
491 let froze = self.set_frozen(true)?;
492 let cleanup_result: Result<()> = (|| {
493 self.kill_all_processes()?;
494 self.wait_until_empty()?;
495 fs::remove_dir(&self.path).map_err(|e| {
496 NucleusError::CgroupError(format!("Failed to remove cgroup: {}", e))
499 })?;
500 Ok(())
501 })();
502 if cleanup_result.is_err() && froze {
503 if let Err(e) = self.set_frozen(false) {
504 warn!(
505 "Failed to unfreeze cgroup {:?} after cleanup error: {}",
506 self.path, e
507 );
508 }
509 }
510 cleanup_result?;
511 }
512
513 self.state = CgroupState::Removed;
515 info!("Successfully cleaned up cgroup");
516
517 Ok(())
518 }
519}
520
521impl Drop for Cgroup {
522 fn drop(&mut self) {
523 if !self.state.is_terminal() && self.path.exists() {
524 let froze = self.set_frozen(true).unwrap_or(false);
525 let _ = self.kill_all_processes();
526 let _ = self.wait_until_empty();
527 let _ = fs::remove_dir(&self.path);
528 if self.path.exists() && froze {
529 let _ = self.set_frozen(false);
530 }
531 }
532 }
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538 use std::ffi::OsString;
539 use std::os::unix::fs::symlink;
540 use std::sync::Mutex;
541
542 static CGROUP_ENV_LOCK: Mutex<()> = Mutex::new(());
543
544 #[test]
545 fn test_resource_limits_unlimited() {
546 let limits = ResourceLimits::unlimited();
547 assert!(limits.memory_bytes.is_none());
548 assert!(limits.memory_high.is_none());
549 assert!(limits.memory_swap_max.is_none());
550 assert!(limits.cpu_quota_us.is_none());
551 assert!(limits.cpu_weight.is_none());
552 assert!(limits.pids_max.is_none());
553 assert!(limits.io_limits.is_empty());
554 }
555
556 #[test]
557 fn test_cgroup_root_override_requires_absolute_path() {
558 assert_eq!(
559 Cgroup::root_path_from_override(None).unwrap(),
560 PathBuf::from(CGROUP_V2_ROOT)
561 );
562 assert_eq!(
563 Cgroup::root_path_from_override(Some(OsString::from(""))).unwrap(),
564 PathBuf::from(CGROUP_V2_ROOT)
565 );
566 assert_eq!(
567 Cgroup::root_path_from_override(Some(OsString::from("/sys/fs/cgroup/example.service")))
568 .unwrap(),
569 PathBuf::from("/sys/fs/cgroup/example.service")
570 );
571 assert!(Cgroup::root_path_from_override(Some(OsString::from("relative"))).is_err());
572 }
573
574 #[test]
575 fn test_cgroup_name_must_be_single_path_component() {
576 assert!(Cgroup::validate_cgroup_name("nucleus-abc123").is_ok());
577 assert!(Cgroup::validate_cgroup_name("").is_err());
578 assert!(Cgroup::validate_cgroup_name("../escape").is_err());
579 assert!(Cgroup::validate_cgroup_name("/sys/fs/cgroup/escape").is_err());
580 assert!(Cgroup::validate_cgroup_name("parent/child").is_err());
581 }
582
583 #[test]
584 fn test_cgroup_root_validation_rejects_regular_filesystem() {
585 let temp = tempfile::tempdir().unwrap();
586
587 assert!(Cgroup::validate_root_path(temp.path()).is_err());
588 }
589
590 #[test]
591 fn test_create_rejects_regular_filesystem_root_before_child_creation() {
592 let temp = tempfile::tempdir().unwrap();
593 let child = temp.path().join("nucleus-bypass");
594
595 assert!(Cgroup::create_in_root("nucleus-bypass", temp.path()).is_err());
596 assert!(
597 !child.exists(),
598 "regular filesystem root must be rejected before creating a fake cgroup"
599 );
600 }
601
602 #[test]
603 fn test_cgroup_root_env_rejects_regular_filesystem() {
604 let _guard = CGROUP_ENV_LOCK.lock().unwrap();
605 let previous = std::env::var_os(NUCLEUS_CGROUP_ROOT_ENV);
606 let temp = tempfile::tempdir().unwrap();
607 let child = temp.path().join("nucleus-bypass");
608
609 std::env::set_var(NUCLEUS_CGROUP_ROOT_ENV, temp.path().as_os_str());
610 let result = Cgroup::create("nucleus-bypass");
611 match previous {
612 Some(value) => std::env::set_var(NUCLEUS_CGROUP_ROOT_ENV, value),
613 None => std::env::remove_var(NUCLEUS_CGROUP_ROOT_ENV),
614 }
615
616 assert!(result.is_err());
617 assert!(
618 !child.exists(),
619 "regular filesystem override must be rejected before creating a fake cgroup"
620 );
621 }
622
623 #[test]
624 fn test_write_value_rejects_preexisting_regular_file() {
625 let temp = tempfile::tempdir().unwrap();
626 let path = temp.path().join("fake-cgroup");
627 fs::create_dir(&path).unwrap();
628 let control_file = path.join("memory.max");
629 fs::write(&control_file, "old").unwrap();
630
631 let cgroup = Cgroup {
632 path,
633 state: CgroupState::Removed,
634 };
635 assert!(cgroup.write_value("memory.max", "123").is_err());
636 assert_eq!(fs::read_to_string(control_file).unwrap(), "old");
637 }
638
639 #[test]
640 fn test_write_value_rejects_symlink_control_file() {
641 let temp = tempfile::tempdir().unwrap();
642 let path = temp.path().join("fake-cgroup");
643 let target = temp.path().join("target");
644 fs::create_dir(&path).unwrap();
645 fs::write(&target, "old").unwrap();
646 symlink(&target, path.join("memory.max")).unwrap();
647
648 let cgroup = Cgroup {
649 path,
650 state: CgroupState::Removed,
651 };
652 assert!(cgroup.write_value("memory.max", "123").is_err());
653 assert_eq!(fs::read_to_string(target).unwrap(), "old");
654 }
655
656 #[test]
660 fn test_cleanup_sets_removed_only_after_success() {
661 let source = include_str!("cgroup.rs");
665 let fn_start = source.find("pub fn cleanup").unwrap();
666 let after = &source[fn_start..];
667 let open = after.find('{').unwrap();
668 let mut depth = 0u32;
669 let mut fn_end = open;
670 for (i, ch) in after[open..].char_indices() {
671 match ch {
672 '{' => depth += 1,
673 '}' => {
674 depth -= 1;
675 if depth == 0 {
676 fn_end = open + i + 1;
677 break;
678 }
679 }
680 _ => {}
681 }
682 }
683 let cleanup_body = &after[..fn_end];
684 let removed_pos = cleanup_body
685 .find("Removed")
686 .expect("must reference Removed state");
687 let remove_dir_pos = cleanup_body
688 .find("remove_dir")
689 .expect("must call remove_dir");
690 assert!(
691 removed_pos > remove_dir_pos,
692 "CgroupState::Removed must be set AFTER remove_dir succeeds, not before"
693 );
694 }
695
696 #[test]
697 fn test_parse_cgroup_events_populated() {
698 assert!(Cgroup::parse_cgroup_events_populated("populated 1\nfrozen 0\n").unwrap());
699 assert!(!Cgroup::parse_cgroup_events_populated("frozen 0\npopulated 0\n").unwrap());
700 }
701
702 #[test]
703 fn test_set_limits_source_enables_memory_oom_group() {
704 let source = include_str!("cgroup.rs");
705 let fn_start = source.find("pub fn set_limits").unwrap();
706 let after = &source[fn_start..];
707 let open = after.find('{').unwrap();
708 let mut depth = 0u32;
709 let mut fn_end = open;
710 for (i, ch) in after[open..].char_indices() {
711 match ch {
712 '{' => depth += 1,
713 '}' => {
714 depth -= 1;
715 if depth == 0 {
716 fn_end = open + i + 1;
717 break;
718 }
719 }
720 _ => {}
721 }
722 }
723 let body = &after[..fn_end];
724 assert!(
725 body.contains("memory.oom.group"),
726 "set_limits must enable memory.oom.group when memory controls are configured"
727 );
728 }
729
730 #[test]
731 fn test_cleanup_source_kills_processes_before_remove_dir() {
732 let source = include_str!("cgroup.rs");
733 let fn_start = source.find("pub fn cleanup").unwrap();
734 let after = &source[fn_start..];
735 let open = after.find('{').unwrap();
736 let mut depth = 0u32;
737 let mut fn_end = open;
738 for (i, ch) in after[open..].char_indices() {
739 match ch {
740 '{' => depth += 1,
741 '}' => {
742 depth -= 1;
743 if depth == 0 {
744 fn_end = open + i + 1;
745 break;
746 }
747 }
748 _ => {}
749 }
750 }
751 let body = &after[..fn_end];
752 let freeze_pos = body.find("set_frozen(true)").unwrap();
753 let kill_pos = body.find("kill_all_processes").unwrap();
754 let remove_dir_pos = body.find("remove_dir").unwrap();
755 assert!(
756 freeze_pos < kill_pos && kill_pos < remove_dir_pos,
757 "cleanup must freeze and kill the cgroup before attempting remove_dir"
758 );
759 }
760}