Skip to main content

miri/shims/unix/
fs.rs

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