From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from atuin.qyliss.net (localhost [IPv6:::1]) by atuin.qyliss.net (Postfix) with ESMTP id 7914E1BA3; Sat, 11 Jul 2026 20:14:25 +0000 (UTC) Received: by atuin.qyliss.net (Postfix, from userid 993) id 4838B1B3B; Sat, 11 Jul 2026 20:14:23 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-26) on atuin.qyliss.net X-Spam-Level: X-Spam-Status: No, score=-0.1 required=3.0 tests=DKIM_SIGNED,DKIM_VALID, DKIM_VALID_AU,DMARC_PASS,FREEMAIL_FROM,RCVD_IN_DNSWL_NONE, SPF_HELO_NONE autolearn=unavailable autolearn_force=no version=4.0.1 Received: from mail-yw1-x1131.google.com (mail-yw1-x1131.google.com [IPv6:2607:f8b0:4864:20::1131]) by atuin.qyliss.net (Postfix) with ESMTPS id 7D67E1A5D for ; Sat, 11 Jul 2026 20:14:18 +0000 (UTC) Received: by mail-yw1-x1131.google.com with SMTP id 00721157ae682-80e2cfe6918so22578877b3.0 for ; Sat, 11 Jul 2026 13:14:18 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1783800857; x=1784405657; darn=spectrum-os.org; h=cc:to:in-reply-to:references:message-id:content-transfer-encoding :content-type:mime-version:subject:date:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=yWvY4eyys7qN7kC2bdlJXvFL6YRpmlN56U3nqVW9hRo=; b=abrhlfm7lVYULjwC1ZAj3cR1YdSDDMxrHJHHUoZOponvg53dZLpHlZtnh3LCSQfsYm HAsK11zx7HdAWUxfxTke/ck2kG1M1RF34LP8raa/ypa4hZH0YMEtRfhP2eIG5eILwiRD ILkWhvdUdV6l4S8DMuItz95tRu/SP/azYs1bCsR9dFsJTvH/VDCWA/lRCPmQRWitvT36 738q9QdphwvqCGEEJPCn+CVp4BDB/8pC/5FR34+ifeYgelWz5Gt7oFkzytWTXDa94HlX 9lW0jNMK0hd+8JTBnoqn5kexsnT9rBdGgNZgKCLaWyrfYXJOOPRdgXtt78G70oxQuFbJ Lsjg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1783800857; x=1784405657; h=cc:to:in-reply-to:references:message-id:content-transfer-encoding :content-type:mime-version:subject:date:from:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=yWvY4eyys7qN7kC2bdlJXvFL6YRpmlN56U3nqVW9hRo=; b=ixqO9/pUnROnvzakz3Nd6IKqjlSJy7PeCsCZIL/Zh2O4Oc9Qb79DYfuKNSMf/w7R3M Fu95nXIwdIHuu4ui+MXjn7jvafpVsOEPpRp6qRhIdJmKMwJYGz10gEZlV5obOLUblZ2T FXg7ll/jQ600xi5KeFVkVWvLyRaM0WU4bY8yLzo3TIaXyNrsx89/tFZCgFJD9ycBC6Vc NedyCbb7xL983rOz1/V88/iXePxX5GOvSFMbsvsViGYkt7l7vTIZcfs3kGTk8pOYqMKO 2b8LVgzGc0Lv/Zo2xs3hrKgFaar2zhiqnCX/qdAE7c8i/Ak9Hod/VHGZpNVneolGimYC osjQ== X-Gm-Message-State: AOJu0YxdpS+aw9cFbm2My3Ocms6jiPzUx0DgPWmg9sKpuHC5B4/pOn3j fN31IwHMzYiuh6tcZIVLr+ur8/fXv+4buOdZRK/dH/bGTZZ3mNAoOWqB/CO4zQ== X-Gm-Gg: AfdE7clLxkISOj4WWHdzv4uOiKZSxhCSQWdusJ73m5jBVQWKI/N0fay7OdGYe644oiP 0d0TdiaKGj5QP+mhOm2oHgm+yfKxSsdDJow1T9jH/Zz6vCpuwe8i7bdW+5mQF6Q2O3iNZVNEkgI 4TGAe3BlSY9v/4zYf3DVF6D0J8emQbeoZyGYmU5wdqvd3+6UU6ZSQzGIlPm81/Z7cGgfj7ttpL3 8uM123SBlQGVM1pb1ObFtA+G2gjgx9r82yxQKs/texfHGTV7/TJvFET6kn43GHxqoYNqMFf1vNr l8L81ly9L8jZz4jqERa9XtSSmlGLEvJKuRF9b9HvgFgF8G2t8RTeTgWUo05mROlxh7ypsG3ShO4 uZvUz0SVj72y4r2QiESGn3ehDP/x7Yjbz7WYbSBlHJiCKqvoxex+PQNhcmAohynq3+fNpfyRrHA aCvBtvM1nfdjm/gMA4hnTI2zgP4g== X-Received: by 2002:a05:690c:e287:20b0:81e:9826:942c with SMTP id 00721157ae682-81e98269599mr11162857b3.0.1783800857014; Sat, 11 Jul 2026 13:14:17 -0700 (PDT) Received: from localhost.localdomain ([185.98.168.14]) by smtp.gmail.com with UTF8SMTPSA id 00721157ae682-81e6bef797esm77612437b3.11.2026.07.11.13.14.15 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sat, 11 Jul 2026 13:14:15 -0700 (PDT) From: Demi Marie Obenour Date: Sat, 11 Jul 2026 16:12:07 -0400 Subject: [PATCH v3 03/22] tools: Add control group manager MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: 7bit Message-Id: <20260711-cgroups-v3-3-5cba61a20cba@gmail.com> References: <20260711-cgroups-v3-0-5cba61a20cba@gmail.com> In-Reply-To: <20260711-cgroups-v3-0-5cba61a20cba@gmail.com> To: Spectrum OS Development X-Mailer: b4 0.15.2 X-Developer-Signature: v=1; a=ed25519-sha256; t=1783800724; l=29204; i=demiobenour@gmail.com; s=20250729; h=from:subject:message-id; bh=sT2id+a/9i3jq0ZO2DRCl4u8DMKO5p1LWmbn79py/wc=; b=0kFTOazApiTF2V+v6SIL8qkVH2ccTY8SFuCBwYstdFwtpWM5RAM5SANa3Mg4pReF/wlTIyrUq dIjiilvjiLwCnr0hNphjsmmqEA8v6fawEt1FXm1mdWMfaMHIKw7g9mb X-Developer-Key: i=demiobenour@gmail.com; a=ed25519; pk=X57Q4/YQDj9t4SBeKaDwvXYKB6quZJVx/DE2Ly2out0= Message-ID-Hash: KW7ISMMCVK4K4KTXP5RIRENVMYTW3VJH X-Message-ID-Hash: KW7ISMMCVK4K4KTXP5RIRENVMYTW3VJH X-MailFrom: demiobenour@gmail.com X-Mailman-Rule-Hits: member-moderation X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; header-match-devel.spectrum-os.org-0; header-match-devel.spectrum-os.org-1; header-match-devel.spectrum-os.org-2; header-match-devel.spectrum-os.org-3; header-match-devel.spectrum-os.org-4; emergency CC: Demi Marie Obenour , Alyssa Ross X-Mailman-Version: 3.3.10 Precedence: list List-Id: Patches and low-level development discussion Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: The cgroup-setup Rust program can create and purge cgroups. It can also wait for one to become empty, spawn a program in a cgroup, and more. In the future, it will also support cgroup-based resource control. This program can also act as an s6 finish script. When invoked as such, it automatically purges the correct cgroup. It also tells s6 to not restart the service if it dumped core. Core dumps are often due to memory corruption, and automatically restarting a service that dumped core makes memory corruption attacks easier. Signed-off-by: Demi Marie Obenour --- .codespellrc | 2 +- host/rootfs/default.nix | 6 +- pkgs/default.nix | 1 + tools/cgroup-setup/Cargo.lock | 67 ++++++ tools/cgroup-setup/Cargo.lock.license | 2 + tools/cgroup-setup/Cargo.toml | 11 + tools/cgroup-setup/default.nix | 18 ++ tools/cgroup-setup/src/cgroup.rs | 203 +++++++++++++++++ tools/cgroup-setup/src/main.rs | 406 ++++++++++++++++++++++++++++++++++ 9 files changed, 712 insertions(+), 4 deletions(-) diff --git a/.codespellrc b/.codespellrc index d8023afc64ec44e98a88c5e397c8d5c681dde063..ae20da8309530759ac729689825a4afc683afa09 100644 --- a/.codespellrc +++ b/.codespellrc @@ -2,4 +2,4 @@ # SPDX-License-Identifier: CC0-1.0 [codespell] -ignore-words-list = crate,passt,rouge,ser +ignore-words-list = crate,passt,rouge,ser,WRONLY diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 4dcc10b9933cdfaf87f87417db1de0daa57b73f5..f161c26e8b3cc3a46d3321d88a52c446e2faac96 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -8,7 +8,7 @@ import ../../lib/call-package.nix ( }: pkgsMusl.callPackage ( -{ spectrum-host-tools, spectrum-router +{ spectrum-host-tools, spectrum-router, spectrum-cgroup-setup , lib, stdenvNoCC, nixos, runCommand, writeClosure, erofs-utils, s6-rc , btrfs-progs, bubblewrap, busybox, cloud-hypervisor, cosmic-files , crosvm, cryptsetup, dejavu_fonts, dbus, execline, foot, fuse3 @@ -27,8 +27,8 @@ let packages = [ btrfs-progs bubblewrap cloud-hypervisor cosmic-files crosvm cryptsetup dbus execline fuse3 inotify-tools iproute2 jq kmod mdevd mount-flatpak s6 - s6-linux-init s6-rc shadow socat spectrum-host-tools spectrum-router - virtiofsd xdg-desktop-portal-spectrum-host + s6-linux-init s6-rc shadow socat spectrum-cgroup-setup spectrum-host-tools + spectrum-router virtiofsd xdg-desktop-portal-spectrum-host (foot.override { allowPgo = false; }) diff --git a/pkgs/default.nix b/pkgs/default.nix index 4dbdfee8ba330f5ba5c9fedee2d1bb8a44af5722..22c0d782bf4b5467fe77d5a2d71fb8df3af3e756 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -51,6 +51,7 @@ let driverSupport = true; }; spectrum-router = self.callSpectrumPackage ../tools/router {}; + spectrum-cgroup-setup = self.callSpectrumPackage ../tools/cgroup-setup {}; xdg-desktop-portal-spectrum-host = self.callSpectrumPackage ../tools/xdg-desktop-portal-spectrum-host {}; diff --git a/tools/cgroup-setup/Cargo.lock b/tools/cgroup-setup/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..fe967b3aa02c296c87b6b36ac59253dbe0a32de9 --- /dev/null +++ b/tools/cgroup-setup/Cargo.lock @@ -0,0 +1,67 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "cgroup-setup" +version = "0.0.0" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/tools/cgroup-setup/Cargo.lock.license b/tools/cgroup-setup/Cargo.lock.license new file mode 100644 index 0000000000000000000000000000000000000000..aa108acd23886d8302eaf7babff90d1b08ae19fb --- /dev/null +++ b/tools/cgroup-setup/Cargo.lock.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: EUPL-1.2+ +SPDX-FileCopyrightText: 2026 Demi Marie Obenour diff --git a/tools/cgroup-setup/Cargo.toml b/tools/cgroup-setup/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..7ed6d6a0ea3bbfc4064b9f39383d0788c4bd84e5 --- /dev/null +++ b/tools/cgroup-setup/Cargo.toml @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2025 Alyssa Ross +# SPDX-FileCopyrightText: 2026 Demi Marie Obenour + +[package] +name = "cgroup-setup" +edition = "2024" + +[dependencies] +libc = "0.2.177" +rustix = { version = "1.1.2", features = ["fs"] } diff --git a/tools/cgroup-setup/default.nix b/tools/cgroup-setup/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..9e716b8f270828b733f39961e5ca37a290a872b4 --- /dev/null +++ b/tools/cgroup-setup/default.nix @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2024 Alyssa Ross +# SPDX-FileCopyrightText: 2025 Yureka Lilian +# SPDX-License-Identifier: MIT + +import ../../lib/call-package.nix ( +{ src, lib, rustPlatform }: + +rustPlatform.buildRustPackage { + name = "spectrum-cgroup-setup"; + + src = lib.fileset.toSource { + root = ../..; + fileset = lib.fileset.intersection src ./.; + }; + sourceRoot = "source/tools/cgroup-setup"; + + cargoLock.lockFile = ./Cargo.lock; +}) (_: {}) diff --git a/tools/cgroup-setup/src/cgroup.rs b/tools/cgroup-setup/src/cgroup.rs new file mode 100644 index 0000000000000000000000000000000000000000..eb9b892ab6f562cffa738abaf9c55dc18001012a --- /dev/null +++ b/tools/cgroup-setup/src/cgroup.rs @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2026 Demi Marie Obenour + +use std::fs::File; +use std::io::{Read as _, Seek as _, Write as _}; +use std::os::unix::prelude::*; + +use std::path::{Path, PathBuf}; + +use rustix::fs::{AtFlags, XattrFlags}; +use rustix::path::Arg; +use rustix::{ + fs::{Mode, OFlags, ResolveFlags}, + io::Errno, +}; + +pub(crate) struct LeafCgroup { + path: PathBuf, + fd: OwnedFd, +} + +pub enum Access { + Read, + Write, +} + +impl LeafCgroup { + pub fn enable_delegation(&self) -> Result<(), Errno> { + rustix::fs::fsetxattr(self.fd.as_fd(), c"user.delegate", b"1", XattrFlags::empty()) + } + + pub fn open_subtree(&self, path: &std::path::Path, access: Access) -> Result { + rustix::fs::openat2( + self.fd.as_fd(), + path, + OFlags::NOATIME + | OFlags::CLOEXEC + | OFlags::NOFOLLOW + | match access { + Access::Write => OFlags::WRONLY, + Access::Read => OFlags::RDONLY, + }, + Mode::empty(), + ResolveFlags::NO_SYMLINKS | ResolveFlags::BENEATH | ResolveFlags::NO_XDEV, + ) + } + + pub fn wait_for_empty(&self, kill: bool) -> std::io::Result<()> { + let kill_fd = if kill { + Some(self.open_subtree(std::path::Path::new("cgroup.kill"), Access::Write)?) + } else { + None + }; + let wait_file = self.open_subtree(std::path::Path::new("cgroup.events"), Access::Read)?; + let poll_fd = wait_file.as_raw_fd(); + let mut wait_fd = File::from(wait_file); + if let Some(kill_fd) = kill_fd { + assert_eq!(rustix::io::write(kill_fd.as_fd(), b"1")?, 1, "kernel bug"); + } + let mut fds = libc::pollfd { + fd: poll_fd, + events: libc::POLLPRI | libc::POLLERR, + revents: 0, + }; + let mut v = vec![]; + loop { + v.clear(); + // SAFETY: FFI call, valid arguments, fds contains 1 element + if unsafe { libc::poll(&raw mut fds, 1, -1) } != 1 { + panic!("poll failed"); + } + wait_fd + .seek(std::io::SeekFrom::Start(0)) + .expect("Seek on control group file should succeed"); + wait_fd + .read_to_end(&mut v) + .expect("reading from control group should work"); + if v.split(|&c| c == b'\n').any(|line| line == b"populated 0") { + break; + } + } + Ok(()) + } +} + +impl AsFd for LeafCgroup { + fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> { + self.fd.as_fd() + } +} + +impl LeafCgroup { + pub(crate) fn open_cgroup_at(&self, p: &Path) -> Result { + 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 { + let fd = rustix::fs::openat2( + fd, + p, + OFlags::NOATIME + | OFlags::CLOEXEC + | OFlags::NOFOLLOW + | OFlags::RDONLY + | OFlags::DIRECTORY, + Mode::empty(), + ResolveFlags::NO_SYMLINKS, + )?; + Ok(Self { + fd, + path: p.to_owned(), + }) + } + pub(crate) fn make_child(&self, p: &Path) -> Result<(), Errno> { + let r = rustix::fs::mkdirat( + self.fd.as_fd(), + p, + Mode::RUSR + | Mode::WUSR + | Mode::XUSR + | Mode::RGRP + | Mode::XGRP + | Mode::ROTH + | Mode::XOTH, + ); + if r == Err(Errno::EXIST) { Ok(()) } else { r } + } + + pub(crate) fn delete_child(&self, path: &Path) -> Result<(), Errno> { + remove_all(100, self.as_fd(), &path.as_cow_c_str().unwrap()) + } + + pub(crate) fn write_cgroup_value(&self, name: &str, value: &str) -> Result<(), String> { + let path = Path::new(name); + let fd = rustix::fs::openat2( + self.as_fd(), + path, + OFlags::NOATIME | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::WRONLY, + Mode::empty(), + ResolveFlags::NO_SYMLINKS | ResolveFlags::BENEATH | ResolveFlags::NO_XDEV, + ) + .map_err(|e| format!("Cannot open {:?}: {}", path, e))?; + File::from(fd) + .write_all(value.as_bytes()) + .map_err(|e| format!("Cannot write {:?} to {:?}: {}", value, path, e)) + } +} + +fn remove_recursively(fd: OwnedFd, remaining_depth: usize) -> Result<(), Errno> { + if remaining_depth < 1 { + panic!("control groups too deeply nested"); + } + let mut d = rustix::fs::Dir::new(fd).expect("cannot start iterating"); + while let Some(element) = d.next() { + let element = element.expect("Iterating through a cgroup directory failed?"); + if element.file_type() != rustix::fs::FileType::Directory { + continue; + } + + let remaining_depth = remaining_depth - 1; + let d: &rustix::fs::Dir = &d; + let dirfd = d.fd().unwrap(); + let path = element.file_name(); + remove_all(remaining_depth, dirfd, path)?; + } + Ok(()) +} + +fn remove_all( + remaining_depth: usize, + dirfd: BorrowedFd<'_>, + path: &std::ffi::CStr, +) -> Result<(), Errno> { + if path == c"." || path == c".." { + return Ok(()); + } + if rustix::fs::unlinkat(dirfd, path, AtFlags::REMOVEDIR).is_ok() { + return Ok(()); + } + let fd = rustix::fs::openat2( + dirfd, + path, + OFlags::NOATIME | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty(), + ResolveFlags::NO_SYMLINKS | ResolveFlags::BENEATH | ResolveFlags::NO_XDEV, + )?; + remove_recursively(fd, remaining_depth)?; + rustix::fs::unlinkat(dirfd, path, AtFlags::REMOVEDIR)?; + Ok(()) +} diff --git a/tools/cgroup-setup/src/main.rs b/tools/cgroup-setup/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..5b2d2eb9831b0c635914a5e4dc90e79a262588a5 --- /dev/null +++ b/tools/cgroup-setup/src/main.rs @@ -0,0 +1,406 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2026 Demi Marie Obenour + +use std::{ + ffi::{OsStr, OsString}, + fs::File, + io::Read as _, + os::unix::prelude::*, + path::{Component, Path, PathBuf}, +}; + +use rustix::{ + fs::{FlockOperation, Mode, OFlags, ResolveFlags}, + io::Errno, +}; + +use crate::cgroup::Access; + +mod cgroup; + +fn main() { + let mut args = std::env::args_os(); + let Some(prog_name) = args.next() else { + eprintln!("No command line arguments (argv[0] is NULL)"); + std::process::exit(1); + }; + match main_(&prog_name, args) { + Ok(()) => {} + Err(e) => { + eprintln!("{prog_name:?}: {}", e); + std::process::exit(1); + } + } +} + +fn main_(prog_name: &OsStr, mut args: std::env::ArgsOs) -> Result<(), String> { + let mut purge = false; + match prog_name + .as_bytes() + .split(|&b| b == b'/') + .next_back() + .unwrap() + { + b"finish" => { + return s6_finish(&mut args); + } + b"cgroup-setup" => {} + e => { + return Err(format!( + "must be invoked as \"cgroup-setup\" or \"finish\", got {:?}", + e + )); + } + }; + let mut leaf = false; + let mut cgroup_relative_path; + let mut delegate = false; + let mut init_subtree = false; + let mut child_name: Option<&'static OsStr> = None; + let mut wait = None; + loop { + cgroup_relative_path = args.next(); + let Some(ref arg_) = cgroup_relative_path else { + break; + }; + let arg_ = arg_.as_bytes(); + if arg_ == b"--" { + cgroup_relative_path = args.next(); + break; + } + if !arg_.starts_with(b"-") { + break; + } + + if !arg_.starts_with(b"--") { + return Err("takes no short options".to_owned()); + } + + match &arg_[2..] { + b"purge" => purge = true, + b"leaf" => leaf = true, + b"delegate" => delegate = true, + b"init-subtree" => init_subtree = true, + b"wait" => wait = Some(true), + b"no-wait" => wait = Some(false), + b"child-name" if child_name.is_none() => match args.next() { + Some(arg) => child_name = Some(arg.leak()), + None => return Err("--child-name: missing argument".to_owned()), + }, + b"child-name" => return Err("--child-name: cannot be used twice".to_owned()), + arg => match str::from_utf8(arg) { + Ok(e) => return Err(format!("unknown long option {e:?}")), + Err(_) => return Err("long option isn't UTF-8".to_owned()), + }, + } + } + + let default_child_name = OsStr::from_bytes(b"$inner.service"); + + let child_name = Path::new(child_name.unwrap_or(default_child_name)); + + let Some(cgroup_relative_path) = cgroup_relative_path else { + return Err("have no positional arguments, expected at least 1".to_owned()); + }; + + // The kernel doesn't care, but displaying messages does. + let cgroup_path = String::try_from(cgroup_relative_path.into_vec()) + .map_err(|e| format!("non-UTF-8 cgroup path not supported (error is {e})"))?; + + if cgroup_path.is_empty() { + return Err("cgroup name is empty".to_owned()); + } + if cgroup_path == ".." { + return Err("cgroup name is ..".to_owned()); + } + // If we aren't asked to create a child process, don't wait for existing + // processes to die unless explicitly asked to. Waiting for a cgroup + // we are in to be empty is a guaranteed deadlock. + let may_wait = cgroup_path != "." && cgroup_path != "/"; + let wait = match wait { + Some(false) => false, + None => may_wait && args.len() > 0, + Some(true) if !may_wait => { + let msg = "Cannot wait for the program's own cgroup or \ + root cgroup to be empty"; + return Err(msg.to_owned()); + } + Some(true) => true, + }; + + let params = CgroupParams { + purge, + leaf, + delegate, + init_subtree, + wait, + }; + + let (full_path, cgroup_target, cgroup) = cgroup_parse(cgroup_path)?; + if params.purge { + purge_cgroup(&cgroup, &cgroup_target)?; + } + let child = match cgroup.open_cgroup_at(&cgroup_target) { + Ok(child_cgroup) => child_cgroup, + Err(Errno::NOENT) => { + if let Err(e) = cgroup.make_child(&cgroup_target) { + return Err(format!("Cannot create child cgroup {full_path:?}: {e}")); + } + cgroup + .open_cgroup_at(&cgroup_target) + .map_err(|e| format!("Cannot open child cgroup {full_path:?}: {e}"))? + } + Err(other) => { + return Err(format!("Cannot open child cgroup {full_path:?}: {other}")); + } + }; + if params.wait { + child + .wait_for_empty(false) + .map_err(|e| format!("Cannot wait for {full_path:?} to be empty: {e}"))?; + } + let pid = std::process::id().to_string(); + if params.leaf { + // If we aren't delegating any cgroups, don't create a sub-cgroup. + child + .write_cgroup_value("cgroup.procs", &pid) + .map_err(|e| format!("Cannot write to {full_path:?}/cgroup.procs: {e}"))?; + } else { + child.make_child(child_name).map_err(|e| { + format!( + "Cannot create child cgroup {}/{}: {e}", + full_path.display(), + child_name.display() + ) + })?; + + // If the child process will need to manage cgroups itself, it will need + // to set up a sub-cgroup due to the "no internal processes" rule. It's + // simplest to just do it automatically. + let grandchild = child.open_cgroup_at(Path::new(child_name)).map_err(|e| { + format!( + "Cannot open child cgroup {}/{}: {e}", + full_path.display(), + child_name.display() + ) + })?; + grandchild + .write_cgroup_value("cgroup.procs", &pid) + .map_err(|e| { + format!( + "Cannot write to {}/{}/cgroup.procs: {e}", + full_path.display(), + child_name.display() + ) + })?; + } + if params.init_subtree { + enable_subtree_control(&cgroup)?; + } + if !params.leaf { + enable_subtree_control(&child)?; + } + if params.delegate { + child + .enable_delegation() + .map_err(|e| format!("Cannot enable cgroup delegation in {full_path:?}: {e}"))?; + } + let Some(program_name) = args.next() else { + return Ok(()); + }; + let e = std::process::Command::new(&program_name).args(args).exec(); + Err(format!("Cannot spawn child {:?}: {}", program_name, e)) +} + +fn s6_finish(args: &mut std::env::ArgsOs) -> Result<(), String> { + if args.len() < 3 { + return Err(format!( + "s6 finish scripts take 3 arguments, got {}", + args.len() + )); + } + let status = parse_digit_string(&args.next().unwrap(), "exit status")?; + let signal = args.next().unwrap(); + let signal = if status == 256 { + Some(parse_digit_string(&signal, "signal number")?) + } else { + None + }; + let service = args + .next() + .unwrap() + .into_string() + .map_err(|e| format!("Service name {e:?} is not UTF-8"))?; + let (_full_path, cgroup_target, cgroup) = cgroup_parse(service)?; + let r = purge_cgroup(&cgroup, &cgroup_target); + if let Some(signal) = signal { + match signal as libc::c_int { + libc::SIGBUS + | libc::SIGFPE + | libc::SIGABRT + | libc::SIGTRAP + | libc::SIGSEGV + | libc::SIGILL => { + // Process *crashed*, indicating a *possible exploit attempt*. + // s6 should *not* restart it. This is distinct from a Rust panic, + // which is much less likely to indicate memory corruption. + if let Err(e) = r { + // do not panic on stderr write failure + eprintln!("Could not purge cgroup: {e}"); + } + std::process::exit(125) + } + _ => return r, + } + } + r +} + +fn parse_digit_string(digits: &OsStr, msg: &str) -> Result { + let checked = match str::from_utf8(digits.as_bytes()) { + Ok(s) => s, + Err(e) => return Err(format!("{msg} is not UTF-8: {e}")), + }; + let r = checked + .parse::() + .map_err(|e| format!("{msg} {digits:?} is a bad 16-bit number: {e}"))?; + match checked.as_bytes() { + b"0" | [b'1'..=b'9', ..] => Ok(r), + [b'0', ..] => Err(format!("{msg} {} has a leading 0", digits.display())), + _ => Err(format!("{msg} {} starts with +", digits.display())), + } +} + +fn cgroup_parse(mut arg: String) -> Result<(PathBuf, PathBuf, cgroup::LeafCgroup), String> { + let cgroup_root = rustix::fs::openat2( + rustix::fs::CWD, + Path::new("/sys/fs/cgroup"), + OFlags::DIRECTORY | OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW, + Mode::empty(), + ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS, + ) + .map_err(|e| format!("cannot open /sys/fs/cgroup: {e}"))?; + let cgroup_absolute = arg.starts_with("/"); + let (full_path, mut cgroup_parent, cgroup_target) = if cgroup_absolute { + if arg.contains("//") { + return Err(format!("Cgroup path {arg:?} contains //")); + } + arg.remove(0); + let to_create = PathBuf::from(arg); + for component in to_create.components() { + match component { + Component::Prefix(_) => unreachable!("not present on Unix"), + Component::CurDir | Component::ParentDir => { + return Err("Cgroup path has . or .. components".to_owned()); + } + Component::RootDir | Component::Normal(_) => {} + } + } + let Some(last_component) = to_create.file_name() else { + return Err(format!("Cgroup path {to_create:?} has no file name")); + }; + if to_create.parent().is_none() { + return Err(format!("Cgroup path {to_create:?} has no parent name")); + } + let r = last_component.as_bytes().to_owned(); + let mut prefix = to_create.clone(); + prefix.pop(); + (to_create, prefix, r) + } else { + if arg.is_empty() { + return Err("Cgroup path is empty".to_string()); + } + + let mut local_cgroup: Vec = std::fs::read("/proc/thread-self/cgroup") + .map_err(|e| format!("cannot read /proc/thread-self/cgroup: {e}"))?; + let local_cgroup_len = local_cgroup.len(); + if local_cgroup_len < 5 + || local_cgroup[..4] != *b"0::/" + || local_cgroup[local_cgroup_len - 1] != b'\n' + { + return Err(format!( + "Invalid contents {local_cgroup:?} of /proc/thread-self/cgroup - \ + do you have cgroups v1 mounted instead of cgroups v2?" + )); + } + + local_cgroup.copy_within(4..local_cgroup_len - 1, 0); + local_cgroup.truncate(local_cgroup_len - 5); + let total_path = PathBuf::from(::from_vec(local_cgroup)); + let mut r = total_path.clone(); + r.push(&arg); + (r, total_path, arg.into()) + }; + if cgroup_parent.file_name() == Some(OsStr::from_bytes(b"$inner.service")) { + cgroup_parent.pop(); + } + if cgroup_parent.as_os_str().is_empty() { + cgroup_parent = ".".into(); + } + let cgroup_target = OsString::from_vec(cgroup_target).into(); + let cgroup = cgroup::LeafCgroup::open_cgroup(cgroup_root.as_fd(), &cgroup_parent) + .map_err(|e| format!("Failed to open {}: {e:?}", cgroup_parent.display()))?; + match rustix::fs::flock(cgroup.as_fd(), FlockOperation::LockExclusive) { + Ok(()) => {} + Err(e) => return Err(format!("Cannot lock cgroup: {e}")), + } + Ok((full_path, cgroup_target, cgroup)) +} + +struct CgroupParams { + purge: bool, + leaf: bool, + delegate: bool, + init_subtree: bool, + wait: bool, +} + +fn enable_subtree_control(cgroup: &cgroup::LeafCgroup) -> Result<(), String> { + let mut buf = Vec::new(); + File::from( + cgroup + .open_subtree(Path::new("cgroup.controllers"), Access::Read) + .map_err(|e| format!("Cannot open cgroup.controllers: {e}"))?, + ) + .read_to_end(&mut buf) + .map_err(|e| format!("cannot read cgroup.controllers: {e}"))?; + let mut subtree = vec![]; + if buf.ends_with(b"\n") { + buf.pop(); + } + for controller in buf.split(|&b| b == b' ').filter(|e| !e.is_empty()) { + for &c in controller { + if c <= b' ' || c >= 0x7F { + return Err(format!("Bad byte {c} in cgroup.controllers")); + } + } + if !subtree.is_empty() { + subtree.push(b' '); + } + subtree.push(b'+'); + subtree.extend_from_slice(controller); + } + if !subtree.is_empty() { + cgroup.write_cgroup_value("cgroup.subtree_control", str::from_utf8(&subtree).unwrap())?; + } + Ok(()) +} + +fn purge_cgroup(cgroup: &cgroup::LeafCgroup, cgroup_target: &Path) -> Result<(), String> { + let child = match cgroup.open_cgroup_at(cgroup_target) { + Ok(child_cgroup) => child_cgroup, + Err(Errno::NOENT) => return Ok(()), + Err(other) => { + return Err(format!( + "Cannot open child cgroup {cgroup_target:?}: {other}" + )); + } + }; + child + .wait_for_empty(true) + .map_err(|e| format!("Cannot kill programs in {cgroup_target:?}: {e}"))?; + + cgroup + .delete_child(cgroup_target) + .map_err(|e| format!("Delete child cgroup {cgroup_target:?}: {e}")) +} -- 2.55.0