//
// Syd: rock-solid application kernel
// src/wordexp.rs: Interface for libc's wordexp(3).
//
// Copyright (c) 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    borrow::Cow,
    env,
    ffi::{CStr, CString, OsStr},
    fmt,
    fs::File,
    io::{Read, Write},
    marker::PhantomData,
    os::{
        fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd},
        unix::ffi::OsStrExt,
    },
    time::{Duration, Instant},
};

use bitflags::bitflags;
use hex::DisplayHex;
use memchr::memchr3;
use nix::{
    errno::Errno,
    fcntl::OFlag,
    libc::{_exit, c_char, prctl, size_t, CLONE_FILES, ENOSYS, SIGCHLD, SIGKILL, SIGSYS},
    mount::{mount, MsFlags},
    sched::{unshare, CloneFlags},
    sys::{
        prctl::set_pdeathsig,
        signal::{sigprocmask, SigSet, SigmaskHow, Signal},
        wait::{Id, WaitPidFlag},
    },
    unistd::{chdir, close, Gid, Pid, Uid},
};

use crate::{
    compat::{pipe2_raw, waitid, WaitStatus},
    config::{MINI_STACK_SIZE, *},
    debug,
    err::err2no,
    fs::{
        create_memfd, safe_clone, safe_copy_if_exists, seal_memfd, set_cloexec, set_nonblock,
        MFD_ALLOW_SEALING,
    },
    get_user_home, get_user_name,
    landlock::RulesetStatus,
    landlock_operation,
    libseccomp::{ScmpAction, ScmpFilterContext, ScmpSyscall},
    log::contains_ascii_unprintable,
    path::PATH_MAX,
    unshare::{GidMap, UidMap},
    xpath, XPath, XPathBuf, MS_NOSYMFOLLOW,
};

bitflags! {
    /// Represents Word Expansion flags.
    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    pub struct WordExpFlags: i32 {
        /// Don’t do command substitution.
        const WRDE_NOCMD = 1 << 2;
        /// Normally during command substitution stderr is redirected to
        /// /dev/null. This flag specifies that stderr is not to be
        /// redirected.
        const WRDE_SHOWERR = 1 << 4;
        /// Consider it an error if an undefined shell variable is expanded.
        /// Note, this is not supported by musl.
        const WRDE_UNDEF = 1 << 5;
    }
}

impl Default for WordExpFlags {
    fn default() -> Self {
        Self::WRDE_NOCMD
    }
}

/// Represents error conditions from wordexp(3).
#[derive(Debug, Eq, PartialEq)]
pub enum WordExpError {
    /// Illegal occurrence of newline or one of |, &, ;, <, >, (, ), {, }.
    BadCharacter,
    /// An undefined shell variable was referenced, and the WRDE_UNDEF
    /// flag told us to consider this an error.
    BadValue,
    /// Command substitution requested, but the WRDE_NOCMD flag told us
    /// to consider this an error.
    CommandSubstitution,
    /// Out of memory.
    OutOfMemory,
    /// /bin/sh returned syntax error.
    Syntax,
    /// System error during pipe or fork.
    SystemError(Errno),
    /// Invalid system call.
    SeccompError,
    /// Process was aborted unexpectedly with signal.
    ProcessError(i32),
    /// Timeout error
    TimeoutError(u64),
}

/// Out of memory.
pub const WRDE_NOSPACE: i32 = 1;
/// Illegal occurrence of newline or one of |, &, ;, <, >, (, ), {, }.
pub const WRDE_BADCHAR: i32 = 2;
/// An undefined shell variable was referenced, and the WRDE_UNDEF
/// flag told us to consider this an error.
pub const WRDE_BADVAL: i32 = 3;
/// Command substitution requested, but the WRDE_NOCMD flag told us
/// to consider this an error.
pub const WRDE_CMDSUB: i32 = 4;
/// /bin/sh returned syntax error.
pub const WRDE_SYNTAX: i32 = 5;

// below are our additions,
// 128 is the errno/signal sentinel.

/// Invalid system call.
pub const WRDE_SECCOMP: i32 = 127;
/// Timeout error
pub const WRDE_TIMEOUT: i32 = 126;

impl From<std::io::Error> for WordExpError {
    fn from(io_err: std::io::Error) -> Self {
        Self::SystemError(err2no(&io_err))
    }
}

impl From<Errno> for WordExpError {
    fn from(err: Errno) -> Self {
        Self::SystemError(err)
    }
}

