//
// Syd: rock-solid application kernel
// src/proc.rs: /proc utilities
//
// Copyright (c) 2023, 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
// Based in part upon procps-ng's library/devname.c which is:
//   Copyright © 2011-2023 Jim Warner <james.warner@comcast.net>
//   Copyright © 2017-2023 Craig Small <csmall@dropbear.xyz>
//   Copyright © 1998-2009 Albert Cahalan
// proc_auxv function is based in part upon procfs crate which is:
//   Copyright (c) 2015 The procfs Developers
//   SPDX-License-Identifier: MIT
// /proc/pid/stat* parsing functions are based in part upon procinfo-rs crate which is:
//   Copyright (c) 2015 The Rust Project Developers
//   SPDX-License-Identifier: MIT
// PROCMAP_QUERY ioctl(2) interface is based on <linux/fs.h> which is:
//   SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
//
// SPDX-License-Identifier: GPL-3.0

// SAFETY: This module has (almost) been liberated from unsafe code!
// proc_set_at_secure_* tests call fork(2) therefore they need unsafe.
// pagemap_query uses ioctl(2) which calls into libc therefore needs unsafe.
// Use deny rather than forbid so we can allow these cases.
#![deny(unsafe_code)]

use std::{
    borrow::Cow,
    ffi::{CStr, OsStr, OsString},
    fmt,
    fs::File,
    io::{BufReader, Cursor, IoSlice, IoSliceMut, Read, Seek, SeekFrom, Write},
    num::NonZeroUsize,
    ops::Range,
    os::{
        fd::{AsFd, AsRawFd, OwnedFd, RawFd},
        unix::ffi::OsStrExt,
    },
    ptr::NonNull,
};

use bitflags::bitflags;
use btoi::{btoi, btoi_radix};
use memchr::{arch::all::is_prefix, memchr, memrchr};
use nix::{
    errno::Errno,
    fcntl::OFlag,
    libc::pid_t,
    sys::{
        stat::Mode,
        sysinfo::sysinfo,
        uio::{process_vm_readv, process_vm_writev, RemoteIoVec},
    },
    unistd::{getpid, lseek64, write, Gid, Pid, Uid, Whence},
    NixPath,
};
use procfs_core::{
    net::UnixNetEntries,
    process::{
        LimitValue, MMPermissions, MMapPath, MemoryMaps, Namespace, Namespaces, SmapsRollup,
    },
    CryptoTable, FromBufRead,
};
use serde::{ser::SerializeMap, Serialize, Serializer};

use crate::{
    compat::{getdents64, openat2, statx, OpenHow, ResolveFlag, STATX_INO},
    config::*,
    confine::SydMemoryMap,
    cookie::{CookieIdx, SYSCOOKIE_POOL},
    elf::ElfType,
    err::{err2no, proc_error_to_errno},
    fd::{is_empty_file, is_open_fd, is_proc, parse_fd, AT_BADFD},
    fs::readlinkat,
    hash::{SydHashMap, SydHashSet, SydIndexSet},
    io::{read_all, read_buf, write_all},
    lookup::safe_open,
    parsers::{
        map_result,
        proc::{
            parse_max_open_files, parse_stat, parse_statm, parse_status, parse_status_tgid,
            parse_status_umask,
        },
        read_to_end,
    },
    path::{XPathBuf, PATH_MAX},
    retry::retry_on_eintr,
    rng::randint,
    sandbox::Sandbox,
    sigset::SydSigSet,
    XPath,
};

bitflags! {
    /// VMA permission flags.
    ///
    /// Can be used as part of `ProcmapQueryFlags` to look up only VMAs
    /// satisfying specified subset of permissions. E.g., specifying
    /// `PROCMAP_QUERY_VMA_READABLE` only will return both readable and
    /// read/write VMAs, while having `PROCMAP_QUERY_VMA_READABLE |
    /// PROCMAP_QUERY_VMA_WRITABLE` will only return read/write VMAs,
    /// though both executable/non-executable and private/shared will be ignored.
    ///
    /// `PROCMAP_QUERY_VMA_*` flags are also returned in `vma_flags`
    /// field to specify actual VMA permissions.
    #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
    pub struct ProcmapQueryFlags: u64 {
        /// query-readable flag
        const VMA_READABLE     = 0x01;
        /// query-writable flag
        const VMA_WRITABLE     = 0x02;
        /// query-executable flag
        const VMA_EXECUTABLE   = 0x04;
        /// query-shared flag
        const VMA_SHARED       = 0x08;

        /// Query modifier flags.
        ///
        /// By default VMA that covers provided address is returned,
        /// or `-ENOENT` is returned. With `PROCMAP_QUERY_COVERING_OR_NEXT_VMA`
        /// flag set, closest VMA with `vma_start > addr` will be returned if
        /// no covering VMA is found.
        ///
        /// `PROCMAP_QUERY_FILE_BACKED_VMA` instructs query to consider only
        /// VMAs that have file backing. Can be combined with
        /// `PROCMAP_QUERY_COVERING_OR_NEXT_VMA` to iterate all VMAs with
        /// file backing.
        const COVERING_OR_NEXT_VMA = 0x10;
        /// query-file-backed flag
        const FILE_BACKED_VMA      = 0x20;
    }
}

impl fmt::Display for ProcmapQueryFlags {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.contains(Self::VMA_READABLE) {
            f.write_str("r")?;
        } else {
            f.write_str("-")?;
        }
        if self.contains(Self::VMA_WRITABLE) {
            f.write_str("w")?;
        } else {
            f.write_str("-")?;
        }
        if self.contains(Self::VMA_EXECUTABLE) {
            f.write_str("x")?;
        } else {
            f.write_str("-")?;
        }
        if self.contains(Self::VMA_SHARED) {
            f.write_str("s")?;
        } else {
            f.write_str("p")?;
        }
        Ok(())
    }
}

impl Serialize for ProcmapQueryFlags {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.collect_str(self)
    }
}

impl From<MMPermissions> for ProcmapQueryFlags {
    fn from(perms: MMPermissions) -> Self {
        let mut flags = Self::empty();

        if perms.contains(MMPermissions::READ) {
            flags.insert(Self::VMA_READABLE);
        }

        if perms.contains(MMPermissions::WRITE) {
            flags.insert(Self::VMA_WRITABLE);
        }

        if perms.contains(MMPermissions::EXECUTE) {
            flags.insert(Self::VMA_EXECUTABLE);
        }

        if perms.contains(MMPermissions::SHARED) {
            flags.insert(Self::VMA_SHARED);
        }

        flags
    }
}

/// Input/output argument structure passed into the _ioctl_(2) call. It can
/// be used to query a set of VMAs (Virtual Memory Areas) of a process.
///
/// Each field can be one of three kinds, marked in a short comment to the
/// right of the field:
///   - "in", input argument, user has to provide this value, kernel doesn't modify it;
///   - "out", output argument, kernel sets this field with VMA data;
///   - "in/out", input and output argument; user provides initial value
///     (used to specify maximum allowable buffer size), and kernel sets it
///     to actual amount of data written (or zero, if there is no data).
///
/// If matching VMA is found (according to criteria specified by
/// `query_addr`/`query_flags`), all the out fields are filled out, and
/// _ioctl_(2) returns `0`. If there is no matching VMA, `Err(Errno::ENOENT)` will be
/// returned. In case of any other error, a negative error code other than
/// `Err(Errno::ENOENT)` is returned.
///
/// Most of the data is similar to the one returned as text in
/// _proc_pid_maps_(5), but `procmap_query` provides more querying flexibility.
/// There are no consistency guarantees between subsequent _ioctl_(2) calls,
/// but data returned for a matched VMA is self-consistent.
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct ProcmapQuery {
    /// Query struct size, for backwards/forward compatibility.
    pub size: u64,

    /// Query flags, a combination of `ProcmapQueryFlags` values.
    /// Defines query filtering and behavior.
    /// (in)
    pub query_flags: u64,

    /// Query address. By default, VMA that covers this address will be looked up.
    /// Modifier flags further adjust behavior.
    /// (in)
    pub query_addr: u64,

    /// VMA starting (inclusive) address, if VMA is found.
    /// (out)
    pub vma_start: u64,

    /// VMA ending (exclusive) address, if VMA is found.
    /// (out)
    pub vma_end: u64,

    /// VMA permissions flags. A combination of `ProcmapQueryFlags::PROCMAP_QUERY_VMA_*`.
    /// (out)
    pub vma_flags: u64,

    /// VMA backing page size granularity.
    /// (out)
    pub vma_page_size: u64,

    /// VMA file offset within the backing file. Zero if no backing file.
    /// (out)
    pub vma_offset: u64,

    /// Backing file's inode number, or zero if no backing file.
    /// (out)
    pub inode: u64,

    /// Backing file's device major number, or zero if no backing file.
    /// (out)
    pub dev_major: u32,

    /// Backing file's device minor number, or zero if no backing file.
    /// (out)
    pub dev_minor: u32,

    /// Maximum/returned size of the VMA name buffer.
    /// Kernel sets to actual bytes written (including terminating NUL).
    /// (in/out)
    pub vma_name_size: u32,

    /// Maximum/returned size of the build ID buffer.
    /// Kernel sets to actual bytes written.
    /// (in/out)
    pub build_id_size: u32,

    /// User-space address of the buffer for VMA name, or zero if unused.
    /// (in)
    pub vma_name_addr: u64,

    /// User-space address of the buffer for build ID, or zero if unused.
    /// (in)
    pub build_id_addr: u64,
}

impl Default for ProcmapQuery {
    fn default() -> Self {
        Self {
            size: size_of::<Self>() as u64,
            query_flags: 0,
            query_addr: 0,
            vma_start: 0,
            vma_end: 0,
            vma_flags: 0,
            vma_page_size: 0,
            vma_offset: 0,
            inode: 0,
            dev_major: 0,
            dev_minor: 0,
            vma_name_size: 0,
            build_id_size: 0,
            vma_name_addr: 0,
            build_id_addr: 0,
        }
    }
}

/// 'f' is the magic number for _procfs_(5) ioctls.
pub const PROCFS_IOCTL_MAGIC: u32 = b'f' as u32;

/// _ioctl_(2) request number for _proc_pid_maps_(5) PROCMAP_QUERY.
pub const PROCMAP_QUERY: libc::c_ulong =
    libc::_IOWR::<ProcmapQuery>(PROCFS_IOCTL_MAGIC, 17) as libc::c_ulong;

/// Safe wrapper around the _proc_pid_maps_(5) PROCMAP_QUERY ioctl(2).
///
/// # Arguments
///
/// * `fd` – Descriptor for _proc_pid_maps_(5)
/// * `flags` – `ProcmapQueryFlags` controlling query filtering and iteration.
/// * `query_addr` – Address to probe; interpretation depends on `flags`.
/// * `name_buf` – Optional buffer to receive the VMA's name (including trailing NUL byte).
/// * `build_id_buf` – Optional buffer to receive the VMA's ELF build ID.
///
/// # Returns
///
/// On success, returns the populated `ProcmapQuery` struct.
/// On error, returns the corresponding `Errno`.
pub fn procmap_query<Fd: AsFd>(
    fd: Fd,
    flags: ProcmapQueryFlags,
    query_addr: u64,
    name_buf: Option<&mut [u8]>,
    build_id_buf: Option<&mut [u8]>,
) -> Result<ProcmapQuery, Errno> {
    let mut q = ProcmapQuery {
        query_addr,
        query_flags: flags.bits(),
        ..Default::default()
    };

    if let Some(buf) = name_buf {
        q.vma_name_size = u32::try_from(buf.len()).or(Err(Errno::EINVAL))?;
        q.vma_name_addr = buf.as_mut_ptr() as u64;
    }

    if let Some(buf) = build_id_buf {
        q.build_id_size = u32::try_from(buf.len()).or(Err(Errno::EINVAL))?;
        q.build_id_addr = buf.as_mut_ptr() as u64;
    }

    // SAFETY:
    // `q` is #[repr(C)] and fully initialized;
    // buffers live at least until ioctl(2) returns.
    #[expect(unsafe_code)]
    Errno::result(unsafe {
        libc::syscall(
            libc::SYS_ioctl,
            fd.as_fd().as_raw_fd(),
            PROCMAP_QUERY,
            &mut q,
            SYSCOOKIE_POOL.get(CookieIdx::ProcmapQueryArg3),
            SYSCOOKIE_POOL.get(CookieIdx::ProcmapQueryArg4),
            SYSCOOKIE_POOL.get(CookieIdx::ProcmapQueryArg5),
        )
    })
    .map(|_| q)
}

/// Process memory usage information.
///
/// All values are in units of pages.
///
/// See `man 5 proc` and `Linux/fs/proc/array.c`.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct Statm {
    /// Total virtual memory size.
    pub size: u64,
}

