syd 3.52.0

rock-solid application kernel
Documentation
//
// 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

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

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

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

use crate::{
    compat::{fstatx, AT_EACCESS, STATX_MODE},
    cookie::{safe_faccess, safe_fdlink, safe_linkat},
    fd::PROC_FILE,
    kernel::{syscall_path_handler, to_atflags},
    lookup::{FileType, FsFlags},
    path::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),
            fsflags: FsFlags::MISS_LAST | FsFlags::NO_FOLLOW_LAST | FsFlags::DOTLAST_EEXIST,
            ..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;

    // 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,
        },
        SysArg {
            dirfd: Some(2),
            path: Some(3),
            fsflags: FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST | FsFlags::DOTLAST_EEXIST,
            ..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> {
    let req = request.scmpreq;

    // SysArg has two elements.
    #[expect(clippy::disallowed_methods)]
    let new_path = &args.1.as_ref().unwrap().path;
    #[expect(clippy::disallowed_methods)]
    let old_parg = &args.0.as_ref().unwrap();
    let old_path = &old_parg.path;
    let is_empty = old_parg.is_empty;

    // Using AT_EMPTY_PATH requires CAP_DAC_READ_SEARCH capability.
    // We only use it when caller has explicitly specified it.
    // For all other cases we use proc(5) indirection.
    //
    // linkat(2) 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.
    //
    // We use MUST_PATH, dir refers to the file.
    assert!(old_path.base().is_empty()); // MUST_PATH!
    let fd = old_path.dir();

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

    // Record blocking call so it can get invalidated.
    request.cache.add_sys_block(req, false)?;

    // All done, call underlying system call.
    let result = if is_empty {
        safe_fdlink(fd, new_path.dir(), new_path.base())
    } else {
        safe_linkat(
            PROC_FILE(),
            &XPathBuf::from_self_fd(fd.as_raw_fd())?,
            new_path.dir(),
            new_path.base(),
            AtFlags::AT_SYMLINK_FOLLOW,
        )
    };

    // Remove invalidation record.
    request.cache.del_sys_block(req.id)?;

    result.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.
    safe_faccess(
        fd,
        AccessFlags::R_OK | AccessFlags::W_OK,
        AT_EACCESS | AtFlags::AT_EMPTY_PATH,
    )
    .or(Err(Errno::EPERM))
}