impl From<i32> for WordExpError {
    fn from(code: i32) -> Self {
        if code > 128 {
            // Used by pipe writer in the confined process.
            return Self::SystemError(Errno::from_raw(code));
        }
        #[allow(clippy::arithmetic_side_effects)]
        match code {
            WRDE_BADCHAR => Self::BadCharacter,
            WRDE_BADVAL => Self::BadValue,
            WRDE_CMDSUB => Self::CommandSubstitution,
            WRDE_NOSPACE => Self::OutOfMemory,
            WRDE_SYNTAX => Self::Syntax,
            // custom errors we invented.
            WRDE_SECCOMP => Self::SeccompError,
            _ => Self::SystemError(Errno::from_raw(code - 128)),
        }
    }
}

impl From<WordExpError> for i32 {
    fn from(val: WordExpError) -> Self {
        #[allow(clippy::arithmetic_side_effects)]
        match val {
            WordExpError::BadCharacter => WRDE_BADCHAR,
            WordExpError::BadValue => WRDE_BADVAL,
            WordExpError::CommandSubstitution => WRDE_CMDSUB,
            WordExpError::OutOfMemory => WRDE_NOSPACE,
            WordExpError::Syntax => WRDE_SYNTAX,
            // custom errors we invented.
            WordExpError::SeccompError => WRDE_SECCOMP,
            WordExpError::ProcessError(sig) => 128 + sig,
            WordExpError::SystemError(errno) => 128 + errno as i32,
            WordExpError::TimeoutError(_) => WRDE_TIMEOUT,
        }
    }
}

impl fmt::Display for WordExpError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            WordExpError::SystemError(Errno::EINVAL) => write!(
                f,
                "environment expansion is not permitted, enable with config/expand"
            ),
            WordExpError::CommandSubstitution => write!(
                f,
                "command substitution is not permitted, enable with config/expand_cmd"
            ),
            WordExpError::BadValue => write!(f, "empty replacement is not permitted"),
            WordExpError::BadCharacter => write!(
                f,
                "illegal occurrence of newline or one of |, &, ;, <, >, (, ), {{, }}"
            ),
            WordExpError::OutOfMemory => write!(f, "out of memory"),
            WordExpError::Syntax => write!(f, "shell syntax error"),
            WordExpError::SeccompError => write!(f, "seccomp error: invalid system call"),
            WordExpError::SystemError(e) => write!(f, "system error: {e}"),
            WordExpError::ProcessError(sig) => {
                let sig = Signal::try_from(*sig)
                    .map(|s| s.as_str())
                    .unwrap_or("SIGUNKNOWN");
                write!(f, "process error: received signal {sig}")
            }
            WordExpError::TimeoutError(t) => {
                let s = if *t > 1 { "s" } else { "" };
                write!(f, "timeout error: runtime exceeded {t} second{s}")
            }
        }
    }
}

#[repr(C)]
struct wordexp_t {
    // Count of words matched
    we_wordc: size_t,
    // List of words
    we_wordv: *mut *mut c_char,
    // Slots to reserve at the beginning.
    we_offs: size_t,
}

extern "C" {
    fn wordexp(s: *const c_char, p: *mut wordexp_t, flags: i32) -> i32;
    fn wordfree(p: *mut wordexp_t);
}

/// `WordExp` wraps wordfree(3) and provides an `Iterator`.
pub struct WordExp<'a> {
    p: wordexp_t,
    i: usize,
    _m: PhantomData<&'a ()>,
}

impl Drop for WordExp<'_> {
    fn drop(&mut self) {
        // SAFETY: In libc we trust.
        unsafe { wordfree(std::ptr::addr_of_mut!(self.p)) };
    }
}

impl<'a> Iterator for WordExp<'a> {
    type Item = &'a OsStr;

    fn next(&mut self) -> Option<Self::Item> {
        if self.i >= self.p.we_wordc {
            return None;
        }
        let off = isize::try_from(self.i).ok()?;

        // SAFETY: In libc, we trust.
        let ptr = unsafe { self.p.we_wordv.offset(off) };
        if ptr.is_null() {
            return None;
        }

        let ret = Some(OsStr::from_bytes(
            // SAFETY: In libc, we trust.
            unsafe { CStr::from_ptr(*ptr) }.to_bytes(),
        ));
        if let Some(i) = self.i.checked_add(1) {
            self.i = i;
        }
        ret
    }
}

