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