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 v7 02/19] tools: Add control group manager
Date: Fri, 21 Aug 2026 02:51:45 -0400	[thread overview]
Message-ID: <20260821-cgroups-v7-2-7f1870dedefc@gmail.com> (raw)
In-Reply-To: <20260821-cgroups-v7-0-7f1870dedefc@gmail.com>

This program has two modes:

1. Create a control group if it doesn't exist, optionally wait for other
   programs in it to exit, and exec another program in it.

2. Purge a control group: kill all programs in it, then delete it.

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      | 289 ++++++++++++++++++++++++++++++++++
 tools/cgroup-setup/src/main.rs        | 168 ++++++++++++++++++++
 9 files changed, 563 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..463d3fc87ee34ccfbf399a337e2f49d9031728ee
--- /dev/null
+++ b/tools/cgroup-setup/src/cgroup.rs
@@ -0,0 +1,289 @@
+// 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 algorithm, 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)?;
+        rustix::fs::unlinkat(parent_fd, path, AtFlags::REMOVEDIR)?;
+    }
+    Ok(())
+}
+
+fn assert_simple_path(current_cgroup: &Path) {
+    let current_cgroup = current_cgroup.as_os_str().as_bytes();
+    if !matches!(current_cgroup, b"" | b".") {
+        for component in current_cgroup.split(|&b| b == b'/') {
+            assert_single_component(Path::new(OsStr::from_bytes(component)));
+        }
+    }
+}
+
+// Convert the cgroup path to one relative to /sys/fs/cgroup.
+//
+// If the path starts with /, the leading / is removed and the result is returned
+// without further processing.  Otherwise, the current cgroup is read from
+// /proc/thread-self/cgroup.  If its last component is $inner.service, that is
+// removed.  Finally, the current cgroup is prepended to the provided cgroup path,
+// with a single / as separator.  The result of this operation is returned.
+fn prepend_current_cgroup_if_needed(path: &Path) -> Result<PathBuf, String> {
+    if let Ok(suffix) = path.strip_prefix("/") {
+        return Ok(suffix.to_owned());
+    }
+    assert_simple_path(path);
+    // /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"))
+        .ok_or_else(|| {
+            "/proc/thread-self/cgroup doesn't start with 0::/ or doesn't end with a newline.\n\
+            Either cgroups aren't in use at all, or you are using cgroups v1."
+                .to_owned()
+        })?;
+    let mut current_cgroup = PathBuf::from(OsStr::from_bytes(current_cgroup));
+    assert_simple_path(&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());
+    }
+    // "." refers to the current cgroup.
+    if path != Path::new(".") {
+        current_cgroup.push(path);
+    }
+    Ok(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}"))?;
+        // It's simpler to always have the root cgroup at the bottom of the stack,
+        // even though no lock needs to be taken on it.  Otherwise, one would need
+        // to special-case the cgroup root.  One could remove the first element if
+        // there is more than one element in the vector, but that's not worth it.
+        // cgroup-setup doesn't operate in an environment where FDs are a limited
+        // resource.
+        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");
+            }
+        }
+        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, 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.  Dir::new() doesn't expose a reference to its internal FD
+        // so it must be delayed until later.
+        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..c757a4ad37c812ef5ce5249dc1bc3104ec246eef
--- /dev/null
+++ b/tools/cgroup-setup/src/main.rs
@@ -0,0 +1,168 @@
+// 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},
+};
+
+fn enable_subtree_control(fd: &dyn AsFd) -> Result<(), String> {
+    rustix::fs::fsetxattr(fd, c"user.delegate", b"1", XattrFlags::empty())
+        .map_err(|e| format!("Cannot enable cgroup delegation: {e}"))?;
+    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![];
+    if buf.is_empty() {
+        return Ok(());
+    }
+    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 spawn_in_cgroup(
+    mut args: std::iter::Peekable<ArgsOs>,
+    cgroup: Option<&dyn AsFd>,
+) -> Result<(), String> {
+    let Some(program_name) = args.next() else {
+        return Ok(());
+    };
+    if let Some(cgroup) = cgroup {
+        let pid = std::process::id().to_string();
+        write_value(
+            cgroup,
+            Path::new("$inner.service/cgroup.procs"),
+            pid.as_bytes(),
+        )
+        .map_err(|e| format!("Cannot move process to child cgroup: {e}"))?;
+    }
+    let e = std::process::Command::new(&program_name).args(args).exec();
+    Err(format!("Cannot spawn child {program_name:?}: {e}"))
+}
+
+// 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 cgroup_setup(args: ArgsOs) -> Result<(), String> {
+    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(option) = arg.as_bytes().strip_prefix(b"--") else {
+            return Err("takes no short options".to_owned());
+        };
+        match option {
+            b"" => break,
+            b"no-wait" => wait = false,
+            _ => 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 create child cgroup {child_cgroup_path:?}: {e}"
+            ));
+        }
+    }
+
+    let child = openat2_simple(&cgroup, child_cgroup_path, OpenFlags::Directory)
+        .map_err(|e| format!("Cannot open 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}"))?;
+    }
+
+    // Spectrum's programs (such as this one) expect cgroup.subtree_control
+    // to be set by the program that created the cgroup.  systemd-aware
+    // programs, like systemd-udevd, expect user.delegate=1 to be set.
+    enable_subtree_control(&child)?;
+
+    // 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 create $inner.service cgroup: {e}")),
+    }
+
+    spawn_in_cgroup(args, Some(&child))
+}
+
+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-21  6:53 UTC|newest]

