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: Alyssa Ross <hi@alyssa.is>, Demi Marie Obenour <demiobenour@gmail.com>
Subject: [PATCH] Set up control groups for most services
Date: Sat, 20 Jun 2026 10:23:09 -0400	[thread overview]
Message-ID: <20260620-cgroups-v1-1-0e5abf35101b@gmail.com> (raw)

The cgroups are handled by a Rust tool.  The name of the cgroup is
autogenerated from the service name.

Using cgroups for process control is not implemented yet.

Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com>
---
 .codespellrc                                       |   2 +-
 host/rootfs/default.nix                            |   6 +-
 host/rootfs/file-list.mk                           |  12 +
 host/rootfs/image/etc/fstab                        |   1 +
 .../s6-linux-init/run-image/service/getty-tty2/run |   1 +
 .../s6-linux-init/run-image/service/getty-tty3/run |   1 +
 .../s6-linux-init/run-image/service/getty-tty4/run |   1 +
 .../run-image/service/root-terminal/run            |   1 +
 .../run-image/service/s6-linux-init-shutdownd/run  |   1 +
 .../run-image/service/s6-svscan-log/run            |   1 +
 .../service/serial-getty-generator/finish          |   5 +
 .../run-image/service/serial-getty-generator/run   |   3 +-
 .../run-image/service/serial-getty/finish          |   5 +
 .../run-image/service/serial-getty/run             |   3 +-
 .../run-image/service/serial-getty/template/run    |   1 +
 .../run-image/service/vm-services/finish           |   5 +
 .../run-image/service/vm-services/run              |   3 +-
 .../vm-services/template/data/service/dbus/finish  |   5 +
 .../vm-services/template/data/service/dbus/run     |  13 +-
 .../template/data/service/spectrum-router/finish   |   5 +
 .../template/data/service/spectrum-router/run      |   3 +-
 .../template/data/service/vhost-user-fs/finish     |   5 +
 .../template/data/service/vhost-user-fs/run        |   1 +
 .../template/data/service/vhost-user-gpu/finish    |   5 +
 .../template/data/service/vhost-user-gpu/run       |   4 +-
 .../xdg-desktop-portal-spectrum-host/finish        |   5 +
 .../service/xdg-desktop-portal-spectrum-host/run   |   2 +
 .../run-image/service/vm-services/template/finish  |   5 +
 .../run-image/service/vm-services/template/run     |   1 +
 .../etc/s6-linux-init/run-image/service/vmm/finish |   5 +
 .../etc/s6-linux-init/run-image/service/vmm/run    |   3 +-
 host/rootfs/image/etc/s6-rc/systemd-udevd/finish   |   5 +
 host/rootfs/image/etc/s6-rc/systemd-udevd/run      |   2 +-
 host/rootfs/image/etc/s6-rc/weston/finish          |   5 +
 host/rootfs/image/etc/s6-rc/weston/run             |   2 +-
 pkgs/default.nix                                   |   1 +
 tools/cgroup-setup/Cargo.lock                      |  67 ++++++
 tools/cgroup-setup/Cargo.lock.license              |   2 +
 tools/cgroup-setup/Cargo.toml                      |  17 ++
 tools/cgroup-setup/default.nix                     |  18 ++
 tools/cgroup-setup/src/cgroup.rs                   | 217 ++++++++++++++++++
 tools/cgroup-setup/src/main.rs                     | 253 +++++++++++++++++++++
 42 files changed, 687 insertions(+), 16 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/host/rootfs/file-list.mk b/host/rootfs/file-list.mk
index 3899d620717fc97f42e669e5313c4100dcf5b1cd..acf5534e7b966811c545daf2d3aa4f26ca59e15e 100644
--- a/host/rootfs/file-list.mk
+++ b/host/rootfs/file-list.mk
@@ -21,25 +21,35 @@ FILES = \
 	image/etc/s6-linux-init/run-image/service/s6-linux-init-shutdownd/run \
 	image/etc/s6-linux-init/run-image/service/s6-svscan-log/notification-fd \
 	image/etc/s6-linux-init/run-image/service/s6-svscan-log/run \
+	image/etc/s6-linux-init/run-image/service/serial-getty-generator/finish \
 	image/etc/s6-linux-init/run-image/service/serial-getty-generator/run \
+	image/etc/s6-linux-init/run-image/service/serial-getty/finish \
 	image/etc/s6-linux-init/run-image/service/serial-getty/notification-fd \
 	image/etc/s6-linux-init/run-image/service/serial-getty/run \
 	image/etc/s6-linux-init/run-image/service/serial-getty/template/run \
+	image/etc/s6-linux-init/run-image/service/vm-services/finish \
 	image/etc/s6-linux-init/run-image/service/vm-services/notification-fd \
 	image/etc/s6-linux-init/run-image/service/vm-services/run \
+	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/dbus/finish \
 	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/dbus/notification-fd \
 	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/dbus/run \
 	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/down \
+	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/finish \
 	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/notification-fd \
 	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run \
+	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-fs/finish \
 	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-fs/notification-fd \
 	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-fs/run \
+	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-gpu/finish \
 	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-gpu/notification-fd \
 	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-gpu/run \
+	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/xdg-desktop-portal-spectrum-host/finish \
 	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/xdg-desktop-portal-spectrum-host/notification-fd \
 	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/xdg-desktop-portal-spectrum-host/run \