/// Process status information.
///
/// See `man 5 proc` and `Linux/fs/proc/array.c`.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct Stat {
    /// Number of threads in this process (since Linux 2.6).
    pub num_threads: u64,
    /// The address above which program heap can be expanded with brk(2).
    pub startbrk: u64,
    /// The address of the start (i.e., bottom) of the stack.
    pub startstack: u64,
    /// The controlling terminal of the process. (The minor device
    /// number is contained in the combination of bits 31 to 20 and 7 to
    /// 0; the major device number is in bits 15 to 8.)
    pub tty_nr: i32,
}

/// Process status information.
///
/// See `man 5 proc` and `Linux/fs/proc/array.c`.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Status {
    /// File mode creation mask (since Linux 4.7).
    pub umask: Mode,
    /// Process ID (i.e., Thread Group ID).
    pub pid: Pid,
    /// Number of signals pending for the thread (see pthreads(7)).
    pub sig_pending_thread: SydSigSet,
    /// Number of signals pending for the process (see signal(7)).
    pub sig_pending_process: SydSigSet,
    /// Mask indicating signals being blocked.
    pub sig_blocked: SydSigSet,
    /// Mask indicating signals being ignored.
    pub sig_ignored: SydSigSet,
    /// Mask indicating signals being caught.
    pub sig_caught: SydSigSet,
}

// Default is used by parsers.
// Pid has no default therefore this is needed.
impl Default for Status {
    fn default() -> Self {
        Self {
            umask: Mode::empty(),
            pid: Pid::from_raw(0),
            sig_pending_thread: SydSigSet::default(),
            sig_pending_process: SydSigSet::default(),
            sig_blocked: SydSigSet::default(),
            sig_ignored: SydSigSet::default(),
            sig_caught: SydSigSet::default(),
        }
    }
}

impl Serialize for Status {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut map = serializer.serialize_map(Some(7))?;
        map.serialize_entry("umask", &self.umask.bits())?;
        map.serialize_entry("pid", &self.pid.as_raw())?;
        map.serialize_entry("sig_pending_thread", &self.sig_pending_thread)?;
        map.serialize_entry("sig_pending_process", &self.sig_pending_process)?;
        map.serialize_entry("sig_blocked", &self.sig_blocked)?;
        map.serialize_entry("sig_ignored", &self.sig_ignored)?;
        map.serialize_entry("sig_caught", &self.sig_caught)?;
        map.end()
    }
}

/// Safe `OFlags` to use when opening proc(5) files.
pub const OFLAG_PROC: OFlag =
    OFlag::from_bits_retain(libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_NOCTTY);

/// Safe `ResolveFlag` to use when opening proc(5) files.
pub const RFLAG_PROC: ResolveFlag = ResolveFlag::from_bits_retain(
    ResolveFlag::RESOLVE_NO_MAGICLINKS.bits()
        | ResolveFlag::RESOLVE_NO_SYMLINKS.bits()
        | ResolveFlag::RESOLVE_BENEATH.bits()
        | ResolveFlag::RESOLVE_NO_XDEV.bits(),
);

// Function to map proc(5) errno ENOENT to ESRCH to reduce confusion.
fn proc_errno(err: Errno) -> Errno {
    match err {
        Errno::ENOENT => Errno::ESRCH,
        err => err,
    }
}

/// Function to safely open a proc(5) file and return an `OwnedFd`.
pub fn safe_open_proc<P: NixPath + ?Sized>(path: &P) -> Result<OwnedFd, Errno> {
    safe_open(PROC_FILE(), path, OFLAG_PROC, RFLAG_PROC)
}

// major 204 is a mess -- "Low-density serial ports"
const LOW_DENSITY_NAMES: &[&str] = &[
    "LU0", "LU1", "LU2", "LU3", "FB0", "SA0", "SA1", "SA2", "SC0", "SC1", "SC2", "SC3", "FW0",
    "FW1", "FW2", "FW3", "AM0", "AM1", "AM2", "AM3", "AM4", "AM5", "AM6", "AM7", "AM8", "AM9",
    "AM10", "AM11", "AM12", "AM13", "AM14", "AM15", "DB0", "DB1", "DB2", "DB3", "DB4", "DB5",
    "DB6", "DB7", "SG0", "SMX0", "SMX1", "SMX2", "MM0", "MM1", "CPM0", "CPM1", "CPM2",
    "CPM3", /* "CPM4", "CPM5", */
    // bad allocation?
    "IOC0", "IOC1", "IOC2", "IOC3", "IOC4", "IOC5", "IOC6", "IOC7", "IOC8", "IOC9", "IOC10",
    "IOC11", "IOC12", "IOC13", "IOC14", "IOC15", "IOC16", "IOC17", "IOC18", "IOC19", "IOC20",
    "IOC21", "IOC22", "IOC23", "IOC24", "IOC25", "IOC26", "IOC27", "IOC28", "IOC29", "IOC30",
    "IOC31", "VR0", "VR1", "IOC84", "IOC85", "IOC86", "IOC87", "IOC88", "IOC89", "IOC90", "IOC91",
    "IOC92", "IOC93", "IOC94", "IOC95", "IOC96", "IOC97", "IOC98", "IOC99", "IOC100", "IOC101",
    "IOC102", "IOC103", "IOC104", "IOC105", "IOC106", "IOC107", "IOC108", "IOC109", "IOC110",
    "IOC111", "IOC112", "IOC113", "IOC114", "IOC115", "SIOC0", "SIOC1", "SIOC2", "SIOC3", "SIOC4",
    "SIOC5", "SIOC6", "SIOC7", "SIOC8", "SIOC9", "SIOC10", "SIOC11", "SIOC12", "SIOC13", "SIOC14",
    "SIOC15", "SIOC16", "SIOC17", "SIOC18", "SIOC19", "SIOC20", "SIOC21", "SIOC22", "SIOC23",
    "SIOC24", "SIOC25", "SIOC26", "SIOC27", "SIOC28", "SIOC29", "SIOC30", "SIOC31", "PSC0", "PSC1",
    "PSC2", "PSC3", "PSC4", "PSC5", "AT0", "AT1", "AT2", "AT3", "AT4", "AT5", "AT6", "AT7", "AT8",
    "AT9", "AT10", "AT11", "AT12", "AT13", "AT14", "AT15", "NX0", "NX1", "NX2", "NX3", "NX4",
    "NX5", "NX6", "NX7", "NX8", "NX9", "NX10", "NX11", "NX12", "NX13", "NX14", "NX15",
    "J0", // minor is 186
    "UL0", "UL1", "UL2", "UL3", "xvc0", // FAIL -- "/dev/xvc0" lacks "tty" prefix
    "PZ0", "PZ1", "PZ2", "PZ3", "TX0", "TX1", "TX2", "TX3", "TX4", "TX5", "TX6", "TX7", "SC0",
    "SC1", "SC2", "SC3", "MAX0", "MAX1", "MAX2", "MAX3",
];

fn read_usize_from_ne_bytes(bytes: &[u8], ptr_size: usize) -> Result<usize, Errno> {
    match ptr_size {
        4 => {
            if bytes.len() < 4 {
                return Err(Errno::EFAULT);
            }
            Ok(usize::try_from(u32::from_ne_bytes(
                bytes[..4].try_into().or(Err(Errno::EINVAL))?,
            ))
            .or(Err(Errno::EINVAL))?)
        }
        8 => {
            if bytes.len() < 8 {
                return Err(Errno::EFAULT);
            }
            Ok(usize::try_from(u64::from_ne_bytes(
                bytes[..8].try_into().or(Err(Errno::EINVAL))?,
            ))
            .or(Err(Errno::EINVAL))?)
        }
        _ => Err(Errno::EINVAL),
    }
}

fn usize_to_ne_bytes(value: usize, sizeof_ptr: usize) -> Vec<u8> {
    #[expect(clippy::cast_possible_truncation)]
    match sizeof_ptr {
        4 => (value as u32).to_ne_bytes().to_vec(),
        8 => (value as u64).to_ne_bytes().to_vec(),
        _ => unreachable!("Invalid pointer size!"),
    }
}

/// Return the stat of the given process.
pub fn proc_stat(pid: Pid) -> Result<Stat, Errno> {
    let mut path = XPathBuf::from_pid(pid)?;
    path.try_reserve(b"/stat".len()).or(Err(Errno::ENOMEM))?;
    path.push(b"stat");

    // Safely open proc(5) file.
    let file = safe_open_proc(&path).map_err(proc_errno)?;

    let mut buf = [0; 1024]; // A typical stat file is about 300 bytes.
    map_result(parse_stat(read_to_end(file, &mut buf)?))
}

/// Return the memory stat of the given process.
pub fn proc_statm(pid: Pid) -> Result<Statm, Errno> {
    let mut path = XPathBuf::from_pid(pid)?;
    path.try_reserve(b"/statm".len()).or(Err(Errno::ENOMEM))?;
    path.push(b"statm");

    // Safely open proc(5) file.
    let file = safe_open_proc(&path).map_err(proc_errno)?;

    let mut buf = [0; 256]; // A typical statm file is about 25 bytes.
    map_result(parse_statm(read_to_end(file, &mut buf)?))
}

/// Return the status of the given process.
pub fn proc_status(pid: Pid) -> Result<Status, Errno> {
    proc_status_read(proc_status_open(pid)?)
}

/// Open a FD to /proc/$pid/status and return an OwnedFd.
pub fn proc_status_open(pid: Pid) -> Result<OwnedFd, Errno> {
    let mut path = XPathBuf::from_pid(pid)?;
    path.try_reserve(b"/status".len()).or(Err(Errno::ENOMEM))?;
    path.push(b"status");

    safe_open_proc(&path).map_err(proc_errno)
}

/// Return the status of the given process from the given Fd.
pub fn proc_status_read<Fd: AsFd>(fd: Fd) -> Result<Status, Errno> {
    // A typical status file is about 1500 bytes.
    //
    // SAFETY: read_to_end will return EOVERFLOW if
    // proc_pid_status(5) ever grows beyond 2048 bytes
    // so we will know.
    let mut buf = [0; 2048];

    map_result(parse_status(read_to_end(fd, &mut buf)?))
}

/// Retrieves the thread group ID (TGID) for the specified thread ID (TID).
pub fn proc_tgid(tid: Pid) -> Result<Pid, Errno> {
    // This function is a hot-path, so we provide a specialized impl.
    // proc_status(tid).map(|stat| stat.pid)
    let fd = proc_status_open(tid)?;

    // Read up to 192 bytes.
    let mut data = [0u8; 192];
    let nread = read_buf(fd, &mut data)?;

    map_result(parse_status_tgid(&data[..nread]))
}

/// Get the umask of the given `Pid`.
pub fn proc_umask(pid: Pid) -> Result<Mode, Errno> {
    // This function is a hot-path, so we provide a specialized impl.
    // proc_status(pid).map(|stat| stat.umask)
    let fd = proc_status_open(pid)?;

    // Read up to 160 bytes.
    let mut data = [0u8; 160];
    let nread = read_buf(fd, &mut data)?;

    map_result(parse_status_umask(&data[..nread]))
}

/// Locate stack and return the stack memory address range.
pub fn proc_stack(pid: Pid) -> Result<Range<u64>, Errno> {
    let maps = proc_maps(pid)?;

    // Iterate over each memory map entry.
    for map in maps {
        if let MMapPath::Stack = map.0.pathname {
            return Ok(map.0.address.0..map.0.address.1);
        }
    }

    Err(Errno::ENOENT)
}

/// Retrieves the address of the start of stack for the specified process ID (pid).
pub fn proc_stack_start(pid: Pid) -> Result<u64, Errno> {
    proc_stat(pid).map(|stat| stat.startstack)
}

/// Retrieves the current stack pointer (rsp) for the specified process ID (pid).
///
/// If the process is "running", it returns `Errno::EBUSY`.
pub fn proc_stack_pointer(pid: Pid) -> Result<u64, Errno> {
    let mut path = XPathBuf::from_pid(pid)?;
    path.try_reserve(b"/syscall".len()).or(Err(Errno::ENOMEM))?;
    path.push(b"syscall");

    // Safely open proc(5) file.
    let file = safe_open_proc(&path).map_err(proc_errno)?;
    let data = read_all(file)?;

    // Check if the contents equal "running".
    if is_prefix(&data, b"running") {
        return Err(Errno::EBUSY);
    }

    // Find the last space; if none, we don't have >= 2 parts.
    let last_sp = match memrchr(b' ', &data) {
        Some(i) => i,
        None => return Err(Errno::ENOENT),
    };

    // Find the previous space (or start of slice) to get the second-to-last part.
    let start = if let Some(prev) = memrchr(b' ', &data[..last_sp]) {
        prev.checked_add(1).ok_or(Errno::EOVERFLOW)?
    } else {
        0
    };

    // Find the slice to the rsp.
    let rsp = &data[start..last_sp];

    // Convert the rsp value from hex to u64.
    if let Some(hex) = rsp.strip_prefix(b"0x") {
        btoi_radix::<u64>(hex, 16).or(Err(Errno::EINVAL))
    } else {
        Err(Errno::ENOENT)
    }
}