impl WordExp<'_> {
    /// Performs shell-like word expansion.
    ///
    /// This is only a thin wrapper around libc's wordexp(3).
    /// Use `WordExp::expand` for safety.
    pub fn expand_word(s: &str, flags: WordExpFlags) -> Result<Self, i32> {
        let c_s = CString::new(s).or(Err(WRDE_BADCHAR))?;

        // SAFETY: init a `wordexp_t' structure.
        let mut p: wordexp_t = unsafe { std::mem::zeroed() };

        // SAFETY: call into libc wordexp(3).
        let ret = unsafe { wordexp(c_s.as_ptr(), std::ptr::addr_of_mut!(p), flags.bits()) };
        if ret != 0 {
            return Err(ret);
        }

        // SAFETY: return iterator for safe access.
        Ok(Self {
            p,
            i: 0,
            _m: PhantomData,
        })
    }

    /// Perform environment/tilde expansion and optionally command substitution.
    #[allow(clippy::cognitive_complexity)]
    pub fn expand_full(input: &str, timeout: Duration) -> Result<Cow<'_, str>, WordExpError> {
        // Quick returns:
        // Empty string or no special characters present.
        if input.is_empty() || memchr3(b'$', b'`', b'(', input.as_bytes()).is_none() {
            return Ok(Cow::Borrowed(input));
        }
        // Zero timeout prevents evaluation.
        if timeout.is_zero() {
            return Err(WordExpError::SystemError(Errno::EINVAL));
        }

        // Create a memory fd to write input into,
        // and pass to the internal /bin/sh invoked
        // by wordexp(3).
        let mut file = create_memfd(b"syd-exp\0", MFD_ALLOW_SEALING).map(File::from)?;
        debug!("ctx": "expand",
            "msg": format!("created memory-file {} with close-on-exec flag set",
                file.as_raw_fd()));

        // Define the `esyd` function.
        file.write_all(ESYD_SH.as_bytes())?;
        file.write_all(b"\n")?;

        // Handle system-wide configuration.
        safe_copy_if_exists(&mut file, XPath::from_bytes(b"/etc/syd/init.sh"))?;
        file.write_all(b"\n")?;

        // Handle user-specific configuration.
        let home = env::var_os("HOME")
            .map(XPathBuf::from)
            .unwrap_or(XPathBuf::from("/proc/self/fdinfo"));
        safe_copy_if_exists(&mut file, &home.join(b".config/syd/init.sh"))?;
        file.write_all(b"\n")?;

        // Write input.
        file.write_all(b"echo -n ")?;
        file.write_all(input.as_bytes())?;
        file.write_all(b"\n")?;

        // Seal memfd for future writes.
        seal_memfd(&file)?;
        debug!("ctx": "expand",
            "msg": format!("sealed memory-file {} against grows, shrinks and writes",
                file.as_raw_fd()));

        // Set close-on-exec to off.
        set_cloexec(&file, false)?;
        debug!("ctx": "expand",
            "msg": format!("set close-on-exec flag to off for memory-file {}",
                file.as_raw_fd()));

        let shell = format!("`. /proc/self/fd/{}`", file.as_raw_fd());
        debug!("ctx": "expand",
            "msg": format!("passing memory file {} to wordexp(3) with {} seconds timeout...",
                file.as_raw_fd(), timeout.as_secs()));
        Ok(Cow::Owned(Self::expand(&shell, true, timeout)?.to_string()))
    }

    /// Perform environment/tilde expansion and optionally command substitution.
    pub fn expand(
        input: &str,
        cmd_subs: bool,
        timeout: Duration,
    ) -> Result<Cow<'_, str>, WordExpError> {
        // Quick returns:
        // Empty string or no special characters present.
        if input.is_empty() || memchr3(b'$', b'`', b'(', input.as_bytes()).is_none() {
            return Ok(Cow::Borrowed(input));
        }
        // Zero timeout prevents evaluation.
        if timeout.is_zero() {
            return Err(WordExpError::SystemError(Errno::EINVAL));
        }

        // Command substitution is optional.
        let mut flags = WordExpFlags::WRDE_SHOWERR;
        if !cmd_subs {
            flags |= WordExpFlags::WRDE_NOCMD;
        }

        // set up pipe to transfer wordexp(3) return string.
        let (pipe_rd, pipe_wr) = pipe2_raw(OFlag::O_CLOEXEC)?;

        // SAFETY: set read end of the pipe as non-blocking.
        let pipe_rd_ref = unsafe { BorrowedFd::borrow_raw(pipe_rd) };
        set_nonblock(&pipe_rd_ref, true)?;
        // SAFETY: Fork and confine before running wordexp(3)!

        let mut stack = [0u8; MINI_STACK_SIZE];
        let epoch = Instant::now();
        let pid_fd = safe_clone(
            Box::new(move || -> isize {
                let _ = close(pipe_rd);
                // SAFETY: acquire a safe File handle to the pipe.
                let mut pipe = unsafe { File::from_raw_fd(pipe_wr) };
                // SAFETY: confine or panic!
                Self::confine();
                debug!("ctx": "expand",
                    "msg": format!("calling wordexp(3), good luck!"));
                // SAFETY: call into libc wordexp(3).
                for word in match Self::expand_word(input, flags) {
                    Ok(iter) => iter,
                    Err(err) =>
                    // SAFETY: In libc we trust.
                    unsafe { _exit(err) },
                } {
                    if word.is_empty() {
                        continue;
                    }
                    if let Err(ref error) = pipe.write_all(word.as_bytes()) {
                        let err = err2no(error) as i32;
                        // SAFETY: In libc we trust.
                        #[allow(clippy::arithmetic_side_effects)]
                        unsafe {
                            _exit(128 + err)
                        };
                    }
                    if let Err(ref error) = pipe.write_all(b" ") {
                        let err = err2no(error) as i32;
                        // SAFETY: In libc we trust.
                        #[allow(clippy::arithmetic_side_effects)]
                        unsafe {
                            _exit(128 + err)
                        };
                    }
                }
                // SAFETY: In libc we trust.
                unsafe { _exit(0) };
            }),
            &mut stack[..],
            0,
            Some(SIGCHLD),
        )?;

        let _ = close(pipe_wr);
        // SAFETY: pipe_rd is a valid FD.
        let mut pipe = unsafe { File::from_raw_fd(pipe_rd) };

        let mut eof = false;
        let mut sig = false;
        let mut err = Errno::UnknownErrno;

        let mut buf = [0u8; PATH_MAX];
        let mut ret = Vec::new();

        loop {
            if !sig && (err as i32 != 0 || epoch.elapsed() >= timeout) {
                // a. out of memory condition
                // b. timeout exceeded
                // send SIGKILL once, and fall-through to wait.
                sig = true;
                // SAFETY: There's no libc wrapper for pidfd_send_signal yet.
                let _ = unsafe {
                    libc::syscall(
                        libc::SYS_pidfd_send_signal,
                        pid_fd.as_fd().as_raw_fd(),
                        SIGKILL,
                        0,
                        0,
                    )
                };
            } else if !eof {
                // read one batch from pipe.
                match pipe.read(&mut buf) {
                    Ok(0) => {
                        // EOF, fall-through to wait.
                        eof = true;
                    }
                    Ok(n) => {
                        // child started writing to the pipe.
                        // this means wordexp(3) is done
                        // executing, so we no longer need
                        // to keep track of timeout.
                        if ret.try_reserve(n).is_err() {
                            err = Errno::ENOMEM;
                        } else {
                            ret.extend(&buf[..n]);
                        }
                        continue;
                    }
                    Err(ref e) if matches!(err2no(e), Errno::EAGAIN | Errno::EINTR) => {
                        std::thread::sleep(Duration::from_millis(100));
                        continue;
                    }
                    Err(ref e) => {
                        err = err2no(e);
                        continue;
                    }
                };
            }

            // wait for process without blocking.
            match waitid(
                Id::PIDFd(pid_fd.as_fd()),
                WaitPidFlag::WEXITED | WaitPidFlag::WNOHANG,
            ) {
                Ok(WaitStatus::Exited(_, 0)) if eof => break,
                Ok(WaitStatus::Exited(_, 0)) => {
                    let mut end = Vec::new();
                    if end.try_reserve(16).is_err() {
                        return Err(WordExpError::OutOfMemory);
                    }
                    if let Err(e) = set_nonblock(&pipe, false) {
                        return Err(WordExpError::SystemError(e));
                    }
                    match pipe.read_to_end(&mut end) {
                        Ok(0) => break,
                        Ok(n) => {
                            if ret.try_reserve(n).is_err() {
                                return Err(WordExpError::OutOfMemory);
                            }
                            ret.extend(&end[..n]);
                            break;
                        }
                        Err(ref e) => return Err(WordExpError::SystemError(err2no(e))),
                    }
                }
                Ok(WaitStatus::Exited(_, n)) => return Err(WordExpError::from(n)),
                Ok(WaitStatus::Signaled(_, SIGSYS, _)) => return Err(WordExpError::SeccompError),
                Ok(WaitStatus::Signaled(_, SIGKILL, _)) if err == Errno::ENOMEM => {
                    return Err(WordExpError::OutOfMemory)
                }
                Ok(WaitStatus::Signaled(_, SIGKILL, _)) if err as i32 != 0 => {
                    return Err(WordExpError::SystemError(err))
                }
                Ok(WaitStatus::Signaled(_, SIGKILL, _)) => {
                    return Err(WordExpError::TimeoutError(timeout.as_secs()))
                }
                Ok(WaitStatus::Signaled(_, sig, _)) => return Err(WordExpError::ProcessError(sig)),
                _ => {}
            };
        }

        // SAFETY: do not allow empty replacement.
        if ret.is_empty() {
            return Err(WordExpError::BadValue);
        }
        ret.pop(); // pop the trailing word separator.

        // SAFETY: hex-encode if expansion is invalid UTF-8.
        let ret = match std::str::from_utf8(&ret) {
            Ok(ret) => ret.to_string(),
            Err(_) => return Ok(ret.to_lower_hex_string().into()),
        };

        // SAFETY: do not allow empty replacement.
        if ret.is_empty() {
            return Err(WordExpError::BadValue);
        }

        // SAFETY: hex-encode if string has non-printables.
        if contains_ascii_unprintable(ret.as_bytes()) {
            Ok(ret.as_bytes().to_lower_hex_string().into())
        } else {
            Ok(ret.into())
        }
    }

    /// Transit the wordexp(3) fork process into a confined state,
    /// with read-only access to the filesystem.
    ///
    /// # Safety
    ///
    /// Panics on all errors except Landlock and namespaces which are
    /// optional as they may not be available.
    #[allow(clippy::cognitive_complexity)]
    #[allow(clippy::disallowed_methods)]
    pub fn confine() {
        // SAFETY: Determine user HOME directory.
        // This will be confined by Landlock.
        let uid = Uid::current();
        let gid = Gid::current();
        let name = get_user_name(uid);
        let home = get_user_home(&name);
        debug!("ctx": "expand",
            "msg": format!("started confining wordexp process {} running as user {name}",
                Pid::this().as_raw()));

        // SAFETY: ensure safe working directory.
        chdir(&home).expect("change dir to home");
        debug!("ctx": "expand",
            "msg": format!("changed directory to {home}"));

        // SAFETY: set up namespace isolation.
        // continue on errors as unprivileged userns may not be supported.
        if unshare(
            CloneFlags::CLONE_NEWUSER
                | CloneFlags::CLONE_NEWCGROUP
                | CloneFlags::CLONE_NEWIPC
                | CloneFlags::CLONE_NEWNET
                | CloneFlags::CLONE_NEWNS
                | CloneFlags::CLONE_NEWPID
                | CloneFlags::CLONE_NEWUTS,
        )
        .is_ok()
        {
            debug!("ctx": "expand",
                "msg": "created/entered into new user, mount, pid, network, cgroup, ipc, and uts namespaces");
            // SAFETY: map current user in new user namespace.
            let uid_buf = {
                let uid_maps = vec![
                    UidMap {
                        inside_uid: uid.into(),
                        outside_uid: uid.into(),
                        count: 1,
                    }, // Map the current user.
                ];
                let mut buf = Vec::new();
                for map in uid_maps {
                    writeln!(
                        &mut buf,
                        "{} {} {}",
                        map.inside_uid, map.outside_uid, map.count
                    )
                    .expect("write uid_map");
                }
                buf
            };
            let gid_buf = {
                let gid_maps = vec![
                    GidMap {
                        inside_gid: gid.into(),
                        outside_gid: gid.into(),
                        count: 1,
                    }, // Map the current group.
                ];
                let mut buf = Vec::new();
                for map in gid_maps {
                    writeln!(
                        &mut buf,
                        "{} {} {}",
                        map.inside_gid, map.outside_gid, map.count
                    )
                    .expect("write gid_map");
                }
                buf
            };
            // Write "deny" to /proc/self/setgroups before writing to gid_map.
            File::create("/proc/self/setgroups")
                .and_then(|mut f| f.write_all(b"deny"))
                .expect("deny setgroups");
            File::create("/proc/self/gid_map")
                .and_then(|mut f| f.write_all(&gid_buf[..]))
                .expect("map current group");
            debug!("ctx": "expand",
                "msg": format!("mapped current group {gid} into new user namespace"));
            File::create("/proc/self/uid_map")
                .and_then(|mut f| f.write_all(&uid_buf[..]))
                .expect("map current user");
            debug!("ctx": "expand",
                "msg": format!("mapped current user {uid} into new user namespace"));

            // SAFETY: remount rootfs as readonly,nosuid,nodev,nosymfollow
            // Careful here, unshare(2) may be available but mount(2) may not be,
            // so we must handle mount errors gracefully.
            let mut flags: MsFlags = MsFlags::MS_BIND
                | MsFlags::MS_REC
                | MsFlags::MS_RDONLY
                | MsFlags::MS_NOSUID
                | MsFlags::MS_NODEV
                | MS_NOSYMFOLLOW;
            if mount(
                Some("none"),
                "/",
                None::<&XPath>,
                MsFlags::MS_PRIVATE | MsFlags::MS_REC,
                None::<&XPath>,
            )
            .is_ok()
            {
                debug!("ctx": "expand",
                    "msg": "set mount propagation to private in the new mount namespace");
                mount(Some("/"), "/", Some("/"), flags, None::<&XPath>).expect("remount rootfs");
                debug!("ctx": "expand",
                    "msg": "remounted root with readonly, nosuid, nodev, and nosymfollow options in the new mount namespace");
                // SAFETY: mount private procfs
                // pid=1 is required to exist before this.
                flags.remove(MsFlags::MS_BIND | MsFlags::MS_REC | MS_NOSYMFOLLOW);
                flags.insert(MsFlags::MS_NOEXEC);
                Self::mount_proc(flags);
            }
        }

        // SAFETY: Landlock: confine filesystem as read-only.
        // continue on errors as Landlock may not be supported.
        let mut path_ro = vec![];
        let mut path_rw = vec![];
        for ro in [
            "/bin",
            "/dev",
            "/lib",
            "/lib64",
            "/libexec",
            "/opt",
            "/proc",
            "/run",
            "/sbin",
            "/usr",
            "/var",
            "/etc/ld.so.conf",
            "/etc/ld.so.cache",
            "/etc/ld.so.conf.d",
            "/etc/ld-x86_64-pc-linux-musl.path",
            "/etc/ld-musl-aarch64.path",
            "/etc/ld-musl-aarch64.d",
            "/etc/hostname",
            "/etc/motd",
            "/etc/os-release",
            "/etc/machine-id",
            "/etc/passwd",
            "/etc/group",
            "/etc/group-",
            "/etc/securetty",
            "/etc/shells",
            "/etc/sysctl.conf",
            "/etc/sysctl.d",
            "/etc/xdg",
            "/etc/networks",
            "/etc/protocols",
            "/etc/services",
            "/etc/environment",
            "/etc/login.defs",
            "/etc/mime.types",
            "/etc/profile",
            "/etc/profile.env",
            "/etc/profile.d",
            "/etc/profile.csh",
            "/etc/bash",
            "/etc/zsh",
            "/etc/zshenv",
            "/etc/zshrc",
            "/etc/zlogin",
            "/etc/zprofile",
            "/etc/syd",
        ] {
            path_ro.push(XPathBuf::from(ro));
        }
        for home_ro in [
            ".profile",
            ".bashrc",
            ".bash_login",
            ".bash_profile",
            ".zshenv",
            ".zshrc",
            ".zlogin",
            ".zprofile",
            ".config/syd",
            ".local/share/syd",
        ] {
            path_ro.push(xpath!("{home}/{home_ro}"));
        }
        for rw in ["/dev/null", "/dev/tty"] {
            path_rw.push(XPathBuf::from(rw));
        }
        // Note we don't use scoped signals of Landlock ABI 6 here,
        // because we want the wordexp process to signal the init
        // process with the parent death signal.
        let abi = crate::landlock::ABI::new_current();
        match landlock_operation(abi, &path_ro, &path_rw, &[], &[], true, false) {
            Ok(status) => match status.ruleset {
                RulesetStatus::FullyEnforced => {
                    debug!("ctx": "expand",
                        "msg": format!("Landlock ABI {} is fully enforced",
                            abi as i32),
                        "abi": abi as i32);
                }
                RulesetStatus::PartiallyEnforced => {
                    debug!("ctx": "expand",
                        "msg": format!("Landlock ABI {} is partially enforced",
                            abi as i32),
                        "abi": abi as i32);
                }
                RulesetStatus::NotEnforced => {
                    debug!("ctx": "expand",
                        "msg": format!("Landlock ABI {} is not enforced",
                            abi as i32),
                        "abi": abi as i32);
                }
            },
            Err(error) => {
                debug!("ctx": "expand",
                    "msg": format!("Landlock ABI {} is unsupported: {error}",
                        abi as i32),
                    "abi": abi as i32);
            }
        }

        const PR_SET_MDWE: nix::libc::c_int = 65;
        const PR_MDWE_REFUSE_EXEC_GAIN: nix::libc::c_ulong = 1;
        // SAFETY: set Memory-Deny-Write-Execute attribute.
        // continue on errors as MDWE may not be supported.
        match Errno::result(unsafe { prctl(PR_SET_MDWE, PR_MDWE_REFUSE_EXEC_GAIN, 0, 0, 0) }) {
            Ok(_) => {
                debug!("ctx": "expand",
                    "msg": "set Memory-Deny-Write-Execute attribute to deny W^X memory");
            }
            Err(Errno::EINVAL) => {
                debug!("ctx": "expand",
                    "msg": "Memory-Deny-Write-Execute attribute requires Linux-6.3 or newer");
            }
            Err(Errno::EPERM) => {
                debug!("ctx": "expand",
                    "msg": "Memory-Deny-Write-Execute attribute was set already");
            }
            Err(errno) => {
                debug!("ctx": "expand",
                    "msg": format!("failed to enable Memory-Deny-Write-Execute attribute: {errno}"));
            }
        }

        // SAFETY: confine with seccomp.
        // panics on errors.
        Self::confine_seccomp();
    }

    #[allow(clippy::disallowed_methods)]
    fn confine_seccomp() {
        let mut filter = ScmpFilterContext::new(ScmpAction::Errno(ENOSYS)).expect("create filter");

        // Enforce the NO_NEW_PRIVS functionality before
        // loading the seccomp filter into the kernel.
        filter.set_ctl_nnp(true).expect("enforce no-new-privs");

        // Deny requests with bad architecture.
        filter
            .set_act_badarch(ScmpAction::Errno(ENOSYS))
            .expect("set bad architecture action");

        // Use a binary tree sorted by syscall number, if possible.
        let _ = filter.set_ctl_optimize(2);

        for sysname in WORDEXP_SYSCALLS {
            if let Ok(syscall) = ScmpSyscall::from_name(sysname) {
                filter
                    .add_rule(ScmpAction::Allow, syscall)
                    .expect("filter syscall");
            }
        }

        filter.load().expect("load filter");
        debug!("ctx": "expand",
            "msg": "loaded seccomp filter");
    }

    #[allow(clippy::cognitive_complexity)]
    #[allow(clippy::disallowed_methods)]
    fn mount_proc(flags: MsFlags) {
        let mut stack = [0u8; MINI_STACK_SIZE];
        safe_clone(
            Box::new(move || -> isize {
                // pid=1 here.
                debug!("ctx": "expand",
                    "msg": "started init process in the new pid namespace");

                // SAFETY: set parent-death signal to SIGKILL
                if set_pdeathsig(Some(Signal::SIGKILL)).is_err() {
                    return 0; // tear down the pid-ns.
                }
                debug!("ctx": "expand",
                    "msg": "set parent-death signal to SIGKILL for the init process");

                // SAFETY: block all signals
                sigprocmask(SigmaskHow::SIG_BLOCK, Some(&SigSet::all()), None)
                    .expect("block signals");

                // SAFETY: mount private procfs, continue on errors.
                match mount(
                    Some("proc"),
                    "/proc",
                    Some("proc"),
                    flags,
                    Some("hidepid=2"),
                ) {
                    Ok(_) => {
                        debug!("ctx": "expand",
                            "msg": "mounted proc with hidepid=2 in the new mount namespace");
                    }
                    Err(errno) => {
                        debug!("ctx": "expand",
                            "msg": format!("failed to mount private procfs: {errno}"));
                    }
                };

                // SAFETY: block until the parent-death signal kills us.
                std::thread::sleep(Duration::MAX);

                unreachable!();
            }),
            &mut stack[..],
            // SAFETY: do not copy pipe-fds into this process.
            // if write end of the pipe remains open unintentionally,
            // the read end will block forever which we absolutely
            // don't want. parent-death signal also helps with this
            // otherwise but better safe than sorry.
            CLONE_FILES,
            Some(SIGCHLD),
        )
        .map(drop)
        .expect("spawn pid1");
    }
}

