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::{self, DirBuilder, File, FileTimes, OpenOptions, TryLockError};
6use std::io::{self, ErrorKind, Read, Seek, SeekFrom, Write};
7use std::path::{self, Path};
8use std::time::SystemTime;
9
10use rand::RngExt;
11use rustc_abi::Size;
12use rustc_data_structures::either::Either;
13use rustc_data_structures::fx::FxHashMap;
14use rustc_target::spec::Os;
15
16use self::shims::time::system_time_to_duration;
17use crate::shims::files::{
18    BIG_ACCESS, DirHandle, DynFileDescriptionRef, EvalContextExt as _, FdNum, FileHandle,
19    open_file_or_dir,
20};
21use crate::shims::os_str::{PathConversion, bytes_to_os_str};
22use crate::shims::sig::Varargs;
23use crate::shims::unix::fd::{EvalContextExt as _, FlockOp, UnixFileDescription};
24use crate::shims::{FdId, FileDescription};
25use crate::*;
26
27/// An open directory stream (`DIR`), tracked by DirTable.
28#[derive(Debug)]
29struct DirStream {
30    /// The directory stream on the host.
31    read_dir: fs::ReadDir,
32    /// Unix directory streams have an "underlying FD" that can be exposed; this is that FD. This is
33    /// not a reference; it *can* be separately closed, leading to UB if the DIR is used again. We
34    /// also store the FD ID to catch cases where the FD was closed and a different one re-opened.
35    fd_num: FdNum,
36    fd_id: FdId,
37    /// The "special" entries that must still be yielded by the iterator.
38    /// Used for `.` and `..`.
39    special_entries: Vec<&'static str>,
40    /// The most recent entry returned by readdir(). Will be freed by the next call.
41    /// The string is the type used to allocate it.
42    entry: Option<(Pointer, &'static str)>,
43}
44
45impl DirStream {
46    fn new(read_dir: fs::ReadDir, fd_num: FdNum, fd_id: FdId) -> Self {
47        Self { read_dir, fd_num, fd_id, special_entries: vec!["..", "."], entry: None }
48    }
49
50    fn next_host_entry(&mut self) -> Option<io::Result<Either<fs::DirEntry, &'static str>>> {
51        if let Some(special) = self.special_entries.pop() {
52            return Some(Ok(Either::Right(special)));
53        }
54        let entry = self.read_dir.next()?;
55        Some(entry.map(Either::Left))
56    }
57}
58
59#[derive(Debug)]
60struct DirEntry {
61    name: OsString,
62    ino: u64,
63    d_type: i32,
64}
65
66/// What a `futimens` `timespec` asks for: leave the timestamp alone (`UTIME_OMIT`) or set it.
67#[derive(Copy, Clone)]
68enum TimeUpdate {
69    Omit,
70    Set(SystemTime),
71}
72
73impl UnixFileDescription for FileHandle {
74    fn pread<'tcx>(
75        &self,
76        communicate_allowed: bool,
77        ptr: Pointer,
78        len: u64,
79        offset: u64,
80        ecx: &mut MiriInterpCx<'tcx>,
81        finish: DynMachineCallback<'tcx, Result<u64, IoError>>,
82    ) -> InterpResult<'tcx> {
83        assert!(communicate_allowed, "isolation should have prevented even opening a file");
84        if !self.readable {
85            return finish.call(ecx, Err(LibcError("EBADF")));
86        }
87
88        let file = &mut &self.file;
89        // Emulates pread using seek + read + seek to restore cursor position.
90        // Correctness of this emulation relies on sequential nature of Miri execution.
91        let result = ecx.read_from_host(ptr, len, |buf| {
92            let cursor_pos = file.stream_position()?;
93            file.seek(SeekFrom::Start(offset))?;
94            let res = file.read(buf);
95            // Attempt to restore cursor position even if the read has failed
96            file.seek(SeekFrom::Start(cursor_pos))
97                .expect("failed to restore file position, this shouldn't be possible");
98            res
99        })?;
100        finish.call(ecx, result)
101    }
102
103    fn pwrite<'tcx>(
104        &self,
105        communicate_allowed: bool,
106        ptr: Pointer,
107        len: u64,
108        offset: u64,
109        ecx: &mut MiriInterpCx<'tcx>,
110        finish: DynMachineCallback<'tcx, Result<u64, IoError>>,
111    ) -> InterpResult<'tcx> {
112        assert!(communicate_allowed, "isolation should have prevented even opening a file");
113        if !self.writable {
114            return finish.call(ecx, Err(LibcError("EBADF")));
115        }
116
117        let file = &mut &self.file;
118        // Emulates pwrite using seek + write + seek to restore cursor position.
119        // Correctness of this emulation relies on sequential nature of Miri execution.
120        let result = ecx.write_to_host(ptr, len, |buf| {
121            let cursor_pos = file.stream_position()?;
122            file.seek(SeekFrom::Start(offset))?;
123            let res = file.write(buf);
124            // Attempt to restore cursor position even if the write has failed
125            file.seek(SeekFrom::Start(cursor_pos))
126                .expect("failed to restore file position, this shouldn't be possible");
127            res
128        })?;
129        finish.call(ecx, result)
130    }
131
132    fn flock<'tcx>(
133        &self,
134        communicate_allowed: bool,
135        op: FlockOp,
136    ) -> InterpResult<'tcx, io::Result<()>> {
137        assert!(communicate_allowed, "isolation should have prevented even opening a file");
138
139        use FlockOp::*;
140        // We must not block the interpreter loop, so we always `try_lock`.
141        let (res, nonblocking) = match op {
142            SharedLock { nonblocking } => (self.file.try_lock_shared(), nonblocking),
143            ExclusiveLock { nonblocking } => (self.file.try_lock(), nonblocking),
144            Unlock => {
145                return interp_ok(self.file.unlock());
146            }
147        };
148
149        match res {
150            Ok(()) => interp_ok(Ok(())),
151            Err(TryLockError::Error(err)) => interp_ok(Err(err)),
152            Err(TryLockError::WouldBlock) =>
153                if nonblocking {
154                    interp_ok(Err(ErrorKind::WouldBlock.into()))
155                } else {
156                    throw_unsup_format!("blocking `flock` is not currently supported");
157                },
158        }
159    }
160
161    fn get_flags<'tcx>(&self, ecx: &mut MiriInterpCx<'tcx>) -> InterpResult<'tcx, Scalar> {
162        interp_ok(match (self.readable, self.writable) {
163            (true, true) => ecx.eval_libc("O_RDWR"),
164            (true, false) => ecx.eval_libc("O_RDONLY"),
165            (false, true) => ecx.eval_libc("O_WRONLY"),
166            _ => unreachable!(),
167        })
168    }
169}
170
171impl UnixFileDescription for DirHandle {
172    fn get_flags<'tcx>(&self, ecx: &mut MiriInterpCx<'tcx>) -> InterpResult<'tcx, Scalar> {
173        // Directories are always readonly on Unix.
174        interp_ok(ecx.eval_libc("O_RDONLY"))
175    }
176}
177
178/// The table of open directories.
179/// Curiously, Unix/POSIX does not unify this into the "file descriptor" concept... everything
180/// is a file, except a directory is not?
181#[derive(Debug)]
182pub struct DirTable {
183    /// Directory iterators used to emulate libc "directory streams", as used in opendir, readdir,
184    /// and closedir.
185    ///
186    /// When opendir is called, a directory iterator is created on the host for the target
187    /// directory, and an entry is stored in this hash map, indexed by an ID which represents
188    /// the directory stream. When readdir is called, the directory stream ID is used to look up
189    /// the corresponding ReadDir iterator from this map, and information from the next
190    /// directory entry is returned. When closedir is called, the ReadDir iterator is removed from
191    /// the map.
192    streams: FxHashMap<u64, DirStream>,
193    /// ID number to be used by the next call to opendir
194    next_id: u64,
195}
196
197impl DirTable {
198    #[expect(clippy::arithmetic_side_effects)]
199    fn insert_new(&mut self, stream: DirStream) -> u64 {
200        let id = self.next_id;
201        self.next_id += 1;
202        self.streams.try_insert(id, stream).unwrap();
203        id
204    }
205}
206
207impl Default for DirTable {
208    fn default() -> DirTable {
209        DirTable {
210            streams: FxHashMap::default(),
211            // Skip 0 as an ID, because it looks like a null pointer to libc
212            next_id: 1,
213        }
214    }
215}
216
217impl VisitProvenance for DirTable {
218    fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
219        let DirTable { streams, next_id: _ } = self;
220
221        for dir in streams.values() {
222            dir.entry.visit_provenance(visit);
223        }
224    }
225}
226
227fn maybe_sync_file(
228    file: &FileHandle,
229    operation: fn(&File) -> std::io::Result<()>,
230) -> std::io::Result<i32> {
231    if !file.writable && cfg!(windows) {
232        // sync_all() and sync_data() will return an error on Windows hosts if the file is not opened
233        // for writing. (FlushFileBuffers requires that the file handle have the
234        // GENERIC_WRITE right)
235        Ok(0i32)
236    } else {
237        let result = operation(&file.file);
238        result.map(|_| 0i32)
239    }
240}
241
242/// The host `FileTimes` that applies the given access and modification time updates.
243fn host_file_times_from_updates(access: TimeUpdate, modified: TimeUpdate) -> FileTimes {
244    let mut filetimes = FileTimes::new();
245    if let TimeUpdate::Set(access) = access {
246        filetimes = filetimes.set_accessed(access);
247    }
248    if let TimeUpdate::Set(modified) = modified {
249        filetimes = filetimes.set_modified(modified);
250    }
251    filetimes
252}
253
254fn same_host_file(file1: &fs::Metadata, file2: &fs::Metadata) -> bool {
255    cfg_select! {
256        unix => {
257            use std::os::unix::fs::MetadataExt;
258            file1.dev() == file2.dev() && file1.ino() == file2.ino()
259        }
260        windows => {
261            use std::os::windows::fs::MetadataExt;
262            file1.volume_serial_number().unwrap() == file2.volume_serial_number().unwrap()
263                && file1.file_index().unwrap() == file2.file_index().unwrap()
264        }
265    }
266}
267
268impl<'tcx> EvalContextExtPrivate<'tcx> for crate::MiriInterpCx<'tcx> {}
269trait EvalContextExtPrivate<'tcx>: crate::MiriInterpCxExt<'tcx> {
270    /// Decode one `futimens` `timespec`, handling the `UTIME_NOW`/`UTIME_OMIT` `tv_nsec` values.
271    /// `None` means the `timespec` is invalid and the caller should report `EINVAL`.
272    fn parse_utimens_timespec(
273        &self,
274        tp: &MPlaceTy<'tcx>,
275    ) -> InterpResult<'tcx, Option<TimeUpdate>> {
276        let this = self.eval_context_ref();
277        // `UTIME_NOW` reads the host clock, which we must not do under isolation.
278        assert!(this.machine.communicate(), "isolation should have prevented reaching this");
279
280        // `tv_nsec` and the `UTIME_*` constants are `c_long`, i.e. the target's `isize`.
281        let nsec_place = this.project_field_named(tp, "tv_nsec")?;
282        let nsec = this.read_scalar(&nsec_place)?.to_target_isize(this)?;
283
284        if nsec == this.eval_libc("UTIME_OMIT").to_target_isize(this)? {
285            return interp_ok(Some(TimeUpdate::Omit));
286        }
287        if nsec == this.eval_libc("UTIME_NOW").to_target_isize(this)? {
288            return interp_ok(Some(TimeUpdate::Set(SystemTime::now())));
289        }
290
291        let Some(duration) = this.read_timespec(tp)? else {
292            return interp_ok(None);
293        };
294        interp_ok(SystemTime::UNIX_EPOCH.checked_add(duration).map(TimeUpdate::Set))
295    }
296
297    /// Decode the `times` argument of `futimens`/`utimensat`: `[atime, mtime]`, or NULL to set both
298    /// to now, as `(access, modified)`.
299    /// `None` means a `timespec` is invalid and the caller should report `EINVAL`.
300    fn parse_utimens_times(
301        &self,
302        times: &MPlaceTy<'tcx>,
303    ) -> InterpResult<'tcx, Option<(TimeUpdate, TimeUpdate)>> {
304        let this = self.eval_context_ref();
305
306        if this.ptr_is_null(times.ptr())? {
307            let now = TimeUpdate::Set(SystemTime::now());
308            interp_ok(Some((now, now)))
309        } else {
310            let access_place = this.project_c_array_field(times, 0)?;
311            let modified_place = this.project_c_array_field(times, 1)?;
312            let Some(access) = this.parse_utimens_timespec(&access_place)? else {
313                return interp_ok(None);
314            };
315            let Some(modified) = this.parse_utimens_timespec(&modified_place)? else {
316                return interp_ok(None);
317            };
318            interp_ok(Some((access, modified)))
319        }
320    }
321
322    fn write_stat_buf(
323        &mut self,
324        metadata: FileMetadata,
325        buf_op: &OpTy<'tcx>,
326    ) -> InterpResult<'tcx, i32> {
327        let this = self.eval_context_mut();
328
329        let (access_sec, access_nsec) = metadata.accessed.unwrap_or((0, 0));
330        let (created_sec, created_nsec) = metadata.created.unwrap_or((0, 0));
331        let (modified_sec, modified_nsec) = metadata.modified.unwrap_or((0, 0));
332
333        let buf = this.deref_pointer(buf_op)?;
334
335        this.write_int_fields_named(
336            &[
337                ("st_dev", metadata.dev.unwrap_or(0).into()),
338                ("st_mode", metadata.mode.into()),
339                ("st_nlink", metadata.nlink.unwrap_or(0).into()),
340                ("st_ino", metadata.ino.unwrap_or(0).into()),
341                ("st_uid", metadata.uid.unwrap_or(0).into()),
342                ("st_gid", metadata.gid.unwrap_or(0).into()),
343                ("st_rdev", 0),
344                ("st_atime", access_sec.into()),
345                ("st_atime_nsec", access_nsec.into()),
346                ("st_mtime", modified_sec.into()),
347                ("st_mtime_nsec", modified_nsec.into()),
348                ("st_ctime", 0),
349                ("st_ctime_nsec", 0),
350                ("st_size", metadata.size.into()),
351                ("st_blocks", metadata.blocks.unwrap_or(0).into()),
352                ("st_blksize", metadata.blksize.unwrap_or(0).into()),
353            ],
354            &buf,
355        )?;
356
357        if matches!(&this.tcx.sess.target.os, Os::MacOs | Os::FreeBsd) {
358            this.write_int_fields_named(
359                &[
360                    ("st_birthtime", created_sec.into()),
361                    ("st_birthtime_nsec", created_nsec.into()),
362                    ("st_flags", 0),
363                    ("st_gen", 0),
364                ],
365                &buf,
366            )?;
367        }
368
369        if matches!(&this.tcx.sess.target.os, Os::Solaris | Os::Illumos) {
370            let st_fstype = this.project_field_named(&buf, "st_fstype")?;
371            // This is an array; write 0 into first element so that it encodes the empty string.
372            this.write_int(0, &this.project_index(&st_fstype, 0)?)?;
373        }
374
375        interp_ok(0)
376    }
377
378    #[cfg(windows)]
379    fn file_type_to_d_type(&self, file_type: fs::FileType) -> InterpResult<'tcx, i32> {
380        let this = self.eval_context_ref();
381        match () {
382            _ if file_type.is_dir() => interp_ok(this.eval_libc("DT_DIR").to_u8()?.into()),
383            _ if file_type.is_file() => interp_ok(this.eval_libc("DT_REG").to_u8()?.into()),
384            _ if file_type.is_symlink() => interp_ok(this.eval_libc("DT_LNK").to_u8()?.into()),
385            // Fallback
386            _ => interp_ok(this.eval_libc("DT_UNKNOWN").to_u8()?.into()),
387        }
388    }
389
390    fn dir_entry_fields(
391        &self,
392        entry: Either<fs::DirEntry, &'static str>,
393    ) -> InterpResult<'tcx, DirEntry> {
394        let this = self.eval_context_ref();
395        interp_ok(match entry {
396            Either::Left(dir_entry) => {
397                cfg_select! {
398                    unix => {
399                        use std::os::unix::fs::DirEntryExt;
400                        DirEntry {
401                            name: dir_entry.file_name(),
402                            d_type: {
403                                // We cannot rely on `dir_entry.file_type`, it might use a separate
404                                // syscall based on `file_name` which is racy.
405                                // It's always okay to return DT_UNKNOWN instead.
406                                this.eval_libc("DT_UNKNOWN").to_u8()?.into()
407                            },
408                            ino: dir_entry.ino(),
409                        }
410                    }
411                    windows => {
412                        DirEntry {
413                            name: dir_entry.file_name(),
414                            // `DirEntry::file_type` is always race-free and never fails on Windows.
415                            d_type: this.file_type_to_d_type(dir_entry.file_type().unwrap())?,
416                            ino: 0u64,
417                        }
418                    }
419                }
420            }
421            Either::Right(special) =>
422                DirEntry {
423                    name: special.into(),
424                    d_type: this.eval_libc("DT_DIR").to_u8()?.into(),
425                    ino: 0,
426                },
427        })
428    }
429
430    fn host_permissions_from_mode(
431        &self,
432        mode: u32,
433        old_permissions: impl FnOnce() -> io::Result<fs::Permissions>,
434    ) -> InterpResult<'tcx, io::Result<fs::Permissions>> {
435        cfg_select! {
436            unix => {
437                use std::os::unix::fs::PermissionsExt;
438
439                let _ = old_permissions;
440                interp_ok(Ok(fs::Permissions::from_mode(mode)))
441            }
442            _ => {
443                let ecx = self.eval_context_ref();
444
445                let mut permissions = match old_permissions() {
446                    Ok(p) => p,
447                    Err(err) => return interp_ok(Err(err)),
448                };
449                // Only support default mode for non-UNIX (i.e. Windows) host.
450                // Ignore the "regular file" bit which is a NOP.
451                let s_ifreg = ecx.eval_libc("S_IFREG");
452                let s_ifreg = u32::try_from(s_ifreg.to_bits(s_ifreg.size())?).unwrap();
453                match mode & !s_ifreg {
454                    0o666 => {
455                        permissions.set_readonly(false);
456                    }
457                    0o444 => {
458                        permissions.set_readonly(true);
459                    }
460                    _ => {
461                        throw_unsup_format!(
462                            "changing permissions to mode 0o{:o} is not supported on non-Unix hosts",
463                            mode
464                        );
465                    }
466                }
467                interp_ok(Ok(permissions))
468            }
469        }
470    }
471}
472
473impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
474pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
475    fn open(
476        &mut self,
477        dirfd: Option<&OpTy<'tcx>>,
478        path_raw: &OpTy<'tcx>,
479        flag: &OpTy<'tcx>,
480        varargs: Varargs<'tcx, '_>,
481    ) -> InterpResult<'tcx, Scalar> {
482        #[cfg(unix)]
483        use std::os::unix::fs::OpenOptionsExt;
484        #[cfg(windows)]
485        use std::os::windows::fs::OpenOptionsExt;
486
487        let ecx = self.eval_context_mut();
488        let open_name = if dirfd.is_some() { "openat" } else { "open" };
489
490        let dirfd = match dirfd {
491            Some(dirfd) => Some(ecx.read_scalar(dirfd)?.to_i32()?),
492            None => None,
493        };
494        let path_raw = ecx.read_pointer(path_raw)?;
495        let flag = ecx.read_scalar(flag)?.to_i32()?;
496
497        let path = ecx.read_path_from_c_str(path_raw)?;
498        // Files in `/proc` won't work properly.
499        if matches!(ecx.tcx.sess.target.os, Os::Linux | Os::Android | Os::Illumos | Os::Solaris)
500            && path::absolute(&path).is_ok_and(|path| path.starts_with("/proc"))
501        {
502            ecx.machine.emit_diagnostic(NonHaltingDiagnostic::FileInProcOpened);
503        }
504
505        // Helper for those flags that read the `mode` parameter.
506        let read_mode = |ecx: &MiriInterpCx<'tcx>,
507                         options: &mut OpenOptions,
508                         fn_name: &str|
509         -> InterpResult<'tcx> {
510            let ([mode], _) = ecx.check_varargs(
511                if ecx.libc_ty_layout("mode_t").size.bytes() >= 4 {
512                    // `mode_t` is big enough, no C integer promotion.
513                    shim_varargs![libc::mode_t]
514                } else {
515                    // Types smaller than int get promoted to int
516                    // (see https://github.com/rust-lang/rust/issues/71915).
517                    shim_varargs![i32]
518                },
519                varargs,
520                fn_name,
521            )?;
522            let mode = ecx.read_scalar(&mode)?.to_u32()?;
523
524            cfg_select! {
525                unix => {
526                    // Support all modes on UNIX host
527                    options.mode(mode);
528                }
529                _ => {
530                    let _unused = options;
531
532                    // Only support default mode for non-UNIX (i.e. Windows) host.
533                    // Ignore the "regular file" bit which is a NOP.
534                    let s_ifreg = ecx.eval_libc("S_IFREG");
535                    let s_ifreg = u32::try_from(s_ifreg.to_bits(s_ifreg.size())?).unwrap();
536                    if mode & !s_ifreg != 0o666 {
537                        throw_unsup_format!(
538                            "{open_name}: non-default mode 0o{:o} is not supported on non-Unix hosts",
539                            mode
540                        );
541                    }
542                }
543            }
544
545            interp_ok(())
546        };
547
548        // We will "subtract" supported flags from this and at the end check that no bits are left.
549        let mut flag = flag;
550
551        let mut options = OpenOptions::new();
552        let mut custom_flags = 0;
553
554        let o_rdonly = ecx.eval_libc_i32("O_RDONLY");
555        let o_wronly = ecx.eval_libc_i32("O_WRONLY");
556        let o_rdwr = ecx.eval_libc_i32("O_RDWR");
557        // The first two bits of the flag correspond to the access mode in linux, macOS and
558        // windows. We need to check that in fact the access mode flags for the current target
559        // only use these two bits, otherwise we are in an unsupported target and should error.
560        if (o_rdonly | o_wronly | o_rdwr) & !0b11 != 0 {
561            throw_unsup_format!("{open_name}: access mode flags on this target are unsupported");
562        }
563        let mut writable = true;
564        let mut readable = true;
565
566        // Now we check the access mode
567        let access_mode = flag & ecx.eval_libc_i32("O_ACCMODE");
568        flag &= !access_mode;
569
570        if access_mode == o_rdonly {
571            writable = false;
572            options.read(true);
573        } else if access_mode == o_wronly {
574            readable = false;
575            options.write(true);
576        } else if access_mode == o_rdwr {
577            options.read(true).write(true);
578        } else {
579            throw_unsup_format!("{open_name}: unsupported access mode {:#x}", access_mode);
580        }
581
582        if ecx.tcx.sess.target.os == Os::Linux {
583            let o_largefile = ecx.eval_libc_i32("O_LARGEFILE");
584            if flag & o_largefile == o_largefile {
585                flag &= !o_largefile;
586                // `fs::File` always opens in largefile mode so this is a NOP.
587            }
588            let o_tmpfile = ecx.eval_libc_i32("O_TMPFILE");
589            // Note that this overaps with O_DIRECTORY! So we have to check O_TMPFILE first.
590            if flag & o_tmpfile == o_tmpfile {
591                // This flag requires a mode.
592                read_mode(
593                    ecx,
594                    &mut options,
595                    if dirfd.is_some() {
596                        "openat(dirfd, pathname, O_TMPFILE, ...)"
597                    } else {
598                        "open(pathname, O_TMPFILE, ...)"
599                    },
600                )?;
601
602                // We don't currently support this, error gracefully.
603                return ecx.set_errno_and_return_neg1_i32(LibcError("EOPNOTSUPP"));
604            }
605        }
606        let o_directory = ecx.eval_libc_i32("O_DIRECTORY");
607        let mut want_directory = false;
608        if flag & o_directory == o_directory {
609            flag &= !o_directory;
610            want_directory = true;
611            // On Unix we can ask the host to only open directories. That's helpful especially in
612            // combination with O_NOFOLLOW as it affects which error we get when the final component
613            // is a symlink. Fixing the error up ourselves is non-trivial so we make the host
614            // generate the right error.
615            #[cfg(unix)]
616            {
617                custom_flags |= libc::O_DIRECTORY;
618            }
619        }
620        let o_append = ecx.eval_libc_i32("O_APPEND");
621        let append = flag & o_append == o_append;
622        if append {
623            flag &= !o_append;
624            options.append(true);
625        }
626        let o_trunc = ecx.eval_libc_i32("O_TRUNC");
627        if flag & o_trunc == o_trunc {
628            flag &= !o_trunc;
629            options.truncate(true);
630        }
631        let o_creat = ecx.eval_libc_i32("O_CREAT");
632        if flag & o_creat == o_creat {
633            flag &= !o_creat;
634            // Get the mode.
635            read_mode(
636                ecx,
637                &mut options,
638                if dirfd.is_some() {
639                    "openat(dirfd, pathname, O_CREAT, ...)"
640                } else {
641                    "open(pathname, O_CREAT, ...)"
642                },
643            )?;
644
645            if want_directory {
646                // O_CREAT + O_DIRECTORY is invalid.
647                return ecx.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
648            }
649
650            // O_EXCL is only allowed if O_CREAT is set.
651            let o_excl = ecx.eval_libc_i32("O_EXCL");
652            if flag & o_excl == o_excl {
653                flag &= !o_excl;
654                options.create_new(true);
655            } else {
656                options.create(true);
657            }
658        }
659        let o_cloexec = ecx.eval_libc_i32("O_CLOEXEC");
660        if flag & o_cloexec == o_cloexec {
661            flag &= !o_cloexec;
662            // We do not need to do anything for this flag because `std` already sets it.
663            // (Technically we do not support *not* setting this flag, but we ignore that.)
664        }
665        let o_nofollow = ecx.eval_libc_i32("O_NOFOLLOW");
666        let mut nofollow = false;
667        if flag & o_nofollow == o_nofollow {
668            flag &= !o_nofollow;
669            nofollow = true;
670            cfg_select! {
671                unix => {
672                    custom_flags |= libc::O_NOFOLLOW;
673                }
674                windows => {
675                    if dirfd.is_some() {
676                        throw_unsup_format!(
677                            "openat: `O_NOFOLLOW` is not supported on Windows hosts"
678                        );
679                    }
680                    custom_flags |=
681                        windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT;
682                }
683            }
684        }
685
686        // If `flag` has any bits left set, those are not supported.
687        if flag != 0 {
688            throw_unsup_format!("{open_name}: unsupported flags: {flag:#x}");
689        }
690
691        // Reject if isolation is enabled.
692        if let IsolatedOp::Reject(reject_with) = ecx.machine.isolated_op {
693            ecx.reject_in_isolation(&format!("`{open_name}`"), reject_with)?;
694            return ecx.set_errno_and_return_neg1_i32(ErrorKind::PermissionDenied);
695        }
696
697        let dirfd = match dirfd {
698            Some(dirfd) => {
699                if path.is_absolute() || dirfd == ecx.eval_libc_i32("AT_FDCWD") {
700                    None
701                } else {
702                    // relative to dirfd, which must be a directory handle
703                    let Some(fd) = ecx.machine.fds.get(dirfd) else {
704                        return ecx.set_errno_and_return_neg1_i32(LibcError("EBADF"));
705                    };
706                    let Some(dir) = fd.downcast::<DirHandle>() else {
707                        return ecx.set_errno_and_return_neg1_i32(LibcError("ENOTDIR"));
708                    };
709                    Some(dir)
710                }
711            }
712            None => None,
713        };
714
715        // Empty path should consistently error.
716        if path.is_empty() {
717            return ecx.set_errno_and_return_neg1_i32(LibcError("ENOENT"));
718        }
719        // Windows does not support "." relative to a `Dir` handle, but the empty path works.
720        // (https://github.com/rust-lang/rust/issues/163032)
721        let path = if cfg!(windows) && dirfd.is_some() && path.to_str() == Some(".") {
722            Path::new("")
723        } else {
724            &path
725        };
726
727        // Let's see what we get when we open this!
728        let f_or_d = open_file_or_dir(
729            dirfd.as_ref().map(|d| &d.dir),
730            path,
731            options.custom_flags(custom_flags),
732        );
733        match f_or_d {
734            Err(err) => ecx.set_errno_and_return_neg1_i32(err),
735            Ok(Either::Right(dir)) => {
736                // This means it cannot be a symlink, so `nofollow` is fine.
737                if writable {
738                    // On Windows, opening a folder writable can succeed.
739                    // But here we want it to always fail.
740                    return ecx.set_errno_and_return_neg1_i32(LibcError("EISDIR"));
741                }
742
743                let Some(fd) = ecx.machine.fds.insert_new(DirHandle { dir }) else {
744                    return ecx.set_errno_and_return_neg1_i32(LibcError("EMFILE"));
745                };
746                interp_ok(Scalar::from_i32(fd))
747            }
748            Ok(Either::Left(file)) => {
749                if want_directory {
750                    return ecx.set_errno_and_return_neg1_i32(LibcError("ENOTDIR"));
751                }
752                if file.metadata().unwrap().is_symlink() {
753                    if nofollow {
754                        // On Windows, FILE_FLAG_OPEN_REPARSE_POINT makes opening still succeed, it
755                        // just opens the symlink rather than the target. Turn that into an error.
756                        return ecx.set_errno_and_return_neg1_i32(LibcError("ELOOP"));
757                    }
758                    panic!("we should not get a symlink here");
759                }
760
761                let Some(fd) =
762                    ecx.machine.fds.insert_new(FileHandle { file, writable, readable, append })
763                else {
764                    return ecx.set_errno_and_return_neg1_i32(LibcError("EMFILE"));
765                };
766                interp_ok(Scalar::from_i32(fd))
767            }
768        }
769    }
770
771    fn lseek(
772        &mut self,
773        fd_num: i32,
774        offset: i128,
775        whence: i32,
776        dest: &MPlaceTy<'tcx>,
777    ) -> InterpResult<'tcx> {
778        let this = self.eval_context_mut();
779
780        // Isolation check is done via `FileDescription` trait.
781
782        let seek_from = if whence == this.eval_libc_i32("SEEK_SET") {
783            if offset < 0 {
784                // Negative offsets return `EINVAL`.
785                return this.set_errno_and_return_neg1(LibcError("EINVAL"), dest);
786            } else {
787                SeekFrom::Start(u64::try_from(offset).unwrap())
788            }
789        } else if whence == this.eval_libc_i32("SEEK_CUR") {
790            SeekFrom::Current(i64::try_from(offset).unwrap())
791        } else if whence == this.eval_libc_i32("SEEK_END") {
792            SeekFrom::End(i64::try_from(offset).unwrap())
793        } else {
794            return this.set_errno_and_return_neg1(LibcError("EINVAL"), dest);
795        };
796
797        let communicate = this.machine.communicate();
798
799        let Some(fd) = this.machine.fds.get(fd_num) else {
800            return this.set_errno_and_return_neg1(LibcError("EBADF"), dest);
801        };
802        let result = fd.seek(communicate, seek_from)?.map(|offset| i64::try_from(offset).unwrap());
803        drop(fd);
804
805        let result = this.try_unwrap_io_result(result)?;
806        this.write_int(result, dest)?;
807        interp_ok(())
808    }
809
810    fn unlink(&mut self, path_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
811        let this = self.eval_context_mut();
812
813        let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
814
815        // Reject if isolation is enabled.
816        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
817            this.reject_in_isolation("`unlink`", reject_with)?;
818            return this.set_errno_and_return_neg1_i32(ErrorKind::PermissionDenied);
819        }
820
821        let result = cfg_select! {
822            unix => fs::remove_file(path),
823            windows => {{
824                // This should be able to remove symlinks to dirs, but on Windows that requires
825                // `remove_dir`. Our work-around is racy but there's not a lot we can do about that.
826                // FIXME: maybe we can retry based on the error code?
827                use std::os::windows::fs::FileTypeExt;
828                let metadata = path.symlink_metadata();
829                if metadata.is_ok_and(|m| m.file_type().is_symlink_dir()) {
830                    fs::remove_dir(path)
831                } else {
832                    fs::remove_file(path)
833                }
834            }}
835        };
836        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result.map(|()| 0))?))
837    }
838
839    fn symlink(
840        &mut self,
841        target_op: &OpTy<'tcx>,
842        linkpath_op: &OpTy<'tcx>,
843    ) -> InterpResult<'tcx, Scalar> {
844        fn create_link(target: &Path, link: &Path) -> std::io::Result<()> {
845            cfg_select! {
846                unix => std::os::unix::fs::symlink(target, link),
847                windows => {
848                    use std::os::windows::fs;
849
850                    // The target filename is interpreted relative to where `link` is located,
851                    // but we need it relative to where we are.
852                    let target_is_dir = link.parent().is_some_and(|dir| dir.join(target).is_dir());
853                    // This is racy, but not much we can do about that.
854                    // FIXME: maybe we can retry based on the error code?
855                    if target_is_dir {
856                        fs::symlink_dir(target, link)
857                    } else {
858                        fs::symlink_file(target, link)
859                    }
860                }
861            }
862        }
863
864        let this = self.eval_context_mut();
865        let target = this.read_path_from_c_str(this.read_pointer(target_op)?)?;
866        let linkpath = this.read_path_from_c_str(this.read_pointer(linkpath_op)?)?;
867
868        // Reject if isolation is enabled.
869        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
870            this.reject_in_isolation("`symlink`", reject_with)?;
871            return this.set_errno_and_return_neg1_i32(ErrorKind::PermissionDenied);
872        }
873
874        let result = create_link(&target, &linkpath).map(|_| 0);
875        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
876    }
877
878    fn linkat(
879        &mut self,
880        oldfd_op: &OpTy<'tcx>,
881        oldpath_op: &OpTy<'tcx>,
882        newfd_op: &OpTy<'tcx>,
883        newpath_op: &OpTy<'tcx>,
884        flags_op: &OpTy<'tcx>,
885    ) -> InterpResult<'tcx, Scalar> {
886        let this = self.eval_context_mut();
887
888        // Load all arguments
889        let flags = this.read_scalar(flags_op)?.to_i32()?;
890        let oldfd = this.read_scalar(oldfd_op)?.to_i32()?;
891        let newfd = this.read_scalar(newfd_op)?.to_i32()?;
892        let oldpath_ptr = this.read_pointer(oldpath_op)?;
893        let newpath_ptr = this.read_pointer(newpath_op)?;
894
895        // Relevant libc constants
896        let at_fdcwd = this.eval_libc_i32("AT_FDCWD");
897
898        // Reject if isolation is enabled.
899        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
900            this.reject_in_isolation("`linkat`", reject_with)?;
901            return this.set_errno_and_return_neg1_i32(ErrorKind::PermissionDenied);
902        }
903
904        // Read flags - only support 0.
905        if flags != 0 {
906            throw_unsup_format!("unsupported linkat flags {:#x}", flags);
907        }
908
909        // Resolve oldpath
910        if oldfd != at_fdcwd {
911            throw_unsup_format!("linkat with `olddirfd` not equal to `AT_FDCWD` is not supported");
912        }
913        if this.ptr_is_null(oldpath_ptr)? {
914            return this.set_errno_and_return_neg1_i32(LibcError("EFAULT"));
915        }
916        let oldpath = this.read_path_from_c_str(oldpath_ptr)?.into_owned();
917
918        // Resolve newpath
919        if newfd != at_fdcwd {
920            throw_unsup_format!("linkat with `newdirfd` not equal to `AT_FDCWD` is not supported");
921        }
922        if this.ptr_is_null(newpath_ptr)? {
923            return this.set_errno_and_return_neg1_i32(LibcError("EFAULT"));
924        }
925        let newpath = this.read_path_from_c_str(newpath_ptr)?.into_owned();
926
927        let result = fs::hard_link(&oldpath, &newpath).map(|()| 0);
928        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
929    }
930
931    fn stat(&mut self, path_op: &OpTy<'tcx>, buf_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
932        let this = self.eval_context_mut();
933
934        if !matches!(
935            &this.tcx.sess.target.os,
936            Os::MacOs | Os::FreeBsd | Os::Solaris | Os::Illumos | Os::Android | Os::Linux
937        ) {
938            panic!("`stat` should not be called on {}", this.tcx.sess.target.os);
939        }
940
941        let path_scalar = this.read_pointer(path_op)?;
942        let path = this.read_path_from_c_str(path_scalar)?.into_owned();
943
944        // Reject if isolation is enabled.
945        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
946            this.reject_in_isolation("`stat`", reject_with)?;
947            return this.set_errno_and_return_neg1_i32(LibcError("EACCES"));
948        }
949
950        let metadata = match FileMetadata::from_host(this, fs::metadata(path))? {
951            Ok(metadata) => metadata,
952            Err(err) => return this.set_errno_and_return_neg1_i32(err),
953        };
954
955        interp_ok(Scalar::from_i32(this.write_stat_buf(metadata, buf_op)?))
956    }
957
958    // `lstat` is used to get symlink metadata.
959    fn lstat(&mut self, path_op: &OpTy<'tcx>, buf_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
960        let this = self.eval_context_mut();
961
962        if !matches!(
963            &this.tcx.sess.target.os,
964            Os::MacOs | Os::FreeBsd | Os::Solaris | Os::Illumos | Os::Android | Os::Linux
965        ) {
966            panic!("`lstat` should not be called on {}", this.tcx.sess.target.os);
967        }
968
969        let path_scalar = this.read_pointer(path_op)?;
970        let path = this.read_path_from_c_str(path_scalar)?.into_owned();
971
972        // Reject if isolation is enabled.
973        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
974            this.reject_in_isolation("`lstat`", reject_with)?;
975            return this.set_errno_and_return_neg1_i32(LibcError("EACCES"));
976        }
977
978        let metadata = match FileMetadata::from_host(this, fs::symlink_metadata(path))? {
979            Ok(metadata) => metadata,
980            Err(err) => return this.set_errno_and_return_neg1_i32(err),
981        };
982
983        interp_ok(Scalar::from_i32(this.write_stat_buf(metadata, buf_op)?))
984    }
985
986    fn fstat(&mut self, fd_op: &OpTy<'tcx>, buf_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
987        let this = self.eval_context_mut();
988
989        if !matches!(
990            &this.tcx.sess.target.os,
991            Os::MacOs | Os::FreeBsd | Os::Solaris | Os::Illumos | Os::Linux | Os::Android
992        ) {
993            panic!("`fstat` should not be called on {}", this.tcx.sess.target.os);
994        }
995
996        let fd = this.read_scalar(fd_op)?.to_i32()?;
997
998        // Reject if isolation is enabled.
999        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1000            this.reject_in_isolation("`fstat`", reject_with)?;
1001            // Set error code as "EBADF" (bad fd); "EACCESS" does not make much sense here.
1002            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1003        }
1004
1005        let metadata = match FileMetadata::from_fd_num(this, fd)? {
1006            Ok(metadata) => metadata,
1007            Err(err) => return this.set_errno_and_return_neg1_i32(err),
1008        };
1009        interp_ok(Scalar::from_i32(this.write_stat_buf(metadata, buf_op)?))
1010    }
1011
1012    fn fstatat(
1013        &mut self,
1014        dirfd_op: &OpTy<'tcx>,
1015        path_op: &OpTy<'tcx>,
1016        buf_op: &OpTy<'tcx>,
1017        flags_op: &OpTy<'tcx>,
1018    ) -> InterpResult<'tcx, Scalar> {
1019        let this = self.eval_context_mut();
1020
1021        if !matches!(
1022            &this.tcx.sess.target.os,
1023            Os::MacOs | Os::FreeBsd | Os::Solaris | Os::Illumos | Os::Linux | Os::Android
1024        ) {
1025            panic!("`fstatat` should not be called on {}", this.tcx.sess.target.os);
1026        }
1027
1028        let dirfd = this.read_scalar(dirfd_op)?.to_i32()?;
1029        let path = this.read_pointer(path_op)?;
1030        let flags = this.read_scalar(flags_op)?.to_i32()?;
1031
1032        // Reject if isolation is enabled.
1033        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1034            this.reject_in_isolation("`fstatat`", reject_with)?;
1035            return this.set_errno_and_return_neg1_i32(LibcError("EACCES"));
1036        }
1037
1038        // Parse flags.
1039        let mut flags = flags;
1040        // AT_SYMLINK_NOFOLLOW
1041        let at_symlink_nofollow = this.eval_libc_i32("AT_SYMLINK_NOFOLLOW");
1042        let symlink_nofollow_flag = flags & at_symlink_nofollow == at_symlink_nofollow;
1043        flags &= !at_symlink_nofollow;
1044        // Complain about unknown flags.
1045        if flags != 0 {
1046            throw_unsup_format!("unsupported flags for `fstatat`: {flags:#x}")
1047        }
1048
1049        let path = this.read_path_from_c_str(path)?.into_owned();
1050        // Resolve dirfd + path to metadata.
1051        let metadata = FileMetadata::at(
1052            this,
1053            dirfd,
1054            &path,
1055            symlink_nofollow_flag,
1056            /* empty_path_flag */ false,
1057        )?;
1058
1059        let metadata = match metadata {
1060            Ok(metadata) => metadata,
1061            Err(err) => return this.set_errno_and_return_neg1_i32(err),
1062        };
1063        interp_ok(Scalar::from_i32(this.write_stat_buf(metadata, buf_op)?))
1064    }
1065
1066    fn linux_statx(
1067        &mut self,
1068        dirfd_op: &OpTy<'tcx>,    // Should be an `int`
1069        pathname_op: &OpTy<'tcx>, // Should be a `const char *`
1070        flags_op: &OpTy<'tcx>,    // Should be an `int`
1071        mask_op: &OpTy<'tcx>,     // Should be an `unsigned int`
1072        statxbuf_op: &OpTy<'tcx>, // Should be a `struct statx *`
1073    ) -> InterpResult<'tcx, Scalar> {
1074        let this = self.eval_context_mut();
1075        this.assert_target_os_is_linux_like("statx");
1076
1077        let dirfd = this.read_scalar(dirfd_op)?.to_i32()?;
1078        let pathname_ptr = this.read_pointer(pathname_op)?;
1079        let flags = this.read_scalar(flags_op)?.to_i32()?;
1080        let _mask = this.read_scalar(mask_op)?.to_u32()?;
1081        let statxbuf_ptr = this.read_pointer(statxbuf_op)?;
1082
1083        // If the statxbuf or pathname pointers are null, the function fails with `EFAULT`.
1084        if this.ptr_is_null(statxbuf_ptr)? || this.ptr_is_null(pathname_ptr)? {
1085            return this.set_errno_and_return_neg1_i32(LibcError("EFAULT"));
1086        }
1087
1088        let statxbuf = this.deref_pointer(statxbuf_op)?;
1089        let path = this.read_path_from_c_str(pathname_ptr)?.into_owned();
1090
1091        // Parse flags.
1092        let mut flags = flags;
1093        // AT_EMPTY_PATH
1094        let at_empty_path = this.eval_libc_i32("AT_EMPTY_PATH");
1095        let empty_path_flag = flags & at_empty_path == at_empty_path;
1096        flags &= !at_empty_path;
1097        // AT_SYMLINK_NOFOLLOW
1098        let at_symlink_nofollow = this.eval_libc_i32("AT_SYMLINK_NOFOLLOW");
1099        let symlink_nofollow_flag = flags & at_symlink_nofollow == at_symlink_nofollow;
1100        flags &= !at_symlink_nofollow;
1101        // Complain about unknown flags.
1102        if flags != 0 {
1103            throw_unsup_format!("unsupported flags for `statx`: {flags:#x}")
1104        }
1105
1106        // Reject if isolation is enabled.
1107        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1108            this.reject_in_isolation("`statx`", reject_with)?;
1109            return this.set_errno_and_return_neg1_i32(LibcError("EACCES"));
1110        }
1111
1112        // Resolve dirfd + path to metadata.
1113        let metadata =
1114            FileMetadata::at(this, dirfd, &path, symlink_nofollow_flag, empty_path_flag)?;
1115
1116        let metadata = match metadata {
1117            Ok(metadata) => metadata,
1118            Err(err) => return this.set_errno_and_return_neg1_i32(err),
1119        };
1120
1121        // The `_mask_op` parameter specifies the file information that the caller requested.
1122        // However, `statx` is allowed to return information that was not requested or to not
1123        // return information that was requested. This `mask` represents the information we can
1124        // actually provide for any target.
1125        let mut mask = this.eval_libc_u32("STATX_TYPE")
1126            | this.eval_libc_u32("STATX_MODE")
1127            | this.eval_libc_u32("STATX_SIZE");
1128
1129        // Check which pieces of metadata we acquired, and set the appropriate flags in the mask.
1130        if metadata.ino.is_some() {
1131            mask |= this.eval_libc_u32("STATX_INO");
1132        }
1133        if metadata.nlink.is_some() {
1134            mask |= this.eval_libc_u32("STATX_NLINK");
1135        }
1136        if metadata.uid.is_some() {
1137            mask |= this.eval_libc_u32("STATX_UID");
1138        }
1139        if metadata.gid.is_some() {
1140            mask |= this.eval_libc_u32("STATX_GID");
1141        }
1142        if metadata.blocks.is_some() {
1143            mask |= this.eval_libc_u32("STATX_BLOCKS");
1144        }
1145
1146        // We need to set the corresponding bits of `mask` if the access, creation and modification
1147        // times were available. Otherwise we let them be zero.
1148        let (access_sec, access_nsec) = metadata
1149            .accessed
1150            .map(|tup| {
1151                mask |= this.eval_libc_u32("STATX_ATIME");
1152                interp_ok(tup)
1153            })
1154            .unwrap_or_else(|| interp_ok((0, 0)))?;
1155
1156        let (created_sec, created_nsec) = metadata
1157            .created
1158            .map(|tup| {
1159                mask |= this.eval_libc_u32("STATX_BTIME");
1160                interp_ok(tup)
1161            })
1162            .unwrap_or_else(|| interp_ok((0, 0)))?;
1163
1164        let (modified_sec, modified_nsec) = metadata
1165            .modified
1166            .map(|tup| {
1167                mask |= this.eval_libc_u32("STATX_MTIME");
1168                interp_ok(tup)
1169            })
1170            .unwrap_or_else(|| interp_ok((0, 0)))?;
1171
1172        // Now we write everything to `statxbuf`. We write a zero for the unavailable fields.
1173        this.write_int_fields_named(
1174            &[
1175                ("stx_mask", mask.into()),
1176                ("stx_mode", metadata.mode.into()),
1177                ("stx_blksize", metadata.blksize.unwrap_or(0).into()),
1178                ("stx_nlink", metadata.nlink.unwrap_or(0).into()),
1179                ("stx_uid", metadata.uid.unwrap_or(0).into()),
1180                ("stx_gid", metadata.gid.unwrap_or(0).into()),
1181                ("stx_ino", metadata.ino.unwrap_or(0).into()),
1182                ("stx_size", metadata.size.into()),
1183                ("stx_blocks", metadata.blocks.unwrap_or(0).into()),
1184                // These fields do not seem to have a bit in `mask`, they are "always present",
1185                // so we make sure they get initialized.
1186                ("stx_attributes_mask", 0),
1187                ("stx_attributes", 0),
1188                ("stx_rdev_major", 0),
1189                ("stx_rdev_minor", 0),
1190                ("stx_dev_major", 0),
1191                ("stx_dev_minor", 0),
1192            ],
1193            &statxbuf,
1194        )?;
1195        #[rustfmt::skip]
1196        this.write_int_fields_named(
1197            &[
1198                ("tv_sec", access_sec.into()),
1199                ("tv_nsec", access_nsec.into()),
1200            ],
1201            &this.project_field_named(&statxbuf, "stx_atime")?,
1202        )?;
1203        #[rustfmt::skip]
1204        this.write_int_fields_named(
1205            &[
1206                ("tv_sec", created_sec.into()),
1207                ("tv_nsec", created_nsec.into()),
1208            ],
1209            &this.project_field_named(&statxbuf, "stx_btime")?,
1210        )?;
1211        #[rustfmt::skip]
1212        this.write_int_fields_named(
1213            &[
1214                ("tv_sec", 0.into()),
1215                ("tv_nsec", 0.into()),
1216            ],
1217            &this.project_field_named(&statxbuf, "stx_ctime")?,
1218        )?;
1219        #[rustfmt::skip]
1220        this.write_int_fields_named(
1221            &[
1222                ("tv_sec", modified_sec.into()),
1223                ("tv_nsec", modified_nsec.into()),
1224            ],
1225            &this.project_field_named(&statxbuf, "stx_mtime")?,
1226        )?;
1227
1228        // The remaining fields have a mask bit associated with them, a bit that we do not
1229        // set. Therefore we also do not initialize them.
1230
1231        interp_ok(Scalar::from_i32(0))
1232    }
1233
1234    fn chmod(&mut self, path_op: &OpTy<'tcx>, mode_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1235        let this = self.eval_context_mut();
1236
1237        let path_ptr = this.read_pointer(path_op)?;
1238        let mode = this.read_scalar(mode_op)?.to_uint(this.libc_ty_layout("mode_t").size)?;
1239
1240        if this.ptr_is_null(path_ptr)? {
1241            return this.set_errno_and_return_neg1_i32(LibcError("EFAULT"));
1242        }
1243        let path = this.read_path_from_c_str(path_ptr)?;
1244
1245        // Reject if isolation is enabled.
1246        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1247            this.reject_in_isolation("`chmod`", reject_with)?;
1248            return this.set_errno_and_return_neg1_i32(LibcError("EACCES"));
1249        }
1250
1251        let permissions = this.host_permissions_from_mode(mode.try_into().unwrap(), || {
1252            // We get here if we need to read the old permissions before we can set new ones.
1253            // This is fundamentally racy. We cannot even use a file handle to make it less racy:
1254            // setting permissions requires opening for writing, but opening for writing fails
1255            // if the file is not already writable... so how do we make a read-only file writable?
1256            fs::metadata(&path).map(|m| m.permissions())
1257        })?;
1258        let res = permissions.and_then(|permissions| fs::set_permissions(path, permissions));
1259        if let Err(err) = res {
1260            return this.set_errno_and_return_neg1_i32(err);
1261        }
1262
1263        interp_ok(Scalar::from_i32(0))
1264    }
1265
1266    fn fchmod(&mut self, fd_op: &OpTy<'tcx>, mode_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1267        let this = self.eval_context_mut();
1268
1269        let fd_num = this.read_scalar(fd_op)?.to_i32()?;
1270        let mode = this.read_scalar(mode_op)?.to_uint(this.libc_ty_layout("mode_t").size)?;
1271
1272        let Some(fd) = this.machine.fds.get(fd_num) else {
1273            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1274        };
1275        let Some(file) = fd.downcast::<FileHandle>() else {
1276            // The docs don't talk about what happens for non-regular files...
1277            throw_unsup_format!("`fchmod` is only supported on regular files")
1278        };
1279        if !file.writable && !file.readable {
1280            // Apparently, `fchmod` on a read-only file is fine. But let's not allow it on a
1281            // path-only file.
1282            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1283        }
1284        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1285
1286        let permissions = this.host_permissions_from_mode(mode.try_into().unwrap(), || {
1287            // We get here if we need to read the old permissions before we can set new ones.
1288            // This is fundamentally racy.
1289            file.file.metadata().map(|m| m.permissions())
1290        })?;
1291        let res = permissions.and_then(|permissions| file.file.set_permissions(permissions));
1292        if let Err(err) = res {
1293            return this.set_errno_and_return_neg1_i32(err);
1294        }
1295
1296        interp_ok(Scalar::from_i32(0))
1297    }
1298
1299    fn rename(
1300        &mut self,
1301        oldpath_op: &OpTy<'tcx>,
1302        newpath_op: &OpTy<'tcx>,
1303    ) -> InterpResult<'tcx, Scalar> {
1304        let this = self.eval_context_mut();
1305
1306        let oldpath_ptr = this.read_pointer(oldpath_op)?;
1307        let newpath_ptr = this.read_pointer(newpath_op)?;
1308
1309        if this.ptr_is_null(oldpath_ptr)? || this.ptr_is_null(newpath_ptr)? {
1310            return this.set_errno_and_return_neg1_i32(LibcError("EFAULT"));
1311        }
1312
1313        let oldpath = this.read_path_from_c_str(oldpath_ptr)?;
1314        let newpath = this.read_path_from_c_str(newpath_ptr)?;
1315
1316        // Reject if isolation is enabled.
1317        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1318            this.reject_in_isolation("`rename`", reject_with)?;
1319            return this.set_errno_and_return_neg1_i32(ErrorKind::PermissionDenied);
1320        }
1321
1322        let result = fs::rename(oldpath, newpath).map(|_| 0);
1323
1324        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
1325    }
1326
1327    fn mkdir(&mut self, path_op: &OpTy<'tcx>, mode_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1328        let this = self.eval_context_mut();
1329
1330        #[cfg_attr(not(unix), allow(unused_variables))]
1331        let mode = if matches!(&this.tcx.sess.target.os, Os::MacOs | Os::FreeBsd) {
1332            u32::from(this.read_scalar(mode_op)?.to_u16()?)
1333        } else {
1334            this.read_scalar(mode_op)?.to_u32()?
1335        };
1336
1337        let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
1338
1339        // Reject if isolation is enabled.
1340        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1341            this.reject_in_isolation("`mkdir`", reject_with)?;
1342            return this.set_errno_and_return_neg1_i32(ErrorKind::PermissionDenied);
1343        }
1344
1345        #[cfg_attr(not(unix), allow(unused_mut))]
1346        let mut builder = DirBuilder::new();
1347
1348        // If the host supports it, forward on the mode of the directory
1349        // (i.e. permission bits and the sticky bit)
1350        #[cfg(unix)]
1351        {
1352            use std::os::unix::fs::DirBuilderExt;
1353            builder.mode(mode);
1354        }
1355
1356        let result = builder.create(path).map(|_| 0i32);
1357
1358        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
1359    }
1360
1361    fn rmdir(&mut self, path_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1362        let this = self.eval_context_mut();
1363
1364        let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
1365
1366        // Reject if isolation is enabled.
1367        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1368            this.reject_in_isolation("`rmdir`", reject_with)?;
1369            return this.set_errno_and_return_neg1_i32(ErrorKind::PermissionDenied);
1370        }
1371
1372        let result = cfg_select! {
1373            unix => fs::remove_dir(path),
1374            windows => {{
1375                // This should *not* be able to remove symlinks to dirs, but on Windows it is. Our
1376                // work-around is racy (it might change to being a directory symlink after we
1377                // checked it), but there's not a lot we can do about that.
1378                use std::os::windows::fs::FileTypeExt;
1379                let metadata = path.symlink_metadata();
1380                if metadata.is_ok_and(|m| m.file_type().is_symlink_dir()) {
1381                    return this.set_errno_and_return_neg1_i32(LibcError("ENOTDIR"));
1382                } else {
1383                    fs::remove_dir(path)
1384                }
1385            }}
1386        };
1387
1388        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result.map(|_| 0i32))?))
1389    }
1390
1391    fn opendir(&mut self, name_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1392        let this = self.eval_context_mut();
1393
1394        let name = this.read_path_from_c_str(this.read_pointer(name_op)?)?;
1395
1396        // Reject if isolation is enabled.
1397        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1398            this.reject_in_isolation("`opendir`", reject_with)?;
1399            this.set_last_error(LibcError("EACCES"))?;
1400            return interp_ok(Scalar::null_ptr(this));
1401        }
1402
1403        let result = fs::read_dir(&name);
1404
1405        match result {
1406            Ok(read_dir) => {
1407                // Create an underlying FD as well, in case someone calls `dirfd` later.
1408                // (We could do this lazily but that does not seem worth it.)
1409                let Ok(dir) = read_dir.dir() else {
1410                    throw_unsup_format!(
1411                        "cannot `opendir` this directory: failed to create directory handle"
1412                    );
1413                };
1414                let dir = this.machine.fds.new_ref(DirHandle { dir });
1415                let dir_fd_id = dir.id();
1416                let Some(dir_fd_num) = this.machine.fds.insert(dir) else {
1417                    return this.set_errno_and_return_neg1_i32(LibcError("EMFILE"));
1418                };
1419
1420                let stream = DirStream::new(read_dir, dir_fd_num, dir_fd_id);
1421                let id = this.machine.dirs.insert_new(stream);
1422
1423                // The libc API for opendir says that this method returns a pointer to an opaque
1424                // structure, but we are returning an ID number. Thus, pass it as a scalar of
1425                // pointer width. This also conveniently means that any attempt to access
1426                // memory via this pointer raises UB.
1427                interp_ok(Scalar::from_target_usize(id, this))
1428            }
1429            Err(e) => {
1430                this.set_last_error(e)?;
1431                interp_ok(Scalar::null_ptr(this))
1432            }
1433        }
1434    }
1435
1436    fn readdir(
1437        &mut self,
1438        dirp_op: &OpTy<'tcx>,
1439        dest: &MPlaceTy<'tcx>,
1440        buf_type: &'static str,
1441    ) -> InterpResult<'tcx> {
1442        let this = self.eval_context_mut();
1443
1444        if !matches!(
1445            &this.tcx.sess.target.os,
1446            Os::Linux | Os::Android | Os::Solaris | Os::Illumos | Os::FreeBsd | Os::MacOs
1447        ) {
1448            throw_unsup_format!("`readdir` is not yet supported on {}", this.tcx.sess.target.os);
1449        }
1450
1451        let dirp = this.read_target_usize(dirp_op)?;
1452
1453        // Reject if isolation is enabled.
1454        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1455            this.reject_in_isolation("`readdir`", reject_with)?;
1456            this.set_last_error(LibcError("EBADF"))?;
1457            this.write_int(0, dest)?;
1458            return interp_ok(());
1459        }
1460
1461        let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| {
1462            err_ub_format!("the DIR pointer passed to `readdir` did not come from opendir")
1463        })?;
1464
1465        // Check that the backing FD is still valid.
1466        if this.machine.fds.get(open_dir.fd_num).is_none_or(|fd| fd.id() != open_dir.fd_id) {
1467            throw_ub_format!("the backing FD for this DIR stream has been tampered with");
1468        }
1469
1470        let entry = match open_dir.next_host_entry() {
1471            Some(Ok(dir_entry)) => {
1472                let dir_entry = this.dir_entry_fields(dir_entry)?;
1473
1474                // Write the directory entry into a newly allocated buffer.
1475                // The name is written with write_bytes, while the rest of the
1476                // dirent64 (or dirent) struct is written using write_int_fields.
1477
1478                // For reference:
1479                // On Linux:
1480                // pub struct dirent64 {
1481                //     pub d_ino: ino64_t,
1482                //     pub d_off: off64_t,
1483                //     pub d_reclen: c_ushort,
1484                //     pub d_type: c_uchar,
1485                //     pub d_name: [c_char; 256],
1486                // }
1487                //
1488                // On Solaris:
1489                // pub struct dirent {
1490                //     pub d_ino: ino64_t,
1491                //     pub d_off: off64_t,
1492                //     pub d_reclen: c_ushort,
1493                //     pub d_name: [c_char; 3],
1494                // }
1495                //
1496                // On FreeBSD:
1497                // pub struct dirent {
1498                //     pub d_fileno: uint32_t,
1499                //     pub d_reclen: uint16_t,
1500                //     pub d_type: uint8_t,
1501                //     pub d_namlen: uint8_t,
1502                //     pub d_name: [c_char; 256],
1503                // }
1504                //
1505                // On macOS:
1506                // pub struct dirent {
1507                //     pub d_ino: u64,
1508                //     pub d_seekoff: u64,
1509                //     pub d_reclen: u16,
1510                //     pub d_namlen: u16,
1511                //     pub d_type: u8,
1512                //     pub d_name: [c_char; 1024],
1513                // }
1514
1515                let dirent_layout = this.libc_ty_layout(buf_type);
1516                let fields = &dirent_layout.fields;
1517                let d_name_offset = fields.offset(fields.count().strict_sub(1)).bytes();
1518
1519                // Determine the size of the buffer we have to allocate.
1520                let mut name = dir_entry.name; // not a Path as there are no separators!
1521                name.push("\0"); // Add a NUL terminator
1522                let name_bytes = name.as_encoded_bytes();
1523                let name_len = u64::try_from(name_bytes.len()).unwrap();
1524                let size = d_name_offset.strict_add(name_len);
1525
1526                let entry = this.allocate_ptr(
1527                    Size::from_bytes(size),
1528                    dirent_layout.align,
1529                    MiriMemoryKind::Runtime.into(),
1530                    AllocInit::Uninit,
1531                )?;
1532                let entry = this.ptr_to_mplace(entry.into(), dirent_layout);
1533
1534                // Write the name.
1535                // The name is not a normal field, we already computed the offset above.
1536                let name_ptr = entry.ptr().wrapping_offset(Size::from_bytes(d_name_offset), this);
1537                this.write_bytes_ptr(name_ptr, name_bytes.iter().copied())?;
1538
1539                // Write common fields.
1540                let ino_name =
1541                    if this.tcx.sess.target.os == Os::FreeBsd { "d_fileno" } else { "d_ino" };
1542                this.write_int_fields_named(
1543                    &[(ino_name, dir_entry.ino.into()), ("d_reclen", size.into())],
1544                    &entry,
1545                )?;
1546
1547                // Write "optional" fields.
1548                if let Some(d_off) = this.try_project_field_named(&entry, "d_off")? {
1549                    this.write_int(0, &d_off)?;
1550                }
1551                if let Some(d_seekoff) = this.try_project_field_named(&entry, "d_seekoff")? {
1552                    this.write_int(0, &d_seekoff)?;
1553                }
1554                if let Some(d_namlen) = this.try_project_field_named(&entry, "d_namlen")? {
1555                    this.write_int(name_len.strict_sub(1), &d_namlen)?;
1556                }
1557                if let Some(d_type) = this.try_project_field_named(&entry, "d_type")? {
1558                    this.write_int(dir_entry.d_type, &d_type)?;
1559                }
1560
1561                Some(entry.ptr())
1562            }
1563            None => {
1564                // end of stream: return NULL
1565                None
1566            }
1567            Some(Err(e)) => {
1568                this.set_last_error(e)?;
1569                None
1570            }
1571        };
1572
1573        let open_dir = this.machine.dirs.streams.get_mut(&dirp).unwrap();
1574        if let Some((old_entry, old_type)) =
1575            std::mem::replace(&mut open_dir.entry, entry.map(|e| (e, buf_type)))
1576        {
1577            if old_type != buf_type {
1578                throw_ub_format!("mixing different `readdir` variants on the same `DIR` instance");
1579            }
1580            this.deallocate_ptr(old_entry, None, MiriMemoryKind::Runtime.into())?;
1581        }
1582
1583        this.write_pointer(entry.unwrap_or_else(Pointer::null), dest)?;
1584        interp_ok(())
1585    }
1586
1587    fn dirfd(&mut self, dirp_op: &OpTy<'tcx>, dest: &MPlaceTy<'tcx>) -> InterpResult<'tcx> {
1588        let this = self.eval_context_mut();
1589
1590        let dirp = this.read_target_usize(dirp_op)?;
1591
1592        let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| {
1593            err_ub_format!("the DIR pointer passed to `readdir` did not come from opendir")
1594        })?;
1595
1596        // Check that the backing FD is still valid.
1597        if this.machine.fds.get(open_dir.fd_num).is_none_or(|fd| fd.id() != open_dir.fd_id) {
1598            throw_ub_format!("the backing FD for this DIR stream has been tampered with");
1599        }
1600
1601        let fd_num = open_dir.fd_num;
1602        this.write_int(fd_num, dest)?;
1603        interp_ok(())
1604    }
1605
1606    fn closedir(&mut self, dirp_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1607        let this = self.eval_context_mut();
1608
1609        let dirp = this.read_target_usize(dirp_op)?;
1610
1611        // Reject if isolation is enabled.
1612        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1613            this.reject_in_isolation("`closedir`", reject_with)?;
1614            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1615        }
1616
1617        let mut open_dir = this.machine.dirs.streams.remove(&dirp).ok_or_else(|| {
1618            err_ub_format!("the DIR pointer passed to `closedir` did not come from opendir")
1619        })?;
1620
1621        // Check that the backing FD is still valid.
1622        if this.machine.fds.get(open_dir.fd_num).is_none_or(|fd| fd.id() != open_dir.fd_id) {
1623            throw_ub_format!("the backing FD for this DIR stream has been tampered with");
1624        }
1625        // And close it.
1626        this.close(open_dir.fd_num)?;
1627
1628        if let Some((entry, _type)) = open_dir.entry.take() {
1629            this.deallocate_ptr(entry, None, MiriMemoryKind::Runtime.into())?;
1630        }
1631        // We drop the `open_dir`, which will close the host dir handle.
1632        drop(open_dir);
1633
1634        interp_ok(Scalar::from_i32(0))
1635    }
1636
1637    fn ftruncate64(&mut self, fd_num: i32, length: i128) -> InterpResult<'tcx, Scalar> {
1638        let this = self.eval_context_mut();
1639
1640        let Some(fd) = this.machine.fds.get(fd_num) else {
1641            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1642        };
1643        let Some(file) = fd.downcast::<FileHandle>() else {
1644            // The docs say that EINVAL is returned when the FD "does not reference a regular file
1645            // or a POSIX shared memory object" (and we don't support shmem objects).
1646            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1647        };
1648        if !file.writable {
1649            // man page says "EBADF or EINVAL", Linux seems to use EINVAL.
1650            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1651        }
1652        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1653
1654        if let Ok(length) = length.try_into() {
1655            let result = file.file.set_len(length);
1656            let result = this.try_unwrap_io_result(result.map(|_| 0i32))?;
1657            interp_ok(Scalar::from_i32(result))
1658        } else {
1659            this.set_errno_and_return_neg1_i32(LibcError("EINVAL"))
1660        }
1661    }
1662
1663    /// NOTE: According to the man page of `possix_fallocate`, it returns the error code instead
1664    /// of setting `errno`.
1665    fn posix_fallocate(
1666        &mut self,
1667        fd_num: i32,
1668        offset: i64,
1669        len: i64,
1670    ) -> InterpResult<'tcx, Scalar> {
1671        let this = self.eval_context_mut();
1672
1673        // Reject if isolation is enabled.
1674        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1675            this.reject_in_isolation("`posix_fallocate`", reject_with)?;
1676            // Return error code "EBADF" (bad fd).
1677            return interp_ok(this.eval_libc("EBADF"));
1678        }
1679
1680        match this.fallocate_impl(fd_num, offset, len)? {
1681            Ok(()) => interp_ok(Scalar::from_i32(0)),
1682            Err(e) => this.io_error_to_errnum(e),
1683        }
1684    }
1685
1686    fn linux_fallocate(
1687        &mut self,
1688        fd: i32,
1689        mode: i32,
1690        offset: i64,
1691        size: i64,
1692    ) -> InterpResult<'tcx, Scalar> {
1693        // This is mostly a copy of `posix_fallocate` except that errors are returned via errno.
1694        let this = self.eval_context_mut();
1695        this.assert_target_os_is_linux_like("fallocate");
1696
1697        // Reject if isolation is enabled.
1698        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1699            this.reject_in_isolation("`fallocate`", reject_with)?;
1700            // Set error code "EBADF" (bad fd).
1701            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1702        }
1703
1704        // We only support `fallocate` as a replacement for `posix_fallocate` on linux,
1705        // so a non-default `mode` is not supported.
1706        if mode != 0 {
1707            throw_unsup_format!("unsupported flags for `fallocate` in `mode` argument: {mode:#x}")
1708        }
1709
1710        match this.fallocate_impl(fd, offset, size)? {
1711            Ok(()) => interp_ok(Scalar::from_i32(0)),
1712            Err(e) => this.set_errno_and_return_neg1_i32(e),
1713        }
1714    }
1715
1716    /// Shared logic between `posix_fallocate` and `linux_fallocate`.
1717    fn fallocate_impl(
1718        &self,
1719        fd_num: i32,
1720        offset: i64,
1721        len: i64,
1722    ) -> InterpResult<'tcx, Result<(), IoError>> {
1723        let this = self.eval_context_ref();
1724
1725        // EINVAL is returned/set when: "offset was less than 0, or len was less than or equal to 0".
1726        if offset < 0 || len <= 0 {
1727            return interp_ok(Err(LibcError("EINVAL")));
1728        }
1729
1730        let Some(fd) = this.machine.fds.get(fd_num) else {
1731            return interp_ok(Err(LibcError("EBADF")));
1732        };
1733        let Some(file) = fd.downcast::<FileHandle>() else {
1734            // Man page specifies to return ENODEV if `fd` is not a regular file.
1735            return interp_ok(Err(LibcError("ENODEV")));
1736        };
1737
1738        if !file.writable {
1739            return interp_ok(Err(LibcError("EBADF")));
1740        }
1741
1742        let current_size = match file.file.metadata() {
1743            Ok(metadata) => metadata.len(),
1744            Err(err) => return interp_ok(Err(err.into())),
1745        };
1746
1747        // Checked i64 addition, to ensure the result does not exceed the max file size.
1748        let new_size = match offset.checked_add(len) {
1749            // `new_size` is definitely non-negative, so we can cast to `u64`.
1750            Some(new_size) => u64::try_from(new_size).unwrap(),
1751            None => return interp_ok(Err(LibcError("EFBIG"))), // new size too big
1752        };
1753
1754        // If the size of the file is less than offset+size, then the file is increased to this
1755        // size; otherwise the file size is left unchanged.
1756        if current_size < new_size {
1757            match file.file.set_len(new_size) {
1758                Ok(()) => interp_ok(Ok(())),
1759                Err(err) => interp_ok(Err(err.into())),
1760            }
1761        } else {
1762            interp_ok(Ok(()))
1763        }
1764    }
1765
1766    fn fsync(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1767        // On macOS, `fsync` (unlike `fcntl(F_FULLFSYNC)`) does not wait for the
1768        // underlying disk to finish writing. In the interest of host compatibility,
1769        // we conservatively implement this with `sync_all`, which
1770        // *does* wait for the disk.
1771
1772        let this = self.eval_context_mut();
1773
1774        let fd = this.read_scalar(fd_op)?.to_i32()?;
1775
1776        this.ffullsync_fd(fd)
1777    }
1778
1779    fn ffullsync_fd(&mut self, fd_num: i32) -> InterpResult<'tcx, Scalar> {
1780        let this = self.eval_context_mut();
1781        let Some(fd) = this.machine.fds.get(fd_num) else {
1782            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1783        };
1784        // Only regular files support synchronization.
1785        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1786            err_unsup_format!("`fsync` is only supported on file-backed file descriptors")
1787        })?;
1788        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1789
1790        let io_result = maybe_sync_file(&file, File::sync_all);
1791        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1792    }
1793
1794    fn fdatasync(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1795        let this = self.eval_context_mut();
1796
1797        let fd = this.read_scalar(fd_op)?.to_i32()?;
1798
1799        let Some(fd) = this.machine.fds.get(fd) else {
1800            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1801        };
1802        // Only regular files support synchronization.
1803        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1804            err_unsup_format!("`fdatasync` is only supported on file-backed file descriptors")
1805        })?;
1806        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1807
1808        let io_result = maybe_sync_file(&file, File::sync_data);
1809        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1810    }
1811
1812    /// `futimens(fd, times)`: set `fd`'s access/modification times. `times` is `[atime, mtime]`, or
1813    /// NULL to set both to now.
1814    fn futimens(
1815        &mut self,
1816        fd_op: &OpTy<'tcx>,
1817        times_op: &OpTy<'tcx>,
1818    ) -> InterpResult<'tcx, Scalar> {
1819        let this = self.eval_context_mut();
1820
1821        let fd_num = this.read_scalar(fd_op)?.to_i32()?;
1822        let times = this.deref_pointer(times_op)?;
1823
1824        let Some(fd) = this.machine.fds.get(fd_num) else {
1825            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1826        };
1827        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1828            err_unsup_format!("`futimens` is only supported on file-backed file descriptors")
1829        })?;
1830        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1831
1832        let Some((access, modified)) = this.parse_utimens_times(&times)? else {
1833            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1834        };
1835        let result = file.file.set_times(host_file_times_from_updates(access, modified));
1836        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result.map(|()| 0i32))?))
1837    }
1838
1839    /// `utimensat(dirfd, path, times, flags)`: like `futimens`, but for the file at `path`. With
1840    /// `AT_SYMLINK_NOFOLLOW`, a symlink at `path` is updated itself rather than its target.
1841    fn utimensat(
1842        &mut self,
1843        dirfd_op: &OpTy<'tcx>,
1844        path_op: &OpTy<'tcx>,
1845        times_op: &OpTy<'tcx>,
1846        flags_op: &OpTy<'tcx>,
1847    ) -> InterpResult<'tcx, Scalar> {
1848        let this = self.eval_context_mut();
1849
1850        let dirfd = this.read_scalar(dirfd_op)?.to_i32()?;
1851        let path_ptr = this.read_pointer(path_op)?;
1852        let times = this.deref_pointer(times_op)?;
1853        let flags = this.read_scalar(flags_op)?.to_i32()?;
1854
1855        if this.ptr_is_null(path_ptr)? {
1856            // With `AT_FDCWD` this is a bad address on all systems. With any other `dirfd`, Linux
1857            // operates on `dirfd` itself (like `futimens`) while macOS still fails.
1858            if dirfd != this.eval_libc_i32("AT_FDCWD") {
1859                throw_unsup_format!(
1860                    "`utimensat` with a NULL path is only supported for `AT_FDCWD`"
1861                );
1862            }
1863            return this.set_errno_and_return_neg1_i32(LibcError("EFAULT"));
1864        }
1865        let path = this.read_path_from_c_str(path_ptr)?.into_owned();
1866
1867        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1868            this.reject_in_isolation("`utimensat`", reject_with)?;
1869            return this.set_errno_and_return_neg1_i32(LibcError("EACCES"));
1870        }
1871
1872        let mut flags = flags;
1873        // Parse known flags and subtract them from `flags`.
1874        let at_symlink_nofollow = this.eval_libc_i32("AT_SYMLINK_NOFOLLOW");
1875        let symlink_nofollow_flag = flags & at_symlink_nofollow == at_symlink_nofollow;
1876        flags &= !at_symlink_nofollow;
1877
1878        if flags != 0 {
1879            throw_unsup_format!("unsupported flags for `utimensat`: {flags:#x}")
1880        }
1881
1882        if !(path.is_absolute() || dirfd == this.eval_libc_i32("AT_FDCWD")) {
1883            throw_unsup_format!("`utimensat` with a path relative to a `dirfd` is not supported");
1884        }
1885
1886        let Some((access, modified)) = this.parse_utimens_times(&times)? else {
1887            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1888        };
1889        if matches!((access, modified), (TimeUpdate::Omit, TimeUpdate::Omit)) {
1890            // Nothing to do. Behavior in this case differs between platforms: Linux always
1891            // succeeds, macOS/FreeBSD check whether the file exists. POSIX arguably allows both.
1892            // We decide to follow Linux.
1893            return interp_ok(Scalar::from_i32(0));
1894        }
1895        let filetimes = host_file_times_from_updates(access, modified);
1896        let result = if symlink_nofollow_flag {
1897            fs::set_times_nofollow(path, filetimes)
1898        } else {
1899            fs::set_times(path, filetimes)
1900        };
1901        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result.map(|()| 0i32))?))
1902    }
1903
1904    fn linux_sync_file_range(
1905        &mut self,
1906        fd_op: &OpTy<'tcx>,
1907        offset_op: &OpTy<'tcx>,
1908        nbytes_op: &OpTy<'tcx>,
1909        flags_op: &OpTy<'tcx>,
1910    ) -> InterpResult<'tcx, Scalar> {
1911        let this = self.eval_context_mut();
1912        this.assert_target_os_is_linux_like("sync_file_range");
1913
1914        let fd = this.read_scalar(fd_op)?.to_i32()?;
1915        let offset = this.read_scalar(offset_op)?.to_i64()?;
1916        let nbytes = this.read_scalar(nbytes_op)?.to_i64()?;
1917        let flags = this.read_scalar(flags_op)?.to_i32()?;
1918
1919        if offset < 0 || nbytes < 0 {
1920            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1921        }
1922        let allowed_flags = this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_BEFORE")
1923            | this.eval_libc_i32("SYNC_FILE_RANGE_WRITE")
1924            | this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_AFTER");
1925        if flags & allowed_flags != flags {
1926            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
1927        }
1928
1929        let Some(fd) = this.machine.fds.get(fd) else {
1930            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
1931        };
1932        // Only regular files support synchronization.
1933        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1934            err_unsup_format!("`sync_data_range` is only supported on file-backed file descriptors")
1935        })?;
1936        assert!(this.machine.communicate(), "isolation should have prevented even opening a file");
1937
1938        let io_result = maybe_sync_file(&file, File::sync_data);
1939        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1940    }
1941
1942    fn readlink(
1943        &mut self,
1944        pathname_op: &OpTy<'tcx>,
1945        buf_op: &OpTy<'tcx>,
1946        bufsize_op: &OpTy<'tcx>,
1947    ) -> InterpResult<'tcx, i64> {
1948        let this = self.eval_context_mut();
1949
1950        let pathname = this.read_os_str_from_c_str(this.read_pointer(pathname_op)?)?;
1951        let buf = this.read_pointer(buf_op)?;
1952        let bufsize = this.read_target_usize(bufsize_op)?;
1953
1954        // Reject if isolation is enabled.
1955        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1956            this.reject_in_isolation("`readlink`", reject_with)?;
1957            this.set_last_error(LibcError("EACCES"))?;
1958            return interp_ok(-1);
1959        }
1960
1961        // Special case the procfs link to the current executable so that
1962        // `std::env::current_exe` works in cross-execution.
1963        let self_exe = match this.tcx.sess.target.os {
1964            Os::Linux | Os::Android => Some("/proc/self/exe"),
1965            Os::Solaris | Os::Illumos => Some("/proc/self/path/a.out"),
1966            _ => None,
1967        };
1968        let result = if self_exe.is_some() && pathname.to_str() == self_exe {
1969            this.machine.current_exe.clone().ok_or(ErrorKind::NotFound.into())
1970        } else if matches!(
1971            this.tcx.sess.target.os,
1972            Os::Linux | Os::Android | Os::Illumos | Os::Solaris
1973        ) && path::absolute(pathname).is_ok_and(|path| path.starts_with("/proc"))
1974        {
1975            // Trying to read a symlink inside `/proc` is likely going to be nonsense, e.g.
1976            // `/proc/self/fd/N` doesn't use the same FD numbering. So we pretend nothing exists.
1977            this.set_last_error(ErrorKind::NotFound)?;
1978            return interp_ok(-1);
1979        } else {
1980            // We read `pathname` as `OsStr` above so we could do the /proc/self/exe check.
1981            // But now we need a (host) path.
1982            let pathname = this.convert_path(Cow::Borrowed(pathname), PathConversion::TargetToHost);
1983            std::fs::read_link(pathname)
1984        };
1985
1986        match result {
1987            Ok(resolved) => {
1988                // 'readlink' truncates the resolved path if the provided buffer is not large
1989                // enough, and does *not* add a null terminator. That means we cannot use the usual
1990                // `write_path_to_c_str` and have to re-implement parts of it ourselves.
1991                let resolved = this
1992                    .convert_path(Cow::Borrowed(resolved.as_ref()), PathConversion::HostToTarget);
1993                let mut path_bytes = resolved.as_encoded_bytes();
1994                let bufsize: usize = bufsize.try_into().unwrap();
1995                if path_bytes.len() > bufsize {
1996                    path_bytes = &path_bytes[..bufsize]
1997                }
1998                this.write_bytes_ptr(buf, path_bytes.iter().copied())?;
1999                interp_ok(path_bytes.len().try_into().unwrap())
2000            }
2001            Err(e) => {
2002                this.set_last_error(e)?;
2003                interp_ok(-1)
2004            }
2005        }
2006    }
2007
2008    fn isatty(&mut self, miri_fd: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
2009        let this = self.eval_context_mut();
2010        // "returns 1 if fd is an open file descriptor referring to a terminal;
2011        // otherwise 0 is returned, and errno is set to indicate the error"
2012        let fd = this.read_scalar(miri_fd)?.to_i32()?;
2013        let error = if let Some(fd) = this.machine.fds.get(fd) {
2014            if fd.is_tty(this.machine.communicate()) {
2015                return interp_ok(Scalar::from_i32(1));
2016            } else {
2017                LibcError("ENOTTY")
2018            }
2019        } else {
2020            // FD does not exist
2021            LibcError("EBADF")
2022        };
2023        this.set_last_error(error)?;
2024        interp_ok(Scalar::from_i32(0))
2025    }
2026
2027    fn realpath(
2028        &mut self,
2029        path_op: &OpTy<'tcx>,
2030        processed_path_op: &OpTy<'tcx>,
2031    ) -> InterpResult<'tcx, Scalar> {
2032        let this = self.eval_context_mut();
2033        this.assert_target_os_is_unix("realpath");
2034
2035        let pathname = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
2036        let processed_ptr = this.read_pointer(processed_path_op)?;
2037
2038        // Reject if isolation is enabled.
2039        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
2040            this.reject_in_isolation("`realpath`", reject_with)?;
2041            this.set_last_error(LibcError("EACCES"))?;
2042            return interp_ok(Scalar::from_target_usize(0, this));
2043        }
2044
2045        let result = std::fs::canonicalize(pathname);
2046        match result {
2047            Ok(resolved) => {
2048                let path_max = this
2049                    .eval_libc_i32("PATH_MAX")
2050                    .try_into()
2051                    .expect("PATH_MAX does not fit in u64");
2052                let dest = if this.ptr_is_null(processed_ptr)? {
2053                    // POSIX says behavior when passing a null pointer is implementation-defined,
2054                    // but GNU/linux, freebsd, netbsd, bionic/android, and macos all treat a null pointer
2055                    // similarly to:
2056                    //
2057                    // "If resolved_path is specified as NULL, then realpath() uses
2058                    // malloc(3) to allocate a buffer of up to PATH_MAX bytes to hold
2059                    // the resolved pathname, and returns a pointer to this buffer.  The
2060                    // caller should deallocate this buffer using free(3)."
2061                    // <https://man7.org/linux/man-pages/man3/realpath.3.html>
2062                    this.alloc_path_as_c_str(&resolved, MiriMemoryKind::C.into())?
2063                } else {
2064                    let (wrote_path, _) =
2065                        this.write_path_to_c_str(&resolved, processed_ptr, path_max)?;
2066
2067                    if !wrote_path {
2068                        // Note that we do not explicitly handle `FILENAME_MAX`
2069                        // (different from `PATH_MAX` above) as it is Linux-specific and
2070                        // seems like a bit of a mess anyway: <https://eklitzke.org/path-max-is-tricky>.
2071                        this.set_last_error(LibcError("ENAMETOOLONG"))?;
2072                        return interp_ok(Scalar::from_target_usize(0, this));
2073                    }
2074                    processed_ptr
2075                };
2076
2077                interp_ok(Scalar::from_maybe_pointer(dest, this))
2078            }
2079            Err(e) => {
2080                this.set_last_error(e)?;
2081                interp_ok(Scalar::from_target_usize(0, this))
2082            }
2083        }
2084    }
2085    fn mkstemp(&mut self, template_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
2086        use rand::seq::IndexedRandom;
2087
2088        // POSIX defines the template string.
2089        const TEMPFILE_TEMPLATE_STR: &str = "XXXXXX";
2090
2091        let this = self.eval_context_mut();
2092        this.assert_target_os_is_unix("mkstemp");
2093
2094        // POSIX defines the maximum number of attempts before failure.
2095        //
2096        // `mkstemp()` relies on `tmpnam()` which in turn relies on `TMP_MAX`.
2097        // POSIX says this about `TMP_MAX`:
2098        // * Minimum number of unique filenames generated by `tmpnam()`.
2099        // * Maximum number of times an application can call `tmpnam()` reliably.
2100        //   * The value of `TMP_MAX` is at least 25.
2101        //   * On XSI-conformant systems, the value of `TMP_MAX` is at least 10000.
2102        // See <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/stdio.h.html>.
2103        let max_attempts = this.eval_libc_u32("TMP_MAX");
2104
2105        // Get the raw bytes from the template -- as a byte slice, this is a string in the target
2106        // (and the target is unix, so a byte slice is the right representation).
2107        let template_ptr = this.read_pointer(template_op)?;
2108        let mut template = this.eval_context_ref().read_c_str(template_ptr)?.to_owned();
2109        let template_bytes = template.as_mut_slice();
2110
2111        // Reject if isolation is enabled.
2112        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
2113            this.reject_in_isolation("`mkstemp`", reject_with)?;
2114            return this.set_errno_and_return_neg1_i32(LibcError("EACCES"));
2115        }
2116
2117        // Get the bytes of the suffix we expect in _target_ encoding.
2118        let suffix_bytes = TEMPFILE_TEMPLATE_STR.as_bytes();
2119
2120        // At this point we have one `&[u8]` that represents the template and one `&[u8]`
2121        // that represents the expected suffix.
2122
2123        // Now we figure out the index of the slice we expect to contain the suffix.
2124        let start_pos = template_bytes.len().saturating_sub(suffix_bytes.len());
2125        let end_pos = template_bytes.len();
2126        let last_six_char_bytes = &template_bytes[start_pos..end_pos];
2127
2128        // If we don't find the suffix, it is an error.
2129        if last_six_char_bytes != suffix_bytes {
2130            return this.set_errno_and_return_neg1_i32(LibcError("EINVAL"));
2131        }
2132
2133        // At this point we know we have 6 ASCII 'X' characters as a suffix.
2134
2135        // From <https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/sysdeps/posix/tempname.c#L175>
2136        const SUBSTITUTIONS: &[char; 62] = &[
2137            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
2138            'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
2139            'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
2140            'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
2141        ];
2142
2143        // The file is opened with specific options, which Rust does not expose in a portable way.
2144        // So we use specific APIs depending on the host OS.
2145        let mut fopts = OpenOptions::new();
2146        fopts.read(true).write(true).create_new(true);
2147
2148        cfg_select! {
2149            unix => {
2150                use std::os::unix::fs::OpenOptionsExt;
2151                // Do not allow others to read or modify this file.
2152                fopts.mode(0o600);
2153                fopts.custom_flags(libc::O_EXCL);
2154            }
2155            windows => {
2156                use std::os::windows::fs::OpenOptionsExt;
2157                // Do not allow others to read or modify this file.
2158                fopts.share_mode(0);
2159            }
2160            _ => {
2161                throw_unsup_format!("`mkstemp` is not supported on this host OS");
2162            }
2163        }
2164
2165        // If the generated file already exists, we will try again `max_attempts` many times.
2166        for _ in 0..max_attempts {
2167            let rng = this.machine.rng.get_mut();
2168
2169            // Generate a random unique suffix.
2170            let unique_suffix =
2171                (0..6).map(|_| SUBSTITUTIONS.choose(rng).unwrap()).collect::<String>();
2172
2173            // Replace the template string with the random string.
2174            template_bytes[start_pos..end_pos].copy_from_slice(unique_suffix.as_bytes());
2175
2176            // Write the modified template back to the passed in pointer to maintain POSIX semantics.
2177            this.write_bytes_ptr(template_ptr, template_bytes.iter().copied())?;
2178
2179            // See if we can create and open this file.
2180            let file = fopts.open(bytes_to_os_str(template_bytes)?);
2181            match file {
2182                Ok(f) => {
2183                    let Some(fd) = this.machine.fds.insert_new(FileHandle {
2184                        file: f,
2185                        writable: true,
2186                        readable: true,
2187                        append: false,
2188                    }) else {
2189                        return this.set_errno_and_return_neg1_i32(LibcError("EMFILE"));
2190                    };
2191                    return interp_ok(Scalar::from_i32(fd));
2192                }
2193                Err(e) =>
2194                    match e.kind() {
2195                        // If the random file already exists, keep trying.
2196                        ErrorKind::AlreadyExists => continue,
2197                        // Any other errors are returned to the caller.
2198                        _ => {
2199                            // "On error, -1 is returned, and errno is set to
2200                            // indicate the error"
2201                            return this.set_errno_and_return_neg1_i32(e);
2202                        }
2203                    },
2204            }
2205        }
2206
2207        // We ran out of attempts to create the file, return an error.
2208        this.set_errno_and_return_neg1_i32(LibcError("EEXIST"))
2209    }
2210
2211    /// Only supports null offset pointers, i.e. copying from and advancing the file positions.
2212    fn linux_copy_file_range(
2213        &mut self,
2214        fd_in_op: &OpTy<'tcx>,
2215        off_in_op: &OpTy<'tcx>,
2216        fd_out_op: &OpTy<'tcx>,
2217        off_out_op: &OpTy<'tcx>,
2218        len_op: &OpTy<'tcx>,
2219        flags_op: &OpTy<'tcx>,
2220        dest: &MPlaceTy<'tcx>,
2221    ) -> InterpResult<'tcx> {
2222        let ecx = self.eval_context_mut();
2223        ecx.assert_target_os_is_linux_like("copy_file_range");
2224
2225        let fd_in = ecx.read_scalar(fd_in_op)?.to_i32()?;
2226        let off_in = ecx.read_pointer(off_in_op)?;
2227        let fd_out = ecx.read_scalar(fd_out_op)?.to_i32()?;
2228        let off_out = ecx.read_pointer(off_out_op)?;
2229        let len = ecx.read_target_usize(len_op)?;
2230        let flags = ecx.read_scalar(flags_op)?.to_u32()?;
2231
2232        let (Some(fd_in), Some(fd_out)) = (ecx.machine.fds.get(fd_in), ecx.machine.fds.get(fd_out))
2233        else {
2234            return ecx.set_errno_and_return_neg1(LibcError("EBADF"), dest);
2235        };
2236        if flags != 0 {
2237            throw_unsup_format!("`copy_file_range` with non-zero flags is not supported");
2238        }
2239        if !ecx.ptr_is_null(off_in)? || !ecx.ptr_is_null(off_out)? {
2240            throw_unsup_format!("`copy_file_range` with non-null offset pointers is not supported");
2241        }
2242        // Like Linux, check for directories (EISDIR), then other non-regular files (EINVAL),
2243        // then the access modes (EBADF).
2244        let is_dir = |fd: &DynFileDescriptionRef| fd.clone().downcast::<DirHandle>().is_some();
2245        let as_regular_file = |fd: DynFileDescriptionRef| {
2246            let file = fd.downcast::<FileHandle>()?;
2247            // Ensure we catch on-disk FIFOs and unix domain sockets.
2248            file.file.metadata().is_ok_and(|metadata| metadata.is_file()).then_some(file)
2249        };
2250        if is_dir(&fd_in) || is_dir(&fd_out) {
2251            return ecx.set_errno_and_return_neg1(LibcError("EISDIR"), dest);
2252        }
2253        let (Some(file_in), Some(file_out)) = (as_regular_file(fd_in), as_regular_file(fd_out))
2254        else {
2255            return ecx.set_errno_and_return_neg1(LibcError("EINVAL"), dest);
2256        };
2257        if !file_in.readable || !file_out.writable || file_out.append {
2258            // The docs explicitly say that O_APPEND on the output file is forbidden.
2259            return ecx.set_errno_and_return_neg1(LibcError("EBADF"), dest);
2260        }
2261        assert!(ecx.machine.communicate(), "isolation should have prevented even opening a file");
2262
2263        // Non-deterministically decide to further reduce the length, simulating a partial copy (but
2264        // never to 0, that would indicate EOF).
2265        let len = if ecx.machine.short_fd_operations
2266            && (file_in.short_fd_operations() || file_out.short_fd_operations())
2267            && len >= 2
2268            && let rng = ecx.machine.rng.get_mut()
2269            // Big accesses are shortened with a chance of 75%.
2270            && (rng.random() || (len >= BIG_ACCESS && rng.random()))
2271        {
2272            // Also cap length to remaining input length as otherwise halving it does nothing.
2273            let mut input = &file_in.file;
2274            let remaining_bytes =
2275                try { input.stream_len()?.saturating_sub(input.stream_position()?).clamp_to(2..) };
2276            let len = len.clamp_to(..=remaining_bytes.unwrap_or(u64::MAX));
2277            len / 2 // since `len` is at least 2, the result is still at least 1
2278        } else {
2279            len
2280        };
2281
2282        let copy = || -> io::Result<u64> {
2283            let (mut input, mut output) = (&file_in.file, &file_out.file);
2284            let (pos_in, pos_out) = (input.stream_position()?, output.stream_position()?);
2285
2286            // Error if both FDs refer to the same file and the ranges overlap.
2287            let (meta_in, meta_out) = (input.metadata()?, output.metadata()?);
2288            if same_host_file(&meta_in, &meta_out)
2289                && (pos_in.checked_add(len).is_none_or(|end_in| end_in > pos_out)
2290                    && pos_out.checked_add(len).is_none_or(|end_out| end_out > pos_in))
2291            {
2292                return Err(io::ErrorKind::InvalidInput.into());
2293            }
2294
2295            io::copy(&mut input.take(len), &mut output).inspect_err(|_| {
2296                // Like Linux, leave the file positions unchanged on error. See
2297                // <https://github.com/torvalds/linux/blob/81dfa035891f0502674332ee97db348a16486de0/fs/read_write.c#L1686-L1703>.
2298                let _ = input.seek(SeekFrom::Start(pos_in));
2299                let _ = output.seek(SeekFrom::Start(pos_out));
2300            })
2301        };
2302        ecx.return_io_result_via_errno(copy(), dest)
2303    }
2304}
2305
2306/// Extracts the number of seconds and nanoseconds elapsed between `time` and the unix epoch when
2307/// `time` is Ok. Returns `None` if `time` is an error. Fails if `time` happens before the unix
2308/// epoch.
2309fn extract_sec_and_nsec<'tcx>(
2310    time: std::io::Result<SystemTime>,
2311) -> InterpResult<'tcx, Option<(u64, u32)>> {
2312    match time.ok() {
2313        Some(time) => {
2314            let duration = system_time_to_duration(&time)?;
2315            interp_ok(Some((duration.as_secs(), duration.subsec_nanos())))
2316        }
2317        None => interp_ok(None),
2318    }
2319}
2320
2321fn file_type_to_mode_name(file_type: std::fs::FileType) -> &'static str {
2322    if file_type.is_file() {
2323        "S_IFREG"
2324    } else if file_type.is_dir() {
2325        "S_IFDIR"
2326    } else if file_type.is_symlink() {
2327        "S_IFLNK"
2328    } else {
2329        // Certain file types are only available when the host is a Unix system.
2330        #[cfg(unix)]
2331        {
2332            use std::os::unix::fs::FileTypeExt;
2333            if file_type.is_socket() {
2334                return "S_IFSOCK";
2335            } else if file_type.is_fifo() {
2336                return "S_IFIFO";
2337            } else if file_type.is_char_device() {
2338                return "S_IFCHR";
2339            } else if file_type.is_block_device() {
2340                return "S_IFBLK";
2341            }
2342        }
2343        "S_IFREG"
2344    }
2345}
2346
2347/// Stores a file's metadata in order to avoid code duplication in the different metadata related
2348/// shims.
2349///
2350/// Some fields are host/platform-specific. `None` means that Miri does not have a real value for
2351/// this field, for example because the metadata is synthetic or because the host platform does not
2352/// expose it. `statx` must only advertise the corresponding `STATX_*` bit when the field is `Some`;
2353/// legacy `stat` writes zero for `None` to preserve the old fallback behavior.
2354struct FileMetadata {
2355    /// This holds both the file type (dir, regular, symlink, ...) and permissions.
2356    mode: u32,
2357    size: u64,
2358    created: Option<(u64, u32)>,
2359    accessed: Option<(u64, u32)>,
2360    modified: Option<(u64, u32)>,
2361    dev: Option<u64>,
2362    ino: Option<u64>,
2363    nlink: Option<u64>,
2364    uid: Option<u32>,
2365    gid: Option<u32>,
2366    blksize: Option<u64>,
2367    blocks: Option<u64>,
2368}
2369
2370impl FileMetadata {
2371    /// Implements the shared "metadata at" semantics of `fstatat` and `statx`.
2372    fn at<'tcx>(
2373        ecx: &mut MiriInterpCx<'tcx>,
2374        dirfd: FdNum,
2375        path: &Path,
2376        symlink_nofollow_flag: bool,
2377        empty_path_flag: bool,
2378    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
2379        // If the path is empty, and the AT_EMPTY_PATH flag is set, we query the open file
2380        // represented by dirfd, whether it's a directory or otherwise.
2381        if path.is_empty() {
2382            // no path: invalid by default, load metadata about dirfd with flag
2383            if !empty_path_flag {
2384                return interp_ok(Err(LibcError("ENOENT")));
2385            }
2386            FileMetadata::from_fd_num(ecx, dirfd)
2387        } else if path.is_absolute() || dirfd == ecx.eval_libc_i32("AT_FDCWD") {
2388            // Either absolute path (dirfd is ignored) or relative to working directory.
2389            FileMetadata::from_host(
2390                ecx,
2391                if symlink_nofollow_flag { path.symlink_metadata() } else { path.metadata() },
2392            )
2393        } else {
2394            // relative to dirfd, which must be a directory handle
2395            let Some(fd) = ecx.machine.fds.get(dirfd) else {
2396                return interp_ok(Err(LibcError("EBADF")));
2397            };
2398            let Some(dir) = fd.downcast::<DirHandle>() else {
2399                return interp_ok(Err(LibcError("ENOTDIR")));
2400            };
2401
2402            // Windows does not support "." relative to a `Dir` handle, but the empty path works.
2403            // (https://github.com/rust-lang/rust/issues/163032)
2404            let path =
2405                if cfg!(windows) && path.to_str() == Some(".") { Path::new("") } else { path };
2406            let metadata = if symlink_nofollow_flag {
2407                dir.dir.symlink_metadata(path)
2408            } else {
2409                dir.dir.metadata(path)
2410            };
2411            FileMetadata::from_host(ecx, metadata)
2412        }
2413    }
2414
2415    fn from_fd_num<'tcx>(
2416        ecx: &mut MiriInterpCx<'tcx>,
2417        fd_num: FdNum,
2418    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
2419        let Some(fd) = ecx.machine.fds.get(fd_num) else {
2420            return interp_ok(Err(LibcError("EBADF")));
2421        };
2422        match fd.metadata(ecx.machine.communicate())? {
2423            Either::Left(host) => Self::from_host(ecx, host),
2424            Either::Right(mode_name) => Self::synthetic(ecx, mode_name),
2425        }
2426    }
2427
2428    fn synthetic<'tcx>(
2429        ecx: &mut MiriInterpCx<'tcx>,
2430        mode_name: &str,
2431    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
2432        let mode = ecx.eval_libc(mode_name);
2433        let mode: u32 = mode.to_uint(ecx.libc_ty_layout("mode_t").size)?.try_into().unwrap();
2434        // We observed 0x777 on sockets and 0x600 on pipes...
2435        let mode = mode | 0o666;
2436        interp_ok(Ok(FileMetadata {
2437            mode,
2438            size: 0,
2439            created: None,
2440            accessed: None,
2441            modified: None,
2442            dev: None,
2443            uid: None,
2444            gid: None,
2445            blksize: None,
2446            blocks: None,
2447            ino: None,
2448            nlink: None,
2449        }))
2450    }
2451
2452    fn from_host<'tcx>(
2453        ecx: &mut MiriInterpCx<'tcx>,
2454        metadata: Result<std::fs::Metadata, std::io::Error>,
2455    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
2456        let metadata = match metadata {
2457            Ok(metadata) => metadata,
2458            Err(e) => {
2459                return interp_ok(Err(e.into()));
2460            }
2461        };
2462
2463        let file_type = metadata.file_type();
2464        let mode = ecx.eval_libc(file_type_to_mode_name(file_type));
2465        let mut mode = mode.to_uint(ecx.libc_ty_layout("mode_t").size)?.try_into().unwrap();
2466
2467        let size = metadata.len();
2468
2469        let created = extract_sec_and_nsec(metadata.created())?;
2470        let accessed = extract_sec_and_nsec(metadata.accessed())?;
2471        let modified = extract_sec_and_nsec(metadata.modified())?;
2472
2473        // FIXME: Provide more fields using platform specific methods.
2474
2475        cfg_select! {
2476            unix => {
2477                use std::os::unix::fs::{MetadataExt, PermissionsExt};
2478
2479                let dev = metadata.dev();
2480                let ino = metadata.ino();
2481                let nlink = metadata.nlink();
2482                let uid = metadata.uid();
2483                let gid = metadata.gid();
2484                let blksize = metadata.blksize();
2485                let blocks = metadata.blocks();
2486
2487                mode |= metadata.permissions().mode();
2488
2489                interp_ok(Ok(FileMetadata {
2490                    mode,
2491                    size,
2492                    created,
2493                    accessed,
2494                    modified,
2495                    dev: Some(dev),
2496                    ino: Some(ino),
2497                    nlink: Some(nlink),
2498                    uid: Some(uid),
2499                    gid: Some(gid),
2500                    blksize: Some(blksize),
2501                    blocks: Some(blocks),
2502                }))
2503            }
2504            _ => {
2505                // Emulate "everyone can read" or "everyone can read and write".
2506                mode |= if metadata.permissions().readonly() { 0o444 } else { 0o666 };
2507
2508                interp_ok(Ok(FileMetadata {
2509                    mode,
2510                    size,
2511                    created,
2512                    accessed,
2513                    modified,
2514                    dev: None,
2515                    ino: None,
2516                    nlink: None,
2517                    uid: None,
2518                    gid: None,
2519                    blksize: None,
2520                    blocks: None,
2521                }))
2522            }
2523        }
2524    }
2525}