//
// Syd: rock-solid application kernel
// src/req.rs: seccomp(2) notify request handling
//
// Copyright (c) 2023, 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
// Based in part upon greenhook which is under public domain.
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    cmp::Ordering,
    collections::hash_map::Entry,
    ffi::CString,
    fs::File,
    io,
    io::{IoSlice, IoSliceMut, Read, Seek, SeekFrom, Write},
    ops::Deref,
    os::{
        fd::{AsFd, AsRawFd, OwnedFd, RawFd},
        unix::ffi::OsStrExt,
    },
    sync::{Arc, RwLock},
};

use bitflags::bitflags;
use data_encoding::HEXLOWER;
use libseccomp::{ScmpArch, ScmpNotifResp, ScmpNotifRespFlags};
use memchr::memchr;
use nix::{
    errno::Errno,
    fcntl::OFlag,
    sys::{
        signal::SaFlags,
        socket::UnixAddr,
        time::TimeSpec,
        uio::{process_vm_readv, process_vm_writev, RemoteIoVec},
    },
    unistd::Pid,
    NixPath,
};
use serde::{ser::SerializeMap, Serialize};
use zeroize::Zeroizing;

use crate::{
    cache::UnixVal,
    compat::{
        fstatx, seccomp_notif_addfd, timespec_tv_nsec_t, OpenHow, ResolveFlag, TimeSpec32,
        TimeSpec64, STATX_INO, XATTR_NAME_MAX,
    },
    config::{MMAP_MIN_ADDR, PAGE_SIZE, PROC_FILE},
    confine::{is_valid_ptr, op2name, scmp_arch_bits, ScmpNotifReq, SydArch, Sydcall, EIDRM},
    error,
    fd::{fd_status_flags, pidfd_getfd, pidfd_open, pidfd_send_signal, to_valid_fd, PIDFD_THREAD},
    fs::{process_mrelease, seccomp_notify_addfd, seccomp_notify_id_valid},
    lookup::{file_type, safe_canonicalize, safe_open_msym, CanonicalPath, FileType, FsFlags},
    path::{XPath, XPathBuf, PATH_MAX, PATH_MIN},
    proc::{
        proc_auxv, proc_comm, proc_get_vma, proc_rand_fd, proc_stack_pointer, proc_status,
        proc_tgid,
    },
    sandbox::{Action, Flags, Sandbox, SandboxGuard},
    workers::WorkerCache,
};

/*
 * Macros
 */
bitflags! {
    /// Flags for `SysArg`.
    #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
    pub(crate) struct SysFlags: u8 {
        /// Whether if it's ok for the path to be empty.
        const EMPTY_PATH = 1 << 0;
        /// The system call should be checked for /dev/syd access.
        const CHECK_MAGIC = 1 << 1;
    }
}

impl SysFlags {
    /// Return true if syscall should be checked for /dev/syd access.
    pub fn is_check_magic(self) -> bool {
        self.contains(Self::CHECK_MAGIC)
    }
}

impl Serialize for SysFlags {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut flags: Vec<&str> = vec![];

        if self.is_empty() {
            return serializer.collect_seq(flags);
        }

        if self.contains(Self::EMPTY_PATH) {
            flags.push("empty-path");
        }

        flags.sort();
        serializer.collect_seq(flags)
    }
}

/// `SysArg` represents a system call path argument,
/// coupled with a directory file descriptor as necessary.
#[derive(Copy, Clone, Debug, Default)]
pub(crate) struct SysArg {
    /// DirFd index in syscall args, if applicable.
    pub(crate) dirfd: Option<usize>,
    /// Path index in syscall args, if applicable.
    pub(crate) path: Option<usize>,
    /// Options for the system call.
    pub(crate) flags: SysFlags,
    /// Options for path canonicalization.
    pub(crate) fsflags: FsFlags,
    /// Whether dot as final component must return the given `Errno`.
    pub(crate) dotlast: Option<Errno>,
}

impl Serialize for SysArg {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut map = serializer.serialize_map(Some(5))?;
        map.serialize_entry("dirfd", &self.dirfd)?;
        map.serialize_entry("path", &self.path)?;
        map.serialize_entry("flags", &self.flags)?;
        map.serialize_entry("fsflags", &self.fsflags)?;
        map.serialize_entry("dotlast", &self.dotlast.map(|e| e as i32))?;
        map.end()
    }
}

impl SysArg {
    pub(crate) fn open(flags: OFlag, atfunc: bool, rflags: ResolveFlag) -> Self {
        let (dirfd, path) = if atfunc {
            (Some(0), Some(1))
        } else {
            (None, Some(0))
        };

        // SAFETY:
        // We do not resolve symbolic links if O_CREAT|O_EXCL is
        // specified to support creating files through dangling symbolic
        // links, see the creat_thru_dangling test for more information.
        // We also set MISS_LAST in this case so we get to assert EEXIST.
        let is_create = flags.contains(OFlag::O_CREAT);
        let is_exclusive_create = is_create && flags.contains(OFlag::O_EXCL);

        let mut fsflags = FsFlags::empty();
        if is_exclusive_create {
            fsflags.insert(FsFlags::MISS_LAST);
        } else if !is_create {
            fsflags.insert(FsFlags::MUST_PATH);
        };

        if flags.contains(OFlag::O_NOFOLLOW) || is_exclusive_create {
            fsflags |= FsFlags::NO_FOLLOW_LAST;
        }

        if rflags.contains(ResolveFlag::RESOLVE_BENEATH) {
            fsflags |= FsFlags::RESOLVE_BENEATH;
        }

        if rflags.contains(ResolveFlag::RESOLVE_IN_ROOT) {
            fsflags |= FsFlags::RESOLVE_IN_ROOT;
        }

        if rflags.contains(ResolveFlag::RESOLVE_NO_SYMLINKS) {
            fsflags |= FsFlags::NO_RESOLVE_PATH;
        }

        if rflags.contains(ResolveFlag::RESOLVE_NO_MAGICLINKS) {
            fsflags |= FsFlags::NO_RESOLVE_PROC;
        }

        if rflags.contains(ResolveFlag::RESOLVE_NO_XDEV) {
            fsflags |= FsFlags::NO_RESOLVE_XDEV;
        }

        Self {
            dirfd,
            path,
            fsflags,
            ..Default::default()
        }
    }
}

// Represents path arguments (max=2).
pub(crate) type PathArg = Option<CanonicalPath>;

#[derive(Debug)]
pub(crate) struct PathArgs(pub(crate) PathArg, pub(crate) PathArg);

/// By using `RemoteProcess`, you can get information about the
/// supervised process.
#[derive(Clone, Debug)]
pub struct RemoteProcess {
    /// The process ID.
    pub pid: Pid,
}

impl PartialEq for RemoteProcess {
    fn eq(&self, other: &Self) -> bool {
        self.pid == other.pid
    }
}

impl Eq for RemoteProcess {}

impl Ord for RemoteProcess {
    fn cmp(&self, other: &Self) -> Ordering {
        self.pid.cmp(&other.pid)
    }
}

impl PartialOrd for RemoteProcess {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl RemoteProcess {
    /// Create a new `RemoteProcess` for the given TID.
    pub(crate) fn new(pid: Pid) -> Self {
        Self { pid }
    }

