syd 3.52.0

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/kernel/chown.rs: chown(2), lchown(2), fchown(2), and fchownat(2) 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::{
    fcntl::AtFlags,
    unistd::{Gid, Uid},
    NixPath,
};

use crate::{
    confine::scmp_arch_has_uid16,
    cookie::{safe_fchown, safe_fchownat},
    kernel::{syscall_path_handler, to_atflags, to_id16},
    lookup::FsFlags,
    req::{SysArg, SysFlags, UNotifyEventRequest},
};

pub(crate) fn sys_fchown(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_fchown_handler(request, "fchown", true)
}

pub(crate) fn sys_fchown32(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_fchown_handler(request, "fchown32", false)
}

pub(crate) fn sys_chown(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_chown_handler(request, "chown", true)
}

pub(crate) fn sys_chown32(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_chown_handler(request, "chown32", false)
}

pub(crate) fn sys_lchown(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_lchown_handler(request, "lchown", true)
}

pub(crate) fn sys_lchown32(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_lchown_handler(request, "lchown32", false)
}

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

    // Reject undefined/invalid/unused flags.
    let flags = match to_atflags(
        req.data.args[4],
        AtFlags::AT_SYMLINK_NOFOLLOW | AtFlags::AT_EMPTY_PATH,
    ) {
        Ok(flags) => flags,
        Err(errno) => return request.fail_syscall(errno),
    };

    let mut fsflags = FsFlags::MUST_PATH;
    if flags.contains(AtFlags::AT_SYMLINK_NOFOLLOW) {
        fsflags |= FsFlags::NO_FOLLOW_LAST;
    }

    let empty = flags.contains(AtFlags::AT_EMPTY_PATH);
    let mut flags = SysFlags::empty();
    if empty {
        flags |= SysFlags::EMPTY_PATH;
    }

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

    syscall_path_handler(request, "fchownat", argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.

        // SysArg has one element.
        // We use MUST_PATH, dir refers to the file.
        #[expect(clippy::disallowed_methods)]
        let path = &path_args.0.as_ref().unwrap().path;
        assert!(path.base().is_empty()); // MUST_PATH!

        // Linux kernel truncates upper bits.
        #[expect(clippy::cast_possible_truncation)]
        let owner = match req.data.args[2] as u32 {
            u32::MAX => None,
            n => Some(Uid::from_raw(n)),
        };
        #[expect(clippy::cast_possible_truncation)]
        let group = match req.data.args[3] as u32 {
            u32::MAX => None,
            n => Some(Gid::from_raw(n)),
        };

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

        // All done, call underlying system call.
        let result = safe_fchownat(path.dir(), owner, group);

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

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

fn syscall_fchown_handler(
    request: UNotifyEventRequest,
    name: &'static str,
    is_16: bool,
) -> ScmpNotifResp {
    let argv = &[SysArg {
        dirfd: Some(0),
        fsflags: FsFlags::MUST_PATH,
        ..Default::default()
    }];

    syscall_path_handler(request, name, argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.

        // SysArg has one element.
        // We use MUST_PATH, dir refers to the file.
        #[expect(clippy::disallowed_methods)]
        let path = &path_args.0.as_ref().unwrap().path;
        assert!(path.base().is_empty()); // MUST_PATH!

        // Accept 16-bit IDs on CONFIG_UID16 architectures.
        let req = request.scmpreq;
        let (arg1, arg2) = if is_16 && scmp_arch_has_uid16(req.data.arch) {
            (to_id16(req.data.args[1]), to_id16(req.data.args[2]))
        } else {
            (req.data.args[1], req.data.args[2])
        };

        // Linux kernel truncates upper bits.
        #[expect(clippy::cast_possible_truncation)]
        let owner = match arg1 as u32 {
            u32::MAX => None,
            n => Some(Uid::from_raw(n)),
        };
        #[expect(clippy::cast_possible_truncation)]
        let group = match arg2 as u32 {
            u32::MAX => None,
            n => Some(Gid::from_raw(n)),
        };

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

        // All done, call underlying system call.
        let result = safe_fchown(path.dir(), owner, group);

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

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

fn syscall_chown_handler(
    request: UNotifyEventRequest,
    name: &'static str,
    is_16: bool,
) -> ScmpNotifResp {
    let argv = &[SysArg {
        path: Some(0),
        ..Default::default()
    }];

    syscall_path_handler(request, name, argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.

        // SysArg has one element.
        // We use MUST_PATH, dir refers to the file.
        #[expect(clippy::disallowed_methods)]
        let path = &path_args.0.as_ref().unwrap().path;
        assert!(path.base().is_empty()); // MUST_PATH!

        // Accept 16-bit IDs on CONFIG_UID16 architectures.
        let req = request.scmpreq;
        let (arg1, arg2) = if is_16 && scmp_arch_has_uid16(req.data.arch) {
            (to_id16(req.data.args[1]), to_id16(req.data.args[2]))
        } else {
            (req.data.args[1], req.data.args[2])
        };

        // Linux kernel truncates upper bits.
        #[expect(clippy::cast_possible_truncation)]
        let owner = match arg1 as u32 {
            u32::MAX => None,
            n => Some(Uid::from_raw(n)),
        };
        #[expect(clippy::cast_possible_truncation)]
        let group = match arg2 as u32 {
            u32::MAX => None,
            n => Some(Gid::from_raw(n)),
        };

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

        // All done, call underlying system call.
        let result = safe_fchownat(path.dir(), owner, group);

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

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

fn syscall_lchown_handler(
    request: UNotifyEventRequest,
    name: &'static str,
    is_16: bool,
) -> ScmpNotifResp {
    let argv = &[SysArg {
        path: Some(0),
        fsflags: FsFlags::MUST_PATH | FsFlags::NO_FOLLOW_LAST,
        ..Default::default()
    }];

    syscall_path_handler(request, name, argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.

        // SysArg has one element.
        // We use MUST_PATH, dir refers to the file.
        #[expect(clippy::disallowed_methods)]
        let path = &path_args.0.as_ref().unwrap().path;
        assert!(path.base().is_empty()); // MUST_PATH!

        // Accept 16-bit IDs on CONFIG_UID16 architectures.
        let req = request.scmpreq;
        let (arg1, arg2) = if is_16 && scmp_arch_has_uid16(req.data.arch) {
            (to_id16(req.data.args[1]), to_id16(req.data.args[2]))
        } else {
            (req.data.args[1], req.data.args[2])
        };

        // Linux kernel truncates upper bits.
        #[expect(clippy::cast_possible_truncation)]
        let owner = match arg1 as u32 {
            u32::MAX => None,
            n => Some(Uid::from_raw(n)),
        };
        #[expect(clippy::cast_possible_truncation)]
        let group = match arg2 as u32 {
            u32::MAX => None,
            n => Some(Gid::from_raw(n)),
        };

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

        // All done, call underlying system call.
        let result = safe_fchownat(path.dir(), owner, group);

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

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