patches and low-level development discussion
 help / color / mirror / code / Atom feed
* [PATCH] Set up control groups for most services
@ 2026-06-20 14:23 Demi Marie Obenour
  2026-06-20 17:27 ` [PATCH v2] " Demi Marie Obenour
  0 siblings, 1 reply; 8+ messages in thread
From: Demi Marie Obenour @ 2026-06-20 14:23 UTC (permalink / raw)
  To: Spectrum OS Development; +Cc: Alyssa Ross, Demi Marie Obenour

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)


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH v2] Set up control groups for most services
  2026-06-20 14:23 [PATCH] Set up control groups for most services Demi Marie Obenour
@ 2026-06-20 17:27 ` Demi Marie Obenour
  2026-06-24 12:13   ` Alyssa Ross
  0 siblings, 1 reply; 8+ messages in thread
From: Demi Marie Obenour @ 2026-06-20 17:27 UTC (permalink / raw)
  To: Spectrum OS Development; +Cc: Alyssa Ross, Demi Marie Obenour

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>
---
Changes in v2:
- Omit resource control support.  It was completely broken, and the way
  Spectrum run scripts work means that it is better to setup resource
  controls just before the execve() into the final service process.
  This will be done by a separate tool.
- Link to v1: https://spectrum-os.org/lists/archives/spectrum-devel/20260620-cgroups-v1-1-0e5abf35101b@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     |   3 +-
 .../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      |   5 +-
 host/rootfs/image/etc/s6-rc/weston/finish          |   5 +
 host/rootfs/image/etc/s6-rc/weston/run             |   3 +-
 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                   | 181 +++++++++++++++++++
 tools/cgroup-setup/src/main.rs                     | 196 +++++++++++++++++++++
 42 files changed, 590 insertions(+), 14 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..0c16cc8060762eac3a91a49b54048407dfbe25c5
--- /dev/null
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/serial-getty-generator/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-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..0c16cc8060762eac3a91a49b54048407dfbe25c5
--- /dev/null
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/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/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..0c16cc8060762eac3a91a49b54048407dfbe25c5
--- /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 -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/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..89676ac91d3e22db7726fc869a065137b031d0be 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 {
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..0c16cc8060762eac3a91a49b54048407dfbe25c5
--- /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 -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/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..0c16cc8060762eac3a91a49b54048407dfbe25c5
--- /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 -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/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..0c16cc8060762eac3a91a49b54048407dfbe25c5
--- /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 -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/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..0c16cc8060762eac3a91a49b54048407dfbe25c5
--- /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 -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/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..0c16cc8060762eac3a91a49b54048407dfbe25c5
--- /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 -- $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..0c16cc8060762eac3a91a49b54048407dfbe25c5
--- /dev/null
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vmm/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/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..0c16cc8060762eac3a91a49b54048407dfbe25c5
--- /dev/null
+++ b/host/rootfs/image/etc/s6-rc/systemd-udevd/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-rc/systemd-udevd/run b/host/rootfs/image/etc/s6-rc/systemd-udevd/run
old mode 100644
new mode 100755
index aec6444e951503eae988e666b77fda8f2ae33d72..c37909af23fa611a75442108100b4fd9db1e22f8
--- a/host/rootfs/image/etc/s6-rc/systemd-udevd/run
+++ b/host/rootfs/image/etc/s6-rc/systemd-udevd/run
@@ -1,7 +1,8 @@
-#!/bin/execlineb -WP
+#!/bin/execlineb -WS1
 # 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 -- $1
 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..0c16cc8060762eac3a91a49b54048407dfbe25c5
--- /dev/null
+++ b/host/rootfs/image/etc/s6-rc/weston/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-rc/weston/run b/host/rootfs/image/etc/s6-rc/weston/run
old mode 100644
new mode 100755
index fd59586c719391deb546c29578341e16a61ed4ce..6794a3ed729c9b99336cb64df95bbb3eba644582
--- a/host/rootfs/image/etc/s6-rc/weston/run
+++ b/host/rootfs/image/etc/s6-rc/weston/run
@@ -1,7 +1,8 @@
-#!/bin/execlineb -WP
+#!/bin/execlineb -WS1
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2021, 2025 Alyssa Ross <hi@alyssa.is>
 
+cgroup-setup -- $1
 importas -Siu WAYLAND_DISPLAY
 
 piperw 4 3
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..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..cd77becc0b40bfdcd983e53b63b7c21e4308d5fc
--- /dev/null
+++ b/tools/cgroup-setup/src/cgroup.rs
@@ -0,0 +1,181 @@
+// 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,
+}
+
+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..358703a73fcbdc38794312fe3987e733f37c2df0
--- /dev/null
+++ b/tools/cgroup-setup/src/main.rs
@@ -0,0 +1,196 @@
+// 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 main() {
+    let mut args = std::env::args_os();
+    let Some(prog_name) = args.next() else {
+        fail!("No command line arguments (argv[0] is NULL)");
+    };
+    let mut cgroup_relative_path = args.next();
+    if cgroup_relative_path.as_deref() == Some(OsStr::from_bytes(b"--")) {
+        cgroup_relative_path = args.next();
+    }
+
+    let Some(cgroup_relative_path) = cgroup_relative_path else {
+        fail!("{prog_name:?}: Have no positional arguments, expect at least 1")
+    };
+    let mut cgroup_relative_path = cgroup_relative_path.into_vec();
+
+    // slow but we do not care
+    if cgroup_relative_path.len() > 247 {
+        fail!("{prog_name:?}: Cgroup name {cgroup_relative_path:?} too long");
+    }
+    if cgroup_relative_path.is_empty() {
+        fail!("{prog_name:?}: Cgroup name {cgroup_relative_path:?} empty");
+    }
+    if cgroup_relative_path[0] == b'-' {
+        fail!("{prog_name:?}: Cgroup name {cgroup_relative_path:?} starts with '-'");
+    }
+    for &i in &cgroup_relative_path {
+        match i {
+            b'/' => fail!(
+                "{prog_name:?}: Cgroup name {cgroup_relative_path:?} \
+contains /"
+            ),
+            b'$' => fail!(
+                "{prog_name:?}: Cgroup name {cgroup_relative_path:?} \
+contains $: did you forget an execline substitution?"
+            ),
+            b'!'..=b'~' => {}
+            _ => fail!(
+                "{prog_name:?}: Cgroup name {cgroup_relative_path:?} \
+contains space, non-ASCII character, or control character (byte {i})"
+            ),
+        }
+    }
+    cgroup_relative_path.extend_from_slice(b".service");
+    let cgroup_relative_path = Path::new(OsStr::from_bytes(&cgroup_relative_path));
+
+    let local_cgroup = std::fs::read("/proc/thread-self/cgroup")
+        .unwrap_or_else(|e| fail!("{prog_name:?}: cannot read /proc/thread-self/cgroup: {e}"));
+
+    if !local_cgroup.starts_with(b"0::/")
+        || !local_cgroup.ends_with(b"\n")
+        || local_cgroup.contains(&b'\0')
+    {
+        fail!("{prog_name:?}: 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,
+    )
+    .unwrap_or_else(|e| fail!("{prog_name:?}: cannot open /sys/fs/cgroup: {e}"));
+
+    let cgroup = match cgroup::LeafCgroup::open_cgroup(cgroup_root.as_fd(), &local_cgroup) {
+        Ok(e) => e,
+        Err(e) => fail!(
+            "{prog_name:?}: Failed to open {}: {e:?}",
+            local_cgroup.display()
+        ),
+    };
+
+    match rustix::fs::flock(cgroup.as_fd(), FlockOperation::LockExclusive) {
+        Ok(()) => {}
+        Err(e) => fail!("{prog_name:?}: cannot lock cgroup: {e}"),
+    }
+    purge_cgroup(&prog_name, &cgroup, cgroup_relative_path);
+    let Some(program_name) = args.next() else {
+        return;
+    };
+    if let Err(e) = cgroup.make_child(cgroup_relative_path) {
+        fail!("{prog_name:?}: cannot create child cgroup: {e}")
+    }
+    let child = cgroup
+        .open_cgroup_at(cgroup_relative_path)
+        .unwrap_or_else(|e| {
+            fail!("{prog_name:?}: cannot create child cgroup {cgroup_relative_path:?}: {e}")
+        });
+    child
+        .make_child(Path::new("@inner.service"))
+        .unwrap_or_else(|e| {
+            fail!("{prog_name:?}: cannot create child cgroup {cgroup_relative_path:?}/@inner.service: {e}")
+        });
+    let child = child
+        .open_cgroup_at(Path::new("@inner.service"))
+        .unwrap_or_else(|e| {
+            fail!("{prog_name:?}: cannot open child cgroup {cgroup_relative_path:?}/@inner.service: {e}")
+        });
+
+    // SAFETY: safe FFI call
+    let pid = unsafe { libc::getpid() };
+    write_cgroup_value(&prog_name, &child, "cgroup.procs", &pid.to_string());
+
+    let e = std::process::Command::new(&program_name).args(args).exec();
+    fail!(
+        "{prog_name:?}: cannot spawn child {:?}: {}",
+        program_name,
+        e
+    );
+}
+
+fn write_cgroup_value(prog_name: &OsStr, 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!("{prog_name:?}: cannot open {:?}: {}", path, e));
+    let () = File::from(fd)
+        .write_all(value.as_bytes())
+        .unwrap_or_else(|e| {
+            fail!(
+                "{prog_name:?}: Cannot write {:?} to {:?}: {}",
+                name,
+                path,
+                e
+            )
+        });
+}
+
+fn purge_cgroup(prog_name: &OsStr, cgroup: &cgroup::LeafCgroup, cgroup_relative_path: &Path) {
+    let child = match cgroup.open_cgroup_at(cgroup_relative_path) {
+        Ok(child_cgroup) => child_cgroup,
+        Err(Errno::NOENT) => return,
+        Err(other) => {
+            fail!("{prog_name:?}: cannot open child cgroup {cgroup_relative_path:?}: {other}")
+        }
+    };
+    child.kill_processes().unwrap_or_else(|e| {
+        fail!("{prog_name:?}: cannot kill programs in {cgroup_relative_path:?}: {e}")
+    });
+
+    cgroup
+        .delete_child(cgroup_relative_path)
+        .unwrap_or_else(|e| {
+            fail!("{prog_name:?}: delete child cgroup {cgroup_relative_path:?}: {e}")
+        });
+}

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

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


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* Re: [PATCH v2] Set up control groups for most services
  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
  0 siblings, 2 replies; 8+ messages in thread
From: Alyssa Ross @ 2026-06-24 12:13 UTC (permalink / raw)
  To: Demi Marie Obenour; +Cc: Spectrum OS Development

[-- Attachment #1: Type: text/plain, Size: 21991 bytes --]

Demi Marie Obenour <demiobenour@gmail.com> writes:

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

Perhaps you could include some more details of how the cgroup hierarchy
is supposed to be set up and why, either in the patch body or in
documentation?

As I understood it, the point of using cgroups was that we could bundle
all services for a VM, including the VMM, into a single cgroup, but I
don't see that here, just a pure translation of the service hierarchy
(which the VMM might not even be part of).  Are per-VM cgroups coming
later?

> 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

Alignment is off here.  The rest of the file uses tabs at 8 characters.
(I know, I know, but it's traditional.)

> 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

What benefit does this cgroup provide?

> 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

We're putting supervisors of instances services in a cgroup?  Can you
explain to me why that's useful?

> 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..89676ac91d3e22db7726fc869a065137b031d0be 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 {

This seems to get stuck in udevadm wait?

> 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"

What's with all this stuff?  Why do we set it only for this program, and
not for any others?  Please don't introduce divergence in build settings
between programs without a good reason.

> diff --git a/tools/cgroup-setup/src/cgroup.rs b/tools/cgroup-setup/src/cgroup.rs
> new file mode 100644
> index 0000000000000000000000000000000000000000..cd77becc0b40bfdcd983e53b63b7c21e4308d5fc
> --- /dev/null
> +++ b/tools/cgroup-setup/src/cgroup.rs
> @@ -0,0 +1,181 @@
> +// 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,

root_fd would be a clearer name, if I'm understanding correctly.

> +}
> +
> +enum Access {
> +    Read,
> +    Write,
> +}

Given this isn't public, it doesn't seem to add any value over passing
around OFlags::RDONLY and OFlags::WRONLY directly.

> +
> +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");
> +            }

Shouldn't we start polling before writing?  Otherwise we might miss the
notification, if it comes in between the write and the poll.

> +            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" {

Could do this in one line, and avoid the labelled break:

if v.split(|&c| c == b'\n').any(|line| line == b"populated 0") {

(I think "line" is a clearer name than "substr".)

> +                    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,

Stray comment.

> +            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..358703a73fcbdc38794312fe3987e733f37c2df0
> --- /dev/null
> +++ b/tools/cgroup-setup/src/main.rs
> @@ -0,0 +1,196 @@
> +// 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)}
> +    };
> +}

I'd find the error handling Rustier if we did error returns with the ?
operator, like how mount-flatpak does it.  The error type can just be
String.

> +
> +fn main() {
> +    let mut args = std::env::args_os();
> +    let Some(prog_name) = args.next() else {
> +        fail!("No command line arguments (argv[0] is NULL)");
> +    };
> +    let mut cgroup_relative_path = args.next();
> +    if cgroup_relative_path.as_deref() == Some(OsStr::from_bytes(b"--")) {
> +        cgroup_relative_path = args.next();
> +    }

We could just not support -- and simplify invocation, right?

> +
> +    let Some(cgroup_relative_path) = cgroup_relative_path else {
> +        fail!("{prog_name:?}: Have no positional arguments, expect at least 1")
> +    };
> +    let mut cgroup_relative_path = cgroup_relative_path.into_vec();
> +
> +    // slow but we do not care

Slow?

> +    if cgroup_relative_path.len() > 247 {

This magic number could perhaps use a name.

> +        fail!("{prog_name:?}: Cgroup name {cgroup_relative_path:?} too long");
> +    }
> +    if cgroup_relative_path.is_empty() {
> +        fail!("{prog_name:?}: Cgroup name {cgroup_relative_path:?} empty");
> +    }
> +    if cgroup_relative_path[0] == b'-' {
> +        fail!("{prog_name:?}: Cgroup name {cgroup_relative_path:?} starts with '-'");
> +    }

Is that a problem?

> +    for &i in &cgroup_relative_path {
> +        match i {
> +            b'/' => fail!(
> +                "{prog_name:?}: Cgroup name {cgroup_relative_path:?} \
> +contains /"
> +            ),
> +            b'$' => fail!(
> +                "{prog_name:?}: Cgroup name {cgroup_relative_path:?} \
> +contains $: did you forget an execline substitution?"
> +            ),
> +            b'!'..=b'~' => {}
> +            _ => fail!(
> +                "{prog_name:?}: Cgroup name {cgroup_relative_path:?} \
> +contains space, non-ASCII character, or control character (byte {i})"
> +            ),

Surely every kernel interface we're going to use is 8-bit clean.  If
this is about log viewing again, I really don't like it.  Even if we're
careful about it in first-party code (which itself is a big ask), we
can't expect it of every other package that might log something.  People
viewing logs have to use robust tools to do so regardless, or we have to
mitigate this somewhere centrally, e.g. in s6 log.  Doing this piecemeal
does not meaningfully mitigate the problem, and arguably creates a false
sense of security.

> +        }
> +    }
> +    cgroup_relative_path.extend_from_slice(b".service");

What do we need the suffix for?

> +    let cgroup_relative_path = Path::new(OsStr::from_bytes(&cgroup_relative_path));
> +
> +    let local_cgroup = std::fs::read("/proc/thread-self/cgroup")

Ooc, why /proc/thread-self instead of /proc/self?

> +        .unwrap_or_else(|e| fail!("{prog_name:?}: cannot read /proc/thread-self/cgroup: {e}"));
> +
> +    if !local_cgroup.starts_with(b"0::/")
> +        || !local_cgroup.ends_with(b"\n")
> +        || local_cgroup.contains(&b'\0')
> +    {
> +        fail!("{prog_name:?}: Kernel bug: Bad contents of /proc/thread-self/cgroup");
> +    }

We are not a kernel fuzzer.  We can't reasonably write programs that
have to anticipate kernel contract violations.

> +
> +    let mut local_cgroup = PathBuf::from(<OsString as OsStringExt>::from_vec(
> +        local_cgroup[4..local_cgroup.len() - 1].to_owned(),

… but maybe we could make this clearer, and still satisfy your instincts
to validate, by using slice::strip_prefix and slice::strip_suffix,
unwrapping the results?

> +    ));
> +
> +    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();
> +            }
> +        }
> +    }

Could we not simplify this with local_cgroup.file_name()?  If it's None,
it's the root directory; otherwise it's the equivalent of normal.

> +
> +    if local_cgroup.as_os_str().is_empty() {
> +        local_cgroup = PathBuf::from(".");
> +    }

Path::is_empty is stable in 1.98.  Could we mention that in a TODO/FIXME
comment so we can use it when available?

> +
> +    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,
> +    )
> +    .unwrap_or_else(|e| fail!("{prog_name:?}: cannot open /sys/fs/cgroup: {e}"));

Why do we need to use openat2 for this, but not for reading
/proc/thread-self/cgroup?  I'm a bit surprised to see openat2 here at
all, since only root could manipulate these paths, in which case they
have no need to use this program as a confused deputy, right?

If openat2 is to stay, isn't OFlags::NOFOLLOW redundant with
ResolveFlags::NO_SYMLINKS?

> +
> +    let cgroup = match cgroup::LeafCgroup::open_cgroup(cgroup_root.as_fd(), &local_cgroup) {
> +        Ok(e) => e,
> +        Err(e) => fail!(
> +            "{prog_name:?}: Failed to open {}: {e:?}",
> +            local_cgroup.display()
> +        ),
> +    };
> +
> +    match rustix::fs::flock(cgroup.as_fd(), FlockOperation::LockExclusive) {
> +        Ok(()) => {}
> +        Err(e) => fail!("{prog_name:?}: cannot lock cgroup: {e}"),
> +    }

Would std::fs::File::lock not be a bit Rustier?

> +    purge_cgroup(&prog_name, &cgroup, cgroup_relative_path);

I find it a little weird that we run this program in both service
startup and finish, when there are really two completely separable parts
to it, right?  Why is purging not a separate program that only runs in
finish?

> +    let Some(program_name) = args.next() else {
> +        return;
> +    };

This could be moved closer to where it's used.

> +    if let Err(e) = cgroup.make_child(cgroup_relative_path) {
> +        fail!("{prog_name:?}: cannot create child cgroup: {e}")
> +    }
> +    let child = cgroup
> +        .open_cgroup_at(cgroup_relative_path)
> +        .unwrap_or_else(|e| {
> +            fail!("{prog_name:?}: cannot create child cgroup {cgroup_relative_path:?}: {e}")
> +        });
> +    child
> +        .make_child(Path::new("@inner.service"))
> +        .unwrap_or_else(|e| {
> +            fail!("{prog_name:?}: cannot create child cgroup {cgroup_relative_path:?}/@inner.service: {e}")
> +        });
> +    let child = child
> +        .open_cgroup_at(Path::new("@inner.service"))
> +        .unwrap_or_else(|e| {
> +            fail!("{prog_name:?}: cannot open child cgroup {cgroup_relative_path:?}/@inner.service: {e}")
> +        });

This would really benefit from having been written up, so I don't have
to guess what it's for.  We only really need it for supervisors, right?

> +
> +    // SAFETY: safe FFI call
> +    let pid = unsafe { libc::getpid() };

Not std::process::id()?

> +    write_cgroup_value(&prog_name, &child, "cgroup.procs", &pid.to_string());
> +
> +    let e = std::process::Command::new(&program_name).args(args).exec();
> +    fail!(
> +        "{prog_name:?}: cannot spawn child {:?}: {}",
> +        program_name,
> +        e
> +    );
> +}
> +
> +fn write_cgroup_value(prog_name: &OsStr, child: &cgroup::LeafCgroup, name: &str, value: &str) {

Perhaps it would be nicer for this to be a method on LeafCgroup?

> +    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!("{prog_name:?}: cannot open {:?}: {}", path, e));
> +    let () = File::from(fd)
> +        .write_all(value.as_bytes())
> +        .unwrap_or_else(|e| {
> +            fail!(
> +                "{prog_name:?}: Cannot write {:?} to {:?}: {}",
> +                name,
> +                path,
> +                e
> +            )
> +        });

Similarly to above, if we don't need openat2, this could be done more
nicely as std::fs::write.

> +}
> +
> +fn purge_cgroup(prog_name: &OsStr, cgroup: &cgroup::LeafCgroup, cgroup_relative_path: &Path) {
> +    let child = match cgroup.open_cgroup_at(cgroup_relative_path) {
> +        Ok(child_cgroup) => child_cgroup,
> +        Err(Errno::NOENT) => return,
> +        Err(other) => {
> +            fail!("{prog_name:?}: cannot open child cgroup {cgroup_relative_path:?}: {other}")
> +        }
> +    };
> +    child.kill_processes().unwrap_or_else(|e| {
> +        fail!("{prog_name:?}: cannot kill programs in {cgroup_relative_path:?}: {e}")
> +    });
> +
> +    cgroup
> +        .delete_child(cgroup_relative_path)
> +        .unwrap_or_else(|e| {
> +            fail!("{prog_name:?}: delete child cgroup {cgroup_relative_path:?}: {e}")
> +        });
> +}

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v2] Set up control groups for most services
  2026-06-24 12:13   ` Alyssa Ross
