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