Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions library/std/src/os/linux/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,24 @@
use crate::fs::Metadata;
#[allow(deprecated)]
use crate::os::linux::raw;
use crate::os::raw::{c_uint, c_void};
use crate::sys::AsInner;
use crate::sys::fs::cfg_has_statx;
cfg_has_statx! {{
use crate::sys::fs::FileAttr;
use crate::sys::FromInner;
} else {
use crate::sys::unsupported;
}}

/// This is the [`statx`] mask expected by [`Metadata::from_statx`], which sets both
/// `STATX_BASIC_STATS` and `STATX_BTIME`. See the [Linux man page] for statx for more
/// details.
///

@Mark-Simulacrum Mark-Simulacrum Jun 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, so it seems like we can't really extend this mask in the future if we want more information unless it's available on all Linux versions we support. E.g., if we for some reason wanted to pass STATX_MNT_ID, we couldn't add it here because the caller may not have allocated enough space for it in their statx they pass to the kernel.

I see that the man page on my system at least says that:

Note that, in general, the kernel does not reject values in mask other than the above. (For an exception, see EINVAL in errors.) Instead, it simply informs the caller which values are supported by this kernel and filesystem via the statx.stx_mask field.
[...]
The status information for the target file is returned in the statx structure pointed to by statxbuf. Included in this is stx_mask which indicates what other information has been returned. stx_mask has the same format as the mask argument and bits are set in it to indicate which fields have been filled in.

I'm wondering:

  • Does std check stax.stx_mask today, and if not, should we do so? Perhaps that's a reason to return Result from from_statx if we didn't in fact get what we asked for?
  • Should we provide a minimum allocation size that we 'recommend' for passing to libc/kernel so that we can extend this?

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the ACP I suggested providing an opaque statx type in the stdlib so that the stdlib can tell the library what size (and alignment) to use for the pointer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also from what Josh said in the ACP:

We don't want to add the opaque struct statx, because people will want to get at the internal structure (e.g. setting additional mask bits, or reading fields out before converting it). People would end up wanting to transmute, and if we create the struct then we have to manage the size and the memory. (Which might also prevent future clever APIs that use kernel-managed buffers or arrays of statx structures or similar, for instance.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm OK landing this as-is if we add unresolved questions to the ACP, but I think we shouldn't stabilize this without a plan for extending this constant. AFAICT, right now we can't really do so (per my previous comment).

I'll nominate for libs-api for now.

/// [`statx`]: https://docs.rs/libc/latest/libc/struct.statx.html
/// [Linux man page]: https://man7.org/linux/man-pages/man2/statx.2.html
#[unstable(feature = "metadata_statx", issue = "156268")]
pub const STATX_MASK: c_uint = libc::STATX_BASIC_STATS | libc::STATX_BTIME;
Comment thread
asder8215 marked this conversation as resolved.

