miri/shims/unix/
fs.rs

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