/// Get the auxiliary vector of the given `Pid`.
pub fn proc_auxv(pid: Pid) -> Result<SydHashMap<u64, u64>, Errno> {
    let mut path = XPathBuf::from_pid(pid)?;
    path.try_reserve(b"/auxv".len()).or(Err(Errno::ENOMEM))?;
    path.push(b"auxv");

    // Safely open proc(5) file.
    let file = safe_open_proc(&path).map(File::from).map_err(proc_errno)?;

    let mut map = SydHashMap::default();

    let buf = read_all(&file)?;
    if buf.is_empty() {
        // Some kernel processes won't have any data for their auxv file.
        return Ok(map);
    }

    let mut file = Cursor::new(buf);
    let mut buf = 0usize.to_ne_bytes();
    loop {
        file.read_exact(&mut buf).map_err(|err| err2no(&err))?;
        let key = usize::from_ne_bytes(buf) as u64;
        file.read_exact(&mut buf).map_err(|err| err2no(&err))?;
        let value = usize::from_ne_bytes(buf) as u64;
        if key == 0 && value == 0 {
            break;
        }
        map.insert(key, value);
    }

    Ok(map)
}

/// Retrieves the current working directory (CWD) of the specified process ID (PID).
///
/// This function reads the symbolic link `/proc/<pid>/cwd` to determine the CWD.
pub fn proc_cwd(pid: Pid) -> Result<XPathBuf, Errno> {
    let path = XPathBuf::from_cwd(pid)?;
    readlinkat(PROC_FILE(), &path).map_err(proc_errno)
}

/// Retrieves the command name (comm) of the specified process ID (PID)
/// as a single string.
pub fn proc_comm(pid: Pid) -> Result<XPathBuf, Errno> {
    let mut path = XPathBuf::from_pid(pid)?;
    path.try_reserve(b"/comm".len()).or(Err(Errno::ENOMEM))?;
    path.push(b"comm");

    // Safely open proc(5) file.
    let file = safe_open_proc(&path).map_err(proc_errno)?;

    // Read up to 16 characters or until EOF.
    let mut comm = [0u8; 16];
    let mut nread = read_buf(file, &mut comm)?;

    // Remove the trailing NUL-byte and return an `XPathBuf`.
    let idx = nread.saturating_sub(1);
    if comm[idx] == 0 {
        nread = idx;
    }
    Ok(XPathBuf::from(OsStr::from_bytes(
        comm[..nread].trim_ascii(),
    )))
}

/// Retrieves the command line of the specified process ID (PID)
/// concatenated as a single string.
///
/// This function reads the `/proc/<pid>/cmdline` file and concatenates
/// the arguments using spaces. The function takes care of replacing null
/// bytes (`'\0'`) with spaces to format the command line as a readable string.
pub fn proc_cmdline(pid: Pid) -> Result<XPathBuf, Errno> {
    // Construct path to the appropriate cmdline file.
    let mut path = XPathBuf::from_pid(pid)?;
    path.try_reserve(b"/cmdline".len()).or(Err(Errno::ENOMEM))?;
    path.push(b"cmdline");

    // Safely open proc(5) file.
    let file = safe_open_proc(&path).map_err(proc_errno)?;

    // Read up to 256 bytes.
    const LIMIT: usize = 256;
    let mut data = [0u8; LIMIT];
    let nread = read_buf(file, &mut data)?;

    let mut data = data.to_vec();
    // Determine if EOF was reached or if we hit the limit
    #[expect(clippy::arithmetic_side_effects)]
    if nread <= 1 {
        // Empty cmdline.
        return Ok(XPathBuf::empty());
    } else if nread >= LIMIT - 1 {
        // Check if the last byte read is not a null byte,
        // indicating there's more data.
        if data[LIMIT - 1] != 0 {
            // Append ellipsis to indicate truncation.
            data.extend_from_slice("…".as_bytes());
        } else {
            // Remove the NUL-byte.
            data.pop();
        }
    } else {
        // If EOF was hit before the limit,
        // resize the buffer to nread - 1 (for null byte).
        data.resize(nread - 1, 0);
    }

    // Replace null bytes with spaces.
    for byte in &mut data {
        if *byte == 0 {
            *byte = b' ';
        }
    }

    Ok(data.into())
}

/// Return the memory maps of the given process.
pub fn proc_maps(pid: Pid) -> Result<Vec<SydMemoryMap>, Errno> {
    proc_maps_read(proc_maps_open(pid)?)
}

/// Open a FD to _proc_pid_maps_(5) and return an OwnedFd.
pub fn proc_maps_open(pid: Pid) -> Result<OwnedFd, Errno> {
    let mut path = XPathBuf::from_pid(pid)?;
    path.try_reserve(b"/maps".len()).or(Err(Errno::ENOMEM))?;
    path.push(b"maps");

    safe_open_proc(&path).map_err(proc_errno)
}

/// Return the memory maps of the given process from the given Fd.
pub fn proc_maps_read(fd: OwnedFd) -> Result<Vec<SydMemoryMap>, Errno> {
    let reader = BufReader::new(File::from(fd));

    MemoryMaps::from_buf_read(reader)
        .map(|maps| maps.0.into_iter().map(SydMemoryMap).collect::<Vec<_>>())
        .map_err(|err| proc_error_to_errno(&err).unwrap_or(Errno::ENOSYS))
        .map_err(proc_errno)
}

/// Return the memory maps of the given process.
pub fn proc_smaps(pid: Pid) -> Result<Vec<SydMemoryMap>, Errno> {
    let mut path = XPathBuf::from_pid(pid)?;
    path.try_reserve(b"/smaps".len()).or(Err(Errno::ENOMEM))?;
    path.push(b"smaps");

    // Safely open proc(5) file.
    let reader = safe_open_proc(&path)
        .map(File::from)
        .map(BufReader::new)
        .map_err(proc_errno)?;

    MemoryMaps::from_buf_read(reader)
        .map(|maps| maps.0.into_iter().map(SydMemoryMap).collect::<Vec<_>>())
        .map_err(|err| proc_error_to_errno(&err).unwrap_or(Errno::ENOSYS))
        .map_err(proc_errno)
}

/// Return the rollup memory map of the given process.
///
/// This is more efficient than using `proc_smaps` and iterating over maps.
/// See: https://www.kernel.org/doc/Documentation/ABI/testing/procfs-smaps_rollup
pub fn proc_smaps_rollup(pid: Pid) -> Result<SydMemoryMap, Errno> {
    let mut path = XPathBuf::from_pid(pid)?;
    path.try_reserve(b"/smaps_rollup".len())
        .or(Err(Errno::ENOMEM))?;
    path.push(b"smaps_rollup");

    // Safely open proc(5) file.
    let reader = safe_open_proc(&path)
        .map(File::from)
        .map(BufReader::new)
        .map_err(proc_errno)?;

    SmapsRollup::from_buf_read(reader)
        .map_err(|err| proc_error_to_errno(&err).unwrap_or(Errno::ENOSYS))
        .map_err(proc_errno)
        .and_then(|maps| {
            maps.memory_map_rollup
                .into_iter()
                .next()
                .map(SydMemoryMap)
                .ok_or(Errno::ENOMEM)
        })
}

/// Calculate total memory usage of a process using _proc_pid_smaps_rollup(5).
///
/// See: https://www.kernel.org/doc/Documentation/ABI/testing/procfs-smaps_rollup
pub fn proc_mem(pid: Pid) -> Result<u64, Errno> {
    let map = proc_smaps_rollup(pid)?;
    let mut sum = 0u64;
    for key in ["Pss", "Private_Dirty", "Shared_Dirty"] {
        let val = map.0.extension.map.get(key).copied().unwrap_or(0);
        sum = sum.saturating_add(val);
    }
    Ok(sum)
}

/// Return the system pipe max limit.
pub fn proc_pipemax() -> Result<libc::c_int, Errno> {
    proc_pipemax_read(proc_pipemax_open()?)
}

/// Open a FD to /proc/sys/fs/pipe-max-size and return an OwnedFd.
pub fn proc_pipemax_open() -> Result<OwnedFd, Errno> {
    safe_open_proc("sys/fs/pipe-max-size")
}

/// Retrieve the system pipe max limit from the given Fd.
pub fn proc_pipemax_read<Fd: AsFd>(fd: Fd) -> Result<libc::c_int, Errno> {
    // Read up to 24 bytes.
    let mut data = [0u8; 24];
    let nread = read_buf(fd, &mut data)?;

    btoi::<libc::c_int>(data[..nread].trim_ascii()).or(Err(Errno::EINVAL))
}

/// Retrieves the value of /proc/sys/fs/file-max.
pub fn proc_fs_file_max() -> Result<u64, Errno> {
    let fd = proc_open().and_then(|fd| {
        safe_open(
            fd,
            c"sys/fs/file/max",
            OFlag::O_RDONLY | OFlag::O_NOCTTY,
            ResolveFlag::RESOLVE_NO_XDEV,
        )
    })?;

    // Check the file is an empty file or bail out.
    if !is_empty_file(&fd).unwrap_or(false) {
        return Err(Errno::EBADFD);
    }

    // Read up to 24 bytes + 1 bytes for \n.
    let mut data = [0u8; 25];
    let nread = read_buf(fd, &mut data)?;

    btoi::<u64>(data[..nread].trim_ascii()).or(Err(Errno::EINVAL))
}

/// Retrieves the value of /proc/sys/fs/nr_open.
pub fn proc_fs_nr_open() -> Result<u64, Errno> {
    let fd = proc_open().and_then(|fd| {
        safe_open(
            fd,
            c"sys/fs/nr_open",
            OFlag::O_RDONLY | OFlag::O_NOCTTY,
            ResolveFlag::RESOLVE_NO_XDEV,
        )
    })?;

    // Check the file is an empty file or bail out.
    if !is_empty_file(&fd).unwrap_or(false) {
        return Err(Errno::EBADFD);
    }

    // Read up to 24 bytes + 1 bytes for \n.
    let mut data = [0u8; 25];
    let nread = read_buf(fd, &mut data)?;

    btoi::<u64>(data[..nread].trim_ascii()).or(Err(Errno::EINVAL))
}

/// Retrieves the value of /proc/sys/vm/mmap_min_addr.
pub fn proc_mmap_min_addr() -> Result<u64, Errno> {
    let fd = proc_open().and_then(|fd| {
        safe_open(
            fd,
            c"sys/vm/mmap_min_addr",
            OFlag::O_RDONLY | OFlag::O_NOCTTY,
            ResolveFlag::RESOLVE_NO_XDEV,
        )
    })?;

    // Check the file is an empty file or bail out.
    if !is_empty_file(&fd).unwrap_or(false) {
        return Err(Errno::EBADFD);
    }

    // Read up to 24 bytes + 1 bytes for \n.
    let mut data = [0u8; 25];
    let nread = read_buf(fd, &mut data)?;

    btoi::<u64>(data[..nread].trim_ascii()).or(Err(Errno::EINVAL))
}

/// Retrieves the value of /proc/sys/kernel/randomize_va_space
pub fn proc_kernel_randomize_va_space() -> Result<u8, Errno> {
    let fd = proc_open().and_then(|fd| {
        safe_open(
            fd,
            c"sys/kernel/randomize_va_space",
            OFlag::O_RDONLY | OFlag::O_NOCTTY,
            ResolveFlag::RESOLVE_NO_XDEV,
        )
    })?;

    // Check the file is an empty file or bail out.
    if !is_empty_file(&fd).unwrap_or(false) {
        return Err(Errno::EBADFD);
    }

    // Read up to 1 byte + 1 byte for \n.
    let mut data = [0u8; 2];
    let nread = read_buf(fd, &mut data)?;

    btoi::<u8>(data[..nread].trim_ascii()).or(Err(Errno::EINVAL))
}