+	image/etc/s6-linux-init/run-image/service/vm-services/template/finish \
 	image/etc/s6-linux-init/run-image/service/vm-services/template/notification-fd \
 	image/etc/s6-linux-init/run-image/service/vm-services/template/run \
+	image/etc/s6-linux-init/run-image/service/vmm/finish \
 	image/etc/s6-linux-init/run-image/service/vmm/notification-fd \
 	image/etc/s6-linux-init/run-image/service/vmm/run \
 	image/etc/s6-linux-init/run-image/service/vmm/template/notification-fd \
@@ -96,6 +106,7 @@ S6_RC_FILES = \
 	image/etc/s6-rc/systemd-udevd-coldplug/dependencies.d/systemd-udevd \
 	image/etc/s6-rc/systemd-udevd-coldplug/type \
 	image/etc/s6-rc/systemd-udevd-coldplug/up \
+	image/etc/s6-rc/systemd-udevd/finish \
 	image/etc/s6-rc/systemd-udevd/notification-fd \
 	image/etc/s6-rc/systemd-udevd/run \
 	image/etc/s6-rc/systemd-udevd/type \
@@ -108,6 +119,7 @@ S6_RC_FILES = \
 	image/etc/s6-rc/vmm-env/contents.d/systemd-udevd-coldplug \
 	image/etc/s6-rc/vmm-env/type \
 	image/etc/s6-rc/weston/dependencies.d/systemd-udevd-coldplug \
+	image/etc/s6-rc/weston/finish \
 	image/etc/s6-rc/weston/notification-fd \
 	image/etc/s6-rc/weston/run \
 	image/etc/s6-rc/weston/type
diff --git a/host/rootfs/image/etc/fstab b/host/rootfs/image/etc/fstab
index 18bb5e45abafeaf43871306ce8233239e5c07f76..d92364d83f4d26f766fcd5b494614c06a630d2fe 100644
--- a/host/rootfs/image/etc/fstab
+++ b/host/rootfs/image/etc/fstab
@@ -6,3 +6,4 @@ tmpfs	/dev/shm	tmpfs	nosuid,nodev					0	0
 tmpfs	/media		tmpfs	nosuid,nodev,noexec,nosymfollow,mode=755	0	0
 sysfs	/sys		sysfs	nosuid,nodev,noexec				0	0
 tmpfs	/tmp		tmpfs	nosuid,nodev					0	0
+cgroup2 /sys/fs/cgroup  cgroup2 nosuid,nodev,noexec,nosymfollow			0	0
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/getty-tty2/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/getty-tty2/run
index 4d2cd7172e887e90d9ce87489cdacf41e51f77e8..5b2421a3dedf7c282ea818f4064535d0fc29d511 100755
--- a/host/rootfs/image/etc/s6-linux-init/run-image/service/getty-tty2/run
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/getty-tty2/run
@@ -2,4 +2,5 @@
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2020-2021 Alyssa Ross <hi@alyssa.is>
 
+# no cgroups so that nohup works
 getty -i -n -l /etc/login 0 tty2 linux
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/getty-tty3/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/getty-tty3/run
index 06abf5422365ff4edc9379e5cbcab716e6830399..3ca382f76368174ee1f99ea880827958c57b602c 100755
--- a/host/rootfs/image/etc/s6-linux-init/run-image/service/getty-tty3/run
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/getty-tty3/run
@@ -2,4 +2,5 @@
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2020-2021 Alyssa Ross <hi@alyssa.is>
 
+# no cgroups so that nohup works
 getty -i -n -l /etc/login 0 tty3 linux
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/getty-tty4/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/getty-tty4/run
index 3dd235fc93d403fc8971e923ec62cb025684b67a..983bd82927e468bab6ae20f61411680d173e4924 100755
--- a/host/rootfs/image/etc/s6-linux-init/run-image/service/getty-tty4/run
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/getty-tty4/run
@@ -2,4 +2,5 @@
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2020-2021 Alyssa Ross <hi@alyssa.is>
 
+# no cgroups so that nohup works
 getty -i -n -l /etc/login 0 tty4 linux
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/root-terminal/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/root-terminal/run
index 86b9a1ef70300de52f33a84cbfac22a77e00df36..fb55327dff5f189f543fb3629702caad9ff247e2 100755
--- a/host/rootfs/image/etc/s6-linux-init/run-image/service/root-terminal/run
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/root-terminal/run
@@ -2,6 +2,7 @@
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2025 Alyssa Ross <hi@alyssa.is>
 
+# cgroups make no sense here
 s6-ipcserver-socketbinder -a 0700 /run/root-terminal
 
 if { chown wayland /run/root-terminal }
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/s6-linux-init-shutdownd/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/s6-linux-init-shutdownd/run
index 327f4ccc5056e7ef87c089e666090307845cae3a..a6733604e91692f8440fd68efeb1a6b7378be167 100755
--- a/host/rootfs/image/etc/s6-linux-init/run-image/service/s6-linux-init-shutdownd/run
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/s6-linux-init-shutdownd/run
@@ -2,4 +2,5 @@
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is>
 
