#![allow(clippy::undocumented_unsafe_blocks)]

use std::{
    env, mem,
    os::{
        fd::{AsRawFd, RawFd},
        unix::ffi::OsStrExt,
    },
    ptr,
};

use btoi::btoi;
use libloading::os::unix::Symbol;
use nix::{
    errno::Errno,
    libc,
    sched::{unshare, CloneFlags},
    sys::signal::{kill, sigprocmask, SigSet, SigmaskHow, Signal},
    unistd::{close, getpid, read, write},
};

use crate::{
    caps,
    config::*,
    fs::{duprand, set_cloexec},
    safe_drop_cap,
    unshare::{
        error::ErrorCode as Err,
        run::{ChildInfo, Exe},
    },
};

unsafe fn fail_errno(code: Err, errno: i32) -> ! {
    let msg = match code {
        Err::CapSet => c"syd: capset error".as_ptr(),
        Err::Exec => c"syd: exec error".as_ptr(),
        Err::ParentDeathSignal => c"syd: parent-death-signal error".as_ptr(),
        Err::PreExec => c"syd: pre-exec error".as_ptr(),
        Err::ProcessStop => c"syd: error stopping process".as_ptr(),
        Err::ResetSignal => c"syd: error reseting signals".as_ptr(),
        Err::Seccomp => c"syd: seccomp error".as_ptr(),
        Err::SeccompSendFd => c"syd: seccomp send notify-fd error".as_ptr(),
        Err::SeccompWaitFd => c"syd: seccomp wait for notify-fd error".as_ptr(),
        Err::UnshareFiles => c"syd: error unsharing files".as_ptr(),
        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
        Err::SetTSC => c"syd: set-tsc error".as_ptr(),
    };
    errno::set_errno(errno::Errno(errno));
    libc::perror(msg as *const libc::c_char);
    libc::_exit(errno);
}

macro_rules! fail_safe {
    ($child:expr, $error:expr) => {
        let errno = Errno::last_raw();
        unsafe { fail_errno($error, errno) }
    };
}

macro_rules! fail_errno_safe {
    ($child:expr, $error:expr, $errno:expr) => {
        unsafe { fail_errno($error, $errno) }
    };
}

