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