syd 3.52.0

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/mount/util.rs: Utilities using the new Linux mount API
//
// Copyright (c) 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

//! Utilities using the new Linux mount API

use std::{
    ffi::CString,
    os::{fd::AsFd, unix::ffi::OsStrExt},
};

use nix::{errno::Errno, fcntl::AtFlags, mount::MsFlags, NixPath};

use crate::{
    error,
    fd::AT_BADFD,
    info,
    mount::api::{
        fsconfig, fsmount, fsopen, mount_setattr, move_mount, open_tree, FsConfigCmd, FsMountFlags,
        FsOpenFlags, MountAttr, MountAttrFlags, MoveMountFlags, OpenTreeFlags, AT_RECURSIVE,
    },
    retry::retry_on_eintr,
};

/// Perform a filesystem mount.
pub fn mount_fs<Fd, P>(
    fsname: &P,
    dst: Fd,
    flags: MountAttrFlags,
    opts: Option<&str>,
) -> Result<(), Errno>
where
    Fd: AsFd,
    P: ?Sized + NixPath + OsStrExt,
{
    let ctx = retry_on_eintr(|| fsopen(fsname, FsOpenFlags::FSOPEN_CLOEXEC))?;

    fsname.with_nix_path(|cstr| {
        fsconfig(
            &ctx,
            FsConfigCmd::SetString,
            Some("source"),
            Some(cstr.to_bytes_with_nul()),
            0,
        )
    })??;
    if let Some(opts) = opts {
        for opt in opts.split(',') {
            if opt.is_empty() {
                continue; // convenience
            }
            let (key, val) = if let Some((key, val)) = opt.split_once('=') {
                let val = CString::new(val)
                    .or(Err(Errno::EINVAL))?
                    .into_bytes_with_nul();
                (key, Some(val))
            } else {
                (opt, None)
            };
            let cmd = if val.is_none() {
                FsConfigCmd::SetFlag
            } else {
                FsConfigCmd::SetString
            };
            retry_on_eintr(|| fsconfig(&ctx, cmd, Some(key), val.as_deref(), 0))?;
        }
    }

    retry_on_eintr(|| {
        fsconfig(
            &ctx,
            FsConfigCmd::CmdCreate,
            None::<&[u8]>,
            None::<&[u8]>,
            0,
        )
    })?;
    retry_on_eintr(|| fsmount(&ctx, FsMountFlags::FSMOUNT_CLOEXEC, flags)).and_then(|mnt| {
        retry_on_eintr(|| {
            move_mount(
                &mnt,
                c"",
                &dst,
                c"",
                MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH | MoveMountFlags::MOVE_MOUNT_T_EMPTY_PATH,
            )
        })
    })
}

/// Perform a recursive bind mount, optionally setting the given propagation type.
pub fn mount_bind<Fd1, Fd2>(src: Fd1, dst: Fd2, flags: MountAttrFlags) -> Result<(), Errno>
where
    Fd1: AsFd,
    Fd2: AsFd,
{
    let clr_flags = mountattr_fixup(flags);
    let attr = MountAttr {
        attr_set: flags.bits().into(),
        attr_clr: clr_flags.bits().into(),
        propagation: 0,
        userns_fd: 0,
    };

    let src = retry_on_eintr(|| {
        open_tree(
            &src,
            c"",
            OpenTreeFlags::OPEN_TREE_CLOEXEC
                | OpenTreeFlags::OPEN_TREE_CLONE
                | OpenTreeFlags::AT_EMPTY_PATH
                | OpenTreeFlags::AT_RECURSIVE,
        )
    })?;
    retry_on_eintr(|| mount_setattr(&src, c"", AtFlags::AT_EMPTY_PATH, attr))?;
    retry_on_eintr(|| {
        move_mount(
            &src,
            c"",
            &dst,
            c"",
            MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH | MoveMountFlags::MOVE_MOUNT_T_EMPTY_PATH,
        )
    })
}

