Skip to main content

miri/shims/unix/
fs.rs

1//! File and file system access
2
3use std::borrow::Cow;
4use std::ffi::OsString;
5use std::fs::{
6    self, DirBuilder, File, FileType, OpenOptions, TryLockError, read_dir, remove_dir, remove_file,
7    rename,
8};
9use std::io::{self, ErrorKind, Read, Seek, SeekFrom, Write};
10use std::path::{self, Path, PathBuf};
11use std::time::SystemTime;
12
13use rustc_abi::Size;
14use rustc_data_structures::either::Either;
15use rustc_data_structures::fx::FxHashMap;
16use rustc_target::spec::Os;
17
18use self::shims::time::system_time_to_duration;
19use crate::shims::files::FileHandle;
20use crate::shims::os_str::bytes_to_os_str;
21use crate::shims::sig::check_min_vararg_count;
22use crate::shims::unix::fd::{FlockOp, UnixFileDescription};
23use crate::*;
24
25/// An open directory, tracked by DirHandler.
26#[derive(Debug)]
27struct OpenDir {
28    /// The "special" entries that must still be yielded by the iterator.
29    /// Used for `.` and `..`.
30    special_entries: Vec<&'static str>,
31    /// The directory reader on the host.
32    read_dir: fs::ReadDir,
33    /// The most recent entry returned by readdir().
34    /// Will be freed by the next call.
35    entry: Option<Pointer>,
36}
37
38impl OpenDir {
39    fn new(read_dir: fs::ReadDir) -> Self {
40        Self { special_entries: vec!["..", "."], read_dir, entry: None }
41    }
42
43    fn next_host_entry(&mut self) -> Option<io::Result<Either<fs::DirEntry, &'static str>>> {
44        if let Some(special) = self.special_entries.pop() {
45            return Some(Ok(Either::Right(special)));
46        }
47        let entry = self.read_dir.next()?;
48        Some(entry.map(Either::Left))
49    }
50}
51
52#[derive(Debug)]
53struct DirEntry {
54    name: OsString,
55    ino: u64,
56    d_type: i32,
57}
58
59impl UnixFileDescription for FileHandle {
60    fn pread<'tcx>(
61        &self,
62        communicate_allowed: bool,
63        offset: u64,
64        ptr: Pointer,
65        len: usize,
66        ecx: &mut MiriInterpCx<'tcx>,
67        finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
68    ) -> InterpResult<'tcx> {
69        assert!(communicate_allowed, "isolation should have prevented even opening a file");
70        let mut bytes = vec![0; len];
71        // Emulates pread using seek + read + seek to restore cursor position.
72        // Correctness of this emulation relies on sequential nature of Miri execution.
73        // The closure is used to emulate `try` block, since we "bubble" `io::Error` using `?`.
74        let file = &mut &self.file;
75        let mut f = || {
76            let cursor_pos = file.stream_position()?;
77            file.seek(SeekFrom::Start(offset))?;
78            let res = file.read(&mut bytes);
79            // Attempt to restore cursor position even if the read has failed
80            file.seek(SeekFrom::Start(cursor_pos))
81                .expect("failed to restore file position, this shouldn't be possible");
82            res
83        };
84        let result = match f() {
85            Ok(read_size) => {
86                // If reading to `bytes` did not fail, we write those bytes to the buffer.
87                // Crucially, if fewer than `bytes.len()` bytes were read, only write
88                // that much into the output buffer!
89                ecx.write_bytes_ptr(ptr, bytes[..read_size].iter().copied())?;
90                Ok(read_size)
91            }
92            Err(e) => Err(IoError::HostError(e)),
93        };
94        finish.call(ecx, result)
95    }
96
97    fn pwrite<'tcx>(
98        &self,
99        communicate_allowed: bool,
100        ptr: Pointer,
101        len: usize,
102        offset: u64,
103        ecx: &mut MiriInterpCx<'tcx>,
104        finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
105    ) -> InterpResult<'tcx> {
106        assert!(communicate_allowed, "isolation should have prevented even opening a file");
107        // Emulates pwrite using seek + write + seek to restore cursor position.
108        // Correctness of this emulation relies on sequential nature of Miri execution.
109        // The closure is used to emulate `try` block, since we "bubble" `io::Error` using `?`.
110        let file = &mut &self.file;
111        let bytes = ecx.read_bytes_ptr_strip_provenance(ptr, Size::from_bytes(len))?;
112        let mut f = || {
113            let cursor_pos = file.stream_position()?;
114            file.seek(SeekFrom::Start(offset))?;
115            let res = file.write(bytes);
116            // Attempt to restore cursor position even if the write has failed
117            file.seek(SeekFrom::Start(cursor_pos))
118                .expect("failed to restore file position, this shouldn't be possible");
119            res
120        };
121        let result = f();
122        finish.call(ecx, result.map_err(IoError::HostError))
123    }
124
125    fn flock<'tcx>(
126        &self,
127        communicate_allowed: bool,
128        op: FlockOp,
129    ) -> InterpResult<'tcx, io::Result<()>> {
130        assert!(communicate_allowed, "isolation should have prevented even opening a file");
131
132        use FlockOp::*;
133        // We must not block the interpreter loop, so we always `try_lock`.
134        let (res, nonblocking) = match op {
135            SharedLock { nonblocking } => (self.file.try_lock_shared(), nonblocking),
136            ExclusiveLock { nonblocking } => (self.file.try_lock(), nonblocking),
137            Unlock => {
138                return interp_ok(self.file.unlock());
139            }
140        };
141
142        match res {
143            Ok(()) => interp_ok(Ok(())),
144            Err(TryLockError::Error(err)) => interp_ok(Err(err)),
145            Err(TryLockError::WouldBlock) =>
146                if nonblocking {
147                    interp_ok(Err(ErrorKind::WouldBlock.into()))
148                } else {
149                    throw_unsup_format!("blocking `flock` is not currently supported");
150                },
151        }
152    }
153}
154
155/// The table of open directories.
156/// Curiously, Unix/POSIX does not unify this into the "file descriptor" concept... everything
157/// is a file, except a directory is not?
158#[derive(Debug)]
159pub struct DirTable {
160    /// Directory iterators used to emulate libc "directory streams", as used in opendir, readdir,
161    /// and closedir.
162    ///
163    /// When opendir is called, a directory iterator is created on the host for the target
164    /// directory, and an entry is stored in this hash map, indexed by an ID which represents
165    /// the directory stream. When readdir is called, the directory stream ID is used to look up
166    /// the corresponding ReadDir iterator from this map, and information from the next
167    /// directory entry is returned. When closedir is called, the ReadDir iterator is removed from
168    /// the map.
169    streams: FxHashMap<u64, OpenDir>,
170    /// ID number to be used by the next call to opendir
171    next_id: u64,
172}
173
174impl DirTable {
175    #[expect(clippy::arithmetic_side_effects)]
176    fn insert_new(&mut self, read_dir: fs::ReadDir) -> u64 {
177        let id = self.next_id;
178        self.next_id += 1;
179        self.streams.try_insert(id, OpenDir::new(read_dir)).unwrap();
180        id
181    }
182}
183
184impl Default for DirTable {
185    fn default() -> DirTable {
186        DirTable {
187            streams: FxHashMap::default(),
188            // Skip 0 as an ID, because it looks like a null pointer to libc
189            next_id: 1,
190        }
191    }
192}
193
194impl VisitProvenance for DirTable {
195    fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
196        let DirTable { streams, next_id: _ } = self;
197
198        for dir in streams.values() {
199            dir.entry.visit_provenance(visit);
200        }
201    }
202}
203
204fn maybe_sync_file(
205    file: &File,
206    writable: bool,
207    operation: fn(&File) -> std::io::Result<()>,
208) -> std::io::Result<i32> {
209    if !writable && cfg!(windows) {
210        // sync_all() and sync_data() will return an error on Windows hosts if the file is not opened
211        // for writing. (FlushFileBuffers requires that the file handle have the
212        // GENERIC_WRITE right)
213        Ok(0i32)
214    } else {
215        let result = operation(file);
216        result.map(|_| 0i32)
217    }
218}
219
220impl<'tcx> EvalContextExtPrivate<'tcx> for crate::MiriInterpCx<'tcx> {}
221trait EvalContextExtPrivate<'tcx>: crate::MiriInterpCxExt<'tcx> {
222    fn write_stat_buf(
223        &mut self,
224        metadata: FileMetadata,
225        buf_op: &OpTy<'tcx>,
226    ) -> InterpResult<'tcx, i32> {
227        let this = self.eval_context_mut();
228
229        let (access_sec, access_nsec) = metadata.accessed.unwrap_or((0, 0));
230        let (created_sec, created_nsec) = metadata.created.unwrap_or((0, 0));
231        let (modified_sec, modified_nsec) = metadata.modified.unwrap_or((0, 0));
232
233        // We do *not* use `deref_pointer_as` here since determining the right pointee type
234        // is highly non-trivial: it depends on which exact alias of the function was invoked
235        // (e.g. `fstat` vs `fstat64`), and then on FreeBSD it also depends on the ABI level
236        // which can be different between the libc used by std and the libc used by everyone else.
237        let buf = this.deref_pointer(buf_op)?;
238
239        // `libc::S_IF*` constants are of type `mode_t`, which varies in width across targets
240        // (`u16` on macOS, `u32` on Linux). Read the scalar using `mode_t`'s size on the target.
241        let mode_t_size = this.libc_ty_layout("mode_t").size;
242        let mode: u32 = metadata.mode.to_uint(mode_t_size)?.try_into().unwrap();
243
244        this.write_int_fields_named(
245            &[
246                ("st_dev", metadata.dev.unwrap_or(0).into()),
247                ("st_mode", mode.into()),
248                ("st_nlink", metadata.nlink.unwrap_or(0).into()),
249                ("st_ino", metadata.ino.unwrap_or(0).into()),
250                ("st_uid", metadata.uid.unwrap_or(0).into()),
251                ("st_gid", metadata.gid.unwrap_or(0).into()),
252                ("st_rdev", 0),
253                ("st_atime", access_sec.into()),
254                ("st_atime_nsec", access_nsec.into()),
255                ("st_mtime", modified_sec.into()),
256                ("st_mtime_nsec", modified_nsec.into()),
257                ("st_ctime", 0),
258                ("st_ctime_nsec", 0),
259                ("st_size", metadata.size.into()),
260                ("st_blocks", metadata.blocks.unwrap_or(0).into()),
261                ("st_blksize", metadata.blksize.unwrap_or(0).into()),
262            ],
263            &buf,
264        )?;
265
266        if matches!(&this.tcx.sess.target.os, Os::MacOs | Os::FreeBsd) {
267            this.write_int_fields_named(
268                &[
269                    ("st_birthtime", created_sec.into()),
270                    ("st_birthtime_nsec", created_nsec.into()),
271                    ("st_flags", 0),
272                    ("st_gen", 0),
273                ],
274                &buf,
275            )?;
276        }
277
278        if matches!(&this.tcx.sess.target.os, Os::Solaris | Os::Illumos) {
279            let st_fstype = this.project_field_named(&buf, "st_fstype")?;
280            // This is an array; write 0 into first element so that it encodes the empty string.
281            this.write_int(0, &this.project_index(&st_fstype, 0)?)?;
282        }
283
284        interp_ok(0)
285    }
286
287    fn file_type_to_d_type(&self, file_type: std::io::Result<FileType>) -> InterpResult<'tcx, i32> {
288        #[cfg(unix)]
289        use std::os::unix::fs::FileTypeExt;
290
291        let this = self.eval_context_ref();
292        match file_type {
293            Ok(file_type) => {
294                match () {
295                    _ if file_type.is_dir() => interp_ok(this.eval_libc("DT_DIR").to_u8()?.into()),
296                    _ if file_type.is_file() => interp_ok(this.eval_libc("DT_REG").to_u8()?.into()),
297                    _ if file_type.is_symlink() =>
298                        interp_ok(this.eval_libc("DT_LNK").to_u8()?.into()),
299                    // Certain file types are only supported when the host is a Unix system.
300                    #[cfg(unix)]
301                    _ if file_type.is_block_device() =>
302                        interp_ok(this.eval_libc("DT_BLK").to_u8()?.into()),
303                    #[cfg(unix)]
304                    _ if file_type.is_char_device() =>
305                        interp_ok(this.eval_libc("DT_CHR").to_u8()?.into()),
306                    #[cfg(unix)]
307                    _ if file_type.is_fifo() =>
308                        interp_ok(this.eval_libc("DT_FIFO").to_u8()?.into()),
309                    #[cfg(unix)]
310                    _ if file_type.is_socket() =>
311                        interp_ok(this.eval_libc("DT_SOCK").to_u8()?.into()),
312                    // Fallback
313                    _ => interp_ok(this.eval_libc("DT_UNKNOWN").to_u8()?.into()),
314                }
315            }
316            Err(_) => {
317                // Fallback on error
318                interp_ok(this.eval_libc("DT_UNKNOWN").to_u8()?.into())
319            }
320        }
321    }
322
323    fn dir_entry_fields(
324        &self,
325        entry: Either<fs::DirEntry, &'static str>,
326    ) -> InterpResult<'tcx, DirEntry> {
327        let this = self.eval_context_ref();
328        interp_ok(match entry {
329            Either::Left(dir_entry) => {
330                DirEntry {
331                    name: dir_entry.file_name(),
332                    d_type: this.file_type_to_d_type(dir_entry.file_type())?,
333                    // If the host is a Unix system, fill in the inode number with its real value.
334                    // If not, use 0 as a fallback value.
335                    #[cfg(unix)]
336                    ino: std::os::unix::fs::DirEntryExt::ino(&dir_entry),
337                    #[cfg(not(unix))]
338                    ino: 0u64,
339                }
340            }
341            Either::Right(special) =>
342                DirEntry {
343                    name: special.into(),
344                    d_type: this.eval_libc("DT_DIR").to_u8()?.into(),
345                    ino: 0,
346                },
347        })
348    }
349}
350
351impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
352pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
353    fn open(
354        &mut self,
355        path_raw: &OpTy<'tcx>,
356        flag: &OpTy<'tcx>,
357        varargs: &[OpTy<'tcx>],
358    ) -> InterpResult<'tcx, Scalar> {
359        let this = self.eval_context_mut();
360
361        let path_raw = this.read_pointer(path_raw)?;
362        let flag = this.read_scalar(flag)?.to_i32()?;
363
364        let path = this.read_path_from_c_str(path_raw)?;
365        // Files in `/proc` won't work properly.
366        if matches!(this.tcx.sess.target.os, Os::Linux | Os::Android | Os::Illumos | Os::Solaris)
367            && path::absolute(&path).is_ok_and(|path| path.starts_with("/proc"))
368        {
369            this.machine.emit_diagnostic(NonHaltingDiagnostic::FileInProcOpened);
370        }
371
372        // We will "subtract" supported flags from this and at the end check that no bits are left.
373        let mut flag = flag;
374
375        let mut options = OpenOptions::new();
376
377        let o_rdonly = this.eval_libc_i32("O_RDONLY");
378        let o_wronly = this.eval_libc_i32("O_WRONLY");
379        let o_rdwr = this.eval_libc_i32("O_RDWR");
380        // The first two bits of the flag correspond to the access mode in linux, macOS and
381        // windows. We need to check that in fact the access mode flags for the current target
382        // only use these two bits, otherwise we are in an unsupported target and should error.
383        if (o_rdonly | o_wronly | o_rdwr) & !0b11 != 0 {
384            throw_unsup_format!("access mode flags on this target are unsupported");
385        }
386        let mut writable = true;
387
388        // Now we check the access mode
389        let access_mode = flag & 0b11;
390        flag &= !access_mode;
391
392        if access_mode == o_rdonly {
393            writable = false;
394            options.read(true);
395        } else if access_mode == o_wronly {
396            options.write(true);
397        } else if access_mode == o_rdwr {
398            options.read(true).write(true);
399        } else {
400            throw_unsup_format!("unsupported access mode {:#x}", access_mode);
401        }
402
403        let o_append = this.eval_libc_i32("O_APPEND");
404        if flag & o_append == o_append {
405            flag &= !o_append;
406            options.append(true);
407        }
408        let o_trunc = this.eval_libc_i32("O_TRUNC");
409        if flag & o_trunc == o_trunc {
410            flag &= !o_trunc;
411            options.truncate(true);
412        }
413        let o_creat = this.eval_libc_i32("O_CREAT");
414        if flag & o_creat == o_creat {
415            flag &= !o_creat;
416            // Get the mode.  On macOS, the argument type `mode_t` is actually `u16`, but
417            // C integer promotion rules mean that on the ABI level, it gets passed as `u32`
418            // (see https://github.com/rust-lang/rust/issues/71915).
419            let [mode] = check_min_vararg_count("open(pathname, O_CREAT, ...)", varargs)?;
420            let mode = this.read_scalar(mode)?.to_u32()?;
421
422            #[cfg(unix)]
423            {
424                // Support all modes on UNIX host
425                use std::os::unix::fs::OpenOptionsExt;
426                options.mode(mode);
427            }
428            #[cfg(not(unix))]
429            {
430                // Only support default mode for non-UNIX (i.e. Windows) host
431                if mode != 0o666 {
432                    throw_unsup_format!(
433                        "non-default mode 0o{:o} is not supported on non-Unix hosts",
434                        mode
435                    );
436                }
437            }
438
439            let o_excl = this.eval_libc_i32("O_EXCL");
440            if flag & o_excl == o_excl {
441                flag &= !o_excl;
442                options.create_new(true);
443            } else {
444                options.create(true);
445            }
446        }
447        let o_cloexec = this.eval_libc_i32("O_CLOEXEC");
448        if flag & o_cloexec == o_cloexec {
449            flag &= !o_cloexec;
450            // We do not need to do anything for this flag because `std` already sets it.
451            // (Technically we do not support *not* setting this flag, but we ignore that.)
452        }
453        if this.tcx.sess.target.os == Os::Linux {
454            let o_tmpfile = this.eval_libc_i32("O_TMPFILE");
455            if flag & o_tmpfile == o_tmpfile {
456                // if the flag contains `O_TMPFILE` then we return a graceful error
457                return this.set_last_error_and_return_i32(LibcError("EOPNOTSUPP"));
458            }
459        }
460
461        let o_nofollow = this.eval_libc_i32("O_NOFOLLOW");
462        if flag & o_nofollow == o_nofollow {
463            flag &= !o_nofollow;
464            #[cfg(unix)]
465            {
466                use std::os::unix::fs::OpenOptionsExt;
467                options.custom_flags(libc::O_NOFOLLOW);
468            }
469            // Strictly speaking, this emulation is not equivalent to the O_NOFOLLOW flag behavior:
470            // the path could change between us checking it here and the later call to `open`.
471            // But it's good enough for Miri purposes.
472            #[cfg(not(unix))]
473            {
474                // O_NOFOLLOW only fails when the trailing component is a symlink;
475                // the entire rest of the path can still contain symlinks.
476                if path.is_symlink() {
477                    return this.set_last_error_and_return_i32(LibcError("ELOOP"));
478                }
479            }
480        }
481
482        // If `flag` has any bits left set, those are not supported.
483        if flag != 0 {
484            throw_unsup_format!("unsupported flags {:#x}", flag);
485        }
486
487        // Reject if isolation is enabled.
488        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
489            this.reject_in_isolation("`open`", reject_with)?;
490            return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);
491        }
492
493        let fd = options
494            .open(path)
495            .map(|file| this.machine.fds.insert_new(FileHandle { file, writable }));
496
497        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(fd)?))
498    }
499
500    fn lseek(
501        &mut self,
502        fd_num: i32,
503        offset: i128,
504        whence: i32,
505        dest: &MPlaceTy<'tcx>,
506    ) -> InterpResult<'tcx> {
507        let this = self.eval_context_mut();
508
509        // Isolation check is done via `FileDescription` trait.
510
511        let seek_from = if whence == this.eval_libc_i32("SEEK_SET") {
512            if offset < 0 {
513                // Negative offsets return `EINVAL`.
514                return this.set_last_error_and_return(LibcError("EINVAL"), dest);
515            } else {
516                SeekFrom::Start(u64::try_from(offset).unwrap())
517            }
518        } else if whence == this.eval_libc_i32("SEEK_CUR") {
519            SeekFrom::Current(i64::try_from(offset).unwrap())
520        } else if whence == this.eval_libc_i32("SEEK_END") {
521            SeekFrom::End(i64::try_from(offset).unwrap())
522        } else {
523            return this.set_last_error_and_return(LibcError("EINVAL"), dest);
524        };
525
526        let communicate = this.machine.communicate();
527
528        let Some(fd) = this.machine.fds.get(fd_num) else {
529            return this.set_last_error_and_return(LibcError("EBADF"), dest);
530        };
531        let result = fd.seek(communicate, seek_from)?.map(|offset| i64::try_from(offset).unwrap());
532        drop(fd);
533
534        let result = this.try_unwrap_io_result(result)?;
535        this.write_int(result, dest)?;
536        interp_ok(())
537    }
538
539    fn unlink(&mut self, path_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
540        let this = self.eval_context_mut();
541
542        let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
543
544        // Reject if isolation is enabled.
545        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
546            this.reject_in_isolation("`unlink`", reject_with)?;
547            return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);
548        }
549
550        let result = remove_file(path).map(|_| 0);
551        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
552    }
553
554    fn symlink(
555        &mut self,
556        target_op: &OpTy<'tcx>,
557        linkpath_op: &OpTy<'tcx>,
558    ) -> InterpResult<'tcx, Scalar> {
559        #[cfg(unix)]
560        fn create_link(src: &Path, dst: &Path) -> std::io::Result<()> {
561            std::os::unix::fs::symlink(src, dst)
562        }
563
564        #[cfg(windows)]
565        fn create_link(src: &Path, dst: &Path) -> std::io::Result<()> {
566            use std::os::windows::fs;
567            if src.is_dir() { fs::symlink_dir(src, dst) } else { fs::symlink_file(src, dst) }
568        }
569
570        let this = self.eval_context_mut();
571        let target = this.read_path_from_c_str(this.read_pointer(target_op)?)?;
572        let linkpath = this.read_path_from_c_str(this.read_pointer(linkpath_op)?)?;
573
574        // Reject if isolation is enabled.
575        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
576            this.reject_in_isolation("`symlink`", reject_with)?;
577            return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);
578        }
579
580        let result = create_link(&target, &linkpath).map(|_| 0);
581        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
582    }
583
584    fn stat(&mut self, path_op: &OpTy<'tcx>, buf_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
585        let this = self.eval_context_mut();
586
587        if !matches!(
588            &this.tcx.sess.target.os,
589            Os::MacOs | Os::FreeBsd | Os::Solaris | Os::Illumos | Os::Android | Os::Linux
590        ) {
591            panic!("`stat` should not be called on {}", this.tcx.sess.target.os);
592        }
593
594        let path_scalar = this.read_pointer(path_op)?;
595        let path = this.read_path_from_c_str(path_scalar)?.into_owned();
596
597        // Reject if isolation is enabled.
598        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
599            this.reject_in_isolation("`stat`", reject_with)?;
600            return this.set_last_error_and_return_i32(LibcError("EACCES"));
601        }
602
603        // `stat` always follows symlinks.
604        let metadata = match FileMetadata::from_path(this, &path, true)? {
605            Ok(metadata) => metadata,
606            Err(err) => return this.set_last_error_and_return_i32(err),
607        };
608
609        interp_ok(Scalar::from_i32(this.write_stat_buf(metadata, buf_op)?))
610    }
611
612    // `lstat` is used to get symlink metadata.
613    fn lstat(&mut self, path_op: &OpTy<'tcx>, buf_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
614        let this = self.eval_context_mut();
615
616        if !matches!(
617            &this.tcx.sess.target.os,
618            Os::MacOs | Os::FreeBsd | Os::Solaris | Os::Illumos | Os::Android | Os::Linux
619        ) {
620            panic!("`lstat` should not be called on {}", this.tcx.sess.target.os);
621        }
622
623        let path_scalar = this.read_pointer(path_op)?;
624        let path = this.read_path_from_c_str(path_scalar)?.into_owned();
625
626        // Reject if isolation is enabled.
627        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
628            this.reject_in_isolation("`lstat`", reject_with)?;
629            return this.set_last_error_and_return_i32(LibcError("EACCES"));
630        }
631
632        let metadata = match FileMetadata::from_path(this, &path, false)? {
633            Ok(metadata) => metadata,
634            Err(err) => return this.set_last_error_and_return_i32(err),
635        };
636
637        interp_ok(Scalar::from_i32(this.write_stat_buf(metadata, buf_op)?))
638    }
639
640    fn fstat(&mut self, fd_op: &OpTy<'tcx>, buf_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
641        let this = self.eval_context_mut();
642
643        if !matches!(
644            &this.tcx.sess.target.os,
645            Os::MacOs | Os::FreeBsd | Os::Solaris | Os::Illumos | Os::Linux | Os::Android
646        ) {
647            panic!("`fstat` should not be called on {}", this.tcx.sess.target.os);
648        }
649
650        let fd = this.read_scalar(fd_op)?.to_i32()?;
651
652        // Reject if isolation is enabled.
653        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
654            this.reject_in_isolation("`fstat`", reject_with)?;
655            // Set error code as "EBADF" (bad fd)
656            return this.set_last_error_and_return_i32(LibcError("EBADF"));
657        }
658
659        let metadata = match FileMetadata::from_fd_num(this, fd)? {
660            Ok(metadata) => metadata,
661            Err(err) => return this.set_last_error_and_return_i32(err),
662        };
663        interp_ok(Scalar::from_i32(this.write_stat_buf(metadata, buf_op)?))
664    }
665
666    fn linux_statx(
667        &mut self,
668        dirfd_op: &OpTy<'tcx>,    // Should be an `int`
669        pathname_op: &OpTy<'tcx>, // Should be a `const char *`
670        flags_op: &OpTy<'tcx>,    // Should be an `int`
671        mask_op: &OpTy<'tcx>,     // Should be an `unsigned int`
672        statxbuf_op: &OpTy<'tcx>, // Should be a `struct statx *`
673    ) -> InterpResult<'tcx, Scalar> {
674        let this = self.eval_context_mut();
675
676        this.assert_target_os(Os::Linux, "statx");
677
678        let dirfd = this.read_scalar(dirfd_op)?.to_i32()?;
679        let pathname_ptr = this.read_pointer(pathname_op)?;
680        let flags = this.read_scalar(flags_op)?.to_i32()?;
681        let _mask = this.read_scalar(mask_op)?.to_u32()?;
682        let statxbuf_ptr = this.read_pointer(statxbuf_op)?;
683
684        // If the statxbuf or pathname pointers are null, the function fails with `EFAULT`.
685        if this.ptr_is_null(statxbuf_ptr)? || this.ptr_is_null(pathname_ptr)? {
686            return this.set_last_error_and_return_i32(LibcError("EFAULT"));
687        }
688
689        let statxbuf = this.deref_pointer_as(statxbuf_op, this.libc_ty_layout("statx"))?;
690
691        let path = this.read_path_from_c_str(pathname_ptr)?.into_owned();
692        // See <https://github.com/rust-lang/rust/pull/79196> for a discussion of argument sizes.
693        let at_empty_path = this.eval_libc_i32("AT_EMPTY_PATH");
694        let empty_path_flag = flags & at_empty_path == at_empty_path;
695        // We only support:
696        // * interpreting `path` as an absolute directory,
697        // * interpreting `path` as a path relative to `dirfd` when the latter is `AT_FDCWD`, or
698        // * interpreting `dirfd` as any file descriptor when `path` is empty and AT_EMPTY_PATH is
699        // set.
700        // Other behaviors cannot be tested from `libstd` and thus are not implemented. If you
701        // found this error, please open an issue reporting it.
702        if !(path.is_absolute()
703            || dirfd == this.eval_libc_i32("AT_FDCWD")
704            || (path.as_os_str().is_empty() && empty_path_flag))
705        {
706            throw_unsup_format!(
707                "using statx is only supported with absolute paths, relative paths with the file \
708                descriptor `AT_FDCWD`, and empty paths with the `AT_EMPTY_PATH` flag set and any \
709                file descriptor"
710            )
711        }
712
713        // Reject if isolation is enabled.
714        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
715            this.reject_in_isolation("`statx`", reject_with)?;
716            let ecode = if path.is_absolute() || dirfd == this.eval_libc_i32("AT_FDCWD") {
717                // since `path` is provided, either absolute or
718                // relative to CWD, `EACCES` is the most relevant.
719                LibcError("EACCES")
720            } else {
721                // `dirfd` is set to target file, and `path` is empty
722                // (or we would have hit the `throw_unsup_format`
723                // above). `EACCES` would violate the spec.
724                assert!(empty_path_flag);
725                LibcError("EBADF")
726            };
727            return this.set_last_error_and_return_i32(ecode);
728        }
729
730        // If the `AT_SYMLINK_NOFOLLOW` flag is set, we query the file's metadata without following
731        // symbolic links.
732        let follow_symlink = flags & this.eval_libc_i32("AT_SYMLINK_NOFOLLOW") == 0;
733
734        // If the path is empty, and the AT_EMPTY_PATH flag is set, we query the open file
735        // represented by dirfd, whether it's a directory or otherwise.
736        let metadata = if path.as_os_str().is_empty() && empty_path_flag {
737            FileMetadata::from_fd_num(this, dirfd)?
738        } else {
739            FileMetadata::from_path(this, &path, follow_symlink)?
740        };
741        let metadata = match metadata {
742            Ok(metadata) => metadata,
743            Err(err) => return this.set_last_error_and_return_i32(err),
744        };
745
746        // The `_mask_op` parameter specifies the file information that the caller requested.
747        // However, `statx` is allowed to return information that was not requested or to not
748        // return information that was requested. This `mask` represents the information we can
749        // actually provide for any target.
750        let mut mask = this.eval_libc_u32("STATX_TYPE") | this.eval_libc_u32("STATX_SIZE");
751
752        // Check which pieces of metadata we acquired, and set the appropriate flags in the mask.
753        if metadata.ino.is_some() {
754            mask |= this.eval_libc_u32("STATX_INO");
755        }
756        if metadata.nlink.is_some() {
757            mask |= this.eval_libc_u32("STATX_NLINK");
758        }
759        if metadata.uid.is_some() {
760            mask |= this.eval_libc_u32("STATX_UID");
761        }
762        if metadata.gid.is_some() {
763            mask |= this.eval_libc_u32("STATX_GID");
764        }
765        if metadata.blocks.is_some() {
766            mask |= this.eval_libc_u32("STATX_BLOCKS");
767        }
768
769        // `statx.stx_mode` is `__u16`. `libc::S_IF*` are of type `mode_t`, which varies in
770        // width across targets (`u16` on macOS, `u32` on Linux). Read using `mode_t`'s size.
771        let mode_t_size = this.libc_ty_layout("mode_t").size;
772        let mode: u16 = metadata
773            .mode
774            .to_uint(mode_t_size)?
775            .try_into()
776            .unwrap_or_else(|_| bug!("libc contains bad value for constant"));
777
778        // We need to set the corresponding bits of `mask` if the access, creation and modification
779        // times were available. Otherwise we let them be zero.
780        let (access_sec, access_nsec) = metadata
781            .accessed
782            .map(|tup| {
783                mask |= this.eval_libc_u32("STATX_ATIME");
784                interp_ok(tup)
785            })
786            .unwrap_or_else(|| interp_ok((0, 0)))?;
787
788        let (created_sec, created_nsec) = metadata
789            .created
790            .map(|tup| {
791                mask |= this.eval_libc_u32("STATX_BTIME");
792                interp_ok(tup)
793            })
794            .unwrap_or_else(|| interp_ok((0, 0)))?;
795
796        let (modified_sec, modified_nsec) = metadata
797            .modified
798            .map(|tup| {
799                mask |= this.eval_libc_u32("STATX_MTIME");
800                interp_ok(tup)
801            })
802            .unwrap_or_else(|| interp_ok((0, 0)))?;
803
804        // Now we write everything to `statxbuf`. We write a zero for the unavailable fields.
805        this.write_int_fields_named(
806            &[
807                ("stx_mask", mask.into()),
808                ("stx_blksize", metadata.blksize.unwrap_or(0).into()),
809                ("stx_attributes", 0),
810                ("stx_nlink", metadata.nlink.unwrap_or(0).into()),
811                ("stx_uid", metadata.uid.unwrap_or(0).into()),
812                ("stx_gid", metadata.gid.unwrap_or(0).into()),
813                ("stx_mode", mode.into()),
814                ("stx_ino", metadata.ino.unwrap_or(0).into()),
815                ("stx_size", metadata.size.into()),
816                ("stx_blocks", metadata.blocks.unwrap_or(0).into()),
817                ("stx_attributes_mask", 0),
818                ("stx_rdev_major", 0),
819                ("stx_rdev_minor", 0),
820                ("stx_dev_major", 0),
821                ("stx_dev_minor", 0),
822            ],
823            &statxbuf,
824        )?;
825        #[rustfmt::skip]
826        this.write_int_fields_named(
827            &[
828                ("tv_sec", access_sec.into()),
829                ("tv_nsec", access_nsec.into()),
830            ],
831            &this.project_field_named(&statxbuf, "stx_atime")?,
832        )?;
833        #[rustfmt::skip]
834        this.write_int_fields_named(
835            &[
836                ("tv_sec", created_sec.into()),
837                ("tv_nsec", created_nsec.into()),
838            ],
839            &this.project_field_named(&statxbuf, "stx_btime")?,
840        )?;
841        #[rustfmt::skip]
842        this.write_int_fields_named(
843            &[
844                ("tv_sec", 0.into()),
845                ("tv_nsec", 0.into()),
846            ],
847            &this.project_field_named(&statxbuf, "stx_ctime")?,
848        )?;
849        #[rustfmt::skip]
850        this.write_int_fields_named(
851            &[
852                ("tv_sec", modified_sec.into()),
853                ("tv_nsec", modified_nsec.into()),
854            ],
855            &this.project_field_named(&statxbuf, "stx_mtime")?,
856        )?;
857
858        interp_ok(Scalar::from_i32(0))
859    }
860
861    fn rename(
862        &mut self,
863        oldpath_op: &OpTy<'tcx>,
864        newpath_op: &OpTy<'tcx>,
865    ) -> InterpResult<'tcx, Scalar> {
866        let this = self.eval_context_mut();
867
868        let oldpath_ptr = this.read_pointer(oldpath_op)?;
869        let newpath_ptr = this.read_pointer(newpath_op)?;
870
871        if this.ptr_is_null(oldpath_ptr)? || this.ptr_is_null(newpath_ptr)? {
872            return this.set_last_error_and_return_i32(LibcError("EFAULT"));
873        }
874
875        let oldpath = this.read_path_from_c_str(oldpath_ptr)?;
876        let newpath = this.read_path_from_c_str(newpath_ptr)?;
877
878        // Reject if isolation is enabled.
879        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
880            this.reject_in_isolation("`rename`", reject_with)?;
881            return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);
882        }
883
884        let result = rename(oldpath, newpath).map(|_| 0);
885
886        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
887    }
888
889    fn mkdir(&mut self, path_op: &OpTy<'tcx>, mode_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
890        let this = self.eval_context_mut();
891
892        #[cfg_attr(not(unix), allow(unused_variables))]
893        let mode = if matches!(&this.tcx.sess.target.os, Os::MacOs | Os::FreeBsd) {
894            u32::from(this.read_scalar(mode_op)?.to_u16()?)
895        } else {
896            this.read_scalar(mode_op)?.to_u32()?
897        };
898
899        let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
900
901        // Reject if isolation is enabled.
902        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
903            this.reject_in_isolation("`mkdir`", reject_with)?;
904            return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);
905        }
906
907        #[cfg_attr(not(unix), allow(unused_mut))]
908        let mut builder = DirBuilder::new();
909
910        // If the host supports it, forward on the mode of the directory
911        // (i.e. permission bits and the sticky bit)
912        #[cfg(unix)]
913        {
914            use std::os::unix::fs::DirBuilderExt;
915            builder.mode(mode);
916        }
917
918        let result = builder.create(path).map(|_| 0i32);
919
920        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
921    }
922
923    fn rmdir(&mut self, path_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
924        let this = self.eval_context_mut();
925
926        let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
927
928        // Reject if isolation is enabled.
929        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
930            this.reject_in_isolation("`rmdir`", reject_with)?;
931            return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);
932        }
933
934        let result = remove_dir(path).map(|_| 0i32);
935
936        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
937    }
938
939    fn opendir(&mut self, name_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
940        let this = self.eval_context_mut();
941
942        let name = this.read_path_from_c_str(this.read_pointer(name_op)?)?;
943
944        // Reject if isolation is enabled.
945        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
946            this.reject_in_isolation("`opendir`", reject_with)?;
947            this.set_last_error(LibcError("EACCES"))?;
948            return interp_ok(Scalar::null_ptr(this));
949        }
950
951        let result = read_dir(name);
952
953        match result {
954            Ok(dir_iter) => {
955                let id = this.machine.dirs.insert_new(dir_iter);
956
957                // The libc API for opendir says that this method returns a pointer to an opaque
958                // structure, but we are returning an ID number. Thus, pass it as a scalar of
959                // pointer width.
960                interp_ok(Scalar::from_target_usize(id, this))
961            }
962            Err(e) => {
963                this.set_last_error(e)?;
964                interp_ok(Scalar::null_ptr(this))
965            }
966        }
967    }
968
969    fn readdir(&mut self, dirp_op: &OpTy<'tcx>, dest: &MPlaceTy<'tcx>) -> InterpResult<'tcx> {
970        let this = self.eval_context_mut();
971
972        if !matches!(
973            &this.tcx.sess.target.os,
974            Os::Linux | Os::Android | Os::Solaris | Os::Illumos | Os::FreeBsd
975        ) {
976            panic!("`readdir` should not be called on {}", this.tcx.sess.target.os);
977        }
978
979        let dirp = this.read_target_usize(dirp_op)?;
980
981        // Reject if isolation is enabled.
982        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
983            this.reject_in_isolation("`readdir`", reject_with)?;
984            this.set_last_error(LibcError("EBADF"))?;
985            this.write_null(dest)?;
986            return interp_ok(());
987        }
988
989        let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| {
990            err_ub_format!("the DIR pointer passed to `readdir` did not come from opendir")
991        })?;
992
993        let entry = match open_dir.next_host_entry() {
994            Some(Ok(dir_entry)) => {
995                let dir_entry = this.dir_entry_fields(dir_entry)?;
996
997                // Write the directory entry into a newly allocated buffer.
998                // The name is written with write_bytes, while the rest of the
999                // dirent64 (or dirent) struct is written using write_int_fields.
1000
1001                // For reference:
1002                // On Linux:
1003                // pub struct dirent64 {
1004                //     pub d_ino: ino64_t,
1005                //     pub d_off: off64_t,
1006                //     pub d_reclen: c_ushort,
1007                //     pub d_type: c_uchar,
1008                //     pub d_name: [c_char; 256],
1009                // }
1010                //
1011                // On Solaris:
1012                // pub struct dirent {
1013                //     pub d_ino: ino64_t,
1014                //     pub d_off: off64_t,
1015                //     pub d_reclen: c_ushort,
1016                //     pub d_name: [c_char; 3],
1017                // }
1018                //
1019                // On FreeBSD:
1020                // pub struct dirent {
1021                //     pub d_fileno: uint32_t,
1022                //     pub d_reclen: uint16_t,
1023                //     pub d_type: uint8_t,
1024                //     pub d_namlen: uint8_t,
1025                //     pub d_name: [c_char; 256],
1026                // }
1027
1028                // We just use the pointee type here since determining the right pointee type
1029                // independently is highly non-trivial: it depends on which exact alias of the
1030                // function was invoked (e.g. `fstat` vs `fstat64`), and then on FreeBSD it also
1031                // depends on the ABI level which can be different between the libc used by std and
1032                // the libc used by everyone else.
1033                let dirent_ty = dest.layout.ty.builtin_deref(true).unwrap();
1034                let dirent_layout = this.layout_of(dirent_ty)?;
1035                let fields = &dirent_layout.fields;
1036                let d_name_offset = fields.offset(fields.count().strict_sub(1)).bytes();
1037
1038                // Determine the size of the buffer we have to allocate.
1039                let mut name = dir_entry.name; // not a Path as there are no separators!
1040                name.push("\0"); // Add a NUL terminator
1041                let name_bytes = name.as_encoded_bytes();
1042                let name_len = u64::try_from(name_bytes.len()).unwrap();
1043                let size = d_name_offset.strict_add(name_len);
1044
1045                let entry = this.allocate_ptr(
1046                    Size::from_bytes(size),
1047                    dirent_layout.align.abi,
1048                    MiriMemoryKind::Runtime.into(),
1049                    AllocInit::Uninit,
1050                )?;
1051                let entry = this.ptr_to_mplace(entry.into(), dirent_layout);
1052
1053                // Write the name.
1054                // The name is not a normal field, we already computed the offset above.
1055                let name_ptr = entry.ptr().wrapping_offset(Size::from_bytes(d_name_offset), this);
1056                this.write_bytes_ptr(name_ptr, name_bytes.iter().copied())?;
1057
1058                // Write common fields.
1059                let ino_name =
1060                    if this.tcx.sess.target.os == Os::FreeBsd { "d_fileno" } else { "d_ino" };
1061                this.write_int_fields_named(
1062                    &[(ino_name, dir_entry.ino.into()), ("d_reclen", size.into())],
1063                    &entry,
1064                )?;
1065
1066                // Write "optional" fields.
1067                if let Some(d_off) = this.try_project_field_named(&entry, "d_off")? {
1068                    this.write_null(&d_off)?;
1069                }
1070                if let Some(d_namlen) = this.try_project_field_named(&entry, "d_namlen")? {
1071                    this.write_int(name_len.strict_sub(1), &d_namlen)?;
1072                }
1073                if let Some(d_type) = this.try_project_field_named(&entry, "d_type")? {
1074                    this.write_int(dir_entry.d_type, &d_type)?;
1075                }
1076
1077                Some(entry.ptr())
1078            }
1079            None => {
1080                // end of stream: return NULL
1081                None
1082            }
1083            Some(Err(e)) => {
1084                this.set_last_error(e)?;
1085                None
1086            }
1087        };
1088
1089        let open_dir = this.machine.dirs.streams.get_mut(&dirp).unwrap();
1090        let old_entry = std::mem::replace(&mut open_dir.entry, entry);
1091        if let Some(old_entry) = old_entry {
1092            this.deallocate_ptr(old_entry, None, MiriMemoryKind::Runtime.into())?;
1093        }
1094
1095        this.write_pointer(entry.unwrap_or_else(Pointer::null), dest)?;
1096        interp_ok(())
1097    }
1098
1099    fn macos_readdir_r(
1100        &mut self,
1101        dirp_op: &OpTy<'tcx>,
1102        entry_op: &OpTy<'tcx>,
1103        result_op: &OpTy<'tcx>,
1104    ) -> InterpResult<'tcx, Scalar> {
1105        let this = self.eval_context_mut();
1106
1107        this.assert_target_os(Os::MacOs, "readdir_r");
1108
1109        let dirp = this.read_target_usize(dirp_op)?;
1110        let result_place = this.deref_pointer_as(result_op, this.machine.layouts.mut_raw_ptr)?;
1111
1112        // Reject if isolation is enabled.
1113        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1114            this.reject_in_isolation("`readdir_r`", reject_with)?;
1115            // Return error code, do *not* set `errno`.
1116            return interp_ok(this.eval_libc("EBADF"));
1117        }
1118
1119        let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| {
1120            err_unsup_format!("the DIR pointer passed to readdir_r did not come from opendir")
1121        })?;
1122        interp_ok(match open_dir.next_host_entry() {
1123            Some(Ok(dir_entry)) => {
1124                let dir_entry = this.dir_entry_fields(dir_entry)?;
1125                // Write into entry, write pointer to result, return 0 on success.
1126                // The name is written with write_os_str_to_c_str, while the rest of the
1127                // dirent struct is written using write_int_fields.
1128
1129                // For reference, on macOS this looks like:
1130                // pub struct dirent {
1131                //     pub d_ino: u64,
1132                //     pub d_seekoff: u64,
1133                //     pub d_reclen: u16,
1134                //     pub d_namlen: u16,
1135                //     pub d_type: u8,
1136                //     pub d_name: [c_char; 1024],
1137                // }
1138
1139                let entry_place = this.deref_pointer_as(entry_op, this.libc_ty_layout("dirent"))?;
1140
1141                // Write the name.
1142                let name_place = this.project_field_named(&entry_place, "d_name")?;
1143                let (name_fits, file_name_buf_len) = this.write_os_str_to_c_str(
1144                    &dir_entry.name,
1145                    name_place.ptr(),
1146                    name_place.layout.size.bytes(),
1147                )?;
1148                if !name_fits {
1149                    throw_unsup_format!(
1150                        "a directory entry had a name too large to fit in libc::dirent"
1151                    );
1152                }
1153
1154                // Write the other fields.
1155                this.write_int_fields_named(
1156                    &[
1157                        ("d_reclen", entry_place.layout.size.bytes().into()),
1158                        ("d_namlen", file_name_buf_len.strict_sub(1).into()),
1159                        ("d_type", dir_entry.d_type.into()),
1160                        ("d_ino", dir_entry.ino.into()),
1161                        ("d_seekoff", 0),
1162                    ],
1163                    &entry_place,
1164                )?;
1165                this.write_scalar(this.read_scalar(entry_op)?, &result_place)?;
1166
1167                Scalar::from_i32(0)
1168            }
1169            None => {
1170                // end of stream: return 0, assign *result=NULL
1171                this.write_null(&result_place)?;
1172                Scalar::from_i32(0)
1173            }
1174            Some(Err(e)) => {
1175                // return positive error number on error (do *not* set last error)
1176                this.io_error_to_errnum(e)?
1177            }
1178        })
1179    }
1180
1181    fn closedir(&mut self, dirp_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1182        let this = self.eval_context_mut();
1183
1184        let dirp = this.read_target_usize(dirp_op)?;
1185
1186        // Reject if isolation is enabled.
1187        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1188            this.reject_in_isolation("`closedir`", reject_with)?;
1189            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1190        }
1191
1192        let Some(mut open_dir) = this.machine.dirs.streams.remove(&dirp) else {
1193            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1194        };
1195        if let Some(entry) = open_dir.entry.take() {
1196            this.deallocate_ptr(entry, None, MiriMemoryKind::Runtime.into())?;
1197        }
1198        // We drop the `open_dir`, which will close the host dir handle.
1199        drop(open_dir);
1200
1201        interp_ok(Scalar::from_i32(0))
1202    }
1203
1204    fn ftruncate64(&mut self, fd_num: i32, length: i128) -> InterpResult<'tcx, Scalar> {
1205        let this = self.eval_context_mut();
1206
1207        // Reject if isolation is enabled.
1208        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1209            this.reject_in_isolation("`ftruncate64`", reject_with)?;
1210            // Set error code as "EBADF" (bad fd)
1211            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1212        }
1213
1214        let Some(fd) = this.machine.fds.get(fd_num) else {
1215            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1216        };
1217
1218        let Some(file) = fd.downcast::<FileHandle>() else {
1219            // The docs say that EINVAL is returned when the FD "does not reference a regular file
1220            // or a POSIX shared memory object" (and we don't support shmem objects).
1221            return interp_ok(this.eval_libc("EINVAL"));
1222        };
1223
1224        if file.writable {
1225            if let Ok(length) = length.try_into() {
1226                let result = file.file.set_len(length);
1227                let result = this.try_unwrap_io_result(result.map(|_| 0i32))?;
1228                interp_ok(Scalar::from_i32(result))
1229            } else {
1230                this.set_last_error_and_return_i32(LibcError("EINVAL"))
1231            }
1232        } else {
1233            // The file is not writable
1234            this.set_last_error_and_return_i32(LibcError("EINVAL"))
1235        }
1236    }
1237
1238    /// NOTE: According to the man page of `possix_fallocate`, it returns the error code instead
1239    /// of setting `errno`.
1240    fn posix_fallocate(
1241        &mut self,
1242        fd_num: i32,
1243        offset: i64,
1244        len: i64,
1245    ) -> InterpResult<'tcx, Scalar> {
1246        let this = self.eval_context_mut();
1247
1248        // Reject if isolation is enabled.
1249        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1250            this.reject_in_isolation("`posix_fallocate`", reject_with)?;
1251            // Return error code "EBADF" (bad fd).
1252            return interp_ok(this.eval_libc("EBADF"));
1253        }
1254
1255        // EINVAL is returned when: "offset was less than 0, or len was less than or equal to 0".
1256        if offset < 0 || len <= 0 {
1257            return interp_ok(this.eval_libc("EINVAL"));
1258        }
1259
1260        // Get the file handle.
1261        let Some(fd) = this.machine.fds.get(fd_num) else {
1262            return interp_ok(this.eval_libc("EBADF"));
1263        };
1264        let Some(file) = fd.downcast::<FileHandle>() else {
1265            // Man page specifies to return ENODEV if `fd` is not a regular file.
1266            return interp_ok(this.eval_libc("ENODEV"));
1267        };
1268
1269        if !file.writable {
1270            // The file is not writable.
1271            return interp_ok(this.eval_libc("EBADF"));
1272        }
1273
1274        let current_size = match file.file.metadata() {
1275            Ok(metadata) => metadata.len(),
1276            Err(err) => return this.io_error_to_errnum(err),
1277        };
1278        // Checked i64 addition, to ensure the result does not exceed the max file size.
1279        let new_size = match offset.checked_add(len) {
1280            // `new_size` is definitely non-negative, so we can cast to `u64`.
1281            Some(new_size) => u64::try_from(new_size).unwrap(),
1282            None => return interp_ok(this.eval_libc("EFBIG")), // new size too big
1283        };
1284        // If the size of the file is less than offset+size, then the file is increased to this size;
1285        // otherwise the file size is left unchanged.
1286        if current_size < new_size {
1287            interp_ok(match file.file.set_len(new_size) {
1288                Ok(()) => Scalar::from_i32(0),
1289                Err(e) => this.io_error_to_errnum(e)?,
1290            })
1291        } else {
1292            interp_ok(Scalar::from_i32(0))
1293        }
1294    }
1295
1296    fn fsync(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1297        // On macOS, `fsync` (unlike `fcntl(F_FULLFSYNC)`) does not wait for the
1298        // underlying disk to finish writing. In the interest of host compatibility,
1299        // we conservatively implement this with `sync_all`, which
1300        // *does* wait for the disk.
1301
1302        let this = self.eval_context_mut();
1303
1304        let fd = this.read_scalar(fd_op)?.to_i32()?;
1305
1306        // Reject if isolation is enabled.
1307        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1308            this.reject_in_isolation("`fsync`", reject_with)?;
1309            // Set error code as "EBADF" (bad fd)
1310            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1311        }
1312
1313        self.ffullsync_fd(fd)
1314    }
1315
1316    fn ffullsync_fd(&mut self, fd_num: i32) -> InterpResult<'tcx, Scalar> {
1317        let this = self.eval_context_mut();
1318        let Some(fd) = this.machine.fds.get(fd_num) else {
1319            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1320        };
1321        // Only regular files support synchronization.
1322        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1323            err_unsup_format!("`fsync` is only supported on file-backed file descriptors")
1324        })?;
1325        let io_result = maybe_sync_file(&file.file, file.writable, File::sync_all);
1326        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1327    }
1328
1329    fn fdatasync(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1330        let this = self.eval_context_mut();
1331
1332        let fd = this.read_scalar(fd_op)?.to_i32()?;
1333
1334        // Reject if isolation is enabled.
1335        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1336            this.reject_in_isolation("`fdatasync`", reject_with)?;
1337            // Set error code as "EBADF" (bad fd)
1338            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1339        }
1340
1341        let Some(fd) = this.machine.fds.get(fd) else {
1342            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1343        };
1344        // Only regular files support synchronization.
1345        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1346            err_unsup_format!("`fdatasync` is only supported on file-backed file descriptors")
1347        })?;
1348        let io_result = maybe_sync_file(&file.file, file.writable, File::sync_data);
1349        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1350    }
1351
1352    fn sync_file_range(
1353        &mut self,
1354        fd_op: &OpTy<'tcx>,
1355        offset_op: &OpTy<'tcx>,
1356        nbytes_op: &OpTy<'tcx>,
1357        flags_op: &OpTy<'tcx>,
1358    ) -> InterpResult<'tcx, Scalar> {
1359        let this = self.eval_context_mut();
1360
1361        let fd = this.read_scalar(fd_op)?.to_i32()?;
1362        let offset = this.read_scalar(offset_op)?.to_i64()?;
1363        let nbytes = this.read_scalar(nbytes_op)?.to_i64()?;
1364        let flags = this.read_scalar(flags_op)?.to_i32()?;
1365
1366        if offset < 0 || nbytes < 0 {
1367            return this.set_last_error_and_return_i32(LibcError("EINVAL"));
1368        }
1369        let allowed_flags = this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_BEFORE")
1370            | this.eval_libc_i32("SYNC_FILE_RANGE_WRITE")
1371            | this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_AFTER");
1372        if flags & allowed_flags != flags {
1373            return this.set_last_error_and_return_i32(LibcError("EINVAL"));
1374        }
1375
1376        // Reject if isolation is enabled.
1377        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1378            this.reject_in_isolation("`sync_file_range`", reject_with)?;
1379            // Set error code as "EBADF" (bad fd)
1380            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1381        }
1382
1383        let Some(fd) = this.machine.fds.get(fd) else {
1384            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1385        };
1386        // Only regular files support synchronization.
1387        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1388            err_unsup_format!("`sync_data_range` is only supported on file-backed file descriptors")
1389        })?;
1390        let io_result = maybe_sync_file(&file.file, file.writable, File::sync_data);
1391        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1392    }
1393
1394    fn readlink(
1395        &mut self,
1396        pathname_op: &OpTy<'tcx>,
1397        buf_op: &OpTy<'tcx>,
1398        bufsize_op: &OpTy<'tcx>,
1399    ) -> InterpResult<'tcx, i64> {
1400        let this = self.eval_context_mut();
1401
1402        let pathname = this.read_path_from_c_str(this.read_pointer(pathname_op)?)?;
1403        let buf = this.read_pointer(buf_op)?;
1404        let bufsize = this.read_target_usize(bufsize_op)?;
1405
1406        // Reject if isolation is enabled.
1407        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1408            this.reject_in_isolation("`readlink`", reject_with)?;
1409            this.set_last_error(LibcError("EACCES"))?;
1410            return interp_ok(-1);
1411        }
1412
1413        let result = std::fs::read_link(pathname);
1414        match result {
1415            Ok(resolved) => {
1416                // 'readlink' truncates the resolved path if the provided buffer is not large
1417                // enough, and does *not* add a null terminator. That means we cannot use the usual
1418                // `write_path_to_c_str` and have to re-implement parts of it ourselves.
1419                let resolved = this.convert_path(
1420                    Cow::Borrowed(resolved.as_ref()),
1421                    crate::shims::os_str::PathConversion::HostToTarget,
1422                );
1423                let mut path_bytes = resolved.as_encoded_bytes();
1424                let bufsize: usize = bufsize.try_into().unwrap();
1425                if path_bytes.len() > bufsize {
1426                    path_bytes = &path_bytes[..bufsize]
1427                }
1428                this.write_bytes_ptr(buf, path_bytes.iter().copied())?;
1429                interp_ok(path_bytes.len().try_into().unwrap())
1430            }
1431            Err(e) => {
1432                this.set_last_error(e)?;
1433                interp_ok(-1)
1434            }
1435        }
1436    }
1437
1438    fn isatty(&mut self, miri_fd: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1439        let this = self.eval_context_mut();
1440        // "returns 1 if fd is an open file descriptor referring to a terminal;
1441        // otherwise 0 is returned, and errno is set to indicate the error"
1442        let fd = this.read_scalar(miri_fd)?.to_i32()?;
1443        let error = if let Some(fd) = this.machine.fds.get(fd) {
1444            if fd.is_tty(this.machine.communicate()) {
1445                return interp_ok(Scalar::from_i32(1));
1446            } else {
1447                LibcError("ENOTTY")
1448            }
1449        } else {
1450            // FD does not exist
1451            LibcError("EBADF")
1452        };
1453        this.set_last_error(error)?;
1454        interp_ok(Scalar::from_i32(0))
1455    }
1456
1457    fn realpath(
1458        &mut self,
1459        path_op: &OpTy<'tcx>,
1460        processed_path_op: &OpTy<'tcx>,
1461    ) -> InterpResult<'tcx, Scalar> {
1462        let this = self.eval_context_mut();
1463        this.assert_target_os_is_unix("realpath");
1464
1465        let pathname = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
1466        let processed_ptr = this.read_pointer(processed_path_op)?;
1467
1468        // Reject if isolation is enabled.
1469        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1470            this.reject_in_isolation("`realpath`", reject_with)?;
1471            this.set_last_error(LibcError("EACCES"))?;
1472            return interp_ok(Scalar::from_target_usize(0, this));
1473        }
1474
1475        let result = std::fs::canonicalize(pathname);
1476        match result {
1477            Ok(resolved) => {
1478                let path_max = this
1479                    .eval_libc_i32("PATH_MAX")
1480                    .try_into()
1481                    .expect("PATH_MAX does not fit in u64");
1482                let dest = if this.ptr_is_null(processed_ptr)? {
1483                    // POSIX says behavior when passing a null pointer is implementation-defined,
1484                    // but GNU/linux, freebsd, netbsd, bionic/android, and macos all treat a null pointer
1485                    // similarly to:
1486                    //
1487                    // "If resolved_path is specified as NULL, then realpath() uses
1488                    // malloc(3) to allocate a buffer of up to PATH_MAX bytes to hold
1489                    // the resolved pathname, and returns a pointer to this buffer.  The
1490                    // caller should deallocate this buffer using free(3)."
1491                    // <https://man7.org/linux/man-pages/man3/realpath.3.html>
1492                    this.alloc_path_as_c_str(&resolved, MiriMemoryKind::C.into())?
1493                } else {
1494                    let (wrote_path, _) =
1495                        this.write_path_to_c_str(&resolved, processed_ptr, path_max)?;
1496
1497                    if !wrote_path {
1498                        // Note that we do not explicitly handle `FILENAME_MAX`
1499                        // (different from `PATH_MAX` above) as it is Linux-specific and
1500                        // seems like a bit of a mess anyway: <https://eklitzke.org/path-max-is-tricky>.
1501                        this.set_last_error(LibcError("ENAMETOOLONG"))?;
1502                        return interp_ok(Scalar::from_target_usize(0, this));
1503                    }
1504                    processed_ptr
1505                };
1506
1507                interp_ok(Scalar::from_maybe_pointer(dest, this))
1508            }
1509            Err(e) => {
1510                this.set_last_error(e)?;
1511                interp_ok(Scalar::from_target_usize(0, this))
1512            }
1513        }
1514    }
1515    fn mkstemp(&mut self, template_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1516        use rand::seq::IndexedRandom;
1517
1518        // POSIX defines the template string.
1519        const TEMPFILE_TEMPLATE_STR: &str = "XXXXXX";
1520
1521        let this = self.eval_context_mut();
1522        this.assert_target_os_is_unix("mkstemp");
1523
1524        // POSIX defines the maximum number of attempts before failure.
1525        //
1526        // `mkstemp()` relies on `tmpnam()` which in turn relies on `TMP_MAX`.
1527        // POSIX says this about `TMP_MAX`:
1528        // * Minimum number of unique filenames generated by `tmpnam()`.
1529        // * Maximum number of times an application can call `tmpnam()` reliably.
1530        //   * The value of `TMP_MAX` is at least 25.
1531        //   * On XSI-conformant systems, the value of `TMP_MAX` is at least 10000.
1532        // See <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/stdio.h.html>.
1533        let max_attempts = this.eval_libc_u32("TMP_MAX");
1534
1535        // Get the raw bytes from the template -- as a byte slice, this is a string in the target
1536        // (and the target is unix, so a byte slice is the right representation).
1537        let template_ptr = this.read_pointer(template_op)?;
1538        let mut template = this.eval_context_ref().read_c_str(template_ptr)?.to_owned();
1539        let template_bytes = template.as_mut_slice();
1540
1541        // Reject if isolation is enabled.
1542        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1543            this.reject_in_isolation("`mkstemp`", reject_with)?;
1544            return this.set_last_error_and_return_i32(LibcError("EACCES"));
1545        }
1546
1547        // Get the bytes of the suffix we expect in _target_ encoding.
1548        let suffix_bytes = TEMPFILE_TEMPLATE_STR.as_bytes();
1549
1550        // At this point we have one `&[u8]` that represents the template and one `&[u8]`
1551        // that represents the expected suffix.
1552
1553        // Now we figure out the index of the slice we expect to contain the suffix.
1554        let start_pos = template_bytes.len().saturating_sub(suffix_bytes.len());
1555        let end_pos = template_bytes.len();
1556        let last_six_char_bytes = &template_bytes[start_pos..end_pos];
1557
1558        // If we don't find the suffix, it is an error.
1559        if last_six_char_bytes != suffix_bytes {
1560            return this.set_last_error_and_return_i32(LibcError("EINVAL"));
1561        }
1562
1563        // At this point we know we have 6 ASCII 'X' characters as a suffix.
1564
1565        // From <https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/sysdeps/posix/tempname.c#L175>
1566        const SUBSTITUTIONS: &[char; 62] = &[
1567            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
1568            'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
1569            'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
1570            'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
1571        ];
1572
1573        // The file is opened with specific options, which Rust does not expose in a portable way.
1574        // So we use specific APIs depending on the host OS.
1575        let mut fopts = OpenOptions::new();
1576        fopts.read(true).write(true).create_new(true);
1577
1578        #[cfg(unix)]
1579        {
1580            use std::os::unix::fs::OpenOptionsExt;
1581            // Do not allow others to read or modify this file.
1582            fopts.mode(0o600);
1583            fopts.custom_flags(libc::O_EXCL);
1584        }
1585        #[cfg(windows)]
1586        {
1587            use std::os::windows::fs::OpenOptionsExt;
1588            // Do not allow others to read or modify this file.
1589            fopts.share_mode(0);
1590        }
1591
1592        // If the generated file already exists, we will try again `max_attempts` many times.
1593        for _ in 0..max_attempts {
1594            let rng = this.machine.rng.get_mut();
1595
1596            // Generate a random unique suffix.
1597            let unique_suffix = SUBSTITUTIONS.choose_multiple(rng, 6).collect::<String>();
1598
1599            // Replace the template string with the random string.
1600            template_bytes[start_pos..end_pos].copy_from_slice(unique_suffix.as_bytes());
1601
1602            // Write the modified template back to the passed in pointer to maintain POSIX semantics.
1603            this.write_bytes_ptr(template_ptr, template_bytes.iter().copied())?;
1604
1605            // To actually open the file, turn this into a host OsString.
1606            let p = bytes_to_os_str(template_bytes)?.to_os_string();
1607
1608            let possibly_unique = std::env::temp_dir().join::<PathBuf>(p.into());
1609
1610            let file = fopts.open(possibly_unique);
1611
1612            match file {
1613                Ok(f) => {
1614                    let fd = this.machine.fds.insert_new(FileHandle { file: f, writable: true });
1615                    return interp_ok(Scalar::from_i32(fd));
1616                }
1617                Err(e) =>
1618                    match e.kind() {
1619                        // If the random file already exists, keep trying.
1620                        ErrorKind::AlreadyExists => continue,
1621                        // Any other errors are returned to the caller.
1622                        _ => {
1623                            // "On error, -1 is returned, and errno is set to
1624                            // indicate the error"
1625                            return this.set_last_error_and_return_i32(e);
1626                        }
1627                    },
1628            }
1629        }
1630
1631        // We ran out of attempts to create the file, return an error.
1632        this.set_last_error_and_return_i32(LibcError("EEXIST"))
1633    }
1634}
1635
1636/// Extracts the number of seconds and nanoseconds elapsed between `time` and the unix epoch when
1637/// `time` is Ok. Returns `None` if `time` is an error. Fails if `time` happens before the unix
1638/// epoch.
1639fn extract_sec_and_nsec<'tcx>(
1640    time: std::io::Result<SystemTime>,
1641) -> InterpResult<'tcx, Option<(u64, u32)>> {
1642    match time.ok() {
1643        Some(time) => {
1644            let duration = system_time_to_duration(&time)?;
1645            interp_ok(Some((duration.as_secs(), duration.subsec_nanos())))
1646        }
1647        None => interp_ok(None),
1648    }
1649}
1650
1651fn file_type_to_mode_name(file_type: std::fs::FileType) -> &'static str {
1652    #[cfg(unix)]
1653    use std::os::unix::fs::FileTypeExt;
1654
1655    if file_type.is_file() {
1656        "S_IFREG"
1657    } else if file_type.is_dir() {
1658        "S_IFDIR"
1659    } else if file_type.is_symlink() {
1660        "S_IFLNK"
1661    } else {
1662        // Certain file types are only available when the host is a Unix system.
1663        #[cfg(unix)]
1664        {
1665            if file_type.is_socket() {
1666                return "S_IFSOCK";
1667            } else if file_type.is_fifo() {
1668                return "S_IFIFO";
1669            } else if file_type.is_char_device() {
1670                return "S_IFCHR";
1671            } else if file_type.is_block_device() {
1672                return "S_IFBLK";
1673            }
1674        }
1675        "S_IFREG"
1676    }
1677}
1678
1679/// Stores a file's metadata in order to avoid code duplication in the different metadata related
1680/// shims.
1681///
1682/// Some fields are host/platform-specific. `None` means that Miri does not have a real value for
1683/// this field, for example because the metadata is synthetic or because the host platform does not
1684/// expose it. `statx` must only advertise the corresponding `STATX_*` bit when the field is `Some`;
1685/// legacy `stat` writes zero for `None` to preserve the old fallback behavior.
1686struct FileMetadata {
1687    mode: Scalar,
1688    size: u64,
1689    created: Option<(u64, u32)>,
1690    accessed: Option<(u64, u32)>,
1691    modified: Option<(u64, u32)>,
1692    dev: Option<u64>,
1693    ino: Option<u64>,
1694    nlink: Option<u64>,
1695    uid: Option<u32>,
1696    gid: Option<u32>,
1697    blksize: Option<u64>,
1698    blocks: Option<u64>,
1699}
1700
1701impl FileMetadata {
1702    fn from_path<'tcx>(
1703        ecx: &mut MiriInterpCx<'tcx>,
1704        path: &Path,
1705        follow_symlink: bool,
1706    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1707        let metadata =
1708            if follow_symlink { std::fs::metadata(path) } else { std::fs::symlink_metadata(path) };
1709
1710        FileMetadata::from_meta(ecx, metadata)
1711    }
1712
1713    fn from_fd_num<'tcx>(
1714        ecx: &mut MiriInterpCx<'tcx>,
1715        fd_num: i32,
1716    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1717        let Some(fd) = ecx.machine.fds.get(fd_num) else {
1718            return interp_ok(Err(LibcError("EBADF")));
1719        };
1720        match fd.metadata()? {
1721            Either::Left(host) => Self::from_meta(ecx, host),
1722            Either::Right(name) => Self::synthetic(ecx, name),
1723        }
1724    }
1725
1726    fn synthetic<'tcx>(
1727        ecx: &mut MiriInterpCx<'tcx>,
1728        mode_name: &str,
1729    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1730        let mode = ecx.eval_libc(mode_name);
1731        interp_ok(Ok(FileMetadata {
1732            mode,
1733            size: 0,
1734            created: None,
1735            accessed: None,
1736            modified: None,
1737            dev: None,
1738            uid: None,
1739            gid: None,
1740            blksize: None,
1741            blocks: None,
1742            ino: None,
1743            nlink: None,
1744        }))
1745    }
1746
1747    fn from_meta<'tcx>(
1748        ecx: &mut MiriInterpCx<'tcx>,
1749        metadata: Result<std::fs::Metadata, std::io::Error>,
1750    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1751        let metadata = match metadata {
1752            Ok(metadata) => metadata,
1753            Err(e) => {
1754                return interp_ok(Err(e.into()));
1755            }
1756        };
1757
1758        let file_type = metadata.file_type();
1759        let mode = ecx.eval_libc(file_type_to_mode_name(file_type));
1760
1761        let size = metadata.len();
1762
1763        let created = extract_sec_and_nsec(metadata.created())?;
1764        let accessed = extract_sec_and_nsec(metadata.accessed())?;
1765        let modified = extract_sec_and_nsec(metadata.modified())?;
1766
1767        // FIXME: Provide more fields using platform specific methods.
1768
1769        cfg_select! {
1770            unix => {
1771                use std::os::unix::fs::MetadataExt;
1772                let dev = metadata.dev();
1773                let ino = metadata.ino();
1774                let nlink = metadata.nlink();
1775                let uid = metadata.uid();
1776                let gid = metadata.gid();
1777                let blksize = metadata.blksize();
1778                let blocks = metadata.blocks();
1779
1780                interp_ok(Ok(FileMetadata {
1781                    mode,
1782                    size,
1783                    created,
1784                    accessed,
1785                    modified,
1786                    dev: Some(dev),
1787                    ino: Some(ino),
1788                    nlink: Some(nlink),
1789                    uid: Some(uid),
1790                    gid: Some(gid),
1791                    blksize: Some(blksize),
1792                    blocks: Some(blocks),
1793                }))
1794            }
1795            _ => interp_ok(Ok(FileMetadata {
1796                mode,
1797                size,
1798                created,
1799                accessed,
1800                modified,
1801                dev: None,
1802                ino: None,
1803                nlink: None,
1804                uid: None,
1805                gid: None,
1806                blksize: None,
1807                blocks: None,
1808            })),
1809        }
1810    }
1811}