syd 3.52.0

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

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

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

use crate::{
    compat::TimeSpec64,
    confine::scmp_arch_is_compat_long32,
    cookie::safe_utimensat,
    kernel::{syscall_path_handler, to_atflags},
    lookup::FsFlags,
    req::{PathArgs, SysArg, SysFlags, UNotifyEventRequest},
};

pub(crate) fn sys_utime(request: UNotifyEventRequest) -> ScmpNotifResp {
    // Read and validate utimbuf structure before path resolution.
    let req = request.scmpreq;
    let is32 = scmp_arch_is_compat_long32(req.data.arch);
    let times = match request.remote_utimbuf(req.data.args[1], is32) {
        Ok(times) => times,
        Err(errno) => return request.fail_syscall(errno),
    };

    let argv = &[SysArg {
        path: Some(0),
        fsflags: FsFlags::MUST_PATH,
        ..Default::default()
    }];

    syscall_path_handler(request, "utime", argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.
        let (atime, mtime) = times;
        syscall_utime_handler(request, path_args, &atime, &mtime)
    })
}

pub(crate) fn sys_utimes(request: UNotifyEventRequest) -> ScmpNotifResp {
    // Read and validate timeval structure before path resolution.
    let req = request.scmpreq;
    let is32 = scmp_arch_is_compat_long32(req.data.arch);
    let times = match request.remote_timeval(req.data.args[1], is32) {
        Ok(times) => times,
        Err(errno) => return request.fail_syscall(errno),
    };

    let argv = &[SysArg {
        path: Some(0),
        fsflags: FsFlags::MUST_PATH,
        ..Default::default()
    }];

    syscall_path_handler(request, "utimes", argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.
        let (atime, mtime) = times;
        syscall_utime_handler(request, path_args, &atime, &mtime)
    })
}

pub(crate) fn sys_futimesat(request: UNotifyEventRequest) -> ScmpNotifResp {
    // Read and validate timeval structure before path resolution.
    let req = request.scmpreq;
    let is32 = scmp_arch_is_compat_long32(req.data.arch);
    let times = match request.remote_timeval(req.data.args[2], is32) {
        Ok(times) => times,
        Err(errno) => return request.fail_syscall(errno),
    };

    // Linux enters fd-only mode for fds only, AT_FDCWD does path lookup.
    #[expect(clippy::cast_possible_truncation)]
    let is_fd = req.data.args[1] == 0 && req.data.args[0] as libc::c_int != libc::AT_FDCWD;

    let fsflags = FsFlags::MUST_PATH;
    let path = if is_fd { None } else { Some(1) };

    let argv = &[SysArg {
        dirfd: Some(0),
        path,
        fsflags,
        ..Default::default()
    }];

    syscall_path_handler(request, "futimesat", argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.
        let (atime, mtime) = times;
        syscall_utime_handler(request, path_args, &atime, &mtime)
    })
}

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

    // Read and validate timespec structure before flag validation.
    // Linux checks UTIME_OMIT before validating flags.
    let is32 = scmp_arch_is_compat_long32(req.data.arch);
    let times = match request.remote_timespec_2(req.data.args[2], is32) {
        Ok(times) => times,
        Err(errno) => return request.fail_syscall(errno),
    };

    // Linux ignores path and flags with UTIME_OMIT, checking only tv_nsec.
    if times.0.tv_nsec() == TimeSpec64::UTIME_OMIT.tv_nsec()
        && times.1.tv_nsec() == TimeSpec64::UTIME_OMIT.tv_nsec()
    {
        return request.return_syscall(0);
    }

    // Reject undefined/invalid flags.
    //
    // Linux rejects all flags for fd-only calls.
    #[expect(clippy::cast_possible_truncation)]
    let is_fd = req.data.args[1] == 0 && req.data.args[0] as libc::c_int != libc::AT_FDCWD;
    let atflags = if is_fd {
        AtFlags::empty()
    } else {
        AtFlags::AT_EMPTY_PATH | AtFlags::AT_SYMLINK_NOFOLLOW
    };

    let atflags = match to_atflags(req.data.args[3], atflags) {
        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_NOFOLLOW) {
        fsflags |= FsFlags::NO_FOLLOW_LAST;
    }

    let argv = &[SysArg {
        dirfd: Some(0),
        path: if is_fd { None } else { Some(1) },
        flags,
        fsflags,
    }];

    syscall_path_handler(request, "utimensat", argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.
        let (atime, mtime) = times;
        syscall_utime_handler(request, path_args, &atime, &mtime)
    })
}

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

    // Read and validate timespec structure before flag validation.
    // Linux checks UTIME_OMIT before validating flags.
    let times = match request.remote_timespec_2(req.data.args[2], false) {
        Ok(times) => times,
        Err(errno) => return request.fail_syscall(errno),
    };

    // Linux ignores path and flags with UTIME_OMIT, checking only tv_nsec.
    if times.0.tv_nsec() == TimeSpec64::UTIME_OMIT.tv_nsec()
        && times.1.tv_nsec() == TimeSpec64::UTIME_OMIT.tv_nsec()
    {
        return request.return_syscall(0);
    }

    // Reject undefined/invalid flags.
    //
    // Linux rejects all flags for fd-only calls.
    #[expect(clippy::cast_possible_truncation)]
    let is_fd = req.data.args[1] == 0 && req.data.args[0] as libc::c_int != libc::AT_FDCWD;
    let atflags = if is_fd {
        AtFlags::empty()
    } else {
        AtFlags::AT_EMPTY_PATH | AtFlags::AT_SYMLINK_NOFOLLOW
    };

    let atflags = match to_atflags(req.data.args[3], atflags) {
        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_NOFOLLOW) {
        fsflags |= FsFlags::NO_FOLLOW_LAST;
    }

    let argv = &[SysArg {
        dirfd: Some(0),
        path: if is_fd { None } else { Some(1) },
        flags,
        fsflags,
    }];
    syscall_path_handler(
        request,
        "utimensat_time64",
        argv,
        |path_args, request, sandbox| {
            drop(sandbox); // release the read-lock.
            let (atime, mtime) = times;
            syscall_utime_handler(request, path_args, &atime, &mtime)
        },
    )
}

/// A helper function to handle utime* syscalls.
fn syscall_utime_handler(
    request: &UNotifyEventRequest,
    args: PathArgs,
    atime: &TimeSpec64,
    mtime: &TimeSpec64,
) -> Result<ScmpNotifResp, Errno> {
    // SysArg has one element.
    #[expect(clippy::disallowed_methods)]
    let path = &args.0.as_ref().unwrap().path;

    // We use MUST_PATH, dir refers to the file.
    assert!(path.base().is_empty()); // MUST_PATH!
    let fd = path.dir();

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

    // All done, call underlying system call.
    let result = safe_utimensat(fd, atime, mtime);

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

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