//
// Syd: rock-solid application kernel
// src/kernel/stat.rs: stat syscall handlers
//
// Copyright (c) 2023, 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::io::BufReader;

use libseccomp::ScmpNotifResp;
use nix::{errno::Errno, fcntl::AtFlags, NixPath};

use crate::{
    compat::{
        fstatat64, statx, FileStat64, FileStatx, FileStatxTimestamp, STATX_BASIC_STATS, STATX_MODE,
        STATX_TYPE,
    },
    config::{API_VERSION, MAGIC_LOAD, MAGIC_PREFIX},
    confine::{is_valid_ptr, scmp_arch_bits, EOWNERDEAD},
    fd::parse_fd,
    hash::SydHashSet,
    kernel::to_atflags,
    lookup::{CanonicalPath, FileInfo, FileType, FsFlags},
    path::XPath,
    req::{SysArg, SysFlags, UNotifyEventRequest},
    retry::retry_on_eintr,
    sandbox::{Capability, Options},
};

pub(crate) fn sys_stat(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // Return EFAULT here for invalid pointers.
    if !is_valid_ptr(req.data.args[1], req.data.arch) {
        return request.fail_syscall(Errno::EFAULT);
    }

    let is32 = scmp_arch_bits(req.data.arch) == 32;

    let arg = SysArg {
        path: Some(0),
        flags: SysFlags::CHECK_MAGIC,
        fsflags: FsFlags::MUST_PATH,
        ..Default::default()
    };

    syscall_stat_handler(request, arg, 1, is32)
}

pub(crate) fn sys_stat64(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // Return EFAULT here for invalid pointers.
    if !is_valid_ptr(req.data.args[1], req.data.arch) {
        return request.fail_syscall(Errno::EFAULT);
    }

    let arg = SysArg {
        path: Some(0),
        flags: SysFlags::CHECK_MAGIC,
        fsflags: FsFlags::MUST_PATH,
        ..Default::default()
    };

    syscall_stat_handler(request, arg, 1, false)
}

pub(crate) fn sys_fstat(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // Return EFAULT here for invalid pointers.
    if !is_valid_ptr(req.data.args[1], req.data.arch) {
        return request.fail_syscall(Errno::EFAULT);
    }

    let is32 = scmp_arch_bits(req.data.arch) == 32;

    let arg = SysArg {
        dirfd: Some(0),
        ..Default::default()
    };

    syscall_stat_handler(request, arg, 1, is32)
}

pub(crate) fn sys_fstat64(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // Return EFAULT here for invalid pointers.
    if !is_valid_ptr(req.data.args[1], req.data.arch) {
        return request.fail_syscall(Errno::EFAULT);
    }

    let arg = SysArg {
        dirfd: Some(0),
        ..Default::default()
    };

    syscall_stat_handler(request, arg, 1, false)
}

pub(crate) fn sys_lstat(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // Return EFAULT here for invalid pointers.
    if !is_valid_ptr(req.data.args[1], req.data.arch) {
        return request.fail_syscall(Errno::EFAULT);
    }

    let is32 = scmp_arch_bits(req.data.arch) == 32;

    let arg = SysArg {
        path: Some(0),
        flags: SysFlags::CHECK_MAGIC,
        fsflags: FsFlags::MUST_PATH | FsFlags::NO_FOLLOW_LAST,
        ..Default::default()
    };

    syscall_stat_handler(request, arg, 1, is32)
}

pub(crate) fn sys_lstat64(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // Return EFAULT here for invalid pointers.
    if !is_valid_ptr(req.data.args[1], req.data.arch) {
        return request.fail_syscall(Errno::EFAULT);
    }

    let arg = SysArg {
        path: Some(0),
        flags: SysFlags::CHECK_MAGIC,
        fsflags: FsFlags::MUST_PATH | FsFlags::NO_FOLLOW_LAST,
        ..Default::default()
    };

    syscall_stat_handler(request, arg, 1, false)
}

