syd 3.52.0

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/kernel/getdents.rs: getdents64(2) handler
//
// Copyright (c) 2023, 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use libseccomp::ScmpNotifResp;
use nix::{errno::Errno, NixPath};

use crate::{
    compat::getdents64, config::DIRENT_BUF_SIZE, fd::to_fd, kernel::sandbox_path,
    lookup::CanonicalPath, req::UNotifyEventRequest, sandbox::Capability,
};

pub(crate) fn sys_getdents64(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_handler!(request, |request: UNotifyEventRequest| {
        let req = request.scmpreq;

        // Validate file descriptor.
        //
        // AT_FDCWD is an invalid file descriptor.
        let fd = to_fd(req.data.args[0])?;

        // Get remote fd, and
        // Readlink /proc/thread-self/fd/$fd.
        //
        // Readdir access check here has been moved to the _open_(2) handler
        // for simplicity and efficiency. The Stat check still takes place.
        let fd = request.get_fd(fd)?;
        let mut path = CanonicalPath::new_fd(fd.into(), req.pid())?;
        if !path.is_dir() {
            return Err(Errno::ENOTDIR);
        }

        #[expect(clippy::disallowed_methods)]
        let fd = path.dir.take().unwrap();
        let mut dir = path.take();

        // Linux kernel truncates upper bits.
        #[expect(clippy::cast_possible_truncation)]
        let count = req.data.args[2] as u32;

        // The count argument to the getdents64(2) call must not be
        // fully trusted, it can be overly large, and allocating a
        // Vector of that capacity may overflow.
        let count = (count as usize).min(DIRENT_BUF_SIZE);

        // There's no guarantee on the order of items returned by
        // getdents64(2), therefore we must potentially check each
        // element for dot or dotdot, until we actually see them.
        let mut seen_dot = false;
        let mut seen_dotdot = false;

        let pid = req.pid();
        let len = dir.len();
        let mut ret: usize = 0;
        while ret == 0 {
            // Zero size returns EINVAL only if file is not at EOF,
            // otherwise it returns 0. We must ask the kernel to make
            // sure we return the correct value.
            let siz = count.checked_sub(ret).ok_or(Errno::EOVERFLOW)?;
            let mut entries = match getdents64(&fd, siz) {
                Ok(entries) => entries,
                Err(Errno::ECANCELED) => break, // EOF or empty directory
                Err(errno) => return Err(errno),
            };

            // Lock sandbox for read to perform Stat access check.
            let sandbox = request.get_sandbox();
            let check_flags = (*sandbox.flags, *sandbox.options).into();

            for entry in &mut entries {
                // Allow the special dot entries `.` and `..`. `..` may
                // point to a denylisted directory, however at this
                // point there's not much we can do: even the root
                // directory, ie `/`, has a `..`. In this exceptional
                // case `..` points to `.`.
                if !seen_dot && entry.is_dot() {
                    seen_dot = true;
                } else if !seen_dotdot && entry.is_dotdot() {
                    seen_dotdot = true;
                } else {
                    // Append entry name to the directory.
                    dir.push(entry.name_bytes());

                    // Run XPath::check() with file type for global restrictions.
                    if dir
                        .check(
                            pid,
                            Some(&entry.file_type()),
                            Some(entry.as_xpath()),
                            check_flags,
                        )
                        .is_err()
                    {
                        // Skip entry.
                        dir.truncate(len);
                        continue;
                    }

                    // Run sandbox access check with Stat capability.
                    let hide = sandbox_path(
                        Some(&request),
                        &sandbox,
                        request.scmpreq.pid(), // Unused when request.is_some()
                        &dir,
                        Capability::CAP_STAT,
                        "getdents64",
                    )
                    .is_err();

                    // Restore directory entry.
                    dir.truncate(len);

                    if hide {
                        // Skip entry.
                        continue;
                    }
                }

                // Access granted, write entry to sandbox process memory.
                // Handle truncation as necessary.
                let buf = entry.as_bytes();
                let siz = count.checked_sub(ret).ok_or(Errno::EOVERFLOW)?;
                let siz = buf.len().min(siz);
                let ptr = req.data.args[1]
                    .checked_add(ret as u64)
                    .ok_or(Errno::EOVERFLOW)?;
                match request.write_mem_all(&buf[..siz], ptr) {
                    Ok(()) => {
                        ret = ret.checked_add(siz).ok_or(Errno::EOVERFLOW)?;
                        if siz != entry.size() || ret >= count {
                            break;
                        }
                    }
                    Err(_) if ret > 0 => break,
                    Err(errno) => return Err(errno),
                };
            }
        }

        #[expect(clippy::cast_possible_wrap)]
        Ok(request.return_syscall(ret as i64))
    })
}