Skip to content

Commit 6e177ac

Browse files
Return file creation times on Linux (using statx) (#248)
* Return file creation times on Linux (using statx) Rust's standard library returns file creation times on Linux (at least with glibc), and now rust-std will too. File creation times are available on Linux via the `statx` syscall (not `fstatat`), introduced in kernel 4.11, which was released in April 2017. The Rust standard library already uses `statx` on Linux with glibc. Making cap-std support file creation times on Linux required two changes: 1. `File::metadata()` uses the standard library (which uses `statx`), but this wouldn't ever copy over the created field on Linux. Now, any time the created field is set in the std Metadata struct, it's also set in the cap-primitives Metadata, regardless of platform. 2. `stat_unchecked` is used in several places, including fetching DirEntry metadata. Before, it called `fstatat` directly. Now, it calls `statx` on Linux when available, and it falls back to `fstatat` otherwise. Fortunately, Dan Gohman (@sunfishcode) had already added a method to convert `statx` results in commit d1fa735 (PR #105) in 2020. This commit also adds a new test to make sure file creation times are set in cap-std Metadata if they are set in std Metadata. * stat_unchecked: Add backticks to comments from PR suggestions Co-authored-by: Dan Gohman <[email protected]> * stat_unchecked: Reflow comment * stat_unchecked: Rewrite match statement for clarity * Rewrite file created times test to do exact comparisons against std * Move file created times test into fs_additional * Expand file created times test to check entire Metadata * stat_unchecked: Handle EPERM errors from statx * fs_additional: Fix non-Unix test build in metadata test I had used `cfg!(unix)` instead of `#[cfg(unix)]` in 2eca023, which doesn't conditionally compile out the Unix-specific code. Co-authored-by: Dan Gohman <[email protected]>
1 parent 17931d6 commit 6e177ac

File tree

4 files changed

+174
-23
lines changed

4 files changed

+174
-23
lines changed

cap-primitives/src/fs/metadata.rs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -48,35 +48,13 @@ impl Metadata {
4848

4949
#[inline]
5050
fn from_parts(std: fs::Metadata, ext: MetadataExt, file_type: FileType) -> Self {
51-
// TODO: Initialize `created` on Linux with `std.created().ok()` once we
52-
// make use of `statx`.
5351
Self {
5452
file_type,
5553
len: std.len(),
5654
permissions: Permissions::from_std(std.permissions()),
5755
modified: std.modified().ok().map(SystemTime::from_std),
5856
accessed: std.accessed().ok().map(SystemTime::from_std),
59-
60-
#[cfg(any(
61-
target_os = "freebsd",
62-
target_os = "openbsd",
63-
target_os = "macos",
64-
target_os = "ios",
65-
target_os = "netbsd",
66-
windows,
67-
))]
6857
created: std.created().ok().map(SystemTime::from_std),
69-
70-
#[cfg(not(any(
71-
target_os = "freebsd",
72-
target_os = "openbsd",
73-
target_os = "macos",
74-
target_os = "ios",
75-
target_os = "netbsd",
76-
windows,
77-
)))]
78-
created: None,
79-
8058
ext,
8159
}
8260
}

