//
// Syd: rock-solid application kernel
// src/lib.rs: Common utility functions
//
// Copyright (c) 2023, 2024, 2025 Ali Polatel <alip@chesswob.org>
// likely and unlikely functions are based on the endorphin crate which is:
//    Copyright (c) 2021 Jun Ryoung Ju (junryoungju@gmail.com)
//    SPDX-License-Identifier: MIT
//
// SPDX-License-Identifier: GPL-3.0

//! # syd: The ☮ther SⒶndbøx
//!
//! [![Shine On You Crazy Diamond!](https://img.shields.io/badge/Shine%20On%20You%20Crazy%20Diamond!-8A2BE2)](https://en.wikipedia.org/wiki/Syd_Barrett)
//! [![license](https://img.shields.io/crates/l/jja.svg)](https://git.sr.ht/~alip/syd/tree/main/item/COPYING)
//! [![msrv](https://img.shields.io/badge/rustc-1.70%2B-green?style=plastic)](https://blog.rust-lang.org/2023/06/01/Rust-1.70.0.html)
//! [![build status](https://builds.sr.ht/~alip/syd.svg)](https://builds.sr.ht/~alip/syd?)
//! [![maintenance-status](https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg)](https://git.sr.ht/~alip/syd)
//! [![dependency status](https://deps.rs/repo/sourcehut/~alip/syd/status.svg)](https://deps.rs/repo/sourcehut/~alip/syd)
//! [![repology](https://repology.org/badge/latest-versions/syd.svg)](https://repology.org/project/syd/versions)
//!
//! [![syd](https://git.sr.ht/~alip/syd/blob/main/data/syd.png)](https://todo.sr.ht/~alip/syd)
//! [![GNU](https://web.archive.org/web/20221222061733if_/https://dev.exherbo.org/~alip/images/gnu.png)](https://www.gnu.org/philosophy/philosophy.html)
//! [![Linux](https://chesswob.org/jja/tux.png)](https://www.kernel.org/category/about.html)
//! [![Exherbo](https://web.archive.org/web/20230518155203if_/https://dev.exherbo.org/~alip/images/zebrapig.png)](https://www.exherbolinux.org/docs/gettingstarted.html)
//! [![musl libc](https://www.chesswob.org/jja/musl-inside.png)](https://www.musl-libc.org/)
//! [![libsecc☮mp](https://web.archive.org/web/20221222061720if_/https://dev.exherbo.org/~alip/images/libseccomp.png)](https://github.com/seccomp/libseccomp)
//! [![Paludis](http://paludis.exherbolinux.org/paludis_270.png)](https://paludis.exherbolinux.org)
//!
//! syd is a **seccomp**(2) based sandboxing utility for modern Linux\[\>=5.6\]
//! machines to sandbox unwanted process access to filesystem and network resources.
//! syd requires *no root access* and *no ptrace* rights. All you need is a
//! recent Linux kernel and libsecc☮mp which is available on many different
//! architectures, including **x86**, **x86\_64**, **x32**, **arm**, **aarch64**,
//! **mips**, **mips64**... This makes it very easy for a regular user to use. This is
//! the motto of syd: *bring easy, simple, flexible and powerful access restriction
//! to the Linux user!*
//!
//! The basic idea of syd is to run a command under certain restrictions. These
//! restrictions define which system calls the command is permitted to run and which
//! argument values are permitted for the given system call. The restrictions may be
//! applied via two ways.  *seccomp-bpf* can be used to apply simple Secure Computing
//! user filters to run sandboxing fully on kernel space, and *seccomp-notify*
//! functionality can be used to run sandboxing on kernel space and fallback to user
//! space to dereference pointer arguments of system calls (**See
//! [Security](#security) about `TOCTOU` et. al**), which are one of
//! **[pathname](https://en.wikipedia.org/wiki/Path_(computing))**, **[UNIX socket
//! address](https://en.wikipedia.org/wiki/Unix_domain_socket)**,
//! **[IPv4](https://en.wikipedia.org/wiki/IPv4)** or
//! **[IPv6](https://en.wikipedia.org/wiki/IPv6)** network address, and make dynamic
//! decisions using [Unix shell style patterns](https://docs.rs/globset) such as
//! `allow/write+/home/syd/***`, or `allow/write+/run/user/*/pulse` for
//! **[pathnames](https://en.wikipedia.org/wiki/Path_(computing))**, and using
//! **[CIDR](https://docs.rs/ipnetwork)** notation such as
//! `allow/net/connect+127.0.0.1/8!9050`, or
//! `allow/net/connect+::1/8!9050` for
//! **[IPv4](https://en.wikipedia.org/wiki/IPv4)** and
//! **[IPv6](https://en.wikipedia.org/wiki/IPv6)** addresses and perform an action
//! which is by default denying the system call with an appropriate error, which is
//! usually **access denied**, aka `EACCES`. For default disallowed system calls,
//! such as `ptrace` or `process_vm_writev` (**See [Security](#security) about
//! `TOCTOU` et. al**) syd returns `EACCES` as well.
//!
//! To be able to use syd, you need a recent Linux kernel with the system calls
//! **pidfd_getfd**, **pidfd_send_signal**. The Secure Computing facility of the
//! Linux kernel should support the **SECCOMP_USER_NOTIF_FLAG_CONTINUE** operation.
//! It is recommended to have the **CONFIG_CROSS_MEMORY_ATTACH** kernel option
//! enabled, if this option is not enabled, syd will fallback to reading/writing
//! from `/proc/$pid/mem`. Linux-5.11 or later is recommended.

// We like clean and simple code with documentation.
// Keep in sync with main.rs.
#![deny(missing_docs)]
#![deny(clippy::allow_attributes_without_reason)]
#![deny(clippy::arithmetic_side_effects)]
#![deny(clippy::as_ptr_cast_mut)]
#![deny(clippy::as_underscore)]
#![deny(clippy::assertions_on_result_states)]
#![deny(clippy::borrow_as_ptr)]
#![deny(clippy::branches_sharing_code)]
#![deny(clippy::case_sensitive_file_extension_comparisons)]
#![deny(clippy::cast_lossless)]
#![deny(clippy::cast_possible_truncation)]
#![deny(clippy::cast_possible_wrap)]
#![deny(clippy::cast_precision_loss)]
#![deny(clippy::cast_ptr_alignment)]
#![deny(clippy::cast_sign_loss)]
#![deny(clippy::checked_conversions)]
#![deny(clippy::clear_with_drain)]
#![deny(clippy::clone_on_ref_ptr)]
#![deny(clippy::cloned_instead_of_copied)]
#![deny(clippy::cognitive_complexity)]
#![deny(clippy::collection_is_never_read)]
#![deny(clippy::copy_iterator)]
#![deny(clippy::create_dir)]
#![deny(clippy::dbg_macro)]
#![deny(clippy::debug_assert_with_mut_call)]
#![deny(clippy::decimal_literal_representation)]
#![deny(clippy::default_trait_access)]
#![deny(clippy::default_union_representation)]
#![deny(clippy::derive_partial_eq_without_eq)]
#![deny(clippy::doc_link_with_quotes)]
//#![deny(clippy::doc_markdown)]
#![deny(clippy::explicit_into_iter_loop)]
#![deny(clippy::explicit_iter_loop)]
#![deny(clippy::fallible_impl_from)]
#![deny(clippy::missing_safety_doc)]
#![deny(clippy::undocumented_unsafe_blocks)]
// TODO: remove the allow-lint below when libseccomp is back to upstream.
#![allow(macro_expanded_macro_exports_accessed_by_absolute_paths)]

/// Utilities for caching
pub mod cache;
/// Compatibility code for different libcs
#[allow(missing_docs)]
pub mod compat;
/// Static configuration, edit & recompile!
pub mod config;
/// DNS utilities
pub mod dns;
/// ELF parser
pub mod elf;
/// Error types and error handling code.
pub mod err;
/// Filesystem utilities
pub mod fs;
/// Utilities for hashing
pub mod hash;
/// Secure computing hooks
pub mod hook;
/// Simple logging on standard error using JSON lines
pub mod log;
/// /proc nom parsers
pub(crate) mod parsers;
/// Path handling for UNIX
pub mod path;
/// /proc utilities
pub mod proc;
/// ptrace(2) utilities
pub mod ptrace;
/// Sandbox configuration
pub mod sandbox;
/// Execute program as sealed anonymous file
pub mod seal;
/// Interface to Linux prctl(2) speculation misfeature interfac
pub mod spec;
/// sysinfo(2) interface
pub mod sysinfo;
/// syslog(2) interface
pub mod syslog;
/// Shell-style wildcard matching
#[allow(clippy::arithmetic_side_effects)]
pub mod wildmatch;
/// Interface to wordexp(3)
pub mod wordexp;

// Vendored crates:
/// Interface to Linux capabilities
#[allow(dead_code)]
#[allow(missing_docs)]
#[allow(clippy::arithmetic_side_effects)]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::disallowed_types)]
#[allow(clippy::missing_safety_doc)]
#[allow(clippy::undocumented_unsafe_blocks)]
pub mod caps;
/// High-level interface to libseccomp.
#[allow(clippy::arithmetic_side_effects)]
#[allow(clippy::cast_lossless)]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::disallowed_methods)]
#[allow(clippy::doc_lazy_continuation)]
#[allow(clippy::undocumented_unsafe_blocks)]
#[allow(missing_docs)]
pub mod libseccomp;
/// Low-level interface to libseccomp.
#[allow(missing_docs)]
pub mod libseccomp_sys;
// Used by Landlock crate.
// lazy_static is a dev-only dependency.
#[cfg(test)]
#[macro_use]
extern crate lazy_static;
/// Interface to LandLock LSM
#[allow(dead_code)]
#[allow(missing_docs)]
#[allow(unused_imports)]
#[allow(clippy::as_underscore)]
#[allow(clippy::borrow_as_ptr)]
#[allow(clippy::cast_lossless)]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::decimal_literal_representation)]
#[allow(clippy::default_trait_access)]
#[allow(clippy::disallowed_methods)]
#[allow(clippy::disallowed_types)]
#[allow(clippy::init_numbered_fields)]
#[allow(clippy::missing_safety_doc)]
#[allow(clippy::type_complexity)]
#[allow(clippy::undocumented_unsafe_blocks)]
pub mod landlock;
/// Read the ELF dependency tree
#[allow(dead_code)]
#[allow(clippy::disallowed_methods)]
#[allow(clippy::manual_flatten)]
#[allow(clippy::too_many_arguments)]
#[allow(missing_docs)]
/// rusty_pool: Self growing / shrinking `ThreadPool` implementation
pub(crate) mod pool;
/// The low-level interface for linux namespaces (containers)
pub mod unshare;

use std::{
    arch::asm,
    collections::HashSet,
    ffi::{CStr, OsStr},
    ops::{
        BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not, RangeInclusive, Sub,
        SubAssign,
    },
    os::fd::{AsFd, AsRawFd, BorrowedFd, RawFd},
    path::Path,
    process::exit,
};

use landlock::{
    path_beneath_rules, Access, AccessFs, AccessNet, NetPort, RestrictionStatus, Ruleset,
    RulesetAttr, RulesetCreatedAttr, RulesetError, ScopeFlag,
};
use lexis::ToName;
use libseccomp::{ScmpAction, ScmpArch, ScmpFilterContext, ScmpSyscall, ScmpVersion};
use nix::{
    dir::Dir,
    errno::Errno,
    fcntl::OFlag,
    libc::c_int,
    mount::MsFlags,
    sched::{unshare, CloneFlags},
    sys::{
        signal::{sigaction, signal, SaFlags, SigAction, SigHandler, SigSet, Signal},
        socket::{socket, AddressFamily, SockFlag, SockType},
        stat::Mode,
        utsname::uname,
        wait::{waitpid, Id, WaitPidFlag, WaitStatus},
    },
    unistd::{fork, ForkResult, Gid, Group, Pid, Uid, User},
};
use procfs::process::{MMPermissions, MMapPath, MemoryMap};
use serde::Serialize;