/// OS-specific extensions to [`fs::Metadata`].
///
Expand Down Expand Up @@ -41,6 +58,53 @@ pub trait MetadataExt {
#[allow(deprecated)]
fn as_raw_stat(&self) -> &raw::stat;

/// Creates a [`Metadata`] from a const void pointer populated by the [`statx`] syscall.
///
/// Currently [`Metadata::from_statx`] is only supported on Linux platforms with a target
/// environment of GNU.
Comment on lines +63 to +64

@Darksonn Darksonn May 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was planning to wait for #154981 before implementing this to avoid this clause.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got you. I think the order doesn't matter right now since this function would automatically have musl support once #154981 lands as the PR updates the cfg_has_statx macro. The only thing that needs to be updated is the documentation for this function, which I'm down to keep my eyes on #154981 to see it get merged in.

///
/// # Safety
///
/// The caller must take care to provide a valid const void pointer containing information
/// populated by the [`statx`] syscall. In particular, the provided pointer should contain
/// statx information pertaining to the mask [`STATX_MASK`], so that there will be no
/// uninitialized data encountered in constructing [`Metadata`].
Comment thread
Mark-Simulacrum marked this conversation as resolved.
///
/// Note that the relevant information is copied out of the structure and the pointer is
/// not retained past the call.
///
/// [`Metadata`]: crate::fs::Metadata
/// [`statx`]: https://docs.rs/libc/latest/libc/struct.statx.html
///
/// ```no_run
/// #![feature(metadata_statx)]
/// use libc::statx;
/// use std::ffi::c_void;
/// use std::fs::{write, Metadata};
/// use std::io;
/// use std::os::linux::fs::{MetadataExt, STATX_MASK};
///
/// fn main() -> io::Result<()> {
/// write("hello.txt", "Hello World!")?;
/// let mut buf = Box::<statx>::new_uninit();
/// unsafe {
/// libc::statx(
/// libc::AT_FDCWD,
/// "hello.txt".as_ptr().cast(),
/// libc::AT_STATX_SYNC_AS_STAT,
/// STATX_MASK,
/// buf.as_mut_ptr().cast()
/// );
/// }
/// let statxbuf: Box<statx> = unsafe { buf.assume_init() };
/// let metadata = unsafe { Metadata::from_statx(&*statxbuf as *const statx as *const c_void) };
/// assert_eq!(metadata.len(), 12); // "Hello World!" is 12 bytes
/// Ok(())
/// }
/// ```
#[unstable(feature = "metadata_statx", issue = "156268")]
unsafe fn from_statx(statxbuf: *const c_void) -> Self;

/// Returns the device ID on which this file resides.
///
/// # Examples
Expand Down Expand Up @@ -337,6 +401,14 @@ impl MetadataExt for Metadata {
&*(self.as_inner().as_inner() as *const libc::stat64 as *const raw::stat)
}
}
cfg_has_statx! {{
unsafe fn from_statx(statxbuf: *const c_void) -> Metadata {
Metadata::from_inner(FileAttr::from_statx(*(statxbuf as *const libc::statx)))
}} else {
unsafe fn from_statx(statxbuf: *const c_void) -> Self {
unsupported();
}
}}
fn st_dev(&self) -> u64 {
self.as_inner().as_inner().st_dev as u64
}
Expand Down
2 changes: 2 additions & 0 deletions library/std/src/sys/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ cfg_select! {
#[cfg(any(target_os = "linux", target_os = "android"))]
pub(super) use unix::CachedFileMetadata;
use crate::sys::helpers::run_path_with_cstr as with_native_path;
#[cfg(all(target_os = "linux", target_env = "gnu"))]
pub(crate) use unix::cfg_has_statx;
}
target_os = "windows" => {
mod windows;
Expand Down
94 changes: 59 additions & 35 deletions library/std/src/sys/fs/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ macro_rules! cfg_has_statx {
};
}

#[cfg(all(target_os = "linux", target_env = "gnu"))]
pub(crate) use cfg_has_statx;