/// Reads the tty number from /proc/[pid]/stat and figures out the corresponding /dev/tty device node path.
#[expect(clippy::arithmetic_side_effects)]
#[expect(clippy::cast_sign_loss)]
#[expect(clippy::disallowed_methods)]
pub fn proc_tty(pid: Pid) -> Result<XPathBuf, Errno> {
    let stat = proc_stat(pid)?;
    if stat.tty_nr <= 0 {
        // Process has no controlling terminal.
        return Err(Errno::ENXIO);
    }

    // minor is bits 31-20 and 7-0.
    // major is 15-8.
    let tty_nr = stat.tty_nr;

    // mmmmmmmmmmmm____MMMMMMMMmmmmmmmm
    // 11111111111100000000000000000000
    let major = (tty_nr & 0xfff00) >> 8;
    let minor = (tty_nr & 0x000ff) | ((tty_nr >> 12) & 0xfff00);

    match major {
        3 => Ok(XPathBuf::from(format!(
            "/dev/tty{}{}",
            "pqrstuvwxyzabcde"[(minor >> 4) as usize..]
                .chars()
                .next()
                .unwrap(),
            "0123456789abcdef"[(minor & 0x0f) as usize..]
                .chars()
                .next()
                .unwrap()
        ))),
        4 => {
            if minor < 64 {
                Ok(XPathBuf::from(format!("/dev/tty{minor}")))
            } else {
                Ok(XPathBuf::from(format!("/dev/ttyS{}", minor - 64)))
            }
        }
        11 => Ok(XPathBuf::from(format!("/dev/ttyB{minor}"))),
        14 => Ok(XPathBuf::from(format!("/dev/tty{minor}"))), // Standard TTYs
        17 => Ok(XPathBuf::from(format!("/dev/ttyH{minor}"))),
        19 | 22 | 23 => Ok(XPathBuf::from(format!("/dev/ttyD{minor}"))),
        24 => Ok(XPathBuf::from(format!("/dev/ttyE{minor}"))),
        32 => Ok(XPathBuf::from(format!("/dev/ttyX{minor}"))),
        43 => Ok(XPathBuf::from(format!("/dev/ttyI{minor}"))),
        46 => Ok(XPathBuf::from(format!("/dev/ttyR{minor}"))),
        48 => Ok(XPathBuf::from(format!("/dev/ttyL{minor}"))),
        57 => Ok(XPathBuf::from(format!("/dev/ttyP{minor}"))),
        71 => Ok(XPathBuf::from(format!("/dev/ttyF{minor}"))),
        75 => Ok(XPathBuf::from(format!("/dev/ttyW{minor}"))),
        78 | 112 => Ok(XPathBuf::from(format!("/dev/ttyM{minor}"))),
        105 => Ok(XPathBuf::from(format!("/dev/ttyV{minor}"))),
        136..=143 => Ok(XPathBuf::from(format!(
            "/dev/pts/{}",
            minor + (major - 136) * 256
        ))),
        148 => Ok(XPathBuf::from(format!("/dev/ttyT{minor}"))),
        154 | 156 => Ok(XPathBuf::from(format!(
            "/dev/ttySR{}",
            minor + if major == 156 { 256 } else { 0 }
        ))),
        164 => Ok(XPathBuf::from(format!("/dev/ttyCH{minor}"))),
        166 => Ok(XPathBuf::from(format!("/dev/ttyACM{minor}"))),
        172 => Ok(XPathBuf::from(format!("/dev/ttyMX{minor}"))),
        174 => Ok(XPathBuf::from(format!("/dev/ttySI{minor}"))),
        188 => Ok(XPathBuf::from(format!("/dev/ttyUSB{minor}"))),
        204 => {
            if minor as usize >= LOW_DENSITY_NAMES.len() {
                Err(Errno::ENXIO)
            } else {
                Ok(XPathBuf::from(format!(
                    "/dev/tty{}",
                    LOW_DENSITY_NAMES[minor as usize]
                )))
            }
        }
        208 => Ok(XPathBuf::from(format!("/dev/ttyU{minor}"))),
        216 => Ok(XPathBuf::from(format!("/dev/ttyUB{minor}"))),
        224 => Ok(XPathBuf::from(format!("/dev/ttyY{minor}"))),
        227 => Ok(XPathBuf::from(format!("/dev/3270/tty{minor}"))),
        229 => Ok(XPathBuf::from(format!("/dev/iseries/vtty{minor}"))),
        256 => Ok(XPathBuf::from(format!("/dev/ttyEQ{minor}"))),
        _ => Err(Errno::ENXIO),
    }
}

/// Returns the number of threads for the given process.
pub fn proc_task_nr(pid: Pid) -> Result<u64, Errno> {
    proc_stat(pid).map(|p| p.num_threads)
}

/// Returns the number of threads for the current process.
pub fn proc_task_nr_syd() -> Result<u64, Errno> {
    proc_stat(Pid::this()).map(|p| p.num_threads)
}

/// Returns the number of processes in the system.
pub fn proc_task_nr_sys() -> Result<u64, Errno> {
    Ok(sysinfo()?.process_count().into())
}

/// Checks if the number of tasks across all processes in the system
/// exceeds the given limit by inspecting the `/proc` filesystem.
/// Current process is not included into the limit.
pub fn proc_task_limit(pid: Pid, max: u64) -> Result<bool, Errno> {
    // Count tasks for the given process, return if limit hit.
    let mut count = proc_task_nr(pid)?;
    if count >= max {
        return Ok(true);
    }

    // This function is only called from the main thread.
    // Hence no concurrent readdir() is possible here.
    // Rewind the directory and get to work!
    let fd = PROC_FILE();
    lseek64(fd, 0, Whence::SeekSet)?;

    let this = Pid::this().as_raw();

    // Allocate a large-enough buffer to read in one go.
    let mut tasks = Vec::new();
    tasks.try_reserve(DIRENT_BUF_SIZE).or(Err(Errno::ENOMEM))?;

    // Count processes in the global /proc namespace,
    // return if limit is hit.
    loop {
        let mut entries = match getdents64(&fd, DIRENT_BUF_SIZE) {
            Ok(entries) => entries,
            // ECANCELED is for EOF or empty directory.
            Err(Errno::ECANCELED | Errno::EACCES | Errno::ENOENT | Errno::EPERM | Errno::ESRCH) => {
                break
            }
            Err(errno) => return Err(errno),
        };

        #[expect(clippy::arithmetic_side_effects)]
        for entry in &mut entries {
            // PID paths are directories.
            if !entry.is_dir() {
                continue;
            }

            // Parse PID, continue on errors.
            let task = match btoi::<pid_t>(entry.name_bytes()) {
                Ok(pid) => pid,
                Err(_) => continue,
            };

            // Skip current process and the given process.
            if task == pid.as_raw() || task == this {
                continue;
            }

            // Push task to the list, return if limit is hit.
            tasks.push(task);
            count += 1;
            if count >= max {
                return Ok(true);
            }
        }
    }

    // Count tasks per-process.
    for task in tasks {
        #[expect(clippy::arithmetic_side_effects)]
        match proc_task_nr(Pid::from_raw(task)) {
            Ok(n) => count += n,
            Err(_) => continue, // task died mid-way?
        }
        if count >= max {
            return Ok(true);
        }
    }

    // If we reached here,
    // we did not hit the limit.
    Ok(false)
}

/// Represents an executable file record from proc_pid_maps(5).
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct SydExecMap {
    /// Path to the executable
    pub path: XPathBuf,
    /// Inode of the executable
    pub inode: u64,
    /// Major device ID of the executable
    pub dev_major: i32,
    /// Minor device ID of the executable
    pub dev_minor: i32,
}

/// Collects all unique paths with executable permissions from the
/// memory maps of a process.
///
/// # Returns
///
/// A `Result` containing a vector of `SydExecMap` objects
/// for all unique paths with executable permissions along with their
/// device ID and inode. On error, it returns an `Errno`. If no executable
/// is found in maps, returns `Errno::ENOEXEC`.
pub fn proc_executables(pid: Pid) -> Result<Vec<SydExecMap>, Errno> {
    if *crate::config::HAVE_PROCMAP_QUERY {
        // Use the efficient PROCMAP_QUERY ioctl(2) on Linux>=6.11.
        return procmap_query_executables(pid);
    }

    let maps = proc_maps(pid)?;

    // Create a SydIndexSet to store unique executable paths while
    // preserving insertion order.
    let mut set = SydIndexSet::default();

    // Iterate over each memory map entry.
    for map in maps {
        // Match on the pathname to extract the path.
        if let MMapPath::Path(path) = map.0.pathname {
            // Check if the map has executable permissions.
            if map.0.perms.contains(MMPermissions::EXECUTE) {
                // Insert the path into the SydIndexSet.
                let exe = SydExecMap {
                    path: path.into(),
                    inode: map.0.inode,
                    dev_major: map.0.dev.0,
                    dev_minor: map.0.dev.1,
                };
                set.insert(exe);
            }
        }
    }

    // Check for no match.
    if set.is_empty() {
        return Err(Errno::ENOEXEC);
    }

    // Convert the SydIndexSet to a vector and return.
    Ok(set.into_iter().collect())
}

/// Collects all unique paths with executable permissions from the
/// memory maps of a process, using the PROCMAP_QUERY _ioctl_(2).
///
/// # Returns
///
/// A `Result` containing a vector of `SydExecMap` objects
/// for all unique paths with executable permissions along with their
/// inode and device numbers. On error, returns an `Errno`. If no executable
/// mappings are found, returns `Errno::ENOEXEC`.
pub fn procmap_query_executables(pid: Pid) -> Result<Vec<SydExecMap>, Errno> {
    // Open /proc/$pid/maps.
    let maps = proc_maps_open(pid)?;

    // Only file-backed, executable VMAs, iterating cover/next.
    let flags = ProcmapQueryFlags::COVERING_OR_NEXT_VMA
        | ProcmapQueryFlags::FILE_BACKED_VMA
        | ProcmapQueryFlags::VMA_EXECUTABLE;

    let mut path = [0u8; PATH_MAX];
    let mut addr = 0u64;
    let mut set = SydIndexSet::default();

    loop {
        match procmap_query(&maps, flags, addr, Some(&mut path), None) {
            Ok(q) => {
                let name_len = q.vma_name_size as usize;
                if name_len == 0 {
                    addr = q.vma_end;
                    continue;
                }

                // SAFETY: ioctl filled buf[..name_len] including trailing NUL.
                let path = CStr::from_bytes_with_nul(&path[..name_len])
                    .map(|cstr| cstr.to_bytes())
                    .map(XPathBuf::from)
                    .map_err(|_| Errno::EINVAL)?;
                #[expect(clippy::cast_possible_wrap)]
                let exe = SydExecMap {
                    path,
                    inode: q.inode,
                    dev_major: q.dev_major as i32,
                    dev_minor: q.dev_minor as i32,
                };
                set.insert(exe);

                addr = q.vma_end;
            }
            Err(Errno::ENOENT) => break,
            Err(errno) => return Err(errno),
        }
    }

    // Check for no match.
    if set.is_empty() {
        return Err(Errno::ENOEXEC);
    }

    // Convert the SydIndexSet to a vector and return.
    Ok(set.into_iter().collect())
}

/// This data structure represents a Virtual Memory Area.
pub struct Vma {
    // VMA address
    ptr: NonNull<libc::c_void>,

    // VMA length
    len: NonZeroUsize,

    // VMA flags
    flags: ProcmapQueryFlags,

    // VMA name
    name: [u8; PATH_MAX],
}

impl Vma {
    fn new(
        ptr: NonNull<libc::c_void>,
        len: NonZeroUsize,
        flags: ProcmapQueryFlags,
        name: [u8; PATH_MAX],
    ) -> Self {
        Self {
            ptr,
            len,
            flags,
            name,
        }
    }

    /// Return VMA address.
    pub fn addr(&self) -> usize {
        self.ptr.as_ptr() as usize
    }

    /// Return VMA pointer.
    pub fn as_ptr(&self) -> NonNull<libc::c_void> {
        self.ptr
    }

    /// Return VMA length.
    pub fn len(&self) -> NonZeroUsize {
        self.len
    }

    /// Return VMA flags.
    pub fn flags(&self) -> ProcmapQueryFlags {
        self.flags
    }

    /// Return VMA name as `XPath`.
    pub fn name(&self) -> &XPath {
        XPath::from_bytes(self.name_bytes())
    }

    /// Return VMA name as bytes.
    pub fn name_bytes(&self) -> &[u8] {
        let len = memchr(0, &self.name).unwrap_or(PATH_MAX);
        &self.name[..len]
    }
}

impl TryFrom<SydMemoryMap> for Vma {
    type Error = Errno;

    fn try_from(map: SydMemoryMap) -> Result<Self, Self::Error> {
        let map = map.0;
        let (start, end) = (map.address.0, map.address.1);
        let ptr = NonNull::new(start as *mut _).ok_or(Errno::EINVAL)?;
        let len = end
            .checked_sub(start)
            .ok_or(Errno::EINVAL)
            .map(usize::try_from)?
            .or(Err(Errno::EINVAL))
            .map(NonZeroUsize::new)?
            .ok_or(Errno::EINVAL)?;

        let mut name = [0u8; PATH_MAX];
        match &map.pathname {
            MMapPath::Path(path) => {
                let bytes = path.as_os_str().as_bytes();
                let len = bytes.len().min(PATH_MAX);
                name[..len].copy_from_slice(&bytes[..len]);
            }
            MMapPath::Heap => {
                name[..7].copy_from_slice(b"[heap]\0");
            }
            MMapPath::Stack => {
                name[..8].copy_from_slice(b"[stack]\0");
            }
            MMapPath::TStack(tid) => {
                use std::io::Write;
                let _ = write!(&mut name[..], "[stack:{tid}]\0");
            }
            MMapPath::Vdso => {
                name[..7].copy_from_slice(b"[vdso]\0");
            }
            MMapPath::Vvar => {
                name[..7].copy_from_slice(b"[vvar]\0");
            }
            MMapPath::Vsyscall => {
                name[..11].copy_from_slice(b"[vsyscall]\0");
            }
            MMapPath::Rollup | MMapPath::Vsys(_) | MMapPath::Other(_) | MMapPath::Anonymous => {}
        }

        Ok(Self::new(ptr, len, map.perms.into(), name))
    }
}