use crate::{
    compat::{fstatx, waitid, STATX_BASIC_STATS},
    err::{err2no, SydResult},
    fs::safe_clone,
    landlock::{RulesetStatus, ABI},
    path::{mask_path, XPath, XPathBuf},
    spec::{speculation_get, SpeculationFeature},
};

/* Data structures */

/// Simple wrapper over ScmpSyscall and ScmpArch to provide Display.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Sydcall(pub ScmpSyscall, pub u32);

impl std::fmt::Display for Sydcall {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let arch = match scmp_arch(self.1) {
            Ok(arch) => arch,
            Err(_) => return write!(f, "?"),
        };

        match self.0.get_name_by_arch(arch).ok() {
            Some(name) => write!(f, "{name}"),
            None => write!(f, "?"),
        }
    }
}

impl Serialize for Sydcall {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let arch = match scmp_arch(self.1) {
            Ok(arch) => arch,
            Err(_) => return serializer.serialize_none(),
        };

        match self.0.get_name_by_arch(arch).ok() {
            Some(name) => serializer.serialize_str(&name),
            None => serializer.serialize_none(),
        }
    }
}

pub(crate) struct SydArch(ScmpArch);

impl Serialize for SydArch {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let arch = format!("{:?}", self.0).to_ascii_lowercase();
        let arch = if arch == { "x8664" } { "x86_64" } else { &arch };
        serializer.serialize_str(arch)
    }
}

/// A wrapper type that wraps MemoryMap and provides `Serialize`.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SydMemoryMap(pub MemoryMap);

impl SydMemoryMap {
    /// Checks if the memory map points to a stack.
    pub fn is_stack(&self) -> bool {
        matches!(self.0.pathname, MMapPath::Stack | MMapPath::TStack(_))
    }
}

impl std::fmt::Display for SydMemoryMap {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        let mmap = &self.0;

        // Build permissions string.
        let perms = format!(
            "{}{}{}{}",
            if mmap.perms.contains(MMPermissions::READ) {
                "r"
            } else {
                "-"
            },
            if mmap.perms.contains(MMPermissions::WRITE) {
                "w"
            } else {
                "-"
            },
            if mmap.perms.contains(MMPermissions::EXECUTE) {
                "x"
            } else {
                "-"
            },
            if mmap.perms.contains(MMPermissions::SHARED) {
                "s"
            } else if mmap.perms.contains(MMPermissions::PRIVATE) {
                "p"
            } else {
                "-"
            }
        );

        // Map pathname.
        let pathname = match &mmap.pathname {
            MMapPath::Path(path) => mask_path(path),
            MMapPath::Heap => "[heap]".to_string(),
            MMapPath::Stack => "[stack]".to_string(),
            MMapPath::TStack(tid) => format!("[stack:{}]", tid),
            MMapPath::Vdso => "[vdso]".to_string(),
            MMapPath::Vvar => "[vvar]".to_string(),
            MMapPath::Vsyscall => "[vsyscall]".to_string(),
            MMapPath::Rollup => "[rollup]".to_string(),
            MMapPath::Anonymous => "[anon]".to_string(),
            MMapPath::Vsys(key) => format!("[vsys:{}]", key),
            MMapPath::Other(pseudo_path) => mask_path(Path::new(pseudo_path)),
        };

        // Format output line.
        write!(
            f,
            "{:x}-{:x} {perms:<4} {:08x} {:02x}:{:02x} {:<10} {pathname}",
            mmap.address.0, mmap.address.1, mmap.offset, mmap.dev.0, mmap.dev.1, mmap.inode,
        )
    }
}

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

// Execution domain constants, taken from sys/personality.h
const PER_LINUX: c_int = 0;
const PER_LINUX_32BIT: c_int = PER_LINUX | ADDR_LIMIT_32BIT;
const PER_LINUX_FDPIC: c_int = PER_LINUX | FDPIC_FUNCPTRS;
const PER_SVR4: c_int = 1 | STICKY_TIMEOUTS | MMAP_PAGE_ZERO;
const PER_SVR3: c_int = 2 | STICKY_TIMEOUTS | SHORT_INODE;
const PER_SCOSVR3: c_int = 3 | STICKY_TIMEOUTS | WHOLE_SECONDS | SHORT_INODE;
const PER_OSR5: c_int = 3 | STICKY_TIMEOUTS | WHOLE_SECONDS;
const PER_WYSEV386: c_int = 4 | STICKY_TIMEOUTS | SHORT_INODE;
const PER_ISCR4: c_int = 5 | STICKY_TIMEOUTS;
const PER_BSD: c_int = 6;
const PER_SUNOS: c_int = PER_BSD | STICKY_TIMEOUTS;
const PER_XENIX: c_int = 7 | STICKY_TIMEOUTS | SHORT_INODE;
const PER_LINUX32: c_int = 8;
const PER_LINUX32_3GB: c_int = PER_LINUX32 | ADDR_LIMIT_3GB;
const PER_IRIX32: c_int = 9 | STICKY_TIMEOUTS;
const PER_IRIXN32: c_int = 0xa | STICKY_TIMEOUTS;
const PER_IRIX64: c_int = 0x0b | STICKY_TIMEOUTS;
const PER_RISCOS: c_int = 0xc;
const PER_SOLARIS: c_int = 0xd | STICKY_TIMEOUTS;
const PER_UW7: c_int = 0xe | STICKY_TIMEOUTS | MMAP_PAGE_ZERO;
const PER_OSF4: c_int = 0xf;
const PER_HPUX: c_int = 0x10;
const PER_MASK: c_int = 0xff;

// Flag constants, taken from sys/personality.h
const UNAME26: c_int = 0x0020000;
const ADDR_NO_RANDOMIZE: c_int = 0x0040000;
const FDPIC_FUNCPTRS: c_int = 0x0080000;
const MMAP_PAGE_ZERO: c_int = 0x0100000;
const ADDR_COMPAT_LAYOUT: c_int = 0x0200000;
const READ_IMPLIES_EXEC: c_int = 0x0400000;
const ADDR_LIMIT_32BIT: c_int = 0x0800000;
const SHORT_INODE: c_int = 0x1000000;
const WHOLE_SECONDS: c_int = 0x2000000;
const STICKY_TIMEOUTS: c_int = 0x4000000;
const ADDR_LIMIT_3GB: c_int = 0x8000000;

// A type that wraps personality(2) return value and implements Display.
struct SydPersona(pub c_int);

impl std::fmt::Display for SydPersona {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        let domain = match self.0 & PER_MASK {
            PER_LINUX => "linux",
            PER_LINUX_32BIT => "linux_32bit",
            PER_LINUX_FDPIC => "linux_fdpic",
            PER_SVR4 => "svr4",
            PER_SVR3 => "svr3",
            PER_SCOSVR3 => "scosvr3",
            PER_OSR5 => "osr5",
            PER_WYSEV386 => "wysev386",
            PER_ISCR4 => "iscr4",
            PER_BSD => "bsd",
            PER_SUNOS => "sunos",
            PER_XENIX => "xenix",
            PER_LINUX32 => "linux32",
            PER_LINUX32_3GB => "linux32_3gb",
            PER_IRIX32 => "irix32",
            PER_IRIXN32 => "irixn32",
            PER_IRIX64 => "irix64",
            PER_RISCOS => "riscos",
            PER_SOLARIS => "solaris",
            PER_UW7 => "uw7",
            PER_OSF4 => "osf4",
            PER_HPUX => "hpux",
            _ => "unknown",
        };

        let flags = [
            (UNAME26, "uname26"),
            (ADDR_NO_RANDOMIZE, "addr-no-randomize"),
            (FDPIC_FUNCPTRS, "fdpic-funcptrs"),
            (MMAP_PAGE_ZERO, "mmap-page-zero"),
            (ADDR_COMPAT_LAYOUT, "addr-compat-layout"),
            (READ_IMPLIES_EXEC, "read-implies-exec"),
            (ADDR_LIMIT_32BIT, "addr-limit-32bit"),
            (SHORT_INODE, "short-inode"),
            (WHOLE_SECONDS, "whole-seconds"),
            (STICKY_TIMEOUTS, "sticky-timeouts"),
            (ADDR_LIMIT_3GB, "addr-limit-3gb"),
        ]
        .iter()
        .filter_map(|&(flag, name)| {
            if self.0 & flag == flag {
                Some(name)
            } else {
                None
            }
        })
        .collect::<Vec<_>>()
        .join(",");

        if flags.is_empty() {
            write!(f, "{domain}")
        } else {
            write!(f, "{domain},{flags}")
        }
    }
}

/// MS_NOSYMFOLLOW is Linux>=5.10 and not defined by libc yet.
pub const MS_NOSYMFOLLOW: MsFlags = MsFlags::from_bits_retain(256);

/// SydMsFlags wraps MsFlags and provides from_name.
/// This is already available in newer bitflags versions.
pub(crate) struct SydMsFlags(MsFlags);

impl SydMsFlags {
    fn from_name(name: &str) -> Option<Self> {
        match name {
            "ro" => Some(SydMsFlags(MsFlags::MS_RDONLY)),
            "nosuid" => Some(SydMsFlags(MsFlags::MS_NOSUID)),
            "nodev" => Some(SydMsFlags(MsFlags::MS_NODEV)),
            "noexec" => Some(SydMsFlags(MsFlags::MS_NOEXEC)),
            "nosymfollow" => Some(SydMsFlags(MS_NOSYMFOLLOW)),
            "sync" => Some(SydMsFlags(MsFlags::MS_SYNCHRONOUS)),
            "remount" => Some(SydMsFlags(MsFlags::MS_REMOUNT)),
            "mandlock" => Some(SydMsFlags(MsFlags::MS_MANDLOCK)),
            "dirsync" => Some(SydMsFlags(MsFlags::MS_DIRSYNC)),
            "noatime" => Some(SydMsFlags(MsFlags::MS_NOATIME)),
            "nodiratime" => Some(SydMsFlags(MsFlags::MS_NODIRATIME)),
            "bind" => Some(SydMsFlags(MsFlags::MS_BIND)),
            "move" => Some(SydMsFlags(MsFlags::MS_MOVE)),
            "rec" => Some(SydMsFlags(MsFlags::MS_REC)),
            "silent" => Some(SydMsFlags(MsFlags::MS_SILENT)),
            "posixacl" => Some(SydMsFlags(MsFlags::MS_POSIXACL)),
            "unbindable" => Some(SydMsFlags(MsFlags::MS_UNBINDABLE)),
            "private" => Some(SydMsFlags(MsFlags::MS_PRIVATE)),
            "slave" => Some(SydMsFlags(MsFlags::MS_SLAVE)),
            "shared" => Some(SydMsFlags(MsFlags::MS_SHARED)),
            "relatime" => Some(SydMsFlags(MsFlags::MS_RELATIME)),
            "i_version" => Some(SydMsFlags(MsFlags::MS_I_VERSION)),
            "strictatime" => Some(SydMsFlags(MsFlags::MS_STRICTATIME)),
            "lazytime" => Some(SydMsFlags(MsFlags::MS_LAZYTIME)),
            // Deprecated: Should only be used in-kernel.
            //"kernmount" => Some(SydMsFlags(MsFlags::MS_KERNMOUNT)),
            // "active" => Some(SydMsFlags(MsFlags::MS_ACTIVE)),
            // "nouser" => Some(SydMsFlags(MsFlags::MS_NOUSER)),
            _ => None,
        }
    }

