//
// 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

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

use std::{
    ffi::CStr,
    fs::{File, Permissions},
    io::{self, Read},
    ops::{Deref, DerefMut},
    os::unix::{fs::PermissionsExt as _, io::AsRawFd, process::CommandExt},
    process::Command,
};

use nix::{
    errno::Errno,
    fcntl::{fcntl, 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,
    },
    sys::memfd::MemFdCreateFlag,
};

use crate::err::err2no;

// SAFETY: The provided slice is nul-terminated and does not contain any interior nul bytes. On Rust
// 1.64 and later (rust-lang/rust#99977), these required invariants are checked at compile time.
//
// The ideal alternative here is to use C-string literals, introduced in Rust 1.77, but that is a
// steep MSRV bump to introduce at time of writing this comment.
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.
    #[allow(clippy::disallowed_methods)]
    let mut file = File::open("/proc/self/exe").map_err(|err| err2no(&err))?;
    if OPTIONS.is_sealed(&file) {
        Ok(())
    } else {
        let mut command = SealedCommand::new(&mut file)?;
        let mut args = std::env::args_os().fuse();
        if let Some(arg0) = args.next() {
            command.arg0(arg0);
        }
        command.args(args);
        Err(err2no(&command.exec()))
    }
}

/// A [`Command`] wrapper that spawns sealed memory-backed programs.
///
/// You can use the standard [`Command`] builder methods (such as [`spawn`][`Command::spawn`] and
/// [`CommandExt::exec`]) via [`Deref` coercion][`DerefMut`].
pub struct SealedCommand {
    inner: Command,
    // we need to keep this memfd open for the lifetime of this struct
    _memfd: File,
}

impl SealedCommand {
    /// Constructs a new [`Command`] for launching the program data in `program` as a sealed
    /// memory-backed file, with the same default configuration as [`Command::new`].
    ///
    /// The memory-backed file will close on `execve(2)`.
    ///
    /// `argv[0]` of the program will default to "syd". [`CommandExt::arg0`] can override this.
    ///
    /// # Errors
    ///
    /// An error is returned if `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<R: Read>(program: &mut R) -> Result<Self, Errno> {
        let mut memfd = OPTIONS.create()?;
        io::copy(program, &mut memfd).or(Err(Errno::EIO))?;
        OPTIONS.seal(&mut memfd)?;

        let mut comm = Command::new(format!("/proc/self/fd/{}", memfd.as_raw_fd()));
        comm.arg0("syd");

        Ok(Self {
            inner: comm,
            _memfd: memfd,
        })
    }
}

impl Deref for SealedCommand {
    type Target = Command;

    fn deref(&self) -> &Command {
        &self.inner
    }
}

impl DerefMut for SealedCommand {
    fn deref_mut(&mut self) -> &mut Command {
        &mut self.inner
    }
}

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.
    ///
    /// This is equivalent to:
    /// ```
    /// # let options = syd::seal::SealOptions::new();
    /// # let reader: &mut &[u8] = &mut &[][..];
    /// let mut file = options.create()?;
    /// std::io::copy(reader, &mut file)?;
    /// options.seal(&mut file)?;
    /// # Ok::<(), std::io::Error>(())
    /// ```
    ///
    /// # Errors
    ///
    /// This method returns an error when any of [`SealOptions::create`], [`std::io::copy`], or
    /// [`SealOptions::seal`] fail.
    pub fn copy_and_seal<R: Read>(&self, reader: &mut R) -> Result<File, Errno> {
        let mut file = self.create()?;
        io::copy(reader, &mut file).or(Err(Errno::EIO))?;
        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().or(Err(Errno::EACCES))?.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)
                    .or(Err(Errno::EACCES))?;
            }
        }

        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, MemFdCreateFlag::from_bits_retain(flags)).map(File::from)
}

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

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

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

    use super::{
        c_int, SealOptions, F_SEAL_EXEC, 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,
    };

    #[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 file = SealOptions::new()
            .executable(false)
            .copy_and_seal(&mut &[][..])
            .unwrap();
        assert_eq!(file.metadata().unwrap().permissions().mode() & 0o111, 0);

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