@ 2026-06-24 12:36     ` Alyssa Ross
  2026-06-25  2:03     ` Demi Marie Obenour
  1 sibling, 0 replies; 8+ messages in thread
From: Alyssa Ross @ 2026-06-24 12:36 UTC (permalink / raw)
  To: Demi Marie Obenour; +Cc: Spectrum OS Development

[-- Attachment #1: Type: text/plain, Size: 996 bytes --]

Alyssa Ross <hi@alyssa.is> writes:

> Demi Marie Obenour <demiobenour@gmail.com> writes:
>
>> 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..89676ac91d3e22db7726fc869a065137b031d0be 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 {
>
> This seems to get stuck in udevadm wait?

Actually I see the same without this patch applied, so ignore this one.

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v2] Set up control groups for most services
  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:49       ` Alyssa Ross
  1 sibling, 2 replies; 8+ messages in thread
From: Demi Marie Obenour @ 2026-06-25  2:03 UTC (permalink / raw)
  To: Alyssa Ross; +Cc: Spectrum OS Development


[-- Attachment #1.1.1: Type: text/plain, Size: 25750 bytes --]

On 6/24/26 08:13, Alyssa Ross wrote:
> Demi Marie Obenour <demiobenour@gmail.com> writes:
> 
>> 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.
>>
> 
> Perhaps you could include some more details of how the cgroup hierarchy
> is supposed to be set up and why, either in the patch body or in
> documentation?

Will fix in v3.

> As I understood it, the point of using cgroups was that we could bundle
> all services for a VM, including the VMM, into a single cgroup, but I
> don't see that here, just a pure translation of the service hierarchy
> (which the VMM might not even be part of).  Are per-VM cgroups coming
> later?

I think it would be significantly simpler to have the VMM be
just another VM service.  This would naturally put it under the
vm-services cgroup, solving this problem.  Since this would mostly
involve changes to execline scripting, I think it would be better if
you wrote this code.

>> 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
> 
> Alignment is off here.  The rest of the file uses tabs at 8 characters.
> (I know, I know, but it's traditional.)
> 
>> 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
> 
> What benefit does this cgroup provide?

None, I'll remove it.

The only requirement of the current code is that if a service is under
a cgroup, its parent should *also* be under a cgroup.  Otherwise you
can get conflicting cgroup paths.

>> 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
> 
> We're putting supervisors of instances services in a cgroup?  Can you
> explain to me why that's useful?

This allows configuring limits that apply to the whole instance, and
means that the whole instance can be reliably killed.  Services spawned
by the instance will be in their own sub-cgroups, so one can apply
separate limits to them so long as those limits do not exceed those
of the containing cgroup.

The hierarchy works like this:

/sys/fs/cgroup
  service1.service
    @inner.service # processes go here
    service2.service
      @inner.service # and here
      service3.service
        @inner.service # and here

You can apply resource limits at any level of the hierarchy.  Limits in
nested cgroups can be smaller but not larger.  Terminating processes
in a cgroup with cgroup.kill also terminates all processes in child
cgroups.  After terminating the processes in a cgroup, cgroup-setup
will delete everything in the hierarchy recursively.

>> 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"
> 
> What's with all this stuff?  Why do we set it only for this program, and
> not for any others?  Please don't introduce divergence in build settings
> between programs without a good reason.

I was trying to reduce the binary size as it seemed rather bloated.
I'll leave them out 

>> diff --git a/tools/cgroup-setup/src/cgroup.rs b/tools/cgroup-setup/src/cgroup.rs
>> new file mode 100644
>> index 0000000000000000000000000000000000000000..cd77becc0b40bfdcd983e53b63b7c21e4308d5fc
>> --- /dev/null
>> +++ b/tools/cgroup-setup/src/cgroup.rs
>> @@ -0,0 +1,181 @@
>> +// 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,
> 
> root_fd would be a clearer name, if I'm understanding correctly.

It's the FD for the cgroup directory, but not for the root of the
cgroup tree.

>> +}
>> +
>> +enum Access {
>> +    Read,
>> +    Write,
>> +}
> 
> Given this isn't public, it doesn't seem to add any value over passing
> around OFlags::RDONLY and OFlags::WRONLY directly.

Will change in v3.

>> +
>> +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");
>> +            }
> 
> Shouldn't we start polling before writing?  Otherwise we might miss the
> notification, if it comes in between the write and the poll.

I'll test this, but I think it shouldn't matter.  If it does matter, I'll
need to switch to epoll (or io_uring, but that's overkill here).

>> +            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" {
> 
> Could do this in one line, and avoid the labelled break:
> 
> if v.split(|&c| c == b'\n').any(|line| line == b"populated 0") {
> 
> (I think "line" is a clearer name than "substr".)

Will fix in v3.

>> +                    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,
> 
> Stray comment.

Will fix in v3.

>> +            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..358703a73fcbdc38794312fe3987e733f37c2df0
>> --- /dev/null
>> +++ b/tools/cgroup-setup/src/main.rs
>> @@ -0,0 +1,196 @@
>> +// 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)}
>> +    };
>> +}
> 
> I'd find the error handling Rustier if we did error returns with the ?
> operator, like how mount-flatpak does it.  The error type can just be
> String.

Sure!

>> +
>> +fn main() {
>> +    let mut args = std::env::args_os();
>> +    let Some(prog_name) = args.next() else {
>> +        fail!("No command line arguments (argv[0] is NULL)");
>> +    };
>> +    let mut cgroup_relative_path = args.next();
>> +    if cgroup_relative_path.as_deref() == Some(OsStr::from_bytes(b"--")) {
>> +        cgroup_relative_path = args.next();
>> +    }
> 
> We could just not support -- and simplify invocation, right?

Only if we never want to support any options in the future.

>> +
>> +    let Some(cgroup_relative_path) = cgroup_relative_path else {
>> +        fail!("{prog_name:?}: Have no positional arguments, expect at least 1")
>> +    };
>> +    let mut cgroup_relative_path = cgroup_relative_path.into_vec();
>> +
>> +    // slow but we do not care
> 
> Slow?

O(n) for a size-n Vec.  Not something you want in a hot path, but fine here.

>> +    if cgroup_relative_path.len() > 247 {
> 
> This magic number could perhaps use a name.

Sure.

>> +        fail!("{prog_name:?}: Cgroup name {cgroup_relative_path:?} too long");
>> +    }
>> +    if cgroup_relative_path.is_empty() {
>> +        fail!("{prog_name:?}: Cgroup name {cgroup_relative_path:?} empty");
>> +    }
>> +    if cgroup_relative_path[0] == b'-' {
>> +        fail!("{prog_name:?}: Cgroup name {cgroup_relative_path:?} starts with '-'");
>> +    }
> 
> Is that a problem?

In theory, no, but it's probably a bug in the caller.

>> +    for &i in &cgroup_relative_path {
>> +        match i {
>> +            b'/' => fail!(
>> +                "{prog_name:?}: Cgroup name {cgroup_relative_path:?} \
>> +contains /"
>> +            ),
>> +            b'$' => fail!(
>> +                "{prog_name:?}: Cgroup name {cgroup_relative_path:?} \
>> +contains $: did you forget an execline substitution?"
>> +            ),
>> +            b'!'..=b'~' => {}
>> +            _ => fail!(
>> +                "{prog_name:?}: Cgroup name {cgroup_relative_path:?} \
>> +contains space, non-ASCII character, or control character (byte {i})"
>> +            ),
> 
> Surely every kernel interface we're going to use is 8-bit clean.  If
> this is about log viewing again, I really don't like it.  Even if we're
> careful about it in first-party code (which itself is a big ask), we
> can't expect it of every other package that might log something.  People
> viewing logs have to use robust tools to do so regardless, or we have to
> mitigate this somewhere centrally, e.g. in s6 log.  Doing this piecemeal
> does not meaningfully mitigate the problem, and arguably creates a false
> sense of security.

This was inspired by systemd, which has restrictions on what characters
are allowed in service names.  Also, restricting allowed characters
makes it easier to find bugs, such as "${a} ${b}" instead of "${a}"
"${b}" in an execline script.

Forbidding '$' is very much intentional, and is inspired by me making
the exact mistake described by the error message.  It also means that
certain names can be reserved for internal use.

>> +        }
>> +    }
>> +    cgroup_relative_path.extend_from_slice(b".service");
> 
> What do we need the suffix for?

The kernel may add new control files at any time.  However, it will
never use the .service suffix to avoid breaking systemd.  Therefore, I
chose to use that suffix to avoid breaking in future kernel releases.

>> +    let cgroup_relative_path = Path::new(OsStr::from_bytes(&cgroup_relative_path));
>> +
>> +    let local_cgroup = std::fs::read("/proc/thread-self/cgroup")
> 
> Ooc, why /proc/thread-self instead of /proc/self?

Why not?  The two are aliases in this case, but in general /proc/thread-self is likely the better choice.

>> +        .unwrap_or_else(|e| fail!("{prog_name:?}: cannot read /proc/thread-self/cgroup: {e}"));
>> +
>> +    if !local_cgroup.starts_with(b"0::/")
>> +        || !local_cgroup.ends_with(b"\n")
>> +        || local_cgroup.contains(&b'\0')
>> +    {
>> +        fail!("{prog_name:?}: Kernel bug: Bad contents of /proc/thread-self/cgroup");
>> +    }
> 
> We are not a kernel fuzzer.  We can't reasonably write programs that
> have to anticipate kernel contract violations.

Fair!

>> +
>> +    let mut local_cgroup = PathBuf::from(<OsString as OsStringExt>::from_vec(
>> +        local_cgroup[4..local_cgroup.len() - 1].to_owned(),
> 
> … but maybe we could make this clearer, and still satisfy your instincts
> to validate, by using slice::strip_prefix and slice::strip_suffix,
> unwrapping the results?

That's even better, because it's much easier to read than magic numbers.

>> +    ));
>> +
>> +    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();
>> +            }
>> +        }
>> +    }
> 
> Could we not simplify this with local_cgroup.file_name()?  If it's None,
> it's the root directory; otherwise it's the equivalent of normal.

Yup!

>> +
>> +    if local_cgroup.as_os_str().is_empty() {
>> +        local_cgroup = PathBuf::from(".");
>> +    }
> 
> Path::is_empty is stable in 1.98.  Could we mention that in a TODO/FIXME
> comment so we can use it when available?

Will do.

>> +    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,
>> +    )
>> +    .unwrap_or_else(|e| fail!("{prog_name:?}: cannot open /sys/fs/cgroup: {e}"));
> 
> Why do we need to use openat2 for this, but not for reading
> /proc/thread-self/cgroup?  I'm a bit surprised to see openat2 here at
> all, since only root could manipulate these paths, in which case they
> have no need to use this program as a confused deputy, right?
> 
> If openat2 is to stay, isn't OFlags::NOFOLLOW redundant with
> ResolveFlags::NO_SYMLINKS?

openat2() is indeed pointless here.

>> +
>> +    let cgroup = match cgroup::LeafCgroup::open_cgroup(cgroup_root.as_fd(), &local_cgroup) {
>> +        Ok(e) => e,
>> +        Err(e) => fail!(
>> +            "{prog_name:?}: Failed to open {}: {e:?}",
>> +            local_cgroup.display()
>> +        ),
>> +    };
>> +
>> +    match rustix::fs::flock(cgroup.as_fd(), FlockOperation::LockExclusive) {
>> +        Ok(()) => {}
>> +        Err(e) => fail!("{prog_name:?}: cannot lock cgroup: {e}"),
>> +    }
> 
> Would std::fs::File::lock not be a bit Rustier?

Yes :).  I know how to 
	
>> +    purge_cgroup(&prog_name, &cgroup, cgroup_relative_path);
> 
> I find it a little weird that we run this program in both service
> startup and finish, when there are really two completely separable parts
> to it, right?  Why is purging not a separate program that only runs in
> finish?

The finish script might fail, time out, etc.  Purging at startup
ensures that any previous cgroup state (like attached BPF programs)
is removed, and it ensures that the program this program invokes will
not run concurrently.  It's just more robust overall.

Purging is idempotent, and purging an already-purged cgroup is very
cheap.  Also, this allows using cgroup-setup without a finish script,
while still ensuring that if the service restarts there are no stale
processes left behind.

>> +    let Some(program_name) = args.next() else {
>> +        return;
>> +    };
> 
> This could be moved closer to where it's used.

Will fix in v3.

>> +    if let Err(e) = cgroup.make_child(cgroup_relative_path) {
>> +        fail!("{prog_name:?}: cannot create child cgroup: {e}")
>> +    }
>> +    let child = cgroup
>> +        .open_cgroup_at(cgroup_relative_path)
>> +        .unwrap_or_else(|e| {
>> +            fail!("{prog_name:?}: cannot create child cgroup {cgroup_relative_path:?}: {e}")
>> +        });
>> +    child
>> +        .make_child(Path::new("@inner.service"))
>> +        .unwrap_or_else(|e| {
>> +            fail!("{prog_name:?}: cannot create child cgroup {cgroup_relative_path:?}/@inner.service: {e}")
>> +        });
>> +    let child = child
>> +        .open_cgroup_at(Path::new("@inner.service"))
>> +        .unwrap_or_else(|e| {
>> +            fail!("{prog_name:?}: cannot open child cgroup {cgroup_relative_path:?}/@inner.service: {e}")
>> +        });
> 
> This would really benefit from having been written up, so I don't have
> to guess what it's for.  We only really need it for supervisors, right?

It's only *needed* when the program might be called recursively,
but it's easier to just use it everywhere.

>> +
>> +    // SAFETY: safe FFI call
>> +    let pid = unsafe { libc::getpid() };
> 
> Not std::process::id()?

Only because I was not aware of it.

>> +    write_cgroup_value(&prog_name, &child, "cgroup.procs", &pid.to_string());
>> +
>> +    let e = std::process::Command::new(&program_name).args(args).exec();
>> +    fail!(
>> +        "{prog_name:?}: cannot spawn child {:?}: {}",
>> +        program_name,
>> +        e
>> +    );
>> +}
>> +
>> +fn write_cgroup_value(prog_name: &OsStr, child: &cgroup::LeafCgroup, name: &str, value: &str) {
> 
> Perhaps it would be nicer for this to be a method on LeafCgroup?

Sure!

>> +    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!("{prog_name:?}: cannot open {:?}: {}", path, e));
>> +    let () = File::from(fd)
>> +        .write_all(value.as_bytes())
>> +        .unwrap_or_else(|e| {
>> +            fail!(
>> +                "{prog_name:?}: Cannot write {:?} to {:?}: {}",
>> +                name,
>> +                path,
>> +                e
>> +            )
>> +        });
> 
> Similarly to above, if we don't need openat2, this could be done more
> nicely as std::fs::write.

std::fs doesn't even expose openat(), and I'm much more comfortable
using a directory FD here.  This is a fairly severe limitation in
the stdlib.  Fixing it requires using the Native API on Windows,
but that's easy enough.

>> +}
>> +
>> +fn purge_cgroup(prog_name: &OsStr, cgroup: &cgroup::LeafCgroup, cgroup_relative_path: &Path) {
>> +    let child = match cgroup.open_cgroup_at(cgroup_relative_path) {
>> +        Ok(child_cgroup) => child_cgroup,
>> +        Err(Errno::NOENT) => return,
>> +        Err(other) => {
>> +            fail!("{prog_name:?}: cannot open child cgroup {cgroup_relative_path:?}: {other}")
>> +        }
>> +    };
>> +    child.kill_processes().unwrap_or_else(|e| {
>> +        fail!("{prog_name:?}: cannot kill programs in {cgroup_relative_path:?}: {e}")
>> +    });
>> +
>> +    cgroup
>> +        .delete_child(cgroup_relative_path)
>> +        .unwrap_or_else(|e| {
>> +            fail!("{prog_name:?}: delete child cgroup {cgroup_relative_path:?}: {e}")
>> +        });
>> +}


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

[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]

[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v2] Set up control groups for most services
  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
  1 sibling, 1 reply; 8+ messages in thread
From: Demi Marie Obenour @ 2026-06-25  3:03 UTC (permalink / raw)
  To: Alyssa Ross; +Cc: Spectrum OS Development


[-- Attachment #1.1.1: Type: text/plain, Size: 6712 bytes --]

On 6/24/26 22:03, Demi Marie Obenour wrote:
> On 6/24/26 08:13, Alyssa Ross wrote:
>> Demi Marie Obenour <demiobenour@gmail.com> writes:
>>
>>> 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.
>>>
>>
>> Perhaps you could include some more details of how the cgroup hierarchy
>> is supposed to be set up and why, either in the patch body or in
>> documentation?
> 
> Will fix in v3.
> 
>> As I understood it, the point of using cgroups was that we could bundle
>> all services for a VM, including the VMM, into a single cgroup, but I
>> don't see that here, just a pure translation of the service hierarchy
>> (which the VMM might not even be part of).  Are per-VM cgroups coming
>> later?
> 
> I think it would be significantly simpler to have the VMM be
> just another VM service.  This would naturally put it under the
> vm-services cgroup, solving this problem.  Since this would mostly
> involve changes to execline scripting, I think it would be better if
> you wrote this code.
> 
>>> 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
>>
>> Alignment is off here.  The rest of the file uses tabs at 8 characters.
>> (I know, I know, but it's traditional.)
>>
>>> 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
>>
>> What benefit does this cgroup provide?
> 
> None, I'll remove it.
> 
> The only requirement of the current code is that if a service is under
> a cgroup, its parent should *also* be under a cgroup.  Otherwise you
> can get conflicting cgroup paths.
> 
>>> 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
>>
>> We're putting supervisors of instances services in a cgroup?  Can you
>> explain to me why that's useful?
> 
> This allows configuring limits that apply to the whole instance, and
> means that the whole instance can be reliably killed.  Services spawned
> by the instance will be in their own sub-cgroups, so one can apply
> separate limits to them so long as those limits do not exceed those
> of the containing cgroup.
> 
> The hierarchy works like this:
> 
> /sys/fs/cgroup
>   service1.service
>     @inner.service # processes go here
>     service2.service
>       @inner.service # and here
>       service3.service
>         @inner.service # and here
> 
> You can apply resource limits at any level of the hierarchy.  Limits in
> nested cgroups can be smaller but not larger.  Terminating processes
> in a cgroup with cgroup.kill also terminates all processes in child
> cgroups.  After terminating the processes in a cgroup, cgroup-setup
> will delete everything in the hierarchy recursively.

More generally:

The design I implemented is based on a model where cgroups and services
are very tightly coupled: most services have an associated cgroup and
every cgroup is attached to a service.  This is like systemd service
units, except that it can be nested many layers deep.

Since a VM may have many processes that serve it, this model requires
that all of these processes be under a single service.  In this case,
that service would be the vm-services service.

That said, I'm not sure which of these service, if any, should be
automatically restarted if they crash.  Automatic restart of services
is actually very useful for attackers, as it lets them retry an exploit
over and over until they get the memory layout they need for success.

If you restart the VMM, you might as well restart every other
service as well.  Unless Cloud Hypervisor automatically reconnects to
vhost-user devices (and I don't think it does), there's no point in
restarting virtiofsd or crosvm without either re-adding the device
or restarting the VMM.  Even if you did re-add the device, I don't
know if the guest would be able to recover.

Without automatic restart, there's no real reason to use s6 to manage
the per-VM services.  s6 assumes you do want to restart services
that crash.

An alternative approach is to have cgroups be freestanding objects
that exist independently of whether there are processes in them.
This corresponds to systemd scope units.  It's not hard to implement
this: just disable automatic purging and pass the full path to the
cgroup.  But it requires each VM service to have identical resource
limits, whereas one typically wants per-process limits.  For instance,
none of the services should be able to create child processes, and
per-service cgroups make this easy: just set the maximum amount of
processes in the cgroup to 1.

Incidentally, this works very well with PID namespaces.  One can have
the "main" process of the service run in a PID namespace, and have
all the others enter that namespace.  Then if the main process dies,
so do all of the others.
-- 
Sincerely,
Demi Marie Obenour (she/her/hers)

[-- Attachment #1.1.2: OpenPGP public key --]
[-- Type: application/pgp-keys, Size: 7253 bytes --]

[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v2] Set up control groups for most services
  2026-06-25  2:03     ` Demi Marie Obenour
  2026-06-25  3:03       ` Demi Marie Obenour
@ 2026-06-25  9:49       ` Alyssa Ross
  1 sibling, 0 replies; 8+ messages in thread
From: Alyssa Ross @ 2026-06-25  9:49 UTC (permalink / raw)
  To: Demi Marie Obenour; +Cc: Spectrum OS Development

[-- Attachment #1: Type: text/plain, Size: 25260 bytes --]

Demi Marie Obenour <demiobenour@gmail.com> writes:

> On 6/24/26 08:13, Alyssa Ross wrote:
>> Demi Marie Obenour <demiobenour@gmail.com> writes:
>> 
>>> 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.
>>>
>> 
>> Perhaps you could include some more details of how the cgroup hierarchy
>> is supposed to be set up and why, either in the patch body or in
>> documentation?
>
> Will fix in v3.
>
>> As I understood it, the point of using cgroups was that we could bundle
>> all services for a VM, including the VMM, into a single cgroup, but I
>> don't see that here, just a pure translation of the service hierarchy
>> (which the VMM might not even be part of).  Are per-VM cgroups coming
>> later?
>
> I think it would be significantly simpler to have the VMM be
> just another VM service.  This would naturally put it under the
> vm-services cgroup, solving this problem.  Since this would mostly
> involve changes to execline scripting, I think it would be better if
> you wrote this code.

No, I don't think it can work that way, because the VMM is in
increasingly many cases not a service at all, since it's run-once.
(Think about e.g. an appimage VM.)  This is why it's not under
vm-services today.  (I thought we'd discussed this before, but it would
have been months ago given how long ago we planned this…)

>>> 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
>> 
>> We're putting supervisors of instances services in a cgroup?  Can you
>> explain to me why that's useful?
>
> This allows configuring limits that apply to the whole instance, and
> means that the whole instance can be reliably killed.  Services spawned
> by the instance will be in their own sub-cgroups, so one can apply
> separate limits to them so long as those limits do not exceed those
> of the containing cgroup.
>
> The hierarchy works like this:
>
> /sys/fs/cgroup
>   service1.service
>     @inner.service # processes go here
>     service2.service
>       @inner.service # and here
>       service3.service
>         @inner.service # and here
>
> You can apply resource limits at any level of the hierarchy.  Limits in
> nested cgroups can be smaller but not larger.  Terminating processes
> in a cgroup with cgroup.kill also terminates all processes in child
> cgroups.  After terminating the processes in a cgroup, cgroup-setup
> will delete everything in the hierarchy recursively.

I just don't really understand when we'd want that.  Why would we want
to limit all instances of vm-services, or all instances of serial-getty?

>>> diff --git a/tools/cgroup-setup/src/cgroup.rs b/tools/cgroup-setup/src/cgroup.rs
>>> new file mode 100644
>>> index 0000000000000000000000000000000000000000..cd77becc0b40bfdcd983e53b63b7c21e4308d5fc
>>> --- /dev/null
>>> +++ b/tools/cgroup-setup/src/cgroup.rs
>>> @@ -0,0 +1,181 @@
>>> +// 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,
>> 
>> root_fd would be a clearer name, if I'm understanding correctly.
>
> It's the FD for the cgroup directory, but not for the root of the
> cgroup tree.

But if you open a child cgroup, it's the same FD, right?  So it's a root
of something.

>>> +}
>>> +
>>> +enum Access {
>>> +    Read,
>>> +    Write,
>>> +}
>> 
>> Given this isn't public, it doesn't seem to add any value over passing
>> around OFlags::RDONLY and OFlags::WRONLY directly.
>
> Will change in v3.
>
>>> +
>>> +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");
>>> +            }
>> 
>> Shouldn't we start polling before writing?  Otherwise we might miss the
>> notification, if it comes in between the write and the poll.
>
> I'll test this, but I think it shouldn't matter.  If it does matter, I'll
> need to switch to epoll (or io_uring, but that's overkill here).

Not sure it's something you'll be able to reproduce in a test.  We'd
need to understand the reason it could never happen.

>>> +            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" {
>> 
>> Could do this in one line, and avoid the labelled break:
>> 
>> if v.split(|&c| c == b'\n').any(|line| line == b"populated 0") {
>> 
>> (I think "line" is a clearer name than "substr".)
>
> Will fix in v3.
>
>>> +                    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,
>> 
>> Stray comment.
>
> Will fix in v3.
>
>>> +            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..358703a73fcbdc38794312fe3987e733f37c2df0
>>> --- /dev/null
>>> +++ b/tools/cgroup-setup/src/main.rs
>>> @@ -0,0 +1,196 @@
>>> +// 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)}
>>> +    };
>>> +}
>> 
>> I'd find the error handling Rustier if we did error returns with the ?
>> operator, like how mount-flatpak does it.  The error type can just be
>> String.
>
> Sure!
>
>>> +
>>> +fn main() {
>>> +    let mut args = std::env::args_os();
>>> +    let Some(prog_name) = args.next() else {
>>> +        fail!("No command line arguments (argv[0] is NULL)");
>>> +    };
>>> +    let mut cgroup_relative_path = args.next();
>>> +    if cgroup_relative_path.as_deref() == Some(OsStr::from_bytes(b"--")) {
>>> +        cgroup_relative_path = args.next();
>>> +    }
>> 
>> We could just not support -- and simplify invocation, right?
>
> Only if we never want to support any options in the future.

I guess it's nice for out-of-tree users, and it's a trivial amount of
code.

>>> +
>>> +    let Some(cgroup_relative_path) = cgroup_relative_path else {
>>> +        fail!("{prog_name:?}: Have no positional arguments, expect at least 1")
>>> +    };
>>> +    let mut cgroup_relative_path = cgroup_relative_path.into_vec();
>>> +
>>> +    // slow but we do not care
>> 
>> Slow?
>
> O(n) for a size-n Vec.  Not something you want in a hot path, but fine here.

How is getting the length of a Vec O(n)?  It stores it.

>>> +    if cgroup_relative_path.len() > 247 {
>> 
>> This magic number could perhaps use a name.
>
> Sure.
>
>>> +        fail!("{prog_name:?}: Cgroup name {cgroup_relative_path:?} too long");
>>> +    }
>>> +    if cgroup_relative_path.is_empty() {
>>> +        fail!("{prog_name:?}: Cgroup name {cgroup_relative_path:?} empty");
>>> +    }
>>> +    if cgroup_relative_path[0] == b'-' {
>>> +        fail!("{prog_name:?}: Cgroup name {cgroup_relative_path:?} starts with '-'");
>>> +    }
>> 
>> Is that a problem?
>
> In theory, no, but it's probably a bug in the caller.
>
>>> +    for &i in &cgroup_relative_path {
>>> +        match i {
>>> +            b'/' => fail!(
>>> +                "{prog_name:?}: Cgroup name {cgroup_relative_path:?} \
>>> +contains /"
>>> +            ),
>>> +            b'$' => fail!(
>>> +                "{prog_name:?}: Cgroup name {cgroup_relative_path:?} \
>>> +contains $: did you forget an execline substitution?"
>>> +            ),
>>> +            b'!'..=b'~' => {}
>>> +            _ => fail!(
>>> +                "{prog_name:?}: Cgroup name {cgroup_relative_path:?} \
>>> +contains space, non-ASCII character, or control character (byte {i})"
>>> +            ),
>> 
>> Surely every kernel interface we're going to use is 8-bit clean.  If
>> this is about log viewing again, I really don't like it.  Even if we're
>> careful about it in first-party code (which itself is a big ask), we
>> can't expect it of every other package that might log something.  People
>> viewing logs have to use robust tools to do so regardless, or we have to
>> mitigate this somewhere centrally, e.g. in s6 log.  Doing this piecemeal
>> does not meaningfully mitigate the problem, and arguably creates a false
>> sense of security.
>
> This was inspired by systemd, which has restrictions on what characters
> are allowed in service names.  Also, restricting allowed characters
> makes it easier to find bugs, such as "${a} ${b}" instead of "${a}"
> "${b}" in an execline script.
>
> Forbidding '$' is very much intentional, and is inspired by me making
> the exact mistake described by the error message.  It also means that
> certain names can be reserved for internal use.

In my opinion, one component should not be responsible for ad-hoc bug
checks in another, but I won't push back too much.  The other character
limitations seem completely arbitrary to me though.

>>> +        }
>>> +    }
>>> +    cgroup_relative_path.extend_from_slice(b".service");
>> 
>> What do we need the suffix for?
>
> The kernel may add new control files at any time.  However, it will
> never use the .service suffix to avoid breaking systemd.  Therefore, I
> chose to use that suffix to avoid breaking in future kernel releases.

Makes sense.

>>> +    let cgroup_relative_path = Path::new(OsStr::from_bytes(&cgroup_relative_path));
>>> +
>>> +    let local_cgroup = std::fs::read("/proc/thread-self/cgroup")
>> 
>> Ooc, why /proc/thread-self instead of /proc/self?
>
> Why not?  The two are aliases in this case, but in general /proc/thread-self is likely the better choice.

I'd just never seen it before.

>>> +        .unwrap_or_else(|e| fail!("{prog_name:?}: cannot read /proc/thread-self/cgroup: {e}"));
>>> +
>>> +    if !local_cgroup.starts_with(b"0::/")
>>> +        || !local_cgroup.ends_with(b"\n")
>>> +        || local_cgroup.contains(&b'\0')
>>> +    {
>>> +        fail!("{prog_name:?}: Kernel bug: Bad contents of /proc/thread-self/cgroup");
>>> +    }
>> 
>> We are not a kernel fuzzer.  We can't reasonably write programs that
>> have to anticipate kernel contract violations.
>
> Fair!
>
>>> +
>>> +    let mut local_cgroup = PathBuf::from(<OsString as OsStringExt>::from_vec(
>>> +        local_cgroup[4..local_cgroup.len() - 1].to_owned(),
>> 
>> … but maybe we could make this clearer, and still satisfy your instincts
>> to validate, by using slice::strip_prefix and slice::strip_suffix,
>> unwrapping the results?
>
> That's even better, because it's much easier to read than magic numbers.
>
>>> +    ));
>>> +
>>> +    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();
>>> +            }
>>> +        }
>>> +    }
>> 
>> Could we not simplify this with local_cgroup.file_name()?  If it's None,
>> it's the root directory; otherwise it's the equivalent of normal.
>
> Yup!
>
>>> +
>>> +    if local_cgroup.as_os_str().is_empty() {
>>> +        local_cgroup = PathBuf::from(".");
>>> +    }
>> 
>> Path::is_empty is stable in 1.98.  Could we mention that in a TODO/FIXME
>> comment so we can use it when available?
>
> Will do.
>
>>> +    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,
>>> +    )
>>> +    .unwrap_or_else(|e| fail!("{prog_name:?}: cannot open /sys/fs/cgroup: {e}"));
>> 
>> Why do we need to use openat2 for this, but not for reading
>> /proc/thread-self/cgroup?  I'm a bit surprised to see openat2 here at
>> all, since only root could manipulate these paths, in which case they
>> have no need to use this program as a confused deputy, right?
>> 
>> If openat2 is to stay, isn't OFlags::NOFOLLOW redundant with
>> ResolveFlags::NO_SYMLINKS?
>
> openat2() is indeed pointless here.