    // Convert MsFlags to a vector of flag names
    #[allow(clippy::cognitive_complexity)]
    fn to_names(&self) -> Vec<&str> {
        let mut names = Vec::new();

        if self.0.contains(MsFlags::MS_RDONLY) {
            names.push("ro");
        }
        if self.0.contains(MsFlags::MS_NOSUID) {
            names.push("nosuid");
        }
        if self.0.contains(MsFlags::MS_NODEV) {
            names.push("nodev");
        }
        if self.0.contains(MsFlags::MS_NOEXEC) {
            names.push("noexec");
        }
        if self.0.contains(MS_NOSYMFOLLOW) {
            names.push("nosymfollow");
        }
        if self.0.contains(MsFlags::MS_SYNCHRONOUS) {
            names.push("sync");
        }
        if self.0.contains(MsFlags::MS_REMOUNT) {
            names.push("remount");
        }
        if self.0.contains(MsFlags::MS_MANDLOCK) {
            names.push("mandlock");
        }
        if self.0.contains(MsFlags::MS_DIRSYNC) {
            names.push("dirsync");
        }
        if self.0.contains(MsFlags::MS_NOATIME) {
            names.push("noatime");
        }
        if self.0.contains(MsFlags::MS_NODIRATIME) {
            names.push("nodiratime");
        }
        if self.0.contains(MsFlags::MS_BIND) {
            names.push("bind");
        }
        if self.0.contains(MsFlags::MS_MOVE) {
            names.push("move");
        }
        if self.0.contains(MsFlags::MS_REC) {
            names.push("rec");
        }
        if self.0.contains(MsFlags::MS_SILENT) {
            names.push("silent");
        }
        if self.0.contains(MsFlags::MS_POSIXACL) {
            names.push("posixacl");
        }
        if self.0.contains(MsFlags::MS_UNBINDABLE) {
            names.push("unbindable");
        }
        if self.0.contains(MsFlags::MS_PRIVATE) {
            names.push("private");
        }
        if self.0.contains(MsFlags::MS_SLAVE) {
            names.push("slave");
        }
        if self.0.contains(MsFlags::MS_SHARED) {
            names.push("shared");
        }
        if self.0.contains(MsFlags::MS_RELATIME) {
            names.push("relatime");
        }
        if self.0.contains(MsFlags::MS_I_VERSION) {
            names.push("i_version");
        }
        if self.0.contains(MsFlags::MS_STRICTATIME) {
            names.push("strictatime");
        }
        if self.0.contains(MsFlags::MS_LAZYTIME) {
            names.push("lazytime");
        }

        names
    }
}

/* Utilities */

/// Print Syd version information,
/// and information about the system to
/// standard output.
pub fn syd_info() {
    #[allow(clippy::disallowed_methods)]
    let major = env!("CARGO_PKG_VERSION_MAJOR")
        .parse::<u64>()
        .expect("CARGO_PKG_VERSION_MAJOR");
    #[allow(clippy::disallowed_methods)]
    let minor = env!("CARGO_PKG_VERSION_MINOR")
        .parse::<u64>()
        .expect("CARGO_PKG_VERSION_MINOR");
    #[allow(clippy::disallowed_methods)]
    let patch = env!("CARGO_PKG_VERSION_PATCH")
        .parse::<u64>()
        .expect("CARGO_PKG_VERSION_PATCH");
    let hex_version = (major << 16) | (minor << 8) | patch;
    let code_name = hex_version
        .to_name()
        .split('_')
        .map(|word| {
            let mut c = word.chars();
            match c.next() {
                None => String::new(),
                Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
            }
        })
        .collect::<Vec<String>>()
        .join(" ");

    println!("syd {} ({})", *crate::config::VERSION, code_name);
    println!("Author: Ali Polatel");
    println!("License: GPL-3.0");

    let feat = [
        #[cfg(debug_assertions)]
        "+debug",
        #[cfg(not(debug_assertions))]
        "-debug",
        #[cfg(feature = "log")]
        "+log",
        #[cfg(not(feature = "log"))]
        "-log",
        #[cfg(feature = "oci")]
        "+oci",
        #[cfg(not(feature = "oci"))]
        "-oci",
    ];
    println!("Features: {}", feat.join(", "));

    let libapi = libseccomp::get_api();
    match ScmpVersion::current() {
        Ok(libver) => {
            println!(
                "LibSeccomp: v{}.{}.{} api:{}",
                libver.major, libver.minor, libver.micro, libapi
            );
        }
        Err(error) => {
            println!("LibSeccomp: ? (error: {error})");
        }
    }

    #[allow(clippy::disallowed_methods)]
    let bpf_jit = match std::fs::read_to_string("/proc/sys/net/core/bpf_jit_enable") {
        Ok(val) => match val.trim() {
            "0" => "disabled".to_string(),
            "1" => "enabled".to_string(),
            "2" => "enabled in debug mode".to_string(),
            n => format!("{n} (error: {})", Errno::EINVAL),
        },
        Err(err) => format!("? (error: {})", err2no(&err)),
    };
    println!("BPF JIT compiler is {bpf_jit}.");

    let abi = ABI::new_current();
    if abi == ABI::Unsupported {
        println!("Landlock is not supported.");
    } else {
        let state = lock_enabled(abi);
        let state_verb = match state {
            0 => "fully enforced",
            1 => "partially enforced",
            2 => "not enforced",
            _ => "unsupported",
        };
        println!("Landlock ABI {} is {state_verb}.", abi as i32);
    }

    println!(
        "User namespaces are {}supported.",
        if ns_enabled(CloneFlags::CLONE_NEWUSER).unwrap_or(false) {
            ""
        } else {
            "not "
        }
    );

    // Check CONFIG_CROSS_MEMORY_ATTACH.
    let cfg_cma = !matches!(
        // SAFETY: We explicitly call the system call with
        // invalid arguments just to check for host Linux
        // kernel support.
        Errno::result(unsafe {
            libc::process_vm_readv(0, std::ptr::null(), 0, std::ptr::null(), 0, 0)
        }),
        Err(Errno::ENOSYS)
    );
    println!(
        "Cross memory attach is {}supported{}",
        if cfg_cma { "" } else { "not " },
        if cfg_cma {
            "."
        } else {
            " (\x1b[91minsecure\x1b[0m)."
        },
    );

    let uname = match uname() {
        Ok(info) => OsStr::to_str(info.release()).unwrap_or("?").to_string(),
        Err(_) => "?".to_string(),
    };
    println!("Host (build): {}", env!("SYD_BUILDHOST"));
    println!(
        "Host (target): {uname} {}",
        seccomp_arch_native_name().unwrap_or("?")
    );

    // SAFETY: In libc we trust.
    let pers = match unsafe { libc::personality(0xFFFFFFFF) } {
        n if n < 0 => format!("? (error: {})", Errno::last()),
        n => SydPersona(n).to_string(),
    };

    println!(
        "Environment: {}-{pers}-{}",
        env!("SYD_TARGET_ENV"),
        env!("SYD_TARGET_POINTER_WIDTH")
    );

    println!(
        "CPU: {} ({} cores), {}-endian",
        num_cpus::get(),
        num_cpus::get_physical(),
        env!("SYD_TARGET_ENDIAN")
    );
    println!("CPUFLAGS: {}", env!("SYD_TARGET_FEATURE"));

    for spec_feat in [
        SpeculationFeature::StoreBypass,
        SpeculationFeature::IndirectBranch,
        SpeculationFeature::L1DFlush,
    ] {
        println!(
            "{}",
            match speculation_get(spec_feat) {
                Ok(status) => status.to_string(),
                Err(errno) => format!("{spec_feat} status: ? (error: {errno})"),
            }
        );
    }
}

#[inline]
pub(crate) fn op2name(op: u8) -> &'static str {
    match op {
        0x2 => "bind",
        0x3 => "connect",
        0x5 => "accept",
        0xb => "sendto",
        0x10 => "sendmsg",
        0x12 => "accept4",
        0x14 => "sendmmsg",
        _ => unreachable!(),
    }
}

#[inline]
pub(crate) fn op2errno(op: u8) -> Errno {
    match op {
        0x2 /*bind*/ => Errno::EADDRNOTAVAIL,
        0x3 /*connect*/ =>  Errno::ECONNREFUSED,
        0x5 | 0x12 /*accept{,4}*/ => Errno::ECONNABORTED,
        _ /*send{to,{m,}msg}*/ => Errno::ENOTCONN,
    }
}

/// Checks if the given namespaces are enabled.
pub fn ns_enabled(ns_flags: CloneFlags) -> Result<bool, Errno> {
    const SAFE_CLONE_FLAGS: libc::c_int = libc::CLONE_FS
        | libc::CLONE_FILES
        | libc::CLONE_IO
        | libc::CLONE_VM
        | libc::CLONE_VFORK
        | libc::CLONE_SIGHAND;

    // All set, spawn the thread to check unprivileged userns.
    let mut stack = [0u8; crate::config::MINI_STACK_SIZE];
    #[allow(clippy::blocks_in_conditions)]
    let pid_fd = safe_clone(
        Box::new(|| -> isize {
            if unshare(ns_flags).is_ok() {
                0
            } else {
                127
            }
        }),
        &mut stack[..],
        SAFE_CLONE_FLAGS,
        Some(libc::SIGCHLD),
    )?;

    loop {
        break match waitid(Id::PIDFd(pid_fd.as_fd()), WaitPidFlag::WEXITED) {
            Ok(crate::compat::WaitStatus::Exited(_, 0)) => Ok(true),
            Ok(_) => Ok(false),
            Err(Errno::EINTR) => continue,
            Err(errno) => Err(errno),
        };
    }
}

/// Checks if the given LandLock ABI is supported.
/// Returns:
/// - 0: Fully enforced
/// - 1: Partially enforced
/// - 2: Not enforced
/// - 127: Unsupported
pub fn lock_enabled(abi: ABI) -> u8 {
    let path_ro = vec![XPathBuf::from("/")];
    let path_rw = vec![XPathBuf::from("/")];
    // Landlock network is ABI>=4.
    let port_if = if abi as i32 >= ABI::V4 as i32 {
        Some((2525, 22))
    } else {
        None
    };

    // A helper function to wrap the operations and reduce duplication
    #[allow(clippy::disallowed_methods)]
    fn landlock_operation(
        abi: ABI,
        path_ro: &[XPathBuf],
        path_rw: &[XPathBuf],
        port_if: Option<(u16, u16)>,
    ) -> Result<RestrictionStatus, RulesetError> {
        // from_all includes IoctlDev of ABI >= 5 as necessary.
        let mut ruleset = Ruleset::default().handle_access(AccessFs::from_all(abi))?;
        let ruleset_ref = &mut ruleset;

        let mut network_rules: Vec<Result<NetPort, RulesetError>> = vec![];
        if let Some((port_bind, port_conn)) = port_if {
            ruleset_ref.handle_access(AccessNet::BindTcp)?;
            network_rules.push(Ok(NetPort::new(port_bind, AccessNet::BindTcp)));

            ruleset_ref.handle_access(AccessNet::ConnectTcp)?;
            network_rules.push(Ok(NetPort::new(port_conn, AccessNet::ConnectTcp)));
        }

        // Landlock network is ABI>=6.
        if abi as i32 >= ABI::V6 as i32 {
            ruleset_ref.handle_access(ScopeFlag::AbstractUnixSocket)?;
            ruleset_ref.handle_access(ScopeFlag::Signal)?;
        }

        ruleset
            .create()?
            .add_rules(path_beneath_rules(path_ro, AccessFs::from_read(abi)))?
            .add_rules(path_beneath_rules(path_rw, AccessFs::from_all(abi)))?
            .add_rules(network_rules)?
            .restrict_self()
    }

    match landlock_operation(abi, &path_ro, &path_rw, port_if) {
        Ok(status) => match status.ruleset {
            RulesetStatus::FullyEnforced => 0,
            RulesetStatus::PartiallyEnforced => 1,
            RulesetStatus::NotEnforced => 2,
        },
        Err(_) => 127,
    }
}

/// Returns true if we are running under syd.
#[allow(clippy::disallowed_methods)]
pub fn syd_enabled() -> bool {
    // This will not work if the sandbox is locked.
    // Path::new("/dev/syd").exists() || Path::new("/dev/syd").exists()
    // SAFETY: In libc, we trust.
    match unsafe { fork() } {
        Ok(ForkResult::Parent { child, .. }) => {
            match waitpid(child, None) {
                Ok(WaitStatus::Exited(_, code)) => {
                    // Check the child's exit status.
                    // Exit status of 0 means syd is enabled.
                    code == 0
                }
                _ => {
                    // If there's an error waiting on the
                    // child, assume syd is not enabled.
                    false
                }
            }
        }
        Ok(ForkResult::Child) => {
            let mut ctx = match ScmpFilterContext::new(ScmpAction::Allow) {
                Ok(ctx) => ctx,
                Err(_) => exit(1),
            };

            let syscall = ScmpSyscall::from_name("open").unwrap();
            if ctx.add_rule(ScmpAction::Notify, syscall).is_err() {
                exit(1);
            }

            if ctx.load().is_err() && Errno::last() == Errno::EBUSY {
                // seccomp filter exists
                // syd is in business.
                exit(0);
            } else {
                // seccomp filter does not exist
                exit(1);
            }
        }
        Err(_) => {
            // If there's an error forking,
            // assume syd is not enabled.
            false
        }
    }
}

// Returns the name of the libsecc☮mp native architecture.
fn seccomp_arch_native_name() -> Option<&'static str> {
    match ScmpArch::native() {
        ScmpArch::X86 => Some("x86"),
        ScmpArch::X8664 => Some("x86_64"),
        ScmpArch::X32 => Some("x32"),
        ScmpArch::Arm => Some("arm"),
        ScmpArch::Aarch64 => Some("aarch64"),
        ScmpArch::Loongarch64 => Some("loongarch64"),
        ScmpArch::M68k => Some("m68k"),
        ScmpArch::Mips => Some("mips"),
        ScmpArch::Mips64 => Some("mips64"),
        ScmpArch::Mips64N32 => Some("mips64n32"),
        ScmpArch::Mipsel => Some("mipsel"),
        ScmpArch::Mipsel64 => Some("mipsel64"),
        ScmpArch::Mipsel64N32 => Some("mipsel64n32"),
        ScmpArch::Ppc => Some("ppc"),
        ScmpArch::Ppc64 => Some("ppc64"),
        ScmpArch::Ppc64Le => Some("ppc64le"),
        ScmpArch::S390 => Some("s390"),
        ScmpArch::S390X => Some("s390x"),
        ScmpArch::Parisc => Some("parisc"),
        ScmpArch::Parisc64 => Some("parisc64"),
        ScmpArch::Riscv64 => Some("riscv64"),
        ScmpArch::Sheb => Some("sheb"),
        ScmpArch::Sh => Some("sh"),
        _ => None,
    }
}

/// Given a `Uid`, return the user name of the user.
/// On any error conditions, return "nobody".
pub fn get_user_name(uid: Uid) -> String {
    match User::from_uid(uid) {
        Ok(Some(user)) => user.name,
        _ => "nobody".to_string(),
    }
}

/// Given a username, return the home directory of the user.
/// On any error conditions, return "/proc/self/fdinfo".
pub fn get_user_home(username: &str) -> XPathBuf {
    // Fetch user details.
    match User::from_name(username) {
        Ok(Some(user)) => user.dir.into(),
        _ => "/proc/self/fdinfo".into(),
    }
}

/// Sets the specified signal to be ignored.
pub fn ignore_signal(signal: Signal) -> Result<(), Errno> {
    let sig_action = SigAction::new(
        SigHandler::SigIgn, // Set to ignore
        SaFlags::empty(),
        SigSet::empty(),
    );

    // SAFETY: The unsafe call to `sigaction` is used to set the
    // signal's disposition to "ignore". We're not invoking any handlers
    // or performing any operations that could lead to data races or
    // other undefined behaviors. Hence, it's safe to call in this
    // context.
    unsafe { sigaction(signal, &sig_action) }.map(drop)
}

/// Sets the specified signal to be set to its default action.
pub fn reset_signal(signal: Signal) -> Result<(), Errno> {
    let sig_action = SigAction::new(
        SigHandler::SigDfl, // Set to default
        SaFlags::empty(),
        SigSet::empty(),
    );

    // SAFETY: The unsafe call to `sigaction` is used to set the
    // signal's disposition to "ignore". We're not invoking any handlers
    // or performing any operations that could lead to data races or
    // other undefined behaviors. Hence, it's safe to call in this
    // context.
    unsafe { sigaction(signal, &sig_action) }.map(drop)
}

/// Ignores all signals except SIG{KILL,STOP,PIPE,CHLD},
/// and all signals with default action Core.
pub fn ignore_signals() -> Result<(), Errno> {
    // Iterate through all possible signals and set them to be ignored.
    // Step 1: Normal signals.
    for signal in Signal::iterator() {
        if !matches!(
            signal,
            Signal::SIGALRM | Signal::SIGKILL | Signal::SIGSTOP | Signal::SIGPIPE | Signal::SIGCHLD
        ) && !is_coredump(signal as i32)
        {
            // 1. Can not ignore SIGKILL and SIGSTOP.
            // 2. Do not need to ignore Signals with default action Core.
            // 3. Ignoring SIGCHLD changes wait semantics which we cannot do.
            // 4. SIGPIPE must be handled outside this function.
            ignore_signal(signal)?;
        }
    }

    // Step 2: Real-time signals.
    for signum in libc::SIGRTMIN()..libc::SIGRTMAX() {
        // SAFETY: nix's signal does not support real-time signals.
        Errno::result(unsafe { libc::signal(signum, libc::SIG_IGN as libc::sighandler_t) })?;
    }

    Ok(())
}

/// Reset all signals to their default dispositions.
pub fn reset_signals() -> Result<(), Errno> {
    // Iterate through all possible signals and set them to be ignored.
    // Step 1: Normal signals.
    for signal in Signal::iterator() {
        if !matches!(signal, Signal::SIGKILL | Signal::SIGSTOP) {
            // Can not ignore SIGKILL and SIGSTOP.
            reset_signal(signal)?;
        }
    }

    // Step 2: Real-time signals.
    for signum in libc::SIGRTMIN()..libc::SIGRTMAX() {
        // SAFETY: nix's signal does not support real-time signals.
        Errno::result(unsafe { libc::signal(signum, libc::SIG_DFL as libc::sighandler_t) })?;
    }

    Ok(())
}

/// A sigset that can handle reserved signals.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct SydSigSet(pub u64);

impl BitOr for SydSigSet {
    type Output = Self;

    fn bitor(self, rhs: Self) -> Self::Output {
        SydSigSet(self.0 | rhs.0)
    }
}

impl BitOrAssign for SydSigSet {
    fn bitor_assign(&mut self, rhs: Self) {
        self.0 |= rhs.0;
    }
}

impl BitAnd for SydSigSet {
    type Output = Self;

    fn bitand(self, rhs: Self) -> Self::Output {
        SydSigSet(self.0 & rhs.0)
    }
}

impl BitAndAssign for SydSigSet {
    fn bitand_assign(&mut self, rhs: Self) {
        self.0 &= rhs.0;
    }
}

impl BitXor for SydSigSet {
    type Output = Self;

    fn bitxor(self, rhs: Self) -> Self::Output {
        SydSigSet(self.0 ^ rhs.0)
    }
}

impl BitXorAssign for SydSigSet {
    fn bitxor_assign(&mut self, rhs: Self) {
        self.0 ^= rhs.0;
    }
}

impl Not for SydSigSet {
    type Output = Self;

    fn not(self) -> Self::Output {
        SydSigSet(!self.0)
    }
}

impl Sub for SydSigSet {
    type Output = Self;

    fn sub(self, rhs: Self) -> Self::Output {
        SydSigSet(self.0 & !rhs.0)
    }
}

impl SubAssign for SydSigSet {
    fn sub_assign(&mut self, rhs: Self) {
        self.0 &= !rhs.0;
    }
}

impl std::fmt::Display for SydSigSet {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let max_signal = libc::SIGRTMAX();
        #[allow(clippy::cast_sign_loss)]
        let mut signals = Vec::with_capacity(max_signal as usize);

        for sig_num in 1..max_signal {
            if self.contains(sig_num) {
                signals.push(sig_num.to_string());
            }
        }

        if !signals.is_empty() {
            write!(f, "{}", signals.join(", "))
        } else {
            write!(f, "?")
        }
    }
}

impl SydSigSet {
    /// Create a new SydSigSet.
    pub fn new(mask: u64) -> Self {
        Self(mask)
    }

    /// Returns `true` if the set is empty.
    pub fn is_empty(&self) -> bool {
        self.0 == 0
    }

    /// Returns `true` if the set contains the given signal `sig`.
    #[allow(clippy::arithmetic_side_effects)]
    #[allow(clippy::cast_sign_loss)]
    pub fn contains(&self, sig: c_int) -> bool {
        if sig < 1 {
            return false;
        }
        let bit = (sig - 1) as u64;
        (self.0 & (1 << bit)) != 0
    }

    /// Returns `true` if this set intersects with the given set (i.e.,
    /// they share any common signals).
    pub fn intersects(&self, other: Self) -> bool {
        (self.0 & other.0) != 0
    }

    /// Add a signal to the set.
    #[allow(clippy::arithmetic_side_effects)]
    #[allow(clippy::cast_sign_loss)]
    pub fn add(&mut self, sig: c_int) {
        if sig < 1 {
            return; // ignore invalid signals
        }
        let bit = (sig - 1) as u64;
        self.0 |= 1 << bit;
    }

    /// Remove a signal from the set.
    #[allow(clippy::arithmetic_side_effects)]
    #[allow(clippy::cast_sign_loss)]
    pub fn del(&mut self, sig: c_int) {
        if sig < 1 {
            return; // ignore invalid signals
        }
        let bit = (sig - 1) as u64;
        self.0 &= !(1 << bit);
    }

    /// Add all signals from another SydSigSet to this one.
    pub fn add_set(&mut self, set: Self) {
        self.0 |= set.0;
    }

    /// Remove all signals present in `set` from `self`.
    pub fn del_set(&mut self, set: Self) {
        self.0 &= !set.0;
    }
}

impl Serialize for SydSigSet {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let max_signal = libc::SIGRTMAX();

        let mut signals = Vec::new();

        for sig_num in 1..max_signal {
            if self.contains(sig_num) {
                // SAFETY: strsignal returns a descriptive string for a
                // valid signal.  It should never return NULL for a
                // valid signal, but if it does, we fallback to the
                // number.
                let c_ptr = unsafe { libc::strsignal(sig_num) };
                if !c_ptr.is_null() {
                    // SAFETY: strsignal returned success,
                    // we have a valid c string.
                    let c_str = unsafe { CStr::from_ptr(c_ptr) };
                    signals.push(c_str.to_string_lossy().into_owned());
                } else {
                    signals.push(sig_num.to_string());
                }
            }
        }

        signals.serialize(serializer)
    }
}

const IOPRIO_CLASS_IDLE: i32 = 3;
const IOPRIO_WHO_PROCESS: i32 = 1;

/// Sets the I/O priority of the current thread to idle.
///
/// This function uses the `ioprio_set` syscall to set the I/O
/// scheduling priority of the current thread to the idle class. The
/// idle I/O class is designed for tasks that should only use disk
/// resources when no other process needs them. When a thread is set to
/// idle, it will not compete with other (non-idle) processes for I/O
/// bandwidth.
///
/// Note that this setting is applied at the thread level in Linux,
/// where each thread is treated as a separate scheduling entity. As a
/// result, calling this function will only affect the I/O priority of
/// the thread from which it is called. If the application is
/// multi-threaded and a global I/O priority change is desired, this
/// function needs to be called from each thread, or specific threads
/// requiring the priority change should be targeted.
///
/// The function does not require any parameters and returns a `Result`:
/// - `Ok(())` on success.
/// - `Err(Errno)` containing Errno.
///
/// # Safety
///
/// This function involves an unsafe block due to the direct system call
/// (`libc::syscall`). The `ioprio_set` syscall is considered
/// unsafe as it directly interfaces with the kernel, bypassing Rust's
/// safety guarantees.  However, the usage in this context is safe given
/// that:
/// - We are specifying `IOPRIO_WHO_PROCESS` with `0`, which correctly
///   targets the current thread.
/// - The `ioprio` value is correctly constructed for the idle I/O
///   class.
///
/// Users of this function do not need to take any special safety precautions.
pub(crate) fn set_io_priority_idle() -> Result<(), Errno> {
    // Set I/O priority: higher bits for the class, lower bits for the priority.
    // IOPRIO_CLASS_IDLE is shifted left by 13 bits to fit the class into higher bits.
    // Priority for idle class is not used, hence set to 0 (lower 13 bits).
    let ioprio = IOPRIO_CLASS_IDLE << 13;

    // SAFETY:
    // The syscall libc::SYS_ioprio_set is used to set the I/O priority
    // of a process. This call is considered unsafe because it involves
    // a direct system call, which bypasses the safety checks and
    // abstractions provided by Rust. However, this usage is safe under
    // the following conditions:
    // 1. The first argument IOPRIO_WHO_PROCESS specifies the target as
    //    a process.
    // 2. The second argument 0 refers to the current process. In the
    //    context of ioprio_set, passing 0 for the 'who' parameter
    //    targets the calling process. This is why getpid() is not
    //    necessary here, as 0 implicitly represents the current
    //    process's PID.
    // 3. The third argument ioprio is correctly constructed with a
    //    valid I/O class and priority, ensuring the syscall behaves as
    //    expected.
    Errno::result(unsafe { libc::syscall(libc::SYS_ioprio_set, IOPRIO_WHO_PROCESS, 0, ioprio) })
        .map(drop)
}

/// Set the current thread's CPU scheduling policy to 'idle'.
///
/// This function sets the CPU scheduling policy of the current thread
/// to SCHED_IDLE, indicating that the thread should only be scheduled
/// to run when the system is idle.
///
/// # Returns
///
/// * `Ok(())` on successful setting of the scheduling policy and priority.
/// * `Err` on failure, with the specific error indicating the cause of the failure.
pub(crate) fn set_cpu_priority_idle() -> Result<(), Errno> {
    // SAFETY: We zero out the sched_param struct. This is safe because:
    // 1. sched_param is a plain data struct with no invariants related
    //    to its fields.
    // 2. All-zero is a valid representation for this struct in the
    //    context of SCHED_IDLE policy.
    let param: libc::sched_param = unsafe { std::mem::zeroed() };

    // SAFETY: The call to libc::sched_setscheduler is safe because:
    // 1. We are passing valid arguments: a PID of 0 for the current
    //    thread, a valid policy (SCHED_IDLE), and a pointer to a
    //    properly initialized sched_param structure.
    // 2. There are no thread-safety issues since the operation only
    //    affects the current thread.
    Errno::result(unsafe {
        libc::sched_setscheduler(0, libc::SCHED_IDLE, std::ptr::addr_of!(param))
    })
    .map(drop)
}

const SECCOMP_ARCH_LIST: &[ScmpArch] = &[
    ScmpArch::X86,
    ScmpArch::X8664,
    ScmpArch::X32,
    ScmpArch::Arm,
    ScmpArch::Aarch64,
    ScmpArch::Loongarch64,
    ScmpArch::M68k,
    ScmpArch::Mips,
    ScmpArch::Mips64,
    ScmpArch::Mips64N32,
    ScmpArch::Mipsel,
    ScmpArch::Mipsel64,
    ScmpArch::Mipsel64N32,
    ScmpArch::Ppc,
    ScmpArch::Ppc64,
    ScmpArch::Ppc64Le,
    ScmpArch::S390,
    ScmpArch::S390X,
    ScmpArch::Parisc,
    ScmpArch::Parisc64,
    ScmpArch::Riscv64,
    ScmpArch::Sheb,
    ScmpArch::Sh,
];

/// Print list of libseccomp's supported architectures
/// Used by `syd --arch list`
pub fn print_seccomp_architectures() {
    let native = ScmpArch::native();
    for arch in SECCOMP_ARCH_LIST {
        let mut repr = format!("{arch:?}").to_ascii_lowercase();
        if repr == "x8664" {
            // Fix potential confusion.
            repr = "x86_64".to_string();
        }
        if *arch == native {
            println!("- {repr} [*]")
        } else {
            println!("- {repr}");
        }
    }
}

// List of libseccomp supported architectures for the current system.
#[cfg(target_arch = "x86_64")]
pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::X8664, ScmpArch::X86, ScmpArch::X32];
#[cfg(target_arch = "x86")]
pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::X86];
#[cfg(target_arch = "arm")]
pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::Arm];
#[cfg(target_arch = "aarch64")]
pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::Aarch64, ScmpArch::Arm];
#[cfg(all(target_arch = "mips", target_endian = "big"))]
pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::Mips];
#[cfg(all(target_arch = "mips", target_endian = "little"))]
pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::Mipsel];
#[cfg(all(target_arch = "mips32r6", target_endian = "big"))]
pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::Mips];
#[cfg(all(target_arch = "mips32r6", target_endian = "little"))]
pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::Mipsel];
#[cfg(all(target_arch = "mips64", target_endian = "big"))]
pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::Mips64, ScmpArch::Mips64N32];
#[cfg(all(target_arch = "mips64", target_endian = "little"))]
pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::Mipsel64, ScmpArch::Mipsel64N32];
#[cfg(all(target_arch = "mips64r6", target_endian = "big"))]
pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::Mips64, ScmpArch::Mips64N32];
#[cfg(all(target_arch = "mips64r6", target_endian = "little"))]
pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::Mipsel64, ScmpArch::Mipsel64N32];
#[cfg(target_arch = "powerpc")]
pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::Ppc];
#[cfg(all(target_arch = "powerpc64", target_endian = "big"))]
pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::Ppc64];
#[cfg(all(target_arch = "powerpc64", target_endian = "little"))]
pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::Ppc64Le];
//#[cfg(target_arch = "parisc")]
//pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::Parisc];
//#[cfg(target_arch = "parisc64")]
//pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::Parisc64, ScmpArch::Parisc];
#[cfg(target_arch = "riscv64")]
pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::Riscv64];
#[cfg(target_arch = "s390x")]
pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::S390X, ScmpArch::S390];
#[cfg(target_arch = "loongarch64")]
pub(crate) const SCMP_ARCH: &[ScmpArch] = &[ScmpArch::Loongarch64];

/// Return true if native architecture has the multiplexed socketcall system call.
/// Panics if it cannot determine the native architecture.
pub fn seccomp_native_has_socketcall() -> bool {
    matches!(
        ScmpArch::native(),
        ScmpArch::X86
            | ScmpArch::Mips
            | ScmpArch::Mipsel
            | ScmpArch::Ppc
            | ScmpArch::Ppc64
            | ScmpArch::Ppc64Le
            | ScmpArch::S390
            | ScmpArch::S390X
    )
}

/// Add all supported architectures to the given filter.
pub fn seccomp_add_architectures(ctx: &mut ScmpFilterContext) -> SydResult<()> {
    // Add architectures based on the current architecture
    for arch in SCMP_ARCH {
        seccomp_add_arch(ctx, *arch)?;
    }
    Ok(())
}

fn seccomp_add_arch(ctx: &mut ScmpFilterContext, arch: ScmpArch) -> SydResult<()> {
    Ok(ctx.add_arch(arch).map(drop)?)
}

/// Check if arch is 64-bit or 32-bit.
#[inline]
pub const fn scmp_arch_bits(arch: ScmpArch) -> usize {
    match arch {
        ScmpArch::X8664
        | ScmpArch::Aarch64
        | ScmpArch::Loongarch64
        | ScmpArch::Mips64
        | ScmpArch::Mips64N32
        | ScmpArch::Mipsel64
        | ScmpArch::Mipsel64N32
        | ScmpArch::Ppc64
        | ScmpArch::Ppc64Le
        | ScmpArch::Parisc64
        | ScmpArch::Riscv64
        | ScmpArch::S390X => 64,
        ScmpArch::X86
        | ScmpArch::X32
        | ScmpArch::Arm
        | ScmpArch::M68k
        | ScmpArch::Mips
        | ScmpArch::Mipsel
        | ScmpArch::Ppc
        | ScmpArch::Parisc
        | ScmpArch::S390
        | ScmpArch::Sheb
        | ScmpArch::Sh => 32,
        _ => 64, // sane default for non-exhaustive enum.
    }
}

/// Helper function to determine if the architecture is big-endian.
#[inline]
pub fn scmp_big_endian(arch: ScmpArch) -> bool {
    matches!(
        arch,
        ScmpArch::Mips
            | ScmpArch::Mips64
            | ScmpArch::Ppc
            | ScmpArch::Ppc64
            | ScmpArch::S390
            | ScmpArch::S390X
            | ScmpArch::Parisc
            | ScmpArch::Parisc64
    )
}

/// Represents seccomp notify data.
/// We redefine this because libseccomp struct is non-exhaustive.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct ScmpNotifData {
    pub(crate) syscall: ScmpSyscall,
    pub(crate) arch: ScmpArch,
    pub(crate) instr_pointer: u64,
    pub(crate) args: [u64; 6],
}

/// Represents a seccomp notify request.
/// We redefine this because libseccomp struct is non-exhaustive.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ScmpNotifReq {
    pub(crate) id: u64,
    pub(crate) pid: u32,
    pub(crate) flags: u32,
    pub(crate) data: ScmpNotifData,
}

impl ScmpNotifData {
    fn from_sys(data: libc::seccomp_data) -> Result<Self, Errno> {
        Ok(Self {
            syscall: ScmpSyscall::from(data.nr),
            arch: scmp_arch(data.arch)?,
            instr_pointer: data.instruction_pointer,
            args: data.args,
        })
    }
}

impl ScmpNotifReq {
    pub(crate) fn from_sys(req: libc::seccomp_notif) -> Result<Self, Errno> {
        Ok(Self {
            id: req.id,
            pid: req.pid,
            flags: req.flags,
            data: ScmpNotifData::from_sys(req.data)?,
        })
    }

    #[inline(always)]
    pub(crate) fn pid(&self) -> Pid {
        #[allow(clippy::cast_possible_wrap)]
        Pid::from_raw(self.pid as libc::pid_t)
    }
}

/// Helper function to convert raw arch value to ScmpArch.
///
/// We need this because ScmpArch::from_sys is not imported.
pub const fn scmp_arch(arch: u32) -> Result<ScmpArch, Errno> {
    match arch {
        libseccomp_sys::SCMP_ARCH_NATIVE => Ok(ScmpArch::Native),
        libseccomp_sys::SCMP_ARCH_X86 => Ok(ScmpArch::X86),
        libseccomp_sys::SCMP_ARCH_X86_64 => Ok(ScmpArch::X8664),
        libseccomp_sys::SCMP_ARCH_X32 => Ok(ScmpArch::X32),
        libseccomp_sys::SCMP_ARCH_ARM => Ok(ScmpArch::Arm),
        libseccomp_sys::SCMP_ARCH_AARCH64 => Ok(ScmpArch::Aarch64),
        libseccomp_sys::SCMP_ARCH_LOONGARCH64 => Ok(ScmpArch::Loongarch64),
        libseccomp_sys::SCMP_ARCH_M68K => Ok(ScmpArch::M68k),
        libseccomp_sys::SCMP_ARCH_MIPS => Ok(ScmpArch::Mips),
        libseccomp_sys::SCMP_ARCH_MIPS64 => Ok(ScmpArch::Mips64),
        libseccomp_sys::SCMP_ARCH_MIPS64N32 => Ok(ScmpArch::Mips64N32),
        libseccomp_sys::SCMP_ARCH_MIPSEL => Ok(ScmpArch::Mipsel),
        libseccomp_sys::SCMP_ARCH_MIPSEL64 => Ok(ScmpArch::Mipsel64),
        libseccomp_sys::SCMP_ARCH_MIPSEL64N32 => Ok(ScmpArch::Mipsel64N32),
        libseccomp_sys::SCMP_ARCH_PPC => Ok(ScmpArch::Ppc),
        libseccomp_sys::SCMP_ARCH_PPC64 => Ok(ScmpArch::Ppc64),
        libseccomp_sys::SCMP_ARCH_PPC64LE => Ok(ScmpArch::Ppc64Le),
        libseccomp_sys::SCMP_ARCH_S390 => Ok(ScmpArch::S390),
        libseccomp_sys::SCMP_ARCH_S390X => Ok(ScmpArch::S390X),
        libseccomp_sys::SCMP_ARCH_PARISC => Ok(ScmpArch::Parisc),
        libseccomp_sys::SCMP_ARCH_PARISC64 => Ok(ScmpArch::Parisc64),
        libseccomp_sys::SCMP_ARCH_RISCV64 => Ok(ScmpArch::Riscv64),
        libseccomp_sys::SCMP_ARCH_SHEB => Ok(ScmpArch::Sheb),
        libseccomp_sys::SCMP_ARCH_SH => Ok(ScmpArch::Sh),
        _ => Err(Errno::ENOSYS),
    }
}

/// Helper function to convert ScmpArch to raw arch values.
///
/// We need this because ScmpArch::from_sys is not imported.
/// This function panics on invalid/unsupported architecture.
pub const fn scmp_arch_raw(arch: ScmpArch) -> u32 {
    match arch {
        ScmpArch::Native => libseccomp_sys::SCMP_ARCH_NATIVE,
        ScmpArch::X86 => libseccomp_sys::SCMP_ARCH_X86,
        ScmpArch::X8664 => libseccomp_sys::SCMP_ARCH_X86_64,
        ScmpArch::X32 => libseccomp_sys::SCMP_ARCH_X32,
        ScmpArch::Arm => libseccomp_sys::SCMP_ARCH_ARM,
        ScmpArch::Aarch64 => libseccomp_sys::SCMP_ARCH_AARCH64,
        ScmpArch::Loongarch64 => libseccomp_sys::SCMP_ARCH_LOONGARCH64,
        ScmpArch::M68k => libseccomp_sys::SCMP_ARCH_M68K,
        ScmpArch::Mips => libseccomp_sys::SCMP_ARCH_MIPS,
        ScmpArch::Mips64 => libseccomp_sys::SCMP_ARCH_MIPS64,
        ScmpArch::Mips64N32 => libseccomp_sys::SCMP_ARCH_MIPS64N32,
        ScmpArch::Mipsel => libseccomp_sys::SCMP_ARCH_MIPSEL,
        ScmpArch::Mipsel64 => libseccomp_sys::SCMP_ARCH_MIPSEL64,
        ScmpArch::Mipsel64N32 => libseccomp_sys::SCMP_ARCH_MIPSEL64N32,
        ScmpArch::Ppc => libseccomp_sys::SCMP_ARCH_PPC,
        ScmpArch::Ppc64 => libseccomp_sys::SCMP_ARCH_PPC64,
        ScmpArch::Ppc64Le => libseccomp_sys::SCMP_ARCH_PPC64LE,
        ScmpArch::S390 => libseccomp_sys::SCMP_ARCH_S390,
        ScmpArch::S390X => libseccomp_sys::SCMP_ARCH_S390X,
        ScmpArch::Parisc => libseccomp_sys::SCMP_ARCH_PARISC,
        ScmpArch::Parisc64 => libseccomp_sys::SCMP_ARCH_PARISC64,
        ScmpArch::Riscv64 => libseccomp_sys::SCMP_ARCH_RISCV64,
        ScmpArch::Sheb => libseccomp_sys::SCMP_ARCH_SHEB,
        ScmpArch::Sh => libseccomp_sys::SCMP_ARCH_SH,
    }
}

/// CLONE_NEWTIME constant to create time namespaces.
pub const CLONE_NEWTIME: libc::c_int = 128;

pub(crate) const NAMESPACE_FLAGS: &[libc::c_int] = &[
    libc::CLONE_NEWNS,
    libc::CLONE_NEWIPC,
    libc::CLONE_NEWNET,
    libc::CLONE_NEWPID,
    libc::CLONE_NEWUTS,
    libc::CLONE_NEWUSER,
    libc::CLONE_NEWCGROUP,
    CLONE_NEWTIME,
];

pub(crate) const NAMESPACE_FLAGS_ALL: libc::c_int = libc::CLONE_NEWNS
    | libc::CLONE_NEWIPC
    | libc::CLONE_NEWNET
    | libc::CLONE_NEWPID
    | libc::CLONE_NEWUTS
    | libc::CLONE_NEWUSER
    | libc::CLONE_NEWCGROUP
    | CLONE_NEWTIME;

pub(crate) const NAMESPACE_NAMES: &[&str] = &[
    "mount", "ipc", "net", "pid", "uts", "user", "cgroup", "time",
];

/// Convert CLONE namespace flags to a Vector of Strings.
pub fn nsflags_name(flags: libc::c_int) -> Vec<String> {
    let mut names = Vec::with_capacity(NAMESPACE_FLAGS.len());
    for &flag in NAMESPACE_FLAGS {
        if flags & flag != 0 {
            names.push(nsflag_name(flag));
        }
    }

    names
}

/// Convert a CLONE namespace flag to its String representation.
pub fn nsflag_name(flag: libc::c_int) -> String {
    match flag {
        libc::CLONE_NEWNS => "mount",
        libc::CLONE_NEWIPC => "ipc",
        libc::CLONE_NEWNET => "net",
        libc::CLONE_NEWPID => "pid",
        libc::CLONE_NEWUTS => "uts",
        libc::CLONE_NEWUSER => "user",
        libc::CLONE_NEWCGROUP => "cgroup",
        CLONE_NEWTIME => "time",
        _ => "?",
    }
    .to_string()
}

/// A helper function to wrap the operations and reduce duplication.
#[allow(clippy::arithmetic_side_effects)]
#[allow(clippy::disallowed_methods)]
pub fn landlock_operation(
    abi: ABI,
    path_ro: &[XPathBuf],
    path_rw: &[XPathBuf],
    port_bind: &[RangeInclusive<u16>],
    port_conn: &[RangeInclusive<u16>],
    scoped_abs: bool,
    scoped_sig: bool,
) -> Result<RestrictionStatus, RulesetError> {
    // from_all includes IoctlDev of ABI >= 5 as necessary.
    let mut ruleset = Ruleset::default().handle_access(AccessFs::from_all(abi))?;
    let ruleset_ref = &mut ruleset;

    // Network is ABI >= 4.
    let mut network_rules_bind: HashSet<u16> = HashSet::new();
    for port_range in port_bind {
        for port in port_range.clone() {
            network_rules_bind.insert(port);
        }
    }
    if network_rules_bind.len() <= usize::from(u16::MAX) + 1 {
        ruleset_ref.handle_access(AccessNet::BindTcp)?;
    } else {
        // SAFETY: All ports are allowed, do not handle capability,
        // rather than allowing each and every port.
        network_rules_bind.clear();
    }

    let mut network_rules_conn: HashSet<u16> = HashSet::new();
    for port_range in port_conn {
        for port in port_range.clone() {
            network_rules_conn.insert(port);
        }
    }
    if network_rules_conn.len() <= usize::from(u16::MAX) + 1 {
        ruleset_ref.handle_access(AccessNet::ConnectTcp)?;
    } else {
        // SAFETY: All ports are allowed, do not handle capability,
        // rather than allowing each and every port.
        network_rules_conn.clear();
    }

    // Scopes are ABI >= 6.
    if scoped_abs {
        ruleset_ref.handle_access(ScopeFlag::AbstractUnixSocket)?;
    }
    if scoped_sig {
        ruleset_ref.handle_access(ScopeFlag::Signal)?;
    }

    ruleset
        .create()?
        .add_rules(path_beneath_rules(path_ro, AccessFs::from_read(abi)))?
        .add_rules(path_beneath_rules(path_rw, AccessFs::from_all(abi)))?
        .add_rules(
            network_rules_bind
                .into_iter()
                .map(|port| Ok::<NetPort, RulesetError>(NetPort::new(port, AccessNet::BindTcp))),
        )?
        .add_rules(
            network_rules_conn
                .into_iter()
                .map(|port| Ok::<NetPort, RulesetError>(NetPort::new(port, AccessNet::ConnectTcp))),
        )?
        .restrict_self()
}

/// Simple human size formatter.
#[allow(clippy::arithmetic_side_effects)]
#[allow(clippy::cast_precision_loss)]
pub fn human_size(bytes: usize) -> String {
    const SIZES: &[char] = &['B', 'K', 'M', 'G', 'T', 'P', 'E'];
    let factor = 1024usize;

    let mut size = bytes as f64;
    let mut i = 0;

    while size > factor as f64 && i < SIZES.len() - 1 {
        size /= factor as f64;
        i += 1;
    }

    format!("{:.2}{}", size, SIZES[i])
}

#[cfg(target_env = "musl")]
pub(crate) type IoctlRequest = libc::c_int;
#[cfg(not(target_env = "musl"))]
pub(crate) type IoctlRequest = libc::c_ulong;

const SIOCGIFFLAGS: IoctlRequest = libc::SIOCGIFFLAGS as IoctlRequest;
const SIOCSIFFLAGS: IoctlRequest = libc::SIOCSIFFLAGS as IoctlRequest;

/// Functionally equivalent to "ifconfig lo up".
pub fn bring_up_loopback() -> Result<(), Errno> {
    // Create a socket
    let sock = socket(
        AddressFamily::Inet,
        SockType::Stream,
        SockFlag::empty(),
        None,
    )?;

    // Prepare the interface request
    let mut ifreq = libc::ifreq {
        #[allow(clippy::cast_possible_wrap)]
        ifr_name: [
            b'l' as libc::c_char,
            b'o' as libc::c_char,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
        ],
        // SAFETY: Manually initialize ifr_ifru
        ifr_ifru: unsafe { std::mem::zeroed() },
    };

    // SAFETY: Get the current flags
    if unsafe { libc::ioctl(sock.as_raw_fd(), SIOCGIFFLAGS, &mut ifreq) } != 0 {
        return Err(Errno::last());
    }

    // Modify the flags to bring up the interface
    // SAFETY: We're accessing the field of a union here.
    #[allow(clippy::cast_possible_truncation)]
    unsafe {
        ifreq.ifr_ifru.ifru_flags |= (libc::IFF_UP | libc::IFF_RUNNING) as libc::c_short
    };

    // SAFETY: Set the new flags
    if unsafe { libc::ioctl(sock.as_raw_fd(), SIOCSIFFLAGS, &mut ifreq) } == 0 {
        Ok(())
    } else {
        Err(Errno::last())
    }
}

const DEV_NULL_MAJOR: u32 = 1;
const DEV_NULL_MINOR: u32 = 3;

// Validate fd is indeed `/dev/null'.
pub(crate) fn validate_dev_null(fd: RawFd) -> Result<(), Errno> {
    // SAFETY: fd must be valid FD, let's trust the caller here.
    let fd = unsafe { BorrowedFd::borrow_raw(fd) };

    let statx = fstatx(&fd, STATX_BASIC_STATS)?;

    // Basic checks: Is it a character device and does it match /dev/null?
    // Check file mode: stx_mode includes the file type bits.
    // Character devices are indicated by S_IFCHR (0o020000).
    #[allow(clippy::cast_possible_truncation)]
    const S_IFCHR: u16 = libc::S_IFCHR as u16;
    if statx.stx_mode & S_IFCHR != S_IFCHR {
        return Err(Errno::EINVAL);
    }

    // Verify the device major and minor numbers.
    // For /dev/null: major=1, minor=3 are canonical.
    // We check stx_rdev_major and stx_rdev_minor as per struct statx fields.
    if statx.stx_rdev_major != DEV_NULL_MAJOR || statx.stx_rdev_minor != DEV_NULL_MINOR {
        return Err(Errno::EINVAL);
    }

    Ok(())
}

/// Check for file descriptor leaks above the standard input, output, and error.
///
/// This function examines the `/proc/self/fd` directory to identify
/// open file descriptors. It prints any open file descriptors other
/// than the standard input (0), output (1), and error (2), indicating
/// potential resource leaks.
///
/// # Parameters
/// - `fd_max`: An optional parameter that sets a maximum file
///   descriptor number to check. If not specified, only the standard
///   file descriptors are considered normal.
///
/// # Returns
/// Returns `true` if leaks are found, otherwise `false`.
pub fn check_fd_leaks(fd_max: Option<RawFd>) -> u32 {
    let proc_fd_path = Path::new("/proc/self/fd");
    let mut dir = match Dir::open(proc_fd_path, OFlag::O_RDONLY, Mode::empty()) {
        Ok(d) => d,
        Err(e) => {
            eprintln!("Failed to open /proc/self/fd: {e}");
            return u32::MAX;
        }
    };

    let mut leaks_found: u32 = 0;
    let dir_fd = dir.as_raw_fd();
    let fd_limit = fd_max.unwrap_or(2); // Default limit only std fds

    for entry in dir.iter() {
        let entry = match entry {
            Ok(e) => e,
            Err(_) => continue,
        };

        let fd_str = entry.file_name().to_string_lossy(); // Use lossy conversion
        let fd = match fd_str.parse::<RawFd>() {
            Ok(fd) => fd,
            Err(_) => continue,
        };

        // Ignore standard file descriptors and the directory stream FD itself
        if fd <= fd_limit || fd == dir_fd {
            continue;
        }

        // Create a PathBuf from the string representation of the file descriptor
        let link_path = proc_fd_path.join(fd_str.into_owned()); // Convert Cow<str> into a String and then into a PathBuf

        match std::fs::read_link(&link_path) {
            Ok(target_path) => {
                eprintln!("!!! Leaked file descriptor {fd} -> {target_path:?} !!!");
                leaks_found = leaks_found.saturating_add(1);
            }
            Err(error) => {
                eprintln!("Failed to read link for FD {fd}: {error}");
            }
        }
    }

    leaks_found
}

/// Parse the given string into a UID.
/// 1. use getpwnam_r(3)
/// 2. parse as integer
pub(crate) fn parse_user(name: &str) -> Result<Uid, Errno> {
    if name.chars().all(|c| c.is_ascii_digit()) {
        Ok(Uid::from_raw(
            name.parse::<libc::uid_t>().or(Err(Errno::EINVAL))?,
        ))
    } else if let Some(user) = User::from_name(name)? {
        Ok(user.uid)
    } else {
        Err(Errno::ENOENT)
    }
}

/// Parse the given string into a GID.
/// 1. use getpwnam_r(3)
/// 2. parse as integer
pub(crate) fn parse_group(name: &str) -> Result<Gid, Errno> {
    if name.chars().all(|c| c.is_ascii_digit()) {
        Ok(Gid::from_raw(
            name.parse::<libc::gid_t>().or(Err(Errno::EINVAL))?,
        ))
    } else if let Some(group) = Group::from_name(name)? {
        Ok(group.gid)
    } else {
        Err(Errno::ENOENT)
    }
}

/// Extends the ioctl value if necessary.
///
/// In musl, ioctl is defined as:
/// `int ioctl(int fd, int req, ...);`
///
/// In glibc, ioctl is defined as:
/// `int ioctl(int fd, unsigned long request, ...);`
///
/// This difference can cause issues when handling ioctl values that are
/// larger than what a signed 32-bit integer can represent.
/// Specifically, values with the high bit set (0x80000000) or the next
/// highest bit set (0x40000000) can be interpreted differently
/// depending on the implementation.
///
/// In a 32-bit signed integer, the high bit (0x80000000) is used as the
/// sign bit, indicating whether the number is positive or negative. If
/// this bit is set, the number is interpreted as negative. The next
/// highest bit (0x40000000) is the largest value that a signed 32-bit
/// integer can represent without becoming negative.
///
/// Therefore, ioctl values that have either of these bits set can cause
/// compatibility issues between musl and glibc. To ensure
/// compatibility, we need to extend such ioctl values to 64 bits by
/// prefixing them with `0xffffffff`, converting them to their unsigned
/// representation.
///
/// # Arguments
///
/// * `value` - The original ioctl value.
///
/// # Returns
///
/// * `Some(extended_value)` - If the value requires extension.
/// * `None` - If the value does not require extension.
#[inline]
pub fn extend_ioctl(value: u64) -> Option<u64> {
    // Check if the high bit (0x80000000) or the next highest bit
    // (0x40000000) is set.  These bits can cause the value to be
    // interpreted as a negative number in a signed 32-bit context.
    if (value & 0x80000000 == 0x80000000) || (value & 0x40000000 == 0x40000000) {
        // If the value requires extension, return the extended value by
        // prefixing with `0xffffffff`.
        Some(0xffffffff00000000 | value)
    } else {
        // If the value does not require extension, return None.
        None
    }
}

/// Drop a Capability from the Effective, Ambient, Inheritable and Permitted capsets.
pub fn safe_drop_cap(cap: caps::Capability) -> Result<(), caps::errors::CapsError> {
    caps::drop(None, caps::CapSet::Effective, cap)?;
    caps::drop(None, caps::CapSet::Ambient, cap)?;
    caps::drop(None, caps::CapSet::Inheritable, cap)?;
    caps::drop(None, caps::CapSet::Permitted, cap)
}

/// Return true if the given signal has default action Core.
#[inline]
#[allow(unreachable_patterns)]
pub(crate) fn is_coredump(sig: i32) -> bool {
    matches!(
        sig,
        libc::SIGABRT
            | libc::SIGBUS
            | libc::SIGFPE
            | libc::SIGILL
            | libc::SIGIOT
            | libc::SIGKILL
            | libc::SIGQUIT
            | libc::SIGSEGV
            | libc::SIGSYS
            | libc::SIGTRAP
            | libc::SIGXCPU
            | libc::SIGXFSZ
    )
}

#[cfg(target_arch = "x86")]
#[inline(always)]
/// Fork fast.
///
/// # Safety
///
/// Unsafe to be fast!
pub unsafe fn fork_fast() {
    asm!(
        "mov eax, 0x2", // 0x2 is the syscall number for fork on x86
        "int 0x80",     // Interrupt to make the syscall
        out("eax") _,
    );
}

#[cfg(target_arch = "x86_64")]
#[inline(always)]
/// Fork fast.
///
/// # Safety
///
/// Unsafe to be fast!
pub unsafe fn fork_fast() {
    // Inline assembly for x86-64
    asm!(
        "mov rax, 57", // 57 is the syscall number for fork on x86-64
        "syscall",
        out("rax") _,
    );
}

#[cfg(target_arch = "aarch64")]
#[inline(always)]
/// Fork fast.
///
/// # Safety
///
/// Unsafe to be fast!
pub unsafe fn fork_fast() {
    asm!(
        "mov x0, 17",  // SIGCHLD
        "mov x1, 0",   // child_stack (null, not recommended)
        "mov x8, 220", // syscall number for clone
        "svc 0",
        options(nostack),
    );
}

#[cfg(target_arch = "arm")]
#[inline(always)]
/// Fork fast.
///
/// # Safety
///
/// Unsafe to be fast!
pub unsafe fn fork_fast() {
    asm!(
        "mov r7, #2", // 2 is the syscall number for fork on ARM
        "swi #0",     // Software interrupt to make the syscall
        out("r0") _,
        options(nostack),
    );
}

/*
 * error[E0658]: inline assembly is not stable yet on this architecture
#[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))]
#[inline(always)]
/// Fork fast.
///
/// # Safety
///
/// Unsafe to be fast!
pub unsafe fn fork_fast() {
    asm!(
        "li 0, 2",     // Load immediate 2 into register r0 (syscall number for fork)
        "sc",          // System call
        out("r3") _,   // Output from r3 (return value of fork)
    );
}
*/

#[cfg(target_arch = "riscv64")]
#[inline(always)]
/// Fork fast.
///
/// # Safety
///
/// Unsafe to be fast!
pub unsafe fn fork_fast() {
    asm!(
        "li a7, 220",  // syscall number for clone on riscv64
        "li a0, 17",   // SIGCHLD
        "li a1, 0",    // child_stack (null, not recommended)
        "ecall",       // make the syscall
        out("a0") _,   // store return value in a0
        options(nostack),
    );
}

/*
 * error[E0658]: inline assembly is not stable yet on this architecture
#[cfg(any(target_arch = "s390x"))]
#[inline(always)]
/// Fork fast.
///
/// # Safety
///
/// Unsafe to be fast!
pub unsafe fn fork_fast() {
    asm!(
        "lgr %r1, 2", // Load syscall number for fork (2) directly into %r1.
        "svc 0",      // Supervisor Call to invoke the syscall.
    );
}
*/

#[cfg(any(
    target_arch = "powerpc",
    target_arch = "powerpc64",
    target_arch = "s390x"
))]
#[inline(always)]
/// Fork fast.
///
/// # Safety
///
/// Unsafe to be fast!
pub unsafe fn fork_fast() {
    let _ = libc::syscall(libc::SYS_fork);
}

#[cfg(not(any(
    target_arch = "aarch64",
    target_arch = "arm",
    target_arch = "powerpc",
    target_arch = "powerpc64",
    target_arch = "riscv64",
    target_arch = "riscv64",
    target_arch = "s390x",
    target_arch = "x86",
    target_arch = "x86_64",
)))]
#[inline(always)]
/// Fork fast.
///
/// # Safety
///
/// Unsafe to be fast!
pub unsafe fn fork_fast() {
    let _ = fork();
}

/// Set SIGPIPE handler to default.
pub fn set_sigpipe_dfl() -> Result<(), Errno> {
    // SAFETY: The nix::sys::signal::signal function is unsafe because
    // it affects the global state of the program by changing how a
    // signal (SIGPIPE in this case) is handled. It's safe to call here
    // because changing the SIGPIPE signal to its default behavior will
    // not interfere with any other part of this program that could be
    // relying on a custom SIGPIPE signal handler.
    unsafe { signal(Signal::SIGPIPE, SigHandler::SigDfl) }.map(drop)
}

#[allow(dead_code)]
#[inline]
#[cold]
fn cold() {}

#[allow(dead_code)]
#[inline]
pub(crate) fn likely(b: bool) -> bool {
    if !b {
        cold()
    }
    b
}

#[allow(dead_code)]
#[inline]
pub(crate) fn unlikely(b: bool) -> bool {
    if b {
        cold()
    }
    b
}

/// Write the message to the invalid fd -42.
/// The idea is to look for it in strace logs.
/// Only works in debug mode, noop in release.
pub fn t(msg: &str) {
    let buf = msg.as_bytes();
    let len = buf.len() as libc::size_t;
    // SAFETY: writing to an invalid fd.
    unsafe { libc::syscall(libc::SYS_write, -31415, buf.as_ptr(), len) };
}

/// Write a formatted message to an invalid fd.
#[macro_export]
macro_rules! t {
    ($($arg:tt)*) => {{
        syd::t(&format!($($arg)*));
    }}
}

/// Write a formatted message to an invalid fd.
#[macro_export]
macro_rules! T {
    ($($arg:tt)*) => {{
        $crate::t(&format!($($arg)*));
    }}
}

#[cfg(feature = "prof")]
#[inline(always)]
#[allow(dead_code)]
pub(crate) fn start_cpu_profile(name: &str) {
    gperftools::profiler::PROFILER
        .lock()
        .expect("lock profiler")
        .start(format!("./syd-cpu-{name}.pprof"))
        .expect("start profiler");
}

#[cfg(not(feature = "prof"))]
#[inline(always)]
#[allow(dead_code)]
pub(crate) fn start_cpu_profile(_name: &str) {}

#[cfg(feature = "prof")]
#[inline(always)]
#[allow(dead_code)]
pub(crate) fn stop_cpu_profile() {
    gperftools::profiler::PROFILER
        .lock()
        .expect("lock profiler")
        .stop()
        .expect("stop profiler");
}

#[cfg(not(feature = "prof"))]
#[inline(always)]
#[allow(dead_code)]
pub(crate) fn stop_cpu_profile() {}

#[cfg(feature = "prof")]
#[inline(always)]
#[allow(dead_code)]
pub(crate) fn start_mem_profile(name: &str) {
    gperftools::heap_profiler::HEAP_PROFILER
        .lock()
        .expect("lock profiler")
        .start(format!("./syd-mem-{name}"))
        .expect("start profiler");
}

#[cfg(not(feature = "prof"))]
#[inline(always)]
#[allow(dead_code)]
pub(crate) fn start_mem_profile(_name: &str) {}

#[cfg(feature = "prof")]
#[inline(always)]
#[allow(dead_code)]
pub(crate) fn dump_mem_profile(name: &str) {
    gperftools::heap_profiler::HEAP_PROFILER
        .lock()
        .expect("lock profiler")
        .dump(format!("./syd-mem-{name}"))
        .expect("dump profiler");
}

#[cfg(not(feature = "prof"))]
#[inline(always)]
#[allow(dead_code)]
pub(crate) fn dump_mem_profile(_name: &str) {}

#[cfg(feature = "prof")]
#[inline(always)]
#[allow(dead_code)]
pub(crate) fn stop_mem_profile() {
    gperftools::heap_profiler::HEAP_PROFILER
        .lock()
        .expect("lock profiler")
        .stop()
        .expect("stop profiler");
}

#[cfg(not(feature = "prof"))]
#[inline(always)]
#[allow(dead_code)]
pub(crate) fn stop_mem_profile() {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_extend_ioctl() {
        const IOCTLS: &[(u64, bool)] = &[
            (0x5451, false),
            (0x5450, false),
            (0x541B, false),
            (0x5421, false),
            (0x5452, false),
            (0x4B66, false),
            (0x5401, false),
            (0x5402, false),
            (0x5403, false),
            (0x5404, false),
            (0x5405, false),
            (0x5406, false),
            (0x5407, false),
            (0x5408, false),
            (0x5456, false),
            (0x5457, false),
            (0x5413, false),
            (0x5414, false),
            (0x5409, false),
            (0x5425, false),
            (0x5427, false),
            (0x5428, false),
            (0x540A, false),
            (0x5411, false),
            (0x540B, false),
            (0x80045430, true),
            (0x80045432, true),
            (0x5432, false),
            (0x5433, false),
            (0x5434, false),
            (0x5435, false),
            (0x40045436, true),
            (0x5437, false),
            (0x80045438, true),
            (0x80045439, true),
            (0x80045440, true),
            (0x5441, false),
            (0x540E, false),
            (0x540F, false),
            (0x5410, false),
            (0x5429, false),
            (0x540C, false),
            (0x80045440, true),
            (0x540D, false),
            (0x5424, false),
            (0x5423, false),
            (0x5420, false),
            (0x80045438, true),
            (0x40045431, true),
            (0x80045439, true),
            (0x5441, false),
            (0x80086601, true),
            (0x5419, false),
            (0x541A, false),
            (0x8910, false),
            (0x8912, false),
            (0x8913, false),
            (0x8915, false),
            (0x8917, false),
            (0x8919, false),
            (0x891b, false),
            (0x891d, false),
            (0x891f, false),
            (0x892, false),
            (0x8925, false),
            (0x8927, false),
            (0x8929, false),
            (0x8933, false),
            (0x8935, false),
            (0x8938, false),
            (0x8940, false),
            (0x8942, false),
            (0x8947, false),
            (0x8948, false),
            (0x894C, false),
            (0x2400, false),
            (0x2401, false),
            (0x2402, false),
            (0x2403, false),
            (0x2405, false),
            (0x40082404, true),
            (0x40082406, true),
            (0x80082407, true),
            (0x40042408, true),
            (0x40042409, true),
            (0xc008240a, true),
            (0x4008240b, true),
        ];

        for (request, extend) in IOCTLS.iter() {
            if *extend {
                assert!(
                    extend_ioctl(*request).is_some(),
                    "OOPS: {request}->{extend}"
                );
            } else {
                assert!(
                    extend_ioctl(*request).is_none(),
                    "OOPS: {request}->{extend}"
                );
            }
        }
    }

    fn max_signal() -> c_int {
        // On mips SIGRTMAX() returns 127 which overflows the sigset.
        // TODO: Figure out how kernel maintains the sigset for realtime signals!
        if cfg!(any(
            target_arch = "mips",
            target_arch = "mips32r6",
            target_arch = "mips64",
            target_arch = "mips64r6"
        )) {
            libc::SIGRTMIN()
        } else {
            libc::SIGRTMAX()
        }
    }

    #[test]
    fn test_sigset_empty_set() {
        let set = SydSigSet::new(0);
        // No signals should be contained.
        for sig_num in 1..max_signal() {
            assert!(
                !set.contains(sig_num),
                "Empty set should not contain any signal"
            );
        }

        // Intersecting empty set with itself is still empty (no intersection means false).
        let empty2 = SydSigSet::new(0);
        assert!(
            !set.intersects(empty2),
            "Empty set should not intersect with another empty set"
        );
    }

    #[test]
    fn test_sigset_single_signal() {
        let sigalrm = libc::SIGALRM; // commonly 14
        let mask = 1u64 << (sigalrm - 1);
        let set = SydSigSet::new(mask);

        // Should contain SIGALRM only.
        assert!(set.contains(sigalrm), "Set should contain SIGALRM");
        for sig_num in 1..max_signal() {
            if sig_num != sigalrm {
                assert!(!set.contains(sig_num), "Only SIGALRM should be set");
            }
        }

        // Intersects with empty set? no
        let empty = SydSigSet::new(0);
        assert!(
            !set.intersects(empty),
            "Single-signal set should not intersect an empty set"
        );

        // Intersect with itself? yes
        assert!(set.intersects(set), "Set should intersect with itself");
    }

    #[test]
    fn test_sigset_multiple_signals() {
        let signals = [libc::SIGINT, libc::SIGALRM, libc::SIGTERM]; // e.g. [2, 14, 15]
        let mut mask = 0u64;
        for &sig in &signals {
            mask |= 1u64 << ((sig - 1) as u64);
        }
        let set = SydSigSet::new(mask);

        // Check contains
        for &sig in &signals {
            assert!(set.contains(sig), "Set should contain signal {}", sig);
        }

        // Check that others are not contained
        for sig_num in 1..max_signal() {
            if !signals.contains(&sig_num) {
                assert!(!set.contains(sig_num), "Only INT, ALRM, TERM should be set");
            }
        }

        // Check intersects
        // Intersect with a set that has one of those signals
        let single = SydSigSet::new(1u64 << ((libc::SIGINT - 1) as u64));
        assert!(
            set.intersects(single),
            "Should intersect since both contain SIGINT"
        );

        // Intersect with a set that shares no signals
        let unrelated_mask = 1u64 << ((libc::SIGHUP - 1) as u64); // SIGHUP=1 if not in the original set
        let unrelated_set = SydSigSet::new(unrelated_mask);
        if !signals.contains(&libc::SIGHUP) {
            assert!(
                !set.intersects(unrelated_set),
                "Should not intersect if no signals in common"
            );
        }

        // Test add and del by starting empty and building the set
        let mut dynamic_set = SydSigSet::new(0);
        for &sig in &signals {
            dynamic_set.add(sig);
            assert!(
                dynamic_set.contains(sig),
                "Signal {} should now be contained",
                sig
            );
        }

        // Remove one signal and ensure it's gone
        dynamic_set.del(libc::SIGALRM);
        assert!(
            !dynamic_set.contains(libc::SIGALRM),
            "SIGALRM should be removed"
        );
        assert!(dynamic_set.contains(libc::SIGINT), "Other signals remain");
        assert!(dynamic_set.contains(libc::SIGTERM), "Other signals remain");
    }

    #[test]
    fn test_sigset_all_signals() {
        let max_sig = max_signal();
        let mut mask = 0u64;
        for sig_num in 1..max_sig {
            mask |= 1u64 << ((sig_num - 1) as u64);
        }
        let set = SydSigSet::new(mask);

        // Should contain all signals up to SIGRTMAX
        for sig_num in 1..max_sig {
            assert!(set.contains(sig_num), "All signals should be contained");
        }

        // Removing a signal from a full set
        let mut copy_set = set;
        copy_set.del(libc::SIGINT);
        assert!(
            !copy_set.contains(libc::SIGINT),
            "SIGINT should be removed from the full set"
        );
        // Others remain
        for sig_num in 1..max_sig {
            if sig_num != libc::SIGINT {
                assert!(
                    copy_set.contains(sig_num),
                    "All others should still be present"
                );
            }
        }

        // Intersects with partial sets
        let single_set = SydSigSet::new(1u64 << ((libc::SIGTERM - 1) as u64));
        assert!(
            set.intersects(single_set),
            "Full set intersects with any non-empty set"
        );
    }

    #[test]
    fn test_sigset_bits_beyond_rtm() {
        let max_sig = max_signal();
        let mut mask = 0u64;

        // Set every bit up to 64 to ensure we cover beyond SIGRTMAX
        for i in 0..64 {
            mask |= 1u64 << i;
        }

        let set = SydSigSet::new(mask);

        // Contains all signals up to SIGRTMAX
        for sig_num in 1..max_sig {
            assert!(
                set.contains(sig_num),
                "All signals up to SIGRTMAX should be contained"
            );
        }
        // No error if we have bits beyond SIGRTMAX; they're simply meaningless beyond that range.
        // Since we don't have signals beyond SIGRTMAX, there's no direct test other than ensuring no panic.
    }

    #[test]
    fn test_sigset_invalid_signals() {
        let mut set = SydSigSet::new(0);

        // Adding an invalid signal (<1) should do nothing
        set.add(0);
        set.add(-1);
        assert!(!set.contains(0), "Invalid signal should not be contained");
        assert!(!set.contains(-1), "Invalid signal should not be contained");
        assert!(!set.contains(1), "We never added a valid signal");

        // Removing an invalid signal does nothing
        set.del(0);
        set.del(-1);
        assert!(
            !set.contains(1),
            "No signals should be added or removed by invalid ops"
        );
    }

    #[test]
    fn test_sigset_intersects() {
        // Create two sets with partial overlap
        // Set A: SIGINT, SIGALRM
        // Set B: SIGALRM, SIGTERM
        let set_a_mask =
            (1u64 << ((libc::SIGINT - 1) as u64)) | (1u64 << ((libc::SIGALRM - 1) as u64));
        let set_b_mask =
            (1u64 << ((libc::SIGALRM - 1) as u64)) | (1u64 << ((libc::SIGTERM - 1) as u64));

        let set_a = SydSigSet::new(set_a_mask);
        let set_b = SydSigSet::new(set_b_mask);

        // They both share SIGALRM
        assert!(set_a.intersects(set_b), "Sets should intersect on SIGALRM");

        // Create a set that does not share any signals with A or B
        let set_c = SydSigSet::new(1u64 << ((libc::SIGHUP - 1) as u64));
        if libc::SIGHUP != libc::SIGINT
            && libc::SIGHUP != libc::SIGALRM
            && libc::SIGHUP != libc::SIGTERM
        {
            assert!(
                !set_a.intersects(set_c),
                "A and C should not intersect if distinct signals"
            );
            assert!(
                !set_b.intersects(set_c),
                "B and C should not intersect if distinct signals"
            );
        }
    }
}