const WORDEXP_SYSCALLS: &[&str] = &[
    "_llseek",
    "_newselect",
    "access",
    "alarm",
    "arch_prctl", // Used during platform-specific initialization by ld-linux.so.
    "arm_fadvise64_64",
    "arm_sync_file_range",
    "breakpoint", // arm
    "brk",
    "cacheflush", // arm
    "cachestat",
    "capget",
    "chdir",
    "clock_getres",
    "clock_getres_time64",
    "clock_gettime",
    "clock_gettime64",
    "clock_nanosleep",
    "clock_nanosleep_time64",
    "clone",
    "clone3",
    "close",
    "close_range",
    "copy_file_range",
    "dup",
    "dup2",
    "dup3",
    "epoll_create",
    "epoll_create1",
    "epoll_ctl",
    "epoll_ctl_old",
    "epoll_pwait",
    "epoll_pwait2",
    "epoll_wait",
    "epoll_wait_old",
    "eventfd",
    "eventfd2",
    "execve",
    "execveat",
    "exit",
    "exit_group",
    "faccessat",
    "faccessat2",
    "fadvise64",
    "fadvise64_64",
    "fchdir",
    "fcntl",
    "fcntl64",
    "fdatasync",
    "fgetxattr",
    "flistxattr",
    "flock",
    "fork",
    "fstat",
    "fstat64",
    "fstatfs",
    "fstatfs64",
    "fsync",
    "futex",
    "futex_time64",
    "futex_waitv",
    "get_mempolicy",
    "get_robust_list",
    "get_thread_area",
    "getcpu",
    "getcwd",
    "getegid",
    "getegid32",
    "geteuid",
    "geteuid32",
    "getgid",
    "getgid32",
    "getgroups",
    "getgroups32",
    "getitimer",
    "getpeername",
    "getpgid",
    "getpgrp",
    "getpid",
    "getpmsg",
    "getppid",
    "getpriority",
    "getrandom",
    "getresgid",
    "getresuid",
    "getrlimit",
    "getrusage",
    "getsid",
    "getsockopt",
    "gettid",
    "gettimeofday",
    "getuid",
    "getuid32",
    "getxattr",
    "io_cancel",
    "io_destroy",
    "io_getevents",
    "io_pgetevents",
    "io_pgetevents_time64",
    "io_setup",
    "io_submit",
    "ioprio_get",
    "ioprio_set",
    "kcmp",
    "kill",
    "landlock_add_rule",
    "landlock_create_ruleset",
    "landlock_restrict_self",
    "lgetxattr",
    "listxattr",
    "llistxattr",
    "lseek",
    "lstat",
    "madvise",
    "membarrier",
    "mlock",
    "mlock2",
    "mlockall",
    "mmap",
    "mmap2",
    "mprotect",
    "mq_getsetattr",
    "mq_notify",
    "mq_open",
    "mq_timedreceive",
    "mq_timedreceive_time64",
    "mq_timedsend",
    "mq_timedsend_time64",
    "mq_unlink",
    "mremap",
    "msgctl",
    "msgget",
    "msgrcv",
    "msync",
    "munlock",
    "munlockall",
    "munmap",
    "nanosleep",
    "newfstatat",
    "oldfstat",
    "oldolduname",
    "olduname",
    "open",
    "openat",
    "openat2",
    "pause",
    "pipe",
    "pipe2",
    "poll",
    "ppoll",
    "ppoll_time64",
    "prctl",
    "pread64",
    "preadv",
    "preadv2",
    "prlimit64",
    "process_madvise",
    "process_mrelease",
    "pselect6",
    "pselect6_time64",
    "pwrite64",
    "pwritev",
    "pwritev2",
    "read",
    "readahead",
    "readlink",
    "readlinkat",
    "readv",
    "remap_file_pages",
    "restart_syscall",
    "riscv_flush_icache",
    "rseq",
    "rt_sigaction",
    "rt_sigpending",
    "rt_sigprocmask",
    "rt_sigqueueinfo",
    "rt_sigreturn",
    "rt_sigsuspend",
    "rt_sigtimedwait",
    "rt_sigtimedwait_time64",
    "rt_tgsigqueueinfo",
    "s390_pci_mmio_read",
    "s390_pci_mmio_write",
    "s390_runtime_instr",
    "sched_get_priority_max",
    "sched_get_priority_min",
    "sched_getaffinity",
    "sched_getattr",
    "sched_getparam",
    "sched_getscheduler",
    "sched_rr_get_interval",
    "sched_rr_get_interval_time64",
    "sched_setaffinity",
    "sched_setattr",
    "sched_setparam",
    "sched_setscheduler",
    "sched_yield",
    "seccomp",
    "select",
    "semctl",
    "semget",
    "semop",
    "semtimedop",
    "semtimedop_time64",
    "set_robust_list",
    "set_thread_area",
    "set_tid_address",
    "set_tls", // arm
    "setitimer",
    "setpgid",
    "setpriority",
    "setrlimit",
    "setsid",
    "setsockopt",
    "shmat",
    "shmctl",
    "shmdt",
    "shmget",
    "sigaction",
    "sigaltstack",
    "signal",
    "signalfd",
    "signalfd4",
    "sigpending",
    "sigprocmask",
    "sigreturn",
    "sigsuspend",
    "splice",
    "stat",
    "stat64",
    "statx",
    "sync_file_range",
    "tee",
    "tgkill",
    "time",
    "timer_create",
    "timer_delete",
    "timer_getoverrun",
    "timer_gettime",
    "timer_gettime64",
    "timer_settime",
    "timer_settime64",
    "timerfd_create",
    "timerfd_gettime",
    "timerfd_gettime64",
    "timerfd_settime",
    "timerfd_settime64",
    "times",
    "tkill",
    "ugetrlimit",
    "umask",
    "uname",
    "vfork",
    "wait4",
    "waitid",
    "waitpid",
    "write",
    "writev",
];