cap-primitives/src/rustix/fs/metadata_ext.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
use crate::fs::PermissionsExt;
44
use crate::fs::{FileTypeExt, Metadata};
55
use crate::time::{Duration, SystemClock, SystemTime};
6+
// TODO: update all these to
7+
// #[cfg(any(target_os = "android", target_os = "linux"))]
8+
// once we're on restix >= v0.34.3.
69
#[cfg(all(target_os = "linux", target_env = "gnu"))]
710
use rustix::fs::{makedev, Statx};
811
use rustix::fs::{RawMode, Stat};
@@ -222,7 +225,6 @@ impl MetadataExt {
222225
/// Constructs a new instance of `Metadata` from the given `Statx`.
223226
#[cfg(all(target_os = "linux", target_env = "gnu"))]
224227
#[inline]
225-
#[allow(dead_code)] // TODO: use `statx` when possible.
226228
pub(crate) fn from_rustix_statx(statx: Statx) -> Metadata {
227229
Metadata {
228230
file_type: FileTypeExt::from_raw_mode(RawMode::from(statx.stx_mode)),

cap-primitives/src/rustix/fs/stat_unchecked.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ use rustix::fs::{statat, AtFlags};
33
use std::path::Path;
44
use std::{fs, io};
55

6+
// TODO: update all these to
7+
// #[cfg(any(target_os = "android", target_os = "linux"))]
8+
// once we're on restix >= v0.34.3.
9+
#[cfg(all(target_os = "linux", target_env = "gnu"))]
10+
use rustix::fs::{statx, StatxFlags};
11+
#[cfg(all(target_os = "linux", target_env = "gnu"))]
12+
use std::sync::atomic::{AtomicU8, Ordering};
13+
614
/// *Unsandboxed* function similar to `stat`, but which does not perform
715
/// sandboxing.
816
pub(crate) fn stat_unchecked(
@@ -15,5 +23,54 @@ pub(crate) fn stat_unchecked(
1523
FollowSymlinks::No => AtFlags::SYMLINK_NOFOLLOW,
1624
};
1725

26+
// `statx` is preferred on Linux because it can return creation times.
27+
// Linux kernels prior to 4.11 don't have `statx` and return `ENOSYS`.
28+
// Older versions of Docker/seccomp would return `EPERM` for `statx`; see
29+
// <https://github.com/rust-lang/rust/pull/65685/>. We store the
30+
// availability in a global to avoid unnecessary syscalls.
31+
#[cfg(all(target_os = "linux", target_env = "gnu"))]
32+
{
33+
// 0: Unknown
34+
// 1: Not available
35+
// 2: Available
36+
static STATX_STATE: AtomicU8 = AtomicU8::new(0);
37+
let state = STATX_STATE.load(Ordering::Relaxed);
38+
if state != 1 {
39+
let statx_result = statx(
40+
start,
41+
path,
42+
atflags,
43+
StatxFlags::BASIC_STATS | StatxFlags::BTIME,
44+
);
45+
match statx_result {
46+
Ok(statx) => {
47+
if state == 0 {
48+
STATX_STATE.store(2, Ordering::Relaxed);
49+
}
50+
return Ok(MetadataExt::from_rustix_statx(statx));
51+
}
52+
Err(rustix::io::Error::NOSYS) => STATX_STATE.store(1, Ordering::Relaxed),
53+
Err(rustix::io::Error::PERM) if state == 0 => {
54+
// This is an unlikely case, as `statx` doesn't normally
55+
// return `PERM` errors. One way this can happen is when
56+
// running on old versions of seccomp/Docker. If `statx` on
57+
// the current working directory returns a similar error,
58+
// then stop using `statx`.
59+
if let Err(rustix::io::Error::PERM) = statx(
60+
rustix::fs::cwd(),
61+
"",
62+
AtFlags::EMPTY_PATH,
63+
StatxFlags::empty(),
64+
) {
65+
STATX_STATE.store(1, Ordering::Relaxed);
66+
} else {
67+
return Err(rustix::io::Error::PERM.into());
68+
}
69+
}
70+
Err(e) => return Err(e.into()),
71+
}
72+
}
73+
}
74+
1875
Ok(statat(start, path, atflags).map(MetadataExt::from_rustix)?)
1976
}

tests/fs_additional.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,3 +872,117 @@ fn reopen_fd() {
872872
let tmpdir2 = check!(cap_std::fs::Dir::reopen_dir(&tmpdir.as_filelike()));
873873
assert!(tmpdir2.exists("subdir"));
874874
}
875+
876+
#[test]
877+
fn metadata_vs_std_fs() {
878+
let tmpdir = tmpdir();
879+
check!(tmpdir.create_dir("dir"));
880+
let dir = check!(tmpdir.open_dir("dir"));
881+
let file = check!(dir.create("file"));
882+
883+
let cap_std_dir = check!(dir.dir_metadata());
884+
let cap_std_file = check!(file.metadata());
885+
let cap_std_dir_entry = {
886+
let mut entries = check!(dir.entries());
887+
let entry = check!(entries.next().unwrap());
888+
assert_eq!(entry.file_name(), "file");
889+
assert!(entries.next().is_none(), "unexpected dir entry");
890+
check!(entry.metadata())
891+
};
892+
893+
let std_dir = check!(dir.into_std_file().metadata());
894+
let std_file = check!(file.into_std().metadata());
895+
896+
match std_dir.created() {
897+
Ok(_) => println!("std::fs supports file created times"),
898+
Err(e) => println!("std::fs doesn't support file created times: {}", e),
899+
}
900+
901+
check_metadata(&std_dir, &cap_std_dir);
902+
check_metadata(&std_file, &cap_std_file);
903+
check_metadata(&std_file, &cap_std_dir_entry);
904+
}
905+
906+
fn check_metadata(std: &std::fs::Metadata, cap: &cap_std::fs::Metadata) {
907+
assert_eq!(std.is_dir(), cap.is_dir());
908+
assert_eq!(std.is_file(), cap.is_file());
909+
assert_eq!(std.is_symlink(), cap.is_symlink());
910+
assert_eq!(std.file_type().is_dir(), cap.file_type().is_dir());
911+
assert_eq!(std.file_type().is_file(), cap.file_type().is_file());
912+
assert_eq!(std.file_type().is_symlink(), cap.file_type().is_symlink());
913+
#[cfg(unix)]
914+
{
915+
use std::os::unix::fs::FileTypeExt;
916+
assert_eq!(
917+
std.file_type().is_block_device(),
918+
cap.file_type().is_block_device()
919+
);
920+
assert_eq!(
921+
std.file_type().is_char_device(),
922+
cap.file_type().is_char_device()
923+
);
924+
assert_eq!(std.file_type().is_fifo(), cap.file_type().is_fifo());
925+
assert_eq!(std.file_type().is_socket(), cap.file_type().is_socket());
926+
}
927+
928+
assert_eq!(std.len(), cap.len());
929+
930+
assert_eq!(std.permissions().readonly(), cap.permissions().readonly());
931+
#[cfg(unix)]
932+
{
933+
use std::os::unix::fs::PermissionsExt;
934+
// The standard library returns file format bits with `mode()`, whereas
935+
// cap-std currently doesn't.
936+
assert_eq!(std.permissions().mode() & 0o7777, cap.permissions().mode());
937+
}
938+
939+
// If the standard library supports file modified/accessed/created times,
940+
// then cap-std should too.
941+
if let Ok(expected) = std.modified() {
942+
assert_eq!(expected, check!(cap.modified()).into_std());
943+
}
944+
// The access times might be a little different due to either our own
945+
// or concurrent accesses.
946+
const ACCESS_TOLERANCE_SEC: u32 = 60;
947+
if let Ok(expected) = std.accessed() {
948+
let access_tolerance = std::time::Duration::from_secs(ACCESS_TOLERANCE_SEC.into());
949+
assert!(
950+
((expected - access_tolerance)..(expected + access_tolerance))
951+
.contains(&check!(cap.accessed()).into_std()),
952+
"std accessed {:#?}, cap accessed {:#?}",
953+
expected,
954+
cap.accessed()
955+
);
956+
}
957+
if let Ok(expected) = std.created() {
958+
assert_eq!(expected, check!(cap.created()).into_std());
959+
}
960+
961+
#[cfg(unix)]
962+
{
963+
use std::os::unix::fs::MetadataExt;
964+
assert_eq!(std.dev(), cap.dev());
965+
assert_eq!(std.ino(), cap.ino());
966+
assert_eq!(std.mode(), cap.mode());
967+
assert_eq!(std.nlink(), cap.nlink());
968+
assert_eq!(std.uid(), cap.uid());
969+
assert_eq!(std.gid(), cap.gid());
970+
assert_eq!(std.rdev(), cap.rdev());
971+
assert_eq!(std.size(), cap.size());
972+
assert!(
973+
((std.atime() - i64::from(ACCESS_TOLERANCE_SEC))
974+
..(std.atime() + i64::from(ACCESS_TOLERANCE_SEC)))
975+
.contains(&cap.atime()),
976+
"std atime {}, cap atime {}",
977+
std.atime(),
978+
cap.atime()
979+
);
980+
assert!((0..1_000_000_000).contains(&cap.atime_nsec()));
981+
assert_eq!(std.mtime(), cap.mtime());
982+
assert_eq!(std.mtime_nsec(), cap.mtime_nsec());
983+
assert_eq!(std.ctime(), cap.ctime());
984+
assert_eq!(std.ctime_nsec(), cap.ctime_nsec());
985+
assert_eq!(std.blksize(), cap.blksize());
986+
assert_eq!(std.blocks(), cap.blocks());
987+
}
988+
}

0 commit comments

Comments
 (0)