Skip to content

Miri reports UB on MacOS when using fstat #1668

Description

@Jefffrey

Reproduction, testing via tempfile (3.27.0):

#[test]
fn test123() {
    let a = tempfile::NamedTempFile::new().unwrap();
    a.reopen().unwrap();
}
  • For reference this is on macOS 26.5.2

Running test:

test123 (main)$ MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running unittests src/main.rs (/Users/jeffrey/.cargo_target_cache/miri/aarch64-apple-darwin/debug/deps/test123-9a724f3234e25023)

running 1 test
test tests::test123 ... error: Undefined Behavior: constructing invalid value of type libc::unix::bsd::apple::stat: at .st_lspare, encountered uninitialized memory, but expected an integer
    --> /Users/jeffrey/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.4/src/backend/libc/fs/syscalls.rs:1570:20
     |
1570 |         let stat = stat.assume_init();
     |                    ^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
     |
     = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
     = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
     = note: this is on thread `tests::test123`
     = note: stack backtrace:
             0: rustix::backend::fs::syscalls::fstat
                 at /Users/jeffrey/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.4/src/backend/libc/fs/syscalls.rs:1570:20: 1570:38
             1: rustix::fs::fd::fstat::<&std::fs::File>
                 at /Users/jeffrey/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.4/src/fs/fd.rs:157:5: 157:45
             2: tempfile::file::imp::platform::reopen
                 at /Users/jeffrey/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tempfile-3.27.0/src/file/imp/unix.rs:83:20: 83:43
             3: tempfile::NamedTempFile::reopen
                 at /Users/jeffrey/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tempfile-3.27.0/src/file/mod.rs:952:9: 952:63
             4: tests::test123
                 at src/main.rs:10:9: 10:19
             5: tests::test123::{closure#0}
                 at src/main.rs:8:17: 8:17

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error

error: test failed, to rerun pass `--bin test123`

Caused by:
  process didn't exit successfully: `/Users/jeffrey/.rustup/toolchains/nightly-aarch64-apple-darwin/bin/cargo-miri runner /Users/jeffrey/.cargo_target_cache/miri/aarch64-apple-darwin/debug/deps/test123-9a724f3234e25023` (exit status: 1)
note: test exited abnormally; to see the full output pass --no-capture to the harness.

It seems at this stat call:

let mut stat = MaybeUninit::<Stat>::uninit();
ret(c::fstat(borrowed_fd(fd), stat.as_mut_ptr().cast()))?;
let stat = stat.assume_init();

In the Miri shim they don't initialize fields st_lspare and st_qspare:

As the man page states they are reserved:

     struct stat { /* when _DARWIN_FEATURE_64_BIT_INODE is defined */
	 dev_t		 st_dev;	   /* ID of device containing file */
	 mode_t 	 st_mode;	   /* Mode of file (see below) */
	 nlink_t	 st_nlink;	   /* Number of hard links */
	 ino_t		 st_ino;	   /* File serial number */
	 uid_t		 st_uid;	   /* User ID of the file */
	 gid_t		 st_gid;	   /* Group ID of the file */
	 dev_t		 st_rdev;	   /* Device ID */
	 struct timespec st_atimespec;	   /* time of last access */
	 struct timespec st_mtimespec;	   /* time of last data modification */
	 struct timespec st_ctimespec;	   /* time of last status change */
	 struct timespec st_birthtimespec; /* time of file creation(birth) */
	 off_t		 st_size;	   /* file size, in bytes */
	 blkcnt_t	 st_blocks;	   /* blocks allocated for file */
	 blksize_t	 st_blksize;	   /* optimal blocksize for I/O */
	 uint32_t	 st_flags;	   /* user defined flags for file */
	 uint32_t	 st_gen;	   /* file generation number */
	 int32_t	 st_lspare;	   /* RESERVED: DO NOT USE! */
	 int64_t	 st_qspare[2];	   /* RESERVED: DO NOT USE! */
     };

But above code assumes the struct (and all fields) are initialized

Activity

  1. Jefffrey commented on Aug 27, 2026

    @Jefffrey
    Author

    (I don't really have a usecase for this, its just something I found and thought its worth recording at least)

  2. RyanWelly commented on Sep 6, 2026

    @RyanWelly

    Also occurs when cross-interpreting x86-64-unknown-freebsd and i686-unknown-linux-gnu with similar errors. I'm running this on macOS 26.5.2 and with tempfile 3.27.0 with the same testcase as Jefffrey.

    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.04s
         Running unittests src/main.rs (target/miri/x86_64-unknown-freebsd/debug/build/fstat64-test/975e78630639fdcc/out/fstat64_test-975e78630639fdcc)
    
    running 1 test
    test tests::test123 ...
    error: Undefined Behavior: constructing invalid value of type libc::unix::bsd::freebsdlike::freebsd::freebsd12::stat: at .st_spare[18], encountered uninitialized memory, but expected an integer
        --> /Users/ryan/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.4/src/backend/libc/fs/syscalls.rs:1570:20
         |
    1570 |         let stat = stat.assume_init();
         |                    ^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
         |
         = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
         = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
         = note: this is on thread `tests::test123`
         = note: stack backtrace:
                 0: rustix::backend::fs::syscalls::fstat
                     at /Users/ryan/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.4/src/backend/libc/fs/syscalls.rs:1570:20: 1570:38
                 1: rustix::fs::fd::fstat::<&std::fs::File>
                     at /Users/ryan/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.4/src/fs/fd.rs:157:5: 157:45
                 2: tempfile::file::imp::platform::reopen
                     at /Users/ryan/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tempfile-3.27.0/src/file/imp/unix.rs:83:20: 83:43
                 3: tempfile::NamedTempFile::reopen
                     at /Users/ryan/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tempfile-3.27.0/src/file/mod.rs:952:9: 952:63
                 4: tests::test123
                     at src/main.rs:10:9: 10:19
                 5: tests::test123::{closure#0}
                     at src/main.rs:8:17: 8:17
    
    note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
    
    error: aborting due to 1 previous error
    
    error: test failed, to rerun pass `--bin fstat64-test`
    
    Caused by:
      process didn't exit successfully: `/Users/ryan/.rustup/toolchains/miri/bin/cargo-miri runner /Users/ryan/Documents/Engineering/Open-Source/miri/fstat64-test/target/miri/x86_64-unknown-freebsd/debug/build/fstat64-test/975e78630639fdcc/out/fstat64_test-975e78630639fdcc` (exit status: 1)
    note: test exited abnormally; to see the full output pass --no-capture to the harness.
  3. xtqqczze commented on Sep 6, 2026

    @xtqqczze
    Contributor

    This is ultimately an issue in libc rather than rustix. The proposed fix in rust-lang/libc#5497 changes the private stat fields to use Padding, which should resolve the Undefined Behavior reported by Miri.

  4. tgross35 commented on Sep 8, 2026

    @tgross35

    Nope, this is unsoundness in rustix. The full code is:

        unsafe {
            #[cfg(test)]
            static_assertions::assert_eq_size!(Stat, c::stat);
    
            let mut stat = MaybeUninit::<Stat>::uninit();
            ret(c::fstat(borrowed_fd(fd), stat.as_mut_ptr().cast()))?;
            let stat = stat.assume_init();
            #[cfg(apple)]
            let stat = fix_negative_stat_nsecs(stat);
            Ok(stat)
        }

    It's making a bogus assertion that stat is fully initialized, while fstat is absolutely no obligation to do this for things like padding fields. This is doing exactly what we say not to do at https://docs.rs/libc/latest/libc/#usage-guidelines:

    Never construct a libc struct with MaybeUninit::uninit(), initialize it, then call assume_init. Many structures have padding fields or may gain fields in the future, and it is far too easy to end up calling assume_init on partially initialized data.

    Instead, use MaybeUninit::zeroed() or the Default implementations that are slowly being added. Alternatively, access fields only via raw pointer without ever using assume_init.

  5. tgross35 commented on Sep 8, 2026

    @tgross35

    See also nix-rust/nix#2720, same class of unsoundness in nix

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions