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, FileTimes, 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::{FieldIdx, 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::FdId;
17use crate::shims::files::{DirHandle, FdNum, FileHandle, open_file_or_dir};
18use crate::shims::os_str::{PathConversion, bytes_to_os_str};
19use crate::shims::sig::Varargs;
20use crate::shims::unix::fd::{EvalContextExt as _, FlockOp, UnixFileDescription};
21use crate::*;
22
23/// An open directory stream (`DIR`), tracked by DirTable.
24#[derive(Debug)]
25struct DirStream {
26    /// The directory stream on the host.
27    read_dir: fs::ReadDir,
28    /// Unix directory streams have an "underlying FD" that can be exposed; this is that FD. This is
29    /// not a reference; it *can* be separately closed, leading to UB if the DIR is used again. We
30    /// also store the FD ID to catch cases where the FD was closed and a different one re-opened.
31    fd_num: FdNum,
32    fd_id: FdId,
33    /// The "special" entries that must still be yielded by the iterator.
34    /// Used for `.` and `..`.
35    special_entries: Vec<&'static str>,
36    /// The most recent entry returned by readdir(). Will be freed by the next call.
37    /// The string is the type used to allocate it.
38    entry: Option<(Pointer, &'static str)>,
39}
40
41impl DirStream {
42    fn new(read_dir: fs::ReadDir, fd_num: FdNum, fd_id: FdId) -> Self {
43        Self { read_dir, fd_num, fd_id, special_entries: vec!["..", "."], entry: None }
44    }
45
46    fn next_host_entry(&mut self) -> Option<io::Result<Either<fs::DirEntry, &'static str>>> {
47        if let Some(special) = self.special_entries.pop() {
48            return Some(Ok(Either::Right(special)));
49        }
50        let entry = self.read_dir.next()?;
51        Some(entry.map(Either::Left))
52    }
53}
54
55#[derive(Debug)]
56struct DirEntry {
57    name: OsString,
58    ino: u64,
59    d_type: i32,
60}
61
62/// What a `futimens` `timespec` asks for: leave the timestamp alone (`UTIME_OMIT`) or set it.
63#[derive(Copy, Clone)]
64enum TimeUpdate {
65    Omit,
66    Set(SystemTime),
67}
68
69impl UnixFileDescription for FileHandle {
70    fn pread<'tcx>(
71        &self,
72        communicate_allowed: bool,
73        offset: u64,
74        ptr: Pointer,
75        len: usize,
76        ecx: &mut MiriInterpCx<'tcx>,
77        finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
78    ) -> InterpResult<'tcx> {
79        assert!(communicate_allowed, "isolation should have prevented even opening a file");
80        if !self.readable {
81            return finish.call(ecx, Err(LibcError("EBADF")));
82        }
83
84        let mut bytes = vec![0; len];
85        // Emulates pread using seek + read + seek to restore cursor position.
86        // Correctness of this emulation relies on sequential nature of Miri execution.
87        // The closure is used to emulate `try` block, since we "bubble" `io::Error` using `?`.
88        let file = &mut &self.file;
89        let mut f = || {
90            let cursor_pos = file.stream_position()?;
91            file.seek(SeekFrom::Start(offset))?;
92            let res = file.read(&mut bytes);
93            // Attempt to restore cursor position even if the read has failed
94            file.seek(SeekFrom::Start(cursor_pos))
95                .expect("failed to restore file position, this shouldn't be possible");
96            res
97        };
98        let result = match f() {
99            Ok(read_size) => {
100                // If reading to `bytes` did not fail, we write those bytes to the buffer.
101                // Crucially, if fewer than `bytes.len()` bytes were read, only write
102                // that much into the output buffer!
103                ecx.write_bytes_ptr(ptr, bytes[..read_size].iter().copied())?;
104                Ok(read_size)
105            }
106            Err(e) => Err(IoError::HostError(e)),
107        };
108        finish.call(ecx, result)
109    }
110
111    fn pwrite<'tcx>(
112        &self,
113        communicate_allowed: bool,
114        ptr: Pointer,
115        len: usize,
116        offset: u64,
117        ecx: &mut MiriInterpCx<'tcx>,
118        finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
119    ) -> InterpResult<'tcx> {
120        assert!(communicate_allowed, "isolation should have prevented even opening a file");
121        if !self.writable {
122            return finish.call(ecx, Err(LibcError("EBADF")));
123        }
124
125        // Emulates pwrite using seek + write + seek to restore cursor position.
126        // Correctness of this emulation relies on sequential nature of Miri execution.
127        // The closure is used to emulate `try` block, since we "bubble" `io::Error` using `?`.
128        let file = &mut &self.file;
129        let bytes = ecx.read_bytes_ptr_strip_provenance(ptr, Size::from_bytes(len))?;
130        let mut f = || {
131            let cursor_pos = file.stream_position()?;
132            file.seek(SeekFrom::Start(offset))?;
133            let res = file.write(bytes);
134            // Attempt to restore cursor position even if the write has failed
135            file.seek(SeekFrom::Start(cursor_pos))
136                .expect("failed to restore file position, this shouldn't be possible");
137            res
138        };
139        let result = f();
140        finish.call(ecx, result.map_err(IoError::HostError))
141    }
142
143    fn flock<'tcx>(
144        &self,
145        communicate_allowed: bool,
146        op: FlockOp,
147    ) -> InterpResult<'tcx, io::Result<()>> {
148        assert!(communicate_allowed, "isolation should have prevented even opening a file");
149
150        use FlockOp::*;
151        // We must not block the interpreter loop, so we always `try_lock`.
152        let (res, nonblocking) = match op {
153            SharedLock { nonblocking } => (self.file.try_lock_shared(), nonblocking),
154            ExclusiveLock { nonblocking } => (self.file.try_lock(), nonblocking),
155            Unlock => {
156                return interp_ok(self.file.unlock());
157            }
158        };
159
160        match res {
161            Ok(()) => interp_ok(Ok(())),
162            Err(TryLockError::Error(err)) => interp_ok(Err(err)),
163            Err(TryLockError::WouldBlock) =>
164                if nonblocking {
165                    interp_ok(Err(ErrorKind::WouldBlock.into()))
166                } else {
167                    throw_unsup_format!("blocking `flock` is not currently supported");
168                },
169        }
170    }
171
172    fn get_flags<'tcx>(&self, ecx: &mut MiriInterpCx<'tcx>) -> InterpResult<'tcx, Scalar> {
173        interp_ok(match (self.readable, self.writable) {
174            (true, true) => ecx.eval_libc("O_RDWR"),
175            (true, false) => ecx.eval_libc("O_RDONLY"),
176            (false, true) => ecx.eval_libc("O_WRONLY"),
177            _ => unreachable!(),
178        })
179    }
180}
181
182impl UnixFileDescription for DirHandle {
183    fn get_flags<'tcx>(&self, ecx: &mut MiriInterpCx<'tcx>) -> InterpResult<'tcx, Scalar> {
184        // Directories are always readonly on Unix.
185        interp_ok(ecx.eval_libc("O_RDONLY"))
186    }
187}
188
189/// The table of open directories.
190/// Curiously, Unix/POSIX does not unify this into the "file descriptor" concept... everything
191/// is a file, except a directory is not?
192#[derive(Debug)]
193pub struct DirTable {
194    /// Directory iterators used to emulate libc "directory streams", as used in opendir, readdir,
195    /// and closedir.
196    ///
197    /// When opendir is called, a directory iterator is created on the host for the target
198    /// directory, and an entry is stored in this hash map, indexed by an ID which represents
199    /// the directory stream. When readdir is called, the directory stream ID is used to look up
200    /// the corresponding ReadDir iterator from this map, and information from the next
201    /// directory entry is returned. When closedir is called, the ReadDir iterator is removed from
202    /// the map.
203    streams: FxHashMap<u64, DirStream>,
204    /// ID number to be used by the next call to opendir
205    next_id: u64,
206}
207
208impl DirTable {
209    #[expect(clippy::arithmetic_side_effects)]
210    fn insert_new(&mut self, stream: DirStream) -> u64 {
211        let id = self.next_id;
212        self.next_id += 1;
213        self.streams.try_insert(id, stream).unwrap();
214        id
215    }
216}
217
218impl Default for DirTable {
219    fn default() -> DirTable {
220        DirTable {
221            streams: FxHashMap::default(),
222            // Skip 0 as an ID, because it looks like a null pointer to libc
223            next_id: 1,
224        }
225    }
226}
227
228impl VisitProvenance for DirTable {
229    fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
230        let DirTable { streams, next_id: _ } = self;
231
232        for dir in streams.values() {
233            dir.entry.visit_provenance(visit);
234        }
235    }
236}
237
238fn maybe_sync_file(
239    file: &FileHandle,
240    operation: fn(&File) -> std::io::Result<()>,
241) -> std::io::Result<i32> {
242    if !file.writable && cfg!(windows) {
243        // sync_all() and sync_data() will return an error on Windows hosts if the file is not opened
244        // for writing. (FlushFileBuffers requires that the file handle have the
245        // GENERIC_WRITE right)
246        Ok(0i32)
247    } else {
248        let result = operation(&file.file);
249        result.map(|_| 0i32)
250    }
251}
252
253/// The host `FileTimes` that applies the given access and modification time updates.
254fn host_file_times_from_updates(access: TimeUpdate, modified: TimeUpdate) -> FileTimes {
255    let mut filetimes = FileTimes::new();
256    if let TimeUpdate::Set(access) = access {
257        filetimes = filetimes.set_accessed(access);
258    }
259    if let TimeUpdate::Set(modified) = modified {
260        filetimes = filetimes.set_modified(modified);
261    }
262    filetimes
263}
264
265impl<'tcx> EvalContextExtPrivate<'tcx> for crate::MiriInterpCx<'tcx> {}
266trait EvalContextExtPrivate<'tcx>: crate::MiriInterpCxExt<'tcx> {
267    /// Decode one `futimens` `timespec`, handling the `UTIME_NOW`/`UTIME_OMIT` `tv_nsec` values.
268    /// `None` means the `timespec` is invalid and the caller should report `EINVAL`.
269    fn parse_utimens_timespec(
270        &self,
271        tp: &MPlaceTy<'tcx>,
272    ) -> InterpResult<'tcx, Option<TimeUpdate>> {
273        let this = self.eval_context_ref();
274        // `UTIME_NOW` reads the host clock, which we must not do under isolation.
275        assert!(this.machine.communicate(), "isolation should have prevented reaching this");
276
277        // `tv_nsec` and the `UTIME_*` constants are `c_long`, i.e. the target's `isize`.
278        let nsec_place = this.project_field(tp, FieldIdx::ONE)?;
279        let nsec = this.read_scalar(&nsec_place)?.to_target_isize(this)?;
280
281        if nsec == this.eval_libc("UTIME_OMIT").to_target_isize(this)? {
282            return interp_ok(Some(TimeUpdate::Omit));
283        }
284        if nsec == this.eval_libc("UTIME_NOW").to_target_isize(this)? {
285            return interp_ok(Some(TimeUpdate::Set(SystemTime::now())));
286        }
287
288        let Some(duration) = this.read_timespec(tp)? else {
289            return interp_ok(None);
290        };
291        interp_ok(SystemTime::UNIX_EPOCH.checked_add(duration).map(TimeUpdate::Set))
292    }
293
294    /// Decode the `times` argument of `futimens`/`utimensat`: `[atime, mtime]`, or NULL to set both
295    /// to now, as `(access, modified)`.
296    /// `None` means a `timespec` is invalid and the caller should report `EINVAL`.
297    fn parse_utimens_times(
298        &self,
299        times_ptr: Pointer,
300    ) -> InterpResult<'tcx, Option<(TimeUpdate, TimeUpdate)>> {
301        let this = self.eval_context_ref();
302
303        if this.ptr_is_null(times_ptr)? {
304            let now = TimeUpdate::Set(SystemTime::now());
305            interp_ok(Some((now, now)))
306        } else {
307            let timespec = this.libc_ty_layout("timespec");
308            let access_place = this.ptr_to_mplace(times_ptr, timespec);
309            let modified_place = access_place.offset(timespec.size, timespec, this)?;
310            let Some(access) = this.parse_utimens_timespec(&access_place)? else {
311                return interp_ok(None);
312            };
313            let Some(modified) = this.parse_utimens_timespec(&modified_place)? else {
314                return interp_ok(None);
315            };
316            interp_ok(Some((access, modified)))
317        }
318    }
319
320    fn write_stat_buf(
321        &mut self,
322        metadata: FileMetadata,
323        buf_op: &OpTy<'tcx>,
324        buf_type: &str,
325    ) -> InterpResult<'tcx, i32> {
326        let this = self.eval_context_mut();
327
328        let (access_sec, access_nsec) = metadata.accessed.unwrap_or((0, 0));
329        let (created_sec, created_nsec) = metadata.created.unwrap_or((0, 0));
330        let (modified_sec, modified_nsec) = metadata.modified.unwrap_or((0, 0));
331
332        let buf = this.deref_pointer_as(buf_op, this.libc_ty_layout(buf_type))?;
333
334        this.write_int_fields_named(
335            &[
336                ("st_dev", metadata.dev.unwrap_or(0).into()),
337                ("st_mode", metadata.mode.into()),
338                ("st_nlink", metadata.nlink.unwrap_or(0).into()),
339                ("st_ino", metadata.ino.unwrap_or(0).into()),
340                ("st_uid", metadata.uid.unwrap_or(0).into()),
341                ("st_gid", metadata.gid.unwrap_or(0).into()),
342                ("st_rdev", 0),
343                ("st_atime", access_sec.into()),
344                ("st_atime_nsec", access_nsec.into()),
345                ("st_mtime", modified_sec.into()),
346                ("st_mtime_nsec", modified_nsec.into()),
347                ("st_ctime", 0),
348                ("st_ctime_nsec", 0),
349                ("st_size", metadata.size.into()),
350                ("st_blocks", metadata.blocks.unwrap_or(0).into()),
351                ("st_blksize", metadata.blksize.unwrap_or(0).into()),
352            ],
353            &buf,
354        )?;
355
356        if matches!(&this.tcx.sess.target.os, Os::MacOs | Os::FreeBsd) {
357            this.write_int_fields_named(
358                &[
359                    ("st_birthtime", created_sec.into()),
360                    ("st_birthtime_nsec", created_nsec.into()),
361                    ("st_flags", 0),
362                    ("st_gen", 0),
363                ],
364                &buf,
365            )?;
366        }
367
368        if matches!(&this.tcx.sess.target.os, Os::Solaris | Os::Illumos) {
369            let st_fstype = this.project_field_named(&buf, "st_fstype")?;
370            // This is an array; write 0 into first element so that it encodes the empty string.
371            this.write_int(0, &this.project_index(&st_fstype, 0)?)?;
372        }
373
374        interp_ok(0)
375    }
376
377    fn file_type_to_d_type(&self, file_type: std::io::Result<FileType>) -> InterpResult<'tcx, i32> {
378        #[cfg(unix)]
379        use std::os::unix::fs::FileTypeExt;
380
381        let this = self.eval_context_ref();
382        match file_type {
383            Ok(file_type) => {
384                match () {
385                    _ if file_type.is_dir() => interp_ok(this.eval_libc("DT_DIR").to_u8()?.into()),
386                    _ if file_type.is_file() => interp_ok(this.eval_libc("DT_REG").to_u8()?.into()),
387                    _ if file_type.is_symlink() =>
388                        interp_ok(this.eval_libc("DT_LNK").to_u8()?.into()),
389                    // Certain file types are only supported when the host is a Unix system.
390                    #[cfg(unix)]
391                    _ if file_type.is_block_device() =>
392                        interp_ok(this.eval_libc("DT_BLK").to_u8()?.into()),
393                    #[cfg(unix)]
394                    _ if file_type.is_char_device() =>
395                        interp_ok(this.eval_libc("DT_CHR").to_u8()?.into()),
396                    #[cfg(unix)]
397                    _ if file_type.is_fifo() =>
398                        interp_ok(this.eval_libc("DT_FIFO").to_u8()?.into()),
399                    #[cfg(unix)]
400                    _ if file_type.is_socket() =>
401                        interp_ok(this.eval_libc("DT_SOCK").to_u8()?.into()),
402                    // Fallback
403                    _ => interp_ok(this.eval_libc("DT_UNKNOWN").to_u8()?.into()),
404                }
405            }
406            Err(_) => {
407                // Fallback on error
408                interp_ok(this.eval_libc("DT_UNKNOWN").to_u8()?.into())
409            }
410        }
411    }
412
413    fn dir_entry_fields(
414        &self,
415        entry: Either<fs::DirEntry, &'static str>,
416    ) -> InterpResult<'tcx, DirEntry> {
417        let this = self.eval_context_ref();
418        interp_ok(match entry {
419            Either::Left(dir_entry) => {
420                DirEntry {
421                    name: dir_entry.file_name(),
422                    d_type: this.file_type_to_d_type(dir_entry.file_type())?,
423                    // If the host is a Unix system, fill in the inode number with its real value.
424                    // If not, use 0 as a fallback value.
425                    #[cfg(unix)]
426                    ino: std::os::unix::fs::DirEntryExt::ino(&dir_entry),
427                    #[cfg(not(unix))]
428                    ino: 0u64,
429                }
430            }
431            Either::Right(special) =>
432                DirEntry {
433                    name: special.into(),
434                    d_type: this.eval_libc("DT_DIR").to_u8()?.into(),
435                    ino: 0,
436                },
437        })
438    }
439
440    fn host_permissions_from_mode(&self, mode: u32) -> InterpResult<'tcx, fs::Permissions> {
441        cfg_select! {
442            unix => {
443                use std::os::unix::fs::PermissionsExt;
444                interp_ok(fs::Permissions::from_mode(mode))
445            }
446            _ => {
447                let _ = mode;
448                throw_unsup_format!("setting file permissions is only supported on Unix hosts")
449            }
450        }
451    }
452}
453
454impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
455pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
456    fn open(
457        &mut self,
458        dirfd: Option<&OpTy<'tcx>>,
459        path_raw: &OpTy<'tcx>,
460        flag: &OpTy<'tcx>,
461        varargs: Varargs<'tcx, '_>,
462    ) -> InterpResult<'tcx, Scalar> {
463        #[cfg(unix)]
464        use std::os::unix::fs::OpenOptionsExt;
465        #[cfg(windows)]
466        use std::os::windows::fs::OpenOptionsExt;
467
468        let this = self.eval_context_mut();
469        let open_name = if dirfd.is_some() { "openat" } else { "open" };
470
471        let dirfd = match dirfd {
472            Some(dirfd) => Some(this.read_scalar(dirfd)?.to_i32()?),
473            None => None,
474        };
475        let path_raw = this.read_pointer(path_raw)?;
476        let flag = this.read_scalar(flag)?.to_i32()?;
477
478        let path = this.read_path_from_c_str(path_raw)?;
479        // Files in `/proc` won't work properly.
480        if matches!(this.tcx.sess.target.os, Os::Linux | Os::Android | Os::Illumos | Os::Solaris)
481            && path::absolute(&path).is_ok_and(|path| path.starts_with("/proc"))
482        {
483            this.machine.emit_diagnostic(NonHaltingDiagnostic::FileInProcOpened);
484        }
485
486        // Helper for those flags that read the `mode` parameter.
487        let read_mode = |this: &MiriInterpCx<'tcx>,
488                         options: &mut OpenOptions,
489                         fn_name: &str|
490         -> InterpResult<'tcx> {
491            let ([mode], _) = this.check_varargs(
492                if this.libc_ty_layout("mode_t").size.bytes() >= 4 {
493                    // `mode_t` is big enough, no C integer promotion.
494                    shim_varargs![libc::mode_t]
495                } else {
496                    // Types smaller than int get promoted to int
497                    // (see https://github.com/rust-lang/rust/issues/71915).
498                    shim_varargs![i32]
499                },
500                varargs,
501                fn_name,
502            )?;
503            let mode = this.read_scalar(mode)?.to_u32()?;
504
505            cfg_select! {
506                unix => {
507                    // Support all modes on UNIX host
508                    options.mode(mode);
509                }
510                _ => {
511                    let _unused = options;
512
513                    // Only support default mode for non-UNIX (i.e. Windows) host
514                    if mode != 0o666 {
515                        throw_unsup_format!(
516                            "{open_name}: non-default mode 0o{:o} is not supported on non-Unix hosts",
517                            mode
518                        );
519                    }
520                }
521            }
522
523            interp_ok(())
524        };
525
526        // We will "subtract" supported flags from this and at the end check that no bits are left.
527        let mut flag = flag;
528
529        let mut options = OpenOptions::new();
530        let mut custom_flags = 0;
531
532        let o_rdonly = this.eval_libc_i32("O_RDONLY");
533        let o_wronly = this.eval_libc_i32("O_WRONLY");
534        let o_rdwr = this.eval_libc_i32("O_RDWR");
535        // The first two bits of the flag correspond to the access mode in linux, macOS and
536        // windows. We need to check that in fact the access mode flags for the current target
537        // only use these two bits, otherwise we are in an unsupported target and should error.
538        if (o_rdonly | o_wronly | o_rdwr) & !0b11 != 0 {
539            throw_unsup_format!("{open_name}: access mode flags on this target are unsupported");
540        }
541        let mut writable = true;
542        let mut readable = true;
543
544        // Now we check the access mode
545        let access_mode = flag & this.eval_libc_i32("O_ACCMODE");
546        flag &= !access_mode;
547
548        if access_mode == o_rdonly {
549            writable = false;
550            options.read(true);
551        } else if access_mode == o_wronly {
552            readable = false;
553            options.write(true);
554        } else if access_mode == o_rdwr {
555            options.read(true).write(true);
556        } else {
557            throw_unsup_format!("{open_name}: unsupported access mode {:#x}", access_mode);
558        }
559
560        if this.tcx.sess.target.os == Os::Linux {
561            let o_largefile = this.eval_libc_i32("O_LARGEFILE");
562            if flag & o_largefile == o_largefile {
563                flag &= !o_largefile;
564                // `fs::File` always opens in largefile mode so this is a NOP.
565            }
566            let o_tmpfile = this.eval_libc_i32("O_TMPFILE");
567            // Note that this overaps with O_DIRECTORY! So we have to check O_TMPFILE first.
568            if flag & o_tmpfile == o_tmpfile {
569                // This flag requires a mode.
570                read_mode(
571                    this,
572                    &mut options,
573                    if dirfd.is_some() {
574                        "openat(dirfd, pathname, O_TMPFILE, ...)"
575                    } else {
576                        "open(pathname, O_TMPFILE, ...)"
577                    },
578                )?;
579
580                // We don't currently support this, error gracefully.
581                return this.set_errno_and_return_neg1_i32(LibcError("EOPNOTSUPP"));
582            }
583        }
584        let o_directory = this.eval_libc_i32("O_DIRECTORY");
585        let mut want_directory = false;
586        if flag & o_directory == o_directory {
587            flag &= !o_directory;
588            want_directory = true;
589            // On Unix we can ask the host to only open directories. That's helpful especially in
590            // combination with O_NOFOLLOW as it affects which error we get when the final component
591            // is a symlink. Fixing the error up ourselves is non-trivial so we make the host
592            // generate the right error.
593            #[cfg(unix)]
594            {
595                custom_flags |= libc::O_DIRECTORY;
596            }
597        }
598        let o_append = this.eval_libc_i32("O_APPEND");
599        if flag & o_append == o_append {
600            flag &= !o_append;
601            options.append(true);
602        }
603        let o_trunc = this.eval_libc_i32("O_TRUNC");
604        if flag & o_trunc == o_trunc {
605            flag &= !o_trunc;
606            options.truncate(true);
607        }
608        let o_creat = this.eval_libc_i32("O_CREAT");
609        if flag & o_creat == o_creat {
610            flag &= !o_creat;
611            // Get the mode.
612            read_mode(
613                this,
614                &mut options,
615                if dirfd.is_some() {
616                    "openat(dirfd, pathname, O_CREAT, ...)"
617                } else {
618                    "open(pathname, O_CREAT, ...)"
619                },
620            )?;
621
622            if want_directory {
623                // O_CREAT + O_DIRECTORY is invalid.
624                return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
625            }
626
627            // O_EXCL is only allowed if O_CREAT is set.
628            let o_excl = this.eval_libc_i32("O_EXCL");
629            if flag & o_excl == o_excl {
630                flag &= !o_excl;
631                options.create_new(true);
632            } else {
633                options.create(true);
634            }
635        }
636        let o_cloexec = this.eval_libc_i32("O_CLOEXEC");
637        if flag & o_cloexec == o_cloexec {
638            flag &= !o_cloexec;
639            // We do not need to do anything for this flag because `std` already sets it.
640            // (Technically we do not support *not* setting this flag, but we ignore that.)
641        }
642        let o_nofollow = this.eval_libc_i32("O_NOFOLLOW");
643        let mut nofollow = false;
644        if flag & o_nofollow == o_nofollow {
645            flag &= !o_nofollow;
646            nofollow = true;
647            cfg_select! {
648                unix => {
649                    custom_flags |= libc::O_NOFOLLOW;
650                }
651                windows => {
652                    if dirfd.is_some() {
653                        throw_unsup_format!(
654                            "openat: `O_NOFOLLOW` is not supported on Windows hosts"
655                        );
656                    }
657                    custom_flags |=
658                        windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT;
659                }
660            }
661        }
662
663        // If `flag` has any bits left set, those are not supported.
664        if flag != 0 {
665            throw_unsup_format!("{open_name}: unsupported flags: {flag:#x}");
666        }
667
668        // Reject if isolation is enabled.
669        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
670            this.reject_in_isolation(&format!("`{open_name}`"), reject_with)?;
671            return this.set_errno_and_return_neg1_i32(ErrorKind::PermissionDenied);
672        }
673
674        let dirfd = match dirfd {
675            Some(dirfd) => {
676                if path.is_absolute() || dirfd == this.eval_libc_i32("AT_FDCWD") {
677                    None
678                } else {
679                    // relative to dirfd, which must be a directory handle
680                    let Some(fd) = this.machine.fds.get(dirfd) else {
681                        return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
682                    };
683                    let Some(dir) = fd.downcast::<DirHandle>() else {
684                        return this.set_errno_and_return_neg1_i32(LibcError("ENOTDIR"));
685                    };
686                    Some(dir)
687                }
688            }
689            None => None,
690        };
691
692        // Let's see what we get when we open this!
693        options.custom_flags(custom_flags);
694        // Windows does not by itself treat `.` correctly so we do that by hand.
695        // (https://github.com/rust-lang/rust/issues/163032)
696        let f_or_d = match &dirfd {
697            // Using `try_clone` here means that on the host, we have the same underlying file
698            // description, even when it would be separate natively. That should be fine since
699            // std never gives us an `fs::Dir` whose internal state (e.g. for iteration) matters.
700            Some(dirfd) if path.to_str() == Some(".") => dirfd.dir.try_clone().map(Either::Right),
701            _ => open_file_or_dir(dirfd.as_ref().map(|d| &d.dir), &path, &options),
702        };
703        match f_or_d {
704            Err(err) => this.set_errno_and_return_neg1_i32(err),
705            Ok(Either::Right(dir)) => {
706                // This means it cannot be a symlink, so `nofollow` is fine.
707                if writable {
708                    // On Windows, opening a folder writable can succeed.
709                    // But here we want it to always fail.
710                    return this.set_errno_and_return_neg1_i32(LibcError("EISDIR"));
711                }
712
713                let fd = this.machine.fds.insert_new(DirHandle {
714                    dir,
715                    #[cfg(bootstrap)]
716                    fallback: match &dirfd {
717                        Some(dirfd) => dirfd.fallback.join(&path).canonicalize().unwrap(),
718                        None => path.canonicalize().unwrap(),
719                    },
720                });
721                interp_ok(Scalar::from_i32(fd))
722            }
723            Ok(Either::Left(file)) => {
724                if want_directory {
725                    return this.set_errno_and_return_neg1_i32(LibcError("ENOTDIR"));
726                }
727                if file.metadata().unwrap().is_symlink() {
728                    if nofollow {
729                        // On Windows, FILE_FLAG_OPEN_REPARSE_POINT makes opening still succeed, it
730                        // just opens the symlink rather than the target. Turn that into an error.
731                        return this.set_errno_and_return_neg1_i32(LibcError("ELOOP"));
732                    }
733                    panic!("we should not get a symlink here");
734                }
735
736                let fd = this.machine.fds.insert_new(FileHandle { file, writable, readable });
737                interp_ok(Scalar::from_i32(fd))
738            }
739        }
740    }
741
742    fn lseek(
743        &mut self,
744        fd_num: i32,
745        offset: i128,
746        whence: i32,
747        dest: &MPlaceTy<'tcx>,
748    ) -> InterpResult<'tcx> {
749        let this = self.eval_context_mut();
750
751        // Isolation check is done via `FileDescription` trait.
752
753        let seek_from = if whence == this.eval_libc_i32("SEEK_SET") {
754            if offset < 0 {
755                // Negative offsets return `EINVAL`.
756                return this.set_errno_and_return_neg1(LibcError("EINVAL"), dest);
757            } else {
758                SeekFrom::Start(u64::try_from(offset).unwrap())
759            }
760        } else if whence == this.eval_libc_i32("SEEK_CUR") {
761            SeekFrom::Current(i64::try_from(offset).unwrap())
762        } else if whence == this.eval_libc_i32("SEEK_END") {
763            SeekFrom::End(i64::try_from(offset).unwrap())
764        } else {
765            return this.set_errno_and_return_neg1(LibcError("EINVAL"), dest);
766        };
767
768        let communicate = this.machine.communicate();
769
770        let Some(fd) = this.machine.fds.get(fd_num) else {
771            return this.set_errno_and_return_neg1(LibcError("EBADF"), dest);
772        };
773        let result = fd.seek(communicate, seek_from)?.map(|offset| i64::try_from(offset).unwrap());
774        drop(fd);
775
776        let result = this.try_unwrap_io_result(result)?;
777        this.write_int(result, dest)?;
778        interp_ok(())
779    }
780
781    fn unlink(&mut self, path_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
782        let this = self.eval_context_mut();
783
784        let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
785
786        // Reject if isolation is enabled.
787        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
788            this.reject_in_isolation("`unlink`", reject_with)?;
789            return this.set_errno_and_return_neg1_i32(ErrorKind::PermissionDenied);
790        }
791
792        #[rustfmt::skip] // work around https://github.com/rust-lang/rustfmt/issues/7045
793        let result = cfg_select! {
794            unix => fs::remove_file(path),
795            windows => {{
796                // This should be able to remove symlinks to dirs, but on Windows that requires
797                // `remove_dir`. Our work-around is racy but there's not a lot we can do about that.
798                // FIXME: maybe we can retry based on the error code?
799                use std::os::windows::fs::FileTypeExt;
800                let metadata = path.symlink_metadata();
801                if metadata.is_ok_and(|m| m.file_type().is_symlink_dir()) {
802                    fs::remove_dir(path)
803                } else {
804                    fs::remove_file(path)
805                }
806            }}
807        };
808        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result.map(|()| 0))?))
809    }
810
811    fn symlink(
812        &mut self,
813        target_op: &OpTy<'tcx>,
814        linkpath_op: &OpTy<'tcx>,
815    ) -> InterpResult<'tcx, Scalar> {
816        fn create_link(target: &Path, link: &Path) -> std::io::Result<()> {
817            cfg_select! {
818                unix => std::os::unix::fs::symlink(target, link),
819                windows => {
820                    use std::os::windows::fs;
821
822                    // The target filename is interpreted relative to where `link` is located,
823                    // but we need it relative to where we are.
824                    let target_is_dir = link.parent().is_some_and(|dir| dir.join(target).is_dir());
825                    // This is racy, but not much we can do about that.
826                    // FIXME: maybe we can retry based on the error code?
827                    if target_is_dir {
828                        fs::symlink_dir(target, link)
829                    } else {
830                        fs::symlink_file(target, link)
831                    }
832                }
833            }
834        }
835
836        let this = self.eval_context_mut();
837        let target = this.read_path_from_c_str(this.read_pointer(target_op)?)?;
838        let linkpath = this.read_path_from_c_str(this.read_pointer(linkpath_op)?)?;
839
840        // Reject if isolation is enabled.
841        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
842            this.reject_in_isolation("`symlink`", reject_with)?;
843            return this.set_errno_and_return_neg1_i32(ErrorKind::PermissionDenied);
844        }
845
846        let result = create_link(&target, &linkpath).map(|_| 0);
847        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
848    }
849
850    fn linkat(
851        &mut self,
852        oldfd_op: &OpTy<'tcx>,
853        oldpath_op: &OpTy<'tcx>,
854        newfd_op: &OpTy<'tcx>,
855        newpath_op: &OpTy<'tcx>,
856        flags_op: &OpTy<'tcx>,
857    ) -> InterpResult<'tcx, Scalar> {
858        let this = self.eval_context_mut();
859
860        // Load all arguments
861        let flags = this.read_scalar(flags_op)?.to_i32()?;
862        let oldfd = this.read_scalar(oldfd_op)?.to_i32()?;
863        let newfd = this.read_scalar(newfd_op)?.to_i32()?;
864        let oldpath_ptr = this.read_pointer(oldpath_op)?;
865        let newpath_ptr = this.read_pointer(newpath_op)?;
866
867        // Relevant libc constants
868        let at_fdcwd = this.eval_libc_i32("AT_FDCWD");
869
870        // Reject if isolation is enabled.
871        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
872            this.reject_in_isolation("`linkat`", reject_with)?;
873            return this.set_errno_and_return_neg1_i32(ErrorKind::PermissionDenied);
874        }
875
876        // Read flags - only support 0.
877        if flags != 0 {
878            throw_unsup_format!("unsupported linkat flags {:#x}", flags);
879        }
880
881        // Resolve oldpath
882        if oldfd != at_fdcwd {
883            throw_unsup_format!("linkat with `olddirfd` not equal to `AT_FDCWD` is not supported");
884        }
885        if this.ptr_is_null(oldpath_ptr)? {
886            return this.set_errno_and_return_neg1_i32(LibcError("EFAULT"));
887        }
888        let oldpath = this.read_path_from_c_str(oldpath_ptr)?.into_owned();
889
890        // Resolve newpath
891        if newfd != at_fdcwd {
892            throw_unsup_format!("linkat with `newdirfd` not equal to `AT_FDCWD` is not supported");
893        }
894        if this.ptr_is_null(newpath_ptr)? {
895            return this.set_errno_and_return_neg1_i32(LibcError("EFAULT"));
896        }
897        let newpath = this.read_path_from_c_str(newpath_ptr)?.into_owned();
898
899        let result = fs::hard_link(&oldpath, &newpath).map(|()| 0);
900        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
901    }
902
903    fn stat(&mut self, path_op: &OpTy<'tcx>, buf_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
904        let this = self.eval_context_mut();
905
906        if !matches!(
907            &this.tcx.sess.target.os,
908            Os::MacOs | Os::FreeBsd | Os::Solaris | Os::Illumos | Os::Android | Os::Linux
909        ) {
910            panic!("`stat` should not be called on {}", this.tcx.sess.target.os);
911        }
912
913        let path_scalar = this.read_pointer(path_op)?;
914        let path = this.read_path_from_c_str(path_scalar)?.into_owned();
915
916        // Reject if isolation is enabled.
917        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
918            this.reject_in_isolation("`stat`", reject_with)?;
919            return this.set_errno_and_return_neg1_i32(LibcError("EACCES"));
920        }
921
922        let metadata = match FileMetadata::from_host(this, fs::metadata(path))? {
923            Ok(metadata) => metadata,
924            Err(err) => return this.set_errno_and_return_neg1_i32(err),
925        };
926
927        interp_ok(Scalar::from_i32(this.write_stat_buf(metadata, buf_op, "stat")?))
928    }
929
930    // `lstat` is used to get symlink metadata.
931    fn lstat(&mut self, path_op: &OpTy<'tcx>, buf_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
932        let this = self.eval_context_mut();
933
934        if !matches!(
935            &this.tcx.sess.target.os,
936            Os::MacOs | Os::FreeBsd | Os::Solaris | Os::Illumos | Os::Android | Os::Linux
937        ) {
938            panic!("`lstat` should not be called on {}", this.tcx.sess.target.os);
939        }
940
941        let path_scalar = this.read_pointer(path_op)?;
942        let path = this.read_path_from_c_str(path_scalar)?.into_owned();
943
944        // Reject if isolation is enabled.
945        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
946            this.reject_in_isolation("`lstat`", reject_with)?;
947            return this.set_errno_and_return_neg1_i32(LibcError("EACCES"));
948        }
949
950        let metadata = match FileMetadata::from_host(this, fs::symlink_metadata(path))? {
951            Ok(metadata) => metadata,
952            Err(err) => return this.set_errno_and_return_neg1_i32(err),
953        };
954
955        interp_ok(Scalar::from_i32(this.write_stat_buf(metadata, buf_op, "stat")?))
956    }
957
958    fn fstat(
959        &mut self,
960        fd_op: &OpTy<'tcx>,
961        buf_op: &OpTy<'tcx>,
962        buf_type: &str,
963    ) -> InterpResult<'tcx, Scalar> {
964        let this = self.eval_context_mut();
965
966        if !matches!(
967            &this.tcx.sess.target.os,
968            Os::MacOs | Os::FreeBsd | Os::Solaris | Os::Illumos | Os::Linux | Os::Android
969        ) {
970            panic!("`fstat` should not be called on {}", this.tcx.sess.target.os);
971        }
972
973        let fd = this.read_scalar(fd_op)?.to_i32()?;
974
975        // Reject if isolation is enabled.
976        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
977            this.reject_in_isolation("`fstat`", reject_with)?;
978            // Set error code as "EBADF" (bad fd); "EACCESS" does not make much sense here.
979            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
980        }
981
982        let metadata = match FileMetadata::from_fd_num(this, fd)? {
983            Ok(metadata) => metadata,
984            Err(err) => return this.set_errno_and_return_neg1_i32(err),
985        };
986        interp_ok(Scalar::from_i32(this.write_stat_buf(metadata, buf_op, buf_type)?))
987    }
988
989    fn fstatat(
990        &mut self,
991        dirfd_op: &OpTy<'tcx>,
992        path_op: &OpTy<'tcx>,
993        buf_op: &OpTy<'tcx>,
994        flags_op: &OpTy<'tcx>,
995    ) -> InterpResult<'tcx, Scalar> {
996        let this = self.eval_context_mut();
997
998        if !matches!(
999            &this.tcx.sess.target.os,
1000            Os::MacOs | Os::FreeBsd | Os::Solaris | Os::Illumos | Os::Linux | Os::Android
1001        ) {
1002            panic!("`fstatat` should not be called on {}", this.tcx.sess.target.os);
1003        }
1004
1005        let dirfd = this.read_scalar(dirfd_op)?.to_i32()?;
1006        let path = this.read_pointer(path_op)?;
1007        let flags = this.read_scalar(flags_op)?.to_i32()?;
1008
1009        // Reject if isolation is enabled.
1010        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1011            this.reject_in_isolation("`fstatat`", reject_with)?;
1012            return this.set_errno_and_return_neg1_i32(LibcError("EACCES"));
1013        }
1014
1015        // Parse flags.
1016        let mut flags = flags;
1017        // AT_SYMLINK_NOFOLLOW
1018        let at_symlink_nofollow = this.eval_libc_i32("AT_SYMLINK_NOFOLLOW");
1019        let symlink_nofollow_flag = flags & at_symlink_nofollow == at_symlink_nofollow;
1020        flags &= !at_symlink_nofollow;
1021        // Complain about unknown flags.
1022        if flags != 0 {
1023            throw_unsup_format!("unsupported flags for `fstatat`: {flags:#x}")
1024        }
1025
1026        let path = this.read_path_from_c_str(path)?.into_owned();
1027        // Resolve dirfd + path to metadata.
1028        let metadata = FileMetadata::at(
1029            this,
1030            dirfd,
1031            &path,
1032            symlink_nofollow_flag,
1033            /* empty_path_flag */ false,
1034        )?;
1035
1036        let metadata = match metadata {
1037            Ok(metadata) => metadata,
1038            Err(err) => return this.set_errno_and_return_neg1_i32(err),
1039        };
1040        interp_ok(Scalar::from_i32(this.write_stat_buf(metadata, buf_op, "stat")?))
1041    }
1042
1043    fn linux_statx(
1044        &mut self,
1045        dirfd_op: &OpTy<'tcx>,    // Should be an `int`
1046        pathname_op: &OpTy<'tcx>, // Should be a `const char *`
1047        flags_op: &OpTy<'tcx>,    // Should be an `int`
1048        mask_op: &OpTy<'tcx>,     // Should be an `unsigned int`
1049        statxbuf_op: &OpTy<'tcx>, // Should be a `struct statx *`
1050    ) -> InterpResult<'tcx, Scalar> {
1051        let this = self.eval_context_mut();
1052
1053        this.assert_target_os(Os::Linux, "statx");
1054
1055        let dirfd = this.read_scalar(dirfd_op)?.to_i32()?;
1056        let pathname_ptr = this.read_pointer(pathname_op)?;
1057        let flags = this.read_scalar(flags_op)?.to_i32()?;
1058        let _mask = this.read_scalar(mask_op)?.to_u32()?;
1059        let statxbuf_ptr = this.read_pointer(statxbuf_op)?;
1060
1061        // If the statxbuf or pathname pointers are null, the function fails with `EFAULT`.
1062        if this.ptr_is_null(statxbuf_ptr)? || this.ptr_is_null(pathname_ptr)? {
1063            return this.set_errno_and_return_neg1_i32(LibcError("EFAULT"));
1064        }
1065
1066        let statxbuf = this.deref_pointer_as(statxbuf_op, this.libc_ty_layout("statx"))?;
1067        let path = this.read_path_from_c_str(pathname_ptr)?.into_owned();
1068
1069        // Parse flags.
1070        let mut flags = flags;
1071        // AT_EMPTY_PATH
1072        let at_empty_path = this.eval_libc_i32("AT_EMPTY_PATH");
1073        let empty_path_flag = flags & at_empty_path == at_empty_path;
1074        flags &= !at_empty_path;
1075        // AT_SYMLINK_NOFOLLOW
1076        let at_symlink_nofollow = this.eval_libc_i32("AT_SYMLINK_NOFOLLOW");
1077        let symlink_nofollow_flag = flags & at_symlink_nofollow == at_symlink_nofollow;
1078        flags &= !at_symlink_nofollow;
1079        // Complain about unknown flags.
1080        if flags != 0 {
1081            throw_unsup_format!("unsupported flags for `statx`: {flags:#x}")
1082        }
1083
1084        // Reject if isolation is enabled.
1085        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1086            this.reject_in_isolation("`statx`", reject_with)?;
1087            return this.set_errno_and_return_neg1_i32(LibcError("EACCES"));
1088        }
1089
1090        // Resolve dirfd + path to metadata.
1091        let metadata =
1092            FileMetadata::at(this, dirfd, &path, symlink_nofollow_flag, empty_path_flag)?;
1093
1094        let metadata = match metadata {
1095            Ok(metadata) => metadata,
1096            Err(err) => return this.set_errno_and_return_neg1_i32(err),
1097        };
1098
1099        // The `_mask_op` parameter specifies the file information that the caller requested.
1100        // However, `statx` is allowed to return information that was not requested or to not
1101        // return information that was requested. This `mask` represents the information we can
1102        // actually provide for any target.
1103        let mut mask = this.eval_libc_u32("STATX_TYPE")
1104            | this.eval_libc_u32("STATX_MODE")
1105            | this.eval_libc_u32("STATX_SIZE");
1106
1107        // Check which pieces of metadata we acquired, and set the appropriate flags in the mask.
1108        if metadata.ino.is_some() {
1109            mask |= this.eval_libc_u32("STATX_INO");
1110        }
1111        if metadata.nlink.is_some() {
1112            mask |= this.eval_libc_u32("STATX_NLINK");
1113        }
1114        if metadata.uid.is_some() {
1115            mask |= this.eval_libc_u32("STATX_UID");
1116        }
1117        if metadata.gid.is_some() {
1118            mask |= this.eval_libc_u32("STATX_GID");
1119        }
1120        if metadata.blocks.is_some() {
1121            mask |= this.eval_libc_u32("STATX_BLOCKS");
1122        }
1123
1124        // We need to set the corresponding bits of `mask` if the access, creation and modification
1125        // times were available. Otherwise we let them be zero.
1126        let (access_sec, access_nsec) = metadata
1127            .accessed
1128            .map(|tup| {
1129                mask |= this.eval_libc_u32("STATX_ATIME");
1130                interp_ok(tup)
1131            })
1132            .unwrap_or_else(|| interp_ok((0, 0)))?;
1133
1134        let (created_sec, created_nsec) = metadata
1135            .created
1136            .map(|tup| {
1137                mask |= this.eval_libc_u32("STATX_BTIME");
1138                interp_ok(tup)
1139            })
1140            .unwrap_or_else(|| interp_ok((0, 0)))?;
1141
1142        let (modified_sec, modified_nsec) = metadata
1143            .modified
1144            .map(|tup| {
1145                mask |= this.eval_libc_u32("STATX_MTIME");
1146                interp_ok(tup)
1147            })
1148            .unwrap_or_else(|| interp_ok((0, 0)))?;
1149
1150        // Now we write everything to `statxbuf`. We write a zero for the unavailable fields.
1151        this.write_int_fields_named(
1152            &[
1153                ("stx_mask", mask.into()),
1154                ("stx_mode", metadata.mode.into()),
1155                ("stx_blksize", metadata.blksize.unwrap_or(0).into()),
1156                ("stx_attributes", 0),
1157                ("stx_nlink", metadata.nlink.unwrap_or(0).into()),
1158                ("stx_uid", metadata.uid.unwrap_or(0).into()),
1159                ("stx_gid", metadata.gid.unwrap_or(0).into()),
1160                ("stx_ino", metadata.ino.unwrap_or(0).into()),
1161                ("stx_size", metadata.size.into()),
1162                ("stx_blocks", metadata.blocks.unwrap_or(0).into()),
1163                ("stx_attributes_mask", 0),
1164                ("stx_rdev_major", 0),
1165                ("stx_rdev_minor", 0),
1166                ("stx_dev_major", 0),
1167                ("stx_dev_minor", 0),
1168            ],
1169            &statxbuf,
1170        )?;
1171        #[rustfmt::skip]
1172        this.write_int_fields_named(
1173            &[
1174                ("tv_sec", access_sec.into()),
1175                ("tv_nsec", access_nsec.into()),
1176            ],
1177            &this.project_field_named(&statxbuf, "stx_atime")?,
1178        )?;
1179        #[rustfmt::skip]
1180        this.write_int_fields_named(
1181            &[
1182                ("tv_sec", created_sec.into()),
1183                ("tv_nsec", created_nsec.into()),
1184            ],
1185            &this.project_field_named(&statxbuf, "stx_btime")?,
1186        )?;
1187        #[rustfmt::skip]
1188        this.write_int_fields_named(
1189            &[
1190                ("tv_sec", 0.into()),
1191                ("tv_nsec", 0.into()),
1192            ],
1193            &this.project_field_named(&statxbuf, "stx_ctime")?,
1194        )?;
1195        #[rustfmt::skip]
1196        this.write_int_fields_named(
1197            &[
1198                ("tv_sec", modified_sec.into()),
1199                ("tv_nsec", modified_nsec.into()),
1200            ],
1201            &this.project_field_named(&statxbuf, "stx_mtime")?,
1202        )?;
1203
1204        interp_ok(Scalar::from_i32(0))
1205    }
1206
1207    fn chmod(&mut self, path_op: &OpTy<'tcx>, mode_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1208        let this = self.eval_context_mut();
1209
1210        let path_ptr = this.read_pointer(path_op)?;
1211        let mode = this.read_scalar(mode_op)?.to_uint(this.libc_ty_layout("mode_t").size)?;
1212
1213        if this.ptr_is_null(path_ptr)? {
1214            return this.set_errno_and_return_neg1_i32(LibcError("EFAULT"));
1215        }
1216        let path = this.read_path_from_c_str(path_ptr)?;
1217
1218        // Reject if isolation is enabled.
1219        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1220            this.reject_in_isolation("`chmod`", reject_with)?;
1221            return this.set_errno_and_return_neg1_i32(LibcError("EACCES"));
1222        }
1223
1224        let permissions = this.host_permissions_from_mode(mode.try_into().unwrap())?;
1225        if let Err(err) = fs::set_permissions(path, permissions) {
1226            return this.set_errno_and_return_neg1_i32(err);
1227        }
1228
1229        interp_ok(Scalar::from_i32(0))
1230    }
1231
1232    fn fchmod(&mut self, fd_op: &OpTy<'tcx>, mode_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1233        let this = self.eval_context_mut();
1234
1235        let fd_num = this.read_scalar(fd_op)?.to_i32()?;
1236        let mode = this.read_scalar(mode_op)?.to_uint(this.libc_ty_layout("mode_t").size)?;
1237
1238        let Some(fd) = this.machine.fds.get(fd_num) else {
1239            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1240        };
1241        let Some(file) = fd.downcast::<FileHandle>() else {
1242            // The docs don't talk about what happens for non-regular files...
1243            throw_unsup_format!("`fchmod` is only supported on regular files")
1244        };
1245        if !file.writable && !file.readable {
1246            // Apparently, `fchmod` on a read-only file is fine. But let's not allow it on a
1247            // path-only file.
1248            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1249        }
1250        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1251
1252        let permissions = this.host_permissions_from_mode(mode.try_into().unwrap())?;
1253        if let Err(err) = file.file.set_permissions(permissions) {
1254            return this.set_errno_and_return_neg1_i32(err);
1255        }
1256
1257        interp_ok(Scalar::from_i32(0))
1258    }
1259
1260    fn rename(
1261        &mut self,
1262        oldpath_op: &OpTy<'tcx>,
1263        newpath_op: &OpTy<'tcx>,
1264    ) -> InterpResult<'tcx, Scalar> {
1265        let this = self.eval_context_mut();
1266
1267        let oldpath_ptr = this.read_pointer(oldpath_op)?;
1268        let newpath_ptr = this.read_pointer(newpath_op)?;
1269
1270        if this.ptr_is_null(oldpath_ptr)? || this.ptr_is_null(newpath_ptr)? {
1271            return this.set_errno_and_return_neg1_i32(LibcError("EFAULT"));
1272        }
1273
1274        let oldpath = this.read_path_from_c_str(oldpath_ptr)?;
1275        let newpath = this.read_path_from_c_str(newpath_ptr)?;
1276
1277        // Reject if isolation is enabled.
1278        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1279            this.reject_in_isolation("`rename`", reject_with)?;
1280            return this.set_errno_and_return_neg1_i32(ErrorKind::PermissionDenied);
1281        }
1282
1283        let result = fs::rename(oldpath, newpath).map(|_| 0);
1284
1285        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
1286    }
1287
1288    fn mkdir(&mut self, path_op: &OpTy<'tcx>, mode_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1289        let this = self.eval_context_mut();
1290
1291        #[cfg_attr(not(unix), allow(unused_variables))]
1292        let mode = if matches!(&this.tcx.sess.target.os, Os::MacOs | Os::FreeBsd) {
1293            u32::from(this.read_scalar(mode_op)?.to_u16()?)
1294        } else {
1295            this.read_scalar(mode_op)?.to_u32()?
1296        };
1297
1298        let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
1299
1300        // Reject if isolation is enabled.
1301        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1302            this.reject_in_isolation("`mkdir`", reject_with)?;
1303            return this.set_errno_and_return_neg1_i32(ErrorKind::PermissionDenied);
1304        }
1305
1306        #[cfg_attr(not(unix), allow(unused_mut))]
1307        let mut builder = DirBuilder::new();
1308
1309        // If the host supports it, forward on the mode of the directory
1310        // (i.e. permission bits and the sticky bit)
1311        #[cfg(unix)]
1312        {
1313            use std::os::unix::fs::DirBuilderExt;
1314            builder.mode(mode);
1315        }
1316
1317        let result = builder.create(path).map(|_| 0i32);
1318
1319        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
1320    }
1321
1322    fn rmdir(&mut self, path_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1323        let this = self.eval_context_mut();
1324
1325        let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
1326
1327        // Reject if isolation is enabled.
1328        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1329            this.reject_in_isolation("`rmdir`", reject_with)?;
1330            return this.set_errno_and_return_neg1_i32(ErrorKind::PermissionDenied);
1331        }
1332
1333        #[rustfmt::skip] // work around https://github.com/rust-lang/rustfmt/issues/7045
1334        let result = cfg_select! {
1335            unix => fs::remove_dir(path),
1336            windows => {{
1337                // This should *not* be able to remove symlinks to dirs, but on Windows it is. Our
1338                // work-around is racy (it might change to being a directory symlink after we
1339                // checked it), but there's not a lot we can do about that.
1340                use std::os::windows::fs::FileTypeExt;
1341                let metadata = path.symlink_metadata();
1342                if metadata.is_ok_and(|m| m.file_type().is_symlink_dir()) {
1343                    return this.set_errno_and_return_neg1_i32(LibcError("ENOTDIR"));
1344                } else {
1345                    fs::remove_dir(path)
1346                }
1347            }}
1348        };
1349
1350        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result.map(|_| 0i32))?))
1351    }
1352
1353    fn opendir(&mut self, name_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1354        let this = self.eval_context_mut();
1355
1356        let name = this.read_path_from_c_str(this.read_pointer(name_op)?)?;
1357
1358        // Reject if isolation is enabled.
1359        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1360            this.reject_in_isolation("`opendir`", reject_with)?;
1361            this.set_last_error(LibcError("EACCES"))?;
1362            return interp_ok(Scalar::null_ptr(this));
1363        }
1364
1365        let result = fs::read_dir(&name);
1366
1367        match result {
1368            Ok(read_dir) => {
1369                // Also open the same directory as a directory handle, so we have
1370                // an underlying FD.
1371                // FIXME(https://github.com/rust-lang/miri/issues/5326): This is racy! We can't even
1372                // verify whether there was a race. We just trust that the directory did not change
1373                // in between above and here. One day, the standard library will support converting
1374                // between `Dir` and `ReadDir` (one of the two directions would suffice for our
1375                // needs), then we'll use that.
1376                let Ok(dir) = fs::Dir::open(&name) else {
1377                    throw_unsup_format!(
1378                        "cannot `opendir` this directory: failed to create directory handle"
1379                    );
1380                };
1381                let dir = this.machine.fds.new_ref(DirHandle {
1382                    dir,
1383                    #[cfg(bootstrap)]
1384                    fallback: name.canonicalize().unwrap(),
1385                });
1386                let dir_fd_id = dir.id();
1387                let dir_fd_num = this.machine.fds.insert(dir);
1388
1389                let stream = DirStream::new(read_dir, dir_fd_num, dir_fd_id);
1390                let id = this.machine.dirs.insert_new(stream);
1391
1392                // The libc API for opendir says that this method returns a pointer to an opaque
1393                // structure, but we are returning an ID number. Thus, pass it as a scalar of
1394                // pointer width. This also conveniently means that any attempt to access
1395                // memory via this pointer raises UB.
1396                interp_ok(Scalar::from_target_usize(id, this))
1397            }
1398            Err(e) => {
1399                this.set_last_error(e)?;
1400                interp_ok(Scalar::null_ptr(this))
1401            }
1402        }
1403    }
1404
1405    fn readdir(
1406        &mut self,
1407        dirp_op: &OpTy<'tcx>,
1408        dest: &MPlaceTy<'tcx>,
1409        buf_type: &'static str,
1410    ) -> InterpResult<'tcx> {
1411        let this = self.eval_context_mut();
1412
1413        if !matches!(
1414            &this.tcx.sess.target.os,
1415            Os::Linux | Os::Android | Os::Solaris | Os::Illumos | Os::FreeBsd | Os::MacOs
1416        ) {
1417            throw_unsup_format!("`readdir` is not yet supported on {}", this.tcx.sess.target.os);
1418        }
1419
1420        let dirp = this.read_target_usize(dirp_op)?;
1421
1422        // Reject if isolation is enabled.
1423        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1424            this.reject_in_isolation("`readdir`", reject_with)?;
1425            this.set_last_error(LibcError("EBADF"))?;
1426            this.write_null(dest)?;
1427            return interp_ok(());
1428        }
1429
1430        let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| {
1431            err_ub_format!("the DIR pointer passed to `readdir` did not come from opendir")
1432        })?;
1433
1434        // Check that the backing FD is still valid.
1435        if this.machine.fds.get(open_dir.fd_num).is_none_or(|fd| fd.id() != open_dir.fd_id) {
1436            throw_ub_format!("the backing FD for this DIR stream has been tampered with");
1437        }
1438
1439        let entry = match open_dir.next_host_entry() {
1440            Some(Ok(dir_entry)) => {
1441                let dir_entry = this.dir_entry_fields(dir_entry)?;
1442
1443                // Write the directory entry into a newly allocated buffer.
1444                // The name is written with write_bytes, while the rest of the
1445                // dirent64 (or dirent) struct is written using write_int_fields.
1446
1447                // For reference:
1448                // On Linux:
1449                // pub struct dirent64 {
1450                //     pub d_ino: ino64_t,
1451                //     pub d_off: off64_t,
1452                //     pub d_reclen: c_ushort,
1453                //     pub d_type: c_uchar,
1454                //     pub d_name: [c_char; 256],
1455                // }
1456                //
1457                // On Solaris:
1458                // pub struct dirent {
1459                //     pub d_ino: ino64_t,
1460                //     pub d_off: off64_t,
1461                //     pub d_reclen: c_ushort,
1462                //     pub d_name: [c_char; 3],
1463                // }
1464                //
1465                // On FreeBSD:
1466                // pub struct dirent {
1467                //     pub d_fileno: uint32_t,
1468                //     pub d_reclen: uint16_t,
1469                //     pub d_type: uint8_t,
1470                //     pub d_namlen: uint8_t,
1471                //     pub d_name: [c_char; 256],
1472                // }
1473                //
1474                // On macOS:
1475                // pub struct dirent {
1476                //     pub d_ino: u64,
1477                //     pub d_seekoff: u64,
1478                //     pub d_reclen: u16,
1479                //     pub d_namlen: u16,
1480                //     pub d_type: u8,
1481                //     pub d_name: [c_char; 1024],
1482                // }
1483
1484                let dirent_layout = this.libc_ty_layout(buf_type);
1485                let fields = &dirent_layout.fields;
1486                let d_name_offset = fields.offset(fields.count().strict_sub(1)).bytes();
1487
1488                // Determine the size of the buffer we have to allocate.
1489                let mut name = dir_entry.name; // not a Path as there are no separators!
1490                name.push("\0"); // Add a NUL terminator
1491                let name_bytes = name.as_encoded_bytes();
1492                let name_len = u64::try_from(name_bytes.len()).unwrap();
1493                let size = d_name_offset.strict_add(name_len);
1494
1495                let entry = this.allocate_ptr(
1496                    Size::from_bytes(size),
1497                    dirent_layout.align.abi,
1498                    MiriMemoryKind::Runtime.into(),
1499                    AllocInit::Uninit,
1500                )?;
1501                let entry = this.ptr_to_mplace(entry.into(), dirent_layout);
1502
1503                // Write the name.
1504                // The name is not a normal field, we already computed the offset above.
1505                let name_ptr = entry.ptr().wrapping_offset(Size::from_bytes(d_name_offset), this);
1506                this.write_bytes_ptr(name_ptr, name_bytes.iter().copied())?;
1507
1508                // Write common fields.
1509                let ino_name =
1510                    if this.tcx.sess.target.os == Os::FreeBsd { "d_fileno" } else { "d_ino" };
1511                this.write_int_fields_named(
1512                    &[(ino_name, dir_entry.ino.into()), ("d_reclen", size.into())],
1513                    &entry,
1514                )?;
1515
1516                // Write "optional" fields.
1517                if let Some(d_off) = this.try_project_field_named(&entry, "d_off")? {
1518                    this.write_null(&d_off)?;
1519                }
1520                if let Some(d_seekoff) = this.try_project_field_named(&entry, "d_seekoff")? {
1521                    this.write_null(&d_seekoff)?;
1522                }
1523                if let Some(d_namlen) = this.try_project_field_named(&entry, "d_namlen")? {
1524                    this.write_int(name_len.strict_sub(1), &d_namlen)?;
1525                }
1526                if let Some(d_type) = this.try_project_field_named(&entry, "d_type")? {
1527                    this.write_int(dir_entry.d_type, &d_type)?;
1528                }
1529
1530                Some(entry.ptr())
1531            }
1532            None => {
1533                // end of stream: return NULL
1534                None
1535            }
1536            Some(Err(e)) => {
1537                this.set_last_error(e)?;
1538                None
1539            }
1540        };
1541
1542        let open_dir = this.machine.dirs.streams.get_mut(&dirp).unwrap();
1543        if let Some((old_entry, old_type)) =
1544            std::mem::replace(&mut open_dir.entry, entry.map(|e| (e, buf_type)))
1545        {
1546            if old_type != buf_type {
1547                throw_ub_format!("mixing different `readdir` variants on the same `DIR` instance");
1548            }
1549            this.deallocate_ptr(old_entry, None, MiriMemoryKind::Runtime.into())?;
1550        }
1551
1552        this.write_pointer(entry.unwrap_or_else(Pointer::null), dest)?;
1553        interp_ok(())
1554    }
1555
1556    fn dirfd(&mut self, dirp_op: &OpTy<'tcx>, dest: &MPlaceTy<'tcx>) -> InterpResult<'tcx> {
1557        let this = self.eval_context_mut();
1558
1559        let dirp = this.read_target_usize(dirp_op)?;
1560
1561        let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| {
1562            err_ub_format!("the DIR pointer passed to `readdir` did not come from opendir")
1563        })?;
1564
1565        // Check that the backing FD is still valid.
1566        if this.machine.fds.get(open_dir.fd_num).is_none_or(|fd| fd.id() != open_dir.fd_id) {
1567            throw_ub_format!("the backing FD for this DIR stream has been tampered with");
1568        }
1569
1570        let fd_num = open_dir.fd_num;
1571        this.write_int(fd_num, dest)?;
1572        interp_ok(())
1573    }
1574
1575    fn closedir(&mut self, dirp_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1576        let this = self.eval_context_mut();
1577
1578        let dirp = this.read_target_usize(dirp_op)?;
1579
1580        // Reject if isolation is enabled.
1581        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1582            this.reject_in_isolation("`closedir`", reject_with)?;
1583            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1584        }
1585
1586        let mut open_dir = this.machine.dirs.streams.remove(&dirp).ok_or_else(|| {
1587            err_ub_format!("the DIR pointer passed to `closedir` did not come from opendir")
1588        })?;
1589
1590        // Check that the backing FD is still valid.
1591        if this.machine.fds.get(open_dir.fd_num).is_none_or(|fd| fd.id() != open_dir.fd_id) {
1592            throw_ub_format!("the backing FD for this DIR stream has been tampered with");
1593        }
1594        // And close it.
1595        this.close(open_dir.fd_num)?;
1596
1597        if let Some((entry, _type)) = open_dir.entry.take() {
1598            this.deallocate_ptr(entry, None, MiriMemoryKind::Runtime.into())?;
1599        }
1600        // We drop the `open_dir`, which will close the host dir handle.
1601        drop(open_dir);
1602
1603        interp_ok(Scalar::from_i32(0))
1604    }
1605
1606    fn ftruncate64(&mut self, fd_num: i32, length: i128) -> InterpResult<'tcx, Scalar> {
1607        let this = self.eval_context_mut();
1608
1609        let Some(fd) = this.machine.fds.get(fd_num) else {
1610            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1611        };
1612        let Some(file) = fd.downcast::<FileHandle>() else {
1613            // The docs say that EINVAL is returned when the FD "does not reference a regular file
1614            // or a POSIX shared memory object" (and we don't support shmem objects).
1615            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1616        };
1617        if !file.writable {
1618            // man page says "EBADF or EINVAL", Linux seems to use EINVAL.
1619            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1620        }
1621        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1622
1623        if let Ok(length) = length.try_into() {
1624            let result = file.file.set_len(length);
1625            let result = this.try_unwrap_io_result(result.map(|_| 0i32))?;
1626            interp_ok(Scalar::from_i32(result))
1627        } else {
1628            this.set_errno_and_return_neg1_i32(LibcError("EINVAL"))
1629        }
1630    }
1631
1632    /// NOTE: According to the man page of `possix_fallocate`, it returns the error code instead
1633    /// of setting `errno`.
1634    fn posix_fallocate(
1635        &mut self,
1636        fd_num: i32,
1637        offset: i64,
1638        len: i64,
1639    ) -> InterpResult<'tcx, Scalar> {
1640        let this = self.eval_context_mut();
1641
1642        // Reject if isolation is enabled.
1643        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1644            this.reject_in_isolation("`posix_fallocate`", reject_with)?;
1645            // Return error code "EBADF" (bad fd).
1646            return interp_ok(this.eval_libc("EBADF"));
1647        }
1648
1649        match this.fallocate_impl(fd_num, offset, len)? {
1650            Ok(()) => interp_ok(Scalar::from_i32(0)),
1651            Err(e) => this.io_error_to_errnum(e),
1652        }
1653    }
1654
1655    fn linux_fallocate(
1656        &mut self,
1657        fd: i32,
1658        mode: i32,
1659        offset: i64,
1660        size: i64,
1661    ) -> InterpResult<'tcx, Scalar> {
1662        // This is mostly a copy of `posix_fallocate` except that errors are returned via errno.
1663        let this = self.eval_context_mut();
1664
1665        // Reject if isolation is enabled.
1666        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1667            this.reject_in_isolation("`fallocate`", reject_with)?;
1668            // Set error code "EBADF" (bad fd).
1669            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1670        }
1671
1672        // We only support `fallocate` as a replacement for `posix_fallocate` on linux,
1673        // so a non-default `mode` is not supported.
1674        if mode != 0 {
1675            throw_unsup_format!("unsupported flags for `fallocate` in `mode` argument: {mode:#x}")
1676        }
1677
1678        match this.fallocate_impl(fd, offset, size)? {
1679            Ok(()) => interp_ok(Scalar::from_i32(0)),
1680            Err(e) => this.set_errno_and_return_neg1_i32(e),
1681        }
1682    }
1683
1684    /// Shared logic between `posix_fallocate` and `linux_fallocate`.
1685    fn fallocate_impl(
1686        &mut self,
1687        fd_num: i32,
1688        offset: i64,
1689        len: i64,
1690    ) -> InterpResult<'tcx, Result<(), IoError>> {
1691        let this = self.eval_context_mut();
1692
1693        // EINVAL is returned/set when: "offset was less than 0, or len was less than or equal to 0".
1694        if offset < 0 || len <= 0 {
1695            return interp_ok(Err(LibcError("EINVAL")));
1696        }
1697
1698        let Some(fd) = this.machine.fds.get(fd_num) else {
1699            return interp_ok(Err(LibcError("EBADF")));
1700        };
1701        let Some(file) = fd.downcast::<FileHandle>() else {
1702            // Man page specifies to return ENODEV if `fd` is not a regular file.
1703            return interp_ok(Err(LibcError("ENODEV")));
1704        };
1705
1706        if !file.writable {
1707            return interp_ok(Err(LibcError("EBADF")));
1708        }
1709
1710        let current_size = match file.file.metadata() {
1711            Ok(metadata) => metadata.len(),
1712            Err(err) => return interp_ok(Err(err.into())),
1713        };
1714
1715        // Checked i64 addition, to ensure the result does not exceed the max file size.
1716        let new_size = match offset.checked_add(len) {
1717            // `new_size` is definitely non-negative, so we can cast to `u64`.
1718            Some(new_size) => u64::try_from(new_size).unwrap(),
1719            None => return interp_ok(Err(LibcError("EFBIG"))), // new size too big
1720        };
1721
1722        // If the size of the file is less than offset+size, then the file is increased to this
1723        // size; otherwise the file size is left unchanged.
1724        if current_size < new_size {
1725            match file.file.set_len(new_size) {
1726                Ok(()) => interp_ok(Ok(())),
1727                Err(err) => interp_ok(Err(err.into())),
1728            }
1729        } else {
1730            interp_ok(Ok(()))
1731        }
1732    }
1733
1734    fn fsync(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1735        // On macOS, `fsync` (unlike `fcntl(F_FULLFSYNC)`) does not wait for the
1736        // underlying disk to finish writing. In the interest of host compatibility,
1737        // we conservatively implement this with `sync_all`, which
1738        // *does* wait for the disk.
1739
1740        let this = self.eval_context_mut();
1741
1742        let fd = this.read_scalar(fd_op)?.to_i32()?;
1743
1744        self.ffullsync_fd(fd)
1745    }
1746
1747    fn ffullsync_fd(&mut self, fd_num: i32) -> InterpResult<'tcx, Scalar> {
1748        let this = self.eval_context_mut();
1749        let Some(fd) = this.machine.fds.get(fd_num) else {
1750            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1751        };
1752        // Only regular files support synchronization.
1753        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1754            err_unsup_format!("`fsync` is only supported on file-backed file descriptors")
1755        })?;
1756        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1757
1758        let io_result = maybe_sync_file(&file, File::sync_all);
1759        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1760    }
1761
1762    fn fdatasync(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1763        let this = self.eval_context_mut();
1764
1765        let fd = this.read_scalar(fd_op)?.to_i32()?;
1766
1767        let Some(fd) = this.machine.fds.get(fd) else {
1768            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1769        };
1770        // Only regular files support synchronization.
1771        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1772            err_unsup_format!("`fdatasync` is only supported on file-backed file descriptors")
1773        })?;
1774        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1775
1776        let io_result = maybe_sync_file(&file, File::sync_data);
1777        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1778    }
1779
1780    /// `futimens(fd, times)`: set `fd`'s access/modification times. `times` is `[atime, mtime]`, or
1781    /// NULL to set both to now.
1782    fn futimens(
1783        &mut self,
1784        fd_op: &OpTy<'tcx>,
1785        times_op: &OpTy<'tcx>,
1786    ) -> InterpResult<'tcx, Scalar> {
1787        let this = self.eval_context_mut();
1788
1789        let fd_num = this.read_scalar(fd_op)?.to_i32()?;
1790        let times_ptr = this.read_pointer(times_op)?;
1791
1792        let Some(fd) = this.machine.fds.get(fd_num) else {
1793            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1794        };
1795        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1796            err_unsup_format!("`futimens` is only supported on file-backed file descriptors")
1797        })?;
1798        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1799
1800        let Some((access, modified)) = this.parse_utimens_times(times_ptr)? else {
1801            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1802        };
1803        let result = file.file.set_times(host_file_times_from_updates(access, modified));
1804        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result.map(|()| 0i32))?))
1805    }
1806
1807    /// `utimensat(dirfd, path, times, flags)`: like `futimens`, but for the file at `path`. With
1808    /// `AT_SYMLINK_NOFOLLOW`, a symlink at `path` is updated itself rather than its target.
1809    fn utimensat(
1810        &mut self,
1811        dirfd_op: &OpTy<'tcx>,
1812        path_op: &OpTy<'tcx>,
1813        times_op: &OpTy<'tcx>,
1814        flags_op: &OpTy<'tcx>,
1815    ) -> InterpResult<'tcx, Scalar> {
1816        let this = self.eval_context_mut();
1817
1818        let dirfd = this.read_scalar(dirfd_op)?.to_i32()?;
1819        let path_ptr = this.read_pointer(path_op)?;
1820        let times_ptr = this.read_pointer(times_op)?;
1821        let flags = this.read_scalar(flags_op)?.to_i32()?;
1822
1823        if this.ptr_is_null(path_ptr)? {
1824            // With `AT_FDCWD` this is a bad address on all systems. With any other `dirfd`, Linux
1825            // operates on `dirfd` itself (like `futimens`) while macOS still fails.
1826            if dirfd != this.eval_libc_i32("AT_FDCWD") {
1827                throw_unsup_format!(
1828                    "`utimensat` with a NULL path is only supported for `AT_FDCWD`"
1829                );
1830            }
1831            return this.set_errno_and_return_neg1_i32(LibcError("EFAULT"));
1832        }
1833        let path = this.read_path_from_c_str(path_ptr)?.into_owned();
1834
1835        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1836            this.reject_in_isolation("`utimensat`", reject_with)?;
1837            return this.set_errno_and_return_neg1_i32(LibcError("EACCES"));
1838        }
1839
1840        let mut flags = flags;
1841        // Parse known flags and subtract them from `flags`.
1842        let at_symlink_nofollow = this.eval_libc_i32("AT_SYMLINK_NOFOLLOW");
1843        let symlink_nofollow_flag = flags & at_symlink_nofollow == at_symlink_nofollow;
1844        flags &= !at_symlink_nofollow;
1845
1846        if flags != 0 {
1847            throw_unsup_format!("unsupported flags for `utimensat`: {flags:#x}")
1848        }
1849
1850        if !(path.is_absolute() || dirfd == this.eval_libc_i32("AT_FDCWD")) {
1851            throw_unsup_format!("`utimensat` with a path relative to a `dirfd` is not supported");
1852        }
1853
1854        let Some((access, modified)) = this.parse_utimens_times(times_ptr)? else {
1855            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1856        };
1857        if matches!((access, modified), (TimeUpdate::Omit, TimeUpdate::Omit)) {
1858            // Nothing to do. Behavior in this case differs between platforms: Linux always
1859            // succeeds, macOS/FreeBSD check whether the file exists. POSIX arguably allows both.
1860            // We decide to follow Linux.
1861            return interp_ok(Scalar::from_i32(0));
1862        }
1863        let filetimes = host_file_times_from_updates(access, modified);
1864        let result = if symlink_nofollow_flag {
1865            fs::set_times_nofollow(path, filetimes)
1866        } else {
1867            fs::set_times(path, filetimes)
1868        };
1869        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result.map(|()| 0i32))?))
1870    }
1871
1872    fn sync_file_range(
1873        &mut self,
1874        fd_op: &OpTy<'tcx>,
1875        offset_op: &OpTy<'tcx>,
1876        nbytes_op: &OpTy<'tcx>,
1877        flags_op: &OpTy<'tcx>,
1878    ) -> InterpResult<'tcx, Scalar> {
1879        let this = self.eval_context_mut();
1880
1881        let fd = this.read_scalar(fd_op)?.to_i32()?;
1882        let offset = this.read_scalar(offset_op)?.to_i64()?;
1883        let nbytes = this.read_scalar(nbytes_op)?.to_i64()?;
1884        let flags = this.read_scalar(flags_op)?.to_i32()?;
1885
1886        if offset < 0 || nbytes < 0 {
1887            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1888        }
1889        let allowed_flags = this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_BEFORE")
1890            | this.eval_libc_i32("SYNC_FILE_RANGE_WRITE")
1891            | this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_AFTER");
1892        if flags & allowed_flags != flags {
1893            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1894        }
1895
1896        let Some(fd) = this.machine.fds.get(fd) else {
1897            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1898        };
1899        // Only regular files support synchronization.
1900        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1901            err_unsup_format!("`sync_data_range` is only supported on file-backed file descriptors")
1902        })?;
1903        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1904
1905        let io_result = maybe_sync_file(&file, File::sync_data);
1906        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1907    }
1908
1909    fn readlink(
1910        &mut self,
1911        pathname_op: &OpTy<'tcx>,
1912        buf_op: &OpTy<'tcx>,
1913        bufsize_op: &OpTy<'tcx>,
1914    ) -> InterpResult<'tcx, i64> {
1915        let this = self.eval_context_mut();
1916
1917        let pathname = this.read_os_str_from_c_str(this.read_pointer(pathname_op)?)?;
1918        let buf = this.read_pointer(buf_op)?;
1919        let bufsize = this.read_target_usize(bufsize_op)?;
1920
1921        // Reject if isolation is enabled.
1922        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1923            this.reject_in_isolation("`readlink`", reject_with)?;
1924            this.set_last_error(LibcError("EACCES"))?;
1925            return interp_ok(-1);
1926        }
1927
1928        // Special case the procfs link to the current executable so that
1929        // `std::env::current_exe` works in cross-execution.
1930        let self_exe = match this.tcx.sess.target.os {
1931            Os::Linux | Os::Android => Some("/proc/self/exe"),
1932            Os::Solaris | Os::Illumos => Some("/proc/self/path/a.out"),
1933            _ => None,
1934        };
1935        let result = if self_exe.is_some() && pathname.to_str() == self_exe {
1936            this.machine.current_exe.clone().ok_or(ErrorKind::NotFound.into())
1937        } else if matches!(
1938            this.tcx.sess.target.os,
1939            Os::Linux | Os::Android | Os::Illumos | Os::Solaris
1940        ) && path::absolute(pathname).is_ok_and(|path| path.starts_with("/proc"))
1941        {
1942            // Trying to read a symlink inside `/proc` is likely going to be nonsense, e.g.
1943            // `/proc/self/fd/N` doesn't use the same FD numbering. So we pretend nothing exists.
1944            this.set_last_error(ErrorKind::NotFound)?;
1945            return interp_ok(-1);
1946        } else {
1947            // We read `pathname` as `OsStr` above so we could do the /proc/self/exe check.
1948            // But now we need a (host) path.
1949            let pathname = this.convert_path(Cow::Borrowed(pathname), PathConversion::TargetToHost);
1950            std::fs::read_link(pathname)
1951        };
1952
1953        match result {
1954            Ok(resolved) => {
1955                // 'readlink' truncates the resolved path if the provided buffer is not large
1956                // enough, and does *not* add a null terminator. That means we cannot use the usual
1957                // `write_path_to_c_str` and have to re-implement parts of it ourselves.
1958                let resolved = this
1959                    .convert_path(Cow::Borrowed(resolved.as_ref()), PathConversion::HostToTarget);
1960                let mut path_bytes = resolved.as_encoded_bytes();
1961                let bufsize: usize = bufsize.try_into().unwrap();
1962                if path_bytes.len() > bufsize {
1963                    path_bytes = &path_bytes[..bufsize]
1964                }
1965                this.write_bytes_ptr(buf, path_bytes.iter().copied())?;
1966                interp_ok(path_bytes.len().try_into().unwrap())
1967            }
1968            Err(e) => {
1969                this.set_last_error(e)?;
1970                interp_ok(-1)
1971            }
1972        }
1973    }
1974
1975    fn isatty(&mut self, miri_fd: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1976        let this = self.eval_context_mut();
1977        // "returns 1 if fd is an open file descriptor referring to a terminal;
1978        // otherwise 0 is returned, and errno is set to indicate the error"
1979        let fd = this.read_scalar(miri_fd)?.to_i32()?;
1980        let error = if let Some(fd) = this.machine.fds.get(fd) {
1981            if fd.is_tty(this.machine.communicate()) {
1982                return interp_ok(Scalar::from_i32(1));
1983            } else {
1984                LibcError("ENOTTY")
1985            }
1986        } else {
1987            // FD does not exist
1988            LibcError("EBADF")
1989        };
1990        this.set_last_error(error)?;
1991        interp_ok(Scalar::from_i32(0))
1992    }
1993
1994    fn realpath(
1995        &mut self,
1996        path_op: &OpTy<'tcx>,
1997        processed_path_op: &OpTy<'tcx>,
1998    ) -> InterpResult<'tcx, Scalar> {
1999        let this = self.eval_context_mut();
2000        this.assert_target_os_is_unix("realpath");
2001
2002        let pathname = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
2003        let processed_ptr = this.read_pointer(processed_path_op)?;
2004
2005        // Reject if isolation is enabled.
2006        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
2007            this.reject_in_isolation("`realpath`", reject_with)?;
2008            this.set_last_error(LibcError("EACCES"))?;
2009            return interp_ok(Scalar::from_target_usize(0, this));
2010        }
2011
2012        let result = std::fs::canonicalize(pathname);
2013        match result {
2014            Ok(resolved) => {
2015                let path_max = this
2016                    .eval_libc_i32("PATH_MAX")
2017                    .try_into()
2018                    .expect("PATH_MAX does not fit in u64");
2019                let dest = if this.ptr_is_null(processed_ptr)? {
2020                    // POSIX says behavior when passing a null pointer is implementation-defined,
2021                    // but GNU/linux, freebsd, netbsd, bionic/android, and macos all treat a null pointer
2022                    // similarly to:
2023                    //
2024                    // "If resolved_path is specified as NULL, then realpath() uses
2025                    // malloc(3) to allocate a buffer of up to PATH_MAX bytes to hold
2026                    // the resolved pathname, and returns a pointer to this buffer.  The
2027                    // caller should deallocate this buffer using free(3)."
2028                    // <https://man7.org/linux/man-pages/man3/realpath.3.html>
2029                    this.alloc_path_as_c_str(&resolved, MiriMemoryKind::C.into())?
2030                } else {
2031                    let (wrote_path, _) =
2032                        this.write_path_to_c_str(&resolved, processed_ptr, path_max)?;
2033
2034                    if !wrote_path {
2035                        // Note that we do not explicitly handle `FILENAME_MAX`
2036                        // (different from `PATH_MAX` above) as it is Linux-specific and
2037                        // seems like a bit of a mess anyway: <https://eklitzke.org/path-max-is-tricky>.
2038                        this.set_last_error(LibcError("ENAMETOOLONG"))?;
2039                        return interp_ok(Scalar::from_target_usize(0, this));
2040                    }
2041                    processed_ptr
2042                };
2043
2044                interp_ok(Scalar::from_maybe_pointer(dest, this))
2045            }
2046            Err(e) => {
2047                this.set_last_error(e)?;
2048                interp_ok(Scalar::from_target_usize(0, this))
2049            }
2050        }
2051    }
2052    fn mkstemp(&mut self, template_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
2053        use rand::seq::IndexedRandom;
2054
2055        // POSIX defines the template string.
2056        const TEMPFILE_TEMPLATE_STR: &str = "XXXXXX";
2057
2058        let this = self.eval_context_mut();
2059        this.assert_target_os_is_unix("mkstemp");
2060
2061        // POSIX defines the maximum number of attempts before failure.
2062        //
2063        // `mkstemp()` relies on `tmpnam()` which in turn relies on `TMP_MAX`.
2064        // POSIX says this about `TMP_MAX`:
2065        // * Minimum number of unique filenames generated by `tmpnam()`.
2066        // * Maximum number of times an application can call `tmpnam()` reliably.
2067        //   * The value of `TMP_MAX` is at least 25.
2068        //   * On XSI-conformant systems, the value of `TMP_MAX` is at least 10000.
2069        // See <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/stdio.h.html>.
2070        let max_attempts = this.eval_libc_u32("TMP_MAX");
2071
2072        // Get the raw bytes from the template -- as a byte slice, this is a string in the target
2073        // (and the target is unix, so a byte slice is the right representation).
2074        let template_ptr = this.read_pointer(template_op)?;
2075        let mut template = this.eval_context_ref().read_c_str(template_ptr)?.to_owned();
2076        let template_bytes = template.as_mut_slice();
2077
2078        // Reject if isolation is enabled.
2079        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
2080            this.reject_in_isolation("`mkstemp`", reject_with)?;
2081            return this.set_errno_and_return_neg1_i32(LibcError("EACCES"));
2082        }
2083
2084        // Get the bytes of the suffix we expect in _target_ encoding.
2085        let suffix_bytes = TEMPFILE_TEMPLATE_STR.as_bytes();
2086
2087        // At this point we have one `&[u8]` that represents the template and one `&[u8]`
2088        // that represents the expected suffix.
2089
2090        // Now we figure out the index of the slice we expect to contain the suffix.
2091        let start_pos = template_bytes.len().saturating_sub(suffix_bytes.len());
2092        let end_pos = template_bytes.len();
2093        let last_six_char_bytes = &template_bytes[start_pos..end_pos];
2094
2095        // If we don't find the suffix, it is an error.
2096        if last_six_char_bytes != suffix_bytes {
2097            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
2098        }
2099
2100        // At this point we know we have 6 ASCII 'X' characters as a suffix.
2101
2102        // From <https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/sysdeps/posix/tempname.c#L175>
2103        const SUBSTITUTIONS: &[char; 62] = &[
2104            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
2105            'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
2106            'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
2107            'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
2108        ];
2109
2110        // The file is opened with specific options, which Rust does not expose in a portable way.
2111        // So we use specific APIs depending on the host OS.
2112        let mut fopts = OpenOptions::new();
2113        fopts.read(true).write(true).create_new(true);
2114
2115        cfg_select! {
2116            unix => {
2117                use std::os::unix::fs::OpenOptionsExt;
2118                // Do not allow others to read or modify this file.
2119                fopts.mode(0o600);
2120                fopts.custom_flags(libc::O_EXCL);
2121            }
2122            windows => {
2123                use std::os::windows::fs::OpenOptionsExt;
2124                // Do not allow others to read or modify this file.
2125                fopts.share_mode(0);
2126            }
2127            _ => {
2128                throw_unsup_format!("`mkstemp` is not supported on this host OS");
2129            }
2130        }
2131
2132        // If the generated file already exists, we will try again `max_attempts` many times.
2133        for _ in 0..max_attempts {
2134            let rng = this.machine.rng.get_mut();
2135
2136            // Generate a random unique suffix.
2137            let unique_suffix =
2138                (0..6).map(|_| SUBSTITUTIONS.choose(rng).unwrap()).collect::<String>();
2139
2140            // Replace the template string with the random string.
2141            template_bytes[start_pos..end_pos].copy_from_slice(unique_suffix.as_bytes());
2142
2143            // Write the modified template back to the passed in pointer to maintain POSIX semantics.
2144            this.write_bytes_ptr(template_ptr, template_bytes.iter().copied())?;
2145
2146            // See if we can create and open this file.
2147            let file = fopts.open(bytes_to_os_str(template_bytes)?);
2148            match file {
2149                Ok(f) => {
2150                    let fd = this.machine.fds.insert_new(FileHandle {
2151                        file: f,
2152                        writable: true,
2153                        readable: true,
2154                    });
2155                    return interp_ok(Scalar::from_i32(fd));
2156                }
2157                Err(e) =>
2158                    match e.kind() {
2159                        // If the random file already exists, keep trying.
2160                        ErrorKind::AlreadyExists => continue,
2161                        // Any other errors are returned to the caller.
2162                        _ => {
2163                            // "On error, -1 is returned, and errno is set to
2164                            // indicate the error"
2165                            return this.set_errno_and_return_neg1_i32(e);
2166                        }
2167                    },
2168            }
2169        }
2170
2171        // We ran out of attempts to create the file, return an error.
2172        this.set_errno_and_return_neg1_i32(LibcError("EEXIST"))
2173    }
2174}
2175
2176/// Extracts the number of seconds and nanoseconds elapsed between `time` and the unix epoch when
2177/// `time` is Ok. Returns `None` if `time` is an error. Fails if `time` happens before the unix
2178/// epoch.
2179fn extract_sec_and_nsec<'tcx>(
2180    time: std::io::Result<SystemTime>,
2181) -> InterpResult<'tcx, Option<(u64, u32)>> {
2182    match time.ok() {
2183        Some(time) => {
2184            let duration = system_time_to_duration(&time)?;
2185            interp_ok(Some((duration.as_secs(), duration.subsec_nanos())))
2186        }
2187        None => interp_ok(None),
2188    }
2189}
2190
2191fn file_type_to_mode_name(file_type: std::fs::FileType) -> &'static str {
2192    if file_type.is_file() {
2193        "S_IFREG"
2194    } else if file_type.is_dir() {
2195        "S_IFDIR"
2196    } else if file_type.is_symlink() {
2197        "S_IFLNK"
2198    } else {
2199        // Certain file types are only available when the host is a Unix system.
2200        #[cfg(unix)]
2201        {
2202            use std::os::unix::fs::FileTypeExt;
2203            if file_type.is_socket() {
2204                return "S_IFSOCK";
2205            } else if file_type.is_fifo() {
2206                return "S_IFIFO";
2207            } else if file_type.is_char_device() {
2208                return "S_IFCHR";
2209            } else if file_type.is_block_device() {
2210                return "S_IFBLK";
2211            }
2212        }
2213        "S_IFREG"
2214    }
2215}
2216
2217/// Stores a file's metadata in order to avoid code duplication in the different metadata related
2218/// shims.
2219///
2220/// Some fields are host/platform-specific. `None` means that Miri does not have a real value for
2221/// this field, for example because the metadata is synthetic or because the host platform does not
2222/// expose it. `statx` must only advertise the corresponding `STATX_*` bit when the field is `Some`;
2223/// legacy `stat` writes zero for `None` to preserve the old fallback behavior.
2224struct FileMetadata {
2225    /// This holds both the file type (dir, regular, symlink, ...) and permissions.
2226    mode: u32,
2227    size: u64,
2228    created: Option<(u64, u32)>,
2229    accessed: Option<(u64, u32)>,
2230    modified: Option<(u64, u32)>,
2231    dev: Option<u64>,
2232    ino: Option<u64>,
2233    nlink: Option<u64>,
2234    uid: Option<u32>,
2235    gid: Option<u32>,
2236    blksize: Option<u64>,
2237    blocks: Option<u64>,
2238}
2239
2240impl FileMetadata {
2241    /// Implements the shared "metadata at" semantics of `fstatat` and `statx`.
2242    fn at<'tcx>(
2243        ecx: &mut MiriInterpCx<'tcx>,
2244        dirfd: FdNum,
2245        path: &Path,
2246        symlink_nofollow_flag: bool,
2247        empty_path_flag: bool,
2248    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
2249        // If the path is empty, and the AT_EMPTY_PATH flag is set, we query the open file
2250        // represented by dirfd, whether it's a directory or otherwise.
2251        if path.is_empty() {
2252            // no path: invalid by default, load metadata about dirfd with flag
2253            if !empty_path_flag {
2254                return interp_ok(Err(LibcError("ENOENT")));
2255            }
2256            FileMetadata::from_fd_num(ecx, dirfd)
2257        } else if path.is_absolute() || dirfd == ecx.eval_libc_i32("AT_FDCWD") {
2258            // Either absolute path (dirfd is ignored) or relative to working directory.
2259            FileMetadata::from_host(
2260                ecx,
2261                if symlink_nofollow_flag { path.symlink_metadata() } else { path.metadata() },
2262            )
2263        } else {
2264            // relative to dirfd, which must be a directory handle
2265            let Some(fd) = ecx.machine.fds.get(dirfd) else {
2266                return interp_ok(Err(LibcError("EBADF")));
2267            };
2268            let Some(dir) = fd.downcast::<DirHandle>() else {
2269                return interp_ok(Err(LibcError("ENOTDIR")));
2270            };
2271
2272            // Windows does not by itself treat `.` correctly so we do that by hand.
2273            // (https://github.com/rust-lang/rust/issues/163032)
2274            #[cfg(not(bootstrap))]
2275            let metadata = if path.to_str() == Some(".") {
2276                dir.dir.self_metadata()
2277            } else if symlink_nofollow_flag {
2278                dir.dir.symlink_metadata(path)
2279            } else {
2280                dir.dir.metadata(path)
2281            };
2282            #[cfg(bootstrap)]
2283            let metadata = if path.to_str() == Some(".") {
2284                dir.dir.metadata()
2285            } else if symlink_nofollow_flag {
2286                dir.fallback.join(path).symlink_metadata()
2287            } else {
2288                dir.fallback.join(path).metadata()
2289            };
2290            FileMetadata::from_host(ecx, metadata)
2291        }
2292    }
2293
2294    fn from_fd_num<'tcx>(
2295        ecx: &mut MiriInterpCx<'tcx>,
2296        fd_num: FdNum,
2297    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
2298        let Some(fd) = ecx.machine.fds.get(fd_num) else {
2299            return interp_ok(Err(LibcError("EBADF")));
2300        };
2301        match fd.metadata()? {
2302            Either::Left(host) => Self::from_host(ecx, host),
2303            Either::Right(mode_name) => Self::synthetic(ecx, mode_name),
2304        }
2305    }
2306
2307    fn synthetic<'tcx>(
2308        ecx: &mut MiriInterpCx<'tcx>,
2309        mode_name: &str,
2310    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
2311        let mode = ecx.eval_libc(mode_name);
2312        let mode: u32 = mode.to_uint(ecx.libc_ty_layout("mode_t").size)?.try_into().unwrap();
2313        // We observed 0x777 on sockets and 0x600 on pipes...
2314        let mode = mode | 0o666;
2315        interp_ok(Ok(FileMetadata {
2316            mode,
2317            size: 0,
2318            created: None,
2319            accessed: None,
2320            modified: None,
2321            dev: None,
2322            uid: None,
2323            gid: None,
2324            blksize: None,
2325            blocks: None,
2326            ino: None,
2327            nlink: None,
2328        }))
2329    }
2330
2331    fn from_host<'tcx>(
2332        ecx: &mut MiriInterpCx<'tcx>,
2333        metadata: Result<std::fs::Metadata, std::io::Error>,
2334    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
2335        let metadata = match metadata {
2336            Ok(metadata) => metadata,
2337            Err(e) => {
2338                return interp_ok(Err(e.into()));
2339            }
2340        };
2341
2342        let file_type = metadata.file_type();
2343        let mode = ecx.eval_libc(file_type_to_mode_name(file_type));
2344        let mut mode = mode.to_uint(ecx.libc_ty_layout("mode_t").size)?.try_into().unwrap();
2345
2346        let size = metadata.len();
2347
2348        let created = extract_sec_and_nsec(metadata.created())?;
2349        let accessed = extract_sec_and_nsec(metadata.accessed())?;
2350        let modified = extract_sec_and_nsec(metadata.modified())?;
2351
2352        // FIXME: Provide more fields using platform specific methods.
2353
2354        cfg_select! {
2355            unix => {
2356                use std::os::unix::fs::{MetadataExt, PermissionsExt};
2357
2358                let dev = metadata.dev();
2359                let ino = metadata.ino();
2360                let nlink = metadata.nlink();
2361                let uid = metadata.uid();
2362                let gid = metadata.gid();
2363                let blksize = metadata.blksize();
2364                let blocks = metadata.blocks();
2365
2366                mode |= metadata.permissions().mode();
2367
2368                interp_ok(Ok(FileMetadata {
2369                    mode,
2370                    size,
2371                    created,
2372                    accessed,
2373                    modified,
2374                    dev: Some(dev),
2375                    ino: Some(ino),
2376                    nlink: Some(nlink),
2377                    uid: Some(uid),
2378                    gid: Some(gid),
2379                    blksize: Some(blksize),
2380                    blocks: Some(blocks),
2381                }))
2382            }
2383            _ => {
2384                // Emulate "everyone can read" or "everyone can read and write".
2385                mode |= if metadata.permissions().readonly() { 0o111 } else { 0o333 };
2386
2387                interp_ok(Ok(FileMetadata {
2388                    mode,
2389                    size,
2390                    created,
2391                    accessed,
2392                    modified,
2393                    dev: None,
2394                    ino: None,
2395                    nlink: None,
2396                    uid: None,
2397                    gid: None,
2398                    blksize: None,
2399                    blocks: None,
2400                }))
2401            }
2402        }
2403    }
2404}