    /// Read path from the given system call argument with the given request.
    /// Check for magic prefix is magic is true.
    ///
    /// If `request` is `Some()` request is validated after
    /// actions that require validation such as proc reads
    /// and fd transfers. Otherwise, the caller must validate
    /// to verify the path read from sandbox process memory
    /// is what's expected.
    #[expect(clippy::type_complexity)]
    pub(crate) fn read_path(
        &self,
        sandbox: &SandboxGuard,
        arch: ScmpArch,
        args: [u64; 6],
        arg: SysArg,
        request: Option<&UNotifyEventRequest>,
    ) -> Result<(CanonicalPath, bool, bool, bool), Errno> {
        let orig = match arg.path {
            Some(idx) => Some(self.remote_path(arch, args[idx], request)?),
            None => None,
        };
        let mut doterr = false;

        // Should we check for magic path?
        let check_magic = arg.flags.is_check_magic();
        let mut is_magic = false;

        let mut empty_path = false;
        let canonical_path = if let Some(path) = orig {
            empty_path = path.is_empty();
            if empty_path && !arg.flags.contains(SysFlags::EMPTY_PATH) {
                return Err(Errno::ENOENT);
            }

            if let Some(errno) = arg.dotlast {
                if path.ends_with_dot() {
                    if errno == Errno::ENOENT {
                        // This will be handled later, as we may
                        // need to return EEXIST instead of ENOENT
                        // if the path exists.
                        doterr = true;
                    } else {
                        return Err(errno);
                    }
                }
            }

            if check_magic && path.is_magic() {
                is_magic = true;
                CanonicalPath::new_magic(path)
            } else if empty_path || path.is_dot() {
                let dirfd = if let Some(idx) = arg.dirfd {
                    // Validate FD argument.
                    //
                    // Note about EMPTY_PATH:
                    // 1. execveat(fd, "", NULL, NULL, AT_EMPTY_PATH)
                    // 2. openat(fd, "", O_TMPFILE|O_RDWR, 0)
                    // In the first case AT_FDCWD is invalid,
                    // but in the second case AT_FDCWD is valid.
                    to_valid_fd(args[idx])?
                } else {
                    libc::AT_FDCWD
                };
                let is_dot = !empty_path;

                // SAFETY: The ends_with_dot check above
                // ensures we return ENOTDIR when e.g. path is
                // a dot and the file descriptor argument is a
                // regular file. This happens because in this
                // case, joining the directory with an empty
                // path on the next branch essentially adds a
                // trailing slash to the path, making the
                // system call emulator fail with ENOTDIR if
                // the argument is not a directory. This way,
                // we avoid stat'ing the path here to
                // determine whether it's a directory or not.
                if let Some(request) = request {
                    if dirfd == libc::AT_FDCWD {
                        let path = CanonicalPath::new_fd(libc::AT_FDCWD.into(), self.pid)?;

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

                        path
                    } else {
                        // SAFETY: Get the file descriptor before access check
                        // as it may change after which is a TOCTOU vector.
                        let fd = request.get_fd(dirfd)?;

                        let path = CanonicalPath::new_fd(fd.into(), self.pid)?;

                        if is_dot && path.typ != Some(FileType::Dir) {
                            // FD-only call, no need to delay ENOTDIR.
                            return Err(Errno::ENOTDIR);
                        }

                        path
                    }
                } else if dirfd == libc::AT_FDCWD {
                    CanonicalPath::new_fd(libc::AT_FDCWD.into(), self.pid)?
                } else {
                    // SAFETY: Get the file descriptor before access check
                    // as it may change after which is a TOCTOU vector.
                    let pid_fd = pidfd_open(self.pid, PIDFD_THREAD)?;
                    let fd = pidfd_getfd(pid_fd, dirfd)?;

                    let path = CanonicalPath::new_fd(fd.into(), self.pid)?;

                    if is_dot && path.typ != Some(FileType::Dir) {
                        // FD-only call, no need to delay ENOTDIR.
                        return Err(Errno::ENOTDIR);
                    }

                    path
                }
            } else {
                let fd = if let Some(idx) = arg.dirfd {
                    // Using a bad directory is okay for absolute paths.
                    if path.is_absolute() {
                        None
                    } else {
                        Some(to_valid_fd(args[idx])?)
                    }
                } else {
                    None
                };

                safe_canonicalize(self.pid, fd, &path, arg.fsflags, Some(sandbox.deref()))?
            }
        } else {
            // SAFETY: SysArg.path is None asserting dirfd is Some.
            #[expect(clippy::disallowed_methods)]
            let idx = arg.dirfd.unwrap();

            // Validate file descriptor.
            //
            // AT_FDCWD is an invalid file descriptor with NULL path.
            let remote_fd = RawFd::try_from(args[idx]).or(Err(Errno::EBADF))?;
            if remote_fd < 0 {
                // Negative file descriptors are invalid with NULL path.
                return Err(Errno::EBADF);
            }

            if let Some(request) = request {
                // SAFETY: Get the file descriptor before access check
                // as it may change after which is a TOCTOU vector.
                let fd = request.get_fd(remote_fd)?;

                // Validate WANT_READ against O_PATH.
                if arg.fsflags.want_read() && fd_status_flags(&fd)?.contains(OFlag::O_PATH) {
                    return Err(Errno::EBADF);
                }

                CanonicalPath::new_fd(fd.into(), self.pid)?
            } else {
                // SAFETY: Get the file descriptor before access check
                // as it may change after which is a TOCTOU vector.
                let pid_fd = pidfd_open(self.pid, PIDFD_THREAD)?;
                let fd = pidfd_getfd(pid_fd, remote_fd)?;

                // Validate WANT_READ against O_PATH.
                if arg.fsflags.want_read() && fd_status_flags(&fd)?.contains(OFlag::O_PATH) {
                    return Err(Errno::EBADF);
                }

                CanonicalPath::new_fd(fd.into(), self.pid)?
            }
        };

        if !is_magic && arg.path.is_some() {
            // SAFETY: Deny access to critical and/or suspicious paths.
            canonical_path.abs().check(
                self.pid,
                canonical_path.typ.as_ref(),
                None,
                !sandbox.flags.allow_unsafe_filename(),
                !sandbox.flags.allow_unsafe_mkbdev(),
            )?;
        }

        Ok((canonical_path, is_magic, doterr, empty_path))
    }

    /// Allocate and read a `Zeroizing` buffer from remote process's memory with `process_vm_readv()`.
    ///
    /// # Safety
    ///
    /// This function is unsafe because the request is not validated.
    pub(crate) unsafe fn read_vec_zeroed(
        &self,
        arch: ScmpArch,
        remote_addr: u64,
        len: usize,
    ) -> Result<Zeroizing<Vec<u8>>, Errno> {
        if Sandbox::use_proc_pid_mem() {
            return self.read_vec_zeroed_proc(arch, remote_addr, len);
        }

        let mut local_buffer = Zeroizing::new(Vec::new());

        // Check for zero length and return an empty Vector.
        if len == 0 {
            return Ok(local_buffer);
        }

        // SAFETY: Check pointer against mmap_min_addr before allocation,
        // but after length is zero check.
        if !is_valid_ptr(remote_addr, arch) {
            return Err(Errno::EFAULT);
        }
        let remote_addr = usize::try_from(remote_addr).or(Err(Errno::EFAULT))?;

        local_buffer.try_reserve(len).or(Err(Errno::ENOMEM))?;

        // SAFETY: We are using `set_len(len)` after reserving enough
        // capacity with `try_reserve(len)`. This is safe because the
        // buffer was already allocated with enough memory to hold `len`
        // elements, and we are not exceeding the reserved capacity. The
        // memory is valid for `len` elements.
        unsafe { local_buffer.set_len(len) };

        let len = process_vm_readv(
            self.pid,
            &mut [IoSliceMut::new(&mut local_buffer)],
            &[RemoteIoVec {
                len,
                base: remote_addr,
            }],
        )?;

        // SAFETY: len is returned by the Linux kernel.
        unsafe { local_buffer.set_len(len) };
        local_buffer.shrink_to_fit();

        Ok(local_buffer)
    }

    /// Allocate and read a buffer from remote process's memory with `process_vm_readv()`.
    ///
    /// # Safety
    ///
    /// This function is unsafe because the request is not validated.
    pub(crate) unsafe fn read_vec(
        &self,
        arch: ScmpArch,
        remote_addr: u64,
        len: usize,
    ) -> Result<Vec<u8>, Errno> {
        if Sandbox::use_proc_pid_mem() {
            return self.read_vec_proc(arch, remote_addr, len);
        }

        let mut local_buffer = Vec::new();

        // Check for zero length and return an empty Vector.
        if len == 0 {
            return Ok(local_buffer);
        }

        // SAFETY: Check pointer against mmap_min_addr before allocation,
        // but after length is zero check.
        if !is_valid_ptr(remote_addr, arch) {
            return Err(Errno::EFAULT);
        }
        let remote_addr = usize::try_from(remote_addr).or(Err(Errno::EFAULT))?;

        local_buffer.try_reserve(len).or(Err(Errno::ENOMEM))?;

        // SAFETY: We are using `set_len(len)` after reserving enough
        // capacity with `try_reserve(len)`. This is safe because the
        // buffer was already allocated with enough memory to hold `len`
        // elements, and we are not exceeding the reserved capacity. The
        // memory is valid for `len` elements.
        unsafe { local_buffer.set_len(len) };

        let len = process_vm_readv(
            self.pid,
            &mut [IoSliceMut::new(&mut local_buffer)],
            &[RemoteIoVec {
                len,
                base: remote_addr,
            }],
        )?;

        // SAFETY: len is returned by the Linux kernel.
        unsafe { local_buffer.set_len(len) };
        local_buffer.shrink_to_fit();

        Ok(local_buffer)
    }

