Skip to main content

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;