Skip to main content

miri/shims/unix/
fs.rs

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