Great — I think we can really simplify the code if we don't use it where
unnecessary, because we can use a lot more of the standard library.
Possibly Rustix isn't even needed?

>>> +
>>> +    let cgroup = match cgroup::LeafCgroup::open_cgroup(cgroup_root.as_fd(), &local_cgroup) {
>>> +        Ok(e) => e,
>>> +        Err(e) => fail!(
>>> +            "{prog_name:?}: Failed to open {}: {e:?}",
>>> +            local_cgroup.display()
>>> +        ),
>>> +    };
>>> +
>>> +    match rustix::fs::flock(cgroup.as_fd(), FlockOperation::LockExclusive) {
>>> +        Ok(()) => {}
>>> +        Err(e) => fail!("{prog_name:?}: cannot lock cgroup: {e}"),
>>> +    }
>> 
>> Would std::fs::File::lock not be a bit Rustier?
>
> Yes :).  I know how to 
> 	
>>> +    purge_cgroup(&prog_name, &cgroup, cgroup_relative_path);
>> 
>> I find it a little weird that we run this program in both service
>> startup and finish, when there are really two completely separable parts
>> to it, right?  Why is purging not a separate program that only runs in
>> finish?
>
> The finish script might fail, time out, etc.  Purging at startup
> ensures that any previous cgroup state (like attached BPF programs)
> is removed, and it ensures that the program this program invokes will
> not run concurrently.  It's just more robust overall.
>
> Purging is idempotent, and purging an already-purged cgroup is very
> cheap.  Also, this allows using cgroup-setup without a finish script,
> while still ensuring that if the service restarts there are no stale
> processes left behind.

