runtime/backlog/cgroup.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Each Linux run's child cgroup: the weight every run holds, and the
7//! memory limit when the arm asked for one (`crates/runtime/spec/Tools/Exec.lean` D29, D33).
8//!
9//! cgroup v2 is a filesystem, so this module is plain file reads and
10//! writes: the harness moves itself into `core` under its own cgroup —
11//! a cgroup that holds processes may not hand out its own resources —
12//! opens `cpu` and `memory` in the parent's `cgroup.subtree_control`,
13//! and gives each run a child named `run-<RunId>`. The child holds
14//! `cpu.weight` 100, the same for every run, and `memory.max` when the
15//! arm asked for a limit; each command of the run writes its pid into
16//! the child's `cgroup.procs`.
17//!
18//! The parent is a parameter rather than a constant, so the tests run
19//! a whole fake `/sys/fs/cgroup` under the temp directory on any
20//! platform, and this file says nothing about what a real machine's
21//! cgroup happens to be. [`platform_shares`] is the one place the
22//! doctor and the wiring read whether this machine's cgroup is
23//! delegated, and a machine that has none, or may not write it, sets
24//! nothing and says so instead.
25
26#[cfg(any(not(windows), test))]
27use std::collections::BTreeMap;
28use std::path::{Path, PathBuf};
29#[cfg(any(not(windows), test))]
30use std::sync::OnceLock;
31
32#[cfg(any(not(windows), test))]
33use kernel::RunId;
34
35#[cfg(any(not(windows), test))]
36use super::Shares;
37
38/// The weight every run's child cgroup holds, from 1 to 10000: the same
39/// for every run, so the runs share the processors evenly among
40/// themselves (`crates/runtime/spec/Tools/Exec.lean` D29).
41#[cfg(any(not(windows), test))]
42const RUN_CPU_WEIGHT: u64 = 100;
43
44/// Which halves of a run's shares this machine can set (D29).
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum PlatformShares {
47 /// Nothing can be set here.
48 None,
49 /// The CPU half alone: macOS's `taskpolicy`.
50 Cpu,
51 /// Both halves: a Windows job, or a Linux cgroup this process may
52 /// write.
53 CpuAndMemory,
54}
55
56/// What this machine can give a run of what an arm asks for (D29, D33).
57///
58/// Windows sets both halves on the run's job, macOS the CPU half
59/// through `taskpolicy`, and Linux sets both where the harness's own
60/// cgroup may be written and nothing where it may not. The doctor and
61/// the backlog read this one answer, so the two cannot tell a machine
62/// different things.
63#[must_use]
64pub fn platform_shares() -> PlatformShares {
65 if cfg!(windows) {
66 PlatformShares::CpuAndMemory
67 } else if cfg!(target_os = "macos") {
68 PlatformShares::Cpu
69 } else {
70 match own_cgroup() {
71 Some(parent) if writable(&parent) => PlatformShares::CpuAndMemory,
72 _ => PlatformShares::None,
73 }
74 }
75}
76
77/// Each run's child cgroup, made under the harness's own (D29, D33).
78///
79/// No Windows run ever holds one — [`own_cgroup`] answers `None` there —
80/// but the tests drive the same type against a fake root on every
81/// platform, so it exists wherever a test build compiles.
82#[cfg(any(not(windows), test))]
83#[derive(Default)]
84pub(super) struct Cgroups {
85 /// The parent the runs' children are made under, once it was found
86 /// writable; `None` where this machine has none.
87 parent: Option<PathBuf>,
88 /// Set once adoption was attempted, so a machine without a
89 /// delegated cgroup asks once rather than once per command.
90 adopted: bool,
91 /// The shares each run holds.
92 runs: BTreeMap<RunId, Shares>,
93}
94
95#[cfg(any(not(windows), test))]
96impl Cgroups {
97 /// Adopts `parent`: the harness moves itself into `parent/core` and
98 /// `parent/cgroup.subtree_control` opens `cpu` and `memory`, so the
99 /// runs' children can hold both. A `parent` that may not be written
100 /// adopts nothing, and its runs read as unshared (D33).
101 ///
102 /// The tests' door onto the same adoption: a real machine reaches it
103 /// through [`Cgroups::this_machine`] and its own delegated parent, so
104 /// this takes the root a test lent it, which no build outside a test
105 /// has a caller for.
106 #[cfg(test)]
107 pub(super) fn adopt(parent: &Path, pid: u32) -> Cgroups {
108 let mut cgroups = Cgroups {
109 parent: None,
110 adopted: true,
111 runs: BTreeMap::new(),
112 };
113 if writable(parent) && core(parent, pid).is_ok() {
114 cgroups.parent = Some(parent.to_path_buf());
115 }
116 cgroups
117 }
118
119 /// The cgroup this process is in on this machine, adopted on first
120 /// use (D33).
121 fn this_machine() -> Cgroups {
122 Cgroups {
123 parent: adopted(),
124 adopted: true,
125 runs: BTreeMap::new(),
126 }
127 }
128
129 /// Puts `pid` into `run`'s child cgroup, making the child on first
130 /// use with the shares `asked` for, and answers the shares the run
131 /// now holds: `asked` when the cgroup took them, `Unset` when it
132 /// did not (D29).
133 pub(super) fn enter(&mut self, run: RunId, pid: u32, asked: Shares) -> Shares {
134 if asked == Shares::Unset {
135 return Shares::Unset;
136 }
137 if !self.adopted {
138 *self = Cgroups::this_machine();
139 }
140 let Some(parent) = &self.parent else {
141 return Shares::Unset;
142 };
143 let dir = parent.join(format!("run-{run}"));
144 let held = match self.runs.get(&run) {
145 Some(held) => *held,
146 None => {
147 if !made(&dir, asked) {
148 return Shares::Unset;
149 }
150 self.runs.insert(run, asked);
151 asked
152 }
153 };
154 // Every command of the run joins its cgroup. One whose pid
155 // cannot be written still runs under its parent's shares, and
156 // it is one more command of this run read as itself only —
157 // which `unfollowed` already says for every Unix command (D29).
158 match std::fs::write(dir.join("cgroup.procs"), pid.to_string()) {
159 Ok(()) | Err(_) => held,
160 }
161 }
162
163 /// The shares `run` holds; `Unset` for a run this machine never
164 /// gave a child cgroup.
165 pub(super) fn held(&self, run: RunId) -> Shares {
166 self.runs.get(&run).copied().unwrap_or_default()
167 }
168
169 /// Lets the run's record go; its cgroup directory stays, because a
170 /// cgroup that holds a process cannot be removed (D33).
171 pub(super) fn forget(&mut self, run: RunId) {
172 self.runs.remove(&run);
173 }
174}
175
176/// Makes the run's child cgroup under `parent` and writes the shares
177/// `asked` for: `cpu.weight` for every run, `memory.max` only for a
178/// limit. `false` when a write was refused, which leaves the run
179/// unshared rather than failing its command (D29).
180#[cfg(any(not(windows), test))]
181fn made(dir: &Path, asked: Shares) -> bool {
182 let weighed = std::fs::create_dir_all(dir)
183 .and_then(|()| std::fs::write(dir.join("cpu.weight"), RUN_CPU_WEIGHT.to_string()));
184 match asked {
185 Shares::CpuAndMemory { limit } => weighed
186 .and_then(|()| std::fs::write(dir.join("memory.max"), limit.get().to_string()))
187 .is_ok(),
188 Shares::Cpu | Shares::Unset => weighed.is_ok(),
189 }
190}
191
192/// Moves this process into `parent/core` and opens `cpu` and `memory`
193/// for the runs' children: a cgroup that holds processes may not enable
194/// its own controllers, so the harness leaves the parent first (D29).
195#[cfg(any(not(windows), test))]
196fn core(parent: &Path, pid: u32) -> std::io::Result<()> {
197 let inner = parent.join("core");
198 std::fs::create_dir_all(&inner)?;
199 std::fs::write(inner.join("cgroup.procs"), pid.to_string())?;
200 std::fs::write(parent.join("cgroup.subtree_control"), "+cpu +memory")
201}
202
203/// The parent the runs' cgroups of this process are made under: the
204/// harness's own cgroup, adopted once on first use, or `None` where
205/// this machine has none it may write (D33).
206#[cfg(any(not(windows), test))]
207fn adopted() -> Option<PathBuf> {
208 static PARENT: OnceLock<Option<PathBuf>> = OnceLock::new();
209 PARENT
210 .get_or_init(|| {
211 let parent = own_cgroup()?;
212 if !writable(&parent) {
213 return None;
214 }
215 core(&parent, std::process::id()).ok()?;
216 Some(parent)
217 })
218 .clone()
219}
220
221/// This process's own cgroup on this machine: the `0::` line of
222/// `/proc/self/cgroup`, under the cgroup v2 mount point. `None` where
223/// the platform has no such file or no such line (D29).
224fn own_cgroup() -> Option<PathBuf> {
225 #[cfg(target_os = "linux")]
226 {
227 let raw = std::fs::read_to_string("/proc/self/cgroup").ok()?;
228 let relative = raw.lines().find_map(|line| line.strip_prefix("0::"))?;
229 Some(Path::new("/sys/fs/cgroup").join(relative.trim_start_matches('/')))
230 }
231 #[cfg(not(target_os = "linux"))]
232 {
233 None
234 }
235}
236
237/// Whether `parent` accepts the writes adoption makes: both files are
238/// opened for writing and closed, which changes nothing (D33).
239fn writable(parent: &Path) -> bool {
240 ["cgroup.procs", "cgroup.subtree_control"]
241 .iter()
242 .all(|name| {
243 std::fs::OpenOptions::new()
244 .write(true)
245 .open(parent.join(name))
246 .is_ok()
247 })
248}
249
250#[cfg(test)]
251mod tests;