Skip to content

Commit c4807ec

Browse files
committed
Detect when statx isn't available on older versions of Docker.
In older versions of Docker, `statx` can fail with `EPERM` meaning it isn't available. Recognize this, and translate it into `ENOSYS`. Also, have `statx` remember this and fail quickly in future calls. This is more emulation than rustix usually likes to do, but in this case, this is the behavior seems likely to be what most users will want, and we might as well put the code in one place. Fixes #352.
1 parent c3a244b commit c4807ec

File tree

4 files changed

+156
-84
lines changed

4 files changed

+156
-84
lines changed

src/fs/statx.rs

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
//! Linux `statx`.
22
3+
use crate::fd::{AsFd, BorrowedFd};
4+
use crate::ffi::CStr;
35
use crate::fs::AtFlags;
46
use crate::{imp, io, path};
5-
use imp::fd::AsFd;
7+
use core::sync::atomic::{AtomicU8, Ordering};
68

79
pub use imp::fs::types::{Statx, StatxFlags, StatxTimestamp};
810

911
/// `statx(dirfd, path, flags, mask, statxbuf)`
1012
///
11-
/// This isn't available on Linux before 4.11; it returns `ENOSYS` in that
12-
/// case.
13+
/// This function returns [`io::Errno::NOSYS`] if `statx` is not available on
14+
/// the platform, such as Linux before 4.11. This also includes older Docker
15+
/// versions where the actual syscall fails with different error codes; Rustix
16+
/// handles this and translates them into `NOSYS`.
1317
///
1418
/// # References
1519
/// - [Linux]
@@ -22,5 +26,66 @@ pub fn statx<P: path::Arg, Fd: AsFd>(
2226
flags: AtFlags,
2327
mask: StatxFlags,
2428
) -> io::Result<Statx> {
25-
path.into_with_c_str(|path| imp::fs::syscalls::statx(dirfd.as_fd(), path, flags, mask))
29+
path.into_with_c_str(|path| _statx(dirfd.as_fd(), path, flags, mask))
30+
}
31+
32+
// Linux kernel prior to 4.11 old versions of Docker don't support `statx`. We
33+
// store the availability in a global to avoid unnecessary syscalls.
34+
//
35+
// 0: Unknown
36+
// 1: Not available
37+
// 2: Available
38+
static STATX_STATE: AtomicU8 = AtomicU8::new(0);
39+
40+
#[inline]
41+
fn _statx(
42+
dirfd: BorrowedFd<'_>,
43+
path: &CStr,
44+
flags: AtFlags,
45+
mask: StatxFlags,
46+
) -> io::Result<Statx> {
47+
match STATX_STATE.load(Ordering::Relaxed) {
48+
0 => statx_init(dirfd, path, flags, mask),
49+
1 => Err(io::Errno::NOSYS),
50+
_ => imp::fs::syscalls::statx(dirfd, path, flags, mask),
51+
}
52+
}
53+
54+
/// The first `statx` call. We don't know if `statx` is available yet.
55+
fn statx_init(
56+
dirfd: BorrowedFd<'_>,
57+
path: &CStr,
58+
flags: AtFlags,
59+
mask: StatxFlags,
60+
) -> io::Result<Statx> {
61+
match imp::fs::syscalls::statx(dirfd, path, flags, mask) {
62+
Err(io::Errno::NOSYS) => statx_error_nosys(),
63+
Err(io::Errno::PERM) => statx_error_perm(),
64+
result => {
65+
STATX_STATE.store(2, Ordering::Relaxed);
66+
result
67+
}
68+
}
69+
}
70+
71+
/// The first `statx` call failed with `NOSYS` (or something we're treating
72+
/// like `NOSYS`).
73+
#[cold]
74+
fn statx_error_nosys() -> io::Result<Statx> {
75+
STATX_STATE.store(1, Ordering::Relaxed);
76+
Err(io::Errno::NOSYS)
77+
}
78+
79+
/// The first `statx` call failed with `PERM`.
80+
#[cold]
81+
fn statx_error_perm() -> io::Result<Statx> {
82+
// Some old versions of Docker have `statx` fail with `PERM` when it isn't
83+
// recognized. Check whether `statx` really is available, and if so, fail
84+
// with `PERM`, and if not, treat it like `NOSYS`.
85+
if imp::fs::syscalls::is_statx_available() {
86+
STATX_STATE.store(2, Ordering::Relaxed);
87+
Err(io::Errno::PERM)
88+
} else {
89+
statx_error_nosys()
90+
}
2691
}