pub(crate) fn sys_statx(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // Reject undefined/invalid flags.
    //  const AT_STATX_SYNC_AS_STAT: AtFlags = AtFlags::empty();
    const AT_STATX_FORCE_SYNC: AtFlags = AtFlags::from_bits_retain(0x2000);
    const AT_STATX_DONT_SYNC: AtFlags = AtFlags::from_bits_retain(0x4000);
    let atflags = match to_atflags(
        req.data.args[2],
        AtFlags::AT_EMPTY_PATH
            | AtFlags::AT_SYMLINK_NOFOLLOW
            | AtFlags::AT_NO_AUTOMOUNT
            | AT_STATX_FORCE_SYNC
            | AT_STATX_DONT_SYNC,
    ) {
        Ok(atflags) => atflags,
        Err(errno) => return request.fail_syscall(errno),
    };

    // Return EFAULT here for invalid pointers.
    if !is_valid_ptr(req.data.args[4], req.data.arch) {
        return request.fail_syscall(Errno::EFAULT);
    }

    let mut flags = SysFlags::empty();
    let mut fsflags = FsFlags::MUST_PATH;
    if atflags.contains(AtFlags::AT_EMPTY_PATH) {
        flags |= SysFlags::EMPTY_PATH;
    } else {
        flags |= SysFlags::CHECK_MAGIC;
    }
    if atflags.contains(AtFlags::AT_SYMLINK_NOFOLLOW) {
        fsflags |= FsFlags::NO_FOLLOW_LAST;
    }

    let arg = SysArg {
        dirfd: Some(0),
        path: Some(1),
        flags,
        fsflags,
        ..Default::default()
    };

    syscall_stat_handler(request, arg, 4, false)
}

pub(crate) fn sys_newfstatat(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // Return EFAULT here for invalid pointers.
    if !is_valid_ptr(req.data.args[2], req.data.arch) {
        return request.fail_syscall(Errno::EFAULT);
    }

    // Reject undefined/invalid flags.
    let atflags = match to_atflags(
        req.data.args[3],
        AtFlags::AT_EMPTY_PATH | AtFlags::AT_SYMLINK_NOFOLLOW,
    ) {
        Ok(atflags) => atflags,
        Err(errno) => return request.fail_syscall(errno),
    };

    let mut flags = SysFlags::empty();
    let mut fsflags = FsFlags::MUST_PATH;
    if atflags.contains(AtFlags::AT_EMPTY_PATH) {
        flags |= SysFlags::EMPTY_PATH;
    } else {
        flags |= SysFlags::CHECK_MAGIC;
    }
    if atflags.contains(AtFlags::AT_SYMLINK_NOFOLLOW) {
        fsflags |= FsFlags::NO_FOLLOW_LAST;
    }

    let arg = SysArg {
        dirfd: Some(0),
        path: Some(1),
        flags,
        fsflags,
        ..Default::default()
    };

    syscall_stat_handler(request, arg, 2, false)
}

