From 84e189fd8a43cef26c8c57c1139ad22a07113032 Mon Sep 17 00:00:00 2001 From: "A. Shannon" <217575710+a-shannon@users.noreply.github.com> Date: Sun, 9 Aug 2026 21:44:18 +0200 Subject: [PATCH] Add getxattrat and listxattrat support Expose directory-relative getxattrat and listxattrat wrappers for both Linux backends. Test no-follow and empty-path behavior, buffer bounds, initialized prefixes, and invalid requests. --- src/backend/libc/fs/syscalls.rs | 67 +++++++ src/backend/linux_raw/fs/syscalls.rs | 63 ++++++- src/fs/xattr.rs | 86 +++++++++ tests/fs/xattr.rs | 273 +++++++++++++++++++++++++++ 4 files changed, 487 insertions(+), 2 deletions(-) diff --git a/src/backend/libc/fs/syscalls.rs b/src/backend/libc/fs/syscalls.rs index 8b7f082f5..f71a843a1 100644 --- a/src/backend/libc/fs/syscalls.rs +++ b/src/backend/libc/fs/syscalls.rs @@ -2475,6 +2475,46 @@ pub(crate) unsafe fn fgetxattr( } } +#[cfg(linux_kernel)] +pub(crate) unsafe fn getxattrat( + dirfd: BorrowedFd<'_>, + path: &CStr, + name: &CStr, + value: (*mut u8, usize), + flags: AtFlags, +) -> io::Result { + use linux_raw_sys::general::xattr_args; + + const SYS_GETXATTRAT: c::c_long = linux_raw_sys::general::__NR_getxattrat as c::c_long; + syscall! { + fn getxattrat_syscall( + base_dirfd: c::c_int, + path: *const ffi::c_char, + at_flags: c::c_uint, + name: *const ffi::c_char, + args: *const xattr_args, + args_size: usize + ) via SYS_GETXATTRAT -> c::ssize_t + } + + let value_size = value.1.try_into().map_err(|_| io::Errno::OVERFLOW)?; + let value_addr = u64::try_from(value.0 as usize).map_err(|_| io::Errno::OVERFLOW)?; + let args = xattr_args { + value: value_addr, + size: value_size, + flags: 0, + }; + + ret_usize(getxattrat_syscall( + borrowed_fd(dirfd), + c_str(path), + flags.bits(), + c_str(name), + &args, + core::mem::size_of::(), + )) +} + #[cfg(any(apple, linux_kernel, target_os = "hurd"))] pub(crate) fn setxattr( path: &CStr, @@ -2627,6 +2667,33 @@ pub(crate) unsafe fn flistxattr(fd: BorrowedFd<'_>, list: (*mut u8, usize)) -> i } } +#[cfg(linux_kernel)] +pub(crate) unsafe fn listxattrat( + dirfd: BorrowedFd<'_>, + path: &CStr, + list: (*mut u8, usize), + flags: AtFlags, +) -> io::Result { + const SYS_LISTXATTRAT: c::c_long = linux_raw_sys::general::__NR_listxattrat as c::c_long; + syscall! { + fn listxattrat_syscall( + base_dirfd: c::c_int, + path: *const ffi::c_char, + at_flags: c::c_uint, + list: *mut ffi::c_char, + size: usize + ) via SYS_LISTXATTRAT -> c::ssize_t + } + + ret_usize(listxattrat_syscall( + borrowed_fd(dirfd), + c_str(path), + flags.bits(), + list.0.cast::(), + list.1, + )) +} + #[cfg(any(apple, linux_kernel, target_os = "hurd"))] pub(crate) fn removexattr(path: &CStr, name: &CStr) -> io::Result<()> { #[cfg(not(apple))] diff --git a/src/backend/linux_raw/fs/syscalls.rs b/src/backend/linux_raw/fs/syscalls.rs index 297782d85..0fc906797 100644 --- a/src/backend/linux_raw/fs/syscalls.rs +++ b/src/backend/linux_raw/fs/syscalls.rs @@ -41,8 +41,9 @@ use core::num::NonZeroU64; #[cfg(any(target_arch = "mips64", target_arch = "mips64r6"))] use linux_raw_sys::general::stat as linux_stat64; use linux_raw_sys::general::{ - open_how, AT_EACCESS, AT_FDCWD, AT_REMOVEDIR, AT_SYMLINK_NOFOLLOW, F_ADD_SEALS, F_GETFL, - F_GET_SEALS, F_SETFL, SEEK_CUR, SEEK_DATA, SEEK_END, SEEK_HOLE, SEEK_SET, STATX__RESERVED, + open_how, xattr_args, AT_EACCESS, AT_FDCWD, AT_REMOVEDIR, AT_SYMLINK_NOFOLLOW, F_ADD_SEALS, + F_GETFL, F_GET_SEALS, F_SETFL, SEEK_CUR, SEEK_DATA, SEEK_END, SEEK_HOLE, SEEK_SET, + STATX__RESERVED, }; #[cfg(target_pointer_width = "32")] use { @@ -1575,6 +1576,40 @@ pub(crate) unsafe fn fgetxattr( )) } +#[inline] +pub(crate) unsafe fn getxattrat( + dirfd: BorrowedFd<'_>, + path: &CStr, + name: &CStr, + value: (*mut u8, usize), + flags: AtFlags, +) -> io::Result { + let value_size = value.1.try_into().map_err(|_| io::Errno::OVERFLOW)?; + let value_addr = u64::try_from(value.0 as usize).map_err(|_| io::Errno::OVERFLOW)?; + let args = xattr_args { + value: value_addr, + size: value_size, + flags: 0, + }; + + let result = ret_usize(syscall!( + __NR_getxattrat, + dirfd, + path, + flags, + name, + by_ref(&args), + size_of::() + )); + + #[cfg(sanitize_memory)] + if let Ok(len) = result { + crate::msan::unpoison(value.0.cast(), len.min(value.1)); + } + + result +} + #[inline] pub(crate) fn setxattr( path: &CStr, @@ -1650,6 +1685,30 @@ pub(crate) unsafe fn flistxattr(fd: BorrowedFd<'_>, list: (*mut u8, usize)) -> i ret_usize(syscall!(__NR_flistxattr, fd, list.0, pass_usize(list.1))) } +#[inline] +pub(crate) unsafe fn listxattrat( + dirfd: BorrowedFd<'_>, + path: &CStr, + list: (*mut u8, usize), + flags: AtFlags, +) -> io::Result { + let result = ret_usize(syscall!( + __NR_listxattrat, + dirfd, + path, + flags, + list.0, + pass_usize(list.1) + )); + + #[cfg(sanitize_memory)] + if let Ok(len) = result { + crate::msan::unpoison(list.0.cast(), len.min(list.1)); + } + + result +} + #[inline] pub(crate) fn removexattr(path: &CStr, name: &CStr) -> io::Result<()> { unsafe { ret(syscall_readonly!(__NR_removexattr, path, name)) } diff --git a/src/fs/xattr.rs b/src/fs/xattr.rs index ea0eaea12..8f8e00eae 100644 --- a/src/fs/xattr.rs +++ b/src/fs/xattr.rs @@ -3,6 +3,8 @@ #![allow(unsafe_code)] use crate::buffer::Buffer; +#[cfg(linux_kernel)] +use crate::fs::AtFlags; use crate::{backend, ffi, io, path}; use backend::c; use backend::fd::AsFd; @@ -100,6 +102,50 @@ pub fn fgetxattr>( }) } +/// `getxattrat(dirfd, path, name, value, flags)`—Get extended filesystem +/// attributes relative to an open directory. +/// +/// `flags` may include [`AtFlags::SYMLINK_NOFOLLOW`] to operate on a symlink +/// itself, or [`AtFlags::EMPTY_PATH`] to operate on `dirfd` when `path` is +/// empty. Linux rejects [`AtFlags::EMPTY_PATH`] with [`io::Errno::BADF`] when +/// `dirfd` is an `O_PATH` descriptor. +/// +/// # Errors +/// +/// This returns [`io::Errno::RANGE`] if the syscall reports a required length +/// larger than the supplied buffer capacity, including a size-zero query for +/// a nonempty value. +/// +/// # References +/// - [Linux] +/// +/// [Linux]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/xattr.c?h=v6.13 +#[cfg(linux_kernel)] +#[inline] +pub fn getxattrat>( + dirfd: Fd, + path: P, + name: Name, + mut value: Buf, + flags: AtFlags, +) -> io::Result { + path.into_with_c_str(|path| { + name.into_with_c_str(|name| { + let value_parts = value.parts_mut(); + let value_capacity = value_parts.1; + // SAFETY: `getxattrat` initializes the returned number of bytes. + let len = unsafe { + backend::fs::syscalls::getxattrat(dirfd.as_fd(), path, name, value_parts, flags)? + }; + if len > value_capacity { + return Err(io::Errno::RANGE); + } + // SAFETY: `getxattrat` returned the number of initialized bytes. + unsafe { Ok(value.assume_init(len)) } + }) + }) +} + /// `setxattr(path, name, value.as_ptr(), value.len(), flags)`—Set extended /// filesystem attributes. /// @@ -215,6 +261,46 @@ pub fn flistxattr>(fd: Fd, mut list: Buf) -> io::Resul unsafe { Ok(list.assume_init(len)) } } +/// `listxattrat(dirfd, path, list, flags)`—List extended filesystem +/// attributes relative to an open directory. +/// +/// `flags` may include [`AtFlags::SYMLINK_NOFOLLOW`] to operate on a symlink +/// itself, or [`AtFlags::EMPTY_PATH`] to operate on `dirfd` when `path` is +/// empty. Linux rejects [`AtFlags::EMPTY_PATH`] with [`io::Errno::BADF`] when +/// `dirfd` is an `O_PATH` descriptor. +/// +/// # Errors +/// +/// This returns [`io::Errno::RANGE`] if the syscall reports a required length +/// larger than the supplied buffer capacity, including a size-zero query for +/// a nonempty list. +/// +/// # References +/// - [Linux] +/// +/// [Linux]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/xattr.c?h=v6.13 +#[cfg(linux_kernel)] +#[inline] +pub fn listxattrat>( + dirfd: Fd, + path: P, + mut list: Buf, + flags: AtFlags, +) -> io::Result { + path.into_with_c_str(|path| { + let list_parts = list.parts_mut(); + let list_capacity = list_parts.1; + // SAFETY: `listxattrat` initializes the returned number of bytes. + let len = + unsafe { backend::fs::syscalls::listxattrat(dirfd.as_fd(), path, list_parts, flags)? }; + if len > list_capacity { + return Err(io::Errno::RANGE); + } + // SAFETY: `listxattrat` returned the number of initialized bytes. + unsafe { Ok(list.assume_init(len)) } + }) +} + /// `removexattr(path, name)`—Remove an extended filesystem attribute. /// /// # References diff --git a/tests/fs/xattr.rs b/tests/fs/xattr.rs index ba2ff17aa..6b3498f72 100644 --- a/tests/fs/xattr.rs +++ b/tests/fs/xattr.rs @@ -152,3 +152,276 @@ fn libc_listxattr(path: &str) -> usize { } }) as usize } + +#[cfg(linux_kernel)] +#[test] +fn xattrat_missing_path_and_unknown_flags() { + use rustix::fs::{getxattrat, listxattrat, AtFlags}; + use rustix::io::Errno; + + let dir = std::fs::File::open(".").unwrap(); + let mut empty: [u8; 0] = []; + + let get_error = getxattrat( + &dir, + "rustix-xattrat-missing-path", + "user.rustix_test", + &mut empty, + AtFlags::empty(), + ) + .unwrap_err(); + if get_error == Errno::NOSYS { + return; + } + assert_eq!(get_error, Errno::NOENT); + + let list_error = listxattrat( + &dir, + "rustix-xattrat-missing-path", + &mut empty, + AtFlags::empty(), + ) + .unwrap_err(); + if list_error == Errno::NOSYS { + return; + } + assert_eq!(list_error, Errno::NOENT); + + let unknown = AtFlags::from_bits_retain(0x8000_0000); + assert_eq!( + getxattrat(&dir, "Cargo.toml", "user.rustix_test", &mut empty, unknown,).unwrap_err(), + Errno::INVAL + ); + assert_eq!( + listxattrat(&dir, "Cargo.toml", &mut empty, unknown).unwrap_err(), + Errno::INVAL + ); +} + +#[cfg(linux_kernel)] +#[test] +fn xattrat_dirfd_symlink_and_empty_path() { + use rustix::fs::{ + getxattrat, listxattrat, openat, setxattr, AtFlags, Mode, OFlags, XattrFlags, + }; + use rustix::io::Errno; + use std::os::unix::fs::symlink; + + const NAME: &str = "user.rustix_xattrat_test"; + const VALUE: &[u8] = b"target-value"; + + let tmp = tempfile::tempdir().unwrap(); + std::fs::write(tmp.path().join("target"), b"").unwrap(); + symlink("target", tmp.path().join("link")).unwrap(); + match setxattr(tmp.path().join("target"), NAME, VALUE, XattrFlags::empty()) { + Ok(()) => {} + Err(Errno::NOTSUP | Errno::PERM | Errno::ACCESS) => return, + Err(error) => panic!("failed to prepare xattr fixture: {error}"), + } + + let dir = std::fs::File::open(tmp.path()).unwrap(); + let mut value = [0_u8; 64]; + let followed = match getxattrat(&dir, "link", NAME, &mut value, AtFlags::empty()) { + Err(Errno::NOSYS) => return, + result => result.unwrap(), + }; + assert_eq!(&value[..followed], VALUE); + + assert_eq!( + getxattrat(&dir, "link", NAME, &mut value, AtFlags::SYMLINK_NOFOLLOW).unwrap_err(), + Errno::NODATA + ); + + let mut followed_list_storage = [0_u8; 4096]; + let followed_list = + match listxattrat(&dir, "link", &mut followed_list_storage, AtFlags::empty()) { + Err(Errno::NOSYS) => return, + result => result.unwrap(), + }; + assert!(xattr_list_contains( + &followed_list_storage[..followed_list], + NAME.as_bytes() + )); + + let mut link_list_storage = [0_u8; 4096]; + let link_list = listxattrat( + &dir, + "link", + &mut link_list_storage, + AtFlags::SYMLINK_NOFOLLOW, + ) + .unwrap(); + assert!(!xattr_list_contains( + &link_list_storage[..link_list], + NAME.as_bytes() + )); + + let target = std::fs::File::open(tmp.path().join("target")).unwrap(); + let target_value_len = getxattrat(&target, "", NAME, &mut value, AtFlags::EMPTY_PATH).unwrap(); + assert_eq!(&value[..target_value_len], VALUE); + let mut target_list_storage = [0_u8; 4096]; + let target_list = + listxattrat(&target, "", &mut target_list_storage, AtFlags::EMPTY_PATH).unwrap(); + assert!(xattr_list_contains( + &target_list_storage[..target_list], + NAME.as_bytes() + )); + + let path_symlink = + openat(&dir, "link", OFlags::PATH | OFlags::NOFOLLOW, Mode::empty()).unwrap(); + assert_eq!( + getxattrat(&path_symlink, "", NAME, &mut value, AtFlags::EMPTY_PATH).unwrap_err(), + Errno::BADF + ); + let mut path_list_storage = [0_u8; 4096]; + assert_eq!( + listxattrat( + &path_symlink, + "", + &mut path_list_storage, + AtFlags::EMPTY_PATH, + ) + .unwrap_err(), + Errno::BADF + ); +} + +#[cfg(linux_kernel)] +#[test] +fn xattrat_buffer_bounds() { + use rustix::buffer::spare_capacity; + use rustix::fs::{getxattrat, listxattrat, setxattr, AtFlags, XattrFlags}; + use rustix::io::Errno; + use std::mem::MaybeUninit; + + const NAME: &str = "user.rustix_xattrat_bounds"; + const VALUE: &[u8] = b"more-than-one-byte"; + + let tmp = tempfile::tempdir().unwrap(); + let target_path = tmp.path().join("target"); + std::fs::write(&target_path, b"").unwrap(); + match setxattr(&target_path, NAME, VALUE, XattrFlags::empty()) { + Ok(()) => {} + Err(Errno::NOTSUP | Errno::PERM | Errno::ACCESS) => return, + Err(error) => panic!("failed to prepare xattr bounds fixture: {error}"), + } + + let dir = std::fs::File::open(tmp.path()).unwrap(); + + let mut successful_uninit = [MaybeUninit::::uninit(); 32]; + let (initialized, remaining) = match getxattrat( + &dir, + "target", + NAME, + &mut successful_uninit, + AtFlags::empty(), + ) { + Err(Errno::NOSYS) => return, + result => result.unwrap(), + }; + assert_eq!(initialized, VALUE); + assert_eq!(remaining.len(), 32 - VALUE.len()); + + let prefix = [0xde, 0xad]; + let mut appended = prefix.to_vec(); + appended.reserve(VALUE.len()); + let appended_len = getxattrat( + &dir, + "target", + NAME, + spare_capacity(&mut appended), + AtFlags::empty(), + ) + .unwrap(); + assert_eq!(appended_len, VALUE.len()); + assert_eq!(&appended[..prefix.len()], &prefix); + assert_eq!(&appended[prefix.len()..], VALUE); + + let expected_list_len = libc_listxattr(target_path.to_str().unwrap()); + assert!(expected_list_len > NAME.len()); + let mut exact_list = vec![MaybeUninit::::uninit(); expected_list_len]; + let (initialized_list, remaining_list) = + listxattrat(&dir, "target", exact_list.as_mut_slice(), AtFlags::empty()).unwrap(); + assert_eq!(initialized_list.len(), expected_list_len); + assert!(remaining_list.is_empty()); + assert!(xattr_list_contains(initialized_list, NAME.as_bytes())); + + let mut list_with_slack = vec![MaybeUninit::::uninit(); expected_list_len + 1]; + let (initialized_list, remaining_list) = listxattrat( + &dir, + "target", + list_with_slack.as_mut_slice(), + AtFlags::empty(), + ) + .unwrap(); + assert_eq!(initialized_list.len(), expected_list_len); + assert_eq!(remaining_list.len(), 1); + assert!(xattr_list_contains(initialized_list, NAME.as_bytes())); + + let mut zero_uninit: [MaybeUninit; 0] = []; + let get_error = match getxattrat(&dir, "target", NAME, &mut zero_uninit, AtFlags::empty()) { + Err(Errno::NOSYS) => return, + result => result.unwrap_err(), + }; + assert_eq!(get_error, Errno::RANGE); + + let mut zero_initialized: [u8; 0] = []; + assert_eq!( + getxattrat( + &dir, + "target", + NAME, + &mut zero_initialized, + AtFlags::empty(), + ) + .unwrap_err(), + Errno::RANGE + ); + assert_eq!( + listxattrat(&dir, "target", &mut zero_uninit, AtFlags::empty(),).unwrap_err(), + Errno::RANGE + ); + + let mut zero_spare = Vec::with_capacity(1); + zero_spare.resize(zero_spare.capacity(), 0xa5); + let original = zero_spare.clone(); + assert_eq!( + getxattrat( + &dir, + "target", + NAME, + spare_capacity(&mut zero_spare), + AtFlags::empty(), + ) + .unwrap_err(), + Errno::RANGE + ); + assert_eq!(zero_spare, original); + assert_eq!( + listxattrat( + &dir, + "target", + spare_capacity(&mut zero_spare), + AtFlags::empty(), + ) + .unwrap_err(), + Errno::RANGE + ); + assert_eq!(zero_spare, original); + + let mut short_get = [MaybeUninit::::uninit(); 1]; + assert_eq!( + getxattrat(&dir, "target", NAME, &mut short_get, AtFlags::empty(),).unwrap_err(), + Errno::RANGE + ); + let mut short_list = [MaybeUninit::::uninit(); 1]; + assert_eq!( + listxattrat(&dir, "target", &mut short_list, AtFlags::empty(),).unwrap_err(), + Errno::RANGE + ); +} + +#[cfg(linux_kernel)] +fn xattr_list_contains(list: &[u8], name: &[u8]) -> bool { + list.split(|byte| *byte == 0).any(|entry| entry == name) +}