src/imp/libc/fs/syscalls.rs

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ use crate::fs::SealFlags;
8989
)))]
9090
// not implemented in libc for netbsd yet
9191
use crate::fs::StatFs;
92+
#[cfg(any(target_os = "android", target_os = "linux"))]
93+
use crate::fs::{cwd, RenameFlags, ResolveFlags, Statx, StatxFlags};
9294
#[cfg(not(any(
9395
target_os = "ios",
9496
target_os = "macos",
@@ -97,8 +99,6 @@ use crate::fs::StatFs;
9799
)))]
98100
use crate::fs::{Dev, FileType};
99101
use crate::fs::{FdFlags, Mode, OFlags, Stat, Timestamps};
100-
#[cfg(any(target_os = "android", target_os = "linux"))]
101-
use crate::fs::{RenameFlags, ResolveFlags, Statx, StatxFlags};
102102
use crate::io::{self, OwnedFd, SeekFrom};
103103
#[cfg(not(target_os = "wasi"))]
104104
use crate::process::{Gid, Uid};
@@ -116,6 +116,8 @@ use core::convert::TryInto;
116116
))]
117117
use core::mem::size_of;
118118
use core::mem::MaybeUninit;
119+
#[cfg(any(target_os = "android", target_os = "linux"))]
120+
use core::ptr::null;
119121
#[cfg(any(
120122
target_os = "android",
121123
target_os = "ios",
@@ -351,14 +353,11 @@ pub(crate) fn statat(dirfd: BorrowedFd<'_>, path: &CStr, flags: AtFlags) -> io::
351353
any(target_pointer_width = "32", target_arch = "mips64")
352354
))]
353355
{
354-
if !NO_STATX.load(Relaxed) {
355-
match statx(dirfd, path, flags, StatxFlags::ALL) {
356-
Ok(x) => return statx_to_stat(x),
357-
Err(io::Errno::NOSYS) => NO_STATX.store(true, Relaxed),
358-
Err(e) => return Err(e),
359-
}
356+
match statx(dirfd, path, flags, StatxFlags::ALL) {
357+
Ok(x) => return statx_to_stat(x),
358+
Err(io::Error::NOSYS) => statat_old(dirfd, path, flags),
359+
Err(e) => return Err(e),
360360
}
361-
statat_old(dirfd, path, flags)
362361
}
363362

364363
// Main version: libc is y2038 safe. Or, the platform is not y2038 safe and
@@ -904,13 +903,6 @@ pub(crate) fn flock(fd: BorrowedFd<'_>, operation: FlockOperation) -> io::Result
904903
unsafe { ret(c::flock(borrowed_fd(fd), operation as c::c_int)) }
905904
}
906905

907-
/// `statx` was introduced in Linux 4.11.
908-
#[cfg(all(
909-
any(target_os = "android", target_os = "linux"),
910-
any(target_pointer_width = "32", target_arch = "mips64")
911-
))]
912-
static NO_STATX: AtomicBool = AtomicBool::new(false);
913-
914906
pub(crate) fn fstat(fd: BorrowedFd<'_>) -> io::Result<Stat> {
915907
// 32-bit and mips64 Linux: `struct stat64` is not y2038 compatible; use
916908
// `statx`.
@@ -919,14 +911,11 @@ pub(crate) fn fstat(fd: BorrowedFd<'_>) -> io::Result<Stat> {
919911
any(target_pointer_width = "32", target_arch = "mips64")
920912
))]
921913
{
922-
if !NO_STATX.load(Relaxed) {
923-
match statx(fd, cstr!(""), AtFlags::EMPTY_PATH, StatxFlags::ALL) {
924-
Ok(x) => return statx_to_stat(x),
925-
Err(io::Errno::NOSYS) => NO_STATX.store(true, Relaxed),
926-
Err(e) => return Err(e),
927-
}
914+
match statx(fd, cstr!(""), AtFlags::EMPTY_PATH, StatxFlags::ALL) {
915+
Ok(x) => return statx_to_stat(x),
916+
Err(io::Error::NOSYS) => fstat_old(fd),
917+
Err(e) => return Err(e),
928918
}
929-
fstat_old(fd)
930919
}
931920

932921
// Main version: libc is y2038 safe. Or, the platform is not y2038 safe and
@@ -1400,13 +1389,9 @@ fn stat64_to_stat(s64: c::stat64) -> io::Result<Stat> {
14001389
}
14011390

14021391
#[cfg(any(target_os = "android", target_os = "linux"))]
1403-
#[allow(non_upper_case_globals)]
1404-
pub(crate) fn statx(
1405-
dirfd: BorrowedFd<'_>,
1406-
path: &CStr,
1407-
flags: AtFlags,
1408-
mask: StatxFlags,
1409-
) -> io::Result<Statx> {
1392+
mod sys {
1393+
use super::{c, BorrowedFd, Statx};
1394+
14101395
#[cfg(all(target_os = "android", target_arch = "arm"))]
14111396
const SYS_statx: c::c_long = 397;
14121397
#[cfg(all(target_os = "android", target_arch = "x86"))]
@@ -1417,18 +1402,27 @@ pub(crate) fn statx(
14171402
const SYS_statx: c::c_long = 332;
14181403

14191404
weak_or_syscall! {
1420-
fn statx(
1405+
pub(super) fn statx(
14211406
pirfd: BorrowedFd<'_>,
14221407
path: *const c::c_char,
14231408
flags: c::c_int,
14241409
mask: c::c_uint,
14251410
buf: *mut Statx
14261411
) via SYS_statx -> c::c_int
14271412
}
1413+
}
14281414

1415+
#[cfg(any(target_os = "android", target_os = "linux"))]
1416+
#[allow(non_upper_case_globals)]
1417+
pub(crate) fn statx(
1418+
dirfd: BorrowedFd<'_>,
1419+
path: &CStr,
1420+
flags: AtFlags,
1421+
mask: StatxFlags,
1422+
) -> io::Result<Statx> {
14291423
let mut statx_buf = MaybeUninit::<Statx>::uninit();
14301424
unsafe {
1431-
ret(statx(
1425+
ret(sys::statx(
14321426
dirfd,
14331427
c_str(path),
14341428
flags.bits(),
@@ -1439,6 +1433,19 @@ pub(crate) fn statx(
14391433
}
14401434
}
14411435

1436+
#[cfg(any(target_os = "android", target_os = "linux"))]
1437+
#[inline]
1438+
pub(crate) fn is_statx_available() -> bool {
1439+
unsafe {
1440+
// Call `statx` with null pointers so that if it fails for any reason
1441+
// other than `EFAULT`, we know it's not supported.
1442+
matches!(
1443+
ret(sys::statx(cwd(), null(), 0, 0, null_mut())),
1444+
Err(io::Errno::FAULT)
1445+
)
1446+
}
1447+
}
1448+
14421449
#[cfg(any(target_os = "ios", target_os = "macos"))]
14431450
pub(crate) unsafe fn fcopyfile(
14441451
from: BorrowedFd<'_>,

src/imp/linux_raw/fs/syscalls.rs

Lines changed: 46 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,9 @@
88
#![allow(clippy::undocumented_unsafe_blocks)]
99

1010
use super::super::c;
11-
#[cfg(all(
12-
target_pointer_width = "32",
13-
any(target_arch = "arm", target_arch = "mips", target_arch = "powerpc")
14-
))]
15-
use super::super::conv::zero;
1611
use super::super::conv::{
1712
by_ref, c_int, c_uint, dev_t, oflags_for_open_how, opt_mut, pass_usize, raw_fd, ret, ret_c_int,
18-
ret_c_uint, ret_owned_fd, ret_usize, size_of, slice_mut,
13+
ret_c_uint, ret_owned_fd, ret_usize, size_of, slice_mut, zero,
1914
};
2015
#[cfg(target_pointer_width = "64")]
2116
use super::super::conv::{loff_t, loff_t_from_u64, ret_u64};
@@ -416,22 +411,15 @@ pub(crate) fn flock(fd: BorrowedFd<'_>, operation: FlockOperation) -> io::Result
416411
unsafe { ret(syscall!(__NR_flock, fd, c_uint(operation as c::c_uint))) }
417412
}
418413

419-
/// `statx` was introduced in Linux 4.11.
420-
#[cfg(any(target_pointer_width = "32", target_arch = "mips64"))]
421-
static NO_STATX: AtomicBool = AtomicBool::new(false);
422-
423414
#[inline]
424415
pub(crate) fn fstat(fd: BorrowedFd<'_>) -> io::Result<Stat> {
425416
#[cfg(any(target_pointer_width = "32", target_arch = "mips64"))]
426417
{
427-
if !NO_STATX.load(Relaxed) {
428-
match statx(fd, cstr!(""), AtFlags::EMPTY_PATH, StatxFlags::ALL) {
429-
Ok(x) => return statx_to_stat(x),
430-
Err(io::Errno::NOSYS) => NO_STATX.store(true, Relaxed),
431-
Err(e) => return Err(e),
432-
}
418+
match statx(fd, cstr!(""), AtFlags::EMPTY_PATH, StatxFlags::ALL) {
419+
Ok(x) => statx_to_stat(x),
420+
Err(io::Errno::NOSYS) => fstat_old(fd),
421+
Err(e) => Err(e),
433422
}
434-
fstat_old(fd)
435423
}
436424

437425
#[cfg(all(target_pointer_width = "64", not(target_arch = "mips64")))]
@@ -462,19 +450,16 @@ fn fstat_old(fd: BorrowedFd<'_>) -> io::Result<Stat> {
462450
pub(crate) fn stat(filename: &CStr) -> io::Result<Stat> {
463451
#[cfg(any(target_pointer_width = "32", target_arch = "mips64"))]
464452
{
465-
if !NO_STATX.load(Relaxed) {
466-
match statx(
467-
crate::fs::cwd().as_fd(),
468-
filename,
469-
AtFlags::empty(),
470-
StatxFlags::ALL,
471-
) {
472-
Ok(x) => return statx_to_stat(x),
473-
Err(io::Errno::NOSYS) => NO_STATX.store(true, Relaxed),
474-
Err(e) => return Err(e),
475-
}
453+
match statx(
454+
crate::fs::cwd().as_fd(),
455+
filename,
456+
AtFlags::empty(),
457+
StatxFlags::ALL,
458+
) {
459+
Ok(x) => return statx_to_stat(x),
460+
Err(io::Errno::NOSYS) => stat_old(filename),
461+
Err(e) => return Err(e),
476462
}
477-
stat_old(filename)
478463
}
479464

480465
#[cfg(all(target_pointer_width = "64", not(target_arch = "mips64")))]
@@ -524,14 +509,11 @@ fn stat_old(filename: &CStr) -> io::Result<Stat> {
524509
pub(crate) fn statat(dirfd: BorrowedFd<'_>, filename: &CStr, flags: AtFlags) -> io::Result<Stat> {
525510
#[cfg(any(target_pointer_width = "32", target_arch = "mips64"))]
526511
{
527-
if !NO_STATX.load(Relaxed) {
528-
match statx(dirfd, filename, flags, StatxFlags::ALL) {
529-
Ok(x) => return statx_to_stat(x),
530-
Err(io::Errno::NOSYS) => NO_STATX.store(true, Relaxed),
531-
Err(e) => return Err(e),
532-
}
512+
match statx(dirfd, filename, flags, StatxFlags::ALL) {
513+
Ok(x) => return statx_to_stat(x),
514+
Err(io::Errno::NOSYS) => statat_old(dirfd, filename, flags),
515+
Err(e) => return Err(e),
533516
}
534-
statat_old(dirfd, filename, flags)
535517
}
536518

537519
#[cfg(all(target_pointer_width = "64", not(target_arch = "mips64")))]
@@ -581,19 +563,16 @@ fn statat_old(dirfd: BorrowedFd<'_>, filename: &CStr, flags: AtFlags) -> io::Res
581563
pub(crate) fn lstat(filename: &CStr) -> io::Result<Stat> {
582564
#[cfg(any(target_pointer_width = "32", target_arch = "mips64"))]
583565
{
584-
if !NO_STATX.load(Relaxed) {
585-
match statx(
586-
crate::fs::cwd().as_fd(),
587-
filename,
588-
AtFlags::SYMLINK_NOFOLLOW,
589-
StatxFlags::ALL,
590-
) {
591-
Ok(x) => return statx_to_stat(x),
592-
Err(io::Errno::NOSYS) => NO_STATX.store(true, Relaxed),
593-
Err(e) => return Err(e),
594-
}
566+
match statx(
567+
crate::fs::cwd().as_fd(),
568+
filename,
569+
AtFlags::SYMLINK_NOFOLLOW,
570+
StatxFlags::ALL,
571+
) {
572+
Ok(x) => return statx_to_stat(x),
573+
Err(io::Errno::NOSYS) => lstat_old(filename),
574+
Err(e) => return Err(e),
595575
}
596-
lstat_old(filename)
597576
}
598577

599578
#[cfg(all(target_pointer_width = "64", not(target_arch = "mips64")))]
@@ -759,6 +738,25 @@ pub(crate) fn statx(
759738
}
760739
}
761740

741+
#[inline]
742+
pub(crate) fn is_statx_available() -> bool {
743+
unsafe {
744+
// Call `statx` with null pointers so that if it fails for any reason
745+
// other than `EFAULT`, we know it's not supported.
746+
matches!(
747+
ret(syscall!(
748+
__NR_statx,
749+
raw_fd(AT_FDCWD),
750+
zero(),
751+
zero(),
752+
zero(),
753+
zero()
754+
)),
755+
Err(io::Errno::FAULT)
756+
)
757+
}
758+
}
759+
762760
#[inline]
763761
pub(crate) fn fstatfs(fd: BorrowedFd<'_>) -> io::Result<StatFs> {
764762
#[cfg(target_pointer_width = "32")]

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@
6767
//! are used (and documentation aliases are used so that the original names
6868
//! can still be searched for).
6969
//! - Provide y2038 compatibility, on platforms which support this.
70+
//! - Correct selected platform bugs, such as behavioral differences when
71+
//! running under seccomp.
7072
//!
7173
//! Things they don't do include:
7274
//! - Detecting whether functions are supported at runtime.

0 commit comments

Comments
 (0)