//
// Syd: rock-solid application kernel
// src/seal.rs: Execute program as sealed anonymous file
//
// Copyright (c) 2024, 2025 Ali Polatel <alip@chesswob.org>
// Based in part upon pentacle which is:
//   Copyright (c) iliana destroyer of worlds <iliana@buttslol.net>
//   SPDX-License-Identifier: MIT
//
// SPDX-License-Identifier: GPL-3.0

// SAFETY: This module has been liberated from unsafe code!
#![forbid(unsafe_code)]

// Last sync with pentacle:
// Version 1.0.0
// Commit:e606ab250e6655865bb93a6d98157093f2eb455f

use std::{
    convert::Infallible,
    env,
    ffi::{CStr, CString},
    fs::{File, Permissions},
    os::unix::{ffi::OsStringExt, fs::PermissionsExt},
};

use libc::mode_t;
use nix::{
    errno::Errno,
    fcntl::{fcntl, AtFlags, FcntlArg, SealFlag},
    libc::{
        c_int, c_uint, F_SEAL_FUTURE_WRITE, F_SEAL_GROW, F_SEAL_SEAL, F_SEAL_SHRINK, F_SEAL_WRITE,
        MFD_ALLOW_SEALING, MFD_CLOEXEC, MFD_EXEC, MFD_NOEXEC_SEAL,
    },
    unistd::execveat,
};

use crate::{
    compat::{fstatx, MFdFlags, STATX_TYPE},
    config::ENV_SKIP_SCMP,
    err::err2no,
    io::ReadFd,
    lookup::FileType,
    retry::retry_on_eintr,
};

// Default memory fd name.
const DEFAULT_MEMFD_NAME: &CStr = c"syd";

// not yet present in the libc crate
// linux: include/uapi/linux/fcntl.h
const F_SEAL_EXEC: c_int = 0x0020;

const OPTIONS: SealOptions = SealOptions::new().close_on_exec(true).executable(true);

/// Ensure the currently running program is a sealed anonymous file.
///
/// For safety, the executable path is located in `/proc/self/maps`, and
/// executable's inode and device ID are verified on open. On verification
/// errors `Errno::EBADF` is returned.
///
/// If the current executable is not a sealed anonymous file, a new
/// anonymous file is created, the executable content is copied to it,
/// the file is sealed, and [`CommandExt::exec`] is called. When the
/// program begins again, this function will detect the executable as a
/// sealed anonymous file and return `Ok(())`.
///
/// You should call this function at the beginning of `main`. This
/// function has the same implications as [`CommandExt::exec`]: no
/// destructors on the current stack or any other thread’s stack will be
/// run.
///
/// # Errors
///
/// An error is returned if the executable file is not a regular file,
/// file fails to open, file verification fails, `memfd_create(2)`
/// fails, the `fcntl(2)` `F_GET_SEALS` or `F_ADD_SEALS` commands fail,
/// or copying from executable file to the anonymous file fails.
pub fn ensure_sealed() -> Result<(), Errno> {
    // SAFETY: This runs before sandboxing starts, so safe open API is not available yet.
    #[expect(clippy::disallowed_methods)]
    let mut file = File::open("/proc/self/exe").map_err(|err| err2no(&err))?;

    if OPTIONS.is_sealed(&file) {
        // Already sealed, move on...
        Ok(())
    } else {
        // Copy into memfd, seal and reexec.
        Err(SealedCommand::new(&mut file)?.exec().unwrap_err())
    }
}

/// A [`Command`] wrapper that spawns sealed memory-backed programs.
pub struct SealedCommand {
    memfd: File,
}

