patches and low-level development discussion
 help / color / mirror / code / Atom feed
From: Demi Marie Obenour <demiobenour@gmail.com>
To: Spectrum OS Development <devel@spectrum-os.org>
Cc: Demi Marie Obenour <demiobenour@gmail.com>, Alyssa Ross <hi@alyssa.is>
Subject: [PATCH v6 02/19] tools: Add control group manager
Date: Wed, 05 Aug 2026 21:16:09 -0400	[thread overview]
Message-ID: <20260805-cgroups-v6-2-086c0f00f55f@gmail.com> (raw)
In-Reply-To: <20260805-cgroups-v6-0-086c0f00f55f@gmail.com>

The cgroup-setup Rust program can create and purge cgroups.  It can also
wait for one to become empty, spawn a program in a cgroup, and more.  In
the future, it will also support cgroup-based resource control.  Locking
is used to ensure that concurrent invocations are safe.

Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com>
---
 .codespellrc                          |   2 +-
 host/rootfs/default.nix               |   6 +-
 pkgs/default.nix                      |   1 +
 tools/cgroup-setup/Cargo.lock         |  67 +++++++++
 tools/cgroup-setup/Cargo.lock.license |   2 +
 tools/cgroup-setup/Cargo.toml         |  10 ++
 tools/cgroup-setup/default.nix        |  22 +++
 tools/cgroup-setup/src/cgroup.rs      | 269 ++++++++++++++++++++++++++++++++++
 tools/cgroup-setup/src/main.rs        | 167 +++++++++++++++++++++
 9 files changed, 542 insertions(+), 4 deletions(-)

diff --git a/.codespellrc b/.codespellrc
index d8023afc64ec44e98a88c5e397c8d5c681dde063..ae20da8309530759ac729689825a4afc683afa09 100644
--- a/.codespellrc
+++ b/.codespellrc
@@ -2,4 +2,4 @@
 # SPDX-License-Identifier: CC0-1.0
 
 [codespell]
