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::file_mode::FileMode;
17use starnix_uapi::{CGROUP_SUPER_MAGIC, CGROUP2_SUPER_MAGIC, errno, error, mode, statfs};
18
19use std::collections::{BTreeSet, HashMap};
20use std::sync::{Arc, Weak};
21
22use crate::directory::{CgroupDirectory, CgroupDirectoryHandle};
23
24pub struct CgroupV1Fs {
25    /// Root cgroup of this hierarchy, held to keep the hierarchy alive while mounted.
26    _root: Arc<CgroupRoot>,
27
28    /// All directory nodes of the filesystem. Held to keep the [`DirectoryNodes`] allocation
29    /// alive while [`CgroupDirectory`] nodes hold `Weak` references to it.
30    _dir_nodes: Arc<DirectoryNodes>,
31
32    /// Name of this filesystem, which is also the name of the cgroup v1 hierarchy.
33    /// E.g., "cgroup" or "cpuset".
34    name: &'static FsStr,
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));
113        let root_dir = dir_nodes.root.clone();
114        let fs = FileSystem::new(
115            kernel,
116            CacheMode::Uncached,
117            CgroupV1Fs { _dir_nodes: dir_nodes.clone(), _root: root.clone(), name: fs_name },
118            options,
119        )?;
120        root_dir.create_root_interface_files(&fs);
121        let root_ino = fs.allocate_ino();
122        fs.create_root_with_info(
123            root_ino,
124            root_dir,
125            FsNodeInfo::new(mode!(IFDIR, 0o555), FsCred::root()),
126        );
127
128        // Populate existing child cgroups if any (e.g. on remount).
129        dir_nodes.populate_from_root(&fs, &root, FsCred::root())?;
130
131        Ok(fs)
132    }
133}
134impl FileSystemOps for CgroupV1Fs {
135    fn name(&self) -> &'static FsStr {
136        self.name
137    }
138    fn statfs(&self, _fs: &FileSystem, _current_task: &CurrentTask) -> Result<statfs, Errno> {
139        Ok(default_statfs(CGROUP_SUPER_MAGIC))
140    }
141}
142
143pub struct CgroupV2Fs {
144    /// All directory nodes of the filesystem. Held to keep the [`DirectoryNodes`] allocation
145    /// alive while [`CgroupDirectory`] nodes hold `Weak` references to it.
146    _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(
172            kernel,
173            CacheMode::Uncached,
174            CgroupV2Fs { _dir_nodes: dir_nodes },
175            options,
176        )?;
177        root.create_root_interface_files(&fs);
178        let root_ino = fs.allocate_ino();
179        fs.create_root_with_info(
180            root_ino,
181            root,
182            FsNodeInfo::new(mode!(IFDIR, 0o555), FsCred::root()),
183        );
184        Ok(fs)
185    }
186}
187
188impl FileSystemOps for CgroupV2Fs {
189    fn name(&self) -> &'static FsStr {
190        b"cgroup2".into()
191    }
192    fn statfs(&self, _fs: &FileSystem, _current_task: &CurrentTask) -> Result<statfs, Errno> {
193        Ok(default_statfs(CGROUP2_SUPER_MAGIC))
194    }
195}
196
197/// Version of a cgroup hierarchy.
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub(crate) enum CgroupVersion {
200    V1(CgroupV1Key),
201    V2,
202}
203
204/// All directory nodes of a cgroup hierarchy.
205pub(crate) struct DirectoryNodes {
206    /// [`CgroupRoot`]'s directory handle. The [`FileSystem`] owns the `FsNode` of the root, and so
207    /// we do not have an [`FsNodeHandle`] of the root.
208    root: CgroupDirectoryHandle,
209
210    /// All non-root cgroup directories, keyed by cgroup's ID. Every non-root cgroup has a
211    /// corresponding node.
212    nodes: LockDepMutex<HashMap<u64, FsNodeHandle>, CgroupDirectoryNodesLock>,
213
214    /// Version of cgroup for this hierarchy (v1 or v2).
215    pub(crate) version: CgroupVersion,
216}
217
218impl DirectoryNodes {
219    pub(crate) fn new(
220        root_cgroup: Weak<CgroupRoot>,
221        version: CgroupVersion,
222    ) -> Arc<DirectoryNodes> {
223        Arc::new_cyclic(|weak_self| Self {
224            root: CgroupDirectory::new_root(root_cgroup, weak_self.clone()),
225            nodes: Default::default(),
226            version,
227        })
228    }
229
230    /// Looks for the corresponding node in the filesystem, errors if not found.
231    pub(crate) fn get_node(&self, cgroup: &Arc<Cgroup>) -> Result<FsNodeHandle, Errno> {
232        let nodes = self.nodes.lock();
233        nodes.get(&cgroup.id()).cloned().ok_or_else(|| errno!(ENOENT))
234    }
235
236    /// Returns the corresponding nodes for a set of cgroups.
237    pub(crate) fn get_nodes(&self, cgroups: &Vec<Arc<Cgroup>>) -> Vec<Option<FsNodeHandle>> {
238        let nodes = self.nodes.lock();
239        cgroups.iter().map(|cgroup| nodes.get(&cgroup.id()).cloned()).collect()
240    }
241
242    /// Creates a [`CgroupDirectory`] and [`FsNodeHandle`] for `cgroup` with `mode` ([`FileMode`])
243    /// and `owner`, and stores the node in [`Self::nodes`].
244    pub(crate) fn add_node(
245        self: &Arc<Self>,
246        cgroup: &Arc<Cgroup>,
247        fs: &FileSystemHandle,
248        mode: FileMode,
249        owner: FsCred,
250    ) -> FsNodeHandle {
251        let directory = CgroupDirectory::new(
252            Arc::downgrade(cgroup) as Weak<dyn CgroupOps>,
253            fs,
254            self,
255            owner.clone(),
256        );
257        // Per `mkdir(2)`: "The argument mode specifies the file mode bits to be
258        // applied when a new directory is created."
259        let node = fs.create_node_and_allocate_node_id(
260            directory,
261            FsNodeInfo::new(mode.with_type(FileMode::IFDIR), owner),
262        );
263        self.nodes.lock().insert(cgroup.id(), node.clone());
264        node
265    }
266
267    /// Removes an entry from [`Self::nodes`], errors if not found.
268    pub(crate) fn remove_node(&self, cgroup: &Arc<Cgroup>) -> Result<FsNodeHandle, Errno> {
269        let id = cgroup.id();
270        let mut nodes = self.nodes.lock();
271        nodes.remove(&id).ok_or_else(|| errno!(ENOENT))
272    }
273
274    fn populate_from_root(
275        self: &Arc<Self>,
276        fs: &FileSystemHandle,
277        root: &Arc<CgroupRoot>,
278        owner: FsCred,
279    ) -> Result<(), Errno> {
280        let children = root.get_children()?;
281        for child in children {
282            self.populate_recursive(fs, &child, owner.clone())?;
283        }
284        Ok(())
285    }
286
287    fn populate_recursive(
288        self: &Arc<Self>,
289        fs: &FileSystemHandle,
290        cgroup: &Arc<Cgroup>,
291        owner: FsCred,
292    ) -> Result<(), Errno> {
293        self.add_node(cgroup, fs, mode!(IFDIR, 0o755), owner.clone());
294
295        let children = cgroup.get_children()?;
296        for child in children {
297            self.populate_recursive(fs, &child, owner.clone())?;
298        }
299        Ok(())
300    }
301}
302
303#[cfg(test)]
304mod test {
305    use super::*;
306    use starnix_core::testing::spawn_kernel_and_run;
307    use starnix_core::vfs::FsNodeOps;
308    use starnix_core::vfs::fs_args::MountParams;
309    use starnix_core::vfs::fs_registry::FsRegistry;
310    use starnix_uapi::file_mode::FileMode;
311    use starnix_uapi::fs_type::FileSystemTypeFlags;
312
313    #[::fuchsia::test]
314    async fn test_filesystem_creates_nodes() {
315        spawn_kernel_and_run(async move |current_task| {
316            let kernel = current_task.kernel();
317            let registry = kernel.expando.get::<FsRegistry>();
318            registry.register(b"cgroup2".into(), FileSystemTypeFlags::empty(), cgroup2_fs);
319
320            let fs = current_task
321                .create_filesystem(b"cgroup2".into(), Default::default())
322                .expect("create_filesystem");
323
324            let cgroupfs = fs.downcast_ops::<CgroupV2Fs>().expect("downcast_ops");
325            let dir_nodes = cgroupfs._dir_nodes.clone();
326            assert!(dir_nodes.nodes.lock().is_empty(), "new filesystem does not contain nodes");
327
328            let root_dir = dir_nodes.root.clone();
329            assert!(root_dir.has_interface_files(), "root directory is initialized");
330        })
331        .await;
332    }
333    #[::fuchsia::test]
334    async fn test_cgroup_v1_remount_preserves_tree() {
335        spawn_kernel_and_run(async move |current_task| {
336            let kernel = current_task.kernel();
337            let registry = kernel.expando.get::<FsRegistry>();
338            registry.register(b"cgroup".into(), FileSystemTypeFlags::empty(), CgroupV1Fs::new_fs);
339
340            let options = FileSystemOptions {
341                params: MountParams::parse(b"memory".into()).unwrap(),
342                ..Default::default()
343            };
344
345            {
346                let fs1 = current_task
347                    .create_filesystem(b"cgroup".into(), options.clone())
348                    .expect("create_filesystem");
349                let cgroupfs1 = fs1.downcast_ops::<CgroupV1Fs>().expect("downcast_ops");
350                let dir_nodes1 = cgroupfs1._dir_nodes.clone();
351                let root_dir1 = dir_nodes1.root.clone();
352                let root_node1 = fs1.root();
353
354                root_dir1
355                    .mkdir(
356                        &root_node1.node,
357                        current_task,
358                        "test_child".into(),
359                        FileMode::default(),
360                        FsCred::root(),
361                    )
362                    .expect("mkdir");
363
364                let lookup_result1 =
365                    root_dir1.lookup(root_node1, current_task, "test_child".into());
366                assert!(lookup_result1.is_ok());
367            }
368
369            let fs2 = current_task
370                .create_filesystem(b"cgroup".into(), options)
371                .expect("create_filesystem");
372            let cgroupfs2 = fs2.downcast_ops::<CgroupV1Fs>().expect("downcast_ops");
373            let dir_nodes2 = cgroupfs2._dir_nodes.clone();
374            let root_dir2 = dir_nodes2.root.clone();
375            let root_node2 = fs2.root();
376
377            let lookup_result2 = root_dir2.lookup(root_node2, current_task, "test_child".into());
378            assert!(lookup_result2.is_ok(), "child cgroup should be preserved on remount");
379        })
380        .await;
381    }
382}