impl SealedCommand {
    /// Constructs a new [`SealedCommand`] for launching the program
    /// data in `program` as a sealed memory-backed file.
    ///
    /// The memory-backed file will close on `execveat(2)`.
    ///
    /// # Errors
    ///
    /// An error is returned if `program` is not a regular file,
    /// `memfd_create(2)` fails, the `fcntl(2)` `F_GET_SEALS` or
    /// `F_ADD_SEALS` commands fail, or copying from `program` to the
    /// anonymous file fails.
    pub fn new<Fd>(program: &mut Fd) -> Result<Self, Errno>
    where
        Fd: ReadFd,
    {
        // Check the file type and bail if it's not a regular file.
        let statx = retry_on_eintr(|| fstatx(&program, STATX_TYPE))?;
        let ftype = FileType::from(mode_t::from(statx.stx_mode));
        if !ftype.is_file() {
            return Err(Errno::ENOEXEC);
        }

        let mut memfd = OPTIONS.create()?;
        crate::io::copy(program, &mut memfd)?;
        OPTIONS.seal(&mut memfd)?;

        Ok(Self { memfd })
    }

    /// Execute the memory-backed file with execveat(2) and AT_EMPTY_PATH.
    ///
    /// The file will be closed on execveat(2).
    pub fn exec(self) -> Result<Infallible, Errno> {
        // Force RUST_BACKTRACE environment variable to 0 for Syd.
        // Passthrough the original value to the sandbox process.
        // See syd.rs for the other branch.
        // Rest is handled in unshare/child.rs.
        match env::var_os("RUST_BACKTRACE") {
            Some(val) => env::set_var("SYD_RUST_BACKTRACE", val),
            None => env::remove_var("SYD_RUST_BACKTRACE"),
        };
        if env::var_os(ENV_SKIP_SCMP).is_none() {
            env::set_var("RUST_BACKTRACE", "0");
        }

        // Collect arguments.
        let args = env::args_os()
            .map(|arg| CString::new(arg.into_vec()).or(Err(Errno::EINVAL)))
            .collect::<Result<Vec<CString>, Errno>>()?;

        // Collect environment variables.
        let envs = env::vars_os()
            .map(|(k, v)| {
                let mut bytes = k.into_vec();
                bytes.push(b'=');
                bytes.extend(v.into_vec());
                CString::new(bytes).or(Err(Errno::EINVAL))
            })
            .collect::<Result<Vec<CString>, Errno>>()?;

        execveat(self.memfd, c"", &args, &envs, AtFlags::AT_EMPTY_PATH)
    }
}

macro_rules! set_flag {
    ($flags:expr, $flag:expr, $value:expr) => {
        if $value {
            $flags |= $flag;
        } else {
            $flags &= !$flag;
        }
    };
}

