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

use nix::{
    errno::Errno,
    sys::signal::{kill, Signal},
    unistd::Pid,
};

use crate::{
    compat::{fstatx, statx},
    config::PROC_FILE,
    debug, error,
    kernel::sandbox_path,
    lookup::{CanonicalPath, FileInfo, FsFlags},
    path::XPathBuf,
    ptrace::{ptrace_get_error, ptrace_syscall_info, ptrace_syscall_info_seccomp},
    req::{RemoteProcess, SysArg},
    sandbox::{Capability, SandboxGuard},
};

// Note, chdir is a ptrace(2) hook, not a seccomp hook!
// The seccomp hook is only used with trace/allow_unsafe_ptrace:1.
pub(crate) fn sysenter_chdir<'a>(
    pid: Pid,
    sandbox: &SandboxGuard,
    data: ptrace_syscall_info_seccomp,
) -> Result<CanonicalPath<'a>, Errno> {
    let mut arg = SysArg {
        path: Some(0),
        ..Default::default()
    };

    // SAFETY: Apply deny_dotdot as necessary for chdir.
    if sandbox.flags.deny_dotdot() {
        arg.fsflags.insert(FsFlags::NO_RESOLVE_DOTDOT);
    }

    // Read remote path.
    let process = RemoteProcess::new(pid);

    // SAFETY: This is a ptrace hook, the PID cannot be validated.
    let (path, _, _, _) = process.read_path(sandbox, data.args, arg, false, None)?;

    // Check for chroot, allow for the common `cd /` use case.
    if sandbox.is_chroot() {
        return if path.abs().is_root() {
            Ok(path)
        } else {
            Err(Errno::ENOENT)
        };
    }

    let hide = sandbox.enabled(Capability::CAP_STAT);
    let mut caps = Capability::empty();
    if let Some(typ) = path.typ.as_ref() {
        if typ.is_dir() {
            caps.insert(Capability::CAP_CHDIR);
        }
    } else {
        return Err(Errno::ENOENT);
    }

    sandbox_path(None, sandbox, pid, path.abs(), caps, hide, "chdir")?;

    if !caps.contains(Capability::CAP_CHDIR) {
        // SAFETY: Return this after sandboxing to honour hidden paths.
        return Err(Errno::ENOTDIR);
    }

    Ok(path)
}

#[expect(clippy::cognitive_complexity)]
pub(crate) fn sysexit_chdir(
    pid: Pid,
    info: ptrace_syscall_info,
    path: CanonicalPath,
) -> Result<(), Errno> {
    // Check for successful sigaction exit.
    match ptrace_get_error(pid, info.arch) {
        Ok(None) => {
            // Successful chdir call, validate CWD magiclink.
        }
        Ok(Some(_)) => {
            // Unsuccessful chdir call, continue process.
            return Ok(());
        }
        Err(Errno::ESRCH) => return Err(Errno::ESRCH),
        Err(_) => {
            // SAFETY: Failed to get return value, terminate the process.
            let _ = kill(pid, Some(Signal::SIGKILL));
            return Err(Errno::ESRCH);
        }
    };

    // SAFETY: Validate /proc/$pid/cwd against TOCTTOU!
    let pfd = XPathBuf::from_cwd(pid)?;
    let stx_cwd = match statx(PROC_FILE(), &pfd, 0, FileInfo::mask()) {
        Ok(stx) => stx,
        Err(errno) => {
            // SAFETY: Failed to stat CWD,
            // assume TOCTTOU: terminate the process.
            error!("ctx": "chdir", "op": "stat_cwd_symlink",
                "msg": format!("failed to stat cwd-symlink for `{path}': {errno}"),
                "err": errno as i32, "pid": pid.as_raw(), "path": &path,
                "tip": "check with SYD_LOG=debug and/or submit a bug report");
            let _ = kill(pid, Some(Signal::SIGKILL));
            return Err(Errno::ESRCH);
        }
    };

    #[expect(clippy::disallowed_methods)]
    let fd = path.dir.as_ref().unwrap();

    let stx_fd = match fstatx(fd, FileInfo::mask()) {
        Ok(stx) => stx,
        Err(errno) => {
            // SAFETY: Failed to stat FD,
            // assume TOCTTOU: terminate the process.
            error!("ctx": "chdir", "op": "fstat_dir_fd",
                "msg": format!("failed to fstat dir-fd for `{path}': {errno}"),
                "err": errno as i32, "pid": pid.as_raw(), "path": &path,
                "tip": "check with SYD_LOG=debug and/or submit a bug report");
            let _ = kill(pid, Some(Signal::SIGKILL));
            return Err(Errno::ESRCH);
        }
    };

    // SAFETY: Validate CWD stat information.
    let mut is_match = true;

    // Step 1: Check inodes.
    if stx_fd.stx_ino != stx_cwd.stx_ino {
        is_match = false;
    }

    // Step 2: Compare mount ids.
    if stx_fd.stx_mnt_id != stx_cwd.stx_mnt_id {
        is_match = false;
    }

    if !is_match {
        // SAFETY: CWD changed, which indicates
        // successful TOCTTOU attempt: terminate the process.
        error!("ctx": "chdir", "op": "dir_mismatch",
            "msg": format!("dir mismatch detected for directory `{path}': assume TOCTTOU!"),
            "pid": pid.as_raw(), "path": &path,
            "cwd_mount_id": stx_cwd.stx_mnt_id,
            "dir_mount_id": stx_fd.stx_mnt_id,
            "cwd_inode": stx_cwd.stx_ino,
            "dir_inode": stx_fd.stx_ino);
        let _ = kill(pid, Some(Signal::SIGKILL));
        return Err(Errno::ESRCH);
    } else {
        debug!("ctx": "chdir", "op": "verify_chdir",
            "msg": format!("dir change to `{path}' approved"),
            "pid": pid.as_raw(),
            "path": &path,
            "cwd_mount_id": stx_cwd.stx_mnt_id,
            "dir_mount_id": stx_fd.stx_mnt_id,
            "cwd_inode": stx_cwd.stx_ino,
            "dir_inode": stx_fd.stx_ino);
    }

    // Continue process.
    Ok(())
}
