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

use libseccomp::ScmpNotifResp;
use nix::errno::Errno;

use crate::{
    ioctl::{ioctl_names_get, Ioctl},
    kernel::sandbox_path,
    log_enabled,
    lookup::FsFlags,
    req::{SysArg, UNotifyEventRequest},
    sandbox::{Action, Capability},
    syslog::LogLevel,
    warn,
};

#[expect(clippy::cognitive_complexity)]
pub(crate) fn sys_ioctl(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_handler!(request, |request: UNotifyEventRequest| {
        let req = request.scmpreq;
        let sandbox = request.get_sandbox();

        // Read the remote path.
        //
        // WANT_READ: ioctl(2) does not work with O_PATH fds.
        let arg = SysArg {
            dirfd: Some(0),
            fsflags: FsFlags::MUST_PATH | FsFlags::WANT_READ,
            ..Default::default()
        };
        let (path, _, _) = request.read_path(&sandbox, arg, false)?;

        // Check if the ioctl(2) request is allowlisted or denylisted.
        let arg = req.data.args[1];
        let list = sandbox.ioctl_is_listed(arg, req.data.arch);
        let mut caps = Capability::CAP_STAT;
        if list.is_none() {
            // Check for ioctl(2) path access unless
            // the request was explicitly allowed or denied.
            caps.insert(Capability::CAP_IOCTL);
        }

        // Check for path access.
        sandbox_path(
            Some(&request),
            &sandbox,
            request.scmpreq.pid(), // Unused when request.is_some()
            path.abs(),
            caps,
            true,
            "ioctl",
        )?;

        // Check file type after path hiding.
        let restrict_magiclinks = !sandbox.flags.allow_unsafe_magiclinks();
        let restrict_mkbdev = !sandbox.flags.allow_unsafe_mkbdev();
        if let Some(typ) = path.typ.as_ref() {
            // Restriction 1: Deny block device ioctl(2),
            // unless trace/allow_unsafe_mkbdev:1 is set.
            if restrict_mkbdev && typ.is_block_device() {
                return Err(Errno::EACCES);
            }

            // Restriction 2: Deny magic link ioctl(2),
            // unless trace/allow_unsafe_magiclinks:1 is set.
            if restrict_magiclinks && typ.is_magic_link() {
                return Err(Errno::EACCES);
            }
        } else {
            // No file type, file disappeared mid-way?
            return Err(Errno::ENOTTY);
        }

        // Check ioctl(2) request access after path hiding.
        if list == Some(true) {
            // _ioctl_(2) request is denylisted.
            let cap = Capability::CAP_IOCTL;
            let action = sandbox.default_action(cap);
            let verbose = sandbox.verbose;
            drop(sandbox); // release the read-lock.

            let filter = action == Action::Filter;
            if !filter && action >= Action::Warn && log_enabled!(LogLevel::Warn) {
                let ctl = ioctl_names_get(arg as Ioctl, req.data.arch);
                let grp = cap.to_string().to_ascii_lowercase();
                if verbose {
                    warn!("ctx": "access", "cap": cap, "act": action,
                        "sys": "ioctl", "ctl": ctl,
                        "tip": format!("configure `{grp}/allow+{arg:#x}'"),
                        "req": &request);
                } else {
                    warn!("ctx": "access", "cap": cap, "act": action,
                        "sys": "ioctl", "ctl": ctl,
                        "tip": format!("configure `{grp}/allow+{arg:#x}'"),
                        "pid": request.scmpreq.pid);
                }
            }

            return match action {
                Action::Allow | Action::Warn => {
                    // SAFETY: ioctl is fd-only.
                    Ok(unsafe { request.continue_syscall() })
                }
                Action::Filter | Action::Deny => Err(Errno::EACCES),
                Action::Panic => panic!(),
                Action::Exit => std::process::exit(libc::EACCES),
                action => {
                    // Stop|Kill
                    let _ = request.kill(action);
                    Err(Errno::EACCES)
                }
            };
        }

        // SAFETY: ioctl is fd-only.
        Ok(unsafe { request.continue_syscall() })
    })
}
