// SPDX-License-Identifier: EUPL-1.2+ // SPDX-FileCopyrightText: 2026 Demi Marie Obenour use std::fs::File; use std::io::{Read, Seek}; use std::os::fd::{AsFd, OwnedFd}; use std::os::fd::{AsRawFd, BorrowedFd}; use std::path::{Path, PathBuf}; use rustix::fs::AtFlags; use rustix::path::Arg; use rustix::{ fs::{Mode, OFlags, ResolveFlags}, io::Errno, }; pub(crate) struct LeafCgroup { path: PathBuf, fd: OwnedFd, } mod epoll { use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; #[expect(dead_code)] pub(super) struct Epoll(OwnedFd, OwnedFd); impl Epoll { #[expect(dead_code)] pub(super) fn new(other_fd: OwnedFd) -> Self { // SAFETY: FFI call with valid arguments let fd = unsafe { libc::epoll_create1(libc::EPOLL_CLOEXEC) }; if fd == -1 { panic!("epoll_create1 failed: {}", std::io::Error::last_os_error()); } let mut event = libc::epoll_event { events: (libc::EPOLLIN | libc::EPOLLPRI | libc::EPOLLRDHUP) as _, r#u64: 0, }; let res = unsafe { libc::epoll_ctl( fd.as_raw_fd(), libc::EPOLL_CTL_ADD, other_fd.as_raw_fd(), &raw mut event, ) }; assert_eq!(res, 0, "kernel out of memory?"); // SAFETY: epoll_create1 returns valid FD or -1 and we checked the error case Self(unsafe { OwnedFd::from_raw_fd(fd) }, other_fd) } } } enum Access { Read, Write, } impl LeafCgroup { fn open_subtree(&self, path: &std::path::Path, access: Access) -> Result { rustix::fs::openat2( self.fd.as_fd(), path, OFlags::NOATIME | OFlags::CLOEXEC | OFlags::NOFOLLOW | match access { Access::Write => OFlags::WRONLY, Access::Read => OFlags::RDONLY, }, Mode::empty(), ResolveFlags::NO_SYMLINKS | ResolveFlags::BENEATH | ResolveFlags::NO_XDEV, ) } pub fn kill_processes(&self) -> std::io::Result<()> { let kill_fd = self.open_subtree(std::path::Path::new("cgroup.kill"), Access::Write)?; let wait_file = self.open_subtree(std::path::Path::new("cgroup.events"), Access::Read)?; let poll_fd = wait_file.as_raw_fd(); let mut wait_fd = File::from(wait_file); assert_eq!(rustix::io::write(kill_fd.as_fd(), b"1")?, 1, "kernel bug"); let mut fds = libc::pollfd { fd: poll_fd, events: libc::POLLIN | libc::POLLPRI | libc::POLLRDHUP, revents: 0, }; // SAFETY: FFI call, valid arguments, fds contains 1 element let mut v = vec![]; 'a: loop { v.clear(); if unsafe { libc::poll(&raw mut fds, 1, -1) } != 1 { panic!("poll failed"); } wait_fd .seek(std::io::SeekFrom::Start(0)) .expect("Seek on control group file should succeed"); wait_fd .read_to_end(&mut v) .expect("reading from control group should work"); for substr in v.split(|&c| c == b'\n') { if substr == b"populated 0" { break 'a; } } } Ok(()) } } impl AsFd for LeafCgroup { fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> { self.fd.as_fd() } } impl LeafCgroup { pub(crate) fn open_cgroup_at(&self, p: &Path) -> Result { let fd = rustix::fs::openat2( self.fd.as_fd(), p, OFlags::NOATIME | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), ResolveFlags::NO_SYMLINKS | ResolveFlags::BENEATH | ResolveFlags::NO_XDEV, )?; Ok(Self { fd, path: self.path.join(p), }) } pub(crate) fn open_cgroup(fd: BorrowedFd, p: &Path) -> Result { let fd = rustix::fs::openat2( fd, p, OFlags::NOATIME | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), //ResolveFlags::NO_SYMLINKS | ResolveFlags::BENEATH | ResolveFlags::NO_XDEV, ResolveFlags::NO_SYMLINKS, )?; Ok(Self { fd, path: p.to_owned(), }) } pub(crate) fn make_child(&self, p: &Path) -> Result<(), Errno> { rustix::fs::mkdirat( self.fd.as_fd(), p, Mode::RUSR | Mode::WUSR | Mode::XUSR | Mode::RGRP | Mode::XGRP | Mode::ROTH | Mode::XOTH, ) } pub(crate) fn delete_child(&self, path: &Path) -> Result<(), Errno> { remove_all(100, self.as_fd(), &path.as_cow_c_str().unwrap()) } } fn remove_recursively(fd: OwnedFd, remaining_depth: usize) -> Result<(), Errno> { if remaining_depth < 1 { panic!("control groups too deeply nested"); } let mut d = rustix::fs::Dir::new(fd).expect("cannot start iterating"); while let Some(element) = d.next() { let element = element.expect("Iterating through a cgroup directory failed?"); if element.file_type() != rustix::fs::FileType::Directory { continue; } let remaining_depth = remaining_depth - 1; let d: &rustix::fs::Dir = &d; let dirfd = d.fd().unwrap(); let path = element.file_name(); remove_all(remaining_depth, dirfd, path)?; } Ok(()) } fn remove_all( remaining_depth: usize, dirfd: BorrowedFd<'_>, path: &std::ffi::CStr, ) -> Result<(), Errno> { if path == c"." || path == c".." { return Ok(()); } if rustix::fs::unlinkat(dirfd, path, AtFlags::REMOVEDIR).is_ok() { return Ok(()); } let fd = rustix::fs::openat2( dirfd, path, OFlags::NOATIME | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), ResolveFlags::NO_SYMLINKS | ResolveFlags::BENEATH | ResolveFlags::NO_XDEV, )?; remove_recursively(fd, remaining_depth)?; rustix::fs::unlinkat(dirfd, path, AtFlags::REMOVEDIR)?; Ok(()) }