Skip to content

Commit 4313471

Browse files
authored
Rollup merge of #106661 - mjguzik:linux_statx, r=Mark-Simulacrum
Stop probing for statx unless necessary As is the current toy program: fn main() -> std::io::Result<()> { use std::fs; let metadata = fs::metadata("foo.txt")?; assert!(!metadata.is_dir()); Ok(()) } ... observed under strace will issue: [snip] statx(0, NULL, AT_STATX_SYNC_AS_STAT, STATX_ALL, NULL) = -1 EFAULT (Bad address) statx(AT_FDCWD, "foo.txt", AT_STATX_SYNC_AS_STAT, STATX_ALL, {stx_mask=STATX_ALL|STATX_MNT_ID, stx_attributes=0, stx_mode=S_IFREG|0644, stx_size=0, ...}) = 0 While statx is not necessarily always present, checking for it can be delayed to the first error condition. Said condition may very well never happen, in which case the check got avoided altogether. Note this is still suboptimal as there still will be programs issuing it, but bulk of the problem is removed. Tested by forbidding the syscall for the binary and observing it correctly falls back to newfstatat. While here tidy up the commentary, in particular by denoting some problems with the current approach.
2 parents d7bc758 + b49aa8d commit 4313471

File tree

2 files changed

+44
-29
lines changed

2 files changed

+44
-29
lines changed

library/std/src/sys/unix/fs.rs

+41-27
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,13 @@ cfg_has_statx! {{
149149
) -> Option<io::Result<FileAttr>> {
150150
use crate::sync::atomic::{AtomicU8, Ordering};
151151

152-
// Linux kernel prior to 4.11 or glibc prior to glibc 2.28 don't support `statx`
153-
// We store the availability in global to avoid unnecessary syscalls.
154-
// 0: Unknown
155-
// 1: Not available
156-
// 2: Available
157-
static STATX_STATE: AtomicU8 = AtomicU8::new(0);
152+
// Linux kernel prior to 4.11 or glibc prior to glibc 2.28 don't support `statx`.
153+
// We check for it on first failure and remember availability to avoid having to
154+
// do it again.
155+
#[repr(u8)]
156+
enum STATX_STATE{ Unknown = 0, Present, Unavailable }
157+
static STATX_SAVED_STATE: AtomicU8 = AtomicU8::new(STATX_STATE::Unknown as u8);
158+
158159
syscall! {
159160
fn statx(
160161
fd: c_int,
@@ -165,31 +166,44 @@ cfg_has_statx! {{
165166
) -> c_int
166167
}
167168

168-
match STATX_STATE.load(Ordering::Relaxed) {
169-
0 => {
170-
// It is a trick to call `statx` with null pointers to check if the syscall
171-
// is available. According to the manual, it is expected to fail with EFAULT.
172-
// We do this mainly for performance, since it is nearly hundreds times
173-
// faster than a normal successful call.
174-
let err = cvt(statx(0, ptr::null(), 0, libc::STATX_ALL, ptr::null_mut()))
175-
.err()
176-
.and_then(|e| e.raw_os_error());
177-
// We don't check `err == Some(libc::ENOSYS)` because the syscall may be limited
178-
// and returns `EPERM`. Listing all possible errors seems not a good idea.
179-
// See: https://github.com/rust-lang/rust/issues/65662
180-
if err != Some(libc::EFAULT) {
181-
STATX_STATE.store(1, Ordering::Relaxed);
182-
return None;
183-
}
184-
STATX_STATE.store(2, Ordering::Relaxed);
185-
}
186-
1 => return None,
187-
_ => {}
169+
if STATX_SAVED_STATE.load(Ordering::Relaxed) == STATX_STATE::Unavailable as u8 {
170+
return None;
188171
}
189172

190173
let mut buf: libc::statx = mem::zeroed();
191174
if let Err(err) = cvt(statx(fd, path, flags, mask, &mut buf)) {
192-
return Some(Err(err));
175+
if STATX_SAVED_STATE.load(Ordering::Relaxed) == STATX_STATE::Present as u8 {
176+
return Some(Err(err));
177+
}
178+
179+
// Availability not checked yet.
180+
//
181+
// First try the cheap way.
182+
if err.raw_os_error() == Some(libc::ENOSYS) {
183+
STATX_SAVED_STATE.store(STATX_STATE::Unavailable as u8, Ordering::Relaxed);
184+
return None;
185+
}
186+
187+
// Error other than `ENOSYS` is not a good enough indicator -- it is
188+
// known that `EPERM` can be returned as a result of using seccomp to
189+
// block the syscall.
190+
// Availability is checked by performing a call which expects `EFAULT`
191+
// if the syscall is usable.
192+
// See: https://github.com/rust-lang/rust/issues/65662
193+
// FIXME this can probably just do the call if `EPERM` was received, but
194+
// previous iteration of the code checked it for all errors and for now
195+
// this is retained.
196+
// FIXME what about transient conditions like `ENOMEM`?
197+
let err2 = cvt(statx(0, ptr::null(), 0, libc::STATX_ALL, ptr::null_mut()))
198+
.err()
199+
.and_then(|e| e.raw_os_error());
200+
if err2 == Some(libc::EFAULT) {
201+
STATX_SAVED_STATE.store(STATX_STATE::Present as u8, Ordering::Relaxed);
202+
return Some(Err(err));
203+
} else {
204+
STATX_SAVED_STATE.store(STATX_STATE::Unavailable as u8, Ordering::Relaxed);
205+
return None;
206+
}
193207
}
194208

195209
// We cannot fill `stat64` exhaustively because of private padding fields.

src/tools/miri/tests/pass-dep/shims/libc-fs-with-isolation.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ fn main() {
2222
}
2323

2424
// test `stat`
25-
assert_eq!(fs::metadata("foo.txt").unwrap_err().kind(), ErrorKind::PermissionDenied);
25+
let err = fs::metadata("foo.txt").unwrap_err();
26+
assert_eq!(err.kind(), ErrorKind::PermissionDenied);
2627
// check that it is the right kind of `PermissionDenied`
27-
assert_eq!(Error::last_os_error().raw_os_error(), Some(libc::EACCES));
28+
assert_eq!(err.raw_os_error(), Some(libc::EACCES));
2829
}

0 commit comments

Comments
 (0)