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