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 v3 03/22] tools: Add control group manager
Date: Sat, 11 Jul 2026 16:12:07 -0400 [thread overview]
Message-ID: <20260711-cgroups-v3-3-5cba61a20cba@gmail.com> (raw)
In-Reply-To: <20260711-cgroups-v3-0-5cba61a20cba@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.
This program can also act as an s6 finish script. When invoked as such,
it automatically purges the correct cgroup. It also tells s6 to not
restart the service if it dumped core. Core dumps are often due to
memory corruption, and automatically restarting a service that dumped
core makes memory corruption attacks easier.
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 | 11 +
tools/cgroup-setup/default.nix | 18 ++
tools/cgroup-setup/src/cgroup.rs | 203 +++++++++++++++++
tools/cgroup-setup/src/main.rs | 406 ++++++++++++++++++++++++++++++++++
9 files changed, 712 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 4dcc10b9933cdfaf87f87417db1de0daa57b73f5..f161c26e8b3cc3a46d3321d88a52c446e2faac96 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 4dbdfee8ba330f5ba5c9fedee2d1bb8a44af5722..22c0d782bf4b5467fe77d5a2d71fb8df3af3e756 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..aa108acd23886d8302eaf7babff90d1b08ae19fb
--- /dev/null
+++ b/tools/cgroup-setup/Cargo.lock.license
@@ -0,0 +1,2 @@
+SPDX-License-Identifier: EUPL-1.2+
+SPDX-FileCopyrightText: 2026 Demi Marie Obenour <demiobenour@gmail.com>
diff --git a/tools/cgroup-setup/Cargo.toml b/tools/cgroup-setup/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..7ed6d6a0ea3bbfc4064b9f39383d0788c4bd84e5
--- /dev/null
+++ b/tools/cgroup-setup/Cargo.toml
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: CC0-1.0
+# SPDX-FileCopyrightText: 2025 Alyssa Ross <hi@alyssa.is>
+# SPDX-FileCopyrightText: 2026 Demi Marie Obenour <demiobenour@gmail.com>
+
+[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..9e716b8f270828b733f39961e5ca37a290a872b4
--- /dev/null
+++ b/tools/cgroup-setup/default.nix
@@ -0,0 +1,18 @@
+# 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;
+}) (_: {})
diff --git a/tools/cgroup-setup/src/cgroup.rs b/tools/cgroup-setup/src/cgroup.rs
new file mode 100644
index 0000000000000000000000000000000000000000..eb9b892ab6f562cffa738abaf9c55dc18001012a
--- /dev/null
+++ b/tools/cgroup-setup/src/cgroup.rs
@@ -0,0 +1,203 @@
+// SPDX-License-Identifier: EUPL-1.2+
+// SPDX-FileCopyrightText: 2026 Demi Marie Obenour <demiobenour@gmail.com>
+
+use std::fs::File;
+use std::io::{Read as _, Seek as _, Write as _};
+use std::os::unix::prelude::*;
+
+use std::path::{Path, PathBuf};
+
+use rustix::fs::{AtFlags, XattrFlags};
+use rustix::path::Arg;
+use rustix::{
+ fs::{Mode, OFlags, ResolveFlags},
+ io::Errno,
+};
+
+pub(crate) struct LeafCgroup {
+ path: PathBuf,
+ fd: OwnedFd,
+}
+
+pub enum Access {
+ Read,
+ Write,
+}
+
+impl LeafCgroup {
+ pub fn enable_delegation(&self) -> Result<(), Errno> {
+ rustix::fs::fsetxattr(self.fd.as_fd(), c"user.delegate", b"1", XattrFlags::empty())
+ }
+
+ pub fn open_subtree(&self, path: &std::path::Path, access: Access) -> Result<OwnedFd, Errno> {
+ 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 wait_for_empty(&self, kill: bool) -> std::io::Result<()> {
+ let kill_fd = if kill {
+ Some(self.open_subtree(std::path::Path::new("cgroup.kill"), Access::Write)?)
+ } else {
+ None
+ };
+ 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);
+ if let Some(kill_fd) = kill_fd {
+ assert_eq!(rustix::io::write(kill_fd.as_fd(), b"1")?, 1, "kernel bug");
+ }
+ let mut fds = libc::pollfd {
+ fd: poll_fd,
+ events: libc::POLLPRI | libc::POLLERR,
+ revents: 0,
+ };
+ let mut v = vec![];
+ loop {
+ v.clear();
+ // SAFETY: FFI call, valid arguments, fds contains 1 element
+ 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");
+ if v.split(|&c| c == b'\n').any(|line| line == b"populated 0") {
+ break;
+ }
+ }
+ 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<Self, Errno> {
+ 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<Self, Errno> {
+ let fd = rustix::fs::openat2(
+ fd,
+ p,
+ OFlags::NOATIME
+ | OFlags::CLOEXEC
+ | OFlags::NOFOLLOW
+ | OFlags::RDONLY
+ | OFlags::DIRECTORY,
+ Mode::empty(),
+ ResolveFlags::NO_SYMLINKS,
+ )?;
+ Ok(Self {
+ fd,
+ path: p.to_owned(),
+ })
+ }
+ pub(crate) fn make_child(&self, p: &Path) -> Result<(), Errno> {
+ let r = rustix::fs::mkdirat(
+ self.fd.as_fd(),
+ p,
+ Mode::RUSR
+ | Mode::WUSR
+ | Mode::XUSR
+ | Mode::RGRP
+ | Mode::XGRP
+ | Mode::ROTH
+ | Mode::XOTH,
+ );
+ if r == Err(Errno::EXIST) { Ok(()) } else { r }
+ }
+
+ pub(crate) fn delete_child(&self, path: &Path) -> Result<(), Errno> {
+ remove_all(100, self.as_fd(), &path.as_cow_c_str().unwrap())
+ }
+
+ pub(crate) fn write_cgroup_value(&self, name: &str, value: &str) -> Result<(), String> {
+ let path = Path::new(name);
+ let fd = rustix::fs::openat2(
+ self.as_fd(),
+ path,
+ OFlags::NOATIME | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::WRONLY,
+ Mode::empty(),
+ ResolveFlags::NO_SYMLINKS | ResolveFlags::BENEATH | ResolveFlags::NO_XDEV,
+ )
+ .map_err(|e| format!("Cannot open {:?}: {}", path, e))?;
+ File::from(fd)
+ .write_all(value.as_bytes())
+ .map_err(|e| format!("Cannot write {:?} to {:?}: {}", value, path, e))
+ }
+}
+
+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(())
+}
diff --git a/tools/cgroup-setup/src/main.rs b/tools/cgroup-setup/src/main.rs
new file mode 100644
index 0000000000000000000000000000000000000000..5b2d2eb9831b0c635914a5e4dc90e79a262588a5
--- /dev/null
+++ b/tools/cgroup-setup/src/main.rs
@@ -0,0 +1,406 @@
+// SPDX-License-Identifier: EUPL-1.2+
+// SPDX-FileCopyrightText: 2026 Demi Marie Obenour <demiobenour@gmail.com>
+
+use std::{
+ ffi::{OsStr, OsString},
+ fs::File,
+ io::Read as _,
+ os::unix::prelude::*,
+ path::{Component, Path, PathBuf},
+};
+
+use rustix::{
+ fs::{FlockOperation, Mode, OFlags, ResolveFlags},
+ io::Errno,
+};
+
+use crate::cgroup::Access;
+
+mod cgroup;
+
+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 main_(&prog_name, args) {
+ Ok(()) => {}
+ Err(e) => {
+ eprintln!("{prog_name:?}: {}", e);
+ std::process::exit(1);
+ }
+ }
+}
+
+fn main_(prog_name: &OsStr, mut args: std::env::ArgsOs) -> Result<(), String> {
+ let mut purge = false;
+ match prog_name
+ .as_bytes()
+ .split(|&b| b == b'/')
+ .next_back()
+ .unwrap()
+ {
+ b"finish" => {
+ return s6_finish(&mut args);
+ }
+ b"cgroup-setup" => {}
+ e => {
+ return Err(format!(
+ "must be invoked as \"cgroup-setup\" or \"finish\", got {:?}",
+ e
+ ));
+ }
+ };
+ let mut leaf = false;
+ let mut cgroup_relative_path;
+ let mut delegate = false;
+ let mut init_subtree = false;
+ let mut child_name: Option<&'static OsStr> = None;
+ let mut wait = None;
+ loop {
+ cgroup_relative_path = args.next();
+ let Some(ref arg_) = cgroup_relative_path else {
+ break;
+ };
+ let arg_ = arg_.as_bytes();
+ if arg_ == b"--" {
+ cgroup_relative_path = args.next();
+ break;
+ }
+ if !arg_.starts_with(b"-") {
+ break;
+ }
+
+ if !arg_.starts_with(b"--") {
+ return Err("takes no short options".to_owned());
+ }
+
+ match &arg_[2..] {
+ b"purge" => purge = true,
+ b"leaf" => leaf = true,
+ b"delegate" => delegate = true,
+ b"init-subtree" => init_subtree = true,
+ b"wait" => wait = Some(true),
+ b"no-wait" => wait = Some(false),
+ b"child-name" if child_name.is_none() => match args.next() {
+ Some(arg) => child_name = Some(arg.leak()),
+ None => return Err("--child-name: missing argument".to_owned()),
+ },
+ b"child-name" => return Err("--child-name: cannot be used twice".to_owned()),
+ arg => match str::from_utf8(arg) {
+ Ok(e) => return Err(format!("unknown long option {e:?}")),
+ Err(_) => return Err("long option isn't UTF-8".to_owned()),
+ },
+ }
+ }
+
+ let default_child_name = OsStr::from_bytes(b"$inner.service");
+
+ let child_name = Path::new(child_name.unwrap_or(default_child_name));
+
+ let Some(cgroup_relative_path) = cgroup_relative_path else {
+ return Err("have no positional arguments, expected at least 1".to_owned());
+ };
+
+ // The kernel doesn't care, but displaying messages does.
+ let cgroup_path = String::try_from(cgroup_relative_path.into_vec())
+ .map_err(|e| format!("non-UTF-8 cgroup path not supported (error is {e})"))?;
+
+ if cgroup_path.is_empty() {
+ return Err("cgroup name is empty".to_owned());
+ }
+ if cgroup_path == ".." {
+ return Err("cgroup name is ..".to_owned());
+ }
+ // If we aren't asked to create a child process, don't wait for existing
+ // processes to die unless explicitly asked to. Waiting for a cgroup
+ // we are in to be empty is a guaranteed deadlock.
+ let may_wait = cgroup_path != "." && cgroup_path != "/";
+ let wait = match wait {
+ Some(false) => false,
+ None => may_wait && args.len() > 0,
+ Some(true) if !may_wait => {
+ let msg = "Cannot wait for the program's own cgroup or \
+ root cgroup to be empty";
+ return Err(msg.to_owned());
+ }
+ Some(true) => true,
+ };
+
+ let params = CgroupParams {
+ purge,
+ leaf,
+ delegate,
+ init_subtree,
+ wait,
+ };
+
+ let (full_path, cgroup_target, cgroup) = cgroup_parse(cgroup_path)?;
+ if params.purge {
+ purge_cgroup(&cgroup, &cgroup_target)?;
+ }
+ let child = match cgroup.open_cgroup_at(&cgroup_target) {
+ Ok(child_cgroup) => child_cgroup,
+ Err(Errno::NOENT) => {
+ if let Err(e) = cgroup.make_child(&cgroup_target) {
+ return Err(format!("Cannot create child cgroup {full_path:?}: {e}"));
+ }
+ cgroup
+ .open_cgroup_at(&cgroup_target)
+ .map_err(|e| format!("Cannot open child cgroup {full_path:?}: {e}"))?
+ }
+ Err(other) => {
+ return Err(format!("Cannot open child cgroup {full_path:?}: {other}"));
+ }
+ };
+ if params.wait {
+ child
+ .wait_for_empty(false)
+ .map_err(|e| format!("Cannot wait for {full_path:?} to be empty: {e}"))?;
+ }
+ let pid = std::process::id().to_string();
+ if params.leaf {
+ // If we aren't delegating any cgroups, don't create a sub-cgroup.
+ child
+ .write_cgroup_value("cgroup.procs", &pid)
+ .map_err(|e| format!("Cannot write to {full_path:?}/cgroup.procs: {e}"))?;
+ } else {
+ child.make_child(child_name).map_err(|e| {
+ format!(
+ "Cannot create child cgroup {}/{}: {e}",
+ full_path.display(),
+ child_name.display()
+ )
+ })?;
+
+ // 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.
+ let grandchild = child.open_cgroup_at(Path::new(child_name)).map_err(|e| {
+ format!(
+ "Cannot open child cgroup {}/{}: {e}",
+ full_path.display(),
+ child_name.display()
+ )
+ })?;
+ grandchild
+ .write_cgroup_value("cgroup.procs", &pid)
+ .map_err(|e| {
+ format!(
+ "Cannot write to {}/{}/cgroup.procs: {e}",
+ full_path.display(),
+ child_name.display()
+ )
+ })?;
+ }
+ if params.init_subtree {
+ enable_subtree_control(&cgroup)?;
+ }
+ if !params.leaf {
+ enable_subtree_control(&child)?;
+ }
+ if params.delegate {
+ child
+ .enable_delegation()
+ .map_err(|e| format!("Cannot enable cgroup delegation in {full_path:?}: {e}"))?;
+ }
+ 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 s6_finish(args: &mut std::env::ArgsOs) -> Result<(), String> {
+ if args.len() < 3 {
+ return Err(format!(
+ "s6 finish scripts take 3 arguments, got {}",
+ args.len()
+ ));
+ }
+ let status = parse_digit_string(&args.next().unwrap(), "exit status")?;
+ let signal = args.next().unwrap();
+ let signal = if status == 256 {
+ Some(parse_digit_string(&signal, "signal number")?)
+ } else {
+ None
+ };
+ let service = args
+ .next()
+ .unwrap()
+ .into_string()
+ .map_err(|e| format!("Service name {e:?} is not UTF-8"))?;
+ let (_full_path, cgroup_target, cgroup) = cgroup_parse(service)?;
+ let r = purge_cgroup(&cgroup, &cgroup_target);
+ if let Some(signal) = signal {
+ match signal as libc::c_int {
+ libc::SIGBUS
+ | libc::SIGFPE
+ | libc::SIGABRT
+ | libc::SIGTRAP
+ | libc::SIGSEGV
+ | libc::SIGILL => {
+ // Process *crashed*, indicating a *possible exploit attempt*.
+ // s6 should *not* restart it. This is distinct from a Rust panic,
+ // which is much less likely to indicate memory corruption.
+ if let Err(e) = r {
+ // do not panic on stderr write failure
+ eprintln!("Could not purge cgroup: {e}");
+ }
+ std::process::exit(125)
+ }
+ _ => return r,
+ }
+ }
+ r
+}
+
+fn parse_digit_string(digits: &OsStr, msg: &str) -> Result<u16, String> {
+ let checked = match str::from_utf8(digits.as_bytes()) {
+ Ok(s) => s,
+ Err(e) => return Err(format!("{msg} is not UTF-8: {e}")),
+ };
+ let r = checked
+ .parse::<u16>()
+ .map_err(|e| format!("{msg} {digits:?} is a bad 16-bit number: {e}"))?;
+ match checked.as_bytes() {
+ b"0" | [b'1'..=b'9', ..] => Ok(r),
+ [b'0', ..] => Err(format!("{msg} {} has a leading 0", digits.display())),
+ _ => Err(format!("{msg} {} starts with +", digits.display())),
+ }
+}
+
+fn cgroup_parse(mut arg: String) -> Result<(PathBuf, PathBuf, cgroup::LeafCgroup), String> {
+ let cgroup_root = rustix::fs::openat2(
+ rustix::fs::CWD,
+ Path::new("/sys/fs/cgroup"),
+ OFlags::DIRECTORY | OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
+ Mode::empty(),
+ ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
+ )
+ .map_err(|e| format!("cannot open /sys/fs/cgroup: {e}"))?;
+ let cgroup_absolute = arg.starts_with("/");
+ let (full_path, mut cgroup_parent, cgroup_target) = if cgroup_absolute {
+ if arg.contains("//") {
+ return Err(format!("Cgroup path {arg:?} contains //"));
+ }
+ arg.remove(0);
+ let to_create = PathBuf::from(arg);
+ for component in to_create.components() {
+ match component {
+ Component::Prefix(_) => unreachable!("not present on Unix"),
+ Component::CurDir | Component::ParentDir => {
+ return Err("Cgroup path has . or .. components".to_owned());
+ }
+ Component::RootDir | Component::Normal(_) => {}
+ }
+ }
+ let Some(last_component) = to_create.file_name() else {
+ return Err(format!("Cgroup path {to_create:?} has no file name"));
+ };
+ if to_create.parent().is_none() {
+ return Err(format!("Cgroup path {to_create:?} has no parent name"));
+ }
+ let r = last_component.as_bytes().to_owned();
+ let mut prefix = to_create.clone();
+ prefix.pop();
+ (to_create, prefix, r)
+ } else {
+ if arg.is_empty() {
+ return Err("Cgroup path is empty".to_string());
+ }
+
+ let mut local_cgroup: Vec<u8> = std::fs::read("/proc/thread-self/cgroup")
+ .map_err(|e| format!("cannot read /proc/thread-self/cgroup: {e}"))?;
+ let local_cgroup_len = local_cgroup.len();
+ if local_cgroup_len < 5
+ || local_cgroup[..4] != *b"0::/"
+ || local_cgroup[local_cgroup_len - 1] != b'\n'
+ {
+ return Err(format!(
+ "Invalid contents {local_cgroup:?} of /proc/thread-self/cgroup - \
+ do you have cgroups v1 mounted instead of cgroups v2?"
+ ));
+ }
+
+ local_cgroup.copy_within(4..local_cgroup_len - 1, 0);
+ local_cgroup.truncate(local_cgroup_len - 5);
+ let total_path = PathBuf::from(<OsString as OsStringExt>::from_vec(local_cgroup));
+ let mut r = total_path.clone();
+ r.push(&arg);
+ (r, total_path, arg.into())
+ };
+ if cgroup_parent.file_name() == Some(OsStr::from_bytes(b"$inner.service")) {
+ cgroup_parent.pop();
+ }
+ if cgroup_parent.as_os_str().is_empty() {
+ cgroup_parent = ".".into();
+ }
+ let cgroup_target = OsString::from_vec(cgroup_target).into();
+ let cgroup = cgroup::LeafCgroup::open_cgroup(cgroup_root.as_fd(), &cgroup_parent)
+ .map_err(|e| format!("Failed to open {}: {e:?}", cgroup_parent.display()))?;
+ match rustix::fs::flock(cgroup.as_fd(), FlockOperation::LockExclusive) {
+ Ok(()) => {}
+ Err(e) => return Err(format!("Cannot lock cgroup: {e}")),
+ }
+ Ok((full_path, cgroup_target, cgroup))
+}
+
+struct CgroupParams {
+ purge: bool,
+ leaf: bool,
+ delegate: bool,
+ init_subtree: bool,
+ wait: bool,
+}
+
+fn enable_subtree_control(cgroup: &cgroup::LeafCgroup) -> Result<(), String> {
+ let mut buf = Vec::new();
+ File::from(
+ cgroup
+ .open_subtree(Path::new("cgroup.controllers"), Access::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.ends_with(b"\n") {
+ buf.pop();
+ }
+ for controller in buf.split(|&b| b == b' ').filter(|e| !e.is_empty()) {
+ for &c in controller {
+ if c <= b' ' || c >= 0x7F {
+ return Err(format!("Bad byte {c} in cgroup.controllers"));
+ }
+ }
+ if !subtree.is_empty() {
+ subtree.push(b' ');
+ }
+ subtree.push(b'+');
+ subtree.extend_from_slice(controller);
+ }
+ if !subtree.is_empty() {
+ cgroup.write_cgroup_value("cgroup.subtree_control", str::from_utf8(&subtree).unwrap())?;
+ }
+ Ok(())
+}
+
+fn purge_cgroup(cgroup: &cgroup::LeafCgroup, cgroup_target: &Path) -> Result<(), String> {
+ let child = match cgroup.open_cgroup_at(cgroup_target) {
+ Ok(child_cgroup) => child_cgroup,
+ Err(Errno::NOENT) => return Ok(()),
+ Err(other) => {
+ return Err(format!(
+ "Cannot open child cgroup {cgroup_target:?}: {other}"
+ ));
+ }
+ };
+ child
+ .wait_for_empty(true)
+ .map_err(|e| format!("Cannot kill programs in {cgroup_target:?}: {e}"))?;
+
+ cgroup
+ .delete_child(cgroup_target)
+ .map_err(|e| format!("Delete child cgroup {cgroup_target:?}: {e}"))
+}
--
2.55.0
next prev parent reply other threads:[~2026-07-11 20:14 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 ` [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-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 ` Demi Marie Obenour [this message]
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=20260711-cgroups-v3-3-5cba61a20cba@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).