    /// Read data from remote process's memory with `process_vm_readv()`.
    ///
    /// # Safety
    ///
    /// This function is unsafe because the request is not validated.
    pub(crate) unsafe fn read_mem(
        &self,
        arch: ScmpArch,
        local_buffer: &mut [u8],
        remote_addr: u64,
        len: usize,
    ) -> Result<usize, Errno> {
        if Sandbox::use_proc_pid_mem() {
            return self.read_mem_proc(arch, local_buffer, remote_addr, len);
        }

        // SAFETY: Check pointer against mmap_min_addr.
        if !is_valid_ptr(remote_addr, arch) {
            return Err(Errno::EFAULT);
        }
        let remote_addr = usize::try_from(remote_addr).or(Err(Errno::EFAULT))?;

        process_vm_readv(
            self.pid,
            &mut [IoSliceMut::new(local_buffer)],
            &[RemoteIoVec {
                len,
                base: remote_addr,
            }],
        )
    }

    /// Fallback method to allocate and read a `Zeroizing` buffer from `/proc/$pid/mem` when `process_vm_readv()` is unavailable.
    ///
    /// # Safety
    ///
    /// This function is unsafe because the request is not validated.
    pub(crate) unsafe fn read_vec_zeroed_proc(
        &self,
        arch: ScmpArch,
        remote_addr: u64,
        len: usize,
    ) -> Result<Zeroizing<Vec<u8>>, Errno> {
        let mut local_buffer = Zeroizing::new(Vec::new());

        // Check for zero length and return an empty Vector.
        if len == 0 {
            return Ok(local_buffer);
        }

        // SAFETY: Check pointer against mmap_min_addr before allocation,
        // but after length is zero check.
        if !is_valid_ptr(remote_addr, arch) {
            return Err(Errno::EFAULT);
        }

        local_buffer.try_reserve(len).or(Err(Errno::ENOMEM))?;

        // SAFETY: We are using `set_len(len)` after reserving enough
        // capacity with `try_reserve(len)`. This is safe because the
        // buffer was already allocated with enough memory to hold `len`
        // elements, and we are not exceeding the reserved capacity. The
        // memory is valid for `len` elements.
        unsafe { local_buffer.set_len(len) };

        let mut path = XPathBuf::from_pid(self.pid)?;
        path.try_reserve(b"/mem".len()).or(Err(Errno::ENOMEM))?;
        path.push(b"mem");

        let mut file = safe_open_msym(PROC_FILE(), &path, OFlag::O_RDONLY, ResolveFlag::empty())
            .map(File::from)
            .or(Err(Errno::EACCES))?;
        file.seek(SeekFrom::Start(remote_addr))
            .or(Err(Errno::EACCES))?;

        let mut nread = 0;
        #[expect(clippy::arithmetic_side_effects)]
        while nread < len {
            match file.read(&mut local_buffer[nread..]) {
                Ok(0) => return Err(Errno::EACCES),
                Ok(n) => nread += n,
                Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {}
                Err(_) => return Err(Errno::EACCES),
            }
        }

        // SAFETY: nread is returned by the Linux kernel.
        unsafe { local_buffer.set_len(nread) };
        local_buffer.shrink_to_fit();

        Ok(local_buffer)
    }

    /// Fallback method to allocate and read a buffer from `/proc/$pid/mem` when `process_vm_readv()` is unavailable.
    ///
    /// # Safety
    ///
    /// This function is unsafe because the request is not validated.
    pub(crate) unsafe fn read_vec_proc(
        &self,
        arch: ScmpArch,
        remote_addr: u64,
        len: usize,
    ) -> Result<Vec<u8>, Errno> {
        let mut local_buffer = Vec::new();

        // Check for zero length and return an empty Vector.
        if len == 0 {
            return Ok(local_buffer);
        }

        // SAFETY: Check pointer against mmap_min_addr before allocation,
        // but after length is zero check.
        if !is_valid_ptr(remote_addr, arch) {
            return Err(Errno::EFAULT);
        }

        local_buffer.try_reserve(len).or(Err(Errno::ENOMEM))?;

        // SAFETY: We are using `set_len(len)` after reserving enough
        // capacity with `try_reserve(len)`. This is safe because the
        // buffer was already allocated with enough memory to hold `len`
        // elements, and we are not exceeding the reserved capacity. The
        // memory is valid for `len` elements.
        unsafe { local_buffer.set_len(len) };

        let mut path = XPathBuf::from_pid(self.pid)?;
        path.try_reserve(b"/mem".len()).or(Err(Errno::ENOMEM))?;
        path.push(b"mem");

        let mut file = safe_open_msym(PROC_FILE(), &path, OFlag::O_RDONLY, ResolveFlag::empty())
            .map(File::from)
            .or(Err(Errno::EACCES))?;
        file.seek(SeekFrom::Start(remote_addr))
            .or(Err(Errno::EACCES))?;

        let mut nread = 0;
        #[expect(clippy::arithmetic_side_effects)]
        while nread < len {
            match file.read(&mut local_buffer[nread..]) {
                Ok(0) => return Err(Errno::EACCES),
                Ok(n) => nread += n,
                Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {}
                Err(_) => return Err(Errno::EACCES),
            }
        }

        // SAFETY: nread is returned by the Linux kernel.
        unsafe { local_buffer.set_len(nread) };
        local_buffer.shrink_to_fit();

        Ok(local_buffer)
    }

    /// Fallback method to read data from `/proc/$pid/mem` when `process_vm_readv()` is unavailable.
    ///
    /// # Safety
    ///
    /// This function is unsafe because the request is not validated.
    pub(crate) unsafe fn read_mem_proc(
        &self,
        arch: ScmpArch,
        local_buffer: &mut [u8],
        remote_addr: u64,
        len: usize,
    ) -> Result<usize, Errno> {
        // SAFETY: Check pointer against mmap_min_addr.
        if !is_valid_ptr(remote_addr, arch) {
            return Err(Errno::EFAULT);
        }

        let mut path = XPathBuf::from_pid(self.pid)?;
        path.try_reserve(b"/mem".len()).or(Err(Errno::ENOMEM))?;
        path.push(b"mem");

        let mut file = safe_open_msym(PROC_FILE(), &path, OFlag::O_RDONLY, ResolveFlag::empty())
            .map(File::from)
            .or(Err(Errno::EACCES))?;
        file.seek(SeekFrom::Start(remote_addr))
            .or(Err(Errno::EACCES))?;

        let mut nread = 0;
        #[expect(clippy::arithmetic_side_effects)]
        while nread < len {
            match file.read(&mut local_buffer[nread..]) {
                Ok(0) => return Err(Errno::EACCES),
                Ok(n) => nread += n,
                Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {}
                Err(_) => return Err(Errno::EACCES),
            }
        }

        Ok(nread)
    }

    /// Write data to remote process's memory with `process_vm_writev()`.
    ///
    /// # Safety
    ///
    /// This function is unsafe because the request is not validated.
    pub(crate) unsafe fn write_mem(
        &self,
        arch: ScmpArch,
        local_buffer: &[u8],
        remote_addr: u64,
    ) -> Result<usize, Errno> {
        if Sandbox::use_proc_pid_mem() {
            return self.write_mem_proc(arch, local_buffer, remote_addr);
        }

        // SAFETY: Check pointer against mmap_min_addr.
        if !is_valid_ptr(remote_addr, arch) {
            return Err(Errno::EFAULT);
        }

        let len = local_buffer.len();
        if len == 0 {
            return Ok(0);
        }
        process_vm_writev(
            self.pid,
            &[IoSlice::new(local_buffer)],
            &[RemoteIoVec {
                len,
                base: usize::try_from(remote_addr).or(Err(Errno::EFAULT))?,
            }],
        )
    }