#[expect(clippy::cognitive_complexity)]
fn syscall_stat_handler(
    request: UNotifyEventRequest,
    arg: SysArg,
    arg_stat: usize,
    is32: bool,
) -> ScmpNotifResp {
    syscall_handler!(request, |request: UNotifyEventRequest| {
        let req = request.scmpreq;
        let sandbox = request.get_sandbox();

        // Read the remote path and check for magic path as necessary.
        let (mut path, magic, empty_path) = request.read_path(&sandbox, arg)?;
        let is_fd = empty_path || arg.path.is_none();

        // Check for chroot.
        if sandbox.is_chroot() {
            return Err(if is_fd { Errno::EACCES } else { Errno::ENOENT });
        }

        let has_crypt = sandbox.enabled(Capability::CAP_CRYPT);
        let restrict_stat_bdev = !sandbox.flags.allow_unsafe_stat_bdev();
        let restrict_stat_cdev = !sandbox.flags.allow_unsafe_stat_cdev();
        let mut ghost = false;
        let caps = *sandbox.state;
        let opts = *sandbox.options;
        if magic {
            if sandbox.locked_drop_for(req.pid()) {
                // Sandbox is locked, access denied.
                return Err(Errno::ENOENT);
            }
            drop(sandbox); // release the read-lock.

            // Handle magic prefix "/dev/syd".
            let cmd = path
                .abs()
                .strip_prefix(MAGIC_PREFIX)
                .unwrap_or_else(|| XPath::from_bytes(&path.abs().as_bytes()[MAGIC_PREFIX.len()..]));

            // Handle magic command.
            ghost = handle_magic_stat(&request, cmd)?;
        } else {
            // Handle fstat for files with encryption in progress.
            #[expect(clippy::disallowed_methods)]
            if is_fd && has_crypt {
                // has_crypt asserts crypt_map is Some.
                let files = request.cache.crypt_map.as_ref().unwrap();

                if let Ok(info) = FileInfo::from_fd(path.dir()) {
                    let files = files.0.lock().unwrap_or_else(|err| err.into_inner());
                    for (enc_path, map) in files.iter() {
                        if info == map.info {
                            // Found underlying encrypted file for the memory fd.
                            // We only ever attempt to encrypt regular files.
                            path = CanonicalPath::new_crypt(
                                path.dir.take().unwrap(),
                                enc_path.clone(),
                            );
                            break;
                        }
                    }
                } // Lock is released here.
            }

            // Return correct stat information for !memfd:syd/ paths.
            // This prefix is internal to Syd and sandbox process cannot
            // create memory file descriptors with this name prefix.
            if is_fd && path.is_memory_fd() && path.abs().starts_with(b"!memfd:syd") {
                let mut p = path.take();
                p.drain(0..b"!memfd:syd".len());
                path = CanonicalPath::new_mask(&p, &p)?;
            }

            // Return correct stat information for masked paths.
            // Fd-only stat(2) calls return correct value already.
            if !is_fd {
                if let Some(mask) = sandbox.is_masked(path.abs()) {
                    let mask = if let Some(mask_dir) = &mask.mask_dir {
                        // Override mask for directories as necessary.
                        if path.is_dir() {
                            Some(mask_dir)
                        } else {
                            mask.mask_all.as_ref()
                        }
                    } else {
                        mask.mask_all.as_ref()
                    };
                    match mask {
                        None => path = CanonicalPath::new_null(),
                        Some(mask) => path = CanonicalPath::new_mask(mask, path.abs())?,
                    };
                }
            }

            drop(sandbox); // release the read-lock.
        }

        // We use MUST_PATH, dir refers to the file.
        assert!(path.base().is_empty()); // MUST_PATH!
        let fd = path.dir();
        let mut flags = libc::AT_EMPTY_PATH;

        #[expect(clippy::cast_possible_truncation)]
        if arg_stat == 4 {
            // statx

            // Support AT_STATX_* flags.
            flags |= req.data.args[2] as libc::c_int
                & !(libc::AT_SYMLINK_NOFOLLOW | libc::AT_EMPTY_PATH);

            // The sidechannel check below requires the mask to have the following items:
            // 1. STATX_TYPE (to check for char/block device)
            // 2. STATX_MODE (to check for world readable/writable)
            // To ensure that here, we inject these two flags into mask
            // noting if they were set originally. This can be in three
            // ways,
            // (a) Explicitly setting STATX_{TYPE,MODE}.
            // (b) Explicitly setting STATX_BASIC_STATS.
            // (c) Setting the catch-all STATX_ALL flag.
            // After the statx(2) call if the flags STATX_{TYPE,MODE}
            // weren't set we clear stx_mode's type and mode bits as
            // necessary and also remove STATX_{TYPE,MODE} from stx_mask
            // as necessary.
            let mut mask = req.data.args[3] as libc::c_uint;
            let orig_mask = mask;
            let basic_stx = (orig_mask & STATX_BASIC_STATS) != 0;
            if !basic_stx {
                mask |= STATX_TYPE | STATX_MODE;
            }

            let mut statx = retry_on_eintr(|| statx(fd, c"", flags, mask))?;

            // Check if the file is a sidechannel device and update its
            // access and modification times to match the creation time
            // if it is. This prevents timing attacks on block or
            // character devices like /dev/ptmx using stat.
            if restrict_stat_bdev || restrict_stat_cdev {
                let filetype = FileType::from(libc::mode_t::from(statx.stx_mode));
                if (restrict_stat_bdev && filetype.is_block_device())
                    || (restrict_stat_cdev && filetype.is_char_device())
                {
                    statx.stx_atime = statx.stx_ctime;
                    statx.stx_mtime = statx.stx_ctime;
                }
            }

            // Restore mask, type and mode, see the comment above.
            #[expect(clippy::cast_possible_truncation)]
            if !basic_stx {
                if (orig_mask & STATX_TYPE) == 0 {
                    statx.stx_mode &= !libc::S_IFMT as u16;
                    statx.stx_mask &= !STATX_TYPE;
                }
                if (orig_mask & STATX_MODE) == 0 {
                    statx.stx_mode &= libc::S_IFMT as u16;
                    statx.stx_mask &= !STATX_MODE;
                }
            }

            // If magic path, mask values for easy id.
            if magic {
                magic_statx(&mut statx, caps, opts);
            }

            // SAFETY: Create an immutable byte slice of struct statx.
            // This slice cannot outlive the struct on the stack.
            let statx = unsafe {
                std::slice::from_raw_parts(
                    std::ptr::addr_of!(statx) as *const u8,
                    size_of_val(&statx),
                )
            };
            let addr = req.data.args[4];
            if addr != 0 {
                request.write_mem(statx, addr)?;
            }
        } else {
            // "stat" | "fstat" | "lstat" | "newfstatat"

            let mut stat = retry_on_eintr(|| fstatat64(fd, c"", flags))?;

            // Check if the file is a sidechannel device and update its
            // access and modification times to match the creation time
            // if it is. This prevents timing attacks on block or
            // character devices like /dev/ptmx using stat.
            if restrict_stat_bdev || restrict_stat_cdev {
                let filetype = FileType::from(stat.st_mode);
                if (restrict_stat_bdev && filetype.is_block_device())
                    || (restrict_stat_cdev && filetype.is_char_device())
                {
                    stat.st_atime = stat.st_ctime;
                    stat.st_mtime = stat.st_ctime;
                    stat.st_atime_nsec = stat.st_ctime_nsec;
                    stat.st_mtime_nsec = stat.st_ctime_nsec;
                }
            }

            // If magic path, mask values for easy id.
            if magic {
                magic_stat(&mut stat, caps, opts);
            }

            let addr = req.data.args[arg_stat];
            if addr != 0 {
                if is32 {
                    let stat32: crate::compat::stat32 = stat.into();

                    // SAFETY: Create an immutable byte slice of struct stat32.
                    // This slice cannot outlive the struct on the stack.
                    let stat = unsafe {
                        std::slice::from_raw_parts(
                            std::ptr::addr_of!(stat32) as *const u8,
                            size_of_val(&stat32),
                        )
                    };
                    request.write_mem(stat, addr)?;
                } else {
                    // SAFETY: Create an immutable byte slice of struct stat.
                    // This slice cannot outlive the struct on the stack.
                    let stat = unsafe {
                        std::slice::from_raw_parts(
                            std::ptr::addr_of!(stat) as *const u8,
                            size_of_val(&stat),
                        )
                    };
                    request.write_mem(stat, addr)?;
                }
            }
        }

        // Use the pseudo errno(3) EOWNERDEAD to initiate ghost mode.
        // We only do it here to ensure metadata of /dev/null was
        // written to sandbox process memory.
        if ghost {
            return Ok(ScmpNotifResp::new(0, 0, EOWNERDEAD, 0));
        }

        // stat(2) system call has been successfully emulated.
        Ok(request.return_syscall(0))
    })
}