#[allow(clippy::cognitive_complexity)]
pub extern "C" fn child_after_clone(arg: *mut libc::c_void) -> libc::c_int {
    // SAFETY: arg is a valid ChildInfo structure.
    let child: Box<ChildInfo> = unsafe { Box::from_raw(arg as *mut ChildInfo) };

    if let Some(&sig) = child.cfg.death_sig.as_ref() {
        if let Err(errno) = Errno::result(unsafe {
            libc::prctl(libc::PR_SET_PDEATHSIG, sig as libc::c_ulong, 0, 0, 0)
        }) {
            fail_errno_safe!(child, Err::ParentDeathSignal, errno as i32);
        }
    }

    if child.cfg.restore_sigmask {
        // Reset blocking signals.
        // Step 1: Reset the signal mask using pthread_sigmask.
        unsafe {
            let mut sigmask: libc::sigset_t = mem::zeroed();
            libc::sigemptyset(&mut sigmask);
            libc::pthread_sigmask(libc::SIG_SETMASK, &sigmask, ptr::null_mut());
        }
        // Step 2: Unblock all signals using sigprocmask.
        let sigmask = SigSet::all();
        if let Err(errno) = sigprocmask(SigmaskHow::SIG_UNBLOCK, Some(&sigmask), None) {
            fail_errno_safe!(child, Err::ResetSignal, errno as i32);
        }

        // Reset all signals to their default dispositions.
        if let Err(errno) = crate::reset_signals() {
            fail_errno_safe!(child, Err::ResetSignal, errno as i32);
        }
    }

    if let Some(callback) = &child.pre_exec {
        if let Err(errno) = callback() {
            fail_errno_safe!(child, Err::PreExec, errno as i32);
        }
    }

    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
    if child.cfg.deny_tsc {
        if let Err(errno) =
            Errno::result(unsafe { libc::prctl(libc::PR_SET_TSC, libc::PR_TSC_SIGSEGV) })
        {
            fail_errno_safe!(child, Err::SetTSC, errno as i32);
        }
    }

    if child.cfg.stop {
        // Stop the process to give the parent a chance to seize us and set ptrace options.
        // This must happen _before_ loading the seccomp filter.
        if let Err(errno) = kill(getpid(), Signal::SIGSTOP) {
            fail_errno_safe!(child, Err::ProcessStop, errno as i32);
        }
    }

    let fdpair = if let Some(seccomp_filter) = &child.seccomp_filter {
        // We'll write seccomp notify fd to the second pipe,
        // and read the acknowledgement notification from
        // the first pipe.
        let pipe_ro = &child.seccomp_pipefd.0 .0;
        let pipe_rw = &child.seccomp_pipefd.1 .1;

        // Load the seccomp filter.
        if let Err(scmp_err) = seccomp_filter.load() {
            fail_errno_safe!(
                child,
                Err::Seccomp,
                scmp_err
                    .sysrawrc()
                    .map(|errno| errno.abs())
                    .unwrap_or_else(|| Errno::last() as i32)
            );
        }

        // Get seccomp notification fd.
        // SAFETY: ScmpFilterContext::get_notify_fd does not move the
        // ownership of the file descriptor so we must not close it.
        let seccomp_fd = match seccomp_filter.get_notify_fd() {
            Ok(fd) => {
                // SAFETY: Duplicate the fd to a random location,
                // for added hardening. This fd is O_CLOEXEC too.
                match duprand(fd) {
                    Ok(fd_rand) => {
                        let _ = close(fd);
                        fd_rand
                    }
                    Err(errno) => fail_errno_safe!(child, Err::Seccomp, errno as i32),
                }
            }
            Err(scmp_err) => fail_errno_safe!(
                child,
                Err::Seccomp,
                scmp_err
                    .sysrawrc()
                    .map(|errno| errno.abs())
                    .unwrap_or_else(|| Errno::last() as i32)
            ),
        };

        // Write the value of the fd to the pipe.
        // Handle partial writes and interrupts.
        let fd_bytes = seccomp_fd.to_le_bytes();
        let mut nwrite = 0;
        while nwrite < fd_bytes.len() {
            #[allow(clippy::arithmetic_side_effects)]
            match write(pipe_rw, &fd_bytes[nwrite..]) {
                Ok(0) => {
                    // Parent died before reading,
                    // this should never happen.
                    fail_errno_safe!(child, Err::SeccompSendFd, Errno::EIO as i32);
                }
                Ok(n) => nwrite += n,
                Err(Errno::EINTR | Errno::EAGAIN) => continue,
                Err(errno) => fail_errno_safe!(child, Err::SeccompSendFd, errno as i32),
            }
        }

        // Wait for the parent to get the file descriptor.
        // Read PID-FD number as reply from the pipe.
        // Handle partial reads and interrupts.
        let mut pid_buf = [0u8; std::mem::size_of::<RawFd>()];
        let mut nread = 0;
        while nread < pid_buf.len() {
            #[allow(clippy::arithmetic_side_effects)]
            match read(pipe_ro.as_raw_fd(), &mut pid_buf[nread..]) {
                Ok(0) => {
                    // Parent died before writing,
                    // this should never happen.
                    fail_errno_safe!(child, Err::SeccompWaitFd, Errno::EIO as i32);
                }
                Ok(n) => nread += n,
                Err(Errno::EINTR | Errno::EAGAIN) => continue,
                Err(errno) => fail_errno_safe!(child, Err::SeccompWaitFd, errno as i32),
            }
        }

        let pid_fd = match pid_buf.as_slice().try_into() {
            Ok(bytes) => RawFd::from_le_bytes(bytes),
            Err(_) => fail_errno_safe!(child, Err::SeccompWaitFd, Errno::EINVAL as i32),
        };

        Some((seccomp_fd, pid_fd))
    } else {
        None
    };

    // Drop the seccomp filter which is going to close down the owned
    // seccomp notify file descriptor. We do this after the fd transfer
    // is done to ensure a potential memory {de,}allocation system call
    // cannot deadlock the process in case memory sandboxing is on.
    drop(child.seccomp_filter);

    // Close down the pipe now that the transfer is done.
    // Single drop is adequate here, beauty of owned fds ;)
    drop(child.seccomp_pipefd);

    // SAFETY: Do not leak the log file descriptor to the sandbox process.
    // We will close it after CLONE_FILES for library load,
    // and set it to O_CLOEXEC for program runs.
    let log_fd = if let Some(fd) = env::var_os(ENV_LOG_FD) {
        btoi::<RawFd>(fd.as_bytes()).ok()
    } else {
        None
    };

    match child.exe {
        Exe::Program(_) => {
            // Nothing left to do, both seccomp-fd and pid-fd
            // are close-on-exec and file table will be unshared
            // on execve(2). Careful, the fd may be invalid...
            if let Some(fd) = log_fd {
                match set_cloexec(&fd, true) {
                    Ok(_) | Err(Errno::EBADF) => {}
                    Err(errno) => fail_errno_safe!(child, Err::UnshareFiles, errno as i32),
                }
            }
        }
        Exe::Library(_) => {
            // Remove the effect of CLONE_FILES, and
            // close down our copies of the pid-fd and seccomp-fd.
            // We only need to do this if we're loading a library,
            // as execve(2) already unshares the file table itself.
            if let Err(errno) = unshare(CloneFlags::CLONE_FILES) {
                fail_errno_safe!(child, Err::UnshareFiles, errno as i32);
            }

            // Close static file descriptors used by syd::proc and friends.
            proc_close();

            if let Some((seccomp_fd, pid_fd)) = fdpair {
                let _ = close(pid_fd);
                let _ = close(seccomp_fd);
            }

            // Close the log descriptor if any.
            if let Some(fd) = log_fd {
                let _ = close(fd);
            }

            // SAFETY: Do not leak the poll file descriptor to the sandbox process.
            // Note, we only have to do this for library load,
            // as epoll file descriptor is O_CLOEXEC and will be closed for program runs.
            if let Some(fd) = env::var_os(ENV_POLL_FD) {
                if let Ok(fd) = btoi::<RawFd>(fd.as_bytes()) {
                    let _ = close(fd);
                }
            }

            // SAFETY: Even after all this, some fds can leak into the library.
            // See: https://builds.sr.ht/~alip/job/1409189#task-test for an example.
            // Until a valid use-case is displayed, let's close all but the standard fds.
            if let Err(errno) = Errno::result(unsafe {
                libc::syscall(libc::SYS_close_range, 3, libc::c_int::MAX, 0)
            }) {
                fail_errno_safe!(child, Err::UnshareFiles, errno as i32);
            }
        }
    }

    // Drop CAP_SYS_PTRACE late as Syd may need it.
    if !child.cfg.keep && safe_drop_cap(caps::Capability::CAP_SYS_PTRACE).is_err() {
        fail_safe!(child, Err::CapSet);
    }
    if caps::securebits::set_keepcaps(true).is_err() {
        fail_safe!(child, Err::CapSet);
    }

    // SAFETY: Clean Syd environment variables from process environment.
    for (key, _) in env::vars_os() {
        if key.as_bytes().starts_with(b"SYD_") && !key.as_bytes().starts_with(b"SYD_TEST_") {
            env::remove_var(key);
        }
    }

    match child.exe {
        Exe::Library(lib) => unsafe {
            let fun: Symbol<unsafe extern "C" fn() -> i32> = match lib.get(b"syd_main") {
                Ok(fun) => fun,
                Err(_) => nix::libc::_exit(nix::libc::EINVAL),
            };
            nix::libc::_exit(fun());
        },
        Exe::Program((filename, ref args)) => {
            let args = &args[..];
            unsafe { libc::execvp(filename, args.as_ptr()) };
            fail_safe!(child, Err::Exec);
        }
    }
}
