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