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};
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")
754            | this.eval_libc_u32("STATX_MODE")
755            | this.eval_libc_u32("STATX_SIZE");
756
757        // Check which pieces of metadata we acquired, and set the appropriate flags in the mask.
758        if metadata.ino.is_some() {
759            mask |= this.eval_libc_u32("STATX_INO");
760        }
761        if metadata.nlink.is_some() {
762            mask |= this.eval_libc_u32("STATX_NLINK");
763        }
764        if metadata.uid.is_some() {
765            mask |= this.eval_libc_u32("STATX_UID");
766        }
767        if metadata.gid.is_some() {
768            mask |= this.eval_libc_u32("STATX_GID");
769        }
770        if metadata.blocks.is_some() {
771            mask |= this.eval_libc_u32("STATX_BLOCKS");
772        }
773
774        // We need to set the corresponding bits of `mask` if the access, creation and modification
775        // times were available. Otherwise we let them be zero.
776        let (access_sec, access_nsec) = metadata
777            .accessed
778            .map(|tup| {
779                mask |= this.eval_libc_u32("STATX_ATIME");
780                interp_ok(tup)
781            })
782            .unwrap_or_else(|| interp_ok((0, 0)))?;
783
784        let (created_sec, created_nsec) = metadata
785            .created
786            .map(|tup| {
787                mask |= this.eval_libc_u32("STATX_BTIME");
788                interp_ok(tup)
789            })
790            .unwrap_or_else(|| interp_ok((0, 0)))?;
791
792        let (modified_sec, modified_nsec) = metadata
793            .modified
794            .map(|tup| {
795                mask |= this.eval_libc_u32("STATX_MTIME");
796                interp_ok(tup)
797            })
798            .unwrap_or_else(|| interp_ok((0, 0)))?;
799
800        // Now we write everything to `statxbuf`. We write a zero for the unavailable fields.
801        this.write_int_fields_named(
802            &[
803                ("stx_mask", mask.into()),
804                ("stx_mode", metadata.mode.into()),
805                ("stx_blksize", metadata.blksize.unwrap_or(0).into()),
806                ("stx_attributes", 0),
807                ("stx_nlink", metadata.nlink.unwrap_or(0).into()),
808                ("stx_uid", metadata.uid.unwrap_or(0).into()),
809                ("stx_gid", metadata.gid.unwrap_or(0).into()),
810                ("stx_ino", metadata.ino.unwrap_or(0).into()),
811                ("stx_size", metadata.size.into()),
812                ("stx_blocks", metadata.blocks.unwrap_or(0).into()),
813                ("stx_attributes_mask", 0),
814                ("stx_rdev_major", 0),
815                ("stx_rdev_minor", 0),
816                ("stx_dev_major", 0),
817                ("stx_dev_minor", 0),
818            ],
819            &statxbuf,
820        )?;
821        #[rustfmt::skip]
822        this.write_int_fields_named(
823            &[
824                ("tv_sec", access_sec.into()),
825                ("tv_nsec", access_nsec.into()),
826            ],
827            &this.project_field_named(&statxbuf, "stx_atime")?,
828        )?;
829        #[rustfmt::skip]
830        this.write_int_fields_named(
831            &[
832                ("tv_sec", created_sec.into()),
833                ("tv_nsec", created_nsec.into()),
834            ],
835            &this.project_field_named(&statxbuf, "stx_btime")?,
836        )?;
837        #[rustfmt::skip]
838        this.write_int_fields_named(
839            &[
840                ("tv_sec", 0.into()),
841                ("tv_nsec", 0.into()),
842            ],
843            &this.project_field_named(&statxbuf, "stx_ctime")?,
844        )?;
845        #[rustfmt::skip]
846        this.write_int_fields_named(
847            &[
848                ("tv_sec", modified_sec.into()),
849                ("tv_nsec", modified_nsec.into()),
850            ],
851            &this.project_field_named(&statxbuf, "stx_mtime")?,
852        )?;
853
854        interp_ok(Scalar::from_i32(0))
855    }
856
857    fn chmod(&mut self, path_op: &OpTy<'tcx>, mode_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
858        let this = self.eval_context_mut();
859
860        let path_ptr = this.read_pointer(path_op)?;
861        let mode = this.read_scalar(mode_op)?.to_uint(this.libc_ty_layout("mode_t").size)?;
862
863        if this.ptr_is_null(path_ptr)? {
864            return this.set_last_error_and_return_i32(LibcError("EFAULT"));
865        }
866        let path = this.read_path_from_c_str(path_ptr)?;
867
868        // Reject if isolation is enabled.
869        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
870            this.reject_in_isolation("`chmod`", reject_with)?;
871            return this.set_last_error_and_return_i32(LibcError("EACCES"));
872        }
873
874        let permissions = this.host_permissions_from_mode(mode.try_into().unwrap())?;
875        if let Err(err) = fs::set_permissions(path, permissions) {
876            return this.set_last_error_and_return_i32(err);
877        }
878
879        interp_ok(Scalar::from_i32(0))
880    }
881
882    fn fchmod(&mut self, fd_op: &OpTy<'tcx>, mode_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
883        let this = self.eval_context_mut();
884
885        let fd_num = this.read_scalar(fd_op)?.to_i32()?;
886        let mode = this.read_scalar(mode_op)?.to_uint(this.libc_ty_layout("mode_t").size)?;
887
888        let Some(fd) = this.machine.fds.get(fd_num) else {
889            return this.set_last_error_and_return_i32(LibcError("EBADF"));
890        };
891        let Some(file) = fd.downcast::<FileHandle>() else {
892            // The docs don't talk about what happens for non-regular files...
893            throw_unsup_format!("`fchmod` is only supported on regular files")
894        };
895
896        // Reject if isolation is enabled.
897        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
898            this.reject_in_isolation("`fchmod`", reject_with)?;
899            return this.set_last_error_and_return_i32(LibcError("EACCES"));
900        }
901
902        let permissions = this.host_permissions_from_mode(mode.try_into().unwrap())?;
903        if let Err(err) = file.file.set_permissions(permissions) {
904            return this.set_last_error_and_return_i32(err);
905        }
906
907        interp_ok(Scalar::from_i32(0))
908    }
909
910    fn rename(
911        &mut self,
912        oldpath_op: &OpTy<'tcx>,
913        newpath_op: &OpTy<'tcx>,
914    ) -> InterpResult<'tcx, Scalar> {
915        let this = self.eval_context_mut();
916
917        let oldpath_ptr = this.read_pointer(oldpath_op)?;
918        let newpath_ptr = this.read_pointer(newpath_op)?;
919
920        if this.ptr_is_null(oldpath_ptr)? || this.ptr_is_null(newpath_ptr)? {
921            return this.set_last_error_and_return_i32(LibcError("EFAULT"));
922        }
923
924        let oldpath = this.read_path_from_c_str(oldpath_ptr)?;
925        let newpath = this.read_path_from_c_str(newpath_ptr)?;
926
927        // Reject if isolation is enabled.
928        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
929            this.reject_in_isolation("`rename`", reject_with)?;
930            return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);
931        }
932
933        let result = fs::rename(oldpath, newpath).map(|_| 0);
934
935        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
936    }
937
938    fn mkdir(&mut self, path_op: &OpTy<'tcx>, mode_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
939        let this = self.eval_context_mut();
940
941        #[cfg_attr(not(unix), allow(unused_variables))]
942        let mode = if matches!(&this.tcx.sess.target.os, Os::MacOs | Os::FreeBsd) {
943            u32::from(this.read_scalar(mode_op)?.to_u16()?)
944        } else {
945            this.read_scalar(mode_op)?.to_u32()?
946        };
947
948        let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
949
950        // Reject if isolation is enabled.
951        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
952            this.reject_in_isolation("`mkdir`", reject_with)?;
953            return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);
954        }
955
956        #[cfg_attr(not(unix), allow(unused_mut))]
957        let mut builder = DirBuilder::new();
958
959        // If the host supports it, forward on the mode of the directory
960        // (i.e. permission bits and the sticky bit)
961        #[cfg(unix)]
962        {
963            use std::os::unix::fs::DirBuilderExt;
964            builder.mode(mode);
965        }
966
967        let result = builder.create(path).map(|_| 0i32);
968
969        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
970    }
971
972    fn rmdir(&mut self, path_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
973        let this = self.eval_context_mut();
974
975        let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
976
977        // Reject if isolation is enabled.
978        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
979            this.reject_in_isolation("`rmdir`", reject_with)?;
980            return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);
981        }
982
983        let result = fs::remove_dir(path).map(|_| 0i32);
984
985        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
986    }
987
988    fn opendir(&mut self, name_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
989        let this = self.eval_context_mut();
990
991        let name = this.read_path_from_c_str(this.read_pointer(name_op)?)?;
992
993        // Reject if isolation is enabled.
994        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
995            this.reject_in_isolation("`opendir`", reject_with)?;
996            this.set_last_error(LibcError("EACCES"))?;
997            return interp_ok(Scalar::null_ptr(this));
998        }
999
1000        let result = fs::read_dir(name);
1001
1002        match result {
1003            Ok(dir_iter) => {
1004                let id = this.machine.dirs.insert_new(dir_iter);
1005
1006                // The libc API for opendir says that this method returns a pointer to an opaque
1007                // structure, but we are returning an ID number. Thus, pass it as a scalar of
1008                // pointer width.
1009                interp_ok(Scalar::from_target_usize(id, this))
1010            }
1011            Err(e) => {
1012                this.set_last_error(e)?;
1013                interp_ok(Scalar::null_ptr(this))
1014            }
1015        }
1016    }
1017
1018    fn readdir(&mut self, dirp_op: &OpTy<'tcx>, dest: &MPlaceTy<'tcx>) -> InterpResult<'tcx> {
1019        let this = self.eval_context_mut();
1020
1021        if !matches!(
1022            &this.tcx.sess.target.os,
1023            Os::Linux | Os::Android | Os::Solaris | Os::Illumos | Os::FreeBsd
1024        ) {
1025            panic!("`readdir` should not be called on {}", this.tcx.sess.target.os);
1026        }
1027
1028        let dirp = this.read_target_usize(dirp_op)?;
1029
1030        // Reject if isolation is enabled.
1031        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1032            this.reject_in_isolation("`readdir`", reject_with)?;
1033            this.set_last_error(LibcError("EBADF"))?;
1034            this.write_null(dest)?;
1035            return interp_ok(());
1036        }
1037
1038        let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| {
1039            err_ub_format!("the DIR pointer passed to `readdir` did not come from opendir")
1040        })?;
1041
1042        let entry = match open_dir.next_host_entry() {
1043            Some(Ok(dir_entry)) => {
1044                let dir_entry = this.dir_entry_fields(dir_entry)?;
1045
1046                // Write the directory entry into a newly allocated buffer.
1047                // The name is written with write_bytes, while the rest of the
1048                // dirent64 (or dirent) struct is written using write_int_fields.
1049
1050                // For reference:
1051                // On Linux:
1052                // pub struct dirent64 {
1053                //     pub d_ino: ino64_t,
1054                //     pub d_off: off64_t,
1055                //     pub d_reclen: c_ushort,
1056                //     pub d_type: c_uchar,
1057                //     pub d_name: [c_char; 256],
1058                // }
1059                //
1060                // On Solaris:
1061                // pub struct dirent {
1062                //     pub d_ino: ino64_t,
1063                //     pub d_off: off64_t,
1064                //     pub d_reclen: c_ushort,
1065                //     pub d_name: [c_char; 3],
1066                // }
1067                //
1068                // On FreeBSD:
1069                // pub struct dirent {
1070                //     pub d_fileno: uint32_t,
1071                //     pub d_reclen: uint16_t,
1072                //     pub d_type: uint8_t,
1073                //     pub d_namlen: uint8_t,
1074                //     pub d_name: [c_char; 256],
1075                // }
1076
1077                // We just use the pointee type here since determining the right pointee type
1078                // independently is highly non-trivial: it depends on which exact alias of the
1079                // function was invoked (e.g. `fstat` vs `fstat64`), and then on FreeBSD it also
1080                // depends on the ABI level which can be different between the libc used by std and
1081                // the libc used by everyone else.
1082                let dirent_ty = dest.layout.ty.builtin_deref(true).unwrap();
1083                let dirent_layout = this.layout_of(dirent_ty)?;
1084                let fields = &dirent_layout.fields;
1085                let d_name_offset = fields.offset(fields.count().strict_sub(1)).bytes();
1086
1087                // Determine the size of the buffer we have to allocate.
1088                let mut name = dir_entry.name; // not a Path as there are no separators!
1089                name.push("\0"); // Add a NUL terminator
1090                let name_bytes = name.as_encoded_bytes();
1091                let name_len = u64::try_from(name_bytes.len()).unwrap();
1092                let size = d_name_offset.strict_add(name_len);
1093
1094                let entry = this.allocate_ptr(
1095                    Size::from_bytes(size),
1096                    dirent_layout.align.abi,
1097                    MiriMemoryKind::Runtime.into(),
1098                    AllocInit::Uninit,
1099                )?;
1100                let entry = this.ptr_to_mplace(entry.into(), dirent_layout);
1101
1102                // Write the name.
1103                // The name is not a normal field, we already computed the offset above.
1104                let name_ptr = entry.ptr().wrapping_offset(Size::from_bytes(d_name_offset), this);
1105                this.write_bytes_ptr(name_ptr, name_bytes.iter().copied())?;
1106
1107                // Write common fields.
1108                let ino_name =
1109                    if this.tcx.sess.target.os == Os::FreeBsd { "d_fileno" } else { "d_ino" };
1110                this.write_int_fields_named(
1111                    &[(ino_name, dir_entry.ino.into()), ("d_reclen", size.into())],
1112                    &entry,
1113                )?;
1114
1115                // Write "optional" fields.
1116                if let Some(d_off) = this.try_project_field_named(&entry, "d_off")? {
1117                    this.write_null(&d_off)?;
1118                }
1119                if let Some(d_namlen) = this.try_project_field_named(&entry, "d_namlen")? {
1120                    this.write_int(name_len.strict_sub(1), &d_namlen)?;
1121                }
1122                if let Some(d_type) = this.try_project_field_named(&entry, "d_type")? {
1123                    this.write_int(dir_entry.d_type, &d_type)?;
1124                }
1125
1126                Some(entry.ptr())
1127            }
1128            None => {
1129                // end of stream: return NULL
1130                None
1131            }
1132            Some(Err(e)) => {
1133                this.set_last_error(e)?;
1134                None
1135            }
1136        };
1137
1138        let open_dir = this.machine.dirs.streams.get_mut(&dirp).unwrap();
1139        let old_entry = std::mem::replace(&mut open_dir.entry, entry);
1140        if let Some(old_entry) = old_entry {
1141            this.deallocate_ptr(old_entry, None, MiriMemoryKind::Runtime.into())?;
1142        }
1143
1144        this.write_pointer(entry.unwrap_or_else(Pointer::null), dest)?;
1145        interp_ok(())
1146    }
1147
1148    fn macos_readdir_r(
1149        &mut self,
1150        dirp_op: &OpTy<'tcx>,
1151        entry_op: &OpTy<'tcx>,
1152        result_op: &OpTy<'tcx>,
1153    ) -> InterpResult<'tcx, Scalar> {
1154        let this = self.eval_context_mut();
1155
1156        this.assert_target_os(Os::MacOs, "readdir_r");
1157
1158        let dirp = this.read_target_usize(dirp_op)?;
1159        let result_place = this.deref_pointer_as(result_op, this.machine.layouts.mut_raw_ptr)?;
1160
1161        // Reject if isolation is enabled.
1162        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1163            this.reject_in_isolation("`readdir_r`", reject_with)?;
1164            // Return error code, do *not* set `errno`.
1165            return interp_ok(this.eval_libc("EBADF"));
1166        }
1167
1168        let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| {
1169            err_unsup_format!("the DIR pointer passed to readdir_r did not come from opendir")
1170        })?;
1171        interp_ok(match open_dir.next_host_entry() {
1172            Some(Ok(dir_entry)) => {
1173                let dir_entry = this.dir_entry_fields(dir_entry)?;
1174                // Write into entry, write pointer to result, return 0 on success.
1175                // The name is written with write_os_str_to_c_str, while the rest of the
1176                // dirent struct is written using write_int_fields.
1177
1178                // For reference, on macOS this looks like:
1179                // pub struct dirent {
1180                //     pub d_ino: u64,
1181                //     pub d_seekoff: u64,
1182                //     pub d_reclen: u16,
1183                //     pub d_namlen: u16,
1184                //     pub d_type: u8,
1185                //     pub d_name: [c_char; 1024],
1186                // }
1187
1188                let entry_place = this.deref_pointer_as(entry_op, this.libc_ty_layout("dirent"))?;
1189
1190                // Write the name.
1191                let name_place = this.project_field_named(&entry_place, "d_name")?;
1192                let (name_fits, file_name_buf_len) = this.write_os_str_to_c_str(
1193                    &dir_entry.name,
1194                    name_place.ptr(),
1195                    name_place.layout.size.bytes(),
1196                )?;
1197                if !name_fits {
1198                    throw_unsup_format!(
1199                        "a directory entry had a name too large to fit in libc::dirent"
1200                    );
1201                }
1202
1203                // Write the other fields.
1204                this.write_int_fields_named(
1205                    &[
1206                        ("d_reclen", entry_place.layout.size.bytes().into()),
1207                        ("d_namlen", file_name_buf_len.strict_sub(1).into()),
1208                        ("d_type", dir_entry.d_type.into()),
1209                        ("d_ino", dir_entry.ino.into()),
1210                        ("d_seekoff", 0),
1211                    ],
1212                    &entry_place,
1213                )?;
1214                this.write_scalar(this.read_scalar(entry_op)?, &result_place)?;
1215
1216                Scalar::from_i32(0)
1217            }
1218            None => {
1219                // end of stream: return 0, assign *result=NULL
1220                this.write_null(&result_place)?;
1221                Scalar::from_i32(0)
1222            }
1223            Some(Err(e)) => {
1224                // return positive error number on error (do *not* set last error)
1225                this.io_error_to_errnum(e)?
1226            }
1227        })
1228    }
1229
1230    fn closedir(&mut self, dirp_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1231        let this = self.eval_context_mut();
1232
1233        let dirp = this.read_target_usize(dirp_op)?;
1234
1235        // Reject if isolation is enabled.
1236        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1237            this.reject_in_isolation("`closedir`", reject_with)?;
1238            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1239        }
1240
1241        let Some(mut open_dir) = this.machine.dirs.streams.remove(&dirp) else {
1242            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1243        };
1244        if let Some(entry) = open_dir.entry.take() {
1245            this.deallocate_ptr(entry, None, MiriMemoryKind::Runtime.into())?;
1246        }
1247        // We drop the `open_dir`, which will close the host dir handle.
1248        drop(open_dir);
1249
1250        interp_ok(Scalar::from_i32(0))
1251    }
1252
1253    fn ftruncate64(&mut self, fd_num: i32, length: i128) -> InterpResult<'tcx, Scalar> {
1254        let this = self.eval_context_mut();
1255
1256        // Reject if isolation is enabled.
1257        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1258            this.reject_in_isolation("`ftruncate64`", reject_with)?;
1259            // Set error code as "EBADF" (bad fd)
1260            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1261        }
1262
1263        let Some(fd) = this.machine.fds.get(fd_num) else {
1264            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1265        };
1266
1267        let Some(file) = fd.downcast::<FileHandle>() else {
1268            // The docs say that EINVAL is returned when the FD "does not reference a regular file
1269            // or a POSIX shared memory object" (and we don't support shmem objects).
1270            return interp_ok(this.eval_libc("EINVAL"));
1271        };
1272
1273        if file.writable {
1274            if let Ok(length) = length.try_into() {
1275                let result = file.file.set_len(length);
1276                let result = this.try_unwrap_io_result(result.map(|_| 0i32))?;
1277                interp_ok(Scalar::from_i32(result))
1278            } else {
1279                this.set_last_error_and_return_i32(LibcError("EINVAL"))
1280            }
1281        } else {
1282            // The file is not writable
1283            this.set_last_error_and_return_i32(LibcError("EINVAL"))
1284        }
1285    }
1286
1287    /// NOTE: According to the man page of `possix_fallocate`, it returns the error code instead
1288    /// of setting `errno`.
1289    fn posix_fallocate(
1290        &mut self,
1291        fd_num: i32,
1292        offset: i64,
1293        len: i64,
1294    ) -> InterpResult<'tcx, Scalar> {
1295        let this = self.eval_context_mut();
1296
1297        // Reject if isolation is enabled.
1298        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1299            this.reject_in_isolation("`posix_fallocate`", reject_with)?;
1300            // Return error code "EBADF" (bad fd).
1301            return interp_ok(this.eval_libc("EBADF"));
1302        }
1303
1304        // EINVAL is returned when: "offset was less than 0, or len was less than or equal to 0".
1305        if offset < 0 || len <= 0 {
1306            return interp_ok(this.eval_libc("EINVAL"));
1307        }
1308
1309        // Get the file handle.
1310        let Some(fd) = this.machine.fds.get(fd_num) else {
1311            return interp_ok(this.eval_libc("EBADF"));
1312        };
1313        let Some(file) = fd.downcast::<FileHandle>() else {
1314            // Man page specifies to return ENODEV if `fd` is not a regular file.
1315            return interp_ok(this.eval_libc("ENODEV"));
1316        };
1317
1318        if !file.writable {
1319            // The file is not writable.
1320            return interp_ok(this.eval_libc("EBADF"));
1321        }
1322
1323        let current_size = match file.file.metadata() {
1324            Ok(metadata) => metadata.len(),
1325            Err(err) => return this.io_error_to_errnum(err),
1326        };
1327        // Checked i64 addition, to ensure the result does not exceed the max file size.
1328        let new_size = match offset.checked_add(len) {
1329            // `new_size` is definitely non-negative, so we can cast to `u64`.
1330            Some(new_size) => u64::try_from(new_size).unwrap(),
1331            None => return interp_ok(this.eval_libc("EFBIG")), // new size too big
1332        };
1333        // If the size of the file is less than offset+size, then the file is increased to this size;
1334        // otherwise the file size is left unchanged.
1335        if current_size < new_size {
1336            interp_ok(match file.file.set_len(new_size) {
1337                Ok(()) => Scalar::from_i32(0),
1338                Err(e) => this.io_error_to_errnum(e)?,
1339            })
1340        } else {
1341            interp_ok(Scalar::from_i32(0))
1342        }
1343    }
1344
1345    fn fsync(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1346        // On macOS, `fsync` (unlike `fcntl(F_FULLFSYNC)`) does not wait for the
1347        // underlying disk to finish writing. In the interest of host compatibility,
1348        // we conservatively implement this with `sync_all`, which
1349        // *does* wait for the disk.
1350
1351        let this = self.eval_context_mut();
1352
1353        let fd = this.read_scalar(fd_op)?.to_i32()?;
1354
1355        // Reject if isolation is enabled.
1356        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1357            this.reject_in_isolation("`fsync`", reject_with)?;
1358            // Set error code as "EBADF" (bad fd)
1359            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1360        }
1361
1362        self.ffullsync_fd(fd)
1363    }
1364
1365    fn ffullsync_fd(&mut self, fd_num: i32) -> InterpResult<'tcx, Scalar> {
1366        let this = self.eval_context_mut();
1367        let Some(fd) = this.machine.fds.get(fd_num) else {
1368            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1369        };
1370        // Only regular files support synchronization.
1371        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1372            err_unsup_format!("`fsync` is only supported on file-backed file descriptors")
1373        })?;
1374        let io_result = maybe_sync_file(&file.file, file.writable, File::sync_all);
1375        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1376    }
1377
1378    fn fdatasync(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1379        let this = self.eval_context_mut();
1380
1381        let fd = this.read_scalar(fd_op)?.to_i32()?;
1382
1383        // Reject if isolation is enabled.
1384        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1385            this.reject_in_isolation("`fdatasync`", reject_with)?;
1386            // Set error code as "EBADF" (bad fd)
1387            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1388        }
1389
1390        let Some(fd) = this.machine.fds.get(fd) else {
1391            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1392        };
1393        // Only regular files support synchronization.
1394        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1395            err_unsup_format!("`fdatasync` is only supported on file-backed file descriptors")
1396        })?;
1397        let io_result = maybe_sync_file(&file.file, file.writable, File::sync_data);
1398        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1399    }
1400
1401    fn sync_file_range(
1402        &mut self,
1403        fd_op: &OpTy<'tcx>,
1404        offset_op: &OpTy<'tcx>,
1405        nbytes_op: &OpTy<'tcx>,
1406        flags_op: &OpTy<'tcx>,
1407    ) -> InterpResult<'tcx, Scalar> {
1408        let this = self.eval_context_mut();
1409
1410        let fd = this.read_scalar(fd_op)?.to_i32()?;
1411        let offset = this.read_scalar(offset_op)?.to_i64()?;
1412        let nbytes = this.read_scalar(nbytes_op)?.to_i64()?;
1413        let flags = this.read_scalar(flags_op)?.to_i32()?;
1414
1415        if offset < 0 || nbytes < 0 {
1416            return this.set_last_error_and_return_i32(LibcError("EINVAL"));
1417        }
1418        let allowed_flags = this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_BEFORE")
1419            | this.eval_libc_i32("SYNC_FILE_RANGE_WRITE")
1420            | this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_AFTER");
1421        if flags & allowed_flags != flags {
1422            return this.set_last_error_and_return_i32(LibcError("EINVAL"));
1423        }
1424
1425        // Reject if isolation is enabled.
1426        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1427            this.reject_in_isolation("`sync_file_range`", reject_with)?;
1428            // Set error code as "EBADF" (bad fd)
1429            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1430        }
1431
1432        let Some(fd) = this.machine.fds.get(fd) else {
1433            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1434        };
1435        // Only regular files support synchronization.
1436        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1437            err_unsup_format!("`sync_data_range` is only supported on file-backed file descriptors")
1438        })?;
1439        let io_result = maybe_sync_file(&file.file, file.writable, File::sync_data);
1440        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1441    }
1442
1443    fn readlink(
1444        &mut self,
1445        pathname_op: &OpTy<'tcx>,
1446        buf_op: &OpTy<'tcx>,
1447        bufsize_op: &OpTy<'tcx>,
1448    ) -> InterpResult<'tcx, i64> {
1449        let this = self.eval_context_mut();
1450
1451        let pathname = this.read_path_from_c_str(this.read_pointer(pathname_op)?)?;
1452        let buf = this.read_pointer(buf_op)?;
1453        let bufsize = this.read_target_usize(bufsize_op)?;
1454
1455        // Reject if isolation is enabled.
1456        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1457            this.reject_in_isolation("`readlink`", reject_with)?;
1458            this.set_last_error(LibcError("EACCES"))?;
1459            return interp_ok(-1);
1460        }
1461
1462        let result = std::fs::read_link(pathname);
1463        match result {
1464            Ok(resolved) => {
1465                // 'readlink' truncates the resolved path if the provided buffer is not large
1466                // enough, and does *not* add a null terminator. That means we cannot use the usual
1467                // `write_path_to_c_str` and have to re-implement parts of it ourselves.
1468                let resolved = this.convert_path(
1469                    Cow::Borrowed(resolved.as_ref()),
1470                    crate::shims::os_str::PathConversion::HostToTarget,
1471                );
1472                let mut path_bytes = resolved.as_encoded_bytes();
1473                let bufsize: usize = bufsize.try_into().unwrap();
1474                if path_bytes.len() > bufsize {
1475                    path_bytes = &path_bytes[..bufsize]
1476                }
1477                this.write_bytes_ptr(buf, path_bytes.iter().copied())?;
1478                interp_ok(path_bytes.len().try_into().unwrap())
1479            }
1480            Err(e) => {
1481                this.set_last_error(e)?;
1482                interp_ok(-1)
1483            }
1484        }
1485    }
1486
1487    fn isatty(&mut self, miri_fd: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1488        let this = self.eval_context_mut();
1489        // "returns 1 if fd is an open file descriptor referring to a terminal;
1490        // otherwise 0 is returned, and errno is set to indicate the error"
1491        let fd = this.read_scalar(miri_fd)?.to_i32()?;
1492        let error = if let Some(fd) = this.machine.fds.get(fd) {
1493            if fd.is_tty(this.machine.communicate()) {
1494                return interp_ok(Scalar::from_i32(1));
1495            } else {
1496                LibcError("ENOTTY")
1497            }
1498        } else {
1499            // FD does not exist
1500            LibcError("EBADF")
1501        };
1502        this.set_last_error(error)?;
1503        interp_ok(Scalar::from_i32(0))
1504    }
1505
1506    fn realpath(
1507        &mut self,
1508        path_op: &OpTy<'tcx>,
1509        processed_path_op: &OpTy<'tcx>,
1510    ) -> InterpResult<'tcx, Scalar> {
1511        let this = self.eval_context_mut();
1512        this.assert_target_os_is_unix("realpath");
1513
1514        let pathname = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
1515        let processed_ptr = this.read_pointer(processed_path_op)?;
1516
1517        // Reject if isolation is enabled.
1518        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1519            this.reject_in_isolation("`realpath`", reject_with)?;
1520            this.set_last_error(LibcError("EACCES"))?;
1521            return interp_ok(Scalar::from_target_usize(0, this));
1522        }
1523
1524        let result = std::fs::canonicalize(pathname);
1525        match result {
1526            Ok(resolved) => {
1527                let path_max = this
1528                    .eval_libc_i32("PATH_MAX")
1529                    .try_into()
1530                    .expect("PATH_MAX does not fit in u64");
1531                let dest = if this.ptr_is_null(processed_ptr)? {
1532                    // POSIX says behavior when passing a null pointer is implementation-defined,
1533                    // but GNU/linux, freebsd, netbsd, bionic/android, and macos all treat a null pointer
1534                    // similarly to:
1535                    //
1536                    // "If resolved_path is specified as NULL, then realpath() uses
1537                    // malloc(3) to allocate a buffer of up to PATH_MAX bytes to hold
1538                    // the resolved pathname, and returns a pointer to this buffer.  The
1539                    // caller should deallocate this buffer using free(3)."
1540                    // <https://man7.org/linux/man-pages/man3/realpath.3.html>
1541                    this.alloc_path_as_c_str(&resolved, MiriMemoryKind::C.into())?
1542                } else {
1543                    let (wrote_path, _) =
1544                        this.write_path_to_c_str(&resolved, processed_ptr, path_max)?;
1545
1546                    if !wrote_path {
1547                        // Note that we do not explicitly handle `FILENAME_MAX`
1548                        // (different from `PATH_MAX` above) as it is Linux-specific and
1549                        // seems like a bit of a mess anyway: <https://eklitzke.org/path-max-is-tricky>.
1550                        this.set_last_error(LibcError("ENAMETOOLONG"))?;
1551                        return interp_ok(Scalar::from_target_usize(0, this));
1552                    }
1553                    processed_ptr
1554                };
1555
1556                interp_ok(Scalar::from_maybe_pointer(dest, this))
1557            }
1558            Err(e) => {
1559                this.set_last_error(e)?;
1560                interp_ok(Scalar::from_target_usize(0, this))
1561            }
1562        }
1563    }
1564    fn mkstemp(&mut self, template_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1565        use rand::seq::IndexedRandom;
1566
1567        // POSIX defines the template string.
1568        const TEMPFILE_TEMPLATE_STR: &str = "XXXXXX";
1569
1570        let this = self.eval_context_mut();
1571        this.assert_target_os_is_unix("mkstemp");
1572
1573        // POSIX defines the maximum number of attempts before failure.
1574        //
1575        // `mkstemp()` relies on `tmpnam()` which in turn relies on `TMP_MAX`.
1576        // POSIX says this about `TMP_MAX`:
1577        // * Minimum number of unique filenames generated by `tmpnam()`.
1578        // * Maximum number of times an application can call `tmpnam()` reliably.
1579        //   * The value of `TMP_MAX` is at least 25.
1580        //   * On XSI-conformant systems, the value of `TMP_MAX` is at least 10000.
1581        // See <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/stdio.h.html>.
1582        let max_attempts = this.eval_libc_u32("TMP_MAX");
1583
1584        // Get the raw bytes from the template -- as a byte slice, this is a string in the target
1585        // (and the target is unix, so a byte slice is the right representation).
1586        let template_ptr = this.read_pointer(template_op)?;
1587        let mut template = this.eval_context_ref().read_c_str(template_ptr)?.to_owned();
1588        let template_bytes = template.as_mut_slice();
1589
1590        // Reject if isolation is enabled.
1591        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1592            this.reject_in_isolation("`mkstemp`", reject_with)?;
1593            return this.set_last_error_and_return_i32(LibcError("EACCES"));
1594        }
1595
1596        // Get the bytes of the suffix we expect in _target_ encoding.
1597        let suffix_bytes = TEMPFILE_TEMPLATE_STR.as_bytes();
1598
1599        // At this point we have one `&[u8]` that represents the template and one `&[u8]`
1600        // that represents the expected suffix.
1601
1602        // Now we figure out the index of the slice we expect to contain the suffix.
1603        let start_pos = template_bytes.len().saturating_sub(suffix_bytes.len());
1604        let end_pos = template_bytes.len();
1605        let last_six_char_bytes = &template_bytes[start_pos..end_pos];
1606
1607        // If we don't find the suffix, it is an error.
1608        if last_six_char_bytes != suffix_bytes {
1609            return this.set_last_error_and_return_i32(LibcError("EINVAL"));
1610        }
1611
1612        // At this point we know we have 6 ASCII 'X' characters as a suffix.
1613
1614        // From <https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/sysdeps/posix/tempname.c#L175>
1615        const SUBSTITUTIONS: &[char; 62] = &[
1616            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
1617            'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
1618            'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
1619            'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
1620        ];
1621
1622        // The file is opened with specific options, which Rust does not expose in a portable way.
1623        // So we use specific APIs depending on the host OS.
1624        let mut fopts = OpenOptions::new();
1625        fopts.read(true).write(true).create_new(true);
1626
1627        #[cfg(unix)]
1628        {
1629            use std::os::unix::fs::OpenOptionsExt;
1630            // Do not allow others to read or modify this file.
1631            fopts.mode(0o600);
1632            fopts.custom_flags(libc::O_EXCL);
1633        }
1634        #[cfg(windows)]
1635        {
1636            use std::os::windows::fs::OpenOptionsExt;
1637            // Do not allow others to read or modify this file.
1638            fopts.share_mode(0);
1639        }
1640
1641        // If the generated file already exists, we will try again `max_attempts` many times.
1642        for _ in 0..max_attempts {
1643            let rng = this.machine.rng.get_mut();
1644
1645            // Generate a random unique suffix.
1646            let unique_suffix =
1647                (0..6).map(|_| SUBSTITUTIONS.choose(rng).unwrap()).collect::<String>();
1648
1649            // Replace the template string with the random string.
1650            template_bytes[start_pos..end_pos].copy_from_slice(unique_suffix.as_bytes());
1651
1652            // Write the modified template back to the passed in pointer to maintain POSIX semantics.
1653            this.write_bytes_ptr(template_ptr, template_bytes.iter().copied())?;
1654
1655            // See if we can create and open this file.
1656            let file = fopts.open(bytes_to_os_str(template_bytes)?);
1657            match file {
1658                Ok(f) => {
1659                    let fd = this.machine.fds.insert_new(FileHandle { file: f, writable: true });
1660                    return interp_ok(Scalar::from_i32(fd));
1661                }
1662                Err(e) =>
1663                    match e.kind() {
1664                        // If the random file already exists, keep trying.
1665                        ErrorKind::AlreadyExists => continue,
1666                        // Any other errors are returned to the caller.
1667                        _ => {
1668                            // "On error, -1 is returned, and errno is set to
1669                            // indicate the error"
1670                            return this.set_last_error_and_return_i32(e);
1671                        }
1672                    },
1673            }
1674        }
1675
1676        // We ran out of attempts to create the file, return an error.
1677        this.set_last_error_and_return_i32(LibcError("EEXIST"))
1678    }
1679}
1680
1681/// Extracts the number of seconds and nanoseconds elapsed between `time` and the unix epoch when
1682/// `time` is Ok. Returns `None` if `time` is an error. Fails if `time` happens before the unix
1683/// epoch.
1684fn extract_sec_and_nsec<'tcx>(
1685    time: std::io::Result<SystemTime>,
1686) -> InterpResult<'tcx, Option<(u64, u32)>> {
1687    match time.ok() {
1688        Some(time) => {
1689            let duration = system_time_to_duration(&time)?;
1690            interp_ok(Some((duration.as_secs(), duration.subsec_nanos())))
1691        }
1692        None => interp_ok(None),
1693    }
1694}
1695
1696fn file_type_to_mode_name(file_type: std::fs::FileType) -> &'static str {
1697    #[cfg(unix)]
1698    use std::os::unix::fs::FileTypeExt;
1699
1700    if file_type.is_file() {
1701        "S_IFREG"
1702    } else if file_type.is_dir() {
1703        "S_IFDIR"
1704    } else if file_type.is_symlink() {
1705        "S_IFLNK"
1706    } else {
1707        // Certain file types are only available when the host is a Unix system.
1708        #[cfg(unix)]
1709        {
1710            if file_type.is_socket() {
1711                return "S_IFSOCK";
1712            } else if file_type.is_fifo() {
1713                return "S_IFIFO";
1714            } else if file_type.is_char_device() {
1715                return "S_IFCHR";
1716            } else if file_type.is_block_device() {
1717                return "S_IFBLK";
1718            }
1719        }
1720        "S_IFREG"
1721    }
1722}
1723
1724/// Stores a file's metadata in order to avoid code duplication in the different metadata related
1725/// shims.
1726///
1727/// Some fields are host/platform-specific. `None` means that Miri does not have a real value for
1728/// this field, for example because the metadata is synthetic or because the host platform does not
1729/// expose it. `statx` must only advertise the corresponding `STATX_*` bit when the field is `Some`;
1730/// legacy `stat` writes zero for `None` to preserve the old fallback behavior.
1731struct FileMetadata {
1732    /// This holds both the file type (dir, regular, symlink, ...) and permissions.
1733    mode: u32,
1734    size: u64,
1735    created: Option<(u64, u32)>,
1736    accessed: Option<(u64, u32)>,
1737    modified: Option<(u64, u32)>,
1738    dev: Option<u64>,
1739    ino: Option<u64>,
1740    nlink: Option<u64>,
1741    uid: Option<u32>,
1742    gid: Option<u32>,
1743    blksize: Option<u64>,
1744    blocks: Option<u64>,
1745}
1746
1747impl FileMetadata {
1748    fn from_path<'tcx>(
1749        ecx: &mut MiriInterpCx<'tcx>,
1750        path: &Path,
1751        follow_symlink: bool,
1752    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1753        let metadata =
1754            if follow_symlink { std::fs::metadata(path) } else { std::fs::symlink_metadata(path) };
1755
1756        FileMetadata::from_meta(ecx, metadata)
1757    }
1758
1759    fn from_fd_num<'tcx>(
1760        ecx: &mut MiriInterpCx<'tcx>,
1761        fd_num: i32,
1762    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1763        let Some(fd) = ecx.machine.fds.get(fd_num) else {
1764            return interp_ok(Err(LibcError("EBADF")));
1765        };
1766        match fd.metadata()? {
1767            Either::Left(host) => Self::from_meta(ecx, host),
1768            Either::Right(name) => Self::synthetic(ecx, name),
1769        }
1770    }
1771
1772    fn synthetic<'tcx>(
1773        ecx: &mut MiriInterpCx<'tcx>,
1774        mode_name: &str,
1775    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1776        let mode = ecx.eval_libc(mode_name);
1777        let mode: u32 = mode.to_uint(ecx.libc_ty_layout("mode_t").size)?.try_into().unwrap();
1778        // We observed 0x777 on sockets and 0x600 on pipes...
1779        let mode = mode | 0o666;
1780        interp_ok(Ok(FileMetadata {
1781            mode,
1782            size: 0,
1783            created: None,
1784            accessed: None,
1785            modified: None,
1786            dev: None,
1787            uid: None,
1788            gid: None,
1789            blksize: None,
1790            blocks: None,
1791            ino: None,
1792            nlink: None,
1793        }))
1794    }
1795
1796    fn from_meta<'tcx>(
1797        ecx: &mut MiriInterpCx<'tcx>,
1798        metadata: Result<std::fs::Metadata, std::io::Error>,
1799    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1800        let metadata = match metadata {
1801            Ok(metadata) => metadata,
1802            Err(e) => {
1803                return interp_ok(Err(e.into()));
1804            }
1805        };
1806
1807        let file_type = metadata.file_type();
1808        let mode = ecx.eval_libc(file_type_to_mode_name(file_type));
1809        let mut mode = mode.to_uint(ecx.libc_ty_layout("mode_t").size)?.try_into().unwrap();
1810
1811        let size = metadata.len();
1812
1813        let created = extract_sec_and_nsec(metadata.created())?;
1814        let accessed = extract_sec_and_nsec(metadata.accessed())?;
1815        let modified = extract_sec_and_nsec(metadata.modified())?;
1816
1817        // FIXME: Provide more fields using platform specific methods.
1818
1819        cfg_select! {
1820            unix => {
1821                use std::os::unix::fs::MetadataExt;
1822                use std::os::unix::fs::PermissionsExt;
1823
1824                let dev = metadata.dev();
1825                let ino = metadata.ino();
1826                let nlink = metadata.nlink();
1827                let uid = metadata.uid();
1828                let gid = metadata.gid();
1829                let blksize = metadata.blksize();
1830                let blocks = metadata.blocks();
1831
1832                mode |= metadata.permissions().mode();
1833
1834                interp_ok(Ok(FileMetadata {
1835                    mode,
1836                    size,
1837                    created,
1838                    accessed,
1839                    modified,
1840                    dev: Some(dev),
1841                    ino: Some(ino),
1842                    nlink: Some(nlink),
1843                    uid: Some(uid),
1844                    gid: Some(gid),
1845                    blksize: Some(blksize),
1846                    blocks: Some(blocks),
1847                }))
1848            }
1849            _ => {
1850                // Emulate "everyone can read" or "everyone can read and write".
1851                mode |= if metadata.permissions().readonly() { 0o111 } else { 0o333 };
1852
1853                interp_ok(Ok(FileMetadata {
1854                    mode,
1855                    size,
1856                    created,
1857                    accessed,
1858                    modified,
1859                    dev: None,
1860                    ino: None,
1861                    nlink: None,
1862                    uid: None,
1863                    gid: None,
1864                    blksize: None,
1865                    blocks: None,
1866                }))
1867            },
1868        }
1869    }
1870}