    /// Fallback method to write data to `/proc/$pid/mem` when `process_vm_writev()` is unavailable.
    ///
    /// # Safety
    ///
    /// This function is unsafe because the request is not validated.
    pub(crate) unsafe fn write_mem_proc(
        &self,
        arch: ScmpArch,
        local_buffer: &[u8],
        remote_addr: u64,
    ) -> Result<usize, Errno> {
        // SAFETY: Check pointer against mmap_min_addr.
        if !is_valid_ptr(remote_addr, arch) {
            return Err(Errno::EFAULT);
        } else if local_buffer.is_empty() {
            return Ok(0);
        }

        let mut path = XPathBuf::from_pid(self.pid)?;
        path.try_reserve(b"/mem".len()).or(Err(Errno::ENOMEM))?;
        path.push(b"mem");

        let mut file = safe_open_msym(PROC_FILE(), &path, OFlag::O_WRONLY, ResolveFlag::empty())
            .map(File::from)
            .or(Err(Errno::EACCES))?;
        file.seek(SeekFrom::Start(remote_addr))
            .or(Err(Errno::EACCES))?;

        let mut nwritten = 0;
        #[expect(clippy::arithmetic_side_effects)]
        while nwritten < local_buffer.len() {
            match file.write(&local_buffer[nwritten..]) {
                Ok(0) => return Err(Errno::EACCES),
                Ok(n) => nwritten += n,
                Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {}
                Err(_) => return Err(Errno::EACCES),
            }
        }

        Ok(nwritten)
    }

    /// Read the path from memory of the process with the given `Pid` with the given address.
    ///
    /// If `request` is `Some()` request is validated after
    /// actions that require validation such as proc reads
    /// and fd transfers. Otherwise, the caller must validate
    /// to verify the path read from sandbox process memory
    /// is what's expected.
    pub(crate) fn remote_path(
        &self,
        arch: ScmpArch,
        addr: u64,
        request: Option<&UNotifyEventRequest>,
    ) -> Result<XPathBuf, Errno> {
        // Initialize path on the stack.
        let mut buf = [0u8; PATH_MAX];

        // Read from process memory.
        // We read PATH_MIN bytes at a time, because most paths are short.
        let mut off = 0;
        #[expect(clippy::arithmetic_side_effects)]
        while off < PATH_MAX {
            // Prepare slice to read.
            let len = PATH_MIN.min(PATH_MAX - off);
            let ptr = &mut buf[off..off + len];

            // Read remote memory.
            // SAFETY:
            // 1. Assume error on zero-read.
            // 2. Validate the request after memory read.
            let len = unsafe { self.read_mem(arch, ptr, addr + off as u64, len) }?;
            if len == 0 {
                return Err(Errno::EFAULT);
            }

            // Check for NUL-byte.
            if let Some(nul) = memchr(0, &ptr[..len]) {
                // SAFETY: Validate memory read as necessary.
                // This is not possible for ptrace(2) hooks.
                if request.map(|req| !req.is_valid()).unwrap_or(false) {
                    return Err(Errno::ESRCH);
                }

                // Adjust to actual size up to NUL-byte.
                off += nul;

                // Allocate vector on heap gracefully.
                let mut vec = Vec::new();
                vec.try_reserve(off).or(Err(Errno::ENOMEM))?;
                vec.extend_from_slice(&buf[..off]);

                return Ok(vec.into());
            }

            off += len;
        }

        Err(Errno::ENAMETOOLONG)
    }
}

/// `UNotifyEventRequest` is the type of parameter that user's function
/// would get.
pub(crate) struct UNotifyEventRequest {
    pub(crate) scmpreq: ScmpNotifReq,
    pub(crate) syscall: Sydcall,
    notify_fd: RawFd,
    pub(crate) cache: Arc<WorkerCache>,
    sandbox: Arc<RwLock<Sandbox>>,
}

impl Serialize for UNotifyEventRequest {
    #[expect(clippy::cognitive_complexity)]
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut map = serializer.serialize_map(Some(8))?;

        map.serialize_entry("pid", &self.scmpreq.pid)?;
        map.serialize_entry("sys", &self.syscall)?;
        map.serialize_entry("args", &self.scmpreq.data.args)?;
        map.serialize_entry("arch", &SydArch::from(self.scmpreq.data.arch))?;

        let pid = self.scmpreq.pid();
        if let Ok(comm) = proc_comm(pid) {
            map.serialize_entry("cmd", &comm)?;
        }
        if let Ok(status) = proc_status(pid) {
            map.serialize_entry("tgid", &status.pid.as_raw())?;
            map.serialize_entry("sig_caught", &status.sig_caught)?;
            map.serialize_entry("sig_blocked", &status.sig_blocked)?;
            map.serialize_entry("sig_ignored", &status.sig_ignored)?;
            map.serialize_entry("sig_pending_thread", &status.sig_pending_thread)?;
            map.serialize_entry("sig_pending_process", &status.sig_pending_process)?;
            map.serialize_entry("umask", &status.umask.bits())?;
        }

        #[expect(clippy::unnecessary_cast)]
        if let Ok(auxv) = proc_auxv(pid) {
            // Note: libc::AT_* constant are u32 on 32-bit...

            // Base and entry addresses
            if let Some(val) = auxv.get(&(libc::AT_BASE as u64)) {
                map.serialize_entry("at_base", val)?;
            }
            if let Some(val) = auxv.get(&(libc::AT_ENTRY as u64)) {
                map.serialize_entry("at_entry", val)?;
            }

            // Program headers
            if let Some(val) = auxv.get(&(libc::AT_PHDR as u64)) {
                map.serialize_entry("at_phdr", val)?;
            }
            if let Some(val) = auxv.get(&(libc::AT_PHENT as u64)) {
                map.serialize_entry("at_phent", val)?;
            }
            if let Some(val) = auxv.get(&(libc::AT_PHNUM as u64)) {
                map.serialize_entry("at_phnum", val)?;
            }

            // Read AT_RANDOM bytes which is 16 bytes of
            // random data placed by the kernel at the
            // specified address.
            if let Some(addr) = auxv.get(&(libc::AT_RANDOM as u64)) {
                let mut at_random = [0u8; 16];
                if *addr >= *MMAP_MIN_ADDR && self.read_mem(&mut at_random, *addr, 16).is_ok() {
                    map.serialize_entry("at_random", &HEXLOWER.encode(&at_random))?;
                }
            }

            // AT_SECURE: we set this ourselves
            // unless trace/allow_unsafe_exec_libc:1 is passed at startup,
            // however when we set it, the value will still incorrectly
            // show as false because this file is not updated after
            // process startup.
            if let Some(val) = auxv.get(&(libc::AT_SECURE as u64)) {
                let sandbox = self.sandbox.read().unwrap_or_else(|err| err.into_inner());
                let seclibc = !sandbox.flags.allow_unsafe_exec_libc();
                drop(sandbox); // release the read-lock.

                if seclibc {
                    map.serialize_entry("at_secure", &true)?;
                } else {
                    map.serialize_entry("at_secure", &(*val != 0))?;
                }
            }
        }

        let ip = self.scmpreq.data.instr_pointer;
        let sp = proc_stack_pointer(pid).ok();
        map.serialize_entry("ip", &ip)?;
        map.serialize_entry("sp", &sp)?;

        let ip_vma = proc_get_vma(pid, ip).ok();
        let sp_vma = sp.and_then(|sp| proc_get_vma(pid, sp).ok());
        map.serialize_entry("ip_vma", &ip_vma)?;
        map.serialize_entry("sp_vma", &sp_vma)?;

        let mut ip_mem = [0u8; 64];
        let mut sp_mem = [0u8; 64];
        let mut ip_read = false;
        let mut sp_read = false;

        if is_valid_ptr(ip, self.scmpreq.data.arch) && self.read_mem(&mut ip_mem, ip, 64).is_ok() {
            ip_read = true;
        }

        if let Some(sp) = sp {
            if is_valid_ptr(ip, self.scmpreq.data.arch)
                && self.read_mem(&mut sp_mem, sp, 64).is_ok()
            {
                sp_read = true;
            }
        }

