Skip to main content

starnix_modules_cgroupfs/
fs.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use starnix_core::security;
6use starnix_core::task::{Cgroup, CgroupOps, CgroupRoot, CgroupV1Key, ControllerType, CurrentTask};
7use starnix_core::vfs::{
8    CacheMode, FileSystem, FileSystemHandle, FileSystemOps, FileSystemOptions, FsNodeHandle,
9    FsNodeInfo, FsStr,
10};
11use starnix_logging::log_warn;
12use starnix_sync::{CgroupDirectoryNodesLock, LockDepMutex};
13use starnix_types::vfs::default_statfs;
14use starnix_uapi::auth::FsCred;
15use starnix_uapi::errors::Errno;
16use starnix_uapi::{CGROUP_SUPER_MAGIC, CGROUP2_SUPER_MAGIC, errno, error, mode, statfs};
17
18use std::collections::{BTreeSet, HashMap};
19use std::sync::{Arc, Weak};
20
21use crate::directory::{CgroupDirectory, CgroupDirectoryHandle};
22
23pub struct CgroupV1Fs {
24    pub root: Arc<CgroupRoot>,
25
26    /// All directory nodes of the filesystem.
27    pub dir_nodes: Arc<DirectoryNodes>,
28
29    /// The name of this filesystem, which is also the name of the cgroup v1 hierarchy.
30    /// E.g., "cgroup" or "cpuset".
31    pub name: &'static FsStr,
32
33    /// The key identifying this hierarchy in the global cgroup v1 state.
34    pub hierarchy_key: CgroupV1Key,
35}
36
37impl CgroupV1Fs {
38    pub fn new_fs(
39        current_task: &CurrentTask,
40        options: FileSystemOptions,
41    ) -> Result<FileSystemHandle, Errno> {
42        Self::new_fs_inner(current_task, options, b"cgroup".into())
43    }
44
45    pub fn new_fs_cpuset(
46        current_task: &CurrentTask,
47        options: FileSystemOptions,
48    ) -> Result<FileSystemHandle, Errno> {
49        Self::new_fs_inner(current_task, options, b"cpuset".into())
50    }
51
52    fn new_fs_inner(
53        current_task: &CurrentTask,
54        options: FileSystemOptions,
55        fs_name: &'static FsStr,
56    ) -> Result<FileSystemHandle, Errno> {
57        let kernel = current_task.kernel();
58
59        let mut params = options.params.clone();
60        // Eat LSM/SELinux options (like `context=`) before running validation. Android's
61        // `libprocessgroup` mounts cgroups with these options. If they are not stripped
62        // by an active LSM, they would be treated as invalid controllers and fail the
63        // strict validation below.
64        let _ = security::sb_eat_lsm_opts(kernel, &mut params)?;
65
66        let name = params.get(b"name").map(|n| String::from_utf8_lossy(n.as_ref()).to_string());
67
68        let mut controllers = BTreeSet::new();
69        for key in params.keys() {
70            let key_str = String::from_utf8_lossy(key.as_ref());
71            if let Ok(controller) = key_str.parse::<ControllerType>() {
72                controllers.insert(controller);
73            } else {
74                // Ignore common cgroup options that we don't support yet to avoid spamming warnings.
75                // TODO(https://fxbug.dev/322255433): Support these options.
76                if key_str != "name"
77                    && key_str != "none"
78                    && key_str != "noprefix"
79                    && key_str != "cpuset_v2_mode"
80                    && key_str != "clone_children"
81                {
82                    log_warn!("cgroup v1: invalid controller or option: {}", key_str);
83                }
84                continue;
85            }
86        }
87
88        if params.get(b"none").is_some() && !controllers.is_empty() {
89            return error!(EINVAL);
90        }
91
92        if fs_name == b"cpuset" {
93            // cpuset filesystem only allows cpuset controller.
94            if !controllers.is_empty()
95                && (controllers.len() > 1 || !controllers.contains(&ControllerType::Cpuset))
96            {
97                return error!(EINVAL);
98            }
99            controllers.insert(ControllerType::Cpuset);
100        }
101
102        if controllers.is_empty() && name.is_none() {
103            log_warn!("Mounting cgroup v1 without controllers or name is not supported");
104            return error!(EINVAL);
105        }
106
107        let root = kernel.cgroups.get_or_create_cgroup1(&controllers, name.as_deref())?;
108
109        let hierarchy_key = CgroupV1Key { controllers, name };
110
111        let dir_nodes =
112            DirectoryNodes::new(Arc::downgrade(&root), CgroupVersion::V1(hierarchy_key.clone()));
113        let root_dir = dir_nodes.root.clone();
114        let fs = FileSystem::new(
115            kernel,
116            CacheMode::Uncached,
117            CgroupV1Fs {
118                dir_nodes: dir_nodes.clone(),
119                root: root.clone(),
120                name: fs_name,
121                hierarchy_key,
122            },
123            options,
124        )?;
125        root_dir.create_root_interface_files(&fs);
126        let root_ino = fs.allocate_ino();
127        fs.create_root(root_ino, root_dir);
128
129        // Populate existing child cgroups if any (e.g. on remount).
130        dir_nodes.populate_from_root(&fs, &root, FsCred::root())?;
131
132        Ok(fs)
133    }
134}
135impl FileSystemOps for CgroupV1Fs {
136    fn name(&self) -> &'static FsStr {
137        self.name
138    }
139    fn statfs(&self, _fs: &FileSystem, _current_task: &CurrentTask) -> Result<statfs, Errno> {
140        Ok(default_statfs(CGROUP_SUPER_MAGIC))
141    }
142}
143
144pub struct CgroupV2Fs {
145    /// All directory nodes of the filesystem.
146    pub dir_nodes: Arc<DirectoryNodes>,
147}
148
149struct CgroupV2FsHandle(FileSystemHandle);
150pub fn cgroup2_fs(
151    current_task: &CurrentTask,
152    options: FileSystemOptions,
153) -> Result<FileSystemHandle, Errno> {
154    Ok(current_task
155        .kernel()
156        .expando
157        .get_or_try_init(|| Ok(CgroupV2FsHandle(CgroupV2Fs::new_fs(current_task, options)?)))?
158        .0
159        .clone())
160}
161
162impl CgroupV2Fs {
163    fn new_fs(
164        current_task: &CurrentTask,
165        options: FileSystemOptions,
166    ) -> Result<FileSystemHandle, Errno> {
167        let kernel = current_task.kernel();
168        let dir_nodes =
169            DirectoryNodes::new(Arc::downgrade(&kernel.cgroups.cgroup2), CgroupVersion::V2);
170        let root = dir_nodes.root.clone();
171        let fs = FileSystem::new(kernel, CacheMode::Uncached, CgroupV2Fs { dir_nodes }, options)?;
172        root.create_root_interface_files(&fs);
173        let root_ino = fs.allocate_ino();
174        fs.create_root(root_ino, root);
175        Ok(fs)
176    }
177}
178
179impl FileSystemOps for CgroupV2Fs {
180    fn name(&self) -> &'static FsStr {
181        b"cgroup2".into()
182    }
183    fn statfs(&self, _fs: &FileSystem, _current_task: &CurrentTask) -> Result<statfs, Errno> {
184        Ok(default_statfs(CGROUP2_SUPER_MAGIC))
185    }
186}
187
188/// Represents all directory nodes of a cgroup hierarchy.
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub enum CgroupVersion {
191    V1(CgroupV1Key),
192    V2,
193}
194
195pub struct DirectoryNodes {
196    /// `CgroupRoot`'s directory handle. The `FileSystem` owns the `FsNode` of the root, and so we
197    /// do not have a `FsNodeHandle` of the root.
198    root: CgroupDirectoryHandle,
199
200    /// All non-root cgroup directories, keyed by cgroup's ID. Every non-root cgroup has a
201    /// corresponding node.
202    nodes: LockDepMutex<HashMap<u64, FsNodeHandle>, CgroupDirectoryNodesLock>,
203
204    /// The version of cgroup for this hierarchy (v1 or v2).
205    pub version: CgroupVersion,
206}
207
208impl DirectoryNodes {
209    pub fn new(root_cgroup: Weak<CgroupRoot>, version: CgroupVersion) -> Arc<DirectoryNodes> {
210        Arc::new_cyclic(|weak_self| Self {
211            root: CgroupDirectory::new_root(root_cgroup, weak_self.clone()),
212            nodes: Default::default(),
213            version,
214        })
215    }
216
217    /// Looks for the corresponding node in the filesystem, errors if not found.
218    pub fn get_node(&self, cgroup: &Arc<Cgroup>) -> Result<FsNodeHandle, Errno> {
219        let nodes = self.nodes.lock();
220        nodes.get(&cgroup.id()).cloned().ok_or_else(|| errno!(ENOENT))
221    }
222
223    /// Returns the corresponding nodes for a set of cgroups.
224    pub fn get_nodes(&self, cgroups: &Vec<Arc<Cgroup>>) -> Vec<Option<FsNodeHandle>> {
225        let nodes = self.nodes.lock();
226        cgroups.iter().map(|cgroup| nodes.get(&cgroup.id()).cloned()).collect()
227    }
228
229    /// Creates a new `FsNode` for `directory` and stores it in `nodes`.
230    pub fn add_node(
231        &self,
232        cgroup: &Arc<Cgroup>,
233        directory: CgroupDirectoryHandle,
234        fs: &FileSystemHandle,
235        owner: FsCred,
236    ) -> FsNodeHandle {
237        let id = cgroup.id();
238        let node = fs.create_node_and_allocate_node_id(
239            directory,
240            FsNodeInfo::new(mode!(IFDIR, 0o755), owner),
241        );
242        let mut nodes = self.nodes.lock();
243        nodes.insert(id, node.clone());
244        node
245    }
246
247    /// Removes an entry from `nodes`, errors if not found.
248    pub fn remove_node(&self, cgroup: &Arc<Cgroup>) -> Result<FsNodeHandle, Errno> {
249        let id = cgroup.id();
250        let mut nodes = self.nodes.lock();
251        nodes.remove(&id).ok_or_else(|| errno!(ENOENT))
252    }
253
254    pub fn populate_from_root(
255        self: &Arc<Self>,
256        fs: &FileSystemHandle,
257        root: &Arc<CgroupRoot>,
258        owner: FsCred,
259    ) -> Result<(), Errno> {
260        let children = root.get_children()?;
261        for child in children {
262            self.populate_recursive(fs, &child, owner.clone())?;
263        }
264        Ok(())
265    }
266
267    fn populate_recursive(
268        self: &Arc<Self>,
269        fs: &FileSystemHandle,
270        cgroup: &Arc<Cgroup>,
271        owner: FsCred,
272    ) -> Result<(), Errno> {
273        let directory = CgroupDirectory::new(
274            Arc::downgrade(cgroup) as Weak<dyn CgroupOps>,
275            fs,
276            self,
277            owner.clone(),
278        );
279        self.add_node(cgroup, directory, fs, owner.clone());
280
281        let children = cgroup.get_children()?;
282        for child in children {
283            self.populate_recursive(fs, &child, owner.clone())?;
284        }
285        Ok(())
286    }
287}
288
289#[cfg(test)]
290mod test {
291    use super::*;
292    use starnix_core::testing::spawn_kernel_and_run;
293    use starnix_core::vfs::FsNodeOps;
294    use starnix_core::vfs::fs_args::MountParams;
295    use starnix_core::vfs::fs_registry::FsRegistry;
296    use starnix_uapi::file_mode::FileMode;
297
298    #[::fuchsia::test]
299    async fn test_filesystem_creates_nodes() {
300        spawn_kernel_and_run(async move |current_task| {
301            let kernel = current_task.kernel();
302            let registry = kernel.expando.get::<FsRegistry>();
303            registry.register(b"cgroup2".into(), cgroup2_fs);
304
305            let fs = current_task
306                .create_filesystem(b"cgroup2".into(), Default::default())
307                .expect("create_filesystem");
308
309            let cgroupfs = fs.downcast_ops::<CgroupV2Fs>().expect("downcast_ops");
310            let dir_nodes = cgroupfs.dir_nodes.clone();
311            assert!(dir_nodes.nodes.lock().is_empty(), "new filesystem does not contain nodes");
312
313            let root_dir = dir_nodes.root.clone();
314            assert!(root_dir.has_interface_files(), "root directory is initialized");
315        })
316        .await;
317    }
318    #[::fuchsia::test]
319    async fn test_cgroup_v1_remount_preserves_tree() {
320        spawn_kernel_and_run(async move |current_task| {
321            let kernel = current_task.kernel();
322            let registry = kernel.expando.get::<FsRegistry>();
323            registry.register(b"cgroup".into(), CgroupV1Fs::new_fs);
324
325            let options = FileSystemOptions {
326                params: MountParams::parse(b"memory".into()).unwrap(),
327                ..Default::default()
328            };
329
330            {
331                let fs1 = current_task
332                    .create_filesystem(b"cgroup".into(), options.clone())
333                    .expect("create_filesystem");
334                let cgroupfs1 = fs1.downcast_ops::<CgroupV1Fs>().expect("downcast_ops");
335                let dir_nodes1 = cgroupfs1.dir_nodes.clone();
336                let root_dir1 = dir_nodes1.root.clone();
337                let root_node1 = fs1.root();
338
339                root_dir1
340                    .mkdir(
341                        &root_node1.node,
342                        current_task,
343                        "test_child".into(),
344                        FileMode::default(),
345                        FsCred::root(),
346                    )
347                    .expect("mkdir");
348
349                let lookup_result1 =
350                    root_dir1.lookup(&root_node1.node, current_task, "test_child".into());
351                assert!(lookup_result1.is_ok());
352            }
353
354            let fs2 = current_task
355                .create_filesystem(b"cgroup".into(), options)
356                .expect("create_filesystem");
357            let cgroupfs2 = fs2.downcast_ops::<CgroupV1Fs>().expect("downcast_ops");
358            let dir_nodes2 = cgroupfs2.dir_nodes.clone();
359            let root_dir2 = dir_nodes2.root.clone();
360            let root_node2 = fs2.root();
361
362            let lookup_result2 =
363                root_dir2.lookup(&root_node2.node, current_task, "test_child".into());
364            assert!(lookup_result2.is_ok(), "child cgroup should be preserved on remount");
365        })
366        .await;
367    }
368}