diff --git a/src/fs/statx.rs b/src/fs/statx.rs index c6165b2c7..fa1d36779 100644 --- a/src/fs/statx.rs +++ b/src/fs/statx.rs @@ -1,15 +1,19 @@ //! Linux `statx`. +use crate::fd::{AsFd, BorrowedFd}; +use crate::ffi::CStr; use crate::fs::AtFlags; use crate::{imp, io, path}; -use imp::fd::AsFd; +use core::sync::atomic::{AtomicU8, Ordering}; pub use imp::fs::types::{Statx, StatxFlags, StatxTimestamp}; /// `statx(dirfd, path, flags, mask, statxbuf)` /// -/// This isn't available on Linux before 4.11; it returns `ENOSYS` in that -/// case. +/// This function returns [`io::Errno::NOSYS`] if `statx` is not available on +/// the platform, such as Linux before 4.11. This also includes older Docker +/// versions where the actual syscall fails with different error codes; Rustix +/// handles this and translates them into `NOSYS`. /// /// # References /// - [Linux] @@ -22,5 +26,66 @@ pub fn statx( flags: AtFlags, mask: StatxFlags, ) -> io::Result { - path.into_with_c_str(|path| imp::fs::syscalls::statx(dirfd.as_fd(), path, flags, mask)) + path.into_with_c_str(|path| _statx(dirfd.as_fd(), path, flags, mask)) +} + +// Linux kernel prior to 4.11 old versions of Docker don't support `statx`. We +// store the availability in a global to avoid unnecessary syscalls. +// +// 0: Unknown +// 1: Not available +// 2: Available +static STATX_STATE: AtomicU8 = AtomicU8::new(0); + +#[inline] +fn _statx( + dirfd: BorrowedFd<'_>, + path: &CStr, + flags: AtFlags, + mask: StatxFlags, +) -> io::Result { + match STATX_STATE.load(Ordering::Relaxed) { + 0 => statx_init(dirfd, path, flags, mask), + 1 => Err(io::Errno::NOSYS), + _ => imp::fs::syscalls::statx(dirfd, path, flags, mask), + } +} + +/// The first `statx` call. We don't know if `statx` is available yet. +fn statx_init( + dirfd: BorrowedFd<'_>, + path: &CStr, + flags: AtFlags, + mask: StatxFlags, +) -> io::Result { + match imp::fs::syscalls::statx(dirfd, path, flags, mask) { + Err(io::Errno::NOSYS) => statx_error_nosys(), + Err(io::Errno::PERM) => statx_error_perm(), + result => { + STATX_STATE.store(2, Ordering::Relaxed); + result + } + } +} + +/// The first `statx` call failed with `NOSYS` (or something we're treating +/// like `NOSYS`). +#[cold] +fn statx_error_nosys() -> io::Result { + STATX_STATE.store(1, Ordering::Relaxed); + Err(io::Errno::NOSYS) +} + +/// The first `statx` call failed with `PERM`. +#[cold] +fn statx_error_perm() -> io::Result { + // Some old versions of Docker have `statx` fail with `PERM` when it isn't + // recognized. Check whether `statx` really is available, and if so, fail + // with `PERM`, and if not, treat it like `NOSYS`. + if imp::fs::syscalls::is_statx_available() { + STATX_STATE.store(2, Ordering::Relaxed); + Err(io::Errno::PERM) + } else { + statx_error_nosys() + } } diff --git a/src/imp/libc/fs/syscalls.rs b/src/imp/libc/fs/syscalls.rs index 5684c0637..5a7aef7e0 100644 --- a/src/imp/libc/fs/syscalls.rs +++ b/src/imp/libc/fs/syscalls.rs @@ -89,6 +89,8 @@ use crate::fs::SealFlags; )))] // not implemented in libc for netbsd yet use crate::fs::StatFs; +#[cfg(any(target_os = "android", target_os = "linux"))] +use crate::fs::{cwd, RenameFlags, ResolveFlags, Statx, StatxFlags}; #[cfg(not(any( target_os = "ios", target_os = "macos", @@ -97,8 +99,6 @@ use crate::fs::StatFs; )))] use crate::fs::{Dev, FileType}; use crate::fs::{FdFlags, Mode, OFlags, Stat, Timestamps}; -#[cfg(any(target_os = "android", target_os = "linux"))] -use crate::fs::{RenameFlags, ResolveFlags, Statx, StatxFlags}; use crate::io::{self, OwnedFd, SeekFrom}; #[cfg(not(target_os = "wasi"))] use crate::process::{Gid, Uid}; @@ -116,6 +116,8 @@ use core::convert::TryInto; ))] use core::mem::size_of; use core::mem::MaybeUninit; +#[cfg(any(target_os = "android", target_os = "linux"))] +use core::ptr::null; #[cfg(any( target_os = "android", target_os = "ios", @@ -123,11 +125,6 @@ use core::mem::MaybeUninit; target_os = "macos" ))] use core::ptr::null_mut; -#[cfg(all( - any(target_os = "android", target_os = "linux"), - any(target_pointer_width = "32", target_arch = "mips64") -))] -use core::sync::atomic::{AtomicBool, Ordering::Relaxed}; #[cfg(any(target_os = "ios", target_os = "macos"))] use { super::super::conv::nonnegative_ret, @@ -351,14 +348,11 @@ pub(crate) fn statat(dirfd: BorrowedFd<'_>, path: &CStr, flags: AtFlags) -> io:: any(target_pointer_width = "32", target_arch = "mips64") ))] { - if !NO_STATX.load(Relaxed) { - match statx(dirfd, path, flags, StatxFlags::BASIC_STATS) { - Ok(x) => return statx_to_stat(x), - Err(io::Errno::NOSYS) => NO_STATX.store(true, Relaxed), - Err(e) => return Err(e), - } + match statx(dirfd, path, flags, StatxFlags::BASIC_STATS) { + Ok(x) => return statx_to_stat(x), + Err(io::Errno::NOSYS) => statat_old(dirfd, path, flags), + Err(e) => return Err(e), } - statat_old(dirfd, path, flags) } // Main version: libc is y2038 safe. Or, the platform is not y2038 safe and @@ -904,13 +898,6 @@ pub(crate) fn flock(fd: BorrowedFd<'_>, operation: FlockOperation) -> io::Result unsafe { ret(c::flock(borrowed_fd(fd), operation as c::c_int)) } } -/// `statx` was introduced in Linux 4.11. -#[cfg(all( - any(target_os = "android", target_os = "linux"), - any(target_pointer_width = "32", target_arch = "mips64") -))] -static NO_STATX: AtomicBool = AtomicBool::new(false); - pub(crate) fn fstat(fd: BorrowedFd<'_>) -> io::Result { // 32-bit and mips64 Linux: `struct stat64` is not y2038 compatible; use // `statx`. @@ -919,14 +906,11 @@ pub(crate) fn fstat(fd: BorrowedFd<'_>) -> io::Result { any(target_pointer_width = "32", target_arch = "mips64") ))] { - if !NO_STATX.load(Relaxed) { - match statx(fd, cstr!(""), AtFlags::EMPTY_PATH, StatxFlags::BASIC_STATS) { - Ok(x) => return statx_to_stat(x), - Err(io::Errno::NOSYS) => NO_STATX.store(true, Relaxed), - Err(e) => return Err(e), - } + match statx(fd, cstr!(""), AtFlags::EMPTY_PATH, StatxFlags::BASIC_STATS) { + Ok(x) => return statx_to_stat(x), + Err(io::Errno::NOSYS) => fstat_old(fd), + Err(e) => return Err(e), } - fstat_old(fd) } // Main version: libc is y2038 safe. Or, the platform is not y2038 safe and @@ -1398,12 +1382,9 @@ fn stat64_to_stat(s64: c::stat64) -> io::Result { #[cfg(any(target_os = "android", target_os = "linux"))] #[allow(non_upper_case_globals)] -pub(crate) fn statx( - dirfd: BorrowedFd<'_>, - path: &CStr, - flags: AtFlags, - mask: StatxFlags, -) -> io::Result { +mod sys { + use super::{c, BorrowedFd, Statx}; + #[cfg(all(target_os = "android", target_arch = "arm"))] const SYS_statx: c::c_long = 397; #[cfg(all(target_os = "android", target_arch = "x86"))] @@ -1414,7 +1395,7 @@ pub(crate) fn statx( const SYS_statx: c::c_long = 332; weak_or_syscall! { - fn statx( + pub(super) fn statx( pirfd: BorrowedFd<'_>, path: *const c::c_char, flags: c::c_int, @@ -1422,10 +1403,19 @@ pub(crate) fn statx( buf: *mut Statx ) via SYS_statx -> c::c_int } +} +#[cfg(any(target_os = "android", target_os = "linux"))] +#[allow(non_upper_case_globals)] +pub(crate) fn statx( + dirfd: BorrowedFd<'_>, + path: &CStr, + flags: AtFlags, + mask: StatxFlags, +) -> io::Result { let mut statx_buf = MaybeUninit::::uninit(); unsafe { - ret(statx( + ret(sys::statx( dirfd, c_str(path), flags.bits(), @@ -1436,6 +1426,19 @@ pub(crate) fn statx( } } +#[cfg(any(target_os = "android", target_os = "linux"))] +#[inline] +pub(crate) fn is_statx_available() -> bool { + unsafe { + // Call `statx` with null pointers so that if it fails for any reason + // other than `EFAULT`, we know it's not supported. + matches!( + ret(sys::statx(cwd(), null(), 0, 0, null_mut())), + Err(io::Errno::FAULT) + ) + } +} + #[cfg(any(target_os = "ios", target_os = "macos"))] pub(crate) unsafe fn fcopyfile( from: BorrowedFd<'_>, diff --git a/src/imp/linux_raw/fs/syscalls.rs b/src/imp/linux_raw/fs/syscalls.rs index 004dbc8f2..d663058c8 100644 --- a/src/imp/linux_raw/fs/syscalls.rs +++ b/src/imp/linux_raw/fs/syscalls.rs @@ -8,14 +8,9 @@ #![allow(clippy::undocumented_unsafe_blocks)] use super::super::c; -#[cfg(all( - target_pointer_width = "32", - any(target_arch = "arm", target_arch = "mips", target_arch = "powerpc") -))] -use super::super::conv::zero; use super::super::conv::{ by_ref, c_int, c_uint, dev_t, oflags_for_open_how, opt_mut, pass_usize, raw_fd, ret, ret_c_int, - ret_c_uint, ret_owned_fd, ret_usize, size_of, slice_mut, + ret_c_uint, ret_owned_fd, ret_usize, size_of, slice_mut, zero, }; #[cfg(target_pointer_width = "64")] use super::super::conv::{loff_t, loff_t_from_u64, ret_u64}; @@ -36,8 +31,6 @@ use crate::io::{self, OwnedFd, SeekFrom}; use crate::process::{Gid, Uid}; use core::convert::TryInto; use core::mem::MaybeUninit; -#[cfg(any(target_pointer_width = "32", target_arch = "mips64"))] -use core::sync::atomic::{AtomicBool, Ordering::Relaxed}; #[cfg(target_arch = "mips64")] use linux_raw_sys::general::stat as linux_stat64; use linux_raw_sys::general::{ @@ -416,22 +409,15 @@ pub(crate) fn flock(fd: BorrowedFd<'_>, operation: FlockOperation) -> io::Result unsafe { ret(syscall!(__NR_flock, fd, c_uint(operation as c::c_uint))) } } -/// `statx` was introduced in Linux 4.11. -#[cfg(any(target_pointer_width = "32", target_arch = "mips64"))] -static NO_STATX: AtomicBool = AtomicBool::new(false); - #[inline] pub(crate) fn fstat(fd: BorrowedFd<'_>) -> io::Result { #[cfg(any(target_pointer_width = "32", target_arch = "mips64"))] { - if !NO_STATX.load(Relaxed) { - match statx(fd, cstr!(""), AtFlags::EMPTY_PATH, StatxFlags::BASIC_STATS) { - Ok(x) => return statx_to_stat(x), - Err(io::Errno::NOSYS) => NO_STATX.store(true, Relaxed), - Err(e) => return Err(e), - } + match statx(fd, cstr!(""), AtFlags::EMPTY_PATH, StatxFlags::BASIC_STATS) { + Ok(x) => statx_to_stat(x), + Err(io::Errno::NOSYS) => fstat_old(fd), + Err(e) => Err(e), } - fstat_old(fd) } #[cfg(all(target_pointer_width = "64", not(target_arch = "mips64")))] @@ -462,19 +448,16 @@ fn fstat_old(fd: BorrowedFd<'_>) -> io::Result { pub(crate) fn stat(filename: &CStr) -> io::Result { #[cfg(any(target_pointer_width = "32", target_arch = "mips64"))] { - if !NO_STATX.load(Relaxed) { - match statx( - crate::fs::cwd().as_fd(), - filename, - AtFlags::empty(), - StatxFlags::BASIC_STATS, - ) { - Ok(x) => return statx_to_stat(x), - Err(io::Errno::NOSYS) => NO_STATX.store(true, Relaxed), - Err(e) => return Err(e), - } + match statx( + crate::fs::cwd().as_fd(), + filename, + AtFlags::empty(), + StatxFlags::BASIC_STATS, + ) { + Ok(x) => return statx_to_stat(x), + Err(io::Errno::NOSYS) => stat_old(filename), + Err(e) => return Err(e), } - stat_old(filename) } #[cfg(all(target_pointer_width = "64", not(target_arch = "mips64")))] @@ -524,14 +507,11 @@ fn stat_old(filename: &CStr) -> io::Result { pub(crate) fn statat(dirfd: BorrowedFd<'_>, filename: &CStr, flags: AtFlags) -> io::Result { #[cfg(any(target_pointer_width = "32", target_arch = "mips64"))] { - if !NO_STATX.load(Relaxed) { - match statx(dirfd, filename, flags, StatxFlags::BASIC_STATS) { - Ok(x) => return statx_to_stat(x), - Err(io::Errno::NOSYS) => NO_STATX.store(true, Relaxed), - Err(e) => return Err(e), - } + match statx(dirfd, filename, flags, StatxFlags::BASIC_STATS) { + Ok(x) => return statx_to_stat(x), + Err(io::Errno::NOSYS) => statat_old(dirfd, filename, flags), + Err(e) => return Err(e), } - statat_old(dirfd, filename, flags) } #[cfg(all(target_pointer_width = "64", not(target_arch = "mips64")))] @@ -581,19 +561,16 @@ fn statat_old(dirfd: BorrowedFd<'_>, filename: &CStr, flags: AtFlags) -> io::Res pub(crate) fn lstat(filename: &CStr) -> io::Result { #[cfg(any(target_pointer_width = "32", target_arch = "mips64"))] { - if !NO_STATX.load(Relaxed) { - match statx( - crate::fs::cwd().as_fd(), - filename, - AtFlags::SYMLINK_NOFOLLOW, - StatxFlags::BASIC_STATS, - ) { - Ok(x) => return statx_to_stat(x), - Err(io::Errno::NOSYS) => NO_STATX.store(true, Relaxed), - Err(e) => return Err(e), - } + match statx( + crate::fs::cwd().as_fd(), + filename, + AtFlags::SYMLINK_NOFOLLOW, + StatxFlags::BASIC_STATS, + ) { + Ok(x) => return statx_to_stat(x), + Err(io::Errno::NOSYS) => lstat_old(filename), + Err(e) => return Err(e), } - lstat_old(filename) } #[cfg(all(target_pointer_width = "64", not(target_arch = "mips64")))] @@ -759,6 +736,25 @@ pub(crate) fn statx( } } +#[inline] +pub(crate) fn is_statx_available() -> bool { + unsafe { + // Call `statx` with null pointers so that if it fails for any reason + // other than `EFAULT`, we know it's not supported. + matches!( + ret(syscall!( + __NR_statx, + raw_fd(AT_FDCWD), + zero(), + zero(), + zero(), + zero() + )), + Err(io::Errno::FAULT) + ) + } +} + #[inline] pub(crate) fn fstatfs(fd: BorrowedFd<'_>) -> io::Result { #[cfg(target_pointer_width = "32")] diff --git a/src/lib.rs b/src/lib.rs index f5999d088..264c39811 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,6 +67,8 @@ //! are used (and documentation aliases are used so that the original names //! can still be searched for). //! - Provide y2038 compatibility, on platforms which support this. +//! - Correct selected platform bugs, such as behavioral differences when +//! running under seccomp. //! //! Things they don't do include: //! - Detecting whether functions are supported at runtime.