        map.serialize_entry(
            "ip_mem",
            &if ip_read {
                Some(HEXLOWER.encode(&ip_mem))
            } else {
                None
            },
        )?;

        map.serialize_entry(
            "sp_mem",
            &if sp_read {
                Some(HEXLOWER.encode(&sp_mem))
            } else {
                None
            },
        )?;

        map.end()
    }
}

impl UNotifyEventRequest {
    pub(crate) fn new(
        scmpreq: ScmpNotifReq,
        syscall: Sydcall,
        notify_fd: RawFd,
        cache: Arc<WorkerCache>,
        sandbox: Arc<RwLock<Sandbox>>,
    ) -> Self {
        UNotifyEventRequest {
            scmpreq,
            syscall,
            notify_fd,
            cache,
            sandbox,
        }
    }

    /// Get a read lock to the sandbox.
    pub(crate) fn get_sandbox(&self) -> SandboxGuard<'_> {
        // Note, if another user of this mutex panicked while holding
        // the mutex, then this call will return an error once the mutex
        // is acquired. We ignore this case here and fall through
        // because Syd emulator threads are free to panic independent of
        // each other.
        SandboxGuard::Read(self.sandbox.read().unwrap_or_else(|err| err.into_inner()))
    }

    /// Get a write lock to the sandbox.
    pub(crate) fn get_mut_sandbox(&self) -> SandboxGuard<'_> {
        // Note, if another user of this mutex panicked while holding
        // the mutex, then this call will return an error once the mutex
        // is acquired. We ignore this case here and fall through
        // because Syd emulator threads are free to panic independent of
        // each other.
        SandboxGuard::Write(self.sandbox.write().unwrap_or_else(|err| err.into_inner()))
    }

    /// Check SCM_RIGHTS file types at sendm{,m}sg(2) boundaries.
    #[expect(clippy::cognitive_complexity)]
    pub(crate) fn check_scm_rights<Fd: AsFd>(
        &self,
        fd: Fd,
        flags: Flags,
        op: u8,
        log_scmp: bool,
    ) -> Result<(), Errno> {
        // SAFETY: Deny sending/receiving file descriptors referring to
        // 1. Directories (pledge does the same).
        // 2. Block devices unless trace/allow_unsafe_mkbdev:1 is set.
        // 3. Symbolic links unless trace/allow_unsafe_symlinks:1 is set.
        //
        // Note, we do allow files of unknown type such as epoll
        // fds and event fds as some programs such as pipewire
        // depend on this. See test-pw-filter test of pipewire
        // for more information about this.
        let ftyp = file_type(fd, None, false)?;
        let emsg = match ftyp {
            FileType::Dir => "report a bug!",
            FileType::Blk if !flags.allow_unsafe_mkbdev() => "use `trace/allow_unsafe_mkbdev:1'",
            FileType::Lnk if !flags.allow_unsafe_symlinks() => {
                "use `trace/allow_unsafe_symlinks:1'"
            }
            _ => return Ok(()),
        };

        if log_scmp {
            error!("ctx": "trusted_scm_rights", "sys": op2name(op),
                "msg": format!("SCM_RIGHTS for unsafe file type `{ftyp:?}' blocked"),
                "tip": emsg, "req": &self);
        } else {
            error!("ctx": "trusted_scm_rights", "sys": op2name(op),
                "msg": format!("SCM_RIGHTS for unsafe file type `{ftyp:?}' blocked"),
                "tip": emsg, "pid": self.scmpreq.pid);
        }

        Err(Errno::EACCES)
    }

    /// Find a bind address by parent and base names.
    ///
    /// Used for informational purposes at recvfrom(2) boundary.
    pub(crate) fn find_unix_addr(&self, base: &XPath) -> Result<UnixAddr, Errno> {
        let unix_map = self
            .cache
            .unix_map
            .read()
            .unwrap_or_else(|err| err.into_inner());
        for unix_val in unix_map.values() {
            if let Some(addr) = unix_val.addr {
                if let Some(path) = addr.path() {
                    let path = XPath::from_bytes(path.as_os_str().as_bytes());
                    if base.is_equal(path.split().1.as_bytes()) {
                        return Ok(addr);
                    }
                }
            }
        }

        Err(Errno::ENOENT)
    }

    /// Add a ptrace(PTRACE_TRACEME) attempt to the PtraceMap.
    ///
    /// Returns `Err(Errno::EPERM)` if the tid has already tried before.
    pub(crate) fn add_ptrace(&self, tid: Pid) -> Result<(), Errno> {
        let mut ptrace_map = self
            .cache
            .ptrace_map
            .write()
            .unwrap_or_else(|err| err.into_inner());

        if ptrace_map.contains_key(&tid) {
            return Err(Errno::EPERM);
        }

        let tgid = proc_tgid(tid)?;
        ptrace_map.insert(tid, tgid);

        Ok(())
    }

    /// Add a bind address to the UnixMap. This has been split from the sandbox policy
    /// as of version 3.33.1 because it has no bearing on access rights and is provided
    /// for convenience for getpeername(2), getsockname(2), recvfrom(2), and recvmsg(2).
    ///
    /// This function is called for bind(2) and connect(2).
    pub(crate) fn add_unix<Fd: AsFd>(
        &self,
        fd: Fd,
        tid: Pid,
        addr: Option<&UnixAddr>,
        peer: Option<&UnixAddr>,
    ) -> Result<(), Errno> {
        // Get socket inode.
        let inode = fstatx(fd, STATX_INO).map(|statx| statx.stx_ino)?;

        // Get process id.
        let pid = proc_tgid(tid)?;

        // Record/merge unix address.
        let mut unix_map = self
            .cache
            .unix_map
            .write()
            .unwrap_or_else(|err| err.into_inner());
        match unix_map.entry(inode) {
            Entry::Occupied(mut entry) => {
                let entry = entry.get_mut();
                entry.pid = pid;
                if let Some(addr) = addr {
                    entry.addr = Some(*addr);
                }
                if let Some(peer) = peer {
                    entry.peer = Some(*peer);
                }
            }
            Entry::Vacant(entry) => {
                entry.insert(UnixVal {
                    pid,
                    addr: addr.copied(),
                    peer: peer.copied(),
                });
            }
        }

        Ok(())
    }

    pub(crate) fn get_unix(&self, inode: u64) -> Option<UnixVal> {
        self.cache
            .unix_map
            .read()
            .unwrap_or_else(|err| err.into_inner())
            .get(&inode)
            .copied()
    }

    /// Read an xattr name from the given address.
    ///
    /// Name must be a NUL-terminated string or `Err(Errno::ERANGE)` is returned.
    pub(crate) fn read_xattr(&self, addr: u64) -> Result<CString, Errno> {
        let mut buf = self.read_vec(addr, XATTR_NAME_MAX)?;
        let z = memchr(0, &buf)
            .ok_or(Errno::ERANGE)?
            .checked_add(1)
            .ok_or(Errno::ERANGE)?;
        buf.truncate(z);
        buf.shrink_to_fit();

        // Check for empty name.
        let len = buf.len(); // Includes NUL-byte.
        if len <= 1 {
            return Err(Errno::ERANGE);
        }

        // Check for qualified name in namespace.attribute form.
        // EINVAL here is expected by sys-apps/attr's tests.
        match memchr(b'.', &buf) {
            None => Err(Errno::EOPNOTSUPP),
            Some(0) => Err(Errno::EINVAL),
            Some(n) if n >= len.saturating_sub(2) => Err(Errno::EINVAL),
            Some(_) => {
                // Release excess memory.
                buf.shrink_to_fit();
                // SAFETY:
                // 1. `buf` has one nul-byte as its last element.
                // 2. `buf` does not have any interior nul-bytes.
                Ok(unsafe { CString::from_vec_with_nul_unchecked(buf) })
            }
        }
    }

    /// Read the sa_flags member of `struct sigaction` from the given address.
    pub(crate) fn read_sa_flags(&self, addr: u64) -> Result<SaFlags, Errno> {
        let req = self.scmpreq;

        // Determine the target word size. (4 for 32-bit, 8 for 64-bit).
        let is32 = scmp_arch_bits(req.data.arch) == 32;
        let word_size = if is32 { 4usize } else { 8usize };

        // Offset of sa_flags within struct sigaction.
        let offset = word_size as u64; // 4 on 32-bit, 8 on 64-bit.

        // Compute absolute read address, checking for overflow.
        let read_addr = addr.checked_add(offset).ok_or(Errno::EFAULT)?;

        // Initialize vector on stack.
        //
        // Buffer up to 8 bytes; will only use first `word_size` bytes.
        let mut buf = [0u8; 8];

        // Read from process memory.
        //
        // Loop until we've read `word_size` bytes,
        // or encounter EOF (zero-read).
        let process = RemoteProcess::new(self.scmpreq.pid());
        let mut nread = 0;
        while nread < word_size {
            // Adjust current slice.
            //
            // Compute absolute read address plus the offset, checking for overflow.
            let slice = &mut buf[nread..word_size];
            let read_addr = read_addr.checked_add(nread as u64).ok_or(Errno::EFAULT)?;

            // Read remote memory.
            //
            // SAFETY: The request is going to be validated.
            let n = unsafe { process.read_mem(req.data.arch, slice, read_addr, slice.len()) }?;

            // SAFETY: Assume error on zero-read.
            if n == 0 {
                return Err(Errno::EFAULT);
            }

            // Compute next offset, check for overflow.
            nread = nread.checked_add(n).ok_or(Errno::EFAULT)?;
        }

        // SAFETY: Check request validity after memory read.
        if !self.is_valid() {
            return Err(Errno::ESRCH);
        }

        // Interpret raw bytes in native endianness.
        #[expect(clippy::cast_possible_truncation)]
        #[expect(clippy::cast_possible_wrap)]
        #[expect(clippy::disallowed_methods)]
        let raw = if word_size == 8 {
            u64::from_ne_bytes(buf) as libc::c_int
        } else {
            // SAFETY: `word_size` must always be 4 here.
            u32::from_ne_bytes(buf[..4].try_into().unwrap()) as libc::c_int
        };

        Ok(SaFlags::from_bits_truncate(raw))
    }

    /// Read the `OpenHow` struct from process memory
    /// at the given address and size.
    pub(crate) fn remote_ohow(&self, addr: u64, size: u64) -> Result<OpenHow, Errno> {
        const OPEN_HOW_SIZE_VER0: usize = 24;
        const OPEN_HOW_SIZE_LATEST: usize = size_of::<OpenHow>();

        // SAFETY: Validate size argument.
        let size = usize::try_from(size).or(Err(Errno::EINVAL))?;
        if size < OPEN_HOW_SIZE_VER0 {
            return Err(Errno::EINVAL);
        }
        if size as u64 > *PAGE_SIZE {
            return Err(Errno::E2BIG);
        }

        // SAFETY: Validate address argument.
        if !is_valid_ptr(addr, self.scmpreq.data.arch) {
            return Err(Errno::EFAULT);
        }

        // Allocate buffer.
        // Size is already capped to page size.
        let raw = self.read_vec(addr, size)?;

        // SAFETY: Validate that the full size was read.
        // Partial read means EFAULT.
        if raw.len() != size {
            return Err(Errno::EFAULT);
        }

        // SAFETY: Verify trailing bytes are zero; otherwise E2BIG.
        if raw.iter().skip(OPEN_HOW_SIZE_LATEST).any(|&b| b != 0) {
            return Err(Errno::E2BIG);
        }

        let mut buf = [0u8; OPEN_HOW_SIZE_LATEST];
        let len = buf.len().min(size);
        buf[..len].copy_from_slice(&raw[..len]);

        // SAFETY: The following unsafe block assumes that:
        // 1. The memory layout of open_how in our Rust environment
        //    matches that of the target process.
        // 2. The request.process.read_mem call has populated buf with valid data
        //    of the appropriate size (ensured by the size check above).
        // 3. The buffer is appropriately aligned for reading an
        //    open_how struct. If the remote process's representation of
        //    open_how was correctly aligned, our local buffer should be
        //    too, since it's an array on the stack.
        Ok(unsafe { std::ptr::read_unaligned(buf.as_ptr() as *const _) })
    }

    /// Read the `libc::utimbuf` struct from process memory at the given address.
    /// Convert it to a `libc::timespec[2]` for easy interoperability.
    pub(crate) fn remote_utimbuf(&self, addr: u64) -> Result<(TimeSpec, TimeSpec), Errno> {
        if addr == 0 {
            // utimbuf pointer is NULL: Set to current time.
            return Ok((TimeSpec::UTIME_NOW, TimeSpec::UTIME_NOW));
        } else if addr < *MMAP_MIN_ADDR {
            // utimbuf pointer is invalid: return EFAULT.
            return Err(Errno::EFAULT);
        }

        const LEN: usize = size_of::<libc::utimbuf>();
        let mut buf = [0u8; LEN];
        self.read_mem(&mut buf, addr, LEN)?;

        // SAFETY: The following unsafe block assumes that:
        // 1. The memory layout of utimbuf in our Rust environment
        //    matches that of the target process.
        // 2. The request.process.read_mem call has populated buf with valid data
        //    of the appropriate size (ensured by the size check above).
        // 3. The buffer is appropriately aligned for reading a utimbuf
        //    struct. If the remote process's representation of utimbuf
        //    was correctly aligned, our local buffer should be too,
        //    since it's an array on the stack.
        let utimbuf: libc::utimbuf = unsafe { std::ptr::read_unaligned(buf.as_ptr() as *const _) };

        Ok((
            TimeSpec::new(utimbuf.actime, 0),
            TimeSpec::new(utimbuf.modtime, 0),
        ))
    }

    /// Read the `libc::timeval[2]` struct from process memory at the given address.
    /// Convert it to a `libc::timespec[2]` for easy interoperability.
    pub(crate) fn remote_timeval(&self, addr: u64) -> Result<(TimeSpec, TimeSpec), Errno> {
        if addr == 0 {
            // timeval pointer is NULL: Set to current time.
            return Ok((TimeSpec::UTIME_NOW, TimeSpec::UTIME_NOW));
        } else if addr < *MMAP_MIN_ADDR {
            // timeval pointer is invalid: return EFAULT.
            return Err(Errno::EFAULT);
        }

        const LEN: usize = size_of::<libc::timeval>() * 2;
        let mut buf = [0u8; LEN];
        self.read_mem(&mut buf, addr, LEN)?;

        // SAFETY: The following unsafe block assumes that:
        // 1. The memory layout of timeval in our Rust environment
        //    matches that of the target process.
        // 2. The request.process.read_mem call has populated buf with valid data
        //    of the appropriate size (ensured by the size check above).
        // 3. The buffer is appropriately aligned for reading a timeval
        //    struct. If the remote process's representation of timeval
        //    was correctly aligned, our local buffer should be too,
        //    since it's an array on the stack.
        #[expect(clippy::cast_ptr_alignment)]
        let timevals = unsafe {
            // Create a raw pointer to the buffer.
            let ptr = buf.as_ptr() as *const libc::timeval;

            // Read the timeval values from the buffer.
            [
                std::ptr::read_unaligned(ptr),
                std::ptr::read_unaligned(ptr.add(1)),
            ]
        };

        Ok((
            TimeSpec::new(
                timevals[0].tv_sec,
                (timevals[0].tv_usec as timespec_tv_nsec_t).saturating_mul(1_000), /* ms->ns */
            ),
            TimeSpec::new(
                timevals[1].tv_sec,
                (timevals[1].tv_usec as timespec_tv_nsec_t).saturating_mul(1_000), /* ms->ns */
            ),
        ))
    }

    /// Read the `TimeSpec32` struct from process memory at the given address.
    pub(crate) fn remote_timespec32(&self, addr: u64) -> Result<TimeSpec, Errno> {
        if addr < *MMAP_MIN_ADDR {
            // timespec pointer is invalid: return EFAULT.
            return Err(Errno::EFAULT);
        }

        const LEN: usize = size_of::<TimeSpec32>();
        let mut buf = [0u8; LEN];
        self.read_mem(&mut buf, addr, LEN)?;

        // SAFETY: The following unsafe block assumes that:
        // 1. The memory layout of timespec in our Rust environment
        //    matches that of the target process.
        // 2. The request.process.read_mem call has populated buf with valid data
        //    of the appropriate size (ensured by the size check above).
        // 3. The buffer is appropriately aligned for reading a timespec
        //    struct. If the remote process's representation of timespec
        //    was correctly aligned, our local buffer should be too,
        //    since it's an array on the stack.
        #[expect(clippy::cast_ptr_alignment)]
        let timespec = unsafe {
            // Create a raw pointer to the buffer.
            let ptr = buf.as_ptr() as *const TimeSpec32;

            // Read the timespec values from the buffer.
            std::ptr::read_unaligned(ptr)
        };

        Ok(TimeSpec::new(
            timespec.tv_sec.into(),
            timespec.tv_nsec.into(),
        ))
    }

    /// Read the `TimeSpec64[2]` struct from process memory at the given address.
    // `as _` casts are used to write portable code for x32 and i386.
    #[expect(clippy::as_underscore)]
    pub(crate) fn remote_timespec64(&self, addr: u64) -> Result<TimeSpec, Errno> {
        if addr < *MMAP_MIN_ADDR {
            // timespec pointer is invalid: return EFAULT.
            return Err(Errno::EFAULT);
        }

        const LEN: usize = size_of::<TimeSpec64>();
        let mut buf = [0u8; LEN];
        self.read_mem(&mut buf, addr, LEN)?;

        // SAFETY: The following unsafe block assumes that:
        // 1. The memory layout of timespec in our Rust environment
        //    matches that of the target process.
        // 2. The request.process.read_mem call has populated buf with valid data
        //    of the appropriate size (ensured by the size check above).
        // 3. The buffer is appropriately aligned for reading a timespec
        //    struct. If the remote process's representation of timespec
        //    was correctly aligned, our local buffer should be too,
        //    since it's an array on the stack.
        #[expect(clippy::cast_ptr_alignment)]
        let timespec = unsafe {
            // Create a raw pointer to the buffer.
            let ptr = buf.as_ptr() as *const TimeSpec64;

            // Read the timespec values from the buffer.
            std::ptr::read_unaligned(ptr)
        };

        Ok(TimeSpec::new(timespec.tv_sec as _, timespec.tv_nsec as _))
    }

    /// Read the `TimeSpec32[2]` struct from process memory at the given address.
    pub(crate) fn remote_timespec32_2(&self, addr: u64) -> Result<(TimeSpec, TimeSpec), Errno> {
        if addr == 0 {
            // timespec pointer is NULL: Set to current time.
            return Ok((TimeSpec::UTIME_NOW, TimeSpec::UTIME_NOW));
        } else if addr < *MMAP_MIN_ADDR {
            // timespec pointer is invalid: return EFAULT.
            return Err(Errno::EFAULT);
        }

        const LEN: usize = size_of::<TimeSpec32>() * 2;
        let mut buf = [0u8; LEN];
        self.read_mem(&mut buf, addr, LEN)?;

        // SAFETY: The following unsafe block assumes that:
        // 1. The memory layout of timespec in our Rust environment
        //    matches that of the target process.
        // 2. The request.process.read_mem call has populated buf with valid data
        //    of the appropriate size (ensured by the size check above).
        // 3. The buffer is appropriately aligned for reading a timespec
        //    struct. If the remote process's representation of timespec
        //    was correctly aligned, our local buffer should be too,
        //    since it's an array on the stack.
        #[expect(clippy::cast_ptr_alignment)]
        let timespecs = unsafe {
            // Create a raw pointer to the buffer.
            let ptr = buf.as_ptr() as *const TimeSpec32;

            // Read the timespec values from the buffer.
            [
                std::ptr::read_unaligned(ptr),
                std::ptr::read_unaligned(ptr.add(1)),
            ]
        };

        Ok((
            TimeSpec::new(timespecs[0].tv_sec.into(), timespecs[0].tv_nsec.into()),
            TimeSpec::new(timespecs[1].tv_sec.into(), timespecs[1].tv_nsec.into()),
        ))
    }

    /// Read the `TimeSpec64[2]` struct from process memory at the given address.
    // `as _` casts are used to write portable code for x32 and i386.
    #[expect(clippy::as_underscore)]
    pub(crate) fn remote_timespec64_2(&self, addr: u64) -> Result<(TimeSpec, TimeSpec), Errno> {
        if addr == 0 {
            // timespec pointer is NULL: Set to current time.
            return Ok((TimeSpec::UTIME_NOW, TimeSpec::UTIME_NOW));
        } else if addr < *MMAP_MIN_ADDR {
            // timespec pointer is invalid: return EFAULT.
            return Err(Errno::EFAULT);
        }

        const LEN: usize = size_of::<TimeSpec64>() * 2;
        let mut buf = [0u8; LEN];
        self.read_mem(&mut buf, addr, LEN)?;

        // SAFETY: The following unsafe block assumes that:
        // 1. The memory layout of timespec in our Rust environment
        //    matches that of the target process.
        // 2. The request.process.read_mem call has populated buf with valid data
        //    of the appropriate size (ensured by the size check above).
        // 3. The buffer is appropriately aligned for reading a timespec
        //    struct. If the remote process's representation of timespec
        //    was correctly aligned, our local buffer should be too,
        //    since it's an array on the stack.
        #[expect(clippy::cast_ptr_alignment)]
        let timespecs = unsafe {
            // Create a raw pointer to the buffer.
            let ptr = buf.as_ptr() as *const TimeSpec64;

            // Read the timespec values from the buffer.
            [
                std::ptr::read_unaligned(ptr),
                std::ptr::read_unaligned(ptr.add(1)),
            ]
        };

        Ok((
            TimeSpec::new(timespecs[0].tv_sec as _, timespecs[0].tv_nsec as _),
            TimeSpec::new(timespecs[1].tv_sec as _, timespecs[1].tv_nsec as _),
        ))
    }

    /// Read path from the given system call argument with the given request.
    /// Check for magic prefix is magic is true.
    ///
    /// Returns `CanonicalPath` and two booleans is-magic and is-empty-path.
    pub(crate) fn read_path(
        &self,
        sandbox: &SandboxGuard,
        arg: SysArg,
    ) -> Result<(CanonicalPath, bool, bool), Errno> {
        let process = RemoteProcess::new(self.scmpreq.pid());

        // SAFETY: The request is validated by read_path.
        let (path, magic, doterr, empty_path) = process.read_path(
            sandbox,
            self.scmpreq.data.arch,
            self.scmpreq.data.args,
            arg,
            Some(self),
        )?;

        // Determine FD-only system calls.
        // We return EACCES rather than ENOENT for these.
        let is_fd = empty_path && arg.flags.contains(SysFlags::EMPTY_PATH);

        // (a) Delayed dotlast Errno::ENOENT handler, see above for the rationale.
        // (b) SAFETY: the Missing check is skipped by fs::canonicalize on purpose,
        // so that EEXIST return value cannot be abused to locate hidden paths.
        if !doterr {
            Ok((path, magic, empty_path))
        } else if path
            .typ
            .as_ref()
            .map(|typ| !typ.is_symlink())
            .unwrap_or(false)
        {
            // Path exists and is not a symbolic link.
            // Return EACCES if this is FD-only call.
            // Return ENOENT if either one of path or parent is hidden.
            // Return EEXIST if not.
            if is_fd {
                Err(Errno::EACCES)
            } else if sandbox.is_hidden(path.abs()) || sandbox.is_hidden(path.abs().parent()) {
                Err(Errno::ENOENT)
            } else {
                Err(Errno::EEXIST)
            }
        } else if is_fd {
            Err(Errno::EACCES)
        } else {
            Err(Errno::ENOENT)
        }
    }

    /// Read a `Zeroizing` vector from remote process's memory with `process_vm_readv()`.
    pub(crate) fn read_vec_zeroed(
        &self,
        remote_addr: u64,
        len: usize,
    ) -> Result<Zeroizing<Vec<u8>>, Errno> {
        let process = RemoteProcess::new(self.scmpreq.pid());

        // SAFETY: The request is validated.
        match unsafe { process.read_vec_zeroed(self.scmpreq.data.arch, remote_addr, len) } {
            Ok(vec) => {
                if self.is_valid() {
                    Ok(vec)
                } else {
                    Err(Errno::ESRCH)
                }
            }
            Err(errno) => Err(errno),
        }
    }

    /// Read a vector from remote process's memory with `process_vm_readv()`.
    pub(crate) fn read_vec(&self, remote_addr: u64, len: usize) -> Result<Vec<u8>, Errno> {
        let process = RemoteProcess::new(self.scmpreq.pid());

        // SAFETY: The request is validated.
        match unsafe { process.read_vec(self.scmpreq.data.arch, remote_addr, len) } {
            Ok(vec) => {
                if self.is_valid() {
                    Ok(vec)
                } else {
                    Err(Errno::ESRCH)
                }
            }
            Err(errno) => Err(errno),
        }
    }

    /// Read data from remote process's memory with `process_vm_readv()`.
    pub(crate) fn read_mem(
        &self,
        local_buffer: &mut [u8],
        remote_addr: u64,
        len: usize,
    ) -> Result<usize, Errno> {
        let process = RemoteProcess::new(self.scmpreq.pid());

        // SAFETY: The request is validated.
        match unsafe { process.read_mem(self.scmpreq.data.arch, local_buffer, remote_addr, len) } {
            Ok(n) => {
                if self.is_valid() {
                    Ok(n)
                } else {
                    Err(Errno::ESRCH)
                }
            }
            Err(errno) => Err(errno),
        }
    }

    /// Write data to remote process's memory with `process_vm_writev()`.
    #[inline(always)]
    pub(crate) fn write_mem(&self, local_buffer: &[u8], remote_addr: u64) -> Result<usize, Errno> {
        if local_buffer.is_empty() {
            return Ok(0);
        }
        let process = RemoteProcess::new(self.scmpreq.pid());

        // SAFETY: The request is validated.
        match unsafe { process.write_mem(self.scmpreq.data.arch, local_buffer, remote_addr) } {
            Ok(n) => {
                if self.is_valid() {
                    Ok(n)
                } else {
                    Err(Errno::ESRCH)
                }
            }
            Err(errno) => Err(errno),
        }
    }

    /// Get file descriptor from remote process with pidfd_getfd(2).
    ///
    /// This function requires Linux 5.6+.
    pub(crate) fn get_fd(&self, remote_fd: RawFd) -> Result<OwnedFd, Errno> {
        // SAFETY: Check if the RawFd is valid.
        if remote_fd < 0 {
            return Err(Errno::EBADF);
        }

        // Open a PidFd or use an already opened one.
        let pid_fd = self.pidfd_open()?;

        // Transfer fd using pidfd_getfd(2)
        pidfd_getfd(pid_fd, remote_fd)
    }

    /// Send a signal to the PIDFd of the process.
    pub(crate) fn pidfd_kill(&self, sig: i32) -> Result<(), Errno> {
        // Open a PidFd by validating it.
        let pid_fd = self.pidfd_open()?;
        pidfd_send_signal(&pid_fd, sig)?;

        // SAFETY: Release memory immediately using process_mrelease(2) if we
        // have sent a SIGKILL to the sandbox process. Above all, this is useful
        // for memory sandboxing.
        if sig == libc::SIGKILL {
            let _ = process_mrelease(&pid_fd);
        }

        Ok(())
    }

    /// Open a PidFd and validate it against the request.
    pub(crate) fn pidfd_open(&self) -> Result<OwnedFd, Errno> {
        // Open the PIDFd.
        let pid_fd = pidfd_open(self.scmpreq.pid(), PIDFD_THREAD)?;

        // SAFETY: Validate the PIDFd by validating the request ID.
        if self.is_valid() {
            Ok(pid_fd)
        } else {
            Err(Errno::ESRCH)
        }
    }

    /// Send the request pid a signal based on the given action.
    ///
    /// Non-signaling actions default to SIGKILL.
    pub(crate) fn kill(&self, action: Action) -> Result<(), Errno> {
        self.pidfd_kill(
            action
                .signal()
                .map(|sig| sig as libc::c_int)
                .unwrap_or(libc::SIGKILL),
        )
    }

    /// Let the kernel continue the syscall.
    ///
    /// # Safety
    /// CAUTION! This method is unsafe because it may suffer TOCTOU attack.
    /// Please read `seccomp_unotify(2)` "NOTES/Design goals; use of `SECCOMP_USER_NOTIF_FLAG_CONTINUE`"
    /// before using this method.
    pub(crate) unsafe fn continue_syscall(&self) -> ScmpNotifResp {
        ScmpNotifResp::new(self.scmpreq.id, 0, 0, ScmpNotifRespFlags::CONTINUE.bits())
    }

    /// Returns error to supervised process.
    pub(crate) fn fail_syscall(&self, err: Errno) -> ScmpNotifResp {
        assert!(err != Errno::UnknownErrno);
        #[expect(clippy::arithmetic_side_effects)]
        ScmpNotifResp::new(self.scmpreq.id, 0, -(err as i32), 0)
    }

    /// Returns value to supervised process.
    pub(crate) fn return_syscall(&self, val: i64) -> ScmpNotifResp {
        ScmpNotifResp::new(self.scmpreq.id, val, 0, 0)
    }

    /// Check if this event is still valid.
    /// In some cases this is necessary, please check `seccomp_unotify(2)` for more information.
    #[inline(always)]
    pub(crate) fn is_valid(&self) -> bool {
        // EAGAIN|EINTR is handled.
        // ENOENT means child died mid-way.
        seccomp_notify_id_valid(self.notify_fd, self.scmpreq.id).is_ok()
    }

    /// Add a file descriptor to the supervised process.
    /// This could help avoid TOCTOU attack in some cases.
    pub(crate) fn add_fd<Fd: AsFd>(
        &self,
        src_fd: Fd,
        close_on_exec: bool,
        randomize_fds: bool,
    ) -> Result<RawFd, Errno> {
        #[expect(clippy::cast_possible_truncation)]
        let (newfd, flags) = if randomize_fds {
            (
                proc_rand_fd(self.scmpreq.pid())?,
                libc::SECCOMP_ADDFD_FLAG_SETFD as u32,
            )
        } else {
            (0, 0)
        };

        let newfd_flags = if close_on_exec {
            libc::O_CLOEXEC as u32
        } else {
            0
        };

        #[expect(clippy::cast_sign_loss)]
        let addfd: seccomp_notif_addfd = seccomp_notif_addfd {
            id: self.scmpreq.id,
            srcfd: src_fd.as_fd().as_raw_fd() as u32,
            newfd: newfd as u32,
            flags,
            newfd_flags,
        };

        // EAGAIN|EINTR is retried.
        // Other errors are fatal,
        // including ENOENT which means child died mid-way.
        seccomp_notify_addfd(self.notify_fd, std::ptr::addr_of!(addfd))
    }

    /// Add a file descriptor to the supervised process,
    /// and reply to the seccomp request at the same time.
    /// This could help avoid TOCTOU attack in some cases.
    pub(crate) fn send_fd<Fd: AsFd>(
        &self,
        src_fd: Fd,
        close_on_exec: bool,
        randomize_fds: bool,
    ) -> Result<ScmpNotifResp, Errno> {
        #[expect(clippy::cast_possible_truncation)]
        let (newfd, flags) = if randomize_fds {
            (
                proc_rand_fd(self.scmpreq.pid())?,
                (libc::SECCOMP_ADDFD_FLAG_SEND as u32 | libc::SECCOMP_ADDFD_FLAG_SETFD as u32),
            )
        } else {
            (0, libc::SECCOMP_ADDFD_FLAG_SEND as u32)
        };

        let newfd_flags = if close_on_exec {
            libc::O_CLOEXEC as u32
        } else {
            0
        };

        #[expect(clippy::cast_sign_loss)]
        let addfd: seccomp_notif_addfd = seccomp_notif_addfd {
            id: self.scmpreq.id,
            srcfd: src_fd.as_fd().as_raw_fd() as u32,
            newfd: newfd as u32,
            flags,
            newfd_flags,
        };

        // EAGAIN|EINTR is retried.
        // Other errors are fatal,
        // including ENOENT which means child died mid-way.
        seccomp_notify_addfd(self.notify_fd, std::ptr::addr_of!(addfd))?;

        // We do not need to send a response,
        // send a dummy response to the caller
        // can skip it gracefully.
        Ok(ScmpNotifResp::new(0, 0, EIDRM, 0))
    }
}
