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

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

use std::os::fd::AsRawFd;

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

use crate::{
    cookie::{safe_fchmod, safe_fchmodat, safe_fchmodat2},
    error,
    fd::{fd_mode, PROC_FILE},
    kernel::{syscall_path_handler, to_atflags, to_mode},
    lookup::{CanonicalPath, FileType, FsFlags},
    path::XPathBuf,
    req::{PathArgs, SysArg, SysFlags, UNotifyEventRequest},
    sandbox::SandboxGuard,
};

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

    // Strip undefined/invalid mode bits.
    let mode = to_mode(req.data.args[1]);

    let argv = &[SysArg {
        dirfd: Some(0),
        fsflags: FsFlags::MUST_PATH,
        ..Default::default()
    }];
    syscall_path_handler(request, "fchmod", argv, |path_args, request, sandbox| {
        // SysArg has one element.
        // We use MUST_PATH, dir refers to the file.
        #[expect(clippy::disallowed_methods)]
        let path = path_args.0.as_ref().unwrap();
        assert!(path.base().is_empty()); // MUST_PATH!

        // We apply force_umask to chmod(2) modes to ensure consistency.
        // Umask is only forced for regular files.
        // Sticky bit is immutable for directories unless trace/allow_unsafe_sticky:1 is set.
        let umask = sandbox.umask.unwrap_or(Mode::empty());
        let restrict_sticky = !sandbox.flags.allow_unsafe_sticky();
        let log_scmp = sandbox.log_scmp();
        drop(sandbox); // release read lock before syscall.
        let mut mode = mode;
        safe_chmod_mode(request, path, &mut mode, umask, restrict_sticky, log_scmp)?;

        safe_fchmod(path.dir(), mode).map(|_| request.return_syscall(0))
    })
}

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

    // Strip undefined/invalid mode bits.
    let mode = to_mode(req.data.args[1]);

    let argv = &[SysArg {
        path: Some(0),
        ..Default::default()
    }];

    syscall_path_handler(request, "chmod", argv, |path_args, request, sandbox| {
        syscall_chmod_handler(request, sandbox, path_args, mode)
    })
}

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

    // Strip undefined/invalid mode bits.
    let mode = to_mode(req.data.args[2]);

    // Note: Unlike fchmodat2, fchmodat always resolves symbolic links.
    let argv = &[SysArg {
        dirfd: Some(0),
        path: Some(1),
        ..Default::default()
    }];

    syscall_path_handler(request, "fchmodat", argv, |path_args, request, sandbox| {
        syscall_chmod_handler(request, sandbox, path_args, mode)
    })
}

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

    // Reject undefined/invalid/unused 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),
    };

    // Strip undefined/invalid mode bits.
    let mode = to_mode(req.data.args[2]);

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

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

    syscall_path_handler(request, "fchmodat2", argv, |path_args, request, sandbox| {
        syscall_chmod_handler(request, sandbox, path_args, mode)
    })
}

/// A helper function to handle chmod, fchmodat, and fchmodat2 syscalls.
fn syscall_chmod_handler(
    request: &UNotifyEventRequest,
    sandbox: SandboxGuard,
    args: PathArgs,
    mut mode: Mode,
) -> Result<ScmpNotifResp, Errno> {
    // SysArg has one element.
    // We use MUST_PATH, dir refers to the file.
    #[expect(clippy::disallowed_methods)]
    let path = args.0.as_ref().unwrap();
    assert!(path.base().is_empty()); // MUST_PATH!
    let fd = path.dir();

    // We apply force_umask to chmod modes to ensure consistency.
    // Umask is only forced for regular files.
    // Sticky bit is immutable for directories unless trace/allow_unsafe_sticky:1 is set.
    let umask = sandbox.umask.unwrap_or(Mode::empty());
    let restrict_sticky = !sandbox.flags.allow_unsafe_sticky();
    let log_scmp = sandbox.log_scmp();
    drop(sandbox); // release read lock before syscall.
    safe_chmod_mode(request, path, &mut mode, umask, restrict_sticky, log_scmp)?;

    match safe_fchmodat2(fd, mode) {
        Ok(_) => Ok(()),
        Err(Errno::ENOSYS) => {
            // Fallback to proc(5) indirection,
            //
            // path to fd is open already!
            let pfd = XPathBuf::from_self_fd(fd.as_raw_fd())?;
            safe_fchmodat(PROC_FILE(), &pfd, mode)
        }
        Err(errno) => Err(errno),
    }
    .map(|_| request.return_syscall(0))
}

#[expect(clippy::cognitive_complexity)]
fn safe_chmod_mode(
    request: &UNotifyEventRequest,
    path: &CanonicalPath,
    mode: &mut Mode,
    umask: Mode,
    restrict_sticky: bool,
    log_scmp: bool,
) -> Result<(), Errno> {
    match path.typ {
        Some(FileType::Reg) => *mode &= !umask,
        Some(FileType::Dir)
            if restrict_sticky
                && !mode.contains(Mode::S_ISVTX)
                && fd_mode(path.dir())?.contains(Mode::S_ISVTX) =>
        {
            mode.insert(Mode::S_ISVTX);
            if log_scmp {
                error!("ctx": "immutable_sticky",
                            "path": path.abs(), "mode": mode.bits(),
                            "msg": "blocked attempt to unset sticky bit",
                            "tip": "fix your program or use `trace/allow_unsafe_sticky:1'",
                            "req": request);
            } else {
                error!("ctx": "immutable_sticky",
                            "path": path.abs(), "mode": mode.bits(),
                            "msg": "blocked attempt to unset sticky bit",
                            "tip": "fix your program or use `trace/allow_unsafe_sticky:1'",
                            "pid": request.scmpreq.pid);
            }
        }
        _ => {}
    }

    Ok(())
}