impl TryFrom<(ProcmapQuery, [u8; PATH_MAX])> for Vma {
    type Error = Errno;

    fn try_from((q, name): (ProcmapQuery, [u8; PATH_MAX])) -> Result<Self, Self::Error> {
        let ptr = NonNull::new(q.vma_start as *mut _).ok_or(Errno::EINVAL)?;
        let len = q
            .vma_end
            .checked_sub(q.vma_start)
            .ok_or(Errno::EINVAL)
            .map(usize::try_from)?
            .or(Err(Errno::EINVAL))
            .map(NonZeroUsize::new)?
            .ok_or(Errno::EINVAL)?;
        let flags = ProcmapQueryFlags::from_bits_truncate(q.vma_flags);
        Ok(Self::new(ptr, len, flags, name))
    }
}

impl fmt::Display for Vma {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let start = self.addr();
        let end = start.saturating_add(self.len().get());
        let flags = self.flags();
        let name = self.name();
        write!(f, "{start:x}-{end:x} {flags} {name}")
    }
}

impl Serialize for Vma {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let start = self.addr();
        let end = start.saturating_add(self.len().get());
        let mut map = serializer.serialize_map(Some(3))?;
        map.serialize_entry("addr", &[start, end])?;
        map.serialize_entry("perm", &self.flags())?;
        map.serialize_entry("name", &self.name())?;
        map.end()
    }
}

/// Get the VMA covering `addr` in process `pid`.
///
/// Tries the ioctl(2)-based fast path on Linux>=6.11,
/// otherwise falls back to parsing _proc_pid_maps_(5).
///
/// On success returns `Vma`.
/// If no VMA covers `addr`, returns `Err(Errno::ENOENT)`.
pub fn proc_get_vma(pid: Pid, addr: u64) -> Result<Vma, Errno> {
    if *HAVE_PROCMAP_QUERY {
        // Use the efficient PROCMAP_QUERY ioctl(2) on Linux>=6.11.
        return procmap_query_get_vma(pid, addr);
    }

    // Fallback: Sequentially scan /proc/$pid/maps.
    let maps = proc_maps(pid)?;

    for map in maps {
        let (start, end) = (map.0.address.0, map.0.address.1);
        if (start..end).contains(&addr) {
            return Vma::try_from(map);
        }
    }

    // Nothing found.
    Err(Errno::ENOENT)
}

fn procmap_query_get_vma(pid: Pid, addr: u64) -> Result<Vma, Errno> {
    // Open /proc/$pid/maps.
    let maps = proc_maps_open(pid)?;

    let flags = ProcmapQueryFlags::COVERING_OR_NEXT_VMA;

    // Call PROCMAP_QUERY ioctl(2).
    //
    // No name or build-id buffers needed.
    let mut name = [0u8; PATH_MAX];
    let q = procmap_query(&maps, flags, addr, Some(&mut name), None)?;

    Vma::try_from((q, name))
}

/// Get all VMAs matching `flags` in process `pid`.
///
/// Tries the ioctl(2)-based fast path on Linux>=6.11,
/// otherwise falls back to parsing _proc_pid_maps_(5).
///
/// On success returns `Vec<Vma>`.
pub fn proc_find_vma(pid: Pid, flags: ProcmapQueryFlags) -> Result<Vec<Vma>, Errno> {
    if *HAVE_PROCMAP_QUERY {
        // Use the efficient PROCMAP_QUERY ioctl(2) on Linux>=6.11.
        return procmap_query_find_vma(pid, flags);
    }

    // Fallback: Sequentially scan /proc/$pid/maps.
    let maps = proc_maps(pid)?;
    let mut vmas = Vec::new();

    for map in maps {
        let perms = ProcmapQueryFlags::from(map.0.perms);

        if flags.contains(ProcmapQueryFlags::VMA_READABLE)
            && !perms.contains(ProcmapQueryFlags::VMA_READABLE)
        {
            continue;
        }

        if flags.contains(ProcmapQueryFlags::VMA_WRITABLE)
            && !perms.contains(ProcmapQueryFlags::VMA_WRITABLE)
        {
            continue;
        }

        if flags.contains(ProcmapQueryFlags::VMA_EXECUTABLE)
            && !perms.contains(ProcmapQueryFlags::VMA_EXECUTABLE)
        {
            continue;
        }

        if flags.contains(ProcmapQueryFlags::VMA_SHARED)
            && !perms.contains(ProcmapQueryFlags::VMA_SHARED)
        {
            continue;
        }

        if flags.contains(ProcmapQueryFlags::FILE_BACKED_VMA)
            && !matches!(map.0.pathname, MMapPath::Path(_))
        {
            continue;
        }

        vmas.push(Vma::try_from(map)?);
    }

    Ok(vmas)
}

/// Get all VMAs matching `flags` in process `pid` using PROCMAP_QUERY ioctl(2).
pub fn procmap_query_find_vma(pid: Pid, flags: ProcmapQueryFlags) -> Result<Vec<Vma>, Errno> {
    // Open /proc/$pid/maps.
    let maps = proc_maps_open(pid)?;

    let query_flags = flags | ProcmapQueryFlags::COVERING_OR_NEXT_VMA;

    let mut vmas = Vec::new();
    let mut addr = 0u64;

    loop {
        // Call PROCMAP_QUERY ioctl(2).
        //
        // No name or build-id buffers needed.
        let mut name = [0u8; PATH_MAX];
        match procmap_query(&maps, query_flags, addr, Some(&mut name), None) {
            Ok(q) => {
                vmas.push(Vma::try_from((q, name))?);
                addr = q.vma_end;
            }
            Err(Errno::ENOENT) => break,
            Err(errno) => return Err(errno),
        }
    }

    Ok(vmas)
}

/// Parses /proc/pid/environ and returns a HashMap.
pub fn proc_environ(pid: Pid) -> Result<SydHashMap<OsString, OsString>, Errno> {
    proc_environ_read(proc_environ_open(pid)?)
}

/// Open a FD to _proc_pid_environ_(5) and return an OwnedFd.
pub fn proc_environ_open(pid: Pid) -> Result<OwnedFd, Errno> {
    let mut path = XPathBuf::from_pid(pid)?;
    path.try_reserve(b"/environ".len()).or(Err(Errno::ENOMEM))?;
    path.push(b"environ");

    safe_open_proc(&path).map_err(proc_errno)
}

/// Parses /proc/pid/environ from given Fd and returns a HashMap.
pub fn proc_environ_read(fd: OwnedFd) -> Result<SydHashMap<OsString, OsString>, Errno> {
    let buf = read_all(fd)?;
    let mut map = SydHashMap::default();
    for slice in buf.split(|b| *b == 0) {
        // slice will be in the form key=var, so split on the first equals sign.
        let mut split = slice.splitn(2, |b| *b == b'=');
        if let (Some(k), Some(v)) = (split.next(), split.next()) {
            map.insert(
                OsStr::from_bytes(k).to_os_string(),
                OsStr::from_bytes(v).to_os_string(),
            );
        };
    }

    Ok(map)
}

/// Describes namespaces to which the process with the corresponding PID belongs.
///
/// The namespace type is the key for the HashMap, i.e 'net', 'user', etc.
pub fn proc_namespaces(pid: Pid) -> Result<Namespaces, Errno> {
    // Open /proc/<pid>/ns as a directory fd.
    //
    // Note, we do not use PROC_FILE() here,
    // because currently the only consumer of
    // this function is syd-run where preopening
    // /proc is needless.
    let mut ns = XPathBuf::from("/proc");
    ns.push_pid(pid);
    ns.push(b"ns");
    #[expect(clippy::disallowed_methods)]
    let dirfd = nix::fcntl::openat(
        AT_BADFD,
        &ns,
        OFLAG_PROC | OFlag::O_DIRECTORY,
        Mode::empty(),
    )
    .map_err(proc_errno)?;

    // Read directory entries using getdents64 in a loop.
    #[expect(clippy::disallowed_types)]
    let mut namespaces = std::collections::HashMap::new();
    let mut seen_dot = false;
    let mut seen_dotdot = false;
    loop {
        let mut entries = match getdents64(&dirfd, DIRENT_BUF_SIZE) {
            Ok(iter) => iter,
            Err(Errno::ECANCELED) => break, // EOF or empty directory
            Err(errno) => return Err(errno),
        };

        for entry in &mut entries {
            if !seen_dot && entry.is_dot() {
                seen_dot = true;
                continue;
            }
            if !seen_dotdot && entry.is_dotdot() {
                seen_dotdot = true;
                continue;
            }

            let (ino, dev) = statx(&dirfd, entry.as_xpath(), 0, STATX_INO).map(|stx| {
                let ino = stx.stx_ino;
                let dev = (u64::from(stx.stx_dev_major) << 32) | u64::from(stx.stx_dev_minor);
                (ino, dev)
            })?;

            let ns_type = OsStr::from_bytes(entry.name_bytes()).to_os_string();
            let mut ns_path = XPathBuf::from("/proc");
            ns_path.push_pid(pid);
            ns_path.push(b"ns");
            ns_path.push(entry.name_bytes());

            let ns = Namespace {
                ns_type: ns_type.clone(),
                path: ns_path.to_path_buf(),
                identifier: ino,
                device_id: dev,
            };

            if namespaces.insert(ns_type, ns).is_some() {
                return Err(Errno::EEXIST);
            }
        }
    }

    Ok(Namespaces(namespaces))
}

/// Parses /proc/crypto and returns the table of crypto algorithms.
pub fn proc_crypto() -> Result<CryptoTable, Errno> {
    proc_crypto_read(proc_crypto_open()?)
}

/// Open a FD to _proc_crypto_(5) and return an OwnedFd.
pub fn proc_crypto_open() -> Result<OwnedFd, Errno> {
    safe_open_proc("crypto")
}

/// Parses /proc/crypto from given Fd and returns the table of crypto algorithms.
pub fn proc_crypto_read(fd: OwnedFd) -> Result<CryptoTable, Errno> {
    CryptoTable::from_buf_read(BufReader::new(File::from(fd))).or(Err(Errno::EPERM))
}

/// Parses /proc/pid/net/unix, return inodes.
///
/// Returns the inodes of all UNIX domain sockets.
pub fn proc_unix_inodes(pid: Pid) -> Result<SydHashSet<u64>, Errno> {
    let mut path = XPathBuf::from_pid(pid)?;
    path.try_reserve(b"/net/unix".len())
        .or(Err(Errno::ENOMEM))?;
    path.push(b"net");
    path.push(b"unix");

    let file = safe_open_proc(&path).map(File::from).map(BufReader::new)?;
    Ok(UnixNetEntries::from_buf_read(file)
        .map(|e| e.0)
        .or(Err(Errno::EPERM))?
        .into_iter()
        .filter(|entry| {
            // The prefix '@' is for abstract sockets.
            entry
                .path
                .as_ref()
                .map(|p| p.as_os_str().as_bytes().first() != Some(&b'@'))
                .unwrap_or(false)
        })
        .map(|entry| entry.inode)
        .collect())
}