Alright.

>>> +    let Some(program_name) = args.next() else {
>>> +        return;
>>> +    };
>> 
>> This could be moved closer to where it's used.
>
> Will fix in v3.
>
>>> +    if let Err(e) = cgroup.make_child(cgroup_relative_path) {
>>> +        fail!("{prog_name:?}: cannot create child cgroup: {e}")
>>> +    }
>>> +    let child = cgroup
>>> +        .open_cgroup_at(cgroup_relative_path)
>>> +        .unwrap_or_else(|e| {
>>> +            fail!("{prog_name:?}: cannot create child cgroup {cgroup_relative_path:?}: {e}")
>>> +        });
>>> +    child
>>> +        .make_child(Path::new("@inner.service"))
>>> +        .unwrap_or_else(|e| {
>>> +            fail!("{prog_name:?}: cannot create child cgroup {cgroup_relative_path:?}/@inner.service: {e}")
>>> +        });
>>> +    let child = child
>>> +        .open_cgroup_at(Path::new("@inner.service"))
>>> +        .unwrap_or_else(|e| {
>>> +            fail!("{prog_name:?}: cannot open child cgroup {cgroup_relative_path:?}/@inner.service: {e}")
>>> +        });
>> 
>> This would really benefit from having been written up, so I don't have
>> to guess what it's for.  We only really need it for supervisors, right?
>
> It's only *needed* when the program might be called recursively,
> but it's easier to just use it everywhere.

