From 1b5b31a743567e25cdab78ad154af1d398293d37 Mon Sep 17 00:00:00 2001 From: "A. Shannon" <217575710+a-shannon@users.noreply.github.com> Date: Sun, 9 Aug 2026 21:43:04 +0200 Subject: [PATCH] Add read-only FS_IOC_FSGETXATTR support Expose Fsxattr, FsxattrFlags, and ioctl_fsgetxattr using the kernel ABI definition. Preserve unknown returned flag bits and test layout, opcode, syscall parity, and error behavior. --- src/fs/ioctl.rs | 171 +++++++++++++++++++++++++++++++++++++++++++++- tests/fs/ioctl.rs | 132 +++++++++++++++++++++++++++++++++++ 2 files changed, 302 insertions(+), 1 deletion(-) diff --git a/src/fs/ioctl.rs b/src/fs/ioctl.rs index 16e0dda12..4e6260c6e 100644 --- a/src/fs/ioctl.rs +++ b/src/fs/ioctl.rs @@ -96,18 +96,27 @@ unsafe impl ioctl::Ioctl for Ficlone<'_> { #[cfg(linux_raw_dep)] bitflags! { - /// `FS_*` constants for use with [`ioctl_getflags`]. + /// `FS_*` constants for use with [`ioctl_getflags`] and [`ioctl_setflags`]. /// /// [`ioctl_getflags`]: crate::fs::ioctl::ioctl_getflags + /// [`ioctl_setflags`]: crate::fs::ioctl::ioctl_setflags + /// + /// Not every flag returned by [`ioctl_getflags`] can be changed with + /// [`ioctl_setflags`]. The kernel validates which changes the filesystem + /// and calling process permit. #[repr(transparent)] #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] pub struct IFlags: ffi::c_uint { /// `FS_APPEND_FL` const APPEND = linux_raw_sys::general::FS_APPEND_FL; + /// `FS_CASEFOLD_FL` + const CASEFOLD = linux_raw_sys::general::FS_CASEFOLD_FL; /// `FS_COMPR_FL` const COMPRESSED = linux_raw_sys::general::FS_COMPR_FL; /// `FS_DIRSYNC_FL` const DIRSYNC = linux_raw_sys::general::FS_DIRSYNC_FL; + /// `FS_ENCRYPT_FL` + const ENCRYPTED = linux_raw_sys::general::FS_ENCRYPT_FL; /// `FS_IMMUTABLE_FL` const IMMUTABLE = linux_raw_sys::general::FS_IMMUTABLE_FL; /// `FS_JOURNAL_DATA_FL` @@ -130,6 +139,8 @@ bitflags! { const TOPDIR = linux_raw_sys::general::FS_TOPDIR_FL; /// `FS_UNRM_FL` const UNRM = linux_raw_sys::general::FS_UNRM_FL; + /// `FS_VERITY_FL` + const VERITY = linux_raw_sys::general::FS_VERITY_FL; } } @@ -167,3 +178,161 @@ pub fn ioctl_setflags(fd: Fd, flags: IFlags) -> io::Result<()> { ioctl::ioctl(fd, ctl) } } + +#[cfg(linux_raw_dep)] +bitflags! { + /// `FS_XFLAG_*` constants for [`Fsxattr::fsx_xflags`]. + /// + /// Values returned by [`ioctl_fsgetxattr`] retain any bits that are not + /// represented by a named constant. + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] + pub struct FsxattrFlags: u32 { + /// `FS_XFLAG_REALTIME` + const REALTIME = linux_raw_sys::general::FS_XFLAG_REALTIME; + /// `FS_XFLAG_PREALLOC` + const PREALLOC = linux_raw_sys::general::FS_XFLAG_PREALLOC; + /// `FS_XFLAG_IMMUTABLE` + const IMMUTABLE = linux_raw_sys::general::FS_XFLAG_IMMUTABLE; + /// `FS_XFLAG_APPEND` + const APPEND = linux_raw_sys::general::FS_XFLAG_APPEND; + /// `FS_XFLAG_SYNC` + const SYNC = linux_raw_sys::general::FS_XFLAG_SYNC; + /// `FS_XFLAG_NOATIME` + const NOATIME = linux_raw_sys::general::FS_XFLAG_NOATIME; + /// `FS_XFLAG_NODUMP` + const NODUMP = linux_raw_sys::general::FS_XFLAG_NODUMP; + /// `FS_XFLAG_RTINHERIT` + const RTINHERIT = linux_raw_sys::general::FS_XFLAG_RTINHERIT; + /// `FS_XFLAG_PROJINHERIT` + const PROJINHERIT = linux_raw_sys::general::FS_XFLAG_PROJINHERIT; + /// `FS_XFLAG_NOSYMLINKS` + const NOSYMLINKS = linux_raw_sys::general::FS_XFLAG_NOSYMLINKS; + /// `FS_XFLAG_EXTSIZE` + const EXTSIZE = linux_raw_sys::general::FS_XFLAG_EXTSIZE; + /// `FS_XFLAG_EXTSZINHERIT` + const EXTSZINHERIT = linux_raw_sys::general::FS_XFLAG_EXTSZINHERIT; + /// `FS_XFLAG_NODEFRAG` + const NODEFRAG = linux_raw_sys::general::FS_XFLAG_NODEFRAG; + /// `FS_XFLAG_FILESTREAM` + const FILESTREAM = linux_raw_sys::general::FS_XFLAG_FILESTREAM; + /// `FS_XFLAG_DAX` + const DAX = linux_raw_sys::general::FS_XFLAG_DAX; + /// `FS_XFLAG_COWEXTSIZE` + const COWEXTSIZE = linux_raw_sys::general::FS_XFLAG_COWEXTSIZE; + /// `FS_XFLAG_HASATTR` + const HASATTR = linux_raw_sys::general::FS_XFLAG_HASATTR; + + /// + const _ = !0; + } +} + +/// Filesystem attributes returned by [`ioctl_fsgetxattr`]. +#[cfg(linux_raw_dep)] +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] +pub struct Fsxattr { + /// Extended filesystem flags. + pub fsx_xflags: FsxattrFlags, + /// Preferred extent size. + pub fsx_extsize: u32, + /// Number of extents. + pub fsx_nextents: u32, + /// Project identifier. + pub fsx_projid: u32, + /// Preferred copy-on-write extent size. + pub fsx_cowextsize: u32, +} + +#[cfg(linux_raw_dep)] +const FS_IOC_FSGETXATTR: ioctl::Opcode = + ioctl::opcode::read::(b'X', 31); + +#[cfg(linux_raw_dep)] +#[inline] +fn fsxattr_from_raw(raw: linux_raw_sys::general::fsxattr) -> Fsxattr { + Fsxattr { + fsx_xflags: FsxattrFlags::from_bits_retain(raw.fsx_xflags), + fsx_extsize: raw.fsx_extsize, + fsx_nextents: raw.fsx_nextents, + fsx_projid: raw.fsx_projid, + fsx_cowextsize: raw.fsx_cowextsize, + } +} + +/// `ioctl(fd, FS_IOC_FSGETXATTR)`—Returns extended filesystem attributes. +#[cfg(linux_raw_dep)] +#[inline] +#[doc(alias = "FS_IOC_FSGETXATTR")] +pub fn ioctl_fsgetxattr(fd: Fd) -> io::Result { + // SAFETY: `fsxattr` consists entirely of integers and bytes, so all-zero + // is a valid value. This also initializes the reserved padding before the + // kernel may read it. + let mut raw: linux_raw_sys::general::fsxattr = unsafe { core::mem::zeroed() }; + + // SAFETY: `FS_IOC_FSGETXATTR` is `_IOR('X', 31, struct fsxattr)`. `raw` + // has the exact kernel layout and is fully initialized, and `Updater` + // permits the kernel to read and write it. + unsafe { + let ctl = + ioctl::Updater::<{ FS_IOC_FSGETXATTR }, linux_raw_sys::general::fsxattr>::new(&mut raw); + ioctl::ioctl(fd, ctl)?; + } + + Ok(fsxattr_from_raw(raw)) +} + +#[cfg(all(test, linux_raw_dep))] +mod tests { + use super::*; + + #[test] + fn test_fsgetxattr_from_raw_fields_and_unknown_bits() { + const UNKNOWN_XFLAG: u32 = 0x0000_0004; + + let attrs = fsxattr_from_raw(linux_raw_sys::general::fsxattr { + fsx_xflags: FsxattrFlags::APPEND.bits() | UNKNOWN_XFLAG, + fsx_extsize: 11, + fsx_nextents: 22, + fsx_projid: 33, + fsx_cowextsize: 44, + fsx_pad: [0; 8], + }); + + assert!(attrs.fsx_xflags.contains(FsxattrFlags::APPEND)); + assert_eq!( + (attrs.fsx_xflags & !FsxattrFlags::APPEND).bits(), + UNKNOWN_XFLAG + ); + assert_eq!(attrs.fsx_extsize, 11); + assert_eq!(attrs.fsx_nextents, 22); + assert_eq!(attrs.fsx_projid, 33); + assert_eq!(attrs.fsx_cowextsize, 44); + } + + #[cfg(not(any( + target_arch = "hexagon", + target_arch = "sparc", + target_arch = "sparc64" + )))] + #[test] + fn test_fsgetxattr_opcode_matches_linux_raw_sys() { + assert_eq!( + FS_IOC_FSGETXATTR, + linux_raw_sys::ioctl::FS_IOC_FSGETXATTR as ioctl::Opcode + ); + } + + #[cfg(any(target_arch = "sparc", target_arch = "sparc64"))] + #[test] + fn test_fsgetxattr_opcode_matches_sparc_uapi() { + assert_eq!(FS_IOC_FSGETXATTR, 0x401c_581f); + } + + #[cfg(target_arch = "hexagon")] + #[test] + fn test_fsgetxattr_opcode_matches_hexagon_uapi() { + assert_eq!(FS_IOC_FSGETXATTR, 0x801c_581f_u32 as ioctl::Opcode); + } +} diff --git a/tests/fs/ioctl.rs b/tests/fs/ioctl.rs index d84d8eb0f..c89fbf8c4 100644 --- a/tests/fs/ioctl.rs +++ b/tests/fs/ioctl.rs @@ -23,3 +23,135 @@ fn test_ioctl_ficlone() { Err(err) => panic!("{:?}", err), } } + +#[cfg(linux_raw_dep)] +#[repr(C)] +#[derive(Debug, Default)] +struct RawFsxattr { + fsx_xflags: u32, + fsx_extsize: u32, + fsx_nextents: u32, + fsx_projid: u32, + fsx_cowextsize: u32, + fsx_pad: [u8; 8], +} + +#[cfg(linux_raw_dep)] +fn raw_fsgetxattr(file: &std::fs::File) -> Result { + use std::os::unix::io::AsRawFd; + + let mut raw = RawFsxattr::default(); + let request = rustix::ioctl::opcode::read::(b'X', 31); + let result = unsafe { libc::ioctl(file.as_raw_fd(), request as _, &mut raw) }; + if result == -1 { + Err(rustix::io::Errno::from_raw_os_error(libc_errno::errno().0)) + } else { + assert_eq!(result, 0); + Ok(raw) + } +} + +#[cfg(linux_raw_dep)] +#[test] +fn test_fsgetxattr_flag_names_and_unknown_bits() { + use rustix::fs::{FsxattrFlags, IFlags}; + + const UNKNOWN_XFLAG: u32 = 0x0000_0004; + + let xflags = FsxattrFlags::from_bits_retain(FsxattrFlags::APPEND.bits() | UNKNOWN_XFLAG); + assert!(xflags.contains(FsxattrFlags::APPEND)); + assert_eq!((xflags & !FsxattrFlags::APPEND).bits(), UNKNOWN_XFLAG); + + assert_eq!( + IFlags::ENCRYPTED.bits(), + linux_raw_sys::general::FS_ENCRYPT_FL + ); + assert_eq!(IFlags::VERITY.bits(), linux_raw_sys::general::FS_VERITY_FL); + assert_eq!( + IFlags::CASEFOLD.bits(), + linux_raw_sys::general::FS_CASEFOLD_FL + ); +} + +#[cfg(linux_raw_dep)] +#[test] +fn test_fsgetxattr_raw_layout() { + use core::mem::{align_of, size_of}; + use linux_raw_sys::general::fsxattr as LinuxFsxattr; + use memoffset::offset_of; + + assert_eq!(size_of::(), 28); + assert_eq!(align_of::(), 4); + assert_eq!(size_of::(), size_of::()); + assert_eq!(align_of::(), align_of::()); + + assert_eq!(offset_of!(RawFsxattr, fsx_xflags), 0); + assert_eq!(offset_of!(RawFsxattr, fsx_extsize), 4); + assert_eq!(offset_of!(RawFsxattr, fsx_nextents), 8); + assert_eq!(offset_of!(RawFsxattr, fsx_projid), 12); + assert_eq!(offset_of!(RawFsxattr, fsx_cowextsize), 16); + assert_eq!(offset_of!(RawFsxattr, fsx_pad), 20); + + assert_eq!( + offset_of!(LinuxFsxattr, fsx_xflags), + offset_of!(RawFsxattr, fsx_xflags) + ); + assert_eq!( + offset_of!(LinuxFsxattr, fsx_extsize), + offset_of!(RawFsxattr, fsx_extsize) + ); + assert_eq!( + offset_of!(LinuxFsxattr, fsx_nextents), + offset_of!(RawFsxattr, fsx_nextents) + ); + assert_eq!( + offset_of!(LinuxFsxattr, fsx_projid), + offset_of!(RawFsxattr, fsx_projid) + ); + assert_eq!( + offset_of!(LinuxFsxattr, fsx_cowextsize), + offset_of!(RawFsxattr, fsx_cowextsize) + ); + assert_eq!( + offset_of!(LinuxFsxattr, fsx_pad), + offset_of!(RawFsxattr, fsx_pad) + ); +} + +#[cfg(linux_raw_dep)] +#[test] +fn test_ioctl_fsgetxattr_matches_raw_ioctl() { + use rustix::io; + + let file = tempfile::tempfile().unwrap(); + let attrs = match rustix::fs::ioctl_fsgetxattr(&file) { + Ok(attrs) => attrs, + Err(err) if matches!(err, io::Errno::NOTTY | io::Errno::OPNOTSUPP) => { + assert_eq!(raw_fsgetxattr(&file).unwrap_err(), err); + eprintln!("skipped: the temporary filesystem does not support FS_IOC_FSGETXATTR"); + return; + } + Err(err) => panic!("FS_IOC_FSGETXATTR failed on the temporary file: {err:?}"), + }; + let raw = raw_fsgetxattr(&file).unwrap(); + + assert_eq!(attrs.fsx_xflags.bits(), raw.fsx_xflags); + assert_eq!(attrs.fsx_extsize, raw.fsx_extsize); + assert_eq!(attrs.fsx_nextents, raw.fsx_nextents); + assert_eq!(attrs.fsx_projid, raw.fsx_projid); + assert_eq!(attrs.fsx_cowextsize, raw.fsx_cowextsize); + assert_eq!(raw.fsx_pad, [0; 8]); +} + +#[cfg(linux_raw_dep)] +#[test] +fn test_ioctl_fsgetxattr_unsupported_fd_errno() { + use rustix::io; + + let null = std::fs::File::open("/dev/null").unwrap(); + let safe_errno = rustix::fs::ioctl_fsgetxattr(&null).unwrap_err(); + let raw_errno = raw_fsgetxattr(&null).unwrap_err(); + + assert_eq!(safe_errno, raw_errno); + assert_eq!(safe_errno, io::Errno::NOTTY); +}