+# cgroups make no sense for shutdownd
 s6-linux-init-shutdownd -Bc /etc/s6-linux-init
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/s6-svscan-log/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/s6-svscan-log/run
index 8f7bd889e50d76a49b98d8ba9e2c6172f287277c..cf3f5c7b499d997fd426062651eca64b3accc146 100755
--- a/host/rootfs/image/etc/s6-linux-init/run-image/service/s6-svscan-log/run
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/s6-svscan-log/run
@@ -2,5 +2,6 @@
 # SPDX-License-Identifier: ISC
 # SPDX-FileCopyrightText: Copyright (c) 2015-2024 Laurent Bercot <ska-skaware@skarnet.org>
 
+# cgroups make no sense for a logger
 redirfd -rnb 0 fifo
 s6-log -bpd3 -- T /run/log
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty-generator/finish b/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty-generator/finish
new file mode 100755
index 0000000000000000000000000000000000000000..6ee682c37219e504e3d9d0e9081ec67e85ad9f5f
--- /dev/null
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty-generator/finish
@@ -0,0 +1,5 @@
+#!/bin/execlineb -WP
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2024-2025 Alyssa Ross <hi@alyssa.is>
+
+cgroup-setup -- serial-getty-generator
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty-generator/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty-generator/run
index bf66ab0878fc6d8e77ca71a4a89e50c139a6bb11..5c9988c994ed9b942329055673b332d0aa918957 100755
--- a/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty-generator/run
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty-generator/run
@@ -1,7 +1,8 @@
-#!/bin/execlineb -WP
+#!/bin/execlineb -WS1
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2024-2025 Alyssa Ross <hi@alyssa.is>
 
