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