cfg_has_statx! {{
#[derive(Clone)]
pub struct FileAttr {
Expand All @@ -133,7 +136,7 @@ cfg_has_statx! {{
}

#[derive(Clone)]
struct StatxExtraFields {
pub struct StatxExtraFields {
// This is needed to check if btime is supported by the filesystem.
stx_mask: u32,
stx_btime: libc::statx_timestamp,
Expand Down Expand Up @@ -212,40 +215,7 @@ cfg_has_statx! {{
STATX_SAVED_STATE.store(STATX_STATE::Present as u8, Ordering::Relaxed);
}

// We cannot fill `stat64` exhaustively because of private padding fields.
let mut stat: stat64 = mem::zeroed();
// `c_ulong` on gnu-mips, `dev_t` otherwise
stat.st_dev = libc::makedev(buf.stx_dev_major, buf.stx_dev_minor) as _;
stat.st_ino = buf.stx_ino as libc::ino64_t;
stat.st_nlink = buf.stx_nlink as libc::nlink_t;
stat.st_mode = buf.stx_mode as libc::mode_t;
stat.st_uid = buf.stx_uid as libc::uid_t;
stat.st_gid = buf.stx_gid as libc::gid_t;
stat.st_rdev = libc::makedev(buf.stx_rdev_major, buf.stx_rdev_minor) as _;
stat.st_size = buf.stx_size as off64_t;
stat.st_blksize = buf.stx_blksize as libc::blksize_t;
stat.st_blocks = buf.stx_blocks as libc::blkcnt64_t;
stat.st_atime = buf.stx_atime.tv_sec as libc::time_t;
// `i64` on gnu-x86_64-x32, `c_ulong` otherwise.
stat.st_atime_nsec = buf.stx_atime.tv_nsec as _;
stat.st_mtime = buf.stx_mtime.tv_sec as libc::time_t;
stat.st_mtime_nsec = buf.stx_mtime.tv_nsec as _;
stat.st_ctime = buf.stx_ctime.tv_sec as libc::time_t;
stat.st_ctime_nsec = buf.stx_ctime.tv_nsec as _;

let extra = StatxExtraFields {
stx_mask: buf.stx_mask,
stx_btime: buf.stx_btime,
// Store full times to avoid 32-bit `time_t` truncation.
#[cfg(target_pointer_width = "32")]
stx_atime: buf.stx_atime,
#[cfg(target_pointer_width = "32")]
stx_ctime: buf.stx_ctime,
#[cfg(target_pointer_width = "32")]
stx_mtime: buf.stx_mtime,
};

Some(Ok(FileAttr { stat, statx_extra_fields: Some(extra) }))
Some(Ok(FileAttr::from_statx(buf)))
}

} else {
Expand Down Expand Up @@ -525,11 +495,65 @@ pub struct DirBuilder {
struct Mode(mode_t);

cfg_has_statx! {{
impl StatxExtraFields {
/// Constructs `StatxExtraFields` from a given `libc::statx` struct
///
/// SAFETY:
/// The caller must take care to provide a `libc::statx` buffer that is
/// populated by the statx syscall with the flags `STATX_BASIC_STATS`
/// and `STATX_BTIME` enabled
pub unsafe fn from_statx(buf: libc::statx) -> Self {
StatxExtraFields {
stx_mask: buf.stx_mask,
stx_btime: buf.stx_btime,
// Store full times to avoid 32-bit `time_t` truncation.
#[cfg(target_pointer_width = "32")]
stx_atime: buf.stx_atime,
#[cfg(target_pointer_width = "32")]
stx_ctime: buf.stx_ctime,
#[cfg(target_pointer_width = "32")]
stx_mtime: buf.stx_mtime,
}
}
}
impl FileAttr {
fn from_stat64(stat: stat64) -> Self {
Self { stat, statx_extra_fields: None }
}

/// Constructs `FileAttr` from a given `libc::statx` struct
///
/// SAFETY:
/// The caller must take care to provide a `libc::statx` buffer that is
/// populated by the statx syscall with the flags `STATX_BASIC_STATS`
/// and `STATX_BTIME` enabled
pub unsafe fn from_statx(buf: libc::statx) -> Self {
// We cannot fill `stat64` exhaustively because of private padding fields.
let mut stat: libc::stat64 = mem::zeroed();
// `c_ulong` on gnu-mips, `dev_t` otherwise
stat.st_dev = libc::makedev((buf).stx_dev_major, (buf).stx_dev_minor) as _;
stat.st_ino = buf.stx_ino as libc::ino64_t;
stat.st_nlink = buf.stx_nlink as libc::nlink_t;
stat.st_mode = buf.stx_mode as libc::mode_t;
stat.st_uid = buf.stx_uid as libc::uid_t;
stat.st_gid = buf.stx_gid as libc::gid_t;
stat.st_rdev = libc::makedev(buf.stx_rdev_major, buf.stx_rdev_minor) as _;
stat.st_size = buf.stx_size as libc::off64_t;
stat.st_blksize = buf.stx_blksize as libc::blksize_t;
stat.st_blocks = buf.stx_blocks as libc::blkcnt64_t;
stat.st_atime = buf.stx_atime.tv_sec as libc::time_t;
// `i64` on gnu-x86_64-x32, `c_ulong` otherwise.
stat.st_atime_nsec = buf.stx_atime.tv_nsec as _;
stat.st_mtime = buf.stx_mtime.tv_sec as libc::time_t;
stat.st_mtime_nsec = buf.stx_mtime.tv_nsec as _;
stat.st_ctime = buf.stx_ctime.tv_sec as libc::time_t;
stat.st_ctime_nsec = buf.stx_ctime.tv_nsec as _;

let statx_extra_fields = Some(StatxExtraFields::from_statx(buf));

Self {stat, statx_extra_fields}
}

#[cfg(target_pointer_width = "32")]
pub fn stx_mtime(&self) -> Option<&libc::statx_timestamp> {
if let Some(ext) = &self.statx_extra_fields {
Expand Down
Loading