/// Sets the AT_SECURE value to 1 in the auxiliary vector of the
/// specified process.
///
/// This function locates the auxiliary vector in the target process's
/// memory and sets the AT_SECURE entry to 1. It uses the
/// `/proc/pid/stat` file to get the address of the start of the stack
/// and parses the stack according to the standard Linux process stack
/// layout.
///
/// # Arguments
///
/// * `pid` - The PID of the target process.
/// * `elf_type` - The ELF type (Elf32 or Elf64) of the target process.
/// * `deny_vdso` - True to deny access to vDSO by overwriting AT_SYSINFO{,_EHDR}.
///
/// # Returns
///
/// Returns `Ok(())` on success, or an `Err(Errno)` if an error occurs.
pub fn proc_set_at_secure(pid: Pid, elf_type: ElfType, deny_vdso: bool) -> Result<(), Errno> {
    // This function requires CONFIG_CROSS_MEMORY_ATTACH,
    // unless Syd is explicitly configured to fallback.
    let mut proc_mem = if Sandbox::use_proc_pid_mem() {
        let mut path = XPathBuf::from_pid(pid)?;
        path.try_reserve(b"/mem".len()).or(Err(Errno::ENOMEM))?;
        path.push(b"mem");

        let mut flags = OFLAG_PROC;
        flags.remove(OFlag::O_RDONLY);
        flags.insert(OFlag::O_RDWR);
        Some(
            safe_open(PROC_FILE(), &path, flags, RFLAG_PROC)
                .map(File::from)
                .map_err(proc_errno)?,
        )
    } else {
        None
    };

    fn proc_read(
        proc_mem: Option<&mut File>,
        pid: Pid,
        buf: &mut [u8],
        base_addr: usize,
    ) -> Result<usize, Errno> {
        match proc_mem {
            Some(file) => {
                file.seek(SeekFrom::Start(base_addr as u64))
                    .map_err(|err| err2no(&err))?;
                read_buf(file, buf)
            }
            None => {
                let local_len = buf.len();
                let mut local_iov = [IoSliceMut::new(buf)];
                let remote_iov = [RemoteIoVec {
                    base: base_addr,
                    len: local_len,
                }];
                process_vm_readv(pid, &mut local_iov, &remote_iov)
            }
        }
    }

    fn proc_write(
        proc_mem: Option<&mut File>,
        pid: Pid,
        buf: &[u8],
        base_addr: usize,
    ) -> Result<usize, Errno> {
        match proc_mem {
            Some(file) => {
                file.seek(SeekFrom::Start(base_addr as u64))
                    .map_err(|err| err2no(&err))?;

                let mut nwrite = 0;
                while nwrite < buf.len() {
                    match retry_on_eintr(|| write(&file, &buf[nwrite..]))? {
                        0 => break,
                        n => nwrite = nwrite.checked_add(n).ok_or(Errno::EOVERFLOW)?,
                    }
                }

                Ok(nwrite)
            }
            None => {
                let local_len = buf.len();
                let local_iov = [IoSlice::new(buf)];
                let remote_iov = [RemoteIoVec {
                    base: base_addr,
                    len: local_len,
                }];
                process_vm_writev(pid, &local_iov, &remote_iov)
            }
        }
    }

    // Get the address of the start of the stack for the process.
    let sp = proc_stack_start(pid)?;
    let sp = usize::try_from(sp).or(Err(Errno::EFAULT))?;

    let (sizeof_ptr, sizeof_ptr2) = match elf_type {
        ElfType::Elf32 => (4, 8),
        ElfType::Elf64 => (8, 16),
    };
    let mut offset = 0usize;

    // Read argc (the argument count).
    let mut buf = [0u8; 8]; // Max size needed for usize.
    let bytes_read = proc_read(proc_mem.as_mut(), pid, &mut buf[..sizeof_ptr], sp)?;
    if bytes_read != sizeof_ptr {
        return Err(Errno::EIO);
    }

    // SAFETY: Ensure argc is not zero.
    // The Linux kernel has a mitigation for this since version 5.18.
    // Syd is recommended to run on Linux-5.19 or later but technically
    // it is possible to run it on Linux-5.0 or later so we include the
    // check here for defense in depth.
    // See:
    // https://github.com/torvalds/linux/blob/88d324e69ea9f3ae1c1905ea75d717c08bdb8e15/fs/exec.c#L1962
    // https://lwn.net/Articles/882799/
    // https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034
    let argc = read_usize_from_ne_bytes(&buf[..sizeof_ptr], sizeof_ptr)?;
    if argc == 0 {
        return Err(Errno::EINVAL);
    }
    offset = offset.checked_add(sizeof_ptr).ok_or(Errno::EINVAL)?;

    // Skip over argv pointers (argc pointers plus a NULL terminator).
    let argv_size = argc
        .checked_add(1)
        .ok_or(Errno::EINVAL)?
        .checked_mul(sizeof_ptr)
        .ok_or(Errno::EINVAL)?;
    offset = offset.checked_add(argv_size).ok_or(Errno::EINVAL)?;

    // Skip over envp pointers until NULL terminator is found.
    loop {
        // Read one pointer at a time.
        let mut envp_buf = [0u8; 8];
        let bytes_read = proc_read(
            proc_mem.as_mut(),
            pid,
            &mut envp_buf[..sizeof_ptr],
            sp.checked_add(offset).ok_or(Errno::EINVAL)?,
        )?;
        if bytes_read != sizeof_ptr {
            return Err(Errno::EIO);
        }
        let envp_ptr = read_usize_from_ne_bytes(&envp_buf[..sizeof_ptr], sizeof_ptr)?;
        offset = offset.checked_add(sizeof_ptr).ok_or(Errno::EINVAL)?;
        if envp_ptr == 0 {
            break;
        }
    }

    // Read the auxiliary vector into a buffer starting from the current
    // offset. Technically ~256 bytes should be enough to locate
    // AT_SECURE because an auxv entry is at most 16 bytes (8 bytes on
    // 32-bit), and AT_SECURE is roughly the 16th on the list but we
    // leave a bit more room for safety and future-compat. Check
    // fs/exec.c in kernel sources for more information.
    const READ_SIZE: usize = 512; // Read up to 512 bytes.
    let mut buf = [0u8; READ_SIZE];
    let sp = sp.checked_add(offset).ok_or(Errno::EINVAL)?;
    let bytes_read = proc_read(proc_mem.as_mut(), pid, &mut buf, sp)?;
    if bytes_read == 0 {
        return Err(Errno::EIO);
    }

    // Now parse the auxiliary vector.
    // For added validation, we ensure we have the sequence of keys:
    // AT_UID -> AT_EUID -> AT_GID -> AT_EGID -> AT_SECURE
    // We also ensure no other key is ever present in this sequence.
    // The AT_NULL key indicates the end of the vector.
    #[expect(clippy::cast_possible_truncation)]
    const AT_NULL: usize = libc::AT_NULL as usize;
    #[expect(clippy::cast_possible_truncation)]
    const AT_UID: usize = libc::AT_UID as usize;
    #[expect(clippy::cast_possible_truncation)]
    const AT_EUID: usize = libc::AT_EUID as usize;
    #[expect(clippy::cast_possible_truncation)]
    const AT_GID: usize = libc::AT_GID as usize;
    #[expect(clippy::cast_possible_truncation)]
    const AT_EGID: usize = libc::AT_EGID as usize;
    #[expect(clippy::cast_possible_truncation)]
    const AT_SECURE: usize = libc::AT_SECURE as usize;
    const AT_REQKEY: &[usize] = &[AT_UID, AT_EUID, AT_GID, AT_EGID, AT_SECURE];
    let mut required_index = 0;

    // Used to deny access to vDSO as necessary.
    //
    // 1. libc crate does not define these for all arches we support,
    //    however the values are consistent across all.
    // 2. AT_SECURE comes after AT_SYSINFO{,_EHDR} in the auxiliary vector.
    const AT_SYSINFO: usize = 32;
    const AT_SYSINFO_EHDR: usize = 33;

    // Store the UID/GID values for comparison.
    // We're going to check these values against ours
    // and fail as necessary.
    let mut at_uid_val = None;
    let mut at_euid_val = None;
    let mut at_gid_val = None;
    let mut at_egid_val = None;

    offset = 0;
    loop {
        // Determine key and value location, and check it's within bounds.
        let key_end = offset.checked_add(sizeof_ptr).ok_or(Errno::EINVAL)?;
        let val_end = key_end.checked_add(sizeof_ptr).ok_or(Errno::EINVAL)?;
        if val_end > bytes_read {
            break;
        }

        // Read key and value from the buffer.
        let key = read_usize_from_ne_bytes(&buf[offset..key_end], sizeof_ptr)?;
        let val = read_usize_from_ne_bytes(&buf[key_end..val_end], sizeof_ptr)?;

        if key == AT_REQKEY[required_index] {
            // Key matches the expected key in sequence.
            match key {
                AT_UID => at_uid_val = Some(val),
                AT_EUID => at_euid_val = Some(val),
                AT_GID => at_gid_val = Some(val),
                AT_EGID => at_egid_val = Some(val),
                _ => {}
            }

            required_index = required_index.checked_add(1).ok_or(Errno::EINVAL)?;
            if required_index >= AT_REQKEY.len() {
                // We've found the sequence ending with AT_SECURE!

                // Verify that the UID/GID values match ours.
                let uid = Uid::current().as_raw() as usize;
                let euid = Uid::effective().as_raw() as usize;
                let gid = Gid::current().as_raw() as usize;
                let egid = Gid::effective().as_raw() as usize;

                if at_uid_val != Some(uid)
                    || at_euid_val != Some(euid)
                    || at_gid_val != Some(gid)
                    || at_egid_val != Some(egid)
                {
                    // SAFETY:
                    // 1. Change return success.
                    // 2. Going and coming without error.
                    // 3. Action brings good fortune.
                    return Err(Errno::EACCES);
                }

                // Only modify AT_SECURE, if it's not already set.
                // SAFETY: We do this check only after UID/GID verification.
                if val != 0 {
                    return Ok(());
                }

                // Overwrite the value in the local buffer.
                let val = usize_to_ne_bytes(1, sizeof_ptr);
                buf[key_end..val_end].copy_from_slice(&val);

                // SAFETY: We have verified that the auxiliary vector
                // contains the expected keys in the correct order, and
                // that the UID/GID values match our own. This ensures
                // that we are modifying a trusted process. We also
                // ensure that the offsets are within the bounds of the
                // read buffer, preventing overflows.
                //
                // Write the modified value back to the target process.
                if proc_write(
                    proc_mem.as_mut(),
                    pid,
                    &buf[key_end..val_end],
                    sp.checked_add(key_end).ok_or(Errno::EINVAL)?,
                )? != sizeof_ptr
                {
                    return Err(Errno::EIO);
                }

                // All done, return success.
                return Ok(());
            }
        } else if required_index > 0 {
            // An unexpected key appeared; validation fails!
            return Err(Errno::EACCES);
        } else if deny_vdso && (key == AT_SYSINFO || key == AT_SYSINFO_EHDR) {
            // Deny access to vDSO as requested.
            // AT_SYS_INFO{,_EHDR} always comes before AT_SECURE.
            //
            // Overwrite the key and value in the local buffer:
            // Overwrite the key with AT_SECURE.
            // Overwrite the value with 1.
            let key = usize_to_ne_bytes(AT_SECURE, sizeof_ptr);
            let val = usize_to_ne_bytes(1, sizeof_ptr);
            buf[offset..key_end].copy_from_slice(&key);
            buf[key_end..val_end].copy_from_slice(&val);

            // Write the modified value back to the target process.
            if proc_write(
                proc_mem.as_mut(),
                pid,
                &buf[offset..val_end],
                sp.checked_add(offset).ok_or(Errno::EINVAL)?,
            )? != sizeof_ptr2
            {
                return Err(Errno::EIO);
            }
        } else if key == AT_NULL {
            break;
        }

        // SAFETY: We check for arithmetic overflow when advancing the
        // offset to prevent wrapping around.
        offset = offset.checked_add(sizeof_ptr2).ok_or(Errno::EINVAL)?;
    }

    Err(Errno::ENOENT)
}

/// List `/proc/pid/fd` contents.
///
/// Return a vector of `(RawFd, XPathBuf)` tuples, where each `RawFd`
/// is the file descriptor number and the `XPathBuf` is the path it points to.
///
/// Useful for debugging file descriptor leaks.
#[expect(clippy::type_complexity)]
pub fn proc_pid_fd(pid: Option<Pid>) -> Result<Vec<(RawFd, XPathBuf)>, Errno> {
    let pid = pid.unwrap_or_else(getpid);

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

    let dir = safe_open(
        PROC_FILE(),
        &dir,
        OFLAG_PROC | OFlag::O_DIRECTORY,
        RFLAG_PROC,
    )?;

    let mut res = vec![];
    let mut seen_dot = false;
    let mut seen_dotdot = false;
    loop {
        let mut entries = match getdents64(&dir, DIRENT_BUF_SIZE) {
            Ok(entries) => entries,
            Err(Errno::ECANCELED) => break, // EOF or empty directory
            Err(errno) => return Err(errno),
        };

        for entry in &mut entries {
            if !seen_dot && entry.is_dot() {
                seen_dot = true;
                continue;
            }
            if !seen_dotdot && entry.is_dotdot() {
                seen_dotdot = true;
                continue;
            }
            let entry = XPath::from_bytes(entry.name_bytes());
            let fd = parse_fd(entry)?;
            let target = readlinkat(&dir, entry)?;

            res.push((fd, target));
        }
    }

    Ok(res)
}

/// Return the max open files soft limit for the given process.
pub fn proc_max_open_files(pid: Pid) -> Result<LimitValue, Errno> {
    let mut path = XPathBuf::from_pid(pid)?;
    path.try_reserve(b"/limits".len()).or(Err(Errno::ENOMEM))?;
    path.push(b"limits");

    // Safely open proc(5) file.
    let file = safe_open_proc(&path)?;

    let mut buf = [0; 1792]; // A typical limits file is about 1500 bytes.
    map_result(parse_max_open_files(read_to_end(file, &mut buf)?))
}

