syd 3.52.0

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/kernel/mknod.rs: mknod(2) and mknodat(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::{
    errno::Errno,
    sys::stat::{Mode, SFlag},
};

use crate::{
    cookie::{safe_mknodat, safe_umask},
    kernel::{syscall_path_handler, to_mode},
    lookup::FsFlags,
    proc::proc_umask,
    req::{PathArgs, SysArg, UNotifyEventRequest},
};

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

    // Reject undefined/invalid kind.
    let kind = match to_sflag(req.data.args[1]) {
        Ok(kind) => kind,
        Err(errno) => return request.fail_syscall(errno),
    };

    // Strip undefined/invalid perm bits.
    let perm = to_mode(req.data.args[1]);

    // Reject invalid dev.
    #[expect(clippy::useless_conversion)]
    let dev: libc::dev_t = match req.data.args[2].try_into() {
        Ok(dev) => dev,
        Err(_) => return request.fail_syscall(Errno::EINVAL),
    };

    // We want NO_FOLLOW_LAST because creating an entry
    // through a dangling symbolic link should return EEXIST!
    let argv = &[SysArg {
        path: Some(0),
        fsflags: FsFlags::MISS_LAST | FsFlags::NO_FOLLOW_LAST | FsFlags::DOTLAST_EEXIST,
        ..Default::default()
    }];
    syscall_path_handler(request, "mknod", argv, |path_args, request, sandbox| {
        let umask = sandbox.umask;
        drop(sandbox); // release the read-lock.
        syscall_mknod_handler(request, path_args, kind, perm, dev, umask)
    })
}

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

    // Reject undefined/invalid kind.
    let kind = match to_sflag(req.data.args[2]) {
        Ok(kind) => kind,
        Err(errno) => return request.fail_syscall(errno),
    };

    // Strip undefined/invalid perm bits.
    let perm = to_mode(req.data.args[2]);

    // Reject invalid dev.
    #[expect(clippy::useless_conversion)]
    let dev: libc::dev_t = match req.data.args[3].try_into() {
        Ok(dev) => dev,
        Err(_) => return request.fail_syscall(Errno::EINVAL),
    };

    // We want NO_FOLLOW_LAST because creating an entry
    // through a dangling symbolic link should return EEXIST!
    let argv = &[SysArg {
        dirfd: Some(0),
        path: Some(1),
        fsflags: FsFlags::MISS_LAST | FsFlags::NO_FOLLOW_LAST | FsFlags::DOTLAST_EEXIST,
        ..Default::default()
    }];
    syscall_path_handler(request, "mknodat", argv, |path_args, request, sandbox| {
        let umask = sandbox.umask;
        drop(sandbox); // release the read-lock.
        syscall_mknod_handler(request, path_args, kind, perm, dev, umask)
    })
}

/// A helper function to handle mknod* syscalls.
fn syscall_mknod_handler(
    request: &UNotifyEventRequest,
    args: PathArgs,
    kind: SFlag,
    mut perm: Mode,
    dev: libc::dev_t,
    force_umask: Option<Mode>,
) -> Result<ScmpNotifResp, Errno> {
    // SysArg has one element.
    #[expect(clippy::disallowed_methods)]
    let path = &args.0.as_ref().unwrap().path;

    // trace/force_umask is only applied to regular files.
    // trace/force_umask overrides POSIX ACLs.
    if kind == SFlag::S_IFREG {
        if let Some(mask) = force_umask {
            perm &= !mask;
        }
    }

    let req = request.scmpreq;
    let mask = proc_umask(req.pid())?;

    // Honour process' umask:
    // Umask is per-thread here.
    // POSIX ACLs may override this.
    safe_umask(mask);

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

    let result = safe_mknodat(path.dir(), path.base(), kind, perm, dev);

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

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

fn to_sflag(arg: u64) -> Result<SFlag, Errno> {
    // Linux kernel truncates upper bits.
    #[expect(clippy::cast_possible_truncation)]
    let kind = (arg as libc::mode_t) & SFlag::S_IFMT.bits();

    // Zero file type is equivalent to S_IFREG.
    let kind = if kind == 0 {
        SFlag::S_IFREG
    } else {
        SFlag::from_bits(kind).ok_or(Errno::EINVAL)?
    };

    match kind {
        SFlag::S_IFREG | SFlag::S_IFCHR | SFlag::S_IFBLK | SFlag::S_IFIFO | SFlag::S_IFSOCK => {
            Ok(kind)
        }
        SFlag::S_IFDIR => Err(Errno::EPERM),
        _ => Err(Errno::EINVAL),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_to_sflag_zero_is_reg_1() {
        assert_eq!(to_sflag(0), Ok(SFlag::S_IFREG));
    }

    #[test]
    fn test_to_sflag_reg_1() {
        assert_eq!(to_sflag(SFlag::S_IFREG.bits() as u64), Ok(SFlag::S_IFREG));
    }

    #[test]
    fn test_to_sflag_chr_1() {
        assert_eq!(to_sflag(SFlag::S_IFCHR.bits() as u64), Ok(SFlag::S_IFCHR));
    }

    #[test]
    fn test_to_sflag_blk_1() {
        assert_eq!(to_sflag(SFlag::S_IFBLK.bits() as u64), Ok(SFlag::S_IFBLK));
    }

    #[test]
    fn test_to_sflag_fifo_1() {
        assert_eq!(to_sflag(SFlag::S_IFIFO.bits() as u64), Ok(SFlag::S_IFIFO));
    }

    #[test]
    fn test_to_sflag_sock_1() {
        assert_eq!(to_sflag(SFlag::S_IFSOCK.bits() as u64), Ok(SFlag::S_IFSOCK));
    }

    #[test]
    fn test_to_sflag_dir_is_eperm_1() {
        assert_eq!(to_sflag(SFlag::S_IFDIR.bits() as u64), Err(Errno::EPERM));
    }

    #[test]
    fn test_to_sflag_high_bits_truncated_1() {
        // High 32 bits should be ignored.
        let high = SFlag::S_IFREG.bits() as u64 | (1u64 << 32);
        assert_eq!(to_sflag(high), Ok(SFlag::S_IFREG));
    }
}