Skip to main content

starnix_modules_procfs/
pid_directory.rs

1// Copyright 2021 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 crate::fs::{HidePid, ProcFs};
6use itertools::Itertools;
7use regex_lite::Regex;
8use starnix_core::mm::{
9    MemoryAccessor, MemoryAccessorExt, MemoryManager, MemoryStats, PAGE_SIZE, ProcMapsFile,
10    ProcPagemapFile, ProcSmapsFile, ProcSmapsRollupFile, SelfStatsCacheMode,
11};
12use starnix_core::security;
13use starnix_core::task::{
14    CurrentTask, Pid, Task, TaskContainer, TaskEntryScope, TaskStateCode, path_from_root,
15};
16use starnix_core::vfs::buffers::{InputBuffer, OutputBuffer};
17use starnix_core::vfs::pseudo::dynamic_file::{DynamicFile, DynamicFileBuf, DynamicFileSource};
18use starnix_core::vfs::pseudo::simple_directory::SimpleDirectory;
19use starnix_core::vfs::pseudo::simple_file::{
20    BytesFile, BytesFileOps, SimpleFileNode, parse_i32_file, parse_unsigned_file,
21    serialize_for_file,
22};
23use starnix_core::vfs::pseudo::stub_empty_file::StubEmptyFile;
24use starnix_core::vfs::pseudo::vec_directory::{VecDirectory, VecDirectoryEntry};
25use starnix_core::vfs::{
26    CallbackSymlinkNode, CheckAccessReason, CloseFreeSafe, DirEntry, DirEntryOps,
27    DirectoryEntryType, DirentSink, FdFlags, FdNumber, FdTable, FileObject, FileOps,
28    FileSystemHandle, FsNode, FsNodeHandle, FsNodeInfo, FsNodeOps, FsStr, FsString,
29    ProcMountinfoFile, ProcMountsFile, SeekTarget, StatxFlags, SymlinkTarget, default_seek,
30    emit_dotdot, fileops_impl_directory, fileops_impl_noop_sync, fileops_impl_seekable,
31    fileops_impl_unbounded_seek, fs_node_impl_dir_readonly, fs_node_impl_dir_readonly_ops,
32};
33use starnix_logging::{bug_ref, track_stub};
34use starnix_sync::{DynamicLockDepRwLock, LockDepReadGuard};
35
36use starnix_task_command::TaskCommand;
37use starnix_types::time::duration_to_scheduler_clock;
38use starnix_uapi::auth::{
39    CAP_SYS_NICE, CAP_SYS_RESOURCE, PTRACE_MODE_ATTACH_FSCREDS, PTRACE_MODE_NOAUDIT,
40    PTRACE_MODE_READ_FSCREDS, PtraceAccessMode,
41};
42use starnix_uapi::device_id::DeviceId;
43use starnix_uapi::errors::Errno;
44use starnix_uapi::file_mode::{Access, FileMode, mode};
45use starnix_uapi::open_flags::OpenFlags;
46use starnix_uapi::resource_limits::Resource;
47use starnix_uapi::user_address::UserAddress;
48use starnix_uapi::{
49    OOM_ADJUST_MIN, OOM_DISABLE, OOM_SCORE_ADJ_MIN, RLIM_INFINITY, errno, error, ino_t, off_t,
50    pid_t, uapi,
51};
52use std::borrow::Cow;
53use std::ops::{Deref, Range};
54use std::sync::{Arc, LazyLock, Weak};
55
56/// Static table of entries in `/proc/<pid>` and `/proc/<pid>/task/<tid>`.
57const TASK_ENTRIES: &[(&[u8], FileMode)] = &[
58    // NOTE: keep entries in sync with `TaskDirectoryNode::lookup()`.
59    (b"cgroup", mode!(IFREG, 0o444)),
60    (b"cwd", mode!(IFLNK, 0o777)),
61    (b"exe", mode!(IFLNK, 0o777)),
62    (b"fd", mode!(IFDIR, 0o500)),
63    (b"fdinfo", mode!(IFDIR, 0o555)),
64    (b"io", mode!(IFREG, 0o400)),
65    (b"limits", mode!(IFREG, 0o444)),
66    (b"maps", mode!(IFREG, 0o444)),
67    (b"mem", mode!(IFREG, 0o600)),
68    (b"root", mode!(IFLNK, 0o777)),
69    (b"sched", mode!(IFREG, 0o644)),
70    (b"schedstat", mode!(IFREG, 0o444)),
71    (b"smaps", mode!(IFREG, 0o444)),
72    (b"smaps_rollup", mode!(IFREG, 0o444)),
73    (b"stat", mode!(IFREG, 0o444)),
74    (b"statm", mode!(IFREG, 0o444)),
75    (b"status", mode!(IFREG, 0o444)),
76    (b"cmdline", mode!(IFREG, 0o444)),
77    (b"environ", mode!(IFREG, 0o400)),
78    (b"auxv", mode!(IFREG, 0o400)),
79    (b"comm", mode!(IFREG, 0o644)),
80    (b"attr", mode!(IFDIR, 0o555)),
81    (b"ns", mode!(IFDIR, 0o511)),
82    (b"mountinfo", mode!(IFREG, 0o444)),
83    (b"mounts", mode!(IFREG, 0o444)),
84    (b"oom_adj", mode!(IFREG, 0o744)),
85    (b"oom_score", mode!(IFREG, 0o444)),
86    (b"oom_score_adj", mode!(IFREG, 0o744)),
87    (b"timerslack_ns", mode!(IFREG, 0o666)),
88    (b"wchan", mode!(IFREG, 0o444)),
89    (b"clear_refs", mode!(IFREG, 0o200)),
90    (b"pagemap", mode!(IFREG, 0o400)),
91    // "task" must be last so we can dynamically include it for ThreadGroups.
92    (b"task", mode!(IFDIR, 0o555)),
93];
94
95/// Returns entries for the `scope` of a task.
96fn task_entries(scope: TaskEntryScope) -> &'static [(&'static [u8], FileMode)] {
97    match scope {
98        TaskEntryScope::Task => &TASK_ENTRIES[..TASK_ENTRIES.len() - 1],
99        TaskEntryScope::ThreadGroup => TASK_ENTRIES,
100    }
101}
102
103/// Represents a directory node for either `/proc/<pid>` or `/proc/<pid>/task/<tid>`.
104///
105/// This directory lazily creates its child entries to save memory.
106///
107/// It pre-allocates a range of inode numbers (`inode_range`) for all its child entries to mark
108/// them as unchanged when re-accessed.
109/// The `creds` stored within is applied to the directory node itself and child entries.
110pub struct TaskDirectory {
111    target: TaskContainer,
112    inode_range: Range<ino_t>,
113}
114
115#[derive(Clone)]
116struct TaskDirectoryNode {
117    task_directory: Arc<TaskDirectory>,
118}
119
120impl Deref for TaskDirectoryNode {
121    type Target = TaskDirectory;
122
123    fn deref(&self) -> &Self::Target {
124        &self.task_directory
125    }
126}
127
128impl TaskDirectory {
129    fn new(fs: &FileSystemHandle, task: &Arc<Task>, target: TaskContainer) -> FsNodeHandle {
130        let creds = task.real_creds().euid_as_fscred();
131        let scope = target.scope;
132        fs.create_node_and_allocate_node_id(
133            TaskDirectoryNode {
134                task_directory: Arc::new(TaskDirectory {
135                    target,
136                    inode_range: fs.allocate_ino_range(task_entries(scope).len()),
137                }),
138            },
139            FsNodeInfo::new(mode!(IFDIR, 0o555), creds),
140        )
141    }
142}
143
144struct TaskDirectoryDirEntryOps {
145    target: TaskContainer,
146}
147
148impl DirEntryOps for TaskDirectoryDirEntryOps {
149    fn revalidate(&self, current_task: &CurrentTask, dir_entry: &DirEntry) -> Result<bool, Errno> {
150        let Ok(target_task) = self.target.get_task() else {
151            return Ok(false);
152        };
153        if let Some(proc_fs) = dir_entry.node.fs().downcast_ops::<ProcFs>() {
154            if !proc_fs.has_pid_permissions(current_task, &target_task, HidePid::Invisible) {
155                return error!(ENOENT);
156            }
157        }
158        Ok(true)
159    }
160}
161
162macro_rules! task_dir_impl_readonly {
163    () => {
164        fn create_dir_entry_ops(&self) -> Box<dyn DirEntryOps> {
165            Box::new(TaskDirectoryDirEntryOps { target: self.target.clone() })
166        }
167
168        fn check_access(
169            &self,
170            node: &FsNode,
171            current_task: &CurrentTask,
172            permission_flags: security::PermissionFlags,
173            info: &DynamicLockDepRwLock<FsNodeInfo>,
174            reason: CheckAccessReason,
175            audit_context: security::Auditable<'_>,
176        ) -> Result<(), Errno> {
177            if let Some(proc_fs) = node.fs().downcast_ops::<ProcFs>() {
178                proc_fs.check_pid_access(current_task, &self.target, HidePid::NoAccess)?;
179            }
180            if permission_flags.as_access().contains(Access::WRITE) {
181                return error!(EROFS, format!("check_access failed: read-only directory"));
182            }
183            node.default_check_access_impl(
184                current_task,
185                permission_flags,
186                reason,
187                info.read(),
188                audit_context,
189            )
190        }
191
192        fn fetch_and_refresh_info<'a>(
193            &self,
194            node: &FsNode,
195            current_task: &CurrentTask,
196            info: &'a DynamicLockDepRwLock<FsNodeInfo>,
197            _flags: StatxFlags,
198        ) -> Result<LockDepReadGuard<'a, FsNodeInfo>, Errno> {
199            if let (Some(proc_fs), Ok(target_task)) =
200                (node.fs().downcast_ops::<ProcFs>(), self.target.get_task())
201            {
202                if !proc_fs.has_pid_permissions(current_task, &target_task, HidePid::Invisible) {
203                    return error!(ENOENT);
204                }
205            }
206            Ok(info.read())
207        }
208
209        fs_node_impl_dir_readonly_ops!();
210    };
211}
212
213impl FsNodeOps for TaskDirectoryNode {
214    task_dir_impl_readonly!();
215
216    fn create_file_ops(
217        &self,
218        _node: &FsNode,
219        _current_task: &CurrentTask,
220        _flags: OpenFlags,
221    ) -> Result<Box<dyn FileOps>, Errno> {
222        Ok(Box::new(self.clone()))
223    }
224
225    fn lookup(
226        &self,
227        entry: &DirEntry,
228        _current_task: &CurrentTask,
229        name: &FsStr,
230    ) -> Result<FsNodeHandle, Errno> {
231        let _task = self.target.get_task()?;
232        let target = self.target.clone();
233        let creds = entry.node.info().cred();
234        let fs = entry.node.fs();
235        let (mode, ino) = task_entries(self.target.scope)
236            .iter()
237            .enumerate()
238            .find_map(|(index, (n, mode))| {
239                if name == *n {
240                    Some((*mode, self.inode_range.start + index as ino_t))
241                } else {
242                    None
243                }
244            })
245            .ok_or_else(|| errno!(ENOENT))?;
246
247        // NOTE: keep entries in sync with `task_entries()`.
248        let ops: Box<dyn FsNodeOps> = match &**name {
249            b"cgroup" => Box::new(CgroupFile::new_node(target)),
250            b"cwd" => Box::new(CallbackSymlinkNode::new(move || {
251                Ok(SymlinkTarget::Node(target.get_task()?.fs()?.cwd()))
252            })),
253            b"exe" => Box::new(CallbackSymlinkNode::new(move || {
254                let task = target.get_task()?;
255                if let Some(node) = task.mm().ok().and_then(|mm| mm.executable_node()) {
256                    Ok(SymlinkTarget::Node(node))
257                } else {
258                    error!(ENOENT)
259                }
260            })),
261            b"fd" => Box::new(FdDirectory::new(target)),
262            b"fdinfo" => Box::new(FdInfoDirectory::new(target)),
263            b"io" => Box::new(IoFile::new_node()),
264            b"limits" => Box::new(LimitsFile::new_node(target)),
265            b"maps" => {
266                let target_for_file = target.clone();
267                Box::new(PtraceCheckedNode::new_node(
268                    target,
269                    PTRACE_MODE_READ_FSCREDS,
270                    move |task| Ok(ProcMapsFile::new(task, target_for_file.clone())),
271                ))
272            }
273            b"mem" => Box::new(MemFile::new_node(target)),
274            b"root" => Box::new(CallbackSymlinkNode::new(move || {
275                Ok(SymlinkTarget::Node(target.get_task()?.fs()?.root()))
276            })),
277            b"sched" => Box::new(StubEmptyFile::new_node(bug_ref!("https://fxbug.dev/322893980"))),
278            b"schedstat" => {
279                Box::new(StubEmptyFile::new_node(bug_ref!("https://fxbug.dev/322894256")))
280            }
281            b"smaps" => {
282                let target_for_file = target.clone();
283                Box::new(PtraceCheckedNode::new_node(
284                    target,
285                    PTRACE_MODE_READ_FSCREDS,
286                    move |task| Ok(ProcSmapsFile::new(task, target_for_file.clone())),
287                ))
288            }
289            b"smaps_rollup" => {
290                let target_for_file = target.clone();
291                Box::new(PtraceCheckedNode::new_node(
292                    target,
293                    PTRACE_MODE_READ_FSCREDS,
294                    move |task| Ok(ProcSmapsRollupFile::new(task, target_for_file.clone())),
295                ))
296            }
297            b"stat" => Box::new(StatFile::new_node(target)),
298            b"statm" => Box::new(StatmFile::new_node(target)),
299            b"status" => Box::new(StatusFile::new_node(target)),
300            b"cmdline" => Box::new(CmdlineFile::new_node(target)),
301            b"environ" => Box::new(EnvironFile::new_node(target)),
302            b"auxv" => Box::new(AuxvFile::new_node(target)),
303            b"comm" => Box::new(CommFile::new_node(target)),
304            b"attr" => {
305                let dir = SimpleDirectory::new();
306                dir.edit(&fs, |dir| {
307                    for (attr, name) in [
308                        (security::ProcAttr::Current, "current"),
309                        (security::ProcAttr::Exec, "exec"),
310                        (security::ProcAttr::FsCreate, "fscreate"),
311                        (security::ProcAttr::KeyCreate, "keycreate"),
312                        (security::ProcAttr::SockCreate, "sockcreate"),
313                    ] {
314                        dir.entry_etc(
315                            name.into(),
316                            AttrNode::new(target.clone(), attr),
317                            mode!(IFREG, 0o666),
318                            DeviceId::NONE,
319                            creds,
320                        );
321                    }
322                    dir.entry_etc(
323                        "prev".into(),
324                        AttrNode::new(target, security::ProcAttr::Previous),
325                        mode!(IFREG, 0o444),
326                        DeviceId::NONE,
327                        creds,
328                    );
329                });
330                Box::new(dir)
331            }
332            b"ns" => Box::new(NsDirectory::new(target)),
333            b"mountinfo" => Box::new(ProcMountinfoFile::new_node(target)),
334            b"mounts" => Box::new(ProcMountsFile::new_node(target)),
335            b"oom_adj" => Box::new(OomAdjFile::new_node(target)),
336            b"oom_score" => Box::new(OomScoreFile::new_node(target)),
337            b"oom_score_adj" => Box::new(OomScoreAdjFile::new_node(target)),
338            b"timerslack_ns" => Box::new(TimerslackNsFile::new_node(target)),
339            b"wchan" => Box::new(BytesFile::new_node(b"0".to_vec())),
340            b"clear_refs" => Box::new(ClearRefsFile::new_node(target)),
341            b"pagemap" => Box::new(PtraceCheckedNode::new_node_with_current_task(
342                target,
343                PTRACE_MODE_READ_FSCREDS,
344                |current_task, task| Ok(ProcPagemapFile::new(current_task, &task)),
345            )),
346            b"task" => Box::new(TaskListDirectory::new_node(target.pid)),
347            name => unreachable!(
348                "entry \"{:?}\" should be supported to keep in sync with task_entries()",
349                name
350            ),
351        };
352
353        Ok(fs.create_node(ino, ops, FsNodeInfo::new(mode, creds)))
354    }
355}
356
357/// `TaskDirectory` doesn't implement the `close` method.
358impl CloseFreeSafe for TaskDirectory {}
359impl FileOps for TaskDirectory {
360    fileops_impl_directory!();
361    fileops_impl_noop_sync!();
362    fileops_impl_unbounded_seek!();
363
364    fn readdir(
365        &self,
366        file: &FileObject,
367        _current_task: &CurrentTask,
368        sink: &mut dyn DirentSink,
369    ) -> Result<(), Errno> {
370        let _task = self.target.get_task().map_err(|_| errno!(ENOENT))?;
371        emit_dotdot(file, sink)?;
372
373        // Skip through the entries until the current offset is reached.
374        // Subtract 2 from the offset to account for `.` and `..`.
375        for (index, (name, mode)) in
376            task_entries(self.target.scope).iter().enumerate().skip(sink.offset() as usize - 2)
377        {
378            sink.add(
379                self.inode_range.start + index as ino_t,
380                sink.offset() + 1,
381                DirectoryEntryType::from_mode(*mode),
382                (*name).into(),
383            )?;
384        }
385        Ok(())
386    }
387
388    fn as_pid(&self, _file: &FileObject) -> Result<Pid, Errno> {
389        Ok(self.target.pid.clone())
390    }
391}
392
393/// Creates an [`FsNode`] that represents the `/proc/<pid>` directory for `task`.
394pub fn pid_directory(
395    current_task: &CurrentTask,
396    fs: &FileSystemHandle,
397    task: &Arc<Task>,
398    tid: Pid,
399) -> FsNodeHandle {
400    // proc(5): "The files inside each /proc/pid directory are normally
401    // owned by the effective user and effective group ID of the process."
402    let fs_node =
403        TaskDirectory::new(fs, task, TaskContainer::from_thread_group(task.pid.clone(), tid));
404
405    security::task_to_fs_node(current_task, task, &fs_node);
406    fs_node
407}
408
409/// Creates an [`FsNode`] that represents the `/proc/<pid>/task/<tid>` directory for `task`.
410fn tid_directory(fs: &FileSystemHandle, task: &Arc<Task>) -> FsNodeHandle {
411    TaskDirectory::new(fs, task, TaskContainer::from_task(task))
412}
413
414/// `FdDirectory` implements the directory listing operations for a `proc/<pid>/fd` directory.
415///
416/// Reading the directory returns a list of all the currently open file descriptors for the
417/// associated task.
418struct FdDirectory {
419    target: TaskContainer,
420}
421
422impl FdDirectory {
423    fn new(target: TaskContainer) -> Self {
424        Self { target }
425    }
426}
427
428impl FsNodeOps for FdDirectory {
429    fs_node_impl_dir_readonly!();
430
431    fn create_file_ops(
432        &self,
433        _node: &FsNode,
434        _current_task: &CurrentTask,
435        _flags: OpenFlags,
436    ) -> Result<Box<dyn FileOps>, Errno> {
437        let task = self.target.get_task()?;
438        let entries =
439            task.files().map_or_else(|_| Vec::new(), |files| fds_to_directory_entries(&files));
440        Ok(VecDirectory::new_file(entries))
441    }
442
443    fn lookup(
444        &self,
445        entry: &DirEntry,
446        _current_task: &CurrentTask,
447        name: &FsStr,
448    ) -> Result<FsNodeHandle, Errno> {
449        let fd = FdNumber::from_fs_str(name).map_err(|_| errno!(ENOENT))?;
450        let task = self.target.get_task()?;
451        // Make sure that the file descriptor exists before creating the node.
452        let file = task.files()?.get_allowing_opath(fd).map_err(|_| errno!(ENOENT))?;
453        // Derive the symlink's mode from the mode in which the file was opened.
454        // Readable and/or writable file descriptors also set execute permission on the symlink
455        // (0500 for O_RDONLY, 0300 for O_WRONLY, 0700 for O_RDWR, and 0000 for O_PATH).
456        let mut access = Access::from_open_flags(file.flags());
457        if access.is_nontrivial() {
458            access |= Access::EXEC;
459        }
460        let mode = FileMode::IFLNK | access.user_mode();
461        let target = self.target.clone();
462        Ok(entry.node.fs().create_node_and_allocate_node_id(
463            CallbackSymlinkNode::new(move || {
464                let task = target.get_task()?;
465                let file = task.files()?.get_allowing_opath(fd).map_err(|_| errno!(ENOENT))?;
466                Ok(SymlinkTarget::Node(file.name.to_passive()))
467            }),
468            FsNodeInfo::new(mode, task.real_fscred()),
469        ))
470    }
471}
472
473const NS_ENTRIES: &[&str] = &[
474    "cgroup",
475    "ipc",
476    "mnt",
477    "net",
478    "pid",
479    "pid_for_children",
480    "time",
481    "time_for_children",
482    "user",
483    "uts",
484];
485
486/// /proc/<pid>/attr directory entry.
487struct AttrNode {
488    attr: security::ProcAttr,
489    target: TaskContainer,
490}
491
492impl AttrNode {
493    fn new(target: TaskContainer, attr: security::ProcAttr) -> impl FsNodeOps {
494        SimpleFileNode::new(move |_| Ok(AttrNode { attr, target: target.clone() }))
495    }
496}
497
498impl FileOps for AttrNode {
499    fileops_impl_seekable!();
500    fileops_impl_noop_sync!();
501
502    fn writes_update_seek_offset(&self) -> bool {
503        false
504    }
505
506    fn read(
507        &self,
508        _file: &FileObject,
509        current_task: &CurrentTask,
510        offset: usize,
511        data: &mut dyn OutputBuffer,
512    ) -> Result<usize, Errno> {
513        let task = self.target.get_task()?;
514        let response = security::get_procattr(current_task, &task, self.attr)?;
515        data.write(&response[offset..])
516    }
517
518    fn write(
519        &self,
520        _file: &FileObject,
521        current_task: &CurrentTask,
522        offset: usize,
523        data: &mut dyn InputBuffer,
524    ) -> Result<usize, Errno> {
525        let task = self.target.get_task()?;
526
527        // If the current task is not the target then writes are not allowed.
528        if current_task.task != task {
529            return error!(EPERM);
530        }
531        if offset != 0 {
532            return error!(EINVAL);
533        }
534
535        let data = data.read_all()?;
536        let data_len = data.len();
537        security::set_procattr(current_task, self.attr, data.as_slice())?;
538        Ok(data_len)
539    }
540}
541
542/// /proc/[pid]/ns directory
543struct NsDirectory {
544    target: TaskContainer,
545}
546
547impl NsDirectory {
548    fn new(target: TaskContainer) -> Self {
549        Self { target }
550    }
551}
552
553impl FsNodeOps for NsDirectory {
554    fs_node_impl_dir_readonly!();
555
556    fn create_file_ops(
557        &self,
558        _node: &FsNode,
559        _current_task: &CurrentTask,
560        _flags: OpenFlags,
561    ) -> Result<Box<dyn FileOps>, Errno> {
562        // For each namespace, this contains a link to the current identifier of the given namespace
563        // for the current task.
564        Ok(VecDirectory::new_file(
565            NS_ENTRIES
566                .iter()
567                .map(|&name| VecDirectoryEntry {
568                    entry_type: DirectoryEntryType::LNK,
569                    name: FsString::from(name),
570                    inode: None,
571                })
572                .collect(),
573        ))
574    }
575
576    fn lookup(
577        &self,
578        entry: &DirEntry,
579        current_task: &CurrentTask,
580        name: &FsStr,
581    ) -> Result<FsNodeHandle, Errno> {
582        // If name is a given namespace, link to the current identifier of the that namespace for
583        // the current task.
584        // If name is {namespace}:[id], get a file descriptor for the given namespace.
585
586        let name = String::from_utf8(name.to_vec()).map_err(|_| errno!(ENOENT))?;
587        let mut elements = name.split(':');
588        let ns = elements.next().expect("name must not be empty");
589        // The name doesn't starts with a known namespace.
590        if !NS_ENTRIES.contains(&ns) {
591            return error!(ENOENT);
592        }
593
594        let task = self.target.get_task()?;
595        if let Some(id) = elements.next() {
596            // The name starts with {namespace}:, check that it matches {namespace}:[id]
597            static NS_IDENTIFIER_RE: LazyLock<Regex> =
598                LazyLock::new(|| Regex::new("^\\[[0-9]+\\]$").unwrap());
599            if !NS_IDENTIFIER_RE.is_match(id) {
600                return error!(ENOENT);
601            }
602            let node_info = || FsNodeInfo::new(mode!(IFREG, 0o444), task.real_fscred());
603            let fallback = || {
604                entry
605                    .node
606                    .fs()
607                    .create_node_and_allocate_node_id(BytesFile::new_node(vec![]), node_info())
608            };
609            Ok(match ns {
610                "cgroup" => {
611                    track_stub!(TODO("https://fxbug.dev/297313673"), "cgroup namespaces");
612                    fallback()
613                }
614                "ipc" => {
615                    track_stub!(TODO("https://fxbug.dev/297313673"), "ipc namespaces");
616                    fallback()
617                }
618                "mnt" => entry
619                    .node
620                    .fs()
621                    .create_node_and_allocate_node_id(current_task.fs().namespace(), node_info()),
622                "net" => {
623                    track_stub!(TODO("https://fxbug.dev/297313673"), "net namespaces");
624                    fallback()
625                }
626                "pid" => {
627                    track_stub!(TODO("https://fxbug.dev/297313673"), "pid namespaces");
628                    fallback()
629                }
630                "pid_for_children" => {
631                    track_stub!(TODO("https://fxbug.dev/297313673"), "pid_for_children namespaces");
632                    fallback()
633                }
634                "time" => {
635                    track_stub!(TODO("https://fxbug.dev/297313673"), "time namespaces");
636                    fallback()
637                }
638                "time_for_children" => {
639                    track_stub!(
640                        TODO("https://fxbug.dev/297313673"),
641                        "time_for_children namespaces"
642                    );
643                    fallback()
644                }
645                "user" => {
646                    track_stub!(TODO("https://fxbug.dev/297313673"), "user namespaces");
647                    fallback()
648                }
649                "uts" => {
650                    track_stub!(TODO("https://fxbug.dev/297313673"), "uts namespaces");
651                    fallback()
652                }
653                _ => return error!(ENOENT),
654            })
655        } else {
656            // The name is {namespace}, link to the correct one of the current task.
657            let id = current_task.fs().namespace().id;
658            Ok(entry.node.fs().create_node_and_allocate_node_id(
659                CallbackSymlinkNode::new(move || {
660                    Ok(SymlinkTarget::Path(format!("{name}:[{id}]").into()))
661                }),
662                FsNodeInfo::new(mode!(IFLNK, 0o777), task.real_fscred()),
663            ))
664        }
665    }
666}
667
668/// `FdInfoDirectory` implements the directory listing operations for a `proc/<pid>/fdinfo`
669/// directory.
670///
671/// Reading the directory returns a list of all the currently open file descriptors for the
672/// associated task.
673struct FdInfoDirectory {
674    target: TaskContainer,
675}
676
677impl FdInfoDirectory {
678    fn new(target: TaskContainer) -> Self {
679        Self { target }
680    }
681}
682
683impl FsNodeOps for FdInfoDirectory {
684    fs_node_impl_dir_readonly!();
685
686    fn create_file_ops(
687        &self,
688        _node: &FsNode,
689        current_task: &CurrentTask,
690        _flags: OpenFlags,
691    ) -> Result<Box<dyn FileOps>, Errno> {
692        let task = self.target.get_task()?;
693        current_task
694            .check_ptrace_access_mode(PTRACE_MODE_READ_FSCREDS, &task)
695            .map_err(|_| errno!(EACCES))?;
696
697        let entries =
698            task.files().map_or_else(|_| Vec::new(), |files| fds_to_directory_entries(&files));
699        Ok(VecDirectory::new_file(entries))
700    }
701
702    fn lookup(
703        &self,
704        entry: &DirEntry,
705        _current_task: &CurrentTask,
706        name: &FsStr,
707    ) -> Result<FsNodeHandle, Errno> {
708        let task = self.target.get_task()?;
709        let fd = FdNumber::from_fs_str(name).map_err(|_| errno!(ENOENT))?;
710        // Verify that the FD exists at lookup time. We intentionally do not store the
711        // `FileHandle` in `FdInfoFile` to avoid pinning the file in memory after the FD is
712        // closed, and to reflect live `FD_CLOEXEC` flags and closed/reassigned FDs on read.
713        task.files()?.get_allowing_opath(fd).map_err(|_| errno!(ENOENT))?;
714        Ok(entry.node.fs().create_node_and_allocate_node_id(
715            FdInfoFile::new_node(self.target.clone(), fd),
716            FsNodeInfo::new(mode!(IFREG, 0o444), task.real_fscred()),
717        ))
718    }
719}
720
721#[derive(Clone)]
722struct FdInfoFile {
723    target: TaskContainer,
724    fd: FdNumber,
725}
726
727impl FdInfoFile {
728    fn new_node(target: TaskContainer, fd: FdNumber) -> impl FsNodeOps {
729        DynamicFile::new_node(Self { target, fd })
730    }
731}
732
733impl DynamicFileSource for FdInfoFile {
734    fn generate(&self, current_task: &CurrentTask, sink: &mut DynamicFileBuf) -> Result<(), Errno> {
735        let task = self.target.get_task().map_err(|_| errno!(ENOENT))?;
736        let (file, fd_flags) = task
737            .files()
738            .map_err(|_| errno!(ENOENT))?
739            .get_allowing_opath_with_flags(self.fd)
740            .map_err(|_| errno!(ENOENT))?;
741        let pos = file.offset.read();
742        let mut flags = file.flags();
743        flags.set(OpenFlags::CLOEXEC, fd_flags.contains(FdFlags::CLOEXEC));
744        writeln!(sink, "pos:\t{}\nflags:\t0{:o}", pos, flags.bits())?;
745        if let Some(extra_fdinfo) = file.extra_fdinfo(current_task) {
746            sink.write(extra_fdinfo.as_slice());
747        }
748        Ok(())
749    }
750}
751
752fn fds_to_directory_entries(files: &FdTable) -> Vec<VecDirectoryEntry> {
753    let mut buf = itoa::Buffer::new();
754    let mut entries = Vec::new();
755    files.for_each_fd(|fd| {
756        entries.push(VecDirectoryEntry {
757            entry_type: DirectoryEntryType::DIR,
758            name: buf.format(fd.raw()).as_bytes().into(),
759            inode: None,
760        });
761    });
762    entries
763}
764
765/// Directory that lists the task IDs (tid) in a process. Located at `/proc/<pid>/task/`.
766struct TaskListDirectory {
767    target: TaskContainer,
768}
769
770struct TaskListFile {
771    target: TaskContainer,
772    inner: Box<dyn FileOps>,
773}
774
775impl FileOps for TaskListFile {
776    fileops_impl_directory!();
777    fileops_impl_noop_sync!();
778    fileops_impl_unbounded_seek!();
779
780    fn readdir(
781        &self,
782        file: &FileObject,
783        current_task: &CurrentTask,
784        sink: &mut dyn DirentSink,
785    ) -> Result<(), Errno> {
786        let _task = self.target.get_task().map_err(|_| errno!(ENOENT))?;
787        self.inner.readdir(file, current_task, sink)
788    }
789}
790
791impl TaskListDirectory {
792    fn new_node(pid: Pid) -> impl FsNodeOps {
793        Self { target: TaskContainer::from_pid(pid) }
794    }
795}
796
797impl FsNodeOps for TaskListDirectory {
798    task_dir_impl_readonly!();
799
800    fn create_file_ops(
801        &self,
802        _node: &FsNode,
803        _current_task: &CurrentTask,
804        _flags: OpenFlags,
805    ) -> Result<Box<dyn FileOps>, Errno> {
806        let task = self.target.get_task().map_err(|_| errno!(ENOENT))?;
807        let mut buf = itoa::Buffer::new();
808        let inner = VecDirectory::new_file(
809            task.thread_group()
810                .read()
811                .task_ids()
812                .map(|tid| VecDirectoryEntry {
813                    entry_type: DirectoryEntryType::DIR,
814                    name: buf.format(tid).as_bytes().into(),
815                    inode: None,
816                })
817                .collect(),
818        );
819        Ok(Box::new(TaskListFile { target: self.target.clone(), inner }))
820    }
821
822    fn lookup(
823        &self,
824        entry: &DirEntry,
825        current_task: &CurrentTask,
826        name: &FsStr,
827    ) -> Result<FsNodeHandle, Errno> {
828        let target_task = self.target.get_task().map_err(|_| errno!(ENOENT))?;
829        let tid = std::str::from_utf8(name)
830            .map_err(|_| errno!(ENOENT))?
831            .parse::<pid_t>()
832            .map_err(|_| errno!(ENOENT))?;
833
834        let task = current_task.get_task(tid).map_err(|_| errno!(ENOENT))?;
835        // Make sure the tid belongs to this process.
836        if task.pid != target_task.thread_group().leader {
837            return error!(ENOENT);
838        }
839        if let Some(proc_fs) = entry.node.fs().downcast_ops::<ProcFs>() {
840            if !proc_fs.has_pid_permissions(current_task, &task, HidePid::Invisible) {
841                return error!(ENOENT);
842            }
843        }
844
845        Ok(tid_directory(&entry.node.fs(), &task))
846    }
847}
848
849#[derive(Clone)]
850struct CgroupFile {
851    target: TaskContainer,
852}
853impl CgroupFile {
854    pub fn new_node(target: TaskContainer) -> impl FsNodeOps {
855        DynamicFile::new_node(Self { target })
856    }
857}
858impl DynamicFileSource for CgroupFile {
859    fn generate(
860        &self,
861        _current_task: &CurrentTask,
862        sink: &mut DynamicFileBuf,
863    ) -> Result<(), Errno> {
864        let task = self.target.get_task()?;
865        let cgroup1 = task.kernel().cgroups.cgroup1.lock();
866        for (key, root) in &cgroup1.hierarchies {
867            let mut parts: Vec<&str> = key.controllers.iter().map(|c| c.as_str()).collect();
868            let name_storage;
869            if let Some(name) = &key.name {
870                name_storage = format!("name={}", name);
871                parts.push(&name_storage);
872            }
873            let controller_str = parts.join(",");
874            let cgroup = root.get_cgroup(&task.pid);
875            let path = path_from_root(cgroup)?;
876            sink.write(format!("{}:{}:{}\n", root.hierarchy_id, controller_str, path).as_bytes());
877        }
878        let cgroup = task.kernel().cgroups.cgroup2.get_cgroup(&task.pid);
879        let path = path_from_root(cgroup)?;
880        sink.write(format!("0::{}\n", path).as_bytes());
881        Ok(())
882    }
883}
884
885fn fill_buf_from_addr_range(
886    task: &Task,
887    range_start: UserAddress,
888    range_end: UserAddress,
889    sink: &mut DynamicFileBuf,
890) -> Result<(), Errno> {
891    #[allow(clippy::manual_saturating_arithmetic)]
892    let len = range_end.ptr().checked_sub(range_start.ptr()).unwrap_or(0);
893    // NB: If this is exercised in a hot-path, we can plumb the reading task
894    // (`CurrentTask`) here to perform a copy without going through the VMO when
895    // unified aspaces is enabled.
896    let buf = task.read_memory_partial_to_vec(range_start, len)?;
897    sink.write(&buf[..]);
898    Ok(())
899}
900
901/// `CmdlineFile` implements `proc/<pid>/cmdline` file.
902#[derive(Clone)]
903pub struct CmdlineFile {
904    target: TaskContainer,
905}
906impl CmdlineFile {
907    pub fn new_node(target: TaskContainer) -> impl FsNodeOps {
908        DynamicFile::new_node(Self { target })
909    }
910}
911impl DynamicFileSource for CmdlineFile {
912    fn generate(
913        &self,
914        _current_task: &CurrentTask,
915        sink: &mut DynamicFileBuf,
916    ) -> Result<(), Errno> {
917        // Opened cmdline file should still be functional once the task is a zombie.
918        let Ok(task) = self.target.get_task() else {
919            return Ok(());
920        };
921        // /proc/<pid>/cmdline is empty for kthreads.
922        let Ok(mm) = task.mm() else {
923            return Ok(());
924        };
925        let (start, end) = {
926            let mm_state = mm.state.read();
927            (mm_state.argv_start, mm_state.argv_end)
928        };
929        fill_buf_from_addr_range(&task, start, end, sink)
930    }
931}
932
933struct PtraceCheckedNode {}
934
935impl PtraceCheckedNode {
936    pub fn new_node<F, O>(
937        target: TaskContainer,
938        mode: PtraceAccessMode,
939        create_ops: F,
940    ) -> impl FsNodeOps
941    where
942        F: Fn(Arc<Task>) -> Result<O, Errno> + Send + Sync + 'static,
943        O: FileOps,
944    {
945        Self::new_node_with_current_task(target, mode, move |_current_task, task| create_ops(task))
946    }
947
948    pub fn new_node_with_current_task<F, O>(
949        target: TaskContainer,
950        mode: PtraceAccessMode,
951        create_ops: F,
952    ) -> impl FsNodeOps
953    where
954        F: Fn(&CurrentTask, Arc<Task>) -> Result<O, Errno> + Send + Sync + 'static,
955        O: FileOps,
956    {
957        SimpleFileNode::new(move |current_task: &CurrentTask| {
958            let task = target.get_task()?;
959            // proc-pid nodes for kthreads do not require ptrace access checks.
960            if task.mm().is_ok() || task.state_code() == TaskStateCode::Zombie {
961                current_task.check_ptrace_access_mode(mode, &task).map_err(|_| errno!(EACCES))?;
962            }
963            create_ops(current_task, task)
964        })
965    }
966}
967
968/// `EnvironFile` implements `proc/<pid>/environ` file.
969#[derive(Clone)]
970pub struct EnvironFile {
971    target: TaskContainer,
972}
973impl EnvironFile {
974    pub fn new_node(target: TaskContainer) -> impl FsNodeOps {
975        let target_for_file = target.clone();
976        PtraceCheckedNode::new_node(target, PTRACE_MODE_READ_FSCREDS, move |_| {
977            Ok(DynamicFile::new(Self { target: target_for_file.clone() }))
978        })
979    }
980}
981impl DynamicFileSource for EnvironFile {
982    fn generate(
983        &self,
984        _current_task: &CurrentTask,
985        sink: &mut DynamicFileBuf,
986    ) -> Result<(), Errno> {
987        let task = self.target.get_task()?;
988        // /proc/<pid>/environ is empty for kthreads.
989        let Ok(mm) = task.mm() else {
990            return Ok(());
991        };
992        let (start, end) = {
993            let mm_state = mm.state.read();
994            (mm_state.environ_start, mm_state.environ_end)
995        };
996        fill_buf_from_addr_range(&task, start, end, sink)
997    }
998}
999
1000/// `AuxvFile` implements `proc/<pid>/auxv` file.
1001#[derive(Clone)]
1002pub struct AuxvFile {
1003    target: TaskContainer,
1004}
1005impl AuxvFile {
1006    pub fn new_node(target: TaskContainer) -> impl FsNodeOps {
1007        let target_for_file = target.clone();
1008        PtraceCheckedNode::new_node(target, PTRACE_MODE_READ_FSCREDS, move |_| {
1009            Ok(DynamicFile::new(Self { target: target_for_file.clone() }))
1010        })
1011    }
1012}
1013impl DynamicFileSource for AuxvFile {
1014    fn generate(
1015        &self,
1016        _current_task: &CurrentTask,
1017        sink: &mut DynamicFileBuf,
1018    ) -> Result<(), Errno> {
1019        let task = self.target.get_task()?;
1020        // /proc/<pid>/auxv is empty for kthreads.
1021        let Ok(mm) = task.mm() else {
1022            return Ok(());
1023        };
1024        let (start, end) = {
1025            let mm_state = mm.state.read();
1026            (mm_state.auxv_start, mm_state.auxv_end)
1027        };
1028        fill_buf_from_addr_range(&task, start, end, sink)
1029    }
1030}
1031
1032/// `CommFile` implements `proc/<pid>/comm` file.
1033#[derive(Clone)]
1034pub struct CommFile {
1035    target: TaskContainer,
1036}
1037impl CommFile {
1038    pub fn new_node(target: TaskContainer) -> impl FsNodeOps {
1039        DynamicFile::new_node(Self { target })
1040    }
1041}
1042
1043impl DynamicFileSource for CommFile {
1044    fn generate(
1045        &self,
1046        _current_task: &CurrentTask,
1047        sink: &mut DynamicFileBuf,
1048    ) -> Result<(), Errno> {
1049        let task = self.target.get_task()?;
1050        sink.write(task.persistent_info.command_guard().comm_name());
1051        sink.write(b"\n");
1052        Ok(())
1053    }
1054
1055    fn write(
1056        &self,
1057        current_task: &CurrentTask,
1058        _offset: usize,
1059        data: &mut dyn InputBuffer,
1060    ) -> Result<usize, Errno> {
1061        let task = self.target.get_task()?;
1062        if !Arc::ptr_eq(&task.thread_group(), &current_task.thread_group()) {
1063            return error!(EINVAL);
1064        }
1065        // What happens if userspace writes to this file in multiple syscalls? We need more
1066        // detailed tests to see when the data is actually committed back to the task.
1067        let bytes = data.read_all()?;
1068        task.set_command_name(TaskCommand::new(&bytes));
1069        Ok(bytes.len())
1070    }
1071}
1072
1073/// `IoFile` implements `proc/<pid>/io` file.
1074#[derive(Clone)]
1075pub struct IoFile {}
1076impl IoFile {
1077    pub fn new_node() -> impl FsNodeOps {
1078        DynamicFile::new_node(Self {})
1079    }
1080}
1081impl DynamicFileSource for IoFile {
1082    fn generate(
1083        &self,
1084        _current_task: &CurrentTask,
1085        sink: &mut DynamicFileBuf,
1086    ) -> Result<(), Errno> {
1087        track_stub!(TODO("https://fxbug.dev/322874250"), "/proc/pid/io");
1088        sink.write(b"rchar: 0\n");
1089        sink.write(b"wchar: 0\n");
1090        sink.write(b"syscr: 0\n");
1091        sink.write(b"syscw: 0\n");
1092        sink.write(b"read_bytes: 0\n");
1093        sink.write(b"write_bytes: 0\n");
1094        sink.write(b"cancelled_write_bytes: 0\n");
1095        Ok(())
1096    }
1097}
1098
1099/// `LimitsFile` implements `proc/<pid>/limits` file.
1100#[derive(Clone)]
1101pub struct LimitsFile {
1102    target: TaskContainer,
1103}
1104impl LimitsFile {
1105    pub fn new_node(target: TaskContainer) -> impl FsNodeOps {
1106        DynamicFile::new_node(Self { target })
1107    }
1108}
1109impl DynamicFileSource for LimitsFile {
1110    fn generate_locked(
1111        &self,
1112        _current_task: &CurrentTask,
1113        sink: &mut DynamicFileBuf,
1114    ) -> Result<(), Errno> {
1115        let task = self.target.get_task()?;
1116        let limits = task.thread_group().limits.lock();
1117
1118        let write_limit = |sink: &mut DynamicFileBuf, value| {
1119            if value == RLIM_INFINITY as u64 {
1120                sink.write(format!("{:<20}", "unlimited").as_bytes());
1121            } else {
1122                sink.write(format!("{:<20}", value).as_bytes());
1123            }
1124        };
1125        sink.write(
1126            format!("{:<25}{:<20}{:<20}{:<10}\n", "Limit", "Soft Limit", "Hard Limit", "Units")
1127                .as_bytes(),
1128        );
1129        for resource in Resource::ALL {
1130            let desc = resource.desc();
1131            let limit = limits.get(resource);
1132            sink.write(format!("{:<25}", desc.name).as_bytes());
1133            write_limit(sink, limit.rlim_cur);
1134            write_limit(sink, limit.rlim_max);
1135            if !desc.unit.is_empty() {
1136                sink.write(format!("{:<10}", desc.unit).as_bytes());
1137            }
1138            sink.write(b"\n");
1139        }
1140        Ok(())
1141    }
1142}
1143
1144/// `MemFile` implements `proc/<pid>/mem` file.
1145pub struct MemFile {
1146    mm: Weak<MemoryManager>,
1147
1148    // TODO: https://fxbug.dev/442459337 - Tear-down MemoryManager internals on process exit, to
1149    // avoid extension of the MM lifetime prolonging access to memory via "/proc/pid/mem", etc
1150    // beyond that of the actual process/address-space.
1151    target: TaskContainer,
1152}
1153
1154impl MemFile {
1155    pub fn new_node(target: TaskContainer) -> impl FsNodeOps {
1156        let target_for_file = target.clone();
1157        PtraceCheckedNode::new_node(target, PTRACE_MODE_ATTACH_FSCREDS, move |task| {
1158            let mm = task.mm().ok().as_ref().map(Arc::downgrade).unwrap_or_default();
1159            Ok(Self { mm, target: target_for_file.clone() })
1160        })
1161    }
1162}
1163
1164impl FileOps for MemFile {
1165    fileops_impl_noop_sync!();
1166
1167    fn is_seekable(&self) -> bool {
1168        true
1169    }
1170
1171    fn seek(
1172        &self,
1173        _file: &FileObject,
1174        _current_task: &CurrentTask,
1175        current_offset: off_t,
1176        target: SeekTarget,
1177    ) -> Result<off_t, Errno> {
1178        default_seek(current_offset, target, || error!(EINVAL))
1179    }
1180
1181    fn read(
1182        &self,
1183        _file: &FileObject,
1184        current_task: &CurrentTask,
1185        offset: usize,
1186        data: &mut dyn OutputBuffer,
1187    ) -> Result<usize, Errno> {
1188        let Ok(_task) = self.target.get_task() else {
1189            return Ok(0);
1190        };
1191        let Some(mm) = self.mm.upgrade() else {
1192            return Ok(0);
1193        };
1194        let mut addr = UserAddress::from(offset as u64);
1195        data.write_each(&mut |bytes| {
1196            let read_bytes = if current_task.has_same_address_space(Some(&mm)) {
1197                current_task.read_memory_partial(addr, bytes)
1198            } else {
1199                mm.syscall_read_memory_partial(addr, bytes)
1200            }
1201            .map_err(|_| errno!(EIO))?;
1202            let actual = read_bytes.len();
1203            addr = (addr + actual)?;
1204            Ok(actual)
1205        })
1206    }
1207
1208    fn write(
1209        &self,
1210        _file: &FileObject,
1211        current_task: &CurrentTask,
1212        offset: usize,
1213        data: &mut dyn InputBuffer,
1214    ) -> Result<usize, Errno> {
1215        let Ok(_task) = self.target.get_task() else {
1216            return Ok(0);
1217        };
1218        let Some(mm) = self.mm.upgrade() else {
1219            return Ok(0);
1220        };
1221        let addr = UserAddress::from(offset as u64);
1222        let mut written = 0;
1223        let result = data.peek_each(&mut |bytes| {
1224            let actual = if current_task.has_same_address_space(Some(&mm)) {
1225                current_task.write_memory_partial((addr + written)?, bytes)
1226            } else {
1227                mm.syscall_write_memory_partial((addr + written)?, bytes)
1228            }
1229            .map_err(|_| errno!(EIO))?;
1230            written += actual;
1231            Ok(actual)
1232        });
1233        data.advance(written)?;
1234        result
1235    }
1236}
1237
1238const STUBBED_MEM_BYTES: usize = 4096;
1239
1240// Workaround for b/525059309: Zircon VMAR walks (`ZX_INFO_VMAR_MAPS`) are extremely
1241// slow and cause Perfetto's `traced_probes` watchdog timeouts and `lmkd` `cmd_procs_prio`
1242// VMAR lock contention when sweeping thread status files. We bypass them when the reader
1243// is `traced_probes` or `lmkd` by returning a 1 page/KB stub.
1244fn should_skip_memory_stats(current_task: &CurrentTask) -> bool {
1245    let comm = current_task.persistent_info.command_guard();
1246    let name = comm.comm_name();
1247    name == b"traced_probes" || name == b"lmkd"
1248}
1249fn stub_memory_stats() -> MemoryStats {
1250    MemoryStats {
1251        vm_size: STUBBED_MEM_BYTES,
1252        vm_rss: STUBBED_MEM_BYTES,
1253        vm_rss_hwm: STUBBED_MEM_BYTES,
1254        rss_anonymous: STUBBED_MEM_BYTES,
1255        rss_file: 0,
1256        rss_shared: 0,
1257        vm_data: 0,
1258        vm_stack: STUBBED_MEM_BYTES,
1259        vm_exe: STUBBED_MEM_BYTES,
1260        vm_swap: 0,
1261        vm_lck: 0,
1262    }
1263}
1264
1265#[derive(Clone)]
1266pub struct StatFile {
1267    target: TaskContainer,
1268}
1269
1270impl StatFile {
1271    pub fn new_node(target: TaskContainer) -> impl FsNodeOps {
1272        DynamicFile::new_node(Self { target })
1273    }
1274}
1275impl DynamicFileSource for StatFile {
1276    fn generate_locked(
1277        &self,
1278        current_task: &CurrentTask,
1279        sink: &mut DynamicFileBuf,
1280    ) -> Result<(), Errno> {
1281        let task = self.target.get_task()?;
1282
1283        // All fields and their types as specified in the man page.
1284        // Unimplemented fields are set to 0 here.
1285        let pid: pid_t; // 1
1286        let comm: TaskCommand;
1287        let state: char;
1288        let ppid: pid_t;
1289        let pgrp: pid_t; // 5
1290        let session: pid_t;
1291        let tty_nr: i32;
1292        let tpgid: i32 = 0;
1293        let flags: u32 = 0;
1294        let minflt: u64 = 0; // 10
1295        let cminflt: u64 = 0;
1296        let majflt: u64 = 0;
1297        let cmajflt: u64 = 0;
1298        let utime: i64;
1299        let stime: i64; // 15
1300        let cutime: i64;
1301        let cstime: i64;
1302        let mut priority: i64;
1303        let nice: i64;
1304        let num_threads: i64; // 20
1305        let itrealvalue: i64 = 0;
1306        let mut starttime: u64 = 0;
1307        let mut vsize: usize = 0;
1308        let mut rss: usize = 0;
1309        let mut rsslim: u64 = 0; // 25
1310        let mut startcode: u64 = 0;
1311        let mut endcode: u64 = 0;
1312        let mut startstack: usize = 0;
1313        let mut kstkesp: u64 = 0;
1314        let mut kstkeip: u64 = 0; // 30
1315        let signal: u64 = 0;
1316        let blocked: u64 = 0;
1317        let siginore: u64 = 0;
1318        let sigcatch: u64 = 0;
1319        let mut wchan: u64 = 0; // 35
1320        let nswap: u64 = 0;
1321        let cnswap: u64 = 0;
1322        let exit_signal: i32 = 0;
1323        let processor: i32 = 0;
1324        let rt_priority: u32 = 0; // 40
1325        let policy: u32 = 0;
1326        let delayacct_blkio_ticks: u64 = 0;
1327        let guest_time: u64 = 0;
1328        let cguest_time: i64 = 0;
1329        let mut start_data: u64 = 0; // 45
1330        let mut end_data: u64 = 0;
1331        let mut start_brk: u64 = 0;
1332        let mut arg_start: usize = 0;
1333        let mut arg_end: usize = 0;
1334        let mut env_start: usize = 0; // 50
1335        let mut env_end: usize = 0;
1336        let mut exit_code: i32 = 0;
1337
1338        pid = self.target.tid.id;
1339        comm = task.command();
1340        state = task.state_code().code_char();
1341        {
1342            let scheduler_state = task.scheduler_state.lock();
1343            priority = scheduler_state.task_priority();
1344            nice = scheduler_state.normal_priority().as_nice() as i64;
1345        }
1346        if let Some(boost) =
1347            task.kernel().shared_futexes.get_rt_mutex_priority_boost(task.get_tid() as u32)
1348        {
1349            priority = std::cmp::min(priority, boost);
1350        }
1351
1352        {
1353            let thread_group = task.thread_group();
1354            let tg_state = thread_group.read();
1355            ppid = tg_state.get_ppid();
1356            pgrp = thread_group.pgid().id;
1357            session = tg_state.session.id;
1358
1359            // TTY device ID.
1360            tty_nr = tg_state
1361                .controlling_terminal
1362                .as_ref()
1363                .map(|t| t.terminal.device().bits())
1364                .unwrap_or(0) as i32;
1365
1366            cutime = duration_to_scheduler_clock(tg_state.children_time_stats.user_time);
1367            cstime = duration_to_scheduler_clock(tg_state.children_time_stats.system_time);
1368
1369            num_threads = std::cmp::max(1, tg_state.tasks_count()) as i64;
1370        }
1371
1372        let time_stats = match self.target.scope {
1373            TaskEntryScope::Task => task.time_stats(),
1374            TaskEntryScope::ThreadGroup => task.thread_group().time_stats(),
1375        };
1376        utime = duration_to_scheduler_clock(time_stats.user_time);
1377        stime = duration_to_scheduler_clock(time_stats.system_time);
1378
1379        if !task.thread_group().process.is_invalid() {
1380            starttime = duration_to_scheduler_clock(
1381                task.thread_group().start_time - zx::MonotonicInstant::ZERO,
1382            ) as u64;
1383        }
1384
1385        if let Ok(mm) = task.mm() {
1386            // TODO(b/525059309): Bypassed for traced_probes due to VMAR walk slowness. Re-enable when optimized.
1387            let mem_stats = if should_skip_memory_stats(current_task) {
1388                stub_memory_stats()
1389            } else {
1390                mm.get_stats(current_task, SelfStatsCacheMode::BypassCache)
1391            };
1392            let page_size = *PAGE_SIZE as usize;
1393            vsize = mem_stats.vm_size;
1394            rss = mem_stats.vm_rss / page_size;
1395            rsslim = task.thread_group().limits.lock().get(Resource::RSS).rlim_max;
1396
1397            {
1398                let mm_state = mm.state.read();
1399                startstack = mm_state.stack_start.ptr();
1400                arg_start = mm_state.argv_start.ptr();
1401                arg_end = mm_state.argv_end.ptr();
1402                env_start = mm_state.environ_start.ptr();
1403                env_end = mm_state.environ_end.ptr();
1404            }
1405        }
1406
1407        // The man page describes that the following fields have "... values displayed as 0" if the
1408        // caller does not have ptrace read access to the target.
1409        // In practice the `startcode` and `endcode` fields appear to be displayed as 1.
1410        if !current_task
1411            .check_ptrace_access_mode(PTRACE_MODE_READ_FSCREDS | PTRACE_MODE_NOAUDIT, &task)
1412            .is_ok()
1413        {
1414            startcode = 1;
1415            endcode = 1;
1416            startstack = 0;
1417            kstkesp = 0;
1418            kstkeip = 0;
1419            wchan = 0;
1420            start_data = 0;
1421            end_data = 0;
1422            start_brk = 0;
1423            arg_start = 0;
1424            arg_end = 0;
1425            env_start = 0;
1426            env_end = 0;
1427            exit_code = 0;
1428        }
1429
1430        write!(sink, "{pid} (")?;
1431        sink.write(comm.comm_name());
1432        writeln!(
1433            sink,
1434            ") {state} {ppid} {pgrp} {session} {tty_nr} {tpgid} {flags} {minflt} {cminflt} {majflt} {cmajflt} {utime} {stime} {cutime} {cstime} {priority} {nice} {num_threads} {itrealvalue} {starttime} {vsize} {rss} {rsslim} {startcode} {endcode} {startstack} {kstkesp} {kstkeip} {signal} {blocked} {siginore} {sigcatch} {wchan} {nswap} {cnswap} {exit_signal} {processor} {rt_priority} {policy} {delayacct_blkio_ticks} {guest_time} {cguest_time} {start_data} {end_data} {start_brk} {arg_start} {arg_end} {env_start} {env_end} {exit_code}"
1435        )?;
1436
1437        Ok(())
1438    }
1439}
1440
1441#[derive(Clone)]
1442pub struct StatmFile {
1443    target: TaskContainer,
1444}
1445impl StatmFile {
1446    pub fn new_node(target: TaskContainer) -> impl FsNodeOps {
1447        DynamicFile::new_node(Self { target })
1448    }
1449}
1450impl DynamicFileSource for StatmFile {
1451    fn generate(&self, current_task: &CurrentTask, sink: &mut DynamicFileBuf) -> Result<(), Errno> {
1452        // /proc/<pid>/statm reports zeroes for kthreads.
1453        let task = self.target.get_task()?;
1454        // TODO(b/525059309): Bypassed for traced_probes due to VMAR walk slowness. Re-enable when optimized.
1455        let mem_stats = if should_skip_memory_stats(current_task) {
1456            stub_memory_stats()
1457        } else {
1458            match task.mm() {
1459                Ok(mm) => mm.get_stats(current_task, SelfStatsCacheMode::BypassCache),
1460                Err(_) => Default::default(),
1461            }
1462        };
1463        let page_size = *PAGE_SIZE as usize;
1464
1465        // 5th and 7th fields are deprecated and should be set to 0.
1466        writeln!(
1467            sink,
1468            "{} {} {} {} 0 {} 0",
1469            mem_stats.vm_size / page_size,
1470            mem_stats.vm_rss / page_size,
1471            mem_stats.rss_shared / page_size,
1472            mem_stats.vm_exe / page_size,
1473            (mem_stats.vm_data + mem_stats.vm_stack) / page_size
1474        )?;
1475        Ok(())
1476    }
1477}
1478
1479#[derive(Clone)]
1480pub struct StatusFile {
1481    target: TaskContainer,
1482}
1483impl StatusFile {
1484    pub fn new_node(target: TaskContainer) -> impl FsNodeOps {
1485        DynamicFile::new_node(Self { target })
1486    }
1487}
1488impl DynamicFileSource for StatusFile {
1489    fn generate(&self, current_task: &CurrentTask, sink: &mut DynamicFileBuf) -> Result<(), Errno> {
1490        let start_monotonic = zx::MonotonicInstant::get();
1491        let start_boot = zx::BootInstant::get();
1492        let task = self.target.get_task()?;
1493        let creds_string = {
1494            // Collect everything stored in info in this block.  There is a lock ordering
1495            // issue with the task lock acquired below, and cloning info is
1496            // expensive.
1497            write!(sink, "Name:\t")?;
1498            sink.write(task.persistent_info.command_guard().comm_name());
1499            let creds = task.persistent_info.real_creds();
1500            format!(
1501                "Uid:\t{}\t{}\t{}\t{}\nGid:\t{}\t{}\t{}\t{}\nGroups:\t{}",
1502                creds.uid,
1503                creds.euid,
1504                creds.saved_uid,
1505                creds.fsuid,
1506                creds.gid,
1507                creds.egid,
1508                creds.saved_gid,
1509                creds.fsgid,
1510                creds.groups.iter().map(|n| n.to_string()).join(" ")
1511            )
1512        };
1513
1514        writeln!(sink)?;
1515
1516        if let Ok(fs) = task.fs() {
1517            writeln!(sink, "Umask:\t0{:03o}", fs.umask().bits())?;
1518        }
1519        {
1520            let task_state = task.read();
1521            writeln!(sink, "SigBlk:\t{:016x}", task_state.signal_mask().0)?;
1522            writeln!(sink, "SigPnd:\t{:016x}", task_state.task_specific_pending_signals().0)?;
1523            writeln!(
1524                sink,
1525                "ShdPnd:\t{:016x}",
1526                task.thread_group().pending_signals.lock().pending().0
1527            )?;
1528
1529            let (sigign, sigcgt) = task.thread_group().signal_actions.ignored_and_caught();
1530            writeln!(sink, "SigIgn:\t{:016x}", sigign.0)?;
1531            writeln!(sink, "SigCgt:\t{:016x}", sigcgt.0)?;
1532
1533            writeln!(sink, "NoNewPrivs:\t{}", task_state.no_new_privs() as u8)?;
1534        }
1535
1536        // Since version 3.8 all nonexistent capabilities are reported as not-enabled.
1537        let creds = task.real_creds();
1538        writeln!(sink, "CapInh:\t{:016x}", creds.cap_inheritable)?;
1539        writeln!(sink, "CapPrm:\t{:016x}", creds.cap_permitted)?;
1540        writeln!(sink, "CapEff:\t{:016x}", creds.cap_effective)?;
1541        writeln!(sink, "CapBnd:\t{:016x}", creds.cap_bounding)?;
1542        writeln!(sink, "CapAmb:\t{:016x}", creds.cap_ambient)?;
1543
1544        let state_code = task.state_code();
1545        writeln!(sink, "State:\t{} ({})", state_code.code_char(), state_code.name())?;
1546
1547        writeln!(sink, "Tgid:\t{}", task.pid)?;
1548        writeln!(sink, "Pid:\t{}", self.target.tid)?;
1549        let (ppid, threads, tracer_pid) = {
1550            let tracer_pid =
1551                task.read().ptrace.as_ref().map_or(0, |p| {
1552                    p.core_state.thread_group.upgrade().map_or(0, |tg| tg.leader.id)
1553                });
1554            let task_group = task.thread_group().read();
1555            (task_group.get_ppid(), task_group.tasks_count(), tracer_pid)
1556        };
1557        writeln!(sink, "PPid:\t{}", ppid)?;
1558        writeln!(sink, "TracerPid:\t{}", tracer_pid)?;
1559
1560        writeln!(sink, "{}", creds_string)?;
1561
1562        if let Ok(mm) = task.mm() {
1563            // TODO(b/525059309): Bypassed for traced_probes due to VMAR walk slowness. Re-enable when optimized.
1564            let mem_stats = if should_skip_memory_stats(current_task) {
1565                stub_memory_stats()
1566            } else {
1567                mm.get_stats(current_task, SelfStatsCacheMode::AllowCached)
1568            };
1569            writeln!(sink, "VmSize:\t{} kB", mem_stats.vm_size / 1024)?;
1570            writeln!(sink, "VmLck:\t{} kB", mem_stats.vm_lck / 1024)?;
1571            writeln!(sink, "VmRSS:\t{} kB", mem_stats.vm_rss / 1024)?;
1572            writeln!(sink, "RssAnon:\t{} kB", mem_stats.rss_anonymous / 1024)?;
1573            writeln!(sink, "RssFile:\t{} kB", mem_stats.rss_file / 1024)?;
1574            writeln!(sink, "RssShmem:\t{} kB", mem_stats.rss_shared / 1024)?;
1575            writeln!(sink, "VmData:\t{} kB", mem_stats.vm_data / 1024)?;
1576            writeln!(sink, "VmStk:\t{} kB", mem_stats.vm_stack / 1024)?;
1577            writeln!(sink, "VmExe:\t{} kB", mem_stats.vm_exe / 1024)?;
1578            writeln!(sink, "VmSwap:\t{} kB", mem_stats.vm_swap / 1024)?;
1579            writeln!(sink, "VmHWM:\t{} kB", mem_stats.vm_rss_hwm / 1024)?;
1580        }
1581        // Report seccomp filter status.
1582        let seccomp = task.seccomp_filter_state.get() as u8;
1583        writeln!(sink, "Seccomp:\t{}", seccomp)?;
1584
1585        // There should be at least one thread in Zombie processes.
1586        writeln!(sink, "Threads:\t{}", std::cmp::max(1, threads))?;
1587
1588        let elapsed_monotonic = zx::MonotonicInstant::get() - start_monotonic;
1589        let elapsed_boot = zx::BootInstant::get() - start_boot;
1590        if elapsed_monotonic > zx::MonotonicDuration::from_millis(100)
1591            || elapsed_boot > zx::BootDuration::from_seconds(1)
1592        {
1593            let target_pid = task.pid.id;
1594            let target_comm =
1595                String::from_utf8_lossy(task.persistent_info.command_guard().comm_name())
1596                    .into_owned();
1597            starnix_logging::log_warn!(
1598                "StatusFile::generate for task {} ({}) took {} ms (monotonic), {} ms (boot)",
1599                target_pid,
1600                target_comm,
1601                elapsed_monotonic.into_millis(),
1602                elapsed_boot.into_millis()
1603            );
1604        }
1605
1606        Ok(())
1607    }
1608}
1609
1610struct OomScoreFile {
1611    target: TaskContainer,
1612}
1613
1614impl OomScoreFile {
1615    fn new_node(target: TaskContainer) -> impl FsNodeOps {
1616        BytesFile::new_node(Self { target })
1617    }
1618}
1619
1620impl BytesFileOps for OomScoreFile {
1621    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
1622        let _task = self.target.get_task()?;
1623        track_stub!(TODO("https://fxbug.dev/322873459"), "/proc/pid/oom_score");
1624        Ok(serialize_for_file(0).into())
1625    }
1626}
1627
1628// Redefine these constants as i32 to avoid conversions below.
1629const OOM_ADJUST_MAX: i32 = uapi::OOM_ADJUST_MAX as i32;
1630const OOM_SCORE_ADJ_MAX: i32 = uapi::OOM_SCORE_ADJ_MAX as i32;
1631
1632struct OomAdjFile {
1633    target: TaskContainer,
1634}
1635impl OomAdjFile {
1636    fn new_node(target: TaskContainer) -> impl FsNodeOps {
1637        BytesFile::new_node(Self { target })
1638    }
1639}
1640
1641impl BytesFileOps for OomAdjFile {
1642    fn write(&self, current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
1643        let value = parse_i32_file(&data)?;
1644        let oom_score_adj = if value == OOM_DISABLE {
1645            OOM_SCORE_ADJ_MIN
1646        } else {
1647            if !(OOM_ADJUST_MIN..=OOM_ADJUST_MAX).contains(&value) {
1648                return error!(EINVAL);
1649            }
1650            let fraction = (value - OOM_ADJUST_MIN) / (OOM_ADJUST_MAX - OOM_ADJUST_MIN);
1651            fraction * (OOM_SCORE_ADJ_MAX - OOM_SCORE_ADJ_MIN) + OOM_SCORE_ADJ_MIN
1652        };
1653        security::check_task_capable(current_task, CAP_SYS_RESOURCE)?;
1654        let task = self.target.get_task()?;
1655        task.write().oom_score_adj = oom_score_adj;
1656        Ok(())
1657    }
1658
1659    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
1660        let task = self.target.get_task()?;
1661        let oom_score_adj = task.read().oom_score_adj;
1662        let oom_adj = if oom_score_adj == OOM_SCORE_ADJ_MIN {
1663            OOM_DISABLE
1664        } else {
1665            let fraction =
1666                (oom_score_adj - OOM_SCORE_ADJ_MIN) / (OOM_SCORE_ADJ_MAX - OOM_SCORE_ADJ_MIN);
1667            fraction * (OOM_ADJUST_MAX - OOM_ADJUST_MIN) + OOM_ADJUST_MIN
1668        };
1669        Ok(serialize_for_file(oom_adj).into())
1670    }
1671}
1672
1673struct OomScoreAdjFile {
1674    target: TaskContainer,
1675}
1676
1677impl OomScoreAdjFile {
1678    fn new_node(target: TaskContainer) -> impl FsNodeOps {
1679        BytesFile::new_node(Self { target })
1680    }
1681}
1682
1683impl BytesFileOps for OomScoreAdjFile {
1684    fn write(&self, current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
1685        let value = parse_i32_file(&data)?;
1686        if !(OOM_SCORE_ADJ_MIN..=OOM_SCORE_ADJ_MAX).contains(&value) {
1687            return error!(EINVAL);
1688        }
1689        security::check_task_capable(current_task, CAP_SYS_RESOURCE)?;
1690        let task = self.target.get_task()?;
1691        task.write().oom_score_adj = value;
1692        Ok(())
1693    }
1694
1695    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
1696        let task = self.target.get_task()?;
1697        let oom_score_adj = task.read().oom_score_adj;
1698        Ok(serialize_for_file(oom_score_adj).into())
1699    }
1700}
1701
1702struct TimerslackNsFile {
1703    target: TaskContainer,
1704}
1705
1706impl TimerslackNsFile {
1707    fn new_node(target: TaskContainer) -> impl FsNodeOps {
1708        BytesFile::new_node(Self { target })
1709    }
1710}
1711
1712impl BytesFileOps for TimerslackNsFile {
1713    fn write(&self, current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
1714        let target_task = self.target.get_task()?;
1715        let same_task = current_task.task.pid == target_task.pid;
1716        if !same_task {
1717            security::check_task_capable(current_task, CAP_SYS_NICE)?;
1718            security::check_task_setscheduler_access(current_task, &target_task)?;
1719        };
1720
1721        let value = parse_unsigned_file(&data)?;
1722        target_task.write().set_timerslack_ns(value);
1723        Ok(())
1724    }
1725
1726    fn read(&self, current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
1727        let target_task = self.target.get_task()?;
1728        let same_task = current_task.task.pid == target_task.pid;
1729        if !same_task {
1730            security::check_task_capable(current_task, CAP_SYS_NICE)?;
1731            security::check_task_getscheduler_access(current_task, &target_task)?;
1732        };
1733
1734        let timerslack_ns = target_task.read().timerslack_ns;
1735        Ok(serialize_for_file(timerslack_ns).into())
1736    }
1737}
1738
1739struct ClearRefsFile {
1740    target: TaskContainer,
1741}
1742
1743impl ClearRefsFile {
1744    fn new_node(target: TaskContainer) -> impl FsNodeOps {
1745        BytesFile::new_node(Self { target })
1746    }
1747}
1748
1749impl BytesFileOps for ClearRefsFile {
1750    fn write(&self, _current_task: &CurrentTask, _data: Vec<u8>) -> Result<(), Errno> {
1751        let _task = self.target.get_task()?;
1752        track_stub!(TODO("https://fxbug.dev/396221597"), "/proc/pid/clear_refs");
1753        Ok(())
1754    }
1755}
1756#[cfg(test)]
1757mod tests {
1758    use super::*;
1759    use starnix_core::testing::{anon_test_file, spawn_kernel_and_run};
1760    use starnix_core::vfs::VecOutputBuffer;
1761
1762    #[::fuchsia::test]
1763    async fn test_fdinfo_file_lazy_and_cloexec() {
1764        spawn_kernel_and_run(async move |current_task| {
1765            let target_file = anon_test_file(
1766                current_task,
1767                Box::new(BytesFile::new(b"0123456789".to_vec())),
1768                OpenFlags::RDONLY,
1769            );
1770            let fd = current_task
1771                .files()
1772                .add(current_task, target_file.clone(), FdFlags::empty())
1773                .expect("add fd");
1774
1775            let fdinfo_file = anon_test_file(
1776                current_task,
1777                Box::new(DynamicFile::new(FdInfoFile {
1778                    target: TaskContainer::from_task(&current_task.task),
1779                    fd,
1780                })),
1781                OpenFlags::RDONLY,
1782            );
1783
1784            // 1. Initial read reflects offset 0 and no O_CLOEXEC.
1785            let mut buf = VecOutputBuffer::new(256);
1786            let n = fdinfo_file.read_at(current_task, 0, &mut buf).expect("read fdinfo");
1787            let text = String::from_utf8(buf.data()[..n].to_vec()).unwrap();
1788            let expected_initial = format!("pos:\t0\nflags:\t0{:o}\n", OpenFlags::RDONLY.bits());
1789            assert_eq!(text, expected_initial);
1790
1791            // 2. Advancing offset and setting FD_CLOEXEC are reflected lazily on next read from 0.
1792            target_file.seek(current_task, SeekTarget::Set(5)).expect("seek");
1793            current_task
1794                .files()
1795                .set_fd_flags_allowing_opath(fd, FdFlags::CLOEXEC)
1796                .expect("set cloexec");
1797
1798            let mut buf2 = VecOutputBuffer::new(256);
1799            let n2 = fdinfo_file.read_at(current_task, 0, &mut buf2).expect("read fdinfo after");
1800            let text2 = String::from_utf8(buf2.data()[..n2].to_vec()).unwrap();
1801            let expected_updated = format!(
1802                "pos:\t5\nflags:\t0{:o}\n",
1803                (OpenFlags::RDONLY | OpenFlags::CLOEXEC).bits()
1804            );
1805            assert_eq!(text2, expected_updated);
1806
1807            // 3. Closing the FD does not keep `target_file` pinned in `FdInfoFile` and subsequent
1808            // reads return ENOENT.
1809            current_task.files().close(fd).expect("close fd");
1810            let mut buf3 = VecOutputBuffer::new(256);
1811            assert_eq!(fdinfo_file.read_at(current_task, 0, &mut buf3), error!(ENOENT));
1812        })
1813        .await;
1814    }
1815}