Yeah, that makes sense.  Would just be nice to have that documented
somewhere so the next person doesn't have to spend the time I did
figuring it out!

>>> +
>>> +    // SAFETY: safe FFI call
>>> +    let pid = unsafe { libc::getpid() };
>> 
>> Not std::process::id()?
>
> Only because I was not aware of it.
>
>>> +    write_cgroup_value(&prog_name, &child, "cgroup.procs", &pid.to_string());
>>> +
>>> +    let e = std::process::Command::new(&program_name).args(args).exec();
>>> +    fail!(
>>> +        "{prog_name:?}: cannot spawn child {:?}: {}",
>>> +        program_name,
>>> +        e
>>> +    );
>>> +}
>>> +
>>> +fn write_cgroup_value(prog_name: &OsStr, child: &cgroup::LeafCgroup, name: &str, value: &str) {
>> 
>> Perhaps it would be nicer for this to be a method on LeafCgroup?
>
> Sure!
>
>>> +    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!("{prog_name:?}: cannot open {:?}: {}", path, e));
>>> +    let () = File::from(fd)
>>> +        .write_all(value.as_bytes())
>>> +        .unwrap_or_else(|e| {
>>> +            fail!(
>>> +                "{prog_name:?}: Cannot write {:?} to {:?}: {}",
>>> +                name,
>>> +                path,
>>> +                e
>>> +            )
>>> +        });
>> 
>> Similarly to above, if we don't need openat2, this could be done more
>> nicely as std::fs::write.
>
> std::fs doesn't even expose openat(), and I'm much more comfortable
> using a directory FD here.  This is a fairly severe limitation in
> the stdlib.  Fixing it requires using the Native API on Windows,
> but that's easy enough.