+cgroup-setup -- $1
 piperw 3 4
 background {
   fdclose 3
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty/finish b/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty/finish
new file mode 100755
index 0000000000000000000000000000000000000000..0c16cc8060762eac3a91a49b54048407dfbe25c5
--- /dev/null
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty/finish
@@ -0,0 +1,5 @@
+#!/bin/execlineb -WS3
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2026 Demi Marie Obenour <demiobenour@gmail.com>
+
+cgroup-setup -- $3
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty/run
index 78f794202bf174f3c036f3e20755ac087a988277..ed0808b9e45b077023f237a0f9c0e5c830015ac2 100755
--- a/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty/run
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty/run
@@ -1,5 +1,6 @@
-#!/bin/execlineb -WP
+#!/bin/execlineb -WS1
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is>
 
+cgroup-setup -- $1
 s6-svscan -d3 instance
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty/template/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty/template/run
index f3ed9a7f08e60206afd222f4fc02c5fb19527561..775b828c72b9d73bef8a3355fb3ae0536975f474 100755
--- a/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty/template/run
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty/template/run
@@ -2,4 +2,5 @@
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2020-2021, 2024 Alyssa Ross <hi@alyssa.is>
 
+# Don't put this in a cgroup.  We want nohup to work.
 getty -i -n -l /etc/login 0,115200,57600,38400,9600 $1 dumb
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/finish b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/finish
new file mode 100755
index 0000000000000000000000000000000000000000..ed324e5a0871e6d019bc925a91f8aa8ab1aa6a18
--- /dev/null
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/finish
@@ -0,0 +1,5 @@
+#!/bin/execlineb -WP
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is>
+
+cgroup-setup -- vm-services-template
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/run
index 78f794202bf174f3c036f3e20755ac087a988277..ed0808b9e45b077023f237a0f9c0e5c830015ac2 100755
--- a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/run
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/run
@@ -1,5 +1,6 @@
-#!/bin/execlineb -WP
+#!/bin/execlineb -WS1
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is>
 
+cgroup-setup -- $1
 s6-svscan -d3 instance
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/dbus/finish b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/dbus/finish
new file mode 100755
index 0000000000000000000000000000000000000000..0299fd3705aaf5950e2ac0f686d3d211a74c9fc2
--- /dev/null
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/dbus/finish
@@ -0,0 +1,5 @@
+#!/bin/execlineb -WP
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2024-2025 Alyssa Ross <hi@alyssa.is>
+
+cgroup-setup -- dbus-daemon
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/dbus/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/dbus/run
index f0507dc92aadf25c092f0719eb767542ac0c4451..1a9b10892805c0350ce12de93e156b2a338ffd24 100755
--- a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/dbus/run
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/dbus/run
@@ -1,7 +1,8 @@
-#!/bin/execlineb -WP
+#!/bin/execlineb -WS1
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2024-2025 Alyssa Ross <hi@alyssa.is>
 
+cgroup-setup -- $1
 importas -i VM VM
 
 if {
@@ -16,10 +17,14 @@ redirfd -r 0 /dev/null
 
 s6-envuidgid fs
 s6-applyuidgid -Uzu 0
-getcwd -E dir
+getcwd dir
+multisubstitute {
+  define VM_ ${VM}
+  importas -iS dir
+}
 nsenter --preserve-credentials -S0
-  --mount=/run/vm/by-id/${VM}/ns/mnt
-  --user=/run/vm/by-id/${VM}/ns/user
+  --mount=/run/vm/by-id/${VM_}/ns/mnt
+  --user=/run/vm/by-id/${VM_}/ns/user
 
 unshare --cgroup --ipc --net --uts
 
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/finish b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/finish
new file mode 100755
index 0000000000000000000000000000000000000000..78d1229247ea2a457833e323a9428e04f9a1ed3c
--- /dev/null
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/finish
@@ -0,0 +1,5 @@
+#!/bin/execlineb -WP
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2026 Demi Marie Obenour <demiobenour@gmail.com>
+
+cgroup-setup -- spectrum-router
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run
index 3de58c27facc78c48176d8f9a6dd1827ac926f1a..21e37ab8a3dad1ab701a0989fb408fed100657cf 100755
--- a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run
@@ -1,8 +1,9 @@
-#!/bin/execlineb -WP
+#!/bin/execlineb -WS1
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
 # SPDX-FileCopyrightText: 2025 Alyssa Ross <hi@alyssa.is>
 
+cgroup-setup -- $1
 importas -i VM VM
 
 s6-ipcserver-socketbinder -a 0770 /run/vm/by-id/${VM}/router-driver.sock
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-fs/finish b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-fs/finish
new file mode 100755
index 0000000000000000000000000000000000000000..4a6ca0452c437e9312964164b75843e28734ae18
--- /dev/null
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-fs/finish
@@ -0,0 +1,5 @@
+#!/bin/execlineb -WP
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2026 Demi Marie Obenour <demiobenour@gmail.com>
+
+cgroup-setup -- vhost-user-fs
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-fs/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-fs/run
index cb059aebc8dfe2c8a5b059d5bfea884de53bf922..d4321263269ae46dd6a1b72e39c57c1c07cb06ba 100755
--- a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-fs/run
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-fs/run
@@ -2,6 +2,7 @@
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2021-2025 Alyssa Ross <hi@alyssa.is>
 
+cgroup-setup -- $1
 s6-ipcserver-socketbinder -a 0700 -B env/virtiofsd.sock
 
 importas -i VM VM
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-gpu/finish b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-gpu/finish
new file mode 100755
index 0000000000000000000000000000000000000000..c1cbc523415b573c4205d5e77d7d5cc29ff51481
--- /dev/null
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-gpu/finish
@@ -0,0 +1,5 @@
+#!/bin/execlineb -WP
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2026 Demi Marie Obenour <demiobenour@gmail.com>
+
+cgroup-setup -- crosvm
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-gpu/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-gpu/run
index c3bfafe02ec6d2ee418fe56b033bbc4c7a73b186..ad705da8460a86667b47d40e61fee167d240a9c6 100755
--- a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-gpu/run
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-gpu/run
@@ -1,8 +1,10 @@
-#!/bin/execlineb -WP
+#!/bin/execlineb -WS1
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2025 Alyssa Ross <hi@alyssa.is>
 # SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com>
 
+cgroup-setup -- $1
+
 s6-ipcserver-socketbinder -a 0700 -b 1 env/crosvm.sock
 
 multisubstitute {
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/xdg-desktop-portal-spectrum-host/finish b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/xdg-desktop-portal-spectrum-host/finish
new file mode 100755
index 0000000000000000000000000000000000000000..d0cd4081fee3df4d898ef1e4803c6c8fa3c3ed45
--- /dev/null
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/xdg-desktop-portal-spectrum-host/finish
@@ -0,0 +1,5 @@
+#!/bin/execlineb -WP
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2026 Demi Marie Obenour <demiobenour@gmail.com>
+
+cgroup-setup -- xdg-desktop-portal-spectrum-host
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/xdg-desktop-portal-spectrum-host/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/xdg-desktop-portal-spectrum-host/run
index 909957b817136149dd01c3b98847238ecda8032d..54860d67f284e7cabbfc955f44ef819ff3f593bc 100755
--- a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/xdg-desktop-portal-spectrum-host/run
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/xdg-desktop-portal-spectrum-host/run
@@ -2,6 +2,8 @@
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2024-2025 Alyssa Ross <hi@alyssa.is>
 
+cgroup-setup -- $1
+
 importas -i VM VM
 
 export DBUS_SESSION_BUS_ADDRESS unix:path=/run/portal-bus/${VM}
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/finish b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/finish
new file mode 100755
index 0000000000000000000000000000000000000000..da3fc34f057ce5788ddf3ff172f122cf53f8b705
--- /dev/null
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/finish
@@ -0,0 +1,5 @@
+#!/bin/execlineb -WS3
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2026 Demi Marie Obenour <demiobenour@gmail.com>
+
+cgroup-setup -- vm-services@${3}
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/run
index bf90e99eef4106a9ad17dfba310b6edb3eaa64a3..13f2cec03a087be4602c14ed78749cce1f6a26a1 100755
--- a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/run
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/run
@@ -2,6 +2,7 @@
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2025 Alyssa Ross <hi@alyssa.is>
 
+cgroup-setup -- $1
 export VM $1
 
 s6-svscan -d3 data/service
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vmm/finish b/host/rootfs/image/etc/s6-linux-init/run-image/service/vmm/finish
new file mode 100755
index 0000000000000000000000000000000000000000..51d7d2a7f92b98761f7b836bf9ee4ebdfab41cc7
--- /dev/null
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vmm/finish
@@ -0,0 +1,5 @@
+#!/bin/execlineb -WP
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is>
+
+cgroup-setup -- vmm
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vmm/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/vmm/run
index 78f794202bf174f3c036f3e20755ac087a988277..ed0808b9e45b077023f237a0f9c0e5c830015ac2 100755
--- a/host/rootfs/image/etc/s6-linux-init/run-image/service/vmm/run
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vmm/run
@@ -1,5 +1,6 @@
-#!/bin/execlineb -WP
+#!/bin/execlineb -WS1
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is>
 
+cgroup-setup -- $1
 s6-svscan -d3 instance
diff --git a/host/rootfs/image/etc/s6-rc/systemd-udevd/finish b/host/rootfs/image/etc/s6-rc/systemd-udevd/finish
new file mode 100755
index 0000000000000000000000000000000000000000..73e4c0315d345a443f844244c84b8546f076771c
--- /dev/null
+++ b/host/rootfs/image/etc/s6-rc/systemd-udevd/finish
@@ -0,0 +1,5 @@
+#!/bin/execlineb -WP
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2026 Demi Marie Obenour <demiobenour@gmail.com>
+
+cgroup-setup -- systemd-udevd
diff --git a/host/rootfs/image/etc/s6-rc/systemd-udevd/run b/host/rootfs/image/etc/s6-rc/systemd-udevd/run
old mode 100644
new mode 100755
index aec6444e951503eae988e666b77fda8f2ae33d72..69c67dd9620e557391076d5505fcdc8bdbddac67
--- a/host/rootfs/image/etc/s6-rc/systemd-udevd/run
+++ b/host/rootfs/image/etc/s6-rc/systemd-udevd/run
@@ -1,7 +1,7 @@
 #!/bin/execlineb -WP
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com>
-s6-setlock /run/sd-notify-wrapper/systemd-udevd.lock
+cgroup-setup -- systemd-udevd
 s6-ipcserver-socketbinder -b0 -m -a 0600 /run/sd-notify-wrapper/systemd-udevd.sock
 background -d {
    fdmove 1 3
diff --git a/host/rootfs/image/etc/s6-rc/weston/finish b/host/rootfs/image/etc/s6-rc/weston/finish
new file mode 100755
index 0000000000000000000000000000000000000000..a37813bddc0c599eb44ab9901e1685be45cba207
--- /dev/null
+++ b/host/rootfs/image/etc/s6-rc/weston/finish
@@ -0,0 +1,5 @@
+#!/bin/execlineb -WP
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2026 Demi Marie Obenour <demiobenour@gmail.com>
+
+cgroup-setup -- weston
diff --git a/host/rootfs/image/etc/s6-rc/weston/run b/host/rootfs/image/etc/s6-rc/weston/run
old mode 100644
new mode 100755
index fd59586c719391deb546c29578341e16a61ed4ce..5a6e64d41d6e4f2714949f13bad3019479560e04
--- a/host/rootfs/image/etc/s6-rc/weston/run
+++ b/host/rootfs/image/etc/s6-rc/weston/run
@@ -2,6 +2,7 @@
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2021, 2025 Alyssa Ross <hi@alyssa.is>
 
+cgroup-setup -- weston
 importas -Siu WAYLAND_DISPLAY
 
 piperw 4 3
@@ -40,7 +41,6 @@ redirfd -r 0 /dev/tty1
 
 importas -i home HOME
 cd $home
-if { udevadm wait /dev/dri/card0 }
 s6-setuidgid wayland
 bwrap
   # no --unshare-net, breaks udev hotplug
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..fd7246cd0bc2e9a980898eae42531fef601f524b
--- /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..9cce0be706151dfd5264a81afb3ee9a6c851367c
--- /dev/null
+++ b/tools/cgroup-setup/Cargo.toml
@@ -0,0 +1,17 @@
+# 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"] }
+
+[profile.release]
+split-debuginfo = "symbols"
+strip = "symbols"
+lto = true
+panic = "abort"
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..3cc6627e01f9bf01118dcfda849d431c128f5017
--- /dev/null
+++ b/tools/cgroup-setup/src/cgroup.rs
@@ -0,0 +1,217 @@
+// SPDX-License-Identifier: EUPL-1.2+
+// SPDX-FileCopyrightText: 2026 Demi Marie Obenour <demiobenour@gmail.com>
+
+use std::fs::File;
+use std::io::{Read, Seek};
+use std::os::fd::{AsFd, OwnedFd};
+use std::os::fd::{AsRawFd, BorrowedFd};
+
+use std::path::{Path, PathBuf};
+
+use rustix::fs::AtFlags;
+use rustix::path::Arg;
+use rustix::{
+    fs::{Mode, OFlags, ResolveFlags},
+    io::Errno,
+};
+
+pub(crate) struct LeafCgroup {
+    path: PathBuf,
+    fd: OwnedFd,
+}
+
+mod epoll {
+    use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
+
+    #[expect(dead_code)]
+    pub(super) struct Epoll(OwnedFd, OwnedFd);
+
+    impl Epoll {
+        #[expect(dead_code)]
+        pub(super) fn new(other_fd: OwnedFd) -> Self {
+            // SAFETY: FFI call with valid arguments
+            let fd = unsafe { libc::epoll_create1(libc::EPOLL_CLOEXEC) };
+            if fd == -1 {
+                panic!("epoll_create1 failed: {}", std::io::Error::last_os_error());
+            }
+
+            let mut event = libc::epoll_event {
+                events: (libc::EPOLLIN | libc::EPOLLPRI | libc::EPOLLRDHUP) as _,
+                r#u64: 0,
+            };
+
+            let res = unsafe {
+                libc::epoll_ctl(
+                    fd.as_raw_fd(),
+                    libc::EPOLL_CTL_ADD,
+                    other_fd.as_raw_fd(),
+                    &raw mut event,
+                )
+            };
+            assert_eq!(res, 0, "kernel out of memory?");
+
+            // SAFETY: epoll_create1 returns valid FD or -1 and we checked the error case
+            Self(unsafe { OwnedFd::from_raw_fd(fd) }, other_fd)
+        }
+    }
+}
+
+enum Access {
+    Read,
+    Write,
+}
+
+impl LeafCgroup {
+    fn open_subtree(&self, path: &std::path::Path, access: Access) -> Result<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 kill_processes(&self) -> std::io::Result<()> {
+        let kill_fd = self.open_subtree(std::path::Path::new("cgroup.kill"), Access::Write)?;
+        let wait_file = self.open_subtree(std::path::Path::new("cgroup.events"), Access::Read)?;
+        let poll_fd = wait_file.as_raw_fd();
+        let mut wait_fd = File::from(wait_file);
+        assert_eq!(rustix::io::write(kill_fd.as_fd(), b"1")?, 1, "kernel bug");
+        let mut fds = libc::pollfd {
+            fd: poll_fd,
+            events: libc::POLLIN | libc::POLLPRI | libc::POLLRDHUP,
+            revents: 0,
+        };
+        // SAFETY: FFI call, valid arguments, fds contains 1 element
+        let mut v = vec![];
+        'a: loop {
+            v.clear();
+            if unsafe { libc::poll(&raw mut fds, 1, -1) } != 1 {
+                panic!("poll failed");
+            }
+            wait_fd
+                .seek(std::io::SeekFrom::Start(0))
+                .expect("Seek on control group file should succeed");
+            wait_fd
+                .read_to_end(&mut v)
+                .expect("reading from control group should work");
+            for substr in v.split(|&c| c == b'\n') {
+                if substr == b"populated 0" {
+                    break 'a;
+                }
+            }
+        }
+        Ok(())
+    }
+}
+
+impl AsFd for LeafCgroup {
+    fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
+        self.fd.as_fd()
+    }
+}
+
+impl LeafCgroup {
+    pub(crate) fn open_cgroup_at(&self, p: &Path) -> Result<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 | ResolveFlags::BENEATH | ResolveFlags::NO_XDEV,
+            ResolveFlags::NO_SYMLINKS,
+        )?;
+        Ok(Self {
+            fd,
+            path: p.to_owned(),
+        })
+    }
+    pub(crate) fn make_child(&self, p: &Path) -> Result<(), Errno> {
+        rustix::fs::mkdirat(
+            self.fd.as_fd(),
+            p,
+            Mode::RUSR
+                | Mode::WUSR
+                | Mode::XUSR
+                | Mode::RGRP
+                | Mode::XGRP
+                | Mode::ROTH
+                | Mode::XOTH,
+        )
+    }
+
+    pub(crate) fn delete_child(&self, path: &Path) -> Result<(), Errno> {
+        remove_all(100, self.as_fd(), &path.as_cow_c_str().unwrap())
+    }
+}
+
+fn remove_recursively(fd: OwnedFd, remaining_depth: usize) -> Result<(), Errno> {
+    if remaining_depth < 1 {
+        panic!("control groups too deeply nested");
+    }
+    let mut d = rustix::fs::Dir::new(fd).expect("cannot start iterating");
+    while let Some(element) = d.next() {
+        let element = element.expect("Iterating through a cgroup directory failed?");
+        if element.file_type() != rustix::fs::FileType::Directory {
+            continue;
+        }
+
+        let remaining_depth = remaining_depth - 1;
+        let d: &rustix::fs::Dir = &d;
+        let dirfd = d.fd().unwrap();
+        let path = element.file_name();
+        remove_all(remaining_depth, dirfd, path)?;
+    }
+    Ok(())
+}
+
+fn remove_all(
+    remaining_depth: usize,
+    dirfd: BorrowedFd<'_>,
+    path: &std::ffi::CStr,
+) -> Result<(), Errno> {
+    if path == c"." || path == c".." {
+        return Ok(());
+    }
+    if rustix::fs::unlinkat(dirfd, path, AtFlags::REMOVEDIR).is_ok() {
+        return Ok(());
+    }
+    let fd = rustix::fs::openat2(
+        dirfd,
+        path,
+        OFlags::NOATIME | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::RDONLY | OFlags::DIRECTORY,
+        Mode::empty(),
+        ResolveFlags::NO_SYMLINKS | ResolveFlags::BENEATH | ResolveFlags::NO_XDEV,
+    )?;
+    remove_recursively(fd, remaining_depth)?;
+    rustix::fs::unlinkat(dirfd, path, AtFlags::REMOVEDIR)?;
+    Ok(())
+}
diff --git a/tools/cgroup-setup/src/main.rs b/tools/cgroup-setup/src/main.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f10eadb013b987d33c741f327e5c180164824801
--- /dev/null
+++ b/tools/cgroup-setup/src/main.rs
@@ -0,0 +1,253 @@
+// SPDX-License-Identifier: EUPL-1.2+
+// SPDX-FileCopyrightText: 2026 Demi Marie Obenour <demiobenour@gmail.com>
+
+use std::{
+    ffi::{OsStr, OsString},
+    fs::File,
+    io::Write as _,
+    os::unix::prelude::*,
+    path::{Component, Path, PathBuf},
+};
+
+use rustix::{
+    fs::{FlockOperation, Mode, OFlags, ResolveFlags},
+    io::Errno,
+};
+
+mod cgroup;
+
+macro_rules! fail {
+    ($($arg:tt)*) => {{
+        eprintln!($($arg)*);
+        std::process::exit(1)}
+    };
+}
+
+fn parse_string(original: &str, msg: &str, num: usize) -> u64 {
+    let (to_parse, scale, unit): (_, u64, &'static str) = match original.as_bytes() {
+        [rest @ .., b'K', b'B'] => (rest, 1_000, "KB"),
+        [rest @ .., b'M', b'B'] => (rest, 1_000_000, "MB"),
+        [rest @ .., b'G', b'B'] => (rest, 1_000_000_000, "GB"),
+        [rest @ .., b'T', b'B'] => (rest, 1_000_000_000_000, "TB"),
+        [rest @ .., b'P', b'B'] => (rest, 1_000_000_000_000_000, "PB"),
+        [rest @ .., b'E', b'B'] => (rest, 1_000_000_000_000_000_000, "EB"),
+        [rest @ .., b'K', b'i', b'B'] => (rest, 1 << 10, "KiB"),
+        [rest @ .., b'M', b'i', b'B'] => (rest, 1 << 20, "MiB"),
+        [rest @ .., b'G', b'i', b'B'] => (rest, 1 << 30, "GiB"),
+        [rest @ .., b'T', b'i', b'B'] => (rest, 1 << 40, "TiB"),
+        [rest @ .., b'P', b'i', b'B'] => (rest, 1 << 50, "PiB"),
+        [rest @ .., b'E', b'i', b'B'] => (rest, 1 << 60, "EiB"),
+        [rest @ .., b'B'] if rest.last().map(u8::is_ascii_digit).unwrap_or(false) => (rest, 1, "B"),
+        _ => fail!("{msg}: {original:?} is missing a unit suffix"),
+    };
+    let (to_parse, radix) = match to_parse {
+        [b'0', b'x' | b'X', s @ ..] => (s, 16),
+        _ => (to_parse, 10),
+    };
+    // SAFETY: lopping ASCII bytes off start
+    // and end of UTF-8 string leaves valid UTF_8
+    let result: u64 =
+        match u64::from_str_radix(unsafe { str::from_utf8_unchecked(to_parse) }, radix) {
+            Ok(e) => e,
+            Err(e) => {
+                fail!("{msg}: argument {num}: Cannot parse as u64: {e}")
+            }
+        };
+    if to_parse[0] == b'0' || to_parse[0] == b'+' {
+        fail!(
+            "{msg}: argument {num}: Leading {} in {original} not allowed",
+            to_parse[0] as char
+        )
+    }
+    result.checked_mul(scale).unwrap_or_else(|| {
+        fail!(
+            "{msg}: {original} is too large for unit {unit} ({original} × {scale} = {} > {})",
+            u128::from(result) * u128::from(scale),
+            u64::MAX,
+        )
+    })
+}
+
+fn main() {
+    let mut args = std::env::args_os();
+    if args.next().is_none() {
+        fail!("No command line arguments (argv[0] is NULL)");
+    }
+    let mut args = args.enumerate().map(|(num, arg)| {
+        match str::from_utf8(<OsStr as OsStrExt>::as_bytes(&arg)) {
+            Ok(v) => (num, v.to_owned()),
+            Err(e) => {
+                fail!(
+                    "Argument {num} bytes {} through {} are invalid UTF-8",
+                    e.valid_up_to(),
+                    e.error_len().unwrap_or_else(|| arg.len())
+                )
+            }
+        }
+    });
+    let mut finished = false;
+    let mut positional_arguments;
+    let mut values = [
+        ("pids.max", None),
+        ("memory.high", None),
+        ("memory.max", None),
+    ];
+
+    let last = loop {
+        let Some((num, arg_str)) = args.next() else {
+            finished = true;
+            break None;
+        };
+        if arg_str.as_bytes().get(0) != Some(&b'-') {
+            break Some(arg_str.to_owned());
+        }
+        let Some(suffix) = arg_str.strip_prefix("--") else {
+            fail!("Short options (with a single '-', as in {arg_str:?}) are not supported")
+        };
+
+        if suffix.is_empty() {
+            break None;
+        }
+
+        for value in &mut values {
+            if let Some(suffix) = suffix.strip_prefix(value.0)
+                && let Some(suffix) = suffix.strip_prefix("=")
+            {
+                value.1 = Some(parse_string(suffix, &value.0, num));
+            }
+        }
+
+        fail!("Unknown option {arg_str:?}")
+    };
+
+    if finished {
+        positional_arguments = vec![]
+    } else {
+        positional_arguments = last.into_iter().chain(args.map(|a| a.1)).collect()
+    }
+
+    if positional_arguments.is_empty() {
+        fail!(
+            "Have {} arguments, expect at least 1",
+            positional_arguments.len()
+        )
+    }
+
+    // slow but we do not care
+    let mut child_path = positional_arguments.remove(0);
+    if child_path.len() > 247 {
+        fail!("Cgroup name {child_path:?} too long");
+    }
+    if child_path.as_bytes().contains(&b'/') {
+        fail!("Cgroup name {child_path:?} contains /");
+    }
+    child_path += ".service";
+    let child_path = Path::new(&child_path);
+
+    let local_cgroup =
+        std::fs::read("/proc/thread-self/cgroup").expect("Should be able to read from procfs");
+    if !local_cgroup.starts_with(b"0::/")
+        || !local_cgroup.ends_with(b"\n")
+        || local_cgroup.contains(&b'\0')
+    {
+        fail!("Kernel bug: Bad contents of /proc/thread-self/cgroup");
+    }
+
+    let mut local_cgroup = PathBuf::from(<OsString as OsStringExt>::from_vec(
+        local_cgroup[4..local_cgroup.len() - 1].to_owned(),
+    ));
+
+    match local_cgroup.components().next_back() {
+        Some(Component::Prefix(_)) => unreachable!("does not occur on Linux"),
+        Some(Component::RootDir) | None => {}
+        Some(Component::CurDir) => unreachable!("not produced by kernel"),
+        Some(Component::ParentDir) => unreachable!("not produced by kernel"),
+        Some(Component::Normal(os_str)) => {
+            if os_str.as_bytes() == b"@inner.service" {
+                let _ = local_cgroup.pop();
+            }
+        }
+    }
+
+    if local_cgroup.as_os_str().is_empty() {
+        local_cgroup = PathBuf::from(".");
+    }
+
+    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,
+    )
+    .expect("Cannot get cgroup root");
+
+    let cgroup = match cgroup::LeafCgroup::open_cgroup(cgroup_root.as_fd(), &local_cgroup) {
+        Ok(e) => e,
+        Err(e) => fail!(
+            "Bad cgroup operation {e:?} opening {}",
+            local_cgroup.display()
+        ),
+    };
+
+    rustix::fs::flock(cgroup.as_fd(), FlockOperation::LockExclusive).expect("oops");
+    purge_cgroup(&cgroup, child_path);
+    if positional_arguments.is_empty() {
+        return;
+    }
+    if let Err(e) = cgroup.make_child(child_path) {
+        fail!("Cannot create child cgroup: {e}")
+    }
+    let child = cgroup
+        .open_cgroup_at(Path::new(&child_path))
+        .expect("cannot open child cgroup");
+    child
+        .make_child(Path::new("@inner.service"))
+        .expect("cannot make child cgroup");
+    let child = child
+        .open_cgroup_at(Path::new("@inner.service"))
+        .expect("cannot open child cgroup");
+
+    for (name, value) in values {
+        if let Some(value) = value {
+            write_cgroup_value(&child, name, &value.to_string());
+        }
+    }
+
+    // SAFETY: safe FFI call
+    let pid = unsafe { libc::getpid() };
+    write_cgroup_value(&child, "cgroup.procs", &pid.to_string());
+
+    let program = positional_arguments.remove(0);
+    let e = std::process::Command::new(program.clone())
+        .args(positional_arguments)
+        .exec();
+    fail!("Cannot spawn child {:?}: {}", program, e);
+}
+
+fn write_cgroup_value(child: &cgroup::LeafCgroup, name: &str, value: &str) {
+    let path = Path::new(name);
+    let fd = rustix::fs::openat2(
+        child.as_fd(),
+        path,
+        OFlags::NOATIME | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::WRONLY,
+        Mode::empty(),
+        ResolveFlags::NO_SYMLINKS | ResolveFlags::BENEATH | ResolveFlags::NO_XDEV,
+    )
+    .unwrap_or_else(|e| fail!("Cannot open {:?}: {}", path, e));
+    let () = File::from(fd)
+        .write_all(value.as_bytes())
+        .unwrap_or_else(|e| fail!("Cannot write {:?} to {:?}: {}", name, path, e));
+}
+
+fn purge_cgroup(cgroup: &cgroup::LeafCgroup, child_path: &Path) {
+    let child = match cgroup.open_cgroup_at(child_path) {
+        Ok(child_cgroup) => child_cgroup,
+        Err(Errno::NOENT) => return,
+        Err(other) => fail!("Cannot open child cgroup {child_path:?}: {other}"),
+    };
+    child.kill_processes().expect("cannot kill children");
+    cgroup
+        .delete_child(child_path)
+        .expect("cannot delete child");
+}

---
base-commit: 36ea9d85403a29f1e700f534020cc4592d78ee50
change-id: 20260528-cgroups-d609e270e649

-- 
Sincerely,
Demi Marie Obenour (she/her/hers)


             reply	other threads:[~2026-06-20 14:31 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-20 14:23 Demi Marie Obenour [this message]
2026-06-20 17:27 ` [PATCH v2] Set up control groups for most services 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

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=20260620-cgroups-v1-1-0e5abf35101b@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/crosvm
	https://spectrum-os.org/git/doc
	https://spectrum-os.org/git/mktuntap
	https://spectrum-os.org/git/nixpkgs
	https://spectrum-os.org/git/spectrum
	https://spectrum-os.org/git/ucspi-vsock
	https://spectrum-os.org/git/www

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).