syd 3.54.1

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/kernel/readlink.rs: readlink syscall handlers
//
// Copyright (c) 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::os::fd::AsRawFd;

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

use crate::{
    confine::is_valid_ptr,
    cookie::{CookieIdx, SYSCOOKIE_POOL},
    id::SydId,
    kernel::sandbox_path,
    lookup::{FileType, FsFlags},
    magic::ProcMagic,
    path::{XPathBuf, PATH_MAX},
    proc::proc_tgid,
    req::{SysArg, SysFlags, UNotifyEventRequest},
    sandbox::Capability,
};

const READLINK_MAX: usize = PATH_MAX * 16;

pub(crate) fn sys_readlink(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // Return EINVAL for zero/negative size.
    // Cap untrusted size to a maximum.
    // Linux kernel truncates upper bits.
    #[expect(clippy::cast_possible_truncation)]
    let size = match usize::try_from(req.data.args[2] as i32) {
        Ok(0) => return request.fail_syscall(Errno::EINVAL),
        Ok(size) => size.min(READLINK_MAX),
        Err(_) => return request.fail_syscall(Errno::EINVAL),
    };

    // Return EFAULT for invalid path pointer.
    if !is_valid_ptr(req.data.args[0], req.data.arch) {
        return request.fail_syscall(Errno::EFAULT);
    }

    let arg = SysArg {
        path: Some(0),
        fsflags: FsFlags::MUST_PATH | FsFlags::NO_FOLLOW_LAST,
        ..Default::default()
    };

    syscall_readlink_handler(request, arg, 1, size)
}

pub(crate) fn sys_readlinkat(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // Return EINVAL for zero/negative size.
    // Cap untrusted size to a maximum.
    // Linux kernel truncates upper bits.
    #[expect(clippy::cast_possible_truncation)]
    let size = match usize::try_from(req.data.args[3] as i32) {
        Ok(0) => return request.fail_syscall(Errno::EINVAL),
        Ok(size) => size.min(READLINK_MAX),
        Err(_) => return request.fail_syscall(Errno::EINVAL),
    };

    // Return EFAULT for invalid path pointer.
    if !is_valid_ptr(req.data.args[1], req.data.arch) {
        return request.fail_syscall(Errno::EFAULT);
    }

    let arg = SysArg {
        dirfd: Some(0),
        path: Some(1),
        flags: SysFlags::EMPTY_PATH | SysFlags::PASS_DELETE,
        fsflags: FsFlags::MUST_PATH | FsFlags::NO_FOLLOW_LAST,
    };

    syscall_readlink_handler(request, arg, 2, size)
}

fn syscall_readlink_handler(
    request: UNotifyEventRequest,
    arg: SysArg,
    buf_idx: usize,
    buf_siz: usize,
) -> ScmpNotifResp {
    syscall_handler!(request, |request: UNotifyEventRequest| {
        let req = request.scmpreq;
        let sandbox = request.get_sandbox();

        // Read the remote path.
        let (path, _, empty_path) = request.read_path(&sandbox, arg)?;

        // Check for access, allow access to fd-only calls.
        if !empty_path && sandbox.enabled(Capability::CAP_READLINK) {
            let sysname = if buf_idx == 1 {
                "readlink"
            } else {
                "readlinkat"
            };
            sandbox_path(
                Some(&request),
                &sandbox,
                request.scmpreq.pid(), // Unused when request.is_some()
                path.abs(),
                Capability::CAP_READLINK,
                sysname,
            )?;
        }

        if let Some(file_type) = &path.typ {
            // Return EINVAL/ENOENT for non-symlinks.
            if !matches!(file_type, FileType::Lnk | FileType::MagicLnk(_)) {
                return if empty_path {
                    // readlinkat(2) on empty path.
                    Err(Errno::ENOENT)
                } else {
                    Err(Errno::EINVAL)
                };
            }

            // Handle magic symlinks as necessary.
            //
            // FileType::Lnk checks are necessary for fd-only calls.
            let maybe_magic_self = match file_type {
                FileType::MagicLnk(ProcMagic::Pid { pid }) => Some((*pid, None)),
                FileType::Lnk if path.abs().is_proc_self(false) => {
                    Some((request.scmpreq.pid(), None))
                }
                FileType::MagicLnk(ProcMagic::Tid { tgid, pid }) => Some((*pid, Some(*tgid))),
                FileType::Lnk if path.abs().is_proc_self(true) => {
                    // Determine thread group id.
                    let pid = request.scmpreq.pid();
                    let tgid = proc_tgid(pid)?;

                    // Validate request after proc(5) read.
                    if !request.is_valid() {
                        return Err(Errno::ESRCH);
                    }

                    Some((pid, Some(tgid)))
                }
                _ => None,
            };

            if let Some((pid, maybe_tgid)) = maybe_magic_self {
                let buf = if let Some(tgid) = maybe_tgid {
                    XPathBuf::from_task(tgid, pid)
                } else {
                    XPathBuf::from_pid(pid)
                }?;

                let buf = buf.as_bytes();
                let siz = buf.len().min(buf_siz);
                request.write_mem_all(&buf[..siz], req.data.args[buf_idx])?;
                #[expect(clippy::cast_possible_wrap)]
                return Ok(request.return_syscall(siz as i64));
            }
        }

        // We use MUST_PATH, dir refers to the file.
        assert!(path.base().is_empty()); // MUST_PATH!
        let fd = path.dir();

        // Check for invalid buffer pointer after path lookup.
        if !is_valid_ptr(req.data.args[buf_idx], req.data.arch) {
            return Err(Errno::EFAULT);
        }

        // Allocate buffer.
        // Size is already capped to a safe maximum.
        let mut buf = Vec::new();
        buf.try_reserve(buf_siz).or(Err(Errno::ENOMEM))?;
        buf.resize(buf_siz, 0);

        // Make the readlinkat(2) syscall.
        //
        // SAFETY:
        // 1. fd is a valid file descriptor.
        // 2. Empty string is a NUL-terminated CStr.
        // 3. buf is allocated on heap. buf_siz is valid length.
        // 4. Trailing arguments are sealed cookies.
        #[expect(clippy::cast_possible_truncation)]
        #[expect(clippy::cast_sign_loss)]
        let size = Errno::result(unsafe {
            libc::syscall(
                libc::SYS_readlinkat,
                fd.as_raw_fd(),
                c"".as_ptr(),
                buf.as_mut_ptr() as *mut libc::c_void,
                buf_siz as libc::size_t,
                SYSCOOKIE_POOL.get(CookieIdx::ReadlinkatArg4),
                SYSCOOKIE_POOL.get(CookieIdx::ReadlinkatArg5),
            )
        })
        .map(|size| size as usize)?;

        // Rearrange !memfd:syd/ links.
        let mut buf = &buf[..size];
        if let Some(path) = SydId::strip_syd_memfd(buf) {
            buf = path;
        }

        // readlink(2) truncates and does NOT add a NUL-byte.
        let size = buf.len();
        request.write_mem_all(buf, req.data.args[buf_idx])?;

        // readlink(2) system call has been successfully emulated.
        #[expect(clippy::cast_possible_wrap)]
        Ok(request.return_syscall(size as i64))
    })
}