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.io_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        // EINVAL is returned when: "offset was less than 0, or len was less than or equal to 0".
1394        if offset < 0 || len <= 0 {
1395            return interp_ok(this.eval_libc("EINVAL"));
1396        }
1397
1398        // Get the file handle.
1399        let Some(fd) = this.machine.fds.get(fd_num) else {
1400            return interp_ok(this.eval_libc("EBADF"));
1401        };
1402        let Some(file) = fd.downcast::<FileHandle>() else {
1403            // Man page specifies to return ENODEV if `fd` is not a regular file.
1404            return interp_ok(this.eval_libc("ENODEV"));
1405        };
1406
1407        if !file.writable {
1408            // The file is not writable.
1409            return interp_ok(this.eval_libc("EBADF"));
1410        }
1411
1412        let current_size = match file.file.metadata() {
1413            Ok(metadata) => metadata.len(),
1414            Err(err) => return this.io_error_to_errnum(err),
1415        };
1416        // Checked i64 addition, to ensure the result does not exceed the max file size.
1417        let new_size = match offset.checked_add(len) {
1418            // `new_size` is definitely non-negative, so we can cast to `u64`.
1419            Some(new_size) => u64::try_from(new_size).unwrap(),
1420            None => return interp_ok(this.eval_libc("EFBIG")), // new size too big
1421        };
1422        // If the size of the file is less than offset+size, then the file is increased to this size;
1423        // otherwise the file size is left unchanged.
1424        if current_size < new_size {
1425            interp_ok(match file.file.set_len(new_size) {
1426                Ok(()) => Scalar::from_i32(0),
1427                Err(e) => this.io_error_to_errnum(e)?,
1428            })
1429        } else {
1430            interp_ok(Scalar::from_i32(0))
1431        }
1432    }
1433
1434    fn fsync(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1435        // On macOS, `fsync` (unlike `fcntl(F_FULLFSYNC)`) does not wait for the
1436        // underlying disk to finish writing. In the interest of host compatibility,
1437        // we conservatively implement this with `sync_all`, which
1438        // *does* wait for the disk.
1439
1440        let this = self.eval_context_mut();
1441
1442        let fd = this.read_scalar(fd_op)?.to_i32()?;
1443
1444        self.ffullsync_fd(fd)
1445    }
1446
1447    fn ffullsync_fd(&mut self, fd_num: i32) -> InterpResult<'tcx, Scalar> {
1448        let this = self.eval_context_mut();
1449        let Some(fd) = this.machine.fds.get(fd_num) else {
1450            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1451        };
1452        // Only regular files support synchronization.
1453        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1454            err_unsup_format!("`fsync` is only supported on file-backed file descriptors")
1455        })?;
1456        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1457
1458        let io_result = maybe_sync_file(&file.file, file.writable, File::sync_all);
1459        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1460    }
1461
1462    fn fdatasync(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1463        let this = self.eval_context_mut();
1464
1465        let fd = this.read_scalar(fd_op)?.to_i32()?;
1466
1467        let Some(fd) = this.machine.fds.get(fd) else {
1468            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1469        };
1470        // Only regular files support synchronization.
1471        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1472            err_unsup_format!("`fdatasync` is only supported on file-backed file descriptors")
1473        })?;
1474        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1475
1476        let io_result = maybe_sync_file(&file.file, file.writable, File::sync_data);
1477        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1478    }
1479
1480    /// `futimens(fd, times)`: set `fd`'s access/modification times. `times` is `[atime, mtime]`, or
1481    /// NULL to set both to now.
1482    fn futimens(
1483        &mut self,
1484        fd_op: &OpTy<'tcx>,
1485        times_op: &OpTy<'tcx>,
1486    ) -> InterpResult<'tcx, Scalar> {
1487        let this = self.eval_context_mut();
1488
1489        let fd_num = this.read_scalar(fd_op)?.to_i32()?;
1490        let times_ptr = this.read_pointer(times_op)?;
1491
1492        let Some(fd) = this.machine.fds.get(fd_num) else {
1493            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1494        };
1495        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1496            err_unsup_format!("`futimens` is only supported on file-backed file descriptors")
1497        })?;
1498        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1499
1500        let (access, modified) = if this.ptr_is_null(times_ptr)? {
1501            let now = TimeUpdate::Set(SystemTime::now());
1502            (now, now)
1503        } else {
1504            let timespec = this.libc_ty_layout("timespec");
1505            let access_place = this.deref_pointer_as(times_op, timespec)?;
1506            let modified_place = access_place.offset(timespec.size, timespec, this)?;
1507            let Some(access) = this.parse_utimens_timespec(&access_place)? else {
1508                return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1509            };
1510            let Some(modified) = this.parse_utimens_timespec(&modified_place)? else {
1511                return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1512            };
1513            (access, modified)
1514        };
1515
1516        let mut filetimes = FileTimes::new();
1517        if let TimeUpdate::Set(access) = access {
1518            filetimes = filetimes.set_accessed(access);
1519        }
1520        if let TimeUpdate::Set(modified) = modified {
1521            filetimes = filetimes.set_modified(modified);
1522        }
1523        let result = file.file.set_times(filetimes);
1524        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result.map(|()| 0i32))?))
1525    }
1526
1527    fn sync_file_range(
1528        &mut self,
1529        fd_op: &OpTy<'tcx>,
1530        offset_op: &OpTy<'tcx>,
1531        nbytes_op: &OpTy<'tcx>,
1532        flags_op: &OpTy<'tcx>,
1533    ) -> InterpResult<'tcx, Scalar> {
1534        let this = self.eval_context_mut();
1535
1536        let fd = this.read_scalar(fd_op)?.to_i32()?;
1537        let offset = this.read_scalar(offset_op)?.to_i64()?;
1538        let nbytes = this.read_scalar(nbytes_op)?.to_i64()?;
1539        let flags = this.read_scalar(flags_op)?.to_i32()?;
1540
1541        if offset < 0 || nbytes < 0 {
1542            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1543        }
1544        let allowed_flags = this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_BEFORE")
1545            | this.eval_libc_i32("SYNC_FILE_RANGE_WRITE")
1546            | this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_AFTER");
1547        if flags & allowed_flags != flags {
1548            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1549        }
1550
1551        let Some(fd) = this.machine.fds.get(fd) else {
1552            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1553        };
1554        // Only regular files support synchronization.
1555        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1556            err_unsup_format!("`sync_data_range` is only supported on file-backed file descriptors")
1557        })?;
1558        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1559
1560        let io_result = maybe_sync_file(&file.file, file.writable, File::sync_data);
1561        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1562    }
1563
1564    fn readlink(
1565        &mut self,
1566        pathname_op: &OpTy<'tcx>,
1567        buf_op: &OpTy<'tcx>,
1568        bufsize_op: &OpTy<'tcx>,
1569    ) -> InterpResult<'tcx, i64> {
1570        let this = self.eval_context_mut();
1571
1572        let pathname = this.read_path_from_c_str(this.read_pointer(pathname_op)?)?;
1573        let buf = this.read_pointer(buf_op)?;
1574        let bufsize = this.read_target_usize(bufsize_op)?;
1575
1576        // Reject if isolation is enabled.
1577        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1578            this.reject_in_isolation("`readlink`", reject_with)?;
1579            this.set_last_error(LibcError("EACCES"))?;
1580            return interp_ok(-1);
1581        }
1582
1583        let result = std::fs::read_link(pathname);
1584        match result {
1585            Ok(resolved) => {
1586                // 'readlink' truncates the resolved path if the provided buffer is not large
1587                // enough, and does *not* add a null terminator. That means we cannot use the usual
1588                // `write_path_to_c_str` and have to re-implement parts of it ourselves.
1589                let resolved = this.convert_path(
1590                    Cow::Borrowed(resolved.as_ref()),
1591                    crate::shims::os_str::PathConversion::HostToTarget,
1592                );
1593                let mut path_bytes = resolved.as_encoded_bytes();
1594                let bufsize: usize = bufsize.try_into().unwrap();
1595                if path_bytes.len() > bufsize {
1596                    path_bytes = &path_bytes[..bufsize]
1597                }
1598                this.write_bytes_ptr(buf, path_bytes.iter().copied())?;
1599                interp_ok(path_bytes.len().try_into().unwrap())
1600            }
1601            Err(e) => {
1602                this.set_last_error(e)?;
1603                interp_ok(-1)
1604            }
1605        }
1606    }
1607
1608    fn isatty(&mut self, miri_fd: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1609        let this = self.eval_context_mut();
1610        // "returns 1 if fd is an open file descriptor referring to a terminal;
1611        // otherwise 0 is returned, and errno is set to indicate the error"
1612        let fd = this.read_scalar(miri_fd)?.to_i32()?;
1613        let error = if let Some(fd) = this.machine.fds.get(fd) {
1614            if fd.is_tty(this.machine.communicate()) {
1615                return interp_ok(Scalar::from_i32(1));
1616            } else {
1617                LibcError("ENOTTY")
1618            }
1619        } else {
1620            // FD does not exist
1621            LibcError("EBADF")
1622        };
1623        this.set_last_error(error)?;
1624        interp_ok(Scalar::from_i32(0))
1625    }
1626
1627    fn realpath(
1628        &mut self,
1629        path_op: &OpTy<'tcx>,
1630        processed_path_op: &OpTy<'tcx>,
1631    ) -> InterpResult<'tcx, Scalar> {
1632        let this = self.eval_context_mut();
1633        this.assert_target_os_is_unix("realpath");
1634
1635        let pathname = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
1636        let processed_ptr = this.read_pointer(processed_path_op)?;
1637
1638        // Reject if isolation is enabled.
1639        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1640            this.reject_in_isolation("`realpath`", reject_with)?;
1641            this.set_last_error(LibcError("EACCES"))?;
1642            return interp_ok(Scalar::from_target_usize(0, this));
1643        }
1644
1645        let result = std::fs::canonicalize(pathname);
1646        match result {
1647            Ok(resolved) => {
1648                let path_max = this
1649                    .eval_libc_i32("PATH_MAX")
1650                    .try_into()
1651                    .expect("PATH_MAX does not fit in u64");
1652                let dest = if this.ptr_is_null(processed_ptr)? {
1653                    // POSIX says behavior when passing a null pointer is implementation-defined,
1654                    // but GNU/linux, freebsd, netbsd, bionic/android, and macos all treat a null pointer
1655                    // similarly to:
1656                    //
1657                    // "If resolved_path is specified as NULL, then realpath() uses
1658                    // malloc(3) to allocate a buffer of up to PATH_MAX bytes to hold
1659                    // the resolved pathname, and returns a pointer to this buffer.  The
1660                    // caller should deallocate this buffer using free(3)."
1661                    // <https://man7.org/linux/man-pages/man3/realpath.3.html>
1662                    this.alloc_path_as_c_str(&resolved, MiriMemoryKind::C.into())?
1663                } else {
1664                    let (wrote_path, _) =
1665                        this.write_path_to_c_str(&resolved, processed_ptr, path_max)?;
1666
1667                    if !wrote_path {
1668                        // Note that we do not explicitly handle `FILENAME_MAX`
1669                        // (different from `PATH_MAX` above) as it is Linux-specific and
1670                        // seems like a bit of a mess anyway: <https://eklitzke.org/path-max-is-tricky>.
1671                        this.set_last_error(LibcError("ENAMETOOLONG"))?;
1672                        return interp_ok(Scalar::from_target_usize(0, this));
1673                    }
1674                    processed_ptr
1675                };
1676
1677                interp_ok(Scalar::from_maybe_pointer(dest, this))
1678            }
1679            Err(e) => {
1680                this.set_last_error(e)?;
1681                interp_ok(Scalar::from_target_usize(0, this))
1682            }
1683        }
1684    }
1685    fn mkstemp(&mut self, template_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1686        use rand::seq::IndexedRandom;
1687
1688        // POSIX defines the template string.
1689        const TEMPFILE_TEMPLATE_STR: &str = "XXXXXX";
1690
1691        let this = self.eval_context_mut();
1692        this.assert_target_os_is_unix("mkstemp");
1693
1694        // POSIX defines the maximum number of attempts before failure.
1695        //
1696        // `mkstemp()` relies on `tmpnam()` which in turn relies on `TMP_MAX`.
1697        // POSIX says this about `TMP_MAX`:
1698        // * Minimum number of unique filenames generated by `tmpnam()`.
1699        // * Maximum number of times an application can call `tmpnam()` reliably.
1700        //   * The value of `TMP_MAX` is at least 25.
1701        //   * On XSI-conformant systems, the value of `TMP_MAX` is at least 10000.
1702        // See <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/stdio.h.html>.
1703        let max_attempts = this.eval_libc_u32("TMP_MAX");
1704
1705        // Get the raw bytes from the template -- as a byte slice, this is a string in the target
1706        // (and the target is unix, so a byte slice is the right representation).
1707        let template_ptr = this.read_pointer(template_op)?;
1708        let mut template = this.eval_context_ref().read_c_str(template_ptr)?.to_owned();
1709        let template_bytes = template.as_mut_slice();
1710
1711        // Reject if isolation is enabled.
1712        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1713            this.reject_in_isolation("`mkstemp`", reject_with)?;
1714            return this.set_errno_and_return_neg1_i32(LibcError("EACCES"));
1715        }
1716
1717        // Get the bytes of the suffix we expect in _target_ encoding.
1718        let suffix_bytes = TEMPFILE_TEMPLATE_STR.as_bytes();
1719
1720        // At this point we have one `&[u8]` that represents the template and one `&[u8]`
1721        // that represents the expected suffix.
1722
1723        // Now we figure out the index of the slice we expect to contain the suffix.
1724        let start_pos = template_bytes.len().saturating_sub(suffix_bytes.len());
1725        let end_pos = template_bytes.len();
1726        let last_six_char_bytes = &template_bytes[start_pos..end_pos];
1727
1728        // If we don't find the suffix, it is an error.
1729        if last_six_char_bytes != suffix_bytes {
1730            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1731        }
1732
1733        // At this point we know we have 6 ASCII 'X' characters as a suffix.
1734
1735        // From <https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/sysdeps/posix/tempname.c#L175>
1736        const SUBSTITUTIONS: &[char; 62] = &[
1737            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
1738            'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
1739            'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
1740            'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
1741        ];
1742
1743        // The file is opened with specific options, which Rust does not expose in a portable way.
1744        // So we use specific APIs depending on the host OS.
1745        let mut fopts = OpenOptions::new();
1746        fopts.read(true).write(true).create_new(true);
1747
1748        cfg_select! {
1749            unix =>
1750            {
1751                use std::os::unix::fs::OpenOptionsExt;
1752                // Do not allow others to read or modify this file.
1753                fopts.mode(0o600);
1754                fopts.custom_flags(libc::O_EXCL);
1755            }
1756            windows =>
1757            {
1758                use std::os::windows::fs::OpenOptionsExt;
1759                // Do not allow others to read or modify this file.
1760                fopts.share_mode(0);
1761            }
1762            _ => {
1763                throw_unsup_format!("`mkstemp` is not supported on this host OS");
1764            }
1765        }
1766
1767        // If the generated file already exists, we will try again `max_attempts` many times.
1768        for _ in 0..max_attempts {
1769            let rng = this.machine.rng.get_mut();
1770
1771            // Generate a random unique suffix.
1772            let unique_suffix =
1773                (0..6).map(|_| SUBSTITUTIONS.choose(rng).unwrap()).collect::<String>();
1774
1775            // Replace the template string with the random string.
1776            template_bytes[start_pos..end_pos].copy_from_slice(unique_suffix.as_bytes());
1777
1778            // Write the modified template back to the passed in pointer to maintain POSIX semantics.
1779            this.write_bytes_ptr(template_ptr, template_bytes.iter().copied())?;
1780
1781            // See if we can create and open this file.
1782            let file = fopts.open(bytes_to_os_str(template_bytes)?);
1783            match file {
1784                Ok(f) => {
1785                    let fd = this.machine.fds.insert_new(FileHandle {
1786                        file: f,
1787                        writable: true,
1788                        readable: true,
1789                    });
1790                    return interp_ok(Scalar::from_i32(fd));
1791                }
1792                Err(e) =>
1793                    match e.kind() {
1794                        // If the random file already exists, keep trying.
1795                        ErrorKind::AlreadyExists => continue,
1796                        // Any other errors are returned to the caller.
1797                        _ => {
1798                            // "On error, -1 is returned, and errno is set to
1799                            // indicate the error"
1800                            return this.set_errno_and_return_neg1_i32(e);
1801                        }
1802                    },
1803            }
1804        }
1805
1806        // We ran out of attempts to create the file, return an error.
1807        this.set_errno_and_return_neg1_i32(LibcError("EEXIST"))
1808    }
1809}
1810
1811/// Extracts the number of seconds and nanoseconds elapsed between `time` and the unix epoch when
1812/// `time` is Ok. Returns `None` if `time` is an error. Fails if `time` happens before the unix
1813/// epoch.
1814fn extract_sec_and_nsec<'tcx>(
1815    time: std::io::Result<SystemTime>,
1816) -> InterpResult<'tcx, Option<(u64, u32)>> {
1817    match time.ok() {
1818        Some(time) => {
1819            let duration = system_time_to_duration(&time)?;
1820            interp_ok(Some((duration.as_secs(), duration.subsec_nanos())))
1821        }
1822        None => interp_ok(None),
1823    }
1824}
1825
1826fn file_type_to_mode_name(file_type: std::fs::FileType) -> &'static str {
1827    #[cfg(unix)]
1828    use std::os::unix::fs::FileTypeExt;
1829
1830    if file_type.is_file() {
1831        "S_IFREG"
1832    } else if file_type.is_dir() {
1833        "S_IFDIR"
1834    } else if file_type.is_symlink() {
1835        "S_IFLNK"
1836    } else {
1837        // Certain file types are only available when the host is a Unix system.
1838        #[cfg(unix)]
1839        {
1840            if file_type.is_socket() {
1841                return "S_IFSOCK";
1842            } else if file_type.is_fifo() {
1843                return "S_IFIFO";
1844            } else if file_type.is_char_device() {
1845                return "S_IFCHR";
1846            } else if file_type.is_block_device() {
1847                return "S_IFBLK";
1848            }
1849        }
1850        "S_IFREG"
1851    }
1852}
1853
1854/// Stores a file's metadata in order to avoid code duplication in the different metadata related
1855/// shims.
1856///
1857/// Some fields are host/platform-specific. `None` means that Miri does not have a real value for
1858/// this field, for example because the metadata is synthetic or because the host platform does not
1859/// expose it. `statx` must only advertise the corresponding `STATX_*` bit when the field is `Some`;
1860/// legacy `stat` writes zero for `None` to preserve the old fallback behavior.
1861struct FileMetadata {
1862    /// This holds both the file type (dir, regular, symlink, ...) and permissions.
1863    mode: u32,
1864    size: u64,
1865    created: Option<(u64, u32)>,
1866    accessed: Option<(u64, u32)>,
1867    modified: Option<(u64, u32)>,
1868    dev: Option<u64>,
1869    ino: Option<u64>,
1870    nlink: Option<u64>,
1871    uid: Option<u32>,
1872    gid: Option<u32>,
1873    blksize: Option<u64>,
1874    blocks: Option<u64>,
1875}
1876
1877impl FileMetadata {
1878    fn from_path<'tcx>(
1879        ecx: &mut MiriInterpCx<'tcx>,
1880        path: &Path,
1881        follow_symlink: bool,
1882    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1883        let metadata =
1884            if follow_symlink { std::fs::metadata(path) } else { std::fs::symlink_metadata(path) };
1885
1886        FileMetadata::from_meta(ecx, metadata)
1887    }
1888
1889    fn from_fd_num<'tcx>(
1890        ecx: &mut MiriInterpCx<'tcx>,
1891        fd_num: i32,
1892    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1893        let Some(fd) = ecx.machine.fds.get(fd_num) else {
1894            return interp_ok(Err(LibcError("EBADF")));
1895        };
1896        match fd.metadata()? {
1897            Either::Left(host) => Self::from_meta(ecx, host),
1898            Either::Right(name) => Self::synthetic(ecx, name),
1899        }
1900    }
1901
1902    fn synthetic<'tcx>(
1903        ecx: &mut MiriInterpCx<'tcx>,
1904        mode_name: &str,
1905    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1906        let mode = ecx.eval_libc(mode_name);
1907        let mode: u32 = mode.to_uint(ecx.libc_ty_layout("mode_t").size)?.try_into().unwrap();
1908        // We observed 0x777 on sockets and 0x600 on pipes...
1909        let mode = mode | 0o666;
1910        interp_ok(Ok(FileMetadata {
1911            mode,
1912            size: 0,
1913            created: None,
1914            accessed: None,
1915            modified: None,
1916            dev: None,
1917            uid: None,
1918            gid: None,
1919            blksize: None,
1920            blocks: None,
1921            ino: None,
1922            nlink: None,
1923        }))
1924    }
1925
1926    fn from_meta<'tcx>(
1927        ecx: &mut MiriInterpCx<'tcx>,
1928        metadata: Result<std::fs::Metadata, std::io::Error>,
1929    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1930        let metadata = match metadata {
1931            Ok(metadata) => metadata,
1932            Err(e) => {
1933                return interp_ok(Err(e.into()));
1934            }
1935        };
1936
1937        let file_type = metadata.file_type();
1938        let mode = ecx.eval_libc(file_type_to_mode_name(file_type));
1939        let mut mode = mode.to_uint(ecx.libc_ty_layout("mode_t").size)?.try_into().unwrap();
1940
1941        let size = metadata.len();
1942
1943        let created = extract_sec_and_nsec(metadata.created())?;
1944        let accessed = extract_sec_and_nsec(metadata.accessed())?;
1945        let modified = extract_sec_and_nsec(metadata.modified())?;
1946
1947        // FIXME: Provide more fields using platform specific methods.
1948
1949        cfg_select! {
1950            unix => {
1951                use std::os::unix::fs::MetadataExt;
1952                use std::os::unix::fs::PermissionsExt;
1953
1954                let dev = metadata.dev();
1955                let ino = metadata.ino();
1956                let nlink = metadata.nlink();
1957                let uid = metadata.uid();
1958                let gid = metadata.gid();
1959                let blksize = metadata.blksize();
1960                let blocks = metadata.blocks();
1961
1962                mode |= metadata.permissions().mode();
1963
1964                interp_ok(Ok(FileMetadata {
1965                    mode,
1966                    size,
1967                    created,
1968                    accessed,
1969                    modified,
1970                    dev: Some(dev),
1971                    ino: Some(ino),
1972                    nlink: Some(nlink),
1973                    uid: Some(uid),
1974                    gid: Some(gid),
1975                    blksize: Some(blksize),
1976                    blocks: Some(blocks),
1977                }))
1978            }
1979            _ => {
1980                // Emulate "everyone can read" or "everyone can read and write".
1981                mode |= if metadata.permissions().readonly() { 0o111 } else { 0o333 };
1982
1983                interp_ok(Ok(FileMetadata {
1984                    mode,
1985                    size,
1986                    created,
1987                    accessed,
1988                    modified,
1989                    dev: None,
1990                    ino: None,
1991                    nlink: None,
1992                    uid: None,
1993                    gid: None,
1994                    blksize: None,
1995                    blocks: None,
1996                }))
1997            },
1998        }
1999    }
2000}