-ignore-words-list = crate,passt,rouge,ser
+ignore-words-list = crate,passt,rouge,ser,WRONLY
diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix
index 6bfeefbe0a5f76c1538ccb40e5eb8f291f5d3592..ccf626e2ec0f4bf96573dc5edf058c9375bb65f6 100644
--- a/host/rootfs/default.nix
+++ b/host/rootfs/default.nix
@@ -8,7 +8,7 @@ import ../../lib/call-package.nix (
 }:
 pkgsMusl.callPackage (
 
-{ spectrum-host-tools, spectrum-router
+{ spectrum-host-tools, spectrum-router, spectrum-cgroup-setup
 , lib, stdenvNoCC, nixos, runCommand, writeClosure, erofs-utils, s6-rc
 , btrfs-progs, bubblewrap, busybox, cloud-hypervisor, cosmic-files
 , crosvm, cryptsetup, dejavu_fonts, dbus, execline, foot, fuse3
@@ -27,8 +27,8 @@ let
   packages = [
     btrfs-progs bubblewrap cloud-hypervisor cosmic-files crosvm cryptsetup dbus
     execline fuse3 inotify-tools iproute2 jq kmod mdevd mount-flatpak s6
-    s6-linux-init s6-rc shadow socat spectrum-host-tools spectrum-router
-    virtiofsd xdg-desktop-portal-spectrum-host
+    s6-linux-init s6-rc shadow socat spectrum-cgroup-setup spectrum-host-tools
+    spectrum-router virtiofsd xdg-desktop-portal-spectrum-host
 
     (foot.override { allowPgo = false; })
 
diff --git a/pkgs/default.nix b/pkgs/default.nix
index 44f7b5ff78cb6b9e755292a6a417d0b627ed3fb0..0a13393164ad5d7f752e630763f3f97166479af5 100644
--- a/pkgs/default.nix
+++ b/pkgs/default.nix
@@ -51,6 +51,7 @@ let
       driverSupport = true;
     };
     spectrum-router = self.callSpectrumPackage ../tools/router {};
+    spectrum-cgroup-setup = self.callSpectrumPackage ../tools/cgroup-setup {};
     xdg-desktop-portal-spectrum-host =
       self.callSpectrumPackage ../tools/xdg-desktop-portal-spectrum-host {};
 
diff --git a/tools/cgroup-setup/Cargo.lock b/tools/cgroup-setup/Cargo.lock
new file mode 100644
index 0000000000000000000000000000000000000000..fe967b3aa02c296c87b6b36ac59253dbe0a32de9
--- /dev/null
+++ b/tools/cgroup-setup/Cargo.lock
@@ -0,0 +1,67 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "cgroup-setup"
+version = "0.0.0"
+dependencies = [
+ "libc",
+ "rustix",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
diff --git a/tools/cgroup-setup/Cargo.lock.license b/tools/cgroup-setup/Cargo.lock.license
new file mode 100644
index 0000000000000000000000000000000000000000..f80078163f4428881b86cb6fef0c90c0c1ebaa91
--- /dev/null
+++ b/tools/cgroup-setup/Cargo.lock.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2026 Demi Marie Obenour <demiobenour@gmail.com>
+SPDX-License-Identifier: CC0-1.0
diff --git a/tools/cgroup-setup/Cargo.toml b/tools/cgroup-setup/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..e44ef64119aefc23a3119fe0ce01566f5d7ec98c
--- /dev/null
+++ b/tools/cgroup-setup/Cargo.toml
@@ -0,0 +1,10 @@
+# SPDX-FileCopyrightText: 2026 Demi Marie Obenour <demiobenour@gmail.com>
+# SPDX-License-Identifier: CC0-1.0
+
+[package]
+name = "cgroup-setup"
+edition = "2024"
+
+[dependencies]
+libc = "0.2.177"
+rustix = { version = "1.1.2", features = ["fs"] }
diff --git a/tools/cgroup-setup/default.nix b/tools/cgroup-setup/default.nix
new file mode 100644
index 0000000000000000000000000000000000000000..fe3a8bcd4e6118f3cbf49c2fc0e3cda01570c484
--- /dev/null
+++ b/tools/cgroup-setup/default.nix
@@ -0,0 +1,22 @@
+# SPDX-FileCopyrightText: 2024 Alyssa Ross <hi@alyssa.is>
+# SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
+# SPDX-License-Identifier: MIT
+
+import ../../lib/call-package.nix (
+{ src, lib, rustPlatform }:
+
+rustPlatform.buildRustPackage {
+  name = "spectrum-cgroup-setup";
+
+  src = lib.fileset.toSource {
+    root = ../..;
+    fileset = lib.fileset.intersection src ./.;
+  };
+  sourceRoot = "source/tools/cgroup-setup";
+
+  cargoLock.lockFile = ./Cargo.lock;
+
+  postInstall = ''
+    ln -s -- cgroup-setup "$out/bin/cgroup-purge"
+  '';
+}) (_: {})
diff --git a/tools/cgroup-setup/src/cgroup.rs b/tools/cgroup-setup/src/cgroup.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c63d7e5a4aa73429578704401c58bbafc79e7c3f
--- /dev/null
+++ b/tools/cgroup-setup/src/cgroup.rs
@@ -0,0 +1,269 @@
+// SPDX-FileCopyrightText: 2026 Demi Marie Obenour <demiobenour@gmail.com>
+// SPDX-License-Identifier: EUPL-1.2+
+
+use std::ffi::OsStr;
+use std::fs::File;
+use std::io::{Read as _, Seek as _, Write as _};
+use std::os::unix::prelude::*;
+
+use std::path::{Component, Path, PathBuf};
+
+use rustix::fs::{AtFlags, CWD, Dir, FlockOperation};
+use rustix::path;
+use rustix::{
+    fs::{Mode, OFlags, ResolveFlags},
+    io::Errno,
+};
+
+pub enum OpenFlags {
+    Read,
+    Write,
+    Directory,
+}
+
+#[derive(Debug)]
+pub(crate) struct Cgroup {
+    fd: Vec<OwnedFd>,
+}
+
+impl AsFd for Cgroup {
+    fn as_fd(&self) -> BorrowedFd<'_> {
+        self.fd.last().unwrap().as_fd()
+    }
+}
+
+fn assert_single_component(component: &Path) {
+    match component.as_os_str().as_bytes() {
+        b"" | b"." | b".." => panic!("bad component"),
+        c if c.contains(&b'\0') => panic!("NUL in component"),
+        c if c.contains(&b'/') => panic!("/ in component"),
+        _ => {}
+    }
+}
+
+// Wrapper around openat2() with better defaults.
+pub fn openat2_simple(
+    fd: impl AsFd,
+    path: impl path::Arg,
+    flags: OpenFlags,
+) -> Result<OwnedFd, Errno> {
+    rustix::fs::openat2(
+        fd.as_fd(),
+        path,
+        OFlags::CLOEXEC
+            | match flags {
+                OpenFlags::Read => OFlags::RDONLY | OFlags::NOCTTY,
+                OpenFlags::Write => OFlags::WRONLY | OFlags::NOCTTY,
+                OpenFlags::Directory => OFlags::RDONLY | OFlags::DIRECTORY,
+            },
+        Mode::empty(),
+        ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_XDEV,
+    )
+}
+
+pub const DEFAULT_LEAF: &str = "$inner.service";
+
+pub fn check_path(path: &Path) -> Result<(), String> {
+    let bytes = path.as_os_str().as_bytes();
+    // Path::components() skips ., so use string manipulation instead.
+    for component in bytes[path.is_absolute() as usize..].split(|&b| b == b'/') {
+        if matches!(component, b"" | b"." | b"..") {
+            return Err(format!("cgroup path {path:?} isn't canonical"));
+        }
+    }
+    Ok(())
+}
+
+// Remove all subdirectories of the given directory recursively,
+// but not the directory itself.  The directory file descriptor
+// is closed.
+//
+// This isn't the most efficient possible algorithm, but
+// simplicity is more important than performance in this
+// case.  Also, it keeps open more file descriptors than
+// strictly necessary, but Spectrum runs with a very high
+// limit for the number of open file descriptors, and it
+// uses shallow control group hierarchies.
+//
+// This uses a recursive algorith, but so does std::fs::remove_dir_all().
+// Trying to be more robust than the standard library is not worthwhile.
+// In particular, the standard library function must be safe on systems
+// where untrusted users (or even network endpoints!) can create deeply
+// nested directory trees, whereas in Spectrum cgroups are only writeable
+// by root.
+fn remove_recursively(mut dirfd: Dir, remaining_depth: usize) -> Result<(), Errno> {
+    if remaining_depth < 1 {
+        panic!("control groups too deeply nested");
+    }
+    while let Some(element) = dirfd.next() {
+        let parent_fd = dirfd.fd().unwrap();
+        let element = element.expect("Iterating through a cgroup directory failed?");
+        let path = element.file_name();
+        if element.file_type() != rustix::fs::FileType::Directory || path == c"." || path == c".." {
+            continue;
+        }
+        let fd = openat2_simple(parent_fd, path, OpenFlags::Directory)?;
+        remove_recursively(Dir::new(fd).unwrap(), remaining_depth - 1)?;
+        match rustix::fs::unlinkat(parent_fd, path, AtFlags::REMOVEDIR) {
+            Err(Errno::NOTEMPTY | Errno::BUSY | Errno::NOENT) | Ok(()) => {}
+            bad => return bad,
+        }
+    }
+    Ok(())
+}
+
+// If the path is absolute, make it relative.
+// Otherwise, read the current cgroup from /proc/thread-self/cgroup
+// and prepend it to the path.
+fn prepend_current_cgroup_if_needed(path: &Path) -> PathBuf {
+    if let Ok(suffix) = path.strip_prefix("/") {
+        suffix.to_owned()
+    } else {
+        // /proc/thread-self is the same as /proc/self, except for the current
+        // thread instead of the initial thread.  In this case, the two are
+        // identical, but using /proc/thread-self is better practice as it is
+        // correct in more cases.  Reading /proc/thread-self/cgroup should
+        // never fail unless the system is seriously broken.
+        let current_cgroup = std::fs::read("/proc/thread-self/cgroup")
+            .expect("cannot read /proc/thread-self/cgroup");
+        // Using this on a system without cgroups v2 mounted is user error
+        // and not supported.
+        let current_cgroup = current_cgroup
+            .strip_prefix(b"0::/")
+            .and_then(|e| e.strip_suffix(b"\n"))
+            .expect("you don't have cgroups v2 mounted");
+        let mut current_cgroup = PathBuf::from(OsStr::from_bytes(current_cgroup));
+        // Strip the implied $inner.service suffix.
+        // This is used to satisfy the "no internal processes" rule.
+        if current_cgroup.ends_with(Path::new(DEFAULT_LEAF)) {
+            assert!(current_cgroup.pop());
+        }
+        current_cgroup.push(path);
+        current_cgroup
+    }
+}
+
+pub(crate) fn write_value(fd: &dyn AsFd, name: &Path, value: &[u8]) -> Result<(), String> {
+    let fd = openat2_simple(fd, name, OpenFlags::Write)
+        .map_err(|e| format!("Cannot open {name:?}: {e}"))?;
+    File::from(fd).write_all(value).map_err(|e| {
+        format!(
+            "Cannot write {:?} to {name:?}: {e}",
+            OsStr::from_bytes(value)
+        )
+    })
+}
+
+impl Cgroup {
+    pub fn new(path: &Path) -> Result<Self, String> {
+        let cgroup_root = rustix::fs::openat2(
+            CWD,
+            Path::new("/sys/fs/cgroup"),
+            OFlags::CLOEXEC | OFlags::DIRECTORY | OFlags::RDONLY,
+            Mode::empty(),
+            ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS,
+        )
+        .map_err(|e| format!("Cannot open /sys/fs/cgroup: {e}"))?;
+        let mut cgroup = Self {
+            fd: vec![(cgroup_root)],
+        };
+
+        let path = prepend_current_cgroup_if_needed(path);
+        for component in path.components() {
+            let Component::Normal(component) = component else {
+                unreachable!()
+            };
+            let sub_fd = openat2_simple(&cgroup, component, OpenFlags::Directory)
+                .map_err(|e| format!("Cannot open sub-cgroup {component:?}: {e}"))?;
+            // Take a shared lock on the cgroup.
+            rustix::fs::flock(&sub_fd, FlockOperation::LockShared)
+                .map_err(|e| format!("Cannot lock sub-cgroup {component:?}: {e}"))?;
+            cgroup.fd.push(sub_fd);
+        }
+        Ok(cgroup)
+    }
+
+    pub fn wait_for_empty(fd: &dyn AsFd) -> std::io::Result<()> {
+        let wait_file = openat2_simple(fd, c"cgroup.events", OpenFlags::Read)?;
+        let mut wait_fd = File::from(wait_file);
+        let mut v = vec![];
+        loop {
+            v.clear();
+            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");
+            // Check that the cgroup isn't already empty.  If it was,
+            // the kernel would not send an event and poll() would wait
+            // forever.
+            if v.split(|&c| c == b'\n').any(|line| line == b"populated 0") {
+                break;
+            }
+            let mut fds = libc::pollfd {
+                fd: wait_fd.as_raw_fd(),
+                events: libc::POLLPRI | libc::POLLERR,
+                revents: 0,
+            };
+            // SAFETY: FFI call, valid arguments, fds contains 1 element
+            if unsafe { libc::poll(&raw mut fds, 1, -1) } != 1 {
+                panic!("poll failed");
+            }
+        }
+        drop(wait_fd);
+        Ok(())
+    }
+
+    pub fn purge_child(&mut self, path: &Path) -> Result<(), String> {
+        assert_single_component(path);
+        // See if we can just delete the child directly.
+        match rustix::fs::unlinkat(&self, Path::new(path), AtFlags::REMOVEDIR) {
+            // If the cgroup was successfully deleted, or if it
+            // has already been deleted, we are done.
+            Ok(()) | Err(Errno::NOENT) => return Ok(()),
+            // If this cgroup is in use, keep going.
+            Err(Errno::BUSY) => {}
+            Err(e) => return Err(format!("Cannot purge {path:?}: {e}")),
+        }
+
+        let sub_fd = match openat2_simple(&self, path, OpenFlags::Directory) {
+            Ok(sub_fd) => sub_fd,
+            Err(Errno::NOENT) => return Ok(()),
+            Err(e) => {
+                return Err(format!("Cannot open sub-cgroup {path:?}: {e}",));
+            }
+        };
+
+        // Take an exclusive lock on the cgroup that is about to be
+        // removed.  This avoids concurrent executions of this program
+        // operating on deleted sub-cgroups.
+        rustix::fs::flock(&sub_fd, FlockOperation::LockExclusive)
+            .map_err(|e| format!("Cannot lock sub-cgroup: {e}"))?;
+
+        // Kill all processes in the child cgroup.
+        write_value(&sub_fd, Path::new("cgroup.kill"), b"1")?;
+
+        // Wait for the child cgroup to become empty.
+        Self::wait_for_empty(&sub_fd)
+            .map_err(|e| format!("Cannot wait for cgroup to become empty: {e}"))
+            .inspect_err(|_| {
+                self.fd.pop().unwrap();
+            })?;
+
+        // Remove the child cgroup and its contents recursively.
+        remove_recursively(Dir::new(sub_fd).unwrap(), 1000)
+            .map_err(|e| format!("Cannot remove: {e}"))?;
+
+        // Delete the cgroup.  If it's been re-created in the meantime
+        // and is currently in use, this is not an error.  Another
+        // process deleting the cgroup is also not an error.  Both of
+        // these can happen because of the time period between
+        // remove_child_directories() closing the file descriptor
+        // (releasing its lock) and the above call to flock().
+        match rustix::fs::unlinkat(&self, path, AtFlags::REMOVEDIR) {
+            Ok(()) | Err(Errno::BUSY) | Err(Errno::NOENT) => Ok(()),
+            Err(e) => Err(format!("Cannot delete: {e}")),
+        }
+    }
+}
diff --git a/tools/cgroup-setup/src/main.rs b/tools/cgroup-setup/src/main.rs
new file mode 100644
index 0000000000000000000000000000000000000000..58ca7295bb348a2c92640a68c60b285b2d7a1494
--- /dev/null
+++ b/tools/cgroup-setup/src/main.rs
@@ -0,0 +1,167 @@
+// SPDX-FileCopyrightText: 2026 Demi Marie Obenour <demiobenour@gmail.com>
+// SPDX-License-Identifier: EUPL-1.2+
+
+mod cgroup;
+
+use cgroup::{Cgroup, OpenFlags, openat2_simple, write_value};
+use rustix::{
+    fs::{FlockOperation, Mode, XattrFlags},
+    io::Errno,
+};
+use std::{
+    env::ArgsOs,
+    fs::File,
+    io::Read as _,
+    os::unix::prelude::*,
+    path::{Path, PathBuf},
+};
+
+// Check that the path is canonical,
+// then split it into basename and filename.
+fn split_path(path: &Path) -> Result<(&Path, &Path), String> {
+    cgroup::check_path(path)?;
+    Ok((path.parent().unwrap(), Path::new(path.file_name().unwrap())))
+}
+
+fn enable_subtree_control(fd: &dyn AsFd) -> Result<(), String> {
+    let mut buf = Vec::new();
+    File::from(
+        openat2_simple(fd, c"cgroup.controllers", OpenFlags::Read)
+            .map_err(|e| format!("Cannot open cgroup.controllers: {e}"))?,
+    )
+    .read_to_end(&mut buf)
+    .map_err(|e| format!("Cannot read cgroup.controllers: {e}"))?;
+    let mut subtree = vec![];
+    for controller in buf.split(|&b| b == b' ') {
+        if !subtree.is_empty() {
+            subtree.push(b' ');
+        }
+        subtree.push(b'+');
+        subtree.extend_from_slice(controller);
+    }
+    if !subtree.is_empty() {
+        write_value(&fd, Path::new("cgroup.subtree_control"), &subtree)?;
+    }
+    Ok(())
+}
+
+fn cgroup_setup(args: ArgsOs) -> Result<(), String> {
+    let mut leaf = false;
+    let mut systemd_delegate = false;
+    let mut wait = true;
+    let mut args = args.peekable();
+    while let Some(arg) = args.peek() {
+        if !arg.as_bytes().starts_with(b"-") {
+            break;
+        }
+        let arg = args.next().unwrap();
+        let Some(arg_) = arg.as_bytes().strip_prefix(b"--") else {
+            return Err("takes no short options".to_owned());
+        };
+        match arg_ {
+            b"" => break,
+            b"leaf" => leaf = true,
+            b"no-wait" => wait = false,
+            b"systemd-delegate" => systemd_delegate = true,
+            _ => return Err(format!("unknown long option {arg:?}")),
+        }
+    }
+    let Some(cgroup_path) = args.next().map(PathBuf::from) else {
+        return Err("have no positional arguments, expected at least 1".to_owned());
+    };
+
+    let (parent_cgroup_path, child_cgroup_path) = split_path(&cgroup_path)?;
+    let cgroup = Cgroup::new(parent_cgroup_path)?;
+    match rustix::fs::mkdirat(&cgroup, child_cgroup_path, Mode::from_raw_mode(0o755)) {
+        Ok(()) | Err(Errno::EXIST) => {}
+        Err(e) => {
+            return Err(format!(
+                "Cannot make child cgroup {child_cgroup_path:?}: {e}"
+            ));
+        }
+    }
+    let child = openat2_simple(&cgroup, child_cgroup_path, OpenFlags::Directory)
+        .map_err(|e| format!("Cannot make child cgroup: {e}"))?;
+    // While waiting, hold an exclusive lock on the child.
+    // This avoids two processes both waiting for the same cgroup to become
+    // empty, then spawning processes in the same cgroup.
+    rustix::fs::flock(&child, FlockOperation::LockExclusive)
+        .map_err(|e| format!("Cannot take an exclusive lock on child cgroup: {e}"))?;
+    if wait {
+        Cgroup::wait_for_empty(&child)
+            .map_err(|e| format!("Cannot wait for {parent_cgroup_path:?} to be empty: {e}"))?;
+    }
+    let pid = std::process::id().to_string();
+    if leaf {
+        if args.len() != 0 {
+            // If we aren't delegating any cgroups, don't create a sub-cgroup.
+            write_value(&child, Path::new("cgroup.procs"), pid.as_bytes())
+                .map_err(|e| format!("Cannot move process to child cgroup: {e}"))?;
+        }
+    } else {
+        // If the child process will need to manage cgroups itself, it will need
+        // to set up a sub-cgroup due to the "no internal processes" rule.  It's
+        // simplest to just do it automatically.  If the cgroup already exists,
+        // that isn't an error.
+        match rustix::fs::mkdirat(&child, cgroup::DEFAULT_LEAF, Mode::from_raw_mode(0o755)) {
+            Ok(()) | Err(Errno::EXIST) => {}
+            Err(e) => return Err(format!("Cannot make child cgroup: {e}")),
+        }
+        if args.len() != 0 {
+            let child_proc_path = Path::new(cgroup::DEFAULT_LEAF).join(Path::new("cgroup.procs"));
+            write_value(&child, &child_proc_path, pid.as_bytes())
+                .map_err(|e| format!("Cannot move process to child cgroup: {e}"))?;
+        }
+        if systemd_delegate {
+            // systemd-aware programs expect to have user.delegate=1
+            // and to set cgroup.subtree_control themselves
+            rustix::fs::fsetxattr(&child, c"user.delegate", b"1", XattrFlags::empty()).map_err(
+                |e| format!("Cannot enable cgroup delegation in {parent_cgroup_path:?}: {e}"),
+            )?
+        } else {
+            // Spectrum's programs do not check for user.delegate=1
+            // and expect the caller to set cgroup.subtree_control.
+            enable_subtree_control(&child)?;
+        }
+    }
+    let Some(program_name) = args.next() else {
+        return Ok(());
+    };
+    let e = std::process::Command::new(&program_name).args(args).exec();
+    Err(format!("Cannot spawn child {program_name:?}: {e}",))
+}
+
+fn cgroup_purge(mut args: ArgsOs) -> Result<(), String> {
+    if args.len() != 1 {
+        return Err("usage: cgroup-purge CGROUP_TO_PURGE".to_owned());
+    }
+    let arg = args.next().unwrap();
+    let (parent, child) = split_path(Path::new(&arg))?;
+    Cgroup::new(parent)?.purge_child(child)
+}
+
+fn run(prog_name: &Path, args: ArgsOs) -> Result<(), String> {
+    match prog_name.file_name().map(|f| f.as_bytes()) {
+        Some(b"cgroup-setup") => cgroup_setup(args),
+        Some(b"cgroup-purge") => cgroup_purge(args),
+        _ => Err(format!(
+            "must be invoked as \"cgroup-setup\" or \
+                 \"cgroup-purge\", got {prog_name:?}",
+        )),
+    }
+}
+
+fn main() {
+    let mut args = std::env::args_os();
+    let Some(prog_name) = args.next() else {
+        eprintln!("No command line arguments (argv[0] is NULL)");
+        std::process::exit(1);
+    };
+    match run(Path::new(&prog_name), args) {
+        Ok(()) => {}
+        Err(e) => {
+            eprintln!("{prog_name:?}: {}", e);
+            std::process::exit(1);
+        }
+    }
+}

-- 
2.55.0


  parent reply	other threads:[~2026-08-06  1:19 UTC|newest]

Thread overview: 132+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-20 14:23 [PATCH] Set up control groups for most services Demi Marie Obenour
2026-06-20 17:27 ` [PATCH v2] " Demi Marie Obenour
2026-06-24 12:13   ` Alyssa Ross
2026-06-24 12:36     ` Alyssa Ross
2026-06-25  2:03     ` Demi Marie Obenour
2026-06-25  3:03       ` Demi Marie Obenour
2026-06-25  9:55         ` Alyssa Ross
2026-06-25  9:49       ` Alyssa Ross
2026-07-11 20:12   ` [PATCH v3 00/22] Control group support Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 01/22] host/rootfs: Mount filesystems before s6-rc-init Demi Marie Obenour
2026-07-13  9:39       ` Alyssa Ross
2026-07-13 17:27         ` Demi Marie Obenour
2026-07-15 18:28           ` Alyssa Ross
2026-07-22  1:59       ` [PATCH v4 00/20] Control group support Demi Marie Obenour
2026-07-22  1:59         ` [PATCH v4 01/20] host/rootfs: Mount filesystems before s6-rc-init Demi Marie Obenour
2026-07-22  1:59         ` [PATCH v4 02/20] tools: Add control group manager Demi Marie Obenour
2026-07-22 16:01           ` Alyssa Ross
2026-07-23 23:07             ` Demi Marie Obenour
2026-07-27 12:10               ` Alyssa Ross
2026-07-30  0:40                 ` Demi Marie Obenour
2026-07-30 14:53                   ` Alyssa Ross
2026-07-22  1:59         ` [PATCH v4 03/20] Documentation: Mention control groups Demi Marie Obenour
2026-07-27 11:22           ` Alyssa Ross
2026-07-28 10:41           ` Valentin Gagarin
2026-07-22  1:59         ` [PATCH v4 04/20] Mount cgroup2 filesystem at /sys/fs/cgroup Demi Marie Obenour
2026-07-27 11:23           ` Alyssa Ross
2026-07-22  1:59         ` [PATCH v4 05/20] host/rootfs: Add helper program for per-VM services Demi Marie Obenour
2026-07-27 11:27           ` Alyssa Ross
2026-07-22  1:59         ` [PATCH v4 06/20] host/rootfs: Enable controllers in sub-cgroups Demi Marie Obenour
2026-07-27 12:11           ` Alyssa Ross
2026-07-28  2:19             ` Demi Marie Obenour
2026-07-29 14:13               ` Alyssa Ross
2026-07-22  1:59         ` [PATCH v4 07/20] host/rootfs: Add comments where cgroups are intentionally not used Demi Marie Obenour
2026-07-27 12:12           ` Alyssa Ross
2026-07-22  1:59         ` [PATCH v4 08/20] host/rootfs: serial-getty-generator: Use cgroups Demi Marie Obenour
2026-07-27 12:14           ` Alyssa Ross
2026-07-22  1:59         ` [PATCH v4 09/20] host/rootfs: Set up parent cgroup for all per-VM services Demi Marie Obenour
2026-07-27 12:16           ` Alyssa Ross
2026-07-28  3:01             ` Demi Marie Obenour
2026-07-29 14:29               ` Alyssa Ross
2026-07-29 20:20                 ` Demi Marie Obenour
2026-07-22  1:59         ` [PATCH v4 10/20] host/rootfs: Create per-VM cgroup for all of the VM's services Demi Marie Obenour
2026-07-22  1:59         ` [PATCH v4 11/20] host/rootfs: run-vmm: Create per-VM cgroup Demi Marie Obenour
2026-07-22  1:59         ` [PATCH v4 12/20] host/rootfs: run-appimage: Purge the " Demi Marie Obenour
2026-07-27 12:18           ` Alyssa Ross
2026-07-22  1:59         ` [PATCH v4 13/20] host/rootfs: run-flatpak: " Demi Marie Obenour
2026-07-27 12:19           ` Alyssa Ross
2026-07-22  1:59         ` [PATCH v4 14/20] host/rootfs: dbus: Run in cgroup Demi Marie Obenour
2026-07-22  1:59         ` [PATCH v4 15/20] host/rootfs: vhost-user-fs: " Demi Marie Obenour
2026-07-22  1:59         ` [PATCH v4 16/20] host/rootfs: vhost-user-gpu: " Demi Marie Obenour
2026-07-22  1:59         ` [PATCH v4 17/20] host/rootfs: xdg-desktop-portal-spectrum-host: " Demi Marie Obenour
2026-07-22  1:59         ` [PATCH v4 18/20] host/rootfs: systemd-udevd: " Demi Marie Obenour
2026-07-27 12:20           ` Alyssa Ross
2026-07-28  3:11             ` Demi Marie Obenour
2026-07-29 14:15               ` Alyssa Ross
2026-07-29 20:39                 ` Demi Marie Obenour
2026-07-30 14:55                   ` Alyssa Ross
2026-07-22  1:59         ` [PATCH v4 19/20] host/rootfs: weston: " Demi Marie Obenour
2026-07-27 12:23           ` Alyssa Ross
2026-07-28  3:14             ` Demi Marie Obenour
2026-07-22  1:59         ` [PATCH v4 20/20] host/rootfs: spectrum-router: " Demi Marie Obenour
2026-07-31 21:54         ` [PATCH v5 00/19] Control group support Demi Marie Obenour
2026-07-31 21:54           ` [PATCH v5 01/19] host/rootfs: Mount filesystems before s6-rc-init Demi Marie Obenour
2026-07-31 21:54           ` [PATCH v5 02/19] tools: Add control group manager Demi Marie Obenour
2026-08-03 12:47             ` Alyssa Ross
2026-08-05  1:36               ` Demi Marie Obenour
2026-08-05 16:39                 ` Alyssa Ross
2026-07-31 21:54           ` [PATCH v5 03/19] Documentation: Mention control groups Demi Marie Obenour
2026-08-03 13:22             ` Alyssa Ross
2026-07-31 21:54           ` [PATCH v5 04/19] Mount cgroup2 filesystem at /sys/fs/cgroup Demi Marie Obenour
2026-07-31 21:54           ` [PATCH v5 05/19] host/rootfs: Enable controllers in non-root cgroups Demi Marie Obenour
2026-07-31 21:54           ` [PATCH v5 06/19] host/rootfs: Add comments where cgroups are intentionally not used Demi Marie Obenour
2026-07-31 21:54           ` [PATCH v5 07/19] host/rootfs: serial-getty-generator: Use cgroups Demi Marie Obenour
2026-07-31 21:54           ` [PATCH v5 08/19] host/rootfs: systemd-udevd: Run in cgroup Demi Marie Obenour
2026-07-31 21:54           ` [PATCH v5 09/19] host/rootfs: weston: " Demi Marie Obenour
2026-07-31 21:54           ` [PATCH v5 10/19] host/rootfs: Set up parent cgroup for all per-VM services Demi Marie Obenour
2026-07-31 21:54           ` [PATCH v5 11/19] host/rootfs: Create per-VM cgroup for all of the VM's services Demi Marie Obenour
2026-07-31 21:54           ` [PATCH v5 12/19] host/rootfs: run-vmm: Create per-VM cgroup Demi Marie Obenour
2026-07-31 21:54           ` [PATCH v5 13/19] host/rootfs: run-appimage: Purge the " Demi Marie Obenour
2026-07-31 21:54           ` [PATCH v5 14/19] host/rootfs: run-flatpak: " Demi Marie Obenour
2026-07-31 21:54           ` [PATCH v5 15/19] host/rootfs: dbus: Run in cgroup Demi Marie Obenour
2026-07-31 21:54           ` [PATCH v5 16/19] host/rootfs: vhost-user-fs: " Demi Marie Obenour
2026-07-31 21:54           ` [PATCH v5 17/19] host/rootfs: vhost-user-gpu: " Demi Marie Obenour
2026-07-31 21:54           ` [PATCH v5 18/19] host/rootfs: xdg-desktop-portal-spectrum-host: " Demi Marie Obenour
2026-07-31 21:54           ` [PATCH v5 19/19] host/rootfs: spectrum-router: " Demi Marie Obenour
2026-08-06  1:16           ` [PATCH v6 00/19] Control group support Demi Marie Obenour
2026-08-06  1:16             ` [PATCH v6 01/19] host/rootfs: Mount filesystems before s6-rc-init Demi Marie Obenour
2026-08-06  1:16             ` Demi Marie Obenour [this message]
2026-08-06  6:58               ` [PATCH v6 02/19] tools: Add control group manager Demi Marie Obenour
2026-08-12 21:10               ` Alyssa Ross
2026-08-06  1:16             ` [PATCH v6 03/19] Documentation: Mention control groups Demi Marie Obenour
2026-08-06  1:16             ` [PATCH v6 04/19] Mount cgroup2 filesystem at /sys/fs/cgroup Demi Marie Obenour
2026-08-06  1:16             ` [PATCH v6 05/19] host/rootfs: Enable controllers in non-root cgroups Demi Marie Obenour
2026-08-06  1:16             ` [PATCH v6 06/19] host/rootfs: Add comments where cgroups are intentionally not used Demi Marie Obenour
2026-08-06  1:16             ` [PATCH v6 07/19] host/rootfs: serial-getty-generator: Use cgroups Demi Marie Obenour
2026-08-06  1:16             ` [PATCH v6 08/19] host/rootfs: systemd-udevd: Run in cgroup Demi Marie Obenour
2026-08-06  1:16             ` [PATCH v6 09/19] host/rootfs: weston: " Demi Marie Obenour
2026-08-06  1:16             ` [PATCH v6 10/19] host/rootfs: Set up parent cgroup for all per-VM services Demi Marie Obenour
2026-08-06  1:16             ` [PATCH v6 11/19] host/rootfs: Create per-VM cgroup for all of the VM's services Demi Marie Obenour
2026-08-06  1:16             ` [PATCH v6 12/19] host/rootfs: run-vmm: Create per-VM cgroup Demi Marie Obenour
2026-08-06  1:16             ` [PATCH v6 13/19] host/rootfs: run-appimage: Purge the " Demi Marie Obenour
2026-08-06  1:16             ` [PATCH v6 14/19] host/rootfs: run-flatpak: " Demi Marie Obenour
2026-08-06  1:16             ` [PATCH v6 15/19] host/rootfs: dbus: Run in cgroup Demi Marie Obenour
2026-08-06  1:16             ` [PATCH v6 16/19] host/rootfs: vhost-user-fs: " Demi Marie Obenour
2026-08-06  1:16             ` [PATCH v6 17/19] host/rootfs: vhost-user-gpu: " Demi Marie Obenour
2026-08-06  1:16             ` [PATCH v6 18/19] host/rootfs: xdg-desktop-portal-spectrum-host: " Demi Marie Obenour
2026-08-06  1:16             ` [PATCH v6 19/19] host/rootfs: spectrum-router: " Demi Marie Obenour
2026-08-12 21:06             ` [PATCH v6 00/19] Control group support Alyssa Ross
2026-07-11 20:12     ` [PATCH v3 02/22] scripts: Support symlinks in s6-rc-compile inputs Demi Marie Obenour
2026-07-13  9:42       ` Alyssa Ross
2026-07-13 14:19         ` Demi Marie Obenour
2026-07-15 18:30           ` Alyssa Ross
2026-07-11 20:12     ` [PATCH v3 03/22] tools: Add control group manager Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 04/22] Documentation: Mention control groups Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 05/22] Mount cgroup2 filesystem at /sys/fs/cgroup Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 06/22] host/rootfs: Add helper program for per-VM services Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 07/22] host/rootfs: Enable controllers in sub-cgroups Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 08/22] host/rootfs: Add comments where cgroups are intentionally not used Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 09/22] host/rootfs: serial-getty-generator: Use cgroups Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 10/22] host/rootfs: Set up parent cgroup for all per-VM services Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 11/22] host/rootfs: Create per-VM cgroup for all of the VM's services Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 12/22] host/rootfs: run-vmm: Create per-VM cgroup Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 13/22] host/rootfs: run-appimage: Purge the " Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 14/22] host/rootfs: run-flatpak: " Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 15/22] host/rootfs: dbus: Run in cgroup Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 16/22] host/rootfs: vhost-user-fs: " Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 17/22] host/rootfs: vhost-user-gpu: " Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 18/22] host/rootfs: xdg-desktop-portal-spectrum-host: " Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 19/22] host/rootfs: systemd-udevd: " Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 20/22] host/rootfs: weston: " Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 21/22] host/rootfs: spectrum-router: " Demi Marie Obenour
2026-07-11 20:12     ` [PATCH v3 22/22] host/rootfs: vm-import: Use elglob -w Demi Marie Obenour

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260805-cgroups-v6-2-086c0f00f55f@gmail.com \
    --to=demiobenour@gmail.com \
    --cc=devel@spectrum-os.org \
    --cc=hi@alyssa.is \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://spectrum-os.org/git/doc
	https://spectrum-os.org/git/mktuntap
	https://spectrum-os.org/git/spectrum
	https://spectrum-os.org/git/ucspi-vsock

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).