Thread overview: 173+ 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             ` [PATCH v6 02/19] tools: Add control group manager Demi Marie Obenour
2026-08-06  6:58               ` 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-08-21  6:51             ` [PATCH v7 " Demi Marie Obenour
2026-08-21  6:51               ` [PATCH v7 01/19] host/rootfs: Mount filesystems before s6-rc-init Demi Marie Obenour
2026-08-21  6:51               ` Demi Marie Obenour [this message]
2026-08-26 14:07                 ` [PATCH v7 02/19] tools: Add control group manager Alyssa Ross
2026-08-30  2:54                   ` Demi Marie Obenour
2026-08-21  6:51               ` [PATCH v7 03/19] Documentation: Mention control groups Demi Marie Obenour
2026-08-21  6:51               ` [PATCH v7 04/19] Mount cgroup2 filesystem at /sys/fs/cgroup Demi Marie Obenour
2026-08-21  6:51               ` [PATCH v7 05/19] host/rootfs: Enable controllers in non-root cgroups Demi Marie Obenour
2026-08-21  6:51               ` [PATCH v7 06/19] host/rootfs: Add comments where cgroups are intentionally not used Demi Marie Obenour
2026-08-21  6:51               ` [PATCH v7 07/19] host/rootfs: serial-getty-generator: Use cgroups Demi Marie Obenour
2026-08-21  6:51               ` [PATCH v7 08/19] host/rootfs: systemd-udevd: Run in cgroup Demi Marie Obenour
2026-08-21  6:51               ` [PATCH v7 09/19] host/rootfs: weston: " Demi Marie Obenour
2026-08-21  6:51               ` [PATCH v7 10/19] host/rootfs: Set up parent cgroup for all per-VM services Demi Marie Obenour
2026-08-21  6:51               ` [PATCH v7 11/19] host/rootfs: Create per-VM cgroup for all of the VM's services Demi Marie Obenour
2026-08-21  6:51               ` [PATCH v7 12/19] host/rootfs: run-vmm: Create per-VM cgroup Demi Marie Obenour
2026-08-21  6:51               ` [PATCH v7 13/19] host/rootfs: run-appimage: Purge the " Demi Marie Obenour
2026-08-21  6:51               ` [PATCH v7 14/19] host/rootfs: run-flatpak: " Demi Marie Obenour
2026-08-21  6:51               ` [PATCH v7 15/19] host/rootfs: dbus: Run in cgroup Demi Marie Obenour
2026-08-21  6:51               ` [PATCH v7 16/19] host/rootfs: vhost-user-fs: " Demi Marie Obenour
2026-08-21  6:52               ` [PATCH v7 17/19] host/rootfs: vhost-user-gpu: " Demi Marie Obenour
2026-08-21  6:52               ` [PATCH v7 18/19] host/rootfs: xdg-desktop-portal-spectrum-host: " Demi Marie Obenour
2026-08-21  6:52               ` [PATCH v7 19/19] host/rootfs: spectrum-router: " Demi Marie Obenour
2026-08-30  6:06               ` [PATCH v8 00/18] Control group support Demi Marie Obenour
2026-08-30  6:06                 ` [PATCH v8 01/18] host/rootfs: Mount filesystems before s6-rc-init Demi Marie Obenour
2026-08-30  6:06                 ` [PATCH v8 02/18] tools: Add control group manager Demi Marie Obenour
2026-08-30  6:06                 ` [PATCH v8 03/18] Documentation: Mention control groups Demi Marie Obenour
2026-08-30  6:06                 ` [PATCH v8 04/18] Mount cgroup2 filesystem at /sys/fs/cgroup Demi Marie Obenour
2026-08-30  6:06                 ` [PATCH v8 05/18] host/rootfs: Add comments where cgroups are intentionally not used Demi Marie Obenour
2026-08-30  6:06                 ` [PATCH v8 06/18] host/rootfs: serial-getty-generator: Use cgroups Demi Marie Obenour
2026-08-30  6:06                 ` [PATCH v8 07/18] host/rootfs: systemd-udevd: Run in cgroup Demi Marie Obenour
2026-08-30  6:06                 ` [PATCH v8 08/18] host/rootfs: weston: " Demi Marie Obenour
2026-08-30  6:06                 ` [PATCH v8 09/18] host/rootfs: Set up parent cgroup for all per-VM services Demi Marie Obenour
2026-08-30  6:06                 ` [PATCH v8 10/18] host/rootfs: Create per-VM cgroup for all of the VM's services Demi Marie Obenour
2026-08-30  6:06                 ` [PATCH v8 11/18] host/rootfs: run-vmm: Create per-VM cgroup Demi Marie Obenour
2026-08-30  6:06                 ` [PATCH v8 12/18] host/rootfs: run-appimage: Purge the " Demi Marie Obenour
2026-08-30  6:06                 ` [PATCH v8 13/18] host/rootfs: run-flatpak: " Demi Marie Obenour
2026-08-30  6:06                 ` [PATCH v8 14/18] host/rootfs: dbus: Run in cgroup Demi Marie Obenour
2026-08-30  6:06                 ` [PATCH v8 15/18] host/rootfs: vhost-user-fs: " Demi Marie Obenour
2026-08-30  6:06                 ` [PATCH v8 16/18] host/rootfs: vhost-user-gpu: " Demi Marie Obenour
2026-08-30  6:06                 ` [PATCH v8 17/18] host/rootfs: xdg-desktop-portal-spectrum-host: " Demi Marie Obenour
2026-08-30  6:06                 ` [PATCH v8 18/18] host/rootfs: spectrum-router: " Demi Marie Obenour
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=20260821-cgroups-v7-2-7f1870dedefc@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).