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
1121        ) {
1122            panic!("`readdir` should not be called 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                // We just use the pointee type here since determining the right pointee type
1175                // independently is highly non-trivial: it depends on which exact alias of the
1176                // function was invoked (e.g. `fstat` vs `fstat64`), and then on FreeBSD it also
1177                // depends on the ABI level which can be different between the libc used by std and
1178                // the libc used by everyone else.
1179                let dirent_ty = dest.layout.ty.builtin_deref(true).unwrap();
1180                let dirent_layout = this.layout_of(dirent_ty)?;
1181                let fields = &dirent_layout.fields;
1182                let d_name_offset = fields.offset(fields.count().strict_sub(1)).bytes();
1183
1184                // Determine the size of the buffer we have to allocate.
1185                let mut name = dir_entry.name; // not a Path as there are no separators!
1186                name.push("\0"); // Add a NUL terminator
1187                let name_bytes = name.as_encoded_bytes();
1188                let name_len = u64::try_from(name_bytes.len()).unwrap();
1189                let size = d_name_offset.strict_add(name_len);
1190
1191                let entry = this.allocate_ptr(
1192                    Size::from_bytes(size),
1193                    dirent_layout.align.abi,
1194                    MiriMemoryKind::Runtime.into(),
1195                    AllocInit::Uninit,
1196                )?;
1197                let entry = this.ptr_to_mplace(entry.into(), dirent_layout);
1198
1199                // Write the name.
1200                // The name is not a normal field, we already computed the offset above.
1201                let name_ptr = entry.ptr().wrapping_offset(Size::from_bytes(d_name_offset), this);
1202                this.write_bytes_ptr(name_ptr, name_bytes.iter().copied())?;
1203
1204                // Write common fields.
1205                let ino_name =
1206                    if this.tcx.sess.target.os == Os::FreeBsd { "d_fileno" } else { "d_ino" };
1207                this.write_int_fields_named(
1208                    &[(ino_name, dir_entry.ino.into()), ("d_reclen", size.into())],
1209                    &entry,
1210                )?;
1211
1212                // Write "optional" fields.
1213                if let Some(d_off) = this.try_project_field_named(&entry, "d_off")? {
1214                    this.write_null(&d_off)?;
1215                }
1216                if let Some(d_namlen) = this.try_project_field_named(&entry, "d_namlen")? {
1217                    this.write_int(name_len.strict_sub(1), &d_namlen)?;
1218                }
1219                if let Some(d_type) = this.try_project_field_named(&entry, "d_type")? {
1220                    this.write_int(dir_entry.d_type, &d_type)?;
1221                }
1222
1223                Some(entry.ptr())
1224            }
1225            None => {
1226                // end of stream: return NULL
1227                None
1228            }
1229            Some(Err(e)) => {
1230                this.set_last_error(e)?;
1231                None
1232            }
1233        };
1234
1235        let open_dir = this.machine.dirs.streams.get_mut(&dirp).unwrap();
1236        let old_entry = std::mem::replace(&mut open_dir.entry, entry);
1237        if let Some(old_entry) = old_entry {
1238            this.deallocate_ptr(old_entry, None, MiriMemoryKind::Runtime.into())?;
1239        }
1240
1241        this.write_pointer(entry.unwrap_or_else(Pointer::null), dest)?;
1242        interp_ok(())
1243    }
1244
1245    fn macos_readdir_r(
1246        &mut self,
1247        dirp_op: &OpTy<'tcx>,
1248        entry_op: &OpTy<'tcx>,
1249        result_op: &OpTy<'tcx>,
1250    ) -> InterpResult<'tcx, Scalar> {
1251        let this = self.eval_context_mut();
1252
1253        this.assert_target_os(Os::MacOs, "readdir_r");
1254
1255        let dirp = this.read_target_usize(dirp_op)?;
1256        let result_place = this.deref_pointer_as(result_op, this.machine.layouts.mut_raw_ptr)?;
1257
1258        // Reject if isolation is enabled.
1259        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1260            this.reject_in_isolation("`readdir_r`", reject_with)?;
1261            // Return error code, do *not* set `errno`.
1262            return interp_ok(this.eval_libc("EBADF"));
1263        }
1264
1265        let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| {
1266            err_unsup_format!("the DIR pointer passed to readdir_r did not come from opendir")
1267        })?;
1268        interp_ok(match open_dir.next_host_entry() {
1269            Some(Ok(dir_entry)) => {
1270                let dir_entry = this.dir_entry_fields(dir_entry)?;
1271                // Write into entry, write pointer to result, return 0 on success.
1272                // The name is written with write_os_str_to_c_str, while the rest of the
1273                // dirent struct is written using write_int_fields.
1274
1275                // For reference, on macOS this looks like:
1276                // pub struct dirent {
1277                //     pub d_ino: u64,
1278                //     pub d_seekoff: u64,
1279                //     pub d_reclen: u16,
1280                //     pub d_namlen: u16,
1281                //     pub d_type: u8,
1282                //     pub d_name: [c_char; 1024],
1283                // }
1284
1285                let entry_place = this.deref_pointer_as(entry_op, this.libc_ty_layout("dirent"))?;
1286
1287                // Write the name.
1288                let name_place = this.project_field_named(&entry_place, "d_name")?;
1289                let (name_fits, file_name_buf_len) = this.write_os_str_to_c_str(
1290                    &dir_entry.name,
1291                    name_place.ptr(),
1292                    name_place.layout.size.bytes(),
1293                )?;
1294                if !name_fits {
1295                    throw_unsup_format!(
1296                        "a directory entry had a name too large to fit in libc::dirent"
1297                    );
1298                }
1299
1300                // Write the other fields.
1301                this.write_int_fields_named(
1302                    &[
1303                        ("d_reclen", entry_place.layout.size.bytes().into()),
1304                        ("d_namlen", file_name_buf_len.strict_sub(1).into()),
1305                        ("d_type", dir_entry.d_type.into()),
1306                        ("d_ino", dir_entry.ino.into()),
1307                        ("d_seekoff", 0),
1308                    ],
1309                    &entry_place,
1310                )?;
1311                this.write_scalar(this.read_scalar(entry_op)?, &result_place)?;
1312
1313                Scalar::from_i32(0)
1314            }
1315            None => {
1316                // end of stream: return 0, assign *result=NULL
1317                this.write_null(&result_place)?;
1318                Scalar::from_i32(0)
1319            }
1320            Some(Err(e)) => {
1321                // return positive error number on error (do *not* set last error)
1322                this.host_error_to_errnum(e)?
1323            }
1324        })
1325    }
1326
1327    fn closedir(&mut self, dirp_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1328        let this = self.eval_context_mut();
1329
1330        let dirp = this.read_target_usize(dirp_op)?;
1331
1332        // Reject if isolation is enabled.
1333        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1334            this.reject_in_isolation("`closedir`", reject_with)?;
1335            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1336        }
1337
1338        let Some(mut open_dir) = this.machine.dirs.streams.remove(&dirp) else {
1339            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1340        };
1341        if let Some(entry) = open_dir.entry.take() {
1342            this.deallocate_ptr(entry, None, MiriMemoryKind::Runtime.into())?;
1343        }
1344        // We drop the `open_dir`, which will close the host dir handle.
1345        drop(open_dir);
1346
1347        interp_ok(Scalar::from_i32(0))
1348    }
1349
1350    fn ftruncate64(&mut self, fd_num: i32, length: i128) -> InterpResult<'tcx, Scalar> {
1351        let this = self.eval_context_mut();
1352
1353        let Some(fd) = this.machine.fds.get(fd_num) else {
1354            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1355        };
1356        let Some(file) = fd.downcast::<FileHandle>() else {
1357            // The docs say that EINVAL is returned when the FD "does not reference a regular file
1358            // or a POSIX shared memory object" (and we don't support shmem objects).
1359            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1360        };
1361        if !file.writable {
1362            // man page says "EBADF or EINVAL", Linux seems to use EINVAL.
1363            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1364        }
1365        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1366
1367        if let Ok(length) = length.try_into() {
1368            let result = file.file.set_len(length);
1369            let result = this.try_unwrap_io_result(result.map(|_| 0i32))?;
1370            interp_ok(Scalar::from_i32(result))
1371        } else {
1372            this.set_errno_and_return_neg1_i32(LibcError("EINVAL"))
1373        }
1374    }
1375
1376    /// NOTE: According to the man page of `possix_fallocate`, it returns the error code instead
1377    /// of setting `errno`.
1378    fn posix_fallocate(
1379        &mut self,
1380        fd_num: i32,
1381        offset: i64,
1382        len: i64,
1383    ) -> InterpResult<'tcx, Scalar> {
1384        let this = self.eval_context_mut();
1385
1386        // Reject if isolation is enabled.
1387        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1388            this.reject_in_isolation("`posix_fallocate`", reject_with)?;
1389            // Return error code "EBADF" (bad fd).
1390            return interp_ok(this.eval_libc("EBADF"));
1391        }
1392
1393        match this.fallocate_impl(fd_num, offset, len)? {
1394            Ok(()) => interp_ok(Scalar::from_i32(0)),
1395            Err(e) => this.io_error_to_errnum(e),
1396        }
1397    }
1398
1399    fn linux_fallocate(
1400        &mut self,
1401        fd: i32,
1402        mode: i32,
1403        offset: i64,
1404        size: i64,
1405    ) -> InterpResult<'tcx, Scalar> {
1406        // This is mostly a copy of `posix_fallocate` except that errors are returned via errno.
1407        let this = self.eval_context_mut();
1408
1409        // Reject if isolation is enabled.
1410        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1411            this.reject_in_isolation("`fallocate`", reject_with)?;
1412            // Set error code "EBADF" (bad fd).
1413            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1414        }
1415
1416        // We only support `fallocate` as a replacement for `posix_fallocate` on linux,
1417        // so a non-default `mode` is not supported.
1418        if mode != 0 {
1419            throw_unsup_format!("unsupported flags for `fallocate` in `mode` argument: {mode}")
1420        }
1421
1422        match this.fallocate_impl(fd, offset, size)? {
1423            Ok(()) => interp_ok(Scalar::from_i32(0)),
1424            Err(e) => this.set_errno_and_return_neg1_i32(e),
1425        }
1426    }
1427
1428    /// Shared logic between `posix_fallocate` and `linux_fallocate`.
1429    fn fallocate_impl(
1430        &mut self,
1431        fd_num: i32,
1432        offset: i64,
1433        len: i64,
1434    ) -> InterpResult<'tcx, Result<(), IoError>> {
1435        let this = self.eval_context_mut();
1436
1437        // EINVAL is returned/set when: "offset was less than 0, or len was less than or equal to 0".
1438        if offset < 0 || len <= 0 {
1439            return interp_ok(Err(LibcError("EINVAL")));
1440        }
1441
1442        let Some(fd) = this.machine.fds.get(fd_num) else {
1443            return interp_ok(Err(LibcError("EBADF")));
1444        };
1445        let Some(file) = fd.downcast::<FileHandle>() else {
1446            // Man page specifies to return ENODEV if `fd` is not a regular file.
1447            return interp_ok(Err(LibcError("ENODEV")));
1448        };
1449
1450        if !file.writable {
1451            return interp_ok(Err(LibcError("EBADF")));
1452        }
1453
1454        let current_size = match file.file.metadata() {
1455            Ok(metadata) => metadata.len(),
1456            Err(err) => return interp_ok(Err(err.into())),
1457        };
1458
1459        // Checked i64 addition, to ensure the result does not exceed the max file size.
1460        let new_size = match offset.checked_add(len) {
1461            // `new_size` is definitely non-negative, so we can cast to `u64`.
1462            Some(new_size) => u64::try_from(new_size).unwrap(),
1463            None => return interp_ok(Err(LibcError("EFBIG"))), // new size too big
1464        };
1465
1466        // If the size of the file is less than offset+size, then the file is increased to this
1467        // size; otherwise the file size is left unchanged.
1468        if current_size < new_size {
1469            match file.file.set_len(new_size) {
1470                Ok(()) => interp_ok(Ok(())),
1471                Err(err) => interp_ok(Err(err.into())),
1472            }
1473        } else {
1474            interp_ok(Ok(()))
1475        }
1476    }
1477
1478    fn fsync(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1479        // On macOS, `fsync` (unlike `fcntl(F_FULLFSYNC)`) does not wait for the
1480        // underlying disk to finish writing. In the interest of host compatibility,
1481        // we conservatively implement this with `sync_all`, which
1482        // *does* wait for the disk.
1483
1484        let this = self.eval_context_mut();
1485
1486        let fd = this.read_scalar(fd_op)?.to_i32()?;
1487
1488        self.ffullsync_fd(fd)
1489    }
1490
1491    fn ffullsync_fd(&mut self, fd_num: i32) -> InterpResult<'tcx, Scalar> {
1492        let this = self.eval_context_mut();
1493        let Some(fd) = this.machine.fds.get(fd_num) else {
1494            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1495        };
1496        // Only regular files support synchronization.
1497        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1498            err_unsup_format!("`fsync` is only supported on file-backed file descriptors")
1499        })?;
1500        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1501
1502        let io_result = maybe_sync_file(&file.file, file.writable, File::sync_all);
1503        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1504    }
1505
1506    fn fdatasync(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1507        let this = self.eval_context_mut();
1508
1509        let fd = this.read_scalar(fd_op)?.to_i32()?;
1510
1511        let Some(fd) = this.machine.fds.get(fd) else {
1512            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1513        };
1514        // Only regular files support synchronization.
1515        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1516            err_unsup_format!("`fdatasync` is only supported on file-backed file descriptors")
1517        })?;
1518        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1519
1520        let io_result = maybe_sync_file(&file.file, file.writable, File::sync_data);
1521        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1522    }
1523
1524    /// `futimens(fd, times)`: set `fd`'s access/modification times. `times` is `[atime, mtime]`, or
1525    /// NULL to set both to now.
1526    fn futimens(
1527        &mut self,
1528        fd_op: &OpTy<'tcx>,
1529        times_op: &OpTy<'tcx>,
1530    ) -> InterpResult<'tcx, Scalar> {
1531        let this = self.eval_context_mut();
1532
1533        let fd_num = this.read_scalar(fd_op)?.to_i32()?;
1534        let times_ptr = this.read_pointer(times_op)?;
1535
1536        let Some(fd) = this.machine.fds.get(fd_num) else {
1537            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1538        };
1539        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1540            err_unsup_format!("`futimens` is only supported on file-backed file descriptors")
1541        })?;
1542        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1543
1544        let (access, modified) = if this.ptr_is_null(times_ptr)? {
1545            let now = TimeUpdate::Set(SystemTime::now());
1546            (now, now)
1547        } else {
1548            let timespec = this.libc_ty_layout("timespec");
1549            let access_place = this.deref_pointer_as(times_op, timespec)?;
1550            let modified_place = access_place.offset(timespec.size, timespec, this)?;
1551            let Some(access) = this.parse_utimens_timespec(&access_place)? else {
1552                return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1553            };
1554            let Some(modified) = this.parse_utimens_timespec(&modified_place)? else {
1555                return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1556            };
1557            (access, modified)
1558        };
1559
1560        let mut filetimes = FileTimes::new();
1561        if let TimeUpdate::Set(access) = access {
1562            filetimes = filetimes.set_accessed(access);
1563        }
1564        if let TimeUpdate::Set(modified) = modified {
1565            filetimes = filetimes.set_modified(modified);
1566        }
1567        let result = file.file.set_times(filetimes);
1568        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result.map(|()| 0i32))?))
1569    }
1570
1571    fn sync_file_range(
1572        &mut self,
1573        fd_op: &OpTy<'tcx>,
1574        offset_op: &OpTy<'tcx>,
1575        nbytes_op: &OpTy<'tcx>,
1576        flags_op: &OpTy<'tcx>,
1577    ) -> InterpResult<'tcx, Scalar> {
1578        let this = self.eval_context_mut();
1579
1580        let fd = this.read_scalar(fd_op)?.to_i32()?;
1581        let offset = this.read_scalar(offset_op)?.to_i64()?;
1582        let nbytes = this.read_scalar(nbytes_op)?.to_i64()?;
1583        let flags = this.read_scalar(flags_op)?.to_i32()?;
1584
1585        if offset < 0 || nbytes < 0 {
1586            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1587        }
1588        let allowed_flags = this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_BEFORE")
1589            | this.eval_libc_i32("SYNC_FILE_RANGE_WRITE")
1590            | this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_AFTER");
1591        if flags & allowed_flags != flags {
1592            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1593        }
1594
1595        let Some(fd) = this.machine.fds.get(fd) else {
1596            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1597        };
1598        // Only regular files support synchronization.
1599        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1600            err_unsup_format!("`sync_data_range` is only supported on file-backed file descriptors")
1601        })?;
1602        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1603
1604        let io_result = maybe_sync_file(&file.file, file.writable, File::sync_data);
1605        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1606    }
1607
1608    fn readlink(
1609        &mut self,
1610        pathname_op: &OpTy<'tcx>,
1611        buf_op: &OpTy<'tcx>,
1612        bufsize_op: &OpTy<'tcx>,
1613    ) -> InterpResult<'tcx, i64> {
1614        let this = self.eval_context_mut();
1615
1616        let pathname = this.read_path_from_c_str(this.read_pointer(pathname_op)?)?;
1617        let buf = this.read_pointer(buf_op)?;
1618        let bufsize = this.read_target_usize(bufsize_op)?;
1619
1620        // Reject if isolation is enabled.
1621        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1622            this.reject_in_isolation("`readlink`", reject_with)?;
1623            this.set_last_error(LibcError("EACCES"))?;
1624            return interp_ok(-1);
1625        }
1626
1627        let result = std::fs::read_link(pathname);
1628        match result {
1629            Ok(resolved) => {
1630                // 'readlink' truncates the resolved path if the provided buffer is not large
1631                // enough, and does *not* add a null terminator. That means we cannot use the usual
1632                // `write_path_to_c_str` and have to re-implement parts of it ourselves.
1633                let resolved = this.convert_path(
1634                    Cow::Borrowed(resolved.as_ref()),
1635                    crate::shims::os_str::PathConversion::HostToTarget,
1636                );
1637                let mut path_bytes = resolved.as_encoded_bytes();
1638                let bufsize: usize = bufsize.try_into().unwrap();
1639                if path_bytes.len() > bufsize {
1640                    path_bytes = &path_bytes[..bufsize]
1641                }
1642                this.write_bytes_ptr(buf, path_bytes.iter().copied())?;
1643                interp_ok(path_bytes.len().try_into().unwrap())
1644            }
1645            Err(e) => {
1646                this.set_last_error(e)?;
1647                interp_ok(-1)
1648            }
1649        }
1650    }
1651
1652    fn isatty(&mut self, miri_fd: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1653        let this = self.eval_context_mut();
1654        // "returns 1 if fd is an open file descriptor referring to a terminal;
1655        // otherwise 0 is returned, and errno is set to indicate the error"
1656        let fd = this.read_scalar(miri_fd)?.to_i32()?;
1657        let error = if let Some(fd) = this.machine.fds.get(fd) {
1658            if fd.is_tty(this.machine.communicate()) {
1659                return interp_ok(Scalar::from_i32(1));
1660            } else {
1661                LibcError("ENOTTY")
1662            }
1663        } else {
1664            // FD does not exist
1665            LibcError("EBADF")
1666        };
1667        this.set_last_error(error)?;
1668        interp_ok(Scalar::from_i32(0))
1669    }
1670
1671    fn realpath(
1672        &mut self,
1673        path_op: &OpTy<'tcx>,
1674        processed_path_op: &OpTy<'tcx>,
1675    ) -> InterpResult<'tcx, Scalar> {
1676        let this = self.eval_context_mut();
1677        this.assert_target_os_is_unix("realpath");
1678
1679        let pathname = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
1680        let processed_ptr = this.read_pointer(processed_path_op)?;
1681
1682        // Reject if isolation is enabled.
1683        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1684            this.reject_in_isolation("`realpath`", reject_with)?;
1685            this.set_last_error(LibcError("EACCES"))?;
1686            return interp_ok(Scalar::from_target_usize(0, this));
1687        }
1688
1689        let result = std::fs::canonicalize(pathname);
1690        match result {
1691            Ok(resolved) => {
1692                let path_max = this
1693                    .eval_libc_i32("PATH_MAX")
1694                    .try_into()
1695                    .expect("PATH_MAX does not fit in u64");
1696                let dest = if this.ptr_is_null(processed_ptr)? {
1697                    // POSIX says behavior when passing a null pointer is implementation-defined,
1698                    // but GNU/linux, freebsd, netbsd, bionic/android, and macos all treat a null pointer
1699                    // similarly to:
1700                    //
1701                    // "If resolved_path is specified as NULL, then realpath() uses
1702                    // malloc(3) to allocate a buffer of up to PATH_MAX bytes to hold
1703                    // the resolved pathname, and returns a pointer to this buffer.  The
1704                    // caller should deallocate this buffer using free(3)."
1705                    // <https://man7.org/linux/man-pages/man3/realpath.3.html>
1706                    this.alloc_path_as_c_str(&resolved, MiriMemoryKind::C.into())?
1707                } else {
1708                    let (wrote_path, _) =
1709                        this.write_path_to_c_str(&resolved, processed_ptr, path_max)?;
1710
1711                    if !wrote_path {
1712                        // Note that we do not explicitly handle `FILENAME_MAX`
1713                        // (different from `PATH_MAX` above) as it is Linux-specific and
1714                        // seems like a bit of a mess anyway: <https://eklitzke.org/path-max-is-tricky>.
1715                        this.set_last_error(LibcError("ENAMETOOLONG"))?;
1716                        return interp_ok(Scalar::from_target_usize(0, this));
1717                    }
1718                    processed_ptr
1719                };
1720
1721                interp_ok(Scalar::from_maybe_pointer(dest, this))
1722            }
1723            Err(e) => {
1724                this.set_last_error(e)?;
1725                interp_ok(Scalar::from_target_usize(0, this))
1726            }
1727        }
1728    }
1729    fn mkstemp(&mut self, template_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1730        use rand::seq::IndexedRandom;
1731
1732        // POSIX defines the template string.
1733        const TEMPFILE_TEMPLATE_STR: &str = "XXXXXX";
1734
1735        let this = self.eval_context_mut();
1736        this.assert_target_os_is_unix("mkstemp");
1737
1738        // POSIX defines the maximum number of attempts before failure.
1739        //
1740        // `mkstemp()` relies on `tmpnam()` which in turn relies on `TMP_MAX`.
1741        // POSIX says this about `TMP_MAX`:
1742        // * Minimum number of unique filenames generated by `tmpnam()`.
1743        // * Maximum number of times an application can call `tmpnam()` reliably.
1744        //   * The value of `TMP_MAX` is at least 25.
1745        //   * On XSI-conformant systems, the value of `TMP_MAX` is at least 10000.
1746        // See <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/stdio.h.html>.
1747        let max_attempts = this.eval_libc_u32("TMP_MAX");
1748
1749        // Get the raw bytes from the template -- as a byte slice, this is a string in the target
1750        // (and the target is unix, so a byte slice is the right representation).
1751        let template_ptr = this.read_pointer(template_op)?;
1752        let mut template = this.eval_context_ref().read_c_str(template_ptr)?.to_owned();
1753        let template_bytes = template.as_mut_slice();
1754
1755        // Reject if isolation is enabled.
1756        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1757            this.reject_in_isolation("`mkstemp`", reject_with)?;
1758            return this.set_errno_and_return_neg1_i32(LibcError("EACCES"));
1759        }
1760
1761        // Get the bytes of the suffix we expect in _target_ encoding.
1762        let suffix_bytes = TEMPFILE_TEMPLATE_STR.as_bytes();
1763
1764        // At this point we have one `&[u8]` that represents the template and one `&[u8]`
1765        // that represents the expected suffix.
1766
1767        // Now we figure out the index of the slice we expect to contain the suffix.
1768        let start_pos = template_bytes.len().saturating_sub(suffix_bytes.len());
1769        let end_pos = template_bytes.len();
1770        let last_six_char_bytes = &template_bytes[start_pos..end_pos];
1771
1772        // If we don't find the suffix, it is an error.
1773        if last_six_char_bytes != suffix_bytes {
1774            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1775        }
1776
1777        // At this point we know we have 6 ASCII 'X' characters as a suffix.
1778
1779        // From <https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/sysdeps/posix/tempname.c#L175>
1780        const SUBSTITUTIONS: &[char; 62] = &[
1781            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
1782            'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
1783            'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
1784            'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
1785        ];
1786
1787        // The file is opened with specific options, which Rust does not expose in a portable way.
1788        // So we use specific APIs depending on the host OS.
1789        let mut fopts = OpenOptions::new();
1790        fopts.read(true).write(true).create_new(true);
1791
1792        cfg_select! {
1793            unix =>
1794            {
1795                use std::os::unix::fs::OpenOptionsExt;
1796                // Do not allow others to read or modify this file.
1797                fopts.mode(0o600);
1798                fopts.custom_flags(libc::O_EXCL);
1799            }
1800            windows =>
1801            {
1802                use std::os::windows::fs::OpenOptionsExt;
1803                // Do not allow others to read or modify this file.
1804                fopts.share_mode(0);
1805            }
1806            _ => {
1807                throw_unsup_format!("`mkstemp` is not supported on this host OS");
1808            }
1809        }
1810
1811        // If the generated file already exists, we will try again `max_attempts` many times.
1812        for _ in 0..max_attempts {
1813            let rng = this.machine.rng.get_mut();
1814
1815            // Generate a random unique suffix.
1816            let unique_suffix =
1817                (0..6).map(|_| SUBSTITUTIONS.choose(rng).unwrap()).collect::<String>();
1818
1819            // Replace the template string with the random string.
1820            template_bytes[start_pos..end_pos].copy_from_slice(unique_suffix.as_bytes());
1821
1822            // Write the modified template back to the passed in pointer to maintain POSIX semantics.
1823            this.write_bytes_ptr(template_ptr, template_bytes.iter().copied())?;
1824
1825            // See if we can create and open this file.
1826            let file = fopts.open(bytes_to_os_str(template_bytes)?);
1827            match file {
1828                Ok(f) => {
1829                    let fd = this.machine.fds.insert_new(FileHandle {
1830                        file: f,
1831                        writable: true,
1832                        readable: true,
1833                    });
1834                    return interp_ok(Scalar::from_i32(fd));
1835                }
1836                Err(e) =>
1837                    match e.kind() {
1838                        // If the random file already exists, keep trying.
1839                        ErrorKind::AlreadyExists => continue,
1840                        // Any other errors are returned to the caller.
1841                        _ => {
1842                            // "On error, -1 is returned, and errno is set to
1843                            // indicate the error"
1844                            return this.set_errno_and_return_neg1_i32(e);
1845                        }
1846                    },
1847            }
1848        }
1849
1850        // We ran out of attempts to create the file, return an error.
1851        this.set_errno_and_return_neg1_i32(LibcError("EEXIST"))
1852    }
1853}
1854
1855/// Extracts the number of seconds and nanoseconds elapsed between `time` and the unix epoch when
1856/// `time` is Ok. Returns `None` if `time` is an error. Fails if `time` happens before the unix
1857/// epoch.
1858fn extract_sec_and_nsec<'tcx>(
1859    time: std::io::Result<SystemTime>,
1860) -> InterpResult<'tcx, Option<(u64, u32)>> {
1861    match time.ok() {
1862        Some(time) => {
1863            let duration = system_time_to_duration(&time)?;
1864            interp_ok(Some((duration.as_secs(), duration.subsec_nanos())))
1865        }
1866        None => interp_ok(None),
1867    }
1868}
1869
1870fn file_type_to_mode_name(file_type: std::fs::FileType) -> &'static str {
1871    #[cfg(unix)]
1872    use std::os::unix::fs::FileTypeExt;
1873
1874    if file_type.is_file() {
1875        "S_IFREG"
1876    } else if file_type.is_dir() {
1877        "S_IFDIR"
1878    } else if file_type.is_symlink() {
1879        "S_IFLNK"
1880    } else {
1881        // Certain file types are only available when the host is a Unix system.
1882        #[cfg(unix)]
1883        {
1884            if file_type.is_socket() {
1885                return "S_IFSOCK";
1886            } else if file_type.is_fifo() {
1887                return "S_IFIFO";
1888            } else if file_type.is_char_device() {
1889                return "S_IFCHR";
1890            } else if file_type.is_block_device() {
1891                return "S_IFBLK";
1892            }
1893        }
1894        "S_IFREG"
1895    }
1896}
1897
1898/// Stores a file's metadata in order to avoid code duplication in the different metadata related
1899/// shims.
1900///
1901/// Some fields are host/platform-specific. `None` means that Miri does not have a real value for
1902/// this field, for example because the metadata is synthetic or because the host platform does not
1903/// expose it. `statx` must only advertise the corresponding `STATX_*` bit when the field is `Some`;
1904/// legacy `stat` writes zero for `None` to preserve the old fallback behavior.
1905struct FileMetadata {
1906    /// This holds both the file type (dir, regular, symlink, ...) and permissions.
1907    mode: u32,
1908    size: u64,
1909    created: Option<(u64, u32)>,
1910    accessed: Option<(u64, u32)>,
1911    modified: Option<(u64, u32)>,
1912    dev: Option<u64>,
1913    ino: Option<u64>,
1914    nlink: Option<u64>,
1915    uid: Option<u32>,
1916    gid: Option<u32>,
1917    blksize: Option<u64>,
1918    blocks: Option<u64>,
1919}
1920
1921impl FileMetadata {
1922    fn from_path<'tcx>(
1923        ecx: &mut MiriInterpCx<'tcx>,
1924        path: &Path,
1925        follow_symlink: bool,
1926    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1927        let metadata =
1928            if follow_symlink { std::fs::metadata(path) } else { std::fs::symlink_metadata(path) };
1929
1930        FileMetadata::from_meta(ecx, metadata)
1931    }
1932
1933    fn from_fd_num<'tcx>(
1934        ecx: &mut MiriInterpCx<'tcx>,
1935        fd_num: i32,
1936    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1937        let Some(fd) = ecx.machine.fds.get(fd_num) else {
1938            return interp_ok(Err(LibcError("EBADF")));
1939        };
1940        match fd.metadata()? {
1941            Either::Left(host) => Self::from_meta(ecx, host),
1942            Either::Right(name) => Self::synthetic(ecx, name),
1943        }
1944    }
1945
1946    fn synthetic<'tcx>(
1947        ecx: &mut MiriInterpCx<'tcx>,
1948        mode_name: &str,
1949    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1950        let mode = ecx.eval_libc(mode_name);
1951        let mode: u32 = mode.to_uint(ecx.libc_ty_layout("mode_t").size)?.try_into().unwrap();
1952        // We observed 0x777 on sockets and 0x600 on pipes...
1953        let mode = mode | 0o666;
1954        interp_ok(Ok(FileMetadata {
1955            mode,
1956            size: 0,
1957            created: None,
1958            accessed: None,
1959            modified: None,
1960            dev: None,
1961            uid: None,
1962            gid: None,
1963            blksize: None,
1964            blocks: None,
1965            ino: None,
1966            nlink: None,
1967        }))
1968    }
1969
1970    fn from_meta<'tcx>(
1971        ecx: &mut MiriInterpCx<'tcx>,
1972        metadata: Result<std::fs::Metadata, std::io::Error>,
1973    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1974        let metadata = match metadata {
1975            Ok(metadata) => metadata,
1976            Err(e) => {
1977                return interp_ok(Err(e.into()));
1978            }
1979        };
1980
1981        let file_type = metadata.file_type();
1982        let mode = ecx.eval_libc(file_type_to_mode_name(file_type));
1983        let mut mode = mode.to_uint(ecx.libc_ty_layout("mode_t").size)?.try_into().unwrap();
1984
1985        let size = metadata.len();
1986
1987        let created = extract_sec_and_nsec(metadata.created())?;
1988        let accessed = extract_sec_and_nsec(metadata.accessed())?;
1989        let modified = extract_sec_and_nsec(metadata.modified())?;
1990
1991        // FIXME: Provide more fields using platform specific methods.
1992
1993        cfg_select! {
1994            unix => {
1995                use std::os::unix::fs::MetadataExt;
1996                use std::os::unix::fs::PermissionsExt;
1997
1998                let dev = metadata.dev();
1999                let ino = metadata.ino();
2000                let nlink = metadata.nlink();
2001                let uid = metadata.uid();
2002                let gid = metadata.gid();
2003                let blksize = metadata.blksize();
2004                let blocks = metadata.blocks();
2005
2006                mode |= metadata.permissions().mode();
2007
2008                interp_ok(Ok(FileMetadata {
2009                    mode,
2010                    size,
2011                    created,
2012                    accessed,
2013                    modified,
2014                    dev: Some(dev),
2015                    ino: Some(ino),
2016                    nlink: Some(nlink),
2017                    uid: Some(uid),
2018                    gid: Some(gid),
2019                    blksize: Some(blksize),
2020                    blocks: Some(blocks),
2021                }))
2022            }
2023            _ => {
2024                // Emulate "everyone can read" or "everyone can read and write".
2025                mode |= if metadata.permissions().readonly() { 0o111 } else { 0o333 };
2026
2027                interp_ok(Ok(FileMetadata {
2028                    mode,
2029                    size,
2030                    created,
2031                    accessed,
2032                    modified,
2033                    dev: None,
2034                    ino: None,
2035                    nlink: None,
2036                    uid: None,
2037                    gid: None,
2038                    blksize: None,
2039                    blocks: None,
2040                }))
2041            },
2042        }
2043    }
2044}