fn handle_magic_stat(request: &UNotifyEventRequest, cmd: &XPath) -> Result<bool, Errno> {
    // Set to true if ghost mode is initiated.
    let mut ghost = false;

    // Acquire a write lock to the sandbox.
    let mut sandbox = request.get_mut_sandbox();

    // Execute magic command.
    if cmd.is_empty() || cmd.is_equal(b".el") || cmd.is_equal(b".sh") {
        // Call no-ops into sandbox for logging as necessary.
        sandbox.config("")?;
    } else if cmd.is_equal(b"panic") {
        // Rejects if locked or drop-only.
        sandbox.panic()?;
    } else if cmd.is_equal(b"ghost") {
        // Reset sandbox to ensure no run-away execs:
        // Reset rejects if drop-only for !ghost.
        // Reset doesn't change state of Crypt sandboxing.
        sandbox.reset(true)?;
        ghost = true;
    } else if let Some(cmd) = cmd.strip_prefix(b"load") {
        // We handle load specially here as it involves process access.
        // 1. Attempt to parse as FD, pidfd_getfd and load it.
        // 2. Attempt to parse as profile name if (1) fails.
        match parse_fd(cmd) {
            Ok(remote_fd) => {
                // parse_config() checks for the file name "/dev/syd/load", and
                // disables config file include feature depending on this check.
                let name = XPath::from_bytes(MAGIC_LOAD);
                let file = request.get_fd(remote_fd).map(BufReader::new)?;
                // FIXME: Do not waste getrandom(2) cycles for the unused hashset.
                sandbox
                    .parse_config(file, name, &mut SydHashSet::default() /*unused*/)
                    .map_err(|err| err.errno().unwrap_or(Errno::EINVAL))?;
                // Fall through to emulate as /dev/null.
            }
            Err(Errno::EBADF) => {
                sandbox
                    .parse_profile(cmd.as_bytes())
                    .map_err(|err| err.errno().unwrap_or(Errno::EINVAL))?;
                // Fall through to emulate as /dev/null.
            }
            Err(errno) => return Err(errno),
        }
    } else {
        // Invalid UTF-8 is not permitted.
        // To include non-UTF-8, user must hex-encoded arguments.
        std::str::from_utf8(cmd.as_bytes())
            .or(Err(Errno::EINVAL))
            .and_then(|cmd| sandbox.config(cmd))?;
    }

    // Sandbox write lock released here.
    // Return true if ghost mode was initiated.
    Ok(ghost)
}

