syd 3.52.0

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

use libseccomp::ScmpNotifResp;
use nix::{errno::Errno, unistd::UnlinkatFlags, NixPath};

use crate::{
    cookie::safe_unlinkat,
    kernel::syscall_path_handler,
    lookup::FsFlags,
    req::{PathArgs, SysArg, UNotifyEventRequest},
};

pub(crate) fn sys_rmdir(request: UNotifyEventRequest) -> ScmpNotifResp {
    // rmdir() does not work on fds!
    // Hence, we have to use WANT_BASE to split base.
    let argv = &[SysArg {
        path: Some(0),
        fsflags: FsFlags::NO_FOLLOW_LAST
            | FsFlags::MUST_PATH
            | FsFlags::WANT_BASE
            | FsFlags::DOTLAST_ERMDIR,
        ..Default::default()
    }];
    syscall_path_handler(request, "rmdir", argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.

        syscall_unlink_handler(request, path_args, true)
    })
}

pub(crate) fn sys_unlink(request: UNotifyEventRequest) -> ScmpNotifResp {
    // unlink() does not work on fds!
    // Hence, we have to use WANT_BASE to split base.
    let argv = &[SysArg {
        path: Some(0),
        fsflags: FsFlags::NO_FOLLOW_LAST
            | FsFlags::MUST_PATH
            | FsFlags::WANT_BASE
            | FsFlags::DOTLAST_EISDIR,
        ..Default::default()
    }];
    syscall_path_handler(request, "unlink", argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.

        syscall_unlink_handler(request, path_args, false)
    })
}

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

    // Linux kernel truncates upper bits.
    #[expect(clippy::cast_possible_truncation)]
    let flags = req.data.args[2] as libc::c_int;

    // Reject invalid flags.
    if flags & !libc::AT_REMOVEDIR != 0 {
        return request.fail_syscall(Errno::EINVAL);
    }

    // unlinkat() does not work on fds!
    // Hence, we have to use WANT_BASE to split base.
    let rmdir = flags & libc::AT_REMOVEDIR != 0;
    let dotlast = if rmdir {
        FsFlags::DOTLAST_ERMDIR
    } else {
        FsFlags::DOTLAST_EISDIR
    };
    let argv = &[SysArg {
        dirfd: Some(0),
        path: Some(1),
        fsflags: FsFlags::NO_FOLLOW_LAST | FsFlags::MUST_PATH | FsFlags::WANT_BASE | dotlast,
        ..Default::default()
    }];
    syscall_path_handler(request, "unlinkat", argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.

        syscall_unlink_handler(request, path_args, rmdir)
    })
}

/// A helper function to handle rmdir and unlink{,at} syscalls.
fn syscall_unlink_handler(
    request: &UNotifyEventRequest,
    args: PathArgs,
    rmdir: bool,
) -> Result<ScmpNotifResp, Errno> {
    // SysArg has one element.
    #[expect(clippy::disallowed_methods)]
    let path = &args.0.as_ref().unwrap().path;

    // Return EACCES if path is a magic symlink.
    // Return E{IS,NOT}DIR if path is not an expected dir/non-dir.
    // Return EBUSY if path is the root directory.
    // path.typ may be None if permission was denied to stat etc.
    if let Some(ftyp) = path.typ {
        if ftyp.is_magic_link() {
            return Err(Errno::EACCES);
        }
        if ftyp.is_dir() && !rmdir {
            return Err(Errno::EISDIR);
        }
        if !ftyp.is_dir() && rmdir {
            return Err(Errno::ENOTDIR);
        }
    }
    if path.base().is_empty() {
        return Err(Errno::EBUSY);
    }

    let flags = if rmdir {
        UnlinkatFlags::RemoveDir
    } else {
        UnlinkatFlags::NoRemoveDir
    };

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

    // All done, call the underlying system call.
    let result = safe_unlinkat(path.dir(), path.base(), flags);

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

    result.map(|_| request.return_syscall(0))
}