macro_rules! seal {
    (
        $seal_ident:ident
        $( { $( #[ $attr:meta ] )* } )? ,
        $must_seal_ident:ident
        $( { $( #[ $must_attr:meta ] )* } )? ,
        $( ? $preflight:ident : )? $flag:expr,
        $try_to:expr,
        $default:expr
    ) => {
        #[doc = concat!("If `true`, try to ", $try_to, ".")]
        #[doc = ""]
        #[doc = "If `false`, also set"]
        #[doc = concat!("[`SealOptions::", stringify!($must_seal_ident), "`]")]
        #[doc = "to `false`."]
        #[doc = ""]
        #[doc = concat!("This flag is `", $default, "` by default.")]
        $($( #[ $attr ] )*)?
        pub const fn $seal_ident(mut self, $seal_ident: bool) -> SealOptions {
            if true $( && self.$preflight() )? {
                set_flag!(self.seal_flags, $flag, $seal_ident);
            }
            if !$seal_ident {
                self.must_seal_flags &= !$flag;
            }
            self
        }

        #[doc = "If `true`, also set"]
        #[doc = concat!("[`SealOptions::", stringify!($seal_ident), "`] to `true`")]
        #[doc = "and ensure it is successful when [`SealOptions::seal`] is called."]
        #[doc = ""]
        #[doc = concat!("This flag is `", $default, "` by default.")]
        $($( #[ $must_attr ] )*)?
        pub const fn $must_seal_ident(mut self, $must_seal_ident: bool) -> SealOptions {
            if $must_seal_ident {
                self.seal_flags |= $flag;
            }
            set_flag!(self.must_seal_flags, $flag, $must_seal_ident);
            self
        }
    };
}

/// Options for creating a sealed anonymous file.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[must_use]
pub struct SealOptions {
    memfd_flags: c_uint,
    seal_flags: c_int,
    must_seal_flags: c_int,
}

impl Default for SealOptions {
    fn default() -> Self {
        Self::new()
    }
}

impl SealOptions {
    /// Create a default set of options ready for configuration.
    ///
    /// This is equivalent to:
    /// ```
    /// use syd::seal::SealOptions;
    /// let options = SealOptions::new()
    ///     .close_on_exec(true)
    ///     .must_seal_seals(true)
    ///     .must_seal_shrinking(true)
    ///     .must_seal_growing(true)
    ///     .must_seal_writing(true)
    ///     .seal_future_writing(false)
    ///     .seal_executable(false);
    /// assert_eq!(options, SealOptions::default());
    /// ```
    pub const fn new() -> Self {
        Self {
            memfd_flags: MFD_ALLOW_SEALING | MFD_CLOEXEC,
            seal_flags: F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE,
            must_seal_flags: F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE,
        }
    }

    /// Sets the close-on-exec (`CLOEXEC`) flag for the new file.
    ///
    /// When a child process is created, the child normally inherits any open file descriptors.
    /// Setting the close-on-exec flag will cause this file descriptor to automatically be closed
    /// instead.
    ///
    /// This flag is `true` by default, matching the behavior of [`std::fs`].
    pub const fn close_on_exec(mut self, close_on_exec: bool) -> SealOptions {
        set_flag!(self.memfd_flags, MFD_CLOEXEC, close_on_exec);
        self
    }

    /// Sets whether the resulting file must have or not have execute permission set.
    ///
    /// If set, the OS is explicitly asked to set the execute permission when `exec` is
    /// `true`, or unset the execute permission when `exec` is `false`. If the OS refuses,
    /// [`SealOptions::create`] tries to set or unset the execute permission, and returns an error
    /// if it fails.
    ///
    /// Calling this function enables the equivalent of calling [`SealOptions::seal_executable`]
    /// with `true` for implementation reasons.
    ///
    /// This flag is neither `true` nor `false` by default; instead behavior is delegated to the
    /// OS's default behavior.
    ///
    /// # Context
    ///
    /// The original `memfd_create(2)` implementation on Linux creates anonymous files with the
    /// executable permission set. Later in Linux 6.3, programs and system administrators were
    /// given tools to control this (see also <https://lwn.net/Articles/918106/>):
    ///
    /// - Setting the sysctl `vm.memfd_noexec = 1` disables creating executable anonymous files
    ///   unless the program requests it with `MFD_EXEC`.
    /// - Setting the sysctl `vm.memfd_noexec = 2` disables the ability to create executable
    ///   anonymous files altogether, and `MFD_NOEXEC_SEAL` _must_ be used.
    /// - Calling `memfd_create(2)` with `MFD_NOEXEC_SEAL` enables the `F_SEAL_EXEC` seal.
    ///
    /// Linux prior to 6.3 is unaware of `MFD_EXEC` and `F_SEAL_EXEC`. If `memfd_create(2)` sets
    /// `errno` to `EINVAL`, this library retries the call without possibly-unknown flags, and the
    /// permission bits of the memfd are adjusted depending on this setting.
    pub const fn executable(mut self, executable: bool) -> SealOptions {
        self.memfd_flags = self.memfd_flags & !MFD_EXEC & !MFD_NOEXEC_SEAL
            | if executable {
                MFD_EXEC
            } else {
                MFD_NOEXEC_SEAL
            };
        self.seal_flags |= F_SEAL_EXEC;
        self
    }

    const fn is_executable_set(&self) -> bool {
        self.memfd_flags & (MFD_EXEC | MFD_NOEXEC_SEAL) != 0
    }

    seal!(
        seal_seals,
        must_seal_seals,
        F_SEAL_SEAL,
        "prevent further seals from being set on this file",
        true
    );
    seal!(
        seal_shrinking,
        must_seal_shrinking,
        F_SEAL_SHRINK,
        "prevent shrinking this file",
        true
    );
    seal!(
        seal_growing,
        must_seal_growing,
        F_SEAL_GROW,
        "prevent growing this file",
        true
    );
    seal!(
        seal_writing,
        must_seal_writing,
        F_SEAL_WRITE,
        "prevent writing to this file",
        true
    );
    seal!(
        seal_future_writing {
            #[doc = ""]
            #[doc = "This requires at least Linux 5.1."]
        },
        must_seal_future_writing {
            #[doc = ""]
            #[doc = "This requires at least Linux 5.1."]
        },
        F_SEAL_FUTURE_WRITE,
        "prevent directly writing to this file or creating new writable mappings, \
            but allow writes to existing writable mappings",
        false
    );
    seal!(
        seal_executable {
            #[doc = ""]
            #[doc = "If [`SealOptions::executable`] has already been called,"]
            #[doc = "this function does nothing."]
            #[doc = ""]
            #[doc = "This requires at least Linux 6.3."]
        },
        must_seal_executable {
            #[doc = ""]
            #[doc = "This requires at least Linux 6.3."]
        },
        ? seal_executable_preflight : F_SEAL_EXEC,
        "prevent modifying the executable permission of the file",
        false
    );

    const fn seal_executable_preflight(&self) -> bool {
        !self.is_executable_set()
    }

    /// Create an anonymous file, copy the contents of `reader` to it, and seal it.
    ///
    /// # Errors
    ///
    /// This method returns an error when any of [`SealOptions::create`], [`syd::io::copy`], or
    /// [`SealOptions::seal`] fail.
    pub fn copy_and_seal<Fd>(&self, reader: &mut Fd) -> Result<File, Errno>
    where
        Fd: ReadFd,
    {
        let mut file = self.create()?;
        crate::io::copy(reader, &mut file)?;
        self.seal(&mut file)?;
        Ok(file)
    }

    /// Create an unsealed anonymous file with these options.
    ///
    /// It is the caller's responsibility to seal this file after writing with
    /// [`SealOptions::seal`]. If possible, avoid using this function and prefer
    /// [`SealOptions::copy_and_seal`].
    ///
    /// # Errors
    ///
    /// This method returns an error when:
    /// - `memfd_create(2)` fails
    /// - `SealOptions::executable` was set but permissions cannot be changed as required
    pub fn create(&self) -> Result<File, Errno> {
        let file = match memfd_create(DEFAULT_MEMFD_NAME, self.memfd_flags) {
            Ok(file) => file,
            Err(Errno::EINVAL) if self.is_executable_set() => {
                // Linux prior to 6.3 will not know about `MFD_EXEC` or `MFD_NOEXEC_SEAL`,
                // and returns `EINVAL` when it gets unknown flag bits. Retry without the
                // possibly-unknown flag, and then attempt to set the appropriate permissions.
                //
                // (If `vm.memfd_noexec = 2`, we won't hit this branch because the OS returns
                // EACCES.)
                memfd_create(
                    DEFAULT_MEMFD_NAME,
                    self.memfd_flags & !MFD_EXEC & !MFD_NOEXEC_SEAL,
                )?
            }
            Err(err) => return Err(err),
        };

        if self.is_executable_set() {
            let permissions = file.metadata().map_err(|err| err2no(&err))?.permissions();
            let new_permissions =
                Permissions::from_mode(if self.memfd_flags & MFD_NOEXEC_SEAL != 0 {
                    permissions.mode() & !0o111
                } else if self.memfd_flags & MFD_EXEC != 0 {
                    permissions.mode() | 0o111
                } else {
                    return Ok(file);
                });
            if permissions != new_permissions {
                file.set_permissions(new_permissions)
                    .map_err(|err| err2no(&err))?;
            }
        }

        Ok(file)
    }

    /// Seal an anonymous file with these options.
    ///
    /// This should be called on a file created with [`SealOptions::create`]. Attempting to use
    /// this method on other files will likely fail.
    ///
    /// # Errors
    ///
    /// This method returns an error when:
    /// - the `fcntl(2)` `F_ADD_SEALS` command fails (other than `EINVAL`).
    /// - the `fcntl(2)` `F_GET_SEALS` command fails.
    /// - if any required seals are not present (in this case errno is set to `EBADF`).
    pub fn seal(&self, file: &mut File) -> Result<(), Errno> {
        // Set seals in groups, based on how recently the seal was added to Linux. Ignore `EINVAL`;
        // we'll verify against `self.must_seal_flags`.
        for group in [
            F_SEAL_EXEC,                                              // Linux 6.3
            F_SEAL_FUTURE_WRITE,                                      // Linux 5.1
            F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE, // Linux 3.17
        ] {
            match fcntl_add_seals(file, self.seal_flags & group) {
                Ok(()) => {}
                Err(Errno::EINVAL) => {}
                Err(err) => return Err(err),
            }
        }

        if self.is_sealed_inner(file)? {
            Ok(())
        } else {
            Err(Errno::EBADF)
        }
    }

    /// Check if `file` is sealed as required by these options.
    ///
    /// If the file doesn't support sealing (or `fcntl(2)` otherwise returns an error), this method
    /// returns `false`.
    pub fn is_sealed(&self, file: &File) -> bool {
        self.is_sealed_inner(file).unwrap_or(false)
    }

    fn is_sealed_inner(&self, file: &File) -> Result<bool, Errno> {
        Ok(fcntl_get_seals(file)? & self.must_seal_flags == self.must_seal_flags)
    }
}

fn memfd_create(name: &CStr, flags: c_uint) -> Result<File, Errno> {
    nix::sys::memfd::memfd_create(name, MFdFlags::from_bits_retain(flags).into()).map(File::from)
}

fn fcntl_get_seals(file: &File) -> Result<c_int, Errno> {
    fcntl(file, FcntlArg::F_GET_SEALS)
}

fn fcntl_add_seals(file: &File, arg: c_int) -> Result<(), Errno> {
    fcntl(file, FcntlArg::F_ADD_SEALS(SealFlag::from_bits_retain(arg))).map(drop)
}

#[cfg(test)]
mod test {
    use std::{fs::File, os::unix::fs::PermissionsExt as _};

    use super::*;

    #[test]
    fn new() {
        let options = SealOptions {
            memfd_flags: MFD_ALLOW_SEALING,
            seal_flags: 0,
            must_seal_flags: 0,
        };
        assert_eq!(
            options
                .close_on_exec(true)
                .must_seal_seals(true)
                .must_seal_shrinking(true)
                .must_seal_growing(true)
                .must_seal_writing(true)
                .seal_future_writing(false)
                .seal_executable(false),
            SealOptions::new()
        );
    }

    #[test]
    fn flags() {
        const ALL_SEALS: c_int = F_SEAL_SEAL
            | F_SEAL_SHRINK
            | F_SEAL_GROW
            | F_SEAL_WRITE
            | F_SEAL_FUTURE_WRITE
            | F_SEAL_EXEC;

        let mut options = SealOptions::new();
        assert_eq!(options.memfd_flags & MFD_ALLOW_SEALING, MFD_ALLOW_SEALING);

        assert_eq!(options.memfd_flags & MFD_CLOEXEC, MFD_CLOEXEC);
        options = options.close_on_exec(false);
        assert_eq!(options.memfd_flags & MFD_CLOEXEC, 0);
        options = options.close_on_exec(true);
        assert_eq!(options.memfd_flags & MFD_CLOEXEC, MFD_CLOEXEC);

        assert_eq!(
            options.seal_flags & ALL_SEALS,
            ALL_SEALS & !F_SEAL_FUTURE_WRITE & !F_SEAL_EXEC
        );
        assert_eq!(
            options.must_seal_flags & ALL_SEALS,
            ALL_SEALS & !F_SEAL_FUTURE_WRITE & !F_SEAL_EXEC
        );
        options = options
            .must_seal_future_writing(true)
            .must_seal_executable(true);
        assert_eq!(options.seal_flags & ALL_SEALS, ALL_SEALS);
        assert_eq!(options.must_seal_flags & ALL_SEALS, ALL_SEALS);
        // `seal_*(false)` unsets `must_seal_*`
        options = options
            .seal_seals(false)
            .seal_shrinking(false)
            .seal_growing(false)
            .seal_writing(false)
            .seal_future_writing(false)
            .seal_executable(false);
        assert_eq!(options.seal_flags & ALL_SEALS, 0);
        assert_eq!(options.must_seal_flags & ALL_SEALS, 0);
        // `seal_*(true)` does not set `must_seal_*`
        options = options
            .seal_seals(true)
            .seal_shrinking(true)
            .seal_growing(true)
            .seal_writing(true)
            .seal_future_writing(true)
            .seal_executable(true);
        assert_eq!(options.seal_flags & ALL_SEALS, ALL_SEALS);
        assert_eq!(options.must_seal_flags & ALL_SEALS, 0);
        // `must_seal_*(true)` sets `seal_*`
        options = options
            .seal_seals(false)
            .seal_shrinking(false)
            .seal_growing(false)
            .seal_writing(false)
            .seal_future_writing(false)
            .seal_executable(false);
        assert_eq!(options.seal_flags & ALL_SEALS, 0);
        assert_eq!(options.must_seal_flags & ALL_SEALS, 0);
        options = options
            .must_seal_seals(true)
            .must_seal_shrinking(true)
            .must_seal_growing(true)
            .must_seal_writing(true)
            .must_seal_future_writing(true)
            .must_seal_executable(true);
        assert_eq!(options.seal_flags & ALL_SEALS, ALL_SEALS);
        assert_eq!(options.must_seal_flags & ALL_SEALS, ALL_SEALS);
        // `must_seal_*(false)` does not unset `seal_*`
        options = options
            .must_seal_seals(false)
            .must_seal_shrinking(false)
            .must_seal_growing(false)
            .must_seal_writing(false)
            .must_seal_future_writing(false)
            .must_seal_executable(false);
        assert_eq!(options.seal_flags & ALL_SEALS, ALL_SEALS);
        assert_eq!(options.must_seal_flags & ALL_SEALS, 0);
    }

    #[test]
    fn execute_flags() {
        let mut options = SealOptions::new();
        assert_eq!(options.seal_flags & F_SEAL_EXEC, 0);
        options = options.seal_executable(true);
        assert_eq!(options.seal_flags & F_SEAL_EXEC, F_SEAL_EXEC);
        options = options.seal_executable(false);
        assert_eq!(options.seal_flags & F_SEAL_EXEC, 0);

        for _ in 0..2 {
            options = options.executable(true);
            assert_eq!(options.memfd_flags & (MFD_EXEC | MFD_NOEXEC_SEAL), MFD_EXEC);
            assert_eq!(options.seal_flags & F_SEAL_EXEC, F_SEAL_EXEC);
            // no-op once `executable` is called
            options = options.seal_executable(false);
            assert_eq!(options.seal_flags & F_SEAL_EXEC, F_SEAL_EXEC);

            options = options.executable(false);
            assert_eq!(
                options.memfd_flags & (MFD_EXEC | MFD_NOEXEC_SEAL),
                MFD_NOEXEC_SEAL
            );
            assert_eq!(options.seal_flags & F_SEAL_EXEC, F_SEAL_EXEC);
            // no-op once `executable` is called
            options = options.seal_executable(false);
            assert_eq!(options.seal_flags & F_SEAL_EXEC, F_SEAL_EXEC);
        }

        assert_eq!(options.must_seal_flags & F_SEAL_EXEC, 0);
        options = options.must_seal_executable(true);
        assert_eq!(options.seal_flags & F_SEAL_EXEC, F_SEAL_EXEC);
        assert_eq!(options.must_seal_flags & F_SEAL_EXEC, F_SEAL_EXEC);
        options = options.seal_executable(false);
        assert_eq!(options.seal_flags & F_SEAL_EXEC, F_SEAL_EXEC);
        assert_eq!(options.must_seal_flags & F_SEAL_EXEC, 0);
    }

    #[test]
    fn executable() {
        let mut null = File::open("/dev/null").unwrap();
        let file = SealOptions::new()
            .executable(false)
            .copy_and_seal(&mut null)
            .unwrap();
        assert_eq!(file.metadata().unwrap().permissions().mode() & 0o111, 0);

        let file = SealOptions::new()
            .executable(true)
            .copy_and_seal(&mut null)
            .unwrap();
        assert_eq!(file.metadata().unwrap().permissions().mode() & 0o111, 0o111);
    }
}