fn magic_stat(stat: &mut FileStat64, caps: Capability, opts: Options) {
    stat.st_ino = 0;
    stat.st_nlink = caps.nlink().into();
    stat.st_mode = magic_mode(caps, opts).into();
    stat.st_rdev = API_VERSION.dev();
    stat.st_atime = 505958400; // must match libsyd!
    stat.st_ctime = -2036448000; // ditto!
    stat.st_mtime = -842745600; // ditto!
}

fn magic_statx(statx: &mut FileStatx, caps: Capability, opts: Options) {
    statx.stx_ino = 0;
    statx.stx_nlink = caps.nlink();
    statx.stx_mode = magic_mode(caps, opts);
    statx.stx_rdev_major = API_VERSION.major().into();
    statx.stx_rdev_minor = API_VERSION.minor().into();
    statx.stx_atime = FileStatxTimestamp {
        tv_sec: 505958400, // must match libsyd!
        ..Default::default()
    };
    statx.stx_ctime = FileStatxTimestamp {
        tv_sec: -2036448000, // ditto!
        ..Default::default()
    };
    statx.stx_mtime = FileStatxTimestamp {
        tv_sec: -842745600, // ditto!
        ..Default::default()
    };
}

#[expect(clippy::cast_possible_truncation)]
fn magic_mode(caps: Capability, opts: Options) -> u16 {
    // Start with file type = character device
    let mut mode: u16 = libc::S_IFCHR as u16;

    // Special bits
    if opts.contains(Options::OPT_UNSHARE_MOUNT) {
        mode |= libc::S_ISVTX as u16;
    }
    if opts.contains(Options::OPT_UNSHARE_USER) {
        mode |= libc::S_ISUID as u16;
    }
    if opts.contains(Options::OPT_UNSHARE_NET) {
        mode |= libc::S_ISGID as u16;
    }

    // Permission bits for owner/group/other
    if caps.contains(Capability::CAP_READ) {
        mode |= libc::S_IRUSR as u16;
    }
    if caps.contains(Capability::CAP_WRITE) {
        mode |= libc::S_IWUSR as u16;
    }
    if caps.contains(Capability::CAP_EXEC) {
        mode |= libc::S_IXUSR as u16;
    }
    if caps.contains(Capability::CAP_STAT) {
        mode |= libc::S_IRGRP as u16;
    }
    if caps.contains(Capability::CAP_PROXY) {
        mode |= libc::S_IWGRP as u16;
    }
    if caps.contains(Capability::CAP_TPE) {
        mode |= libc::S_IXGRP as u16;
    }
    if caps.contains(Capability::CAP_LOCK) {
        mode |= libc::S_IROTH as u16;
    }
    if caps.contains(Capability::CAP_CRYPT) {
        mode |= libc::S_IWOTH as u16;
    }
    if caps.contains(Capability::CAP_FORCE) {
        mode |= libc::S_IXOTH as u16;
    }

    mode
}