/// Change propagation type of rootfs.
///
/// `proptype` must be one of `MsFlags::MS_SHARED`, `MsFlags::MS_SLAVE`,
/// `MsFlags::MS_PRIVATE`, or `MsFlags::MS_UNBINDABLE`.
pub fn set_root_mount_propagation(proptype: MsFlags) -> Result<(), Errno> {
    // The into conversion is necessary on 32-bit.
    #[expect(clippy::useless_conversion)]
    let attr = MountAttr {
        attr_set: 0,
        attr_clr: 0,
        propagation: proptype.bits().into(),
        userns_fd: 0,
    };

    retry_on_eintr(|| open_tree(AT_BADFD, "/", OpenTreeFlags::OPEN_TREE_CLOEXEC))
        .and_then(|fd| {
            retry_on_eintr(|| mount_setattr(&fd, c"", AtFlags::AT_EMPTY_PATH | AT_RECURSIVE, attr))
        })
        .inspect(|_| {
            let propname = propagation_name(proptype);
            info!("ctx": "run", "op": "set_root_mount_propagation",
                "type": propname, "bits": proptype.bits(),
                "msg": format!("set root mount propagation type to {propname}"));
        })
        .inspect_err(|errno| {
            let propname = propagation_name(proptype);
            error!("ctx": "run", "op": "set_root_mount_propagation",
                "type": propname, "bits": proptype.bits(), "err": *errno as i32,
                "msg": format!("set root mount propagation type to {propname} failed: {errno}"));
        })
}

const fn propagation_name(proptype: MsFlags) -> &'static str {
    match proptype {
        MsFlags::MS_SHARED => "shared",
        MsFlags::MS_SLAVE => "slave",
        MsFlags::MS_PRIVATE => "private",
        MsFlags::MS_UNBINDABLE => "unbindable",
        _ => "unknown",
    }
}

// If MOUNT_ATTR_NOATIME or MOUNT_ATTR_STRICTATIME is set,
// we should add the flag MOUNT_ATTR__ATIME to ensure the
// kernel can perform correct validation.
fn mountattr_fixup(flags: MountAttrFlags) -> MountAttrFlags {
    if flags.intersects(MountAttrFlags::MOUNT_ATTR__ATIME) {
        MountAttrFlags::MOUNT_ATTR__ATIME
    } else {
        MountAttrFlags::empty()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::mount::api::MountAttrFlags;

    #[test]
    fn test_mountattr_fixup_1() {
        let result = mountattr_fixup(MountAttrFlags::empty());
        assert!(result.is_empty());
    }

    #[test]
    fn test_mountattr_fixup_2() {
        let result = mountattr_fixup(MountAttrFlags::MOUNT_ATTR__ATIME);
        assert_eq!(result, MountAttrFlags::MOUNT_ATTR__ATIME);
    }

    #[test]
    fn test_mountattr_fixup_3() {
        let result = mountattr_fixup(MountAttrFlags::MOUNT_ATTR_RDONLY);
        assert!(result.is_empty());
    }

    #[test]
    fn test_mountattr_fixup_4() {
        let flags = MountAttrFlags::MOUNT_ATTR__ATIME | MountAttrFlags::MOUNT_ATTR_RDONLY;
        let result = mountattr_fixup(flags);
        assert_eq!(result, MountAttrFlags::MOUNT_ATTR__ATIME);
    }

    #[test]
    fn test_propagation_name_1() {
        assert_eq!(propagation_name(MsFlags::MS_SHARED), "shared");
    }

    #[test]
    fn test_propagation_name_2() {
        assert_eq!(propagation_name(MsFlags::MS_SLAVE), "slave");
    }

    #[test]
    fn test_propagation_name_3() {
        assert_eq!(propagation_name(MsFlags::MS_PRIVATE), "private");
    }

    #[test]
    fn test_propagation_name_4() {
        assert_eq!(propagation_name(MsFlags::MS_UNBINDABLE), "unbindable");
    }

    #[test]
    fn test_propagation_name_5() {
        assert_eq!(propagation_name(MsFlags::MS_RDONLY), "unknown");
    }
}