From a0c9c391b232d7633f6e103b5e802e3669bbe3fb Mon Sep 17 00:00:00 2001 From: ootinnyoo Date: Sat, 27 Dec 2025 15:06:54 -0500 Subject: [PATCH] implement sys_statx --- etc/syscalls_linux_aarch64.md | 2 +- libkernel/src/fs/attr.rs | 2 + libkernel/src/fs/filesystems/tmpfs.rs | 11 +- libkernel/src/fs/mod.rs | 14 ++ src/arch/arm64/exceptions/syscall.rs | 11 ++ src/fs/mod.rs | 8 + src/fs/syscalls/at/mod.rs | 1 + src/fs/syscalls/at/stat.rs | 14 +- src/fs/syscalls/at/statx.rs | 210 ++++++++++++++++++++++++++ usertest/src/main.rs | 88 +++++++++++ 10 files changed, 345 insertions(+), 16 deletions(-) create mode 100644 src/fs/syscalls/at/statx.rs diff --git a/etc/syscalls_linux_aarch64.md b/etc/syscalls_linux_aarch64.md index 1f53782d..631d6af8 100644 --- a/etc/syscalls_linux_aarch64.md +++ b/etc/syscalls_linux_aarch64.md @@ -275,7 +275,7 @@ | 0x120 (288) | pkey_mprotect | (unsigned long start, size_t len, unsigned long prot, int pkey) | __arm64_sys_pkey_mprotect | false | | 0x121 (289) | pkey_alloc | (unsigned long flags, unsigned long init_val) | __arm64_sys_pkey_alloc | false | | 0x122 (290) | pkey_free | (int pkey) | __arm64_sys_pkey_free | false | -| 0x123 (291) | statx | (int dfd, const char *filename, unsigned flags, unsigned int mask, struct statx *buffer) | __arm64_sys_statx | false | +| 0x123 (291) | statx | (int dfd, const char *filename, unsigned flags, unsigned int mask, struct statx *buffer) | __arm64_sys_statx | true | | 0x124 (292) | io_pgetevents | (aio_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct __kernel_timespec *timeout, const struct __aio_sigset *usig) | __arm64_sys_io_pgetevents | false | | 0x125 (293) | rseq | (struct rseq *rseq, u32 rseq_len, int flags, u32 sig) | __arm64_sys_rseq | ENOSYS | | 0x126 (294) | kexec_file_load | (int kernel_fd, int initrd_fd, unsigned long cmdline_len, const char *cmdline_ptr, unsigned long flags) | __arm64_sys_kexec_file_load | false | diff --git a/libkernel/src/fs/attr.rs b/libkernel/src/fs/attr.rs index 85c944f3..2019e8c6 100644 --- a/libkernel/src/fs/attr.rs +++ b/libkernel/src/fs/attr.rs @@ -52,6 +52,7 @@ pub struct FileAttr { pub block_size: u32, pub blocks: u64, pub atime: Duration, // Access time (e.g., seconds since epoch) + pub btime: Duration, // Creation time pub mtime: Duration, // Modification time pub ctime: Duration, // Change time pub file_type: FileType, @@ -69,6 +70,7 @@ impl Default for FileAttr { block_size: 0, blocks: 0, atime: Duration::new(0, 0), + btime: Duration::new(0, 0), mtime: Duration::new(0, 0), ctime: Duration::new(0, 0), file_type: FileType::File, diff --git a/libkernel/src/fs/filesystems/tmpfs.rs b/libkernel/src/fs/filesystems/tmpfs.rs index db2e35ca..86348655 100644 --- a/libkernel/src/fs/filesystems/tmpfs.rs +++ b/libkernel/src/fs/filesystems/tmpfs.rs @@ -126,13 +126,14 @@ where G: PageAllocGetter, T: AddressTranslator<()>, { - fn new(id: InodeId) -> Result { + fn new(id: InodeId, mode: FilePermissions) -> Result { Ok(Self { id, attr: SpinLockIrq::new(FileAttr { file_type: FileType::File, size: 0, nlinks: 1, + mode, ..Default::default() }), inner: SpinLockIrq::new(TmpFsRegInner { @@ -434,7 +435,7 @@ where let inode_id = InodeId::from_fsid_and_inodeid(fs.id(), new_id); let inode: Arc = match file_type { - FileType::File => Arc::new(TmpFsReg::::new(inode_id)?), + FileType::File => Arc::new(TmpFsReg::::new(inode_id, mode)?), FileType::Directory => TmpFsDirInode::::new(new_id, self.fs.clone(), mode), _ => return Err(KernelError::NotSupported), }; @@ -817,7 +818,11 @@ mod tests { ) { init_allocator(); let fs = TmpFs::new(0); - let reg = TmpFsReg::new(InodeId::from_fsid_and_inodeid(0, 1024)).unwrap(); + let reg = TmpFsReg::new( + InodeId::from_fsid_and_inodeid(0, 1024), + FilePermissions::all(), + ) + .unwrap(); (fs, reg) } diff --git a/libkernel/src/fs/mod.rs b/libkernel/src/fs/mod.rs index 6df2f9d8..2bd83bf5 100644 --- a/libkernel/src/fs/mod.rs +++ b/libkernel/src/fs/mod.rs @@ -106,6 +106,20 @@ pub enum FileType { Socket, } +impl From for u32 { + fn from(file_type: FileType) -> Self { + match file_type { + FileType::Directory => 0o040000, + FileType::CharDevice(_) => 0o020000, + FileType::BlockDevice(_) => 0o060000, + FileType::File => 0o100000, + FileType::Fifo => 0o010000, + FileType::Symlink => 0o120000, + FileType::Socket => 0o140000, + } + } +} + /// A stateful, streaming iterator for reading directory entries. #[async_trait] pub trait DirStream: Send + Sync { diff --git a/src/arch/arm64/exceptions/syscall.rs b/src/arch/arm64/exceptions/syscall.rs index 8a07881a..aefecbb7 100644 --- a/src/arch/arm64/exceptions/syscall.rs +++ b/src/arch/arm64/exceptions/syscall.rs @@ -15,6 +15,7 @@ use crate::{ readlink::sys_readlinkat, rename::{sys_renameat, sys_renameat2}, stat::sys_newfstatat, + statx::sys_statx, symlink::sys_symlinkat, unlink::sys_unlinkat, utime::sys_utimensat, @@ -420,6 +421,16 @@ pub async fn handle_syscall() { ) .await } + 0x123 => { + sys_statx( + arg1.into(), + TUA::from_value(arg2 as _), + arg3 as _, + arg4 as _, + TUA::from_value(arg5 as _), + ) + .await + } 0x125 => Err(KernelError::NotSupported), 0x1b7 => { sys_faccessat2( diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 2eb7c401..12d44f06 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -583,6 +583,14 @@ impl VFS { .exchange(old_name, new_parent_inode, new_name) .await } + + pub fn is_mount_root(&self, id: InodeId) -> bool { + self.state + .lock_save_irq() + .mounts + .values() + .any(|mount| mount.root_inode.id() == id) + } } pub static VFS: VFS = VFS::new(); diff --git a/src/fs/syscalls/at/mod.rs b/src/fs/syscalls/at/mod.rs index 332340b3..0cbf48cd 100644 --- a/src/fs/syscalls/at/mod.rs +++ b/src/fs/syscalls/at/mod.rs @@ -18,6 +18,7 @@ pub mod open; pub mod readlink; pub mod rename; pub mod stat; +pub mod statx; pub mod symlink; pub mod unlink; pub mod utime; diff --git a/src/fs/syscalls/at/stat.rs b/src/fs/syscalls/at/stat.rs index 93f66abb..1acabca4 100644 --- a/src/fs/syscalls/at/stat.rs +++ b/src/fs/syscalls/at/stat.rs @@ -7,7 +7,7 @@ use crate::{ use core::ffi::c_char; use libkernel::{ error::Result, - fs::{FileType, attr::FileAttr, path::Path}, + fs::{attr::FileAttr, path::Path}, memory::address::TUA, }; @@ -42,20 +42,10 @@ unsafe impl UserCopyable for Stat {} impl From for Stat { fn from(value: FileAttr) -> Self { - let type_val = match value.file_type { - FileType::Directory => 0o040000, - FileType::CharDevice(_) => 0o020000, - FileType::BlockDevice(_) => 0o060000, - FileType::File => 0o100000, - FileType::Fifo => 0o010000, - FileType::Symlink => 0o120000, - FileType::Socket => 0o140000, - }; - Self { st_dev: value.id.fs_id(), st_ino: value.id.inode_id(), - st_mode: value.mode.bits() as u32 | type_val, + st_mode: value.mode.bits() as u32 | u32::from(value.file_type), st_nlink: value.nlinks, st_uid: value.uid.into(), st_gid: value.gid.into(), diff --git a/src/fs/syscalls/at/statx.rs b/src/fs/syscalls/at/statx.rs new file mode 100644 index 00000000..00651f6d --- /dev/null +++ b/src/fs/syscalls/at/statx.rs @@ -0,0 +1,210 @@ +use crate::{ + current_task, + fs::{ + VFS, + syscalls::at::{resolve_at_start_node, resolve_path_flags}, + }, + memory::uaccess::{UserCopyable, copy_to_user, cstr::UserCStr}, + process::fd_table::Fd, +}; +use core::{ffi::c_char, time::Duration}; +use libkernel::{error::Result, fs::path::Path, memory::address::TUA}; + +use super::AtFlags; + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct StatXMask: u32 { + const STATX_TYPE = 0x0001; + const STATX_MODE = 0x0002; + const STATX_NLINK = 0x0004; + const STATX_UID = 0x0008; + const STATX_GID = 0x0010; + const STATX_ATIME = 0x0020; + const STATX_MTIME = 0x0040; + const STATX_CTIME = 0x0080; + const STATX_INO = 0x0100; + const STATX_SIZE = 0x0200; + const STATX_BLOCKS = 0x0400; + const STATX_BASIC_STATS = 0x07ff; + const STATX_BTIME = 0x0800; + const STATX_ALL = 0x0fff; + const STATX_MNT_ID = 0x1000; + const STATX_DIOALIGN = 0x2000; + const STATX_MNT_ID_UNIQUE = 0x4000; + const STATX_SUBVOL = 0x8000; + const STATX_WRITE_ATOMIC = 0x10000; + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct StatXAttr: u64 { + const STATX_ATTR_COMPRESSED = 0x0004; + const STATX_ATTR_IMMUTABLE = 0x0010; + const STATX_ATTR_APPEND = 0x0020; + const STATX_ATTR_NODUMP = 0x0040; + const STATX_ATTR_ENCRYPTED = 0x0800; + const STATX_ATTR_AUTOMOUNT = 0x1000; + const STATX_ATTR_MOUNT_ROOT = 0x2000; + const STATX_ATTR_VERITY = 0x100000; + const STATX_ATTR_DAX = 0x200000; + const STATX_ATTR_NOSECURITY = 0x00400000; + } +} + +#[repr(C)] +#[derive(Debug, Default, Clone, Copy)] +pub struct StatX { + pub stx_mask: u32, // Mask of supported fields + pub stx_blksize: u32, // Block size + pub stx_attributes: u64, // Attributes + pub stx_nlink: u32, // Link count + pub stx_uid: u32, // User ID of owner + pub stx_gid: u32, // Group ID of group + pub stx_mode: u16, // File mode + pub __pad1: u16, // Padding + pub stx_ino: u64, // Inode number + pub stx_size: u64, // Size + pub stx_blocks: u64, // Number of blocks allocated + pub stx_attributes_mask: u64, // Mask of supported attributes + pub stx_atime: StatXTimestamp, // Access time + pub stx_btime: StatXTimestamp, // Creation time + pub stx_ctime: StatXTimestamp, // Change time + pub stx_mtime: StatXTimestamp, // Modification time + + // Currently not supported on any current filesystems + pub stx_rdev_major: u32, // Device major ID + pub stx_rdev_minor: u32, // Device minor ID + pub stx_dev_major: u32, // Filesystem major ID + pub stx_dev_minor: u32, // Filesystem minor ID + pub stx_mnt_id: u64, // Mount ID + pub stx_dio_mem_align: u32, // Alignment of memory for direct I/O + pub stx_dio_offset_align: u32, // Alignment of offset for direct I/O + pub stx_subvol: u64, // Subvolume ID + pub stx_atomic_write_unit_min: u32, // Minimum atomic write direct I/O size + pub stx_atomic_write_unit_max: u32, // Maximum atomic write direct I/O size + pub stx_atomic_write_segments_max: u32, // Maximum number of segments for atomic writes + pub stx_dio_read_offset_align: u32, // Alignment of offset for direct I/O read + pub stx_atomic_write_unit_max_opt: u32, // Maximum size optimized for atomic writes + + // Unused + pub __unused1: u64, + pub __unused2: u64, + pub __unused3: u64, + pub __unused4: u64, + pub __unused5: u64, + pub __unused6: u64, +} + +#[repr(C)] +#[derive(Debug, Default, Clone, Copy)] +pub struct StatXTimestamp { + pub tv_sec: i64, + pub tv_nsec: u32, + pub __pad1: i32, +} + +impl From for StatXTimestamp { + fn from(duration: Duration) -> Self { + Self { + tv_sec: duration.as_secs() as i64, + tv_nsec: duration.subsec_nanos(), + __pad1: 0, + } + } +} + +unsafe impl UserCopyable for StatX {} + +pub async fn sys_statx( + dirfd: Fd, + path: TUA, + flags: i32, + mask: u32, + statbuf: TUA, +) -> Result { + let mut buf = [0; 1024]; + + let task = current_task(); + let flags = AtFlags::from_bits_truncate(flags); + let mask = StatXMask::from_bits_truncate(mask); + let path = Path::new(UserCStr::from_ptr(path).copy_from_user(&mut buf).await?); + + let start_node = resolve_at_start_node(dirfd, path).await?; + let node = resolve_path_flags(dirfd, path, start_node, task.clone(), flags).await?; + + let attr = node.getattr().await?; + + let mut stat_x = StatX::default(); + + // TODO: right now, the attr is applied unconditionally even if the data is not supported, as + // long as the input mask is set. at some point, the fs should be checked if these attributes + // are supported and will return useful data. + if mask.contains(StatXMask::STATX_TYPE) { + stat_x.stx_mask |= StatXMask::STATX_TYPE.bits(); + stat_x.stx_mode = u32::from(attr.file_type) as u16; + } + + if mask.contains(StatXMask::STATX_MODE) { + stat_x.stx_mask |= StatXMask::STATX_MODE.bits(); + stat_x.stx_mode |= attr.mode.bits() as u16; + } + + if mask.contains(StatXMask::STATX_NLINK) { + stat_x.stx_mask |= StatXMask::STATX_NLINK.bits(); + stat_x.stx_nlink = attr.nlinks; + } + + if mask.contains(StatXMask::STATX_UID) { + stat_x.stx_mask |= StatXMask::STATX_UID.bits(); + stat_x.stx_uid = attr.uid.into(); + } + + if mask.contains(StatXMask::STATX_GID) { + stat_x.stx_mask |= StatXMask::STATX_GID.bits(); + stat_x.stx_gid = attr.gid.into(); + } + + if mask.contains(StatXMask::STATX_ATIME) { + stat_x.stx_mask |= StatXMask::STATX_ATIME.bits(); + stat_x.stx_atime = attr.atime.into(); + } + + if mask.contains(StatXMask::STATX_MTIME) { + stat_x.stx_mask |= StatXMask::STATX_MTIME.bits(); + stat_x.stx_mtime = attr.mtime.into(); + } + + if mask.contains(StatXMask::STATX_CTIME) { + stat_x.stx_mask |= StatXMask::STATX_CTIME.bits(); + stat_x.stx_ctime = attr.ctime.into(); + } + + if mask.contains(StatXMask::STATX_INO) { + stat_x.stx_mask |= StatXMask::STATX_INO.bits(); + stat_x.stx_ino = attr.id.inode_id(); + } + + if mask.contains(StatXMask::STATX_SIZE) { + stat_x.stx_mask |= StatXMask::STATX_SIZE.bits(); + stat_x.stx_size = attr.size; + } + + if mask.contains(StatXMask::STATX_BLOCKS) { + stat_x.stx_mask |= StatXMask::STATX_BLOCKS.bits(); + stat_x.stx_blocks = attr.blocks; + } + + if mask.contains(StatXMask::STATX_BTIME) { + stat_x.stx_mask |= StatXMask::STATX_BTIME.bits(); + stat_x.stx_btime = attr.btime.into(); + } + + stat_x.stx_attributes_mask = StatXAttr::STATX_ATTR_MOUNT_ROOT.bits(); + if VFS.is_mount_root(attr.id) { + stat_x.stx_attributes |= StatXAttr::STATX_ATTR_MOUNT_ROOT.bits(); + } + + copy_to_user(statbuf, stat_x).await?; + + Ok(0) +} diff --git a/usertest/src/main.rs b/usertest/src/main.rs index 745922b2..101a9d38 100644 --- a/usertest/src/main.rs +++ b/usertest/src/main.rs @@ -563,6 +563,93 @@ fn test_utimens() { println!(" OK"); } +fn test_statx() { + #[repr(C)] + #[derive(Debug, Default, Clone, Copy)] + pub struct StatX { + pub stx_mask: u32, + pub stx_blksize: u32, + pub stx_attributes: u64, + pub stx_nlink: u32, + pub stx_uid: u32, + pub stx_gid: u32, + pub stx_mode: u16, + pub __pad1: u16, + pub stx_ino: u64, + pub stx_size: u64, + pub stx_blocks: u64, + pub stx_attributes_mask: u64, + pub stx_atime: StatXTimestamp, + pub stx_btime: StatXTimestamp, + pub stx_ctime: StatXTimestamp, + pub stx_mtime: StatXTimestamp, + pub stx_rdev_major: u32, + pub stx_rdev_minor: u32, + pub stx_dev_major: u32, + pub stx_dev_minor: u32, + pub stx_mnt_id: u64, + pub stx_dio_mem_align: u32, + pub stx_dio_offset_align: u32, + pub stx_subvol: u64, + pub stx_atomic_write_unit_min: u32, + pub stx_atomic_write_unit_max: u32, + pub stx_atomic_write_segments_max: u32, + pub stx_dio_read_offset_align: u32, + pub stx_atomic_write_unit_max_opt: u32, + pub __unused1: u64, + pub __unused2: u64, + pub __unused3: u64, + pub __unused4: u64, + pub __unused5: u64, + pub __unused6: u64, + } + + #[repr(C)] + #[derive(Debug, Default, Clone, Copy)] + pub struct StatXTimestamp { + pub tv_sec: i64, + pub tv_nsec: u32, + pub __pad1: i32, + } + + print!("Testing statx syscall ..."); + let file = "/tmp/statx_test"; + let c_file = CString::new(file).unwrap(); + let data = b"Hello, world!"; + let mut buffer = MaybeUninit::uninit(); + unsafe { + let fd = libc::open(c_file.as_ptr(), libc::O_WRONLY | libc::O_CREAT, 0o644); + if fd < 0 { + panic!("open failed"); + } + let ret = libc::write(fd, data.as_ptr() as *const libc::c_void, data.len()); + if ret < 0 { + panic!("write failed"); + } + libc::close(fd); + let ret = libc::syscall( + libc::SYS_statx, + libc::AT_FDCWD, + c_file.as_ptr(), + 0, + 0x000007ff as libc::c_uint, + buffer.as_mut_ptr(), + ); + if ret < 0 { + panic!("statx failed"); + } + let statx: StatX = buffer.assume_init(); + assert_eq!(statx.stx_mask, 0x000007ff); + assert_eq!(statx.stx_nlink, 1); + assert_eq!(statx.stx_mode as u32, libc::S_IFREG | 0o644); + assert_eq!(statx.stx_uid, libc::getuid()); + assert_eq!(statx.stx_gid, libc::getgid()); + assert_eq!(statx.stx_size, data.len() as u64); + } + fs::remove_file(file).expect("Failed to delete file"); + println!(" OK"); +} + fn test_rust_file() { print!("Testing rust file operations ..."); use std::fs::{self, File}; @@ -708,6 +795,7 @@ fn main() { run_test(test_truncate); run_test(test_ftruncate); run_test(test_utimens); + run_test(test_statx); run_test(test_rust_file); run_test(test_rust_dir); run_test(test_rust_thread);