/// Pick a random fd number and check via KCMP_FILE whether it's open in `pid`.
/// On success returns the fd; if none are found after probing the full range,
/// returns `Err(Errno::EBADF)`. Other errors (permissions, invalid pid, etc.)
/// are returned immediately.
pub fn proc_rand_fd(pid: Pid) -> Result<RawFd, Errno> {
    // SAFETY: Cap to a sane maximum because sufficiently
    // big values of the hard limit tend to return ENOMEM.
    const OFILE_MAX: u64 = 0x10000;
    let range_start = 7u64;
    let range_end = match proc_max_open_files(pid)? {
        LimitValue::Unlimited => OFILE_MAX,
        LimitValue::Value(val) => val.saturating_sub(1).min(OFILE_MAX),
    };

    if range_end <= range_start {
        return Err(Errno::EMFILE);
    }
    let range = range_start..=range_end;

    // SAFETY: To make this file descriptor harder to spot by an
    // attacker we duplicate it to a random fd number.
    for _ in range.clone() {
        #[expect(clippy::cast_possible_truncation)]
        let fd_rand = randint(range.clone())? as RawFd;

        // Check if the slot is free.
        // This is subject to race, and optimally should
        // be implemented in the host kernel.
        return match is_open_fd(pid, fd_rand) {
            Ok(true) => continue,
            Ok(false) => Ok(fd_rand),
            Err(errno) => Err(errno),
        };
    }

    Err(Errno::EBADF)
}

/// Log `/proc/self/fd` contents.
///
/// Useful for debugging file descriptor leaks.
pub fn log_proc_pid_fd(pid: Option<Pid>) -> Result<(), Errno> {
    let fds = proc_pid_fd(pid)?;
    let pid = pid.unwrap_or_else(getpid);

    crate::error!("ctx": "log_proc_self_fd",
        "msg": format!("List of /proc/{pid}/fd"),
        "fds": fds, "pid": pid.as_raw());

    Ok(())
}

/// Kernel taint strings.
///
/// See: https://docs.kernel.org/admin-guide/tainted-kernels.html#table-for-decoding-tainted-state
pub static KERNEL_TAINT_STRINGS: [&str; 20] = [
    "P (Proprietary module was loaded)",
    "F (Module was force loaded)",
    "S (Kernel running on an out of specification system)",
    "R (Module was force unloaded)",
    "M (Processor reported a Machine Check Exception)",
    "B (Bad page referenced or unexpected page flags)",
    "U (Taint requested by userspace application)",
    "D (Kernel died recently: OOPS/BUG)",
    "A (ACPI table overridden by user)",
    "W (Kernel issued warning)",
    "C (Staging driver was loaded)",
    "I (Workaround for bug in platform firmware applied)",
    "O (Externally-built \"out-of-tree\" module was loaded)",
    "E (Unsigned module was loaded)",
    "L (Soft lockup occurred)",
    "K (Kernel has been live patched)",
    "X (Auxiliary taint, used by distros)",
    "T (Kernel built with struct randomization plugin)",
    "N (An in-kernel test has been run)",
    "J (Userspace used a mutating debug operation in fwctl)",
];

bitflags! {
    /// Bitflags for `/proc/sys/kernel/tainted`.
    #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
    pub struct KernelTaintFlags: u64 {
        /// Proprietary module was loaded
        const PROPRIETARY_MODULE = 1 << 0; // P
        /// Module was force loaded
        const MODULE_FORCE_LOADED = 1 << 1; // F
        /// Kernel running on an out of specification system
        const OUT_OF_SPEC_SYSTEM = 1 << 2; // S
        /// Module was force unloaded
        const MODULE_FORCE_UNLOADED = 1 << 3; // R
        /// Processor reported a Machine Check Exception
        const MACHINE_CHECK_EXCEPTION = 1 << 4; // M
        /// Bad page referenced or unexpected page flags
        const BAD_PAGE_REFERENCE = 1 << 5; // B
        /// Taint requested by userspace application
        const USER_REQUEST = 1 << 6; // U
        /// Kernel died recently: OOPS/BUG
        const OOPS_OR_BUG = 1 << 7; // D
        /// ACPI table overridden by user
        const ACPI_TABLE_OVERRIDDEN = 1 << 8; // A
        /// Kernel issued warning
        const WARNING_ISSUED = 1 << 9; // W
        /// Staging driver was loaded
        const STAGING_DRIVER_LOADED = 1 << 10; // C
        /// Workaround for bug in platform firmware applied
        const WORKAROUND_FW_BUG = 1 << 11; // I
        /// Externally-built "out-of-tree" module was loaded
        const OUT_OF_TREE_MODULE = 1 << 12; // O
        /// Unsigned module was loaded
        const UNSIGNED_MODULE_LOADED = 1 << 13; // E
        /// Soft lockup occurred
        const SOFT_LOCKUP_OCCURRED = 1 << 14; // L
        /// Kernel has been live patched
        const LIVE_PATCHED = 1 << 15; // K
        /// Auxiliary taint, used by distros
        const AUXILIARY = 1 << 16; // X
        /// Kernel built with struct randomization plugin
        const BUILT_WITH_RANDOMIZATION = 1 << 17; // T
        /// An in-kernel test has been run
        const IN_KERNEL_TEST_RUN = 1 << 18; // N
        /// Userspace used a mutating debug operation in fwctl
        const FWCTL_DEBUG_WRITE_USED = 1 << 19; // J
    }
}

impl KernelTaintFlags {
    /// Returns taint reasons (verbatim) for set bits 0..=19.
    pub fn reasons(self) -> impl Iterator<Item = &'static str> + 'static {
        (0..KERNEL_TAINT_STRINGS.len())
            .filter(move |bit| self.bits() & (1u64 << bit) != 0)
            .map(|bit| KERNEL_TAINT_STRINGS[bit])
    }

    /// True if *no* bits are set (bitfield == 0).
    pub fn is_clean(self) -> bool {
        self.is_empty()
    }
}

impl fmt::Display for KernelTaintFlags {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.is_clean() {
            write!(f, "Kernel is not tainted (0).")
        } else {
            write!(
                f,
                "Kernel is tainted: {}.",
                self.reasons().collect::<Vec<_>>().join(", ")
            )
        }
    }
}

/// Retrieves the value of /proc/sys/kernel/tainted as KernelTaintFlags.
pub fn proc_kernel_taint() -> Result<KernelTaintFlags, Errno> {
    let fd = proc_open().and_then(|fd| {
        safe_open(
            fd,
            c"sys/kernel/tainted",
            OFlag::O_RDONLY | OFlag::O_NOCTTY,
            ResolveFlag::RESOLVE_NO_XDEV,
        )
    })?;

    // Check the file is an empty file or bail out.
    if !is_empty_file(&fd).unwrap_or(false) {
        return Err(Errno::EBADFD);
    }

    // Read up to 24 bytes + 1 byte for \n.
    let mut data = [0u8; 25];
    let nread = read_buf(fd, &mut data)?;

    // Parse decimal -> u64, then map to flags (retain unknown higher bits).
    let val = btoi::<u64>(data[..nread].trim_ascii()).or(Err(Errno::EINVAL))?;
    Ok(KernelTaintFlags::from_bits_retain(val))
}

/// Set max PID limit for PID namespace.
pub fn proc_pid_max<Fd: AsFd>(fd_proc: Fd, pid_max: u64) -> Result<(), Errno> {
    let mut buf = itoa::Buffer::new();
    let pid_max = buf.format(pid_max).as_bytes();
    proc_write(&fd_proc, b"sys/kernel/pid/max", pid_max, false /*pid*/)
}

/// Set up user mappings for user namespace.
pub fn proc_map_user<Fd: AsFd>(
    fd_proc: Fd,
    uid: Uid,
    gid: Gid,
    map_root: bool,
) -> Result<(), Errno> {
    let uid_map = {
        let mut buf = Vec::new();
        write!(
            &mut buf,
            "{} {uid} 1",
            if map_root { 0 } else { uid.as_raw() }
        )
        .map_err(|err| err2no(&err))?;
        buf
    };
    let gid_map = {
        let mut buf = Vec::new();
        write!(
            &mut buf,
            "{} {gid} 1",
            if map_root { 0 } else { gid.as_raw() }
        )
        .map_err(|err| err2no(&err))?;
        buf
    };

    // Write "deny" to /proc/self/setgroups before writing to gid_map.
    proc_write(&fd_proc, b"setgroups", b"deny", true /*pid*/)?;

    // Write GID mapping to gid_map.
    proc_write(&fd_proc, b"gid_map", &gid_map, true /*pid*/)?;

    // Write UID mapping to uid map.
    proc_write(&fd_proc, b"uid_map", &uid_map, true /*pid*/)?;

    Ok(())
}

/// Set up time offsets for time namespace.
pub fn proc_set_time<Fd: AsFd>(fd_proc: Fd, boffset: i64, moffset: i64) -> Result<(), Errno> {
    // <clock-id> <offset-secs> <offset-nanosecs>
    let mut buf = itoa::Buffer::new();
    let mut data = "monotonic ".to_string();
    data.push_str(buf.format(moffset));
    data.push_str(" 0\nboottime ");
    data.push_str(buf.format(boffset));
    data.push_str(" 0\n");

    proc_write(
        fd_proc,
        b"timens_offsets",
        data.as_bytes(),
        true, /*pid*/
    )
}

// Write the given $data to /proc/$pid/$name.
fn proc_write<Fd: AsFd>(fd_proc: Fd, name: &[u8], data: &[u8], pid: bool) -> Result<(), Errno> {
    let pfd = if pid {
        let mut pfd = XPathBuf::from_pid(Pid::this())?;
        pfd.try_reserve(name.len().checked_add(1).ok_or(Errno::EOVERFLOW)?)
            .or(Err(Errno::ENOMEM))?;
        pfd.push(name);
        Cow::Owned(pfd)
    } else {
        Cow::Borrowed(XPath::from_bytes(name))
    };

    // Open proc file safely.
    let fd = safe_open(
        fd_proc,
        pfd.as_ref(),
        OFlag::O_WRONLY,
        ResolveFlag::RESOLVE_NO_XDEV,
    )?;

    // Check the file is an empty file or bail out.
    if !is_empty_file(&fd).unwrap_or(false) {
        return Err(Errno::EBADFD);
    }

    write_all(&fd, data)
}

/// Open /proc safely and validate procfs.
pub fn proc_open() -> Result<OwnedFd, Errno> {
    let how = OpenHow::new()
        .flags(OFlag::O_PATH | OFlag::O_DIRECTORY | OFlag::O_NOFOLLOW | OFlag::O_CLOEXEC)
        .resolve(ResolveFlag::RESOLVE_NO_MAGICLINKS | ResolveFlag::RESOLVE_NO_SYMLINKS);
    // This function is called before syd::config::proc_init.
    #[expect(clippy::disallowed_methods)]
    let fd_proc = openat2(AT_BADFD, "/proc", how)?;

    // SAFETY: Validate what we've opened is procfs(5).
    if !is_proc(&fd_proc).unwrap_or(false) {
        return Err(Errno::ENODEV);
    }

    Ok(fd_proc)
}

#[cfg(test)]
mod tests {
    use std::os::unix::fs::PermissionsExt;

    use nix::sys::stat::umask;

    use super::*;

    fn setup() -> bool {
        let _ = crate::log::log_init_simple(crate::syslog::LogLevel::Warn);

        if let Err(error) = crate::config::proc_init() {
            eprintln!("Failed to initialize proc: {error:?}");
            return false;
        }

        true
    }

    #[test]
    fn test_invalid_pid() {
        if !setup() {
            return;
        }

        let result = proc_tgid(Pid::from_raw(i32::MAX));
        assert!(result.is_err(), "{result:?}");

        let result = proc_umask(Pid::from_raw(i32::MAX));
        assert!(result.is_err(), "{result:?}");
    }

    #[test]
    fn test_parsing_valid_umask_values() {
        if !setup() {
            return;
        }

        // This test sets various umask values and then checks if our function correctly identifies them.
        let umasks = [
            Mode::from_bits_truncate(0o0000),
            Mode::from_bits_truncate(0o0002),
            Mode::from_bits_truncate(0o0022),
            Mode::from_bits_truncate(0o0077),
            Mode::from_bits_truncate(0o0777),
        ];

        for &my_umask in &umasks {
            umask(my_umask);
            let result = proc_umask(Pid::this()).unwrap();
            assert_eq!(result, my_umask, "{result:o} != {my_umask:o}");
        }

        // Resetting the umask to a default value after test
        umask(Mode::from_bits_truncate(0o0022));
    }

    // Skip test if PROCMAP_QUERY ioctl(2) is not supported.
    macro_rules! skip_if_procmap_query_not_supported {
        () => {
            if !*crate::config::HAVE_PROCMAP_QUERY {
                eprintln!("skipping: PROCMAP_QUERY not supported on this kernel (requires >=6.11)");
                return;
            }
        };
    }

