//
// Syd: rock-solid application kernel
// src/kernel/link.rs: link(2) and linkat(2) handlers
//
// Copyright (c) 2023, 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
// safe_hardlink_source function is based in part upon fs/namei.c of Linux kernel which is:
//   Copyright (C) 1991, 1992  Linus Torvalds
//   SPDX-License-Identifier: GPL-2.0
//
// SPDX-License-Identifier: GPL-3.0

use std::os::fd::{AsFd, AsRawFd};

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

use crate::{
    compat::{fstatx, STATX_MODE},
    config::PROC_FILE,
    cookie::safe_linkat,
    kernel::{syscall_path_handler, to_atflags},
    lookup::{FileType, FsFlags},
    path::{XPath, XPathBuf},
    req::{PathArgs, SysArg, SysFlags, UNotifyEventRequest},
};

pub(crate) fn sys_link(request: UNotifyEventRequest) -> ScmpNotifResp {
    let argv = &[
        SysArg {
            path: Some(0),
            fsflags: FsFlags::MUST_PATH | FsFlags::NO_FOLLOW_LAST,
            ..Default::default()
        },
        SysArg {
            path: Some(1),
            dotlast: Some(Errno::ENOENT),
            fsflags: FsFlags::MISS_LAST | FsFlags::NO_FOLLOW_LAST,
            ..Default::default()
        },
    ];

    syscall_path_handler(request, "link", argv, |path_args, request, sandbox| {
        let restrict_hardlinks = !sandbox.flags.allow_unsafe_hardlinks();
        drop(sandbox); // release the read-lock.

        syscall_link_handler(request, path_args, restrict_hardlinks)
    })
}

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

    // SAFETY: Reject undefined/invalid flags.
    let atflags = match to_atflags(
        req.data.args[4],
        AtFlags::AT_EMPTY_PATH | AtFlags::AT_SYMLINK_FOLLOW,
    ) {
        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;
    }
    if !atflags.contains(AtFlags::AT_SYMLINK_FOLLOW) {
        fsflags |= FsFlags::NO_FOLLOW_LAST;
    }

    let argv = &[
        SysArg {
            dirfd: Some(0),
            path: Some(1),
            flags,
            fsflags,
            ..Default::default()
        },
        SysArg {
            dirfd: Some(2),
            path: Some(3),
            dotlast: Some(Errno::ENOENT),
            fsflags: FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
            ..Default::default()
        },
    ];

    syscall_path_handler(request, "linkat", argv, |path_args, request, sandbox| {
        let restrict_hardlinks = !sandbox.flags.allow_unsafe_hardlinks();
        drop(sandbox); // release the read-lock.

        syscall_link_handler(request, path_args, restrict_hardlinks)
    })
}

/// A helper function to handle link{,at} syscalls.
fn syscall_link_handler(
    request: &UNotifyEventRequest,
    args: PathArgs,
    restrict_hardlinks: bool,
) -> Result<ScmpNotifResp, Errno> {
    // SAFETY: SysArg has two elements.
    #[expect(clippy::disallowed_methods)]
    let old_path = args.0.as_ref().unwrap();
    #[expect(clippy::disallowed_methods)]
    let new_path = args.1.as_ref().unwrap();

    // SAFETY: Using AT_EMPTY_PATH requires CAP_DAC_READ_SEARCH
    // capability which we cannot expect to have here.
    // Therefore we must use procfs(5) indirection.
    //
    // Note, linkat does not follow symbolic links in old path by
    // default unless AT_SYMLINK_FOLLOW flag is passed. As such,
    // AT_SYMLINK_NOFOLLOW is an invalid flag for linkat.
    let fd = old_path
        .dir
        .as_ref()
        .map(|fd| fd.as_fd())
        .ok_or(Errno::EBADF)?;
    assert!(old_path.base().is_empty()); // MUST_PATH!

    // SAFETY: Restrictions a la CONFIG_GRKERNSEC_LINK.
    if restrict_hardlinks {
        safe_hardlink_source(fd, old_path.typ.unwrap_or(FileType::Unk))?;
    }

    safe_linkat(
        PROC_FILE(),
        &XPathBuf::from_self_fd(fd.as_raw_fd())?,
        new_path
            .dir
            .as_ref()
            .map(|fd| fd.as_fd())
            .ok_or(Errno::EBADF)?,
        new_path.base(),
        AtFlags::AT_SYMLINK_FOLLOW,
    )
    .map(|_| request.return_syscall(0))
}

// Determine whether creating a hardlink to the given file descriptor is safe,
// based on mode bits and ownership. This implements Linux's protected_hardlinks
// and grsecurity-style GRKERNSEC_LINK policy: disallow hardlinking to setuid/setgid
// or privileged files not owned by the caller.
fn safe_hardlink_source<Fd: AsFd>(fd: Fd, typ: FileType) -> Result<(), Errno> {
    // Check file type.
    if typ.is_symlink() {
        // link(2) does not dereference symlinks,
        // so we allow this file type here.
        // This is consistent with protected_hardlinks=1.
        return Ok(());
    } else if !typ.is_file() {
        // Special files should not get pinned to the filesystem.
        return Err(Errno::EPERM);
    }

    // Check file mode.
    let mode = fstatx(&fd, STATX_MODE).map(|stx| libc::mode_t::from(stx.stx_mode))?;

    // Setuid files should not get pinned to the filesystem.
    if (mode & libc::S_ISUID) != 0 {
        return Err(Errno::EPERM);
    }

    // Executable setgid files should not get pinned to the filesystem.
    if (mode & (libc::S_ISGID | libc::S_IXGRP)) == (libc::S_ISGID | libc::S_IXGRP) {
        return Err(Errno::EPERM);
    }

    // Caller must have both read and write access to the file.
    faccessat(
        &fd,
        XPath::empty(),
        AccessFlags::R_OK | AccessFlags::W_OK,
        crate::compat::AT_EACCESS | AtFlags::AT_EMPTY_PATH,
    )
    .or(Err(Errno::EPERM))
}
