miri/shims/unix/
fs.rs

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