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

use std::os::fd::{AsFd, AsRawFd, FromRawFd, IntoRawFd, RawFd};

use libseccomp::{ScmpArch, ScmpNotifResp};
use nix::errno::Errno;

use crate::{
    compat::{Fanotify, MarkFlags, MaskFlags},
    config::PROC_FILE,
    confine::scmp_arch_bits,
    kernel::syscall_path_handler,
    lookup::{file_type, FsFlags},
    path::XPathBuf,
    req::{SysArg, UNotifyEventRequest},
};

#[expect(clippy::arithmetic_side_effects)]
pub(crate) fn sys_fanotify_mark(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // SAFETY: Reject undefined flags.
    let flags: libc::c_uint = match req.data.args[1].try_into() {
        Ok(flags) => flags,
        Err(_) => return request.fail_syscall(Errno::EINVAL),
    };

    // SAFETY: Reject invalid flags.
    let flags = match MarkFlags::from_bits(flags) {
        Some(flags) => flags,
        None => return request.fail_syscall(Errno::EINVAL),
    };

    // SAFETY: Reject undefined/invalid masks.
    // Note, mask is a 64-bit value but kernel uses the lower 32-bits only.
    let is32 = scmp_arch_bits(req.data.arch) == 32;
    let (mask, narg) = if is32 {
        let mask = match req.data.arch {
            ScmpArch::Ppc => req.data.args[3],
            _ => req.data.args[2],
        };

        (mask, 3)
    } else {
        (req.data.args[2], 2)
    };

    let mask = match MaskFlags::from_bits(mask) {
        Some(mask) => mask,
        None => return request.fail_syscall(Errno::EINVAL),
    };

    // fanotify(7) requires read access to the file or directory,
    // with the exception of symbolic links. However, due to
    // the `/proc' indirection in the handler we must not pass
    // WANT_READ here. See LTP fanotify tests.
    let mut fsflags = FsFlags::MUST_PATH;
    if flags.contains(MarkFlags::FAN_MARK_DONT_FOLLOW) {
        fsflags |= FsFlags::NO_FOLLOW_LAST;
    }

    let pidx = narg + 2;
    let argv = &[SysArg {
        dirfd: Some(narg + 1),
        path: if req.data.args[pidx] != 0 {
            Some(pidx)
        } else {
            None
        },
        fsflags,
        ..Default::default()
    }];

    syscall_path_handler(
        request,
        "fanotify_mark",
        argv,
        |path_args, request, sandbox| {
            let restrict_notify_bdev = !sandbox.flags.allow_unsafe_notify_bdev();
            let restrict_notify_cdev = !sandbox.flags.allow_unsafe_notify_cdev();
            drop(sandbox); // release the read-lock.

            // SAFETY: SysArg has one element.
            #[expect(clippy::disallowed_methods)]
            let path = path_args.0.as_ref().unwrap();

            // SAFETY: MUST_PATH asserts dir is Some.
            #[expect(clippy::disallowed_methods)]
            let fd = path.dir.as_ref().map(|fd| fd.as_fd()).unwrap();

            // SAFETY:
            // 1. Strip FAN_{ACCESS,ACCESS_PERM,MODIFY} if we're marking a sidechannel device.
            // 2. Strip FAN_MARK_DONT_FOLLOW which has already been handled during canonicalization.
            let mut mask = mask;
            if restrict_notify_bdev || restrict_notify_cdev {
                // SAFETY:
                // 1. Strip IN_{ACCESS,MODIFY} if we're marking a sidechannel device.
                // 2. Strip IN_DONT_FOLLOW which has already been handled during canonicalization.
                let filetype = file_type(fd, None, false)?;
                if (restrict_notify_bdev && filetype.is_block_device())
                    || (restrict_notify_cdev && filetype.is_char_device())
                {
                    mask.remove(MaskFlags::FAN_ACCESS);
                    mask.remove(MaskFlags::FAN_ACCESS_PERM);
                    mask.remove(MaskFlags::FAN_MODIFY);
                }
            }
            let mut flags = flags;
            flags.remove(MarkFlags::FAN_MARK_DONT_FOLLOW);

            // SAFETY: We open a FD to the path and then use the
            // proc path $PROC_FILE/thread-self/fd/$fd in address'
            // path argument to avoid symlink TOCTOU.
            let pfd = XPathBuf::from_self_fd(fd.as_raw_fd())?;

            // Validate the FANotify FD.
            let notify_fd = RawFd::try_from(req.data.args[0]).or(Err(Errno::EBADF))?;
            if notify_fd < 0 {
                return Err(Errno::EBADF);
            }

            // Get the FANotify FD.
            let notify_fd = request.get_fd(notify_fd).map(|notify_fd| {
                // SAFETY: pidfd_getfd(2) returns valid FD on success.
                unsafe { Fanotify::from_raw_fd(notify_fd.into_raw_fd()) }
            })?;

            // Call fanotify_mark(2) through type-safe interface.
            notify_fd
                .mark(flags, mask, PROC_FILE(), Some(&pfd))
                .map(|_| request.return_syscall(0))
        },
    )
}