We have a lock on the cgroup though.  What are we protecting against by
using openat?

>>> +}
>>> +
>>> +fn purge_cgroup(prog_name: &OsStr, cgroup: &cgroup::LeafCgroup, cgroup_relative_path: &Path) {
>>> +    let child = match cgroup.open_cgroup_at(cgroup_relative_path) {
>>> +        Ok(child_cgroup) => child_cgroup,
>>> +        Err(Errno::NOENT) => return,
>>> +        Err(other) => {
>>> +            fail!("{prog_name:?}: cannot open child cgroup {cgroup_relative_path:?}: {other}")
>>> +        }
>>> +    };
>>> +    child.kill_processes().unwrap_or_else(|e| {
>>> +        fail!("{prog_name:?}: cannot kill programs in {cgroup_relative_path:?}: {e}")
>>> +    });
>>> +
>>> +    cgroup
>>> +        .delete_child(cgroup_relative_path)
>>> +        .unwrap_or_else(|e| {
>>> +            fail!("{prog_name:?}: delete child cgroup {cgroup_relative_path:?}: {e}")
>>> +        });
>>> +}

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v2] Set up control groups for most services
  2026-06-25  3:03       ` Demi Marie Obenour
@ 2026-06-25  9:55         ` Alyssa Ross
  0 siblings, 0 replies; 8+ messages in thread