    #[test]
    fn test_procmap_query_default_struct() {
        // Default values should be correct and size set properly.
        let q = ProcmapQuery::default();
        assert_eq!(q.size as usize, size_of::<ProcmapQuery>());
        assert_eq!(q.query_flags, 0);
        assert_eq!(q.query_addr, 0);
        assert_eq!(q.vma_start, 0);
        assert_eq!(q.vma_end, 0);
        assert_eq!(q.vma_flags, 0);
        assert_eq!(q.vma_page_size, 0);
        assert_eq!(q.vma_offset, 0);
        assert_eq!(q.inode, 0);
        assert_eq!(q.dev_major, 0);
        assert_eq!(q.dev_minor, 0);
        assert_eq!(q.vma_name_size, 0);
        assert_eq!(q.build_id_size, 0);
        assert_eq!(q.vma_name_addr, 0);
        assert_eq!(q.build_id_addr, 0);
    }

    #[test]
    fn test_procmap_query_basic_query_no_buffers() {
        skip_if_procmap_query_not_supported!();

        let maps = File::open("/proc/self/maps").unwrap();

        // Use COVERING_OR_NEXT to ensure we get the first VMA
        let q = procmap_query(
            &maps,
            ProcmapQueryFlags::COVERING_OR_NEXT_VMA | ProcmapQueryFlags::VMA_READABLE,
            0,
            None,
            None,
        )
        .expect("basic query failed");

        // vma_start < vma_end for valid VMA
        assert!(q.vma_start < q.vma_end);

        // Permissions bits should include readable.
        let perms = ProcmapQueryFlags::from_bits_truncate(q.vma_flags);
        assert!(perms.contains(ProcmapQueryFlags::VMA_READABLE));

        // Name buffers unchanged.
        assert_eq!(q.vma_name_addr, 0);
        assert_eq!(q.vma_name_size, 0);
    }

    #[test]
    fn test_procmap_query_name_buffer() {
        skip_if_procmap_query_not_supported!();

        let maps = File::open("/proc/self/maps").unwrap();

        let mut buf = [0u8; PATH_MAX];
        let q = procmap_query(
            &maps,
            ProcmapQueryFlags::COVERING_OR_NEXT_VMA,
            0,
            Some(&mut buf),
            None,
        )
        .expect("query with name buffer failed");

        // Buffer address and size reflected.
        assert_eq!(q.vma_name_size as usize <= PATH_MAX, true);
        assert_ne!(q.vma_name_addr, 0);

        // The name should be NUL-terminated and valid.
        let slice = &buf[..q.vma_name_size as usize];
        let cstr = CStr::from_bytes_with_nul(slice).expect("vma name buffer not NUL terminated");
        assert!(!cstr.to_bytes().is_empty(), "empty VMA name");
    }

    #[test]
    fn test_procmap_query_build_id_buffer() {
        skip_if_procmap_query_not_supported!();

        let maps = File::open("/proc/self/maps").unwrap();

        let mut name_buf = [0u8; 512];
        let mut build_buf = [0u8; 64];
        let q = procmap_query(
            &maps,
            ProcmapQueryFlags::COVERING_OR_NEXT_VMA | ProcmapQueryFlags::FILE_BACKED_VMA,
            0,
            Some(&mut name_buf),
            Some(&mut build_buf),
        )
        .expect("query with both buffers failed");

        // Build ID may be absent; ensure size is <= buffer.
        assert!(q.build_id_size as usize <= build_buf.len());

        // Name still valid.
        let slice = &name_buf[..q.vma_name_size as usize];
        let _ = CStr::from_bytes_with_nul(slice).expect("invalid VMA name");
    }

    #[test]
    fn test_procmap_query_all_vmas_iteration() {
        skip_if_procmap_query_not_supported!();

        let maps = File::open("/proc/self/maps").unwrap();

        let flags = ProcmapQueryFlags::COVERING_OR_NEXT_VMA | ProcmapQueryFlags::FILE_BACKED_VMA;
        let mut addr = 0;
        let mut buf = [0u8; PATH_MAX];
        let mut seen = 0;

        loop {
            match procmap_query(&maps, flags, addr, Some(&mut buf), None) {
                Ok(q) => {
                    assert!(q.vma_start < q.vma_end);
                    seen += 1;
                    addr = q.vma_end;
                }
                Err(Errno::ENOENT) => break,
                Err(errno) => panic!("unexpected error during iteration: {errno}"),
            }
        }

        assert!(seen > 0, "expected to see at least one VMA!");
    }

    #[test]
    fn test_procmap_query_executable_filter() {
        skip_if_procmap_query_not_supported!();

        let maps = File::open("/proc/self/maps").unwrap();

        let flags = ProcmapQueryFlags::COVERING_OR_NEXT_VMA
            | ProcmapQueryFlags::FILE_BACKED_VMA
            | ProcmapQueryFlags::VMA_EXECUTABLE;
        let mut addr = 0;
        let mut buf = [0u8; PATH_MAX];
        let mut found_exec = false;

        loop {
            match procmap_query(&maps, flags, addr, Some(&mut buf), None) {
                Ok(q) => {
                    let perms = ProcmapQueryFlags::from_bits_truncate(q.vma_flags);
                    assert!(perms.contains(ProcmapQueryFlags::VMA_EXECUTABLE));
                    found_exec = true;
                    addr = q.vma_end;
                }
                Err(Errno::ENOENT) => break,
                Err(errno) => panic!("unexpected error: {errno}"),
            }
        }

        assert!(found_exec, "no executable VMAs found!");
    }

    #[test]
    fn test_procmap_query_build_id_only() {
        skip_if_procmap_query_not_supported!();

        let maps = File::open("/proc/self/maps").unwrap();

        let mut build_buf = [0u8; 64];
        let q = procmap_query(
            &maps,
            ProcmapQueryFlags::COVERING_OR_NEXT_VMA,
            0,
            None,
            Some(&mut build_buf),
        )
        .expect("query build-id only failed");

        // Name buffer should remain unset.
        assert_eq!(q.vma_name_addr, 0);
        assert_eq!(q.vma_name_size, 0);

        // Build ID size should be <= buffer length.
        assert!(q.build_id_size as usize <= build_buf.len());

        // If build ID present, buffer slice is accessible
        if q.build_id_size > 0 {
            let _ = &build_buf[..q.build_id_size as usize];
        }
    }

    #[test]
    fn test_procmap_query_page_size_nonzero() {
        skip_if_procmap_query_not_supported!();

        let maps = File::open("/proc/self/maps").unwrap();

        let q = procmap_query(
            &maps,
            ProcmapQueryFlags::COVERING_OR_NEXT_VMA,
            0,
            None,
            None,
        )
        .expect("basic query failed");

        // Page size should be non-zero.
        assert!(q.vma_page_size > 0);

        // Offset should be within the VMA range.
        let vma_len = q.vma_end - q.vma_start;
        assert!(q.vma_offset <= vma_len);
    }

    #[test]
    fn test_procmap_query_collect_two_vmas() {
        skip_if_procmap_query_not_supported!();

        let maps = File::open("/proc/self/maps").unwrap();

        let flags = ProcmapQueryFlags::COVERING_OR_NEXT_VMA;
        let mut addr = 0;
        let mut seen_addrs = Vec::new();

        // Collect the start addresses of the first two VMAs.
        for _ in 0..2 {
            let q = procmap_query(&maps, flags, addr, None, None).expect("query iteration failed");
            seen_addrs.push(q.vma_start);
            addr = q.vma_end;
        }

        assert_eq!(seen_addrs.len(), 2);
        assert!(seen_addrs[0] < seen_addrs[1], "VMAs did not advance!");
    }

    #[test]
    fn test_procmap_query_name_buffer_too_small() {
        skip_if_procmap_query_not_supported!();

        let maps = File::open("/proc/self/maps").unwrap();

        let mut buf = [0u8; 1];
        let err = procmap_query(
            &maps,
            ProcmapQueryFlags::COVERING_OR_NEXT_VMA | ProcmapQueryFlags::FILE_BACKED_VMA,
            0,
            Some(&mut buf),
            None,
        )
        .unwrap_err();
        assert_eq!(err, Errno::ENAMETOOLONG);
    }

    #[test]
    fn test_procmap_query_addr_out_of_range() {
        skip_if_procmap_query_not_supported!();

        let maps = File::open("/proc/self/maps").unwrap();

        let result = procmap_query(
            &maps,
            ProcmapQueryFlags::COVERING_OR_NEXT_VMA,
            u64::MAX,
            None,
            None,
        );
        assert_eq!(result.unwrap_err(), Errno::ENOENT);
    }

    #[test]
    fn test_procmap_query_size_field_constant() {
        skip_if_procmap_query_not_supported!();

        let maps = File::open("/proc/self/maps").unwrap();

        let q = procmap_query(
            &maps,
            ProcmapQueryFlags::COVERING_OR_NEXT_VMA,
            0,
            None,
            None,
        )
        .unwrap();
        assert_eq!(q.size as usize, size_of::<ProcmapQuery>());
    }

    #[test]
    fn test_procmap_query_writable_filtering() {
        skip_if_procmap_query_not_supported!();

        let maps = File::open("/proc/self/maps").unwrap();

        let flags = ProcmapQueryFlags::COVERING_OR_NEXT_VMA
            | ProcmapQueryFlags::FILE_BACKED_VMA
            | ProcmapQueryFlags::VMA_WRITABLE;
        let mut addr = 0;
        let mut count = 0;
        let mut buf = [0u8; 256];
        while let Ok(q) = procmap_query(&maps, flags, addr, Some(&mut buf), None) {
            let perms = ProcmapQueryFlags::from_bits_truncate(q.vma_flags);
            assert!(perms.contains(ProcmapQueryFlags::VMA_WRITABLE));
            count += 1;
            addr = q.vma_end;
        }
        assert!(count > 0, "expected at least one writable VMA");
    }

    #[test]
    fn test_procmap_query_file_backed_inode_nonzero() {
        skip_if_procmap_query_not_supported!();

        let maps = File::open("/proc/self/maps").unwrap();

        let flags = ProcmapQueryFlags::COVERING_OR_NEXT_VMA | ProcmapQueryFlags::FILE_BACKED_VMA;
        let mut buf = [0u8; 512];

        let q = procmap_query(&maps, flags, 0, Some(&mut buf), None).unwrap();
        assert!(
            q.inode != 0,
            "expected inode of a file-backed VMA to be non-zero"
        );
    }

    #[test]
    fn test_proc_executables_ok_non_empty() {
        if !setup() {
            return;
        }

        // Self PID must return at least one executable.
        let list = proc_executables(Pid::this()).expect("expected executables");
        assert!(!list.is_empty(), "no executables found for self");
    }

    #[test]
    fn test_proc_executables_unique_and_ordered() {
        if !setup() {
            return;
        }

        // Ensure uniqueness and preserved order.
        let bins = proc_executables(Pid::this()).unwrap();
        let mut seen = SydIndexSet::default();
        for bin in &bins {
            let path = &bin.path;
            assert!(seen.insert(path.clone()), "duplicate path {path}!");
        }

        // Check that insertion order matches returned order.
        let collected: Vec<_> = seen.into_iter().collect();
        let returned: Vec<_> = bins.iter().map(|bin| bin.path.clone()).collect();
        assert_eq!(collected, returned);
    }

    #[test]
    fn test_proc_executables_paths_exist_and_executable() {
        if !setup() {
            return;
        }

        // Returned paths should exist on filesystem and have exec bit.
        // ld.so may not have the exec bit on some systems, e.g. i386.
        let bins = proc_executables(Pid::this()).unwrap();
        for (idx, bin) in bins.into_iter().enumerate() {
            let path = bin.path;
            let md = std::fs::metadata(&path).expect("path does not exist");
            if idx == 0 {
                let perms = md.permissions().mode();
                assert!(
                    perms & 0o111 != 0,
                    "file {path} is not executable (mode {perms:o})",
                );
            }
        }
    }

    #[test]
    fn test_proc_kernel_randomize_va_space() {
        matches!(proc_kernel_randomize_va_space(), Ok(0 | 1 | 2));
    }

    #[test]
    fn test_proc_find_vma() {
        if !setup() {
            return;
        }

        let pid = Pid::this();
        for flags in [
            ProcmapQueryFlags::VMA_READABLE,
            ProcmapQueryFlags::VMA_EXECUTABLE,
            ProcmapQueryFlags::VMA_READABLE | ProcmapQueryFlags::VMA_EXECUTABLE,
        ] {
            let vmas = proc_find_vma(pid, flags).unwrap();
            assert!(!vmas.is_empty());
            for vma in vmas {
                assert!(vma.flags().contains(flags));
            }
        }
    }
}