From: Alyssa Ross @ 2026-06-25  9:55 UTC (permalink / raw)
  To: Demi Marie Obenour; +Cc: Spectrum OS Development

[-- Attachment #1: Type: text/plain, Size: 7740 bytes --]

Demi Marie Obenour <demiobenour@gmail.com> writes:

> On 6/24/26 22:03, Demi Marie Obenour wrote:
>> On 6/24/26 08:13, Alyssa Ross wrote:
>>> Demi Marie Obenour <demiobenour@gmail.com> writes:
>>>
>>>> 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.
>>>>
>>>
>>> Perhaps you could include some more details of how the cgroup hierarchy
>>> is supposed to be set up and why, either in the patch body or in
>>> documentation?
>> 
>> Will fix in v3.
>> 
>>> As I understood it, the point of using cgroups was that we could bundle
>>> all services for a VM, including the VMM, into a single cgroup, but I
>>> don't see that here, just a pure translation of the service hierarchy
>>> (which the VMM might not even be part of).  Are per-VM cgroups coming
>>> later?
>> 
>> I think it would be significantly simpler to have the VMM be
>> just another VM service.  This would naturally put it under the
>> vm-services cgroup, solving this problem.  Since this would mostly
>> involve changes to execline scripting, I think it would be better if
>> you wrote this code.
>> 
>>>> 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
>>>
>>> Alignment is off here.  The rest of the file uses tabs at 8 characters.
>>> (I know, I know, but it's traditional.)
>>>
>>>> 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
>>>
>>> What benefit does this cgroup provide?
>> 
>> None, I'll remove it.
>> 
>> The only requirement of the current code is that if a service is under
>> a cgroup, its parent should *also* be under a cgroup.  Otherwise you
>> can get conflicting cgroup paths.
>> 
>>>> 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
>>>
>>> We're putting supervisors of instances services in a cgroup?  Can you
>>> explain to me why that's useful?
>> 
>> This allows configuring limits that apply to the whole instance, and
>> means that the whole instance can be reliably killed.  Services spawned
>> by the instance will be in their own sub-cgroups, so one can apply
>> separate limits to them so long as those limits do not exceed those
>> of the containing cgroup.
>> 
>> The hierarchy works like this:
>> 
>> /sys/fs/cgroup
>>   service1.service
>>     @inner.service # processes go here
>>     service2.service
>>       @inner.service # and here
>>       service3.service
>>         @inner.service # and here
>> 
>> You can apply resource limits at any level of the hierarchy.  Limits in
>> nested cgroups can be smaller but not larger.  Terminating processes
>> in a cgroup with cgroup.kill also terminates all processes in child
>> cgroups.  After terminating the processes in a cgroup, cgroup-setup
>> will delete everything in the hierarchy recursively.
>
> More generally:

(I already replied to the previous email before seeing this, but I'll
try to respond in depth here too.)

> The design I implemented is based on a model where cgroups and services
> are very tightly coupled: most services have an associated cgroup and
> every cgroup is attached to a service.  This is like systemd service
> units, except that it can be nested many layers deep.
>
> Since a VM may have many processes that serve it, this model requires
> that all of these processes be under a single service.  In this case,
> that service would be the vm-services service.
>
> That said, I'm not sure which of these service, if any, should be
> automatically restarted if they crash.  Automatic restart of services
> is actually very useful for attackers, as it lets them retry an exploit
> over and over until they get the memory layout they need for success.

This is exactly why this model doesn't fit.  It does not make sense to
use an s6 service for something that doesn't get restarted.  Oneshot
services are not a concept that exist in s6.  For a clean, repeatable
environment etc. one can use s6-sudod, but even if we had an s6-sudo
service for starting VMMs, they still wouldn't be under a vm-services
instance.  The service↔cgroup equivalence from systemd does not make
sense here.

> If you restart the VMM, you might as well restart every other
> service as well.  Unless Cloud Hypervisor automatically reconnects to
> vhost-user devices (and I don't think it does), there's no point in
> restarting virtiofsd or crosvm without either re-adding the device
> or restarting the VMM.  Even if you did re-add the device, I don't
> know if the guest would be able to recover.

Guests are able to recover from e.g. dbus crashing trivially, and I've
seen virtio-fs working fine after a virtiofsd restart too.  That's why
we run them as services.  It's set up the way it is for a reason!

> Without automatic restart, there's no real reason to use s6 to manage
> the per-VM services.  s6 assumes you do want to restart services
> that crash.

Which we do, generally.

> An alternative approach is to have cgroups be freestanding objects
> that exist independently of whether there are processes in them.
> This corresponds to systemd scope units.  It's not hard to implement
> this: just disable automatic purging and pass the full path to the
> cgroup.  But it requires each VM service to have identical resource
> limits, whereas one typically wants per-process limits.  For instance,
> none of the services should be able to create child processes, and
> per-service cgroups make this easy: just set the maximum amount of
> processes in the cgroup to 1.

Why would you be restricted to identical resource limits.

> Incidentally, this works very well with PID namespaces.  One can have
> the "main" process of the service run in a PID namespace, and have
> all the others enter that namespace.  Then if the main process dies,
> so do all of the others.

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]

^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-06-25  9:55 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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

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