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::shims::files::FileHandle;
17use crate::shims::os_str::bytes_to_os_str;
18use crate::shims::sig::check_min_vararg_count;
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", metadata.dev.into()),
136                ("st_mode", mode.try_into().unwrap()),
137                ("st_nlink", 0),
138                ("st_ino", 0),
139                ("st_uid", metadata.uid.into()),
140                ("st_gid", metadata.gid.into()),
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 readdir64(&mut self, dirent_type: &str, dirp_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
904        let this = self.eval_context_mut();
905
906        if !matches!(&*this.tcx.sess.target.os, "linux" | "solaris" | "illumos" | "freebsd") {
907            panic!("`linux_solaris_readdir64` should not be called on {}", this.tcx.sess.target.os);
908        }
909
910        let dirp = this.read_target_usize(dirp_op)?;
911
912        // Reject if isolation is enabled.
913        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
914            this.reject_in_isolation("`readdir`", reject_with)?;
915            this.set_last_error(LibcError("EBADF"))?;
916            return interp_ok(Scalar::null_ptr(this));
917        }
918
919        let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| {
920            err_unsup_format!("the DIR pointer passed to readdir64 did not come from opendir")
921        })?;
922
923        let entry = match open_dir.read_dir.next() {
924            Some(Ok(dir_entry)) => {
925                // If the host is a Unix system, fill in the inode number with its real value.
926                // If not, use 0 as a fallback value.
927                #[cfg(unix)]
928                let ino = std::os::unix::fs::DirEntryExt::ino(&dir_entry);
929                #[cfg(not(unix))]
930                let ino = 0u64;
931
932                // Write the directory entry into a newly allocated buffer.
933                // The name is written with write_bytes, while the rest of the
934                // dirent64 (or dirent) struct is written using write_int_fields.
935
936                // For reference:
937                // On Linux:
938                // pub struct dirent64 {
939                //     pub d_ino: ino64_t,
940                //     pub d_off: off64_t,
941                //     pub d_reclen: c_ushort,
942                //     pub d_type: c_uchar,
943                //     pub d_name: [c_char; 256],
944                // }
945                //
946                // On Solaris:
947                // pub struct dirent {
948                //     pub d_ino: ino64_t,
949                //     pub d_off: off64_t,
950                //     pub d_reclen: c_ushort,
951                //     pub d_name: [c_char; 3],
952                // }
953                //
954                // On FreeBSD:
955                // pub struct dirent{
956                //     pub d_fileno: uint32_t,
957                //     pub d_reclen: uint16_t,
958                //     pub d_type: uint8_t,
959                //     pub d_namlen: uint8_t,
960                //     pub d_name: [c_char; 256]
961                // }
962
963                let mut name = dir_entry.file_name(); // not a Path as there are no separators!
964                name.push("\0"); // Add a NUL terminator
965                let name_bytes = name.as_encoded_bytes();
966                let name_len = u64::try_from(name_bytes.len()).unwrap();
967
968                let dirent_layout = this.libc_ty_layout(dirent_type);
969                let fields = &dirent_layout.fields;
970                let last_field = fields.count().strict_sub(1);
971                let d_name_offset = fields.offset(last_field).bytes();
972                let size = d_name_offset.strict_add(name_len);
973
974                let entry = this.allocate_ptr(
975                    Size::from_bytes(size),
976                    dirent_layout.align.abi,
977                    MiriMemoryKind::Runtime.into(),
978                    AllocInit::Uninit,
979                )?;
980                let entry = this.ptr_to_mplace(entry.into(), dirent_layout);
981
982                // Write common fields
983                let ino_name =
984                    if this.tcx.sess.target.os == "freebsd" { "d_fileno" } else { "d_ino" };
985                this.write_int_fields_named(
986                    &[(ino_name, ino.into()), ("d_reclen", size.into())],
987                    &entry,
988                )?;
989
990                // Write "optional" fields.
991                if let Some(d_off) = this.try_project_field_named(&entry, "d_off")? {
992                    this.write_null(&d_off)?;
993                }
994
995                if let Some(d_namlen) = this.try_project_field_named(&entry, "d_namlen")? {
996                    this.write_int(name_len.strict_sub(1), &d_namlen)?;
997                }
998
999                let file_type = this.file_type_to_d_type(dir_entry.file_type())?;
1000                if let Some(d_type) = this.try_project_field_named(&entry, "d_type")? {
1001                    this.write_int(file_type, &d_type)?;
1002                }
1003
1004                // The name is not a normal field, we already computed the offset above.
1005                let name_ptr = entry.ptr().wrapping_offset(Size::from_bytes(d_name_offset), this);
1006                this.write_bytes_ptr(name_ptr, name_bytes.iter().copied())?;
1007
1008                Some(entry.ptr())
1009            }
1010            None => {
1011                // end of stream: return NULL
1012                None
1013            }
1014            Some(Err(e)) => {
1015                this.set_last_error(e)?;
1016                None
1017            }
1018        };
1019
1020        let open_dir = this.machine.dirs.streams.get_mut(&dirp).unwrap();
1021        let old_entry = std::mem::replace(&mut open_dir.entry, entry);
1022        if let Some(old_entry) = old_entry {
1023            this.deallocate_ptr(old_entry, None, MiriMemoryKind::Runtime.into())?;
1024        }
1025
1026        interp_ok(Scalar::from_maybe_pointer(entry.unwrap_or_else(Pointer::null), this))
1027    }
1028
1029    fn macos_fbsd_readdir_r(
1030        &mut self,
1031        dirp_op: &OpTy<'tcx>,
1032        entry_op: &OpTy<'tcx>,
1033        result_op: &OpTy<'tcx>,
1034    ) -> InterpResult<'tcx, Scalar> {
1035        let this = self.eval_context_mut();
1036
1037        if !matches!(&*this.tcx.sess.target.os, "macos" | "freebsd") {
1038            panic!("`macos_fbsd_readdir_r` should not be called on {}", this.tcx.sess.target.os);
1039        }
1040
1041        let dirp = this.read_target_usize(dirp_op)?;
1042        let result_place = this.deref_pointer_as(result_op, this.machine.layouts.mut_raw_ptr)?;
1043
1044        // Reject if isolation is enabled.
1045        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1046            this.reject_in_isolation("`readdir_r`", reject_with)?;
1047            // Return error code, do *not* set `errno`.
1048            return interp_ok(this.eval_libc("EBADF"));
1049        }
1050
1051        let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| {
1052            err_unsup_format!("the DIR pointer passed to readdir_r did not come from opendir")
1053        })?;
1054        interp_ok(match open_dir.read_dir.next() {
1055            Some(Ok(dir_entry)) => {
1056                // Write into entry, write pointer to result, return 0 on success.
1057                // The name is written with write_os_str_to_c_str, while the rest of the
1058                // dirent struct is written using write_int_fields.
1059
1060                // For reference, on macOS this looks like:
1061                // pub struct dirent {
1062                //     pub d_ino: u64,
1063                //     pub d_seekoff: u64,
1064                //     pub d_reclen: u16,
1065                //     pub d_namlen: u16,
1066                //     pub d_type: u8,
1067                //     pub d_name: [c_char; 1024],
1068                // }
1069
1070                let entry_place = this.deref_pointer_as(entry_op, this.libc_ty_layout("dirent"))?;
1071                let name_place = this.project_field_named(&entry_place, "d_name")?;
1072
1073                let file_name = dir_entry.file_name(); // not a Path as there are no separators!
1074                let (name_fits, file_name_buf_len) = this.write_os_str_to_c_str(
1075                    &file_name,
1076                    name_place.ptr(),
1077                    name_place.layout.size.bytes(),
1078                )?;
1079                let file_name_len = file_name_buf_len.strict_sub(1);
1080                if !name_fits {
1081                    throw_unsup_format!(
1082                        "a directory entry had a name too large to fit in libc::dirent"
1083                    );
1084                }
1085
1086                // If the host is a Unix system, fill in the inode number with its real value.
1087                // If not, use 0 as a fallback value.
1088                #[cfg(unix)]
1089                let ino = std::os::unix::fs::DirEntryExt::ino(&dir_entry);
1090                #[cfg(not(unix))]
1091                let ino = 0u64;
1092
1093                let file_type = this.file_type_to_d_type(dir_entry.file_type())?;
1094
1095                // Common fields.
1096                this.write_int_fields_named(
1097                    &[
1098                        ("d_reclen", 0),
1099                        ("d_namlen", file_name_len.into()),
1100                        ("d_type", file_type.into()),
1101                    ],
1102                    &entry_place,
1103                )?;
1104                // Special fields.
1105                match &*this.tcx.sess.target.os {
1106                    "macos" => {
1107                        #[rustfmt::skip]
1108                        this.write_int_fields_named(
1109                            &[
1110                                ("d_ino", ino.into()),
1111                                ("d_seekoff", 0),
1112                            ],
1113                            &entry_place,
1114                        )?;
1115                    }
1116                    "freebsd" => {
1117                        #[rustfmt::skip]
1118                        this.write_int_fields_named(
1119                            &[
1120                                ("d_fileno", ino.into()),
1121                                ("d_off", 0),
1122                            ],
1123                            &entry_place,
1124                        )?;
1125                    }
1126                    _ => unreachable!(),
1127                }
1128                this.write_scalar(this.read_scalar(entry_op)?, &result_place)?;
1129
1130                Scalar::from_i32(0)
1131            }
1132            None => {
1133                // end of stream: return 0, assign *result=NULL
1134                this.write_null(&result_place)?;
1135                Scalar::from_i32(0)
1136            }
1137            Some(Err(e)) => {
1138                // return positive error number on error (do *not* set last error)
1139                this.io_error_to_errnum(e)?
1140            }
1141        })
1142    }
1143
1144    fn closedir(&mut self, dirp_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1145        let this = self.eval_context_mut();
1146
1147        let dirp = this.read_target_usize(dirp_op)?;
1148
1149        // Reject if isolation is enabled.
1150        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1151            this.reject_in_isolation("`closedir`", reject_with)?;
1152            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1153        }
1154
1155        let Some(mut open_dir) = this.machine.dirs.streams.remove(&dirp) else {
1156            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1157        };
1158        if let Some(entry) = open_dir.entry.take() {
1159            this.deallocate_ptr(entry, None, MiriMemoryKind::Runtime.into())?;
1160        }
1161        // We drop the `open_dir`, which will close the host dir handle.
1162        drop(open_dir);
1163
1164        interp_ok(Scalar::from_i32(0))
1165    }
1166
1167    fn ftruncate64(&mut self, fd_num: i32, length: i128) -> InterpResult<'tcx, Scalar> {
1168        let this = self.eval_context_mut();
1169
1170        // Reject if isolation is enabled.
1171        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1172            this.reject_in_isolation("`ftruncate64`", reject_with)?;
1173            // Set error code as "EBADF" (bad fd)
1174            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1175        }
1176
1177        let Some(fd) = this.machine.fds.get(fd_num) else {
1178            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1179        };
1180
1181        // FIXME: Support ftruncate64 for all FDs
1182        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1183            err_unsup_format!("`ftruncate64` is only supported on file-backed file descriptors")
1184        })?;
1185
1186        if file.writable {
1187            if let Ok(length) = length.try_into() {
1188                let result = file.file.set_len(length);
1189                let result = this.try_unwrap_io_result(result.map(|_| 0i32))?;
1190                interp_ok(Scalar::from_i32(result))
1191            } else {
1192                this.set_last_error_and_return_i32(LibcError("EINVAL"))
1193            }
1194        } else {
1195            // The file is not writable
1196            this.set_last_error_and_return_i32(LibcError("EINVAL"))
1197        }
1198    }
1199
1200    fn fsync(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1201        // On macOS, `fsync` (unlike `fcntl(F_FULLFSYNC)`) does not wait for the
1202        // underlying disk to finish writing. In the interest of host compatibility,
1203        // we conservatively implement this with `sync_all`, which
1204        // *does* wait for the disk.
1205
1206        let this = self.eval_context_mut();
1207
1208        let fd = this.read_scalar(fd_op)?.to_i32()?;
1209
1210        // Reject if isolation is enabled.
1211        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1212            this.reject_in_isolation("`fsync`", reject_with)?;
1213            // Set error code as "EBADF" (bad fd)
1214            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1215        }
1216
1217        self.ffullsync_fd(fd)
1218    }
1219
1220    fn ffullsync_fd(&mut self, fd_num: i32) -> InterpResult<'tcx, Scalar> {
1221        let this = self.eval_context_mut();
1222        let Some(fd) = this.machine.fds.get(fd_num) else {
1223            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1224        };
1225        // Only regular files support synchronization.
1226        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1227            err_unsup_format!("`fsync` is only supported on file-backed file descriptors")
1228        })?;
1229        let io_result = maybe_sync_file(&file.file, file.writable, File::sync_all);
1230        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1231    }
1232
1233    fn fdatasync(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1234        let this = self.eval_context_mut();
1235
1236        let fd = this.read_scalar(fd_op)?.to_i32()?;
1237
1238        // Reject if isolation is enabled.
1239        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1240            this.reject_in_isolation("`fdatasync`", reject_with)?;
1241            // Set error code as "EBADF" (bad fd)
1242            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1243        }
1244
1245        let Some(fd) = this.machine.fds.get(fd) else {
1246            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1247        };
1248        // Only regular files support synchronization.
1249        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1250            err_unsup_format!("`fdatasync` is only supported on file-backed file descriptors")
1251        })?;
1252        let io_result = maybe_sync_file(&file.file, file.writable, File::sync_data);
1253        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1254    }
1255
1256    fn sync_file_range(
1257        &mut self,
1258        fd_op: &OpTy<'tcx>,
1259        offset_op: &OpTy<'tcx>,
1260        nbytes_op: &OpTy<'tcx>,
1261        flags_op: &OpTy<'tcx>,
1262    ) -> InterpResult<'tcx, Scalar> {
1263        let this = self.eval_context_mut();
1264
1265        let fd = this.read_scalar(fd_op)?.to_i32()?;
1266        let offset = this.read_scalar(offset_op)?.to_i64()?;
1267        let nbytes = this.read_scalar(nbytes_op)?.to_i64()?;
1268        let flags = this.read_scalar(flags_op)?.to_i32()?;
1269
1270        if offset < 0 || nbytes < 0 {
1271            return this.set_last_error_and_return_i32(LibcError("EINVAL"));
1272        }
1273        let allowed_flags = this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_BEFORE")
1274            | this.eval_libc_i32("SYNC_FILE_RANGE_WRITE")
1275            | this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_AFTER");
1276        if flags & allowed_flags != flags {
1277            return this.set_last_error_and_return_i32(LibcError("EINVAL"));
1278        }
1279
1280        // Reject if isolation is enabled.
1281        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1282            this.reject_in_isolation("`sync_file_range`", reject_with)?;
1283            // Set error code as "EBADF" (bad fd)
1284            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1285        }
1286
1287        let Some(fd) = this.machine.fds.get(fd) else {
1288            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1289        };
1290        // Only regular files support synchronization.
1291        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1292            err_unsup_format!("`sync_data_range` is only supported on file-backed file descriptors")
1293        })?;
1294        let io_result = maybe_sync_file(&file.file, file.writable, File::sync_data);
1295        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1296    }
1297
1298    fn readlink(
1299        &mut self,
1300        pathname_op: &OpTy<'tcx>,
1301        buf_op: &OpTy<'tcx>,
1302        bufsize_op: &OpTy<'tcx>,
1303    ) -> InterpResult<'tcx, i64> {
1304        let this = self.eval_context_mut();
1305
1306        let pathname = this.read_path_from_c_str(this.read_pointer(pathname_op)?)?;
1307        let buf = this.read_pointer(buf_op)?;
1308        let bufsize = this.read_target_usize(bufsize_op)?;
1309
1310        // Reject if isolation is enabled.
1311        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1312            this.reject_in_isolation("`readlink`", reject_with)?;
1313            this.set_last_error(LibcError("EACCES"))?;
1314            return interp_ok(-1);
1315        }
1316
1317        let result = std::fs::read_link(pathname);
1318        match result {
1319            Ok(resolved) => {
1320                // 'readlink' truncates the resolved path if the provided buffer is not large
1321                // enough, and does *not* add a null terminator. That means we cannot use the usual
1322                // `write_path_to_c_str` and have to re-implement parts of it ourselves.
1323                let resolved = this.convert_path(
1324                    Cow::Borrowed(resolved.as_ref()),
1325                    crate::shims::os_str::PathConversion::HostToTarget,
1326                );
1327                let mut path_bytes = resolved.as_encoded_bytes();
1328                let bufsize: usize = bufsize.try_into().unwrap();
1329                if path_bytes.len() > bufsize {
1330                    path_bytes = &path_bytes[..bufsize]
1331                }
1332                this.write_bytes_ptr(buf, path_bytes.iter().copied())?;
1333                interp_ok(path_bytes.len().try_into().unwrap())
1334            }
1335            Err(e) => {
1336                this.set_last_error(e)?;
1337                interp_ok(-1)
1338            }
1339        }
1340    }
1341
1342    fn isatty(&mut self, miri_fd: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1343        let this = self.eval_context_mut();
1344        // "returns 1 if fd is an open file descriptor referring to a terminal;
1345        // otherwise 0 is returned, and errno is set to indicate the error"
1346        let fd = this.read_scalar(miri_fd)?.to_i32()?;
1347        let error = if let Some(fd) = this.machine.fds.get(fd) {
1348            if fd.is_tty(this.machine.communicate()) {
1349                return interp_ok(Scalar::from_i32(1));
1350            } else {
1351                LibcError("ENOTTY")
1352            }
1353        } else {
1354            // FD does not exist
1355            LibcError("EBADF")
1356        };
1357        this.set_last_error(error)?;
1358        interp_ok(Scalar::from_i32(0))
1359    }
1360
1361    fn realpath(
1362        &mut self,
1363        path_op: &OpTy<'tcx>,
1364        processed_path_op: &OpTy<'tcx>,
1365    ) -> InterpResult<'tcx, Scalar> {
1366        let this = self.eval_context_mut();
1367        this.assert_target_os_is_unix("realpath");
1368
1369        let pathname = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
1370        let processed_ptr = this.read_pointer(processed_path_op)?;
1371
1372        // Reject if isolation is enabled.
1373        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1374            this.reject_in_isolation("`realpath`", reject_with)?;
1375            this.set_last_error(LibcError("EACCES"))?;
1376            return interp_ok(Scalar::from_target_usize(0, this));
1377        }
1378
1379        let result = std::fs::canonicalize(pathname);
1380        match result {
1381            Ok(resolved) => {
1382                let path_max = this
1383                    .eval_libc_i32("PATH_MAX")
1384                    .try_into()
1385                    .expect("PATH_MAX does not fit in u64");
1386                let dest = if this.ptr_is_null(processed_ptr)? {
1387                    // POSIX says behavior when passing a null pointer is implementation-defined,
1388                    // but GNU/linux, freebsd, netbsd, bionic/android, and macos all treat a null pointer
1389                    // similarly to:
1390                    //
1391                    // "If resolved_path is specified as NULL, then realpath() uses
1392                    // malloc(3) to allocate a buffer of up to PATH_MAX bytes to hold
1393                    // the resolved pathname, and returns a pointer to this buffer.  The
1394                    // caller should deallocate this buffer using free(3)."
1395                    // <https://man7.org/linux/man-pages/man3/realpath.3.html>
1396                    this.alloc_path_as_c_str(&resolved, MiriMemoryKind::C.into())?
1397                } else {
1398                    let (wrote_path, _) =
1399                        this.write_path_to_c_str(&resolved, processed_ptr, path_max)?;
1400
1401                    if !wrote_path {
1402                        // Note that we do not explicitly handle `FILENAME_MAX`
1403                        // (different from `PATH_MAX` above) as it is Linux-specific and
1404                        // seems like a bit of a mess anyway: <https://eklitzke.org/path-max-is-tricky>.
1405                        this.set_last_error(LibcError("ENAMETOOLONG"))?;
1406                        return interp_ok(Scalar::from_target_usize(0, this));
1407                    }
1408                    processed_ptr
1409                };
1410
1411                interp_ok(Scalar::from_maybe_pointer(dest, this))
1412            }
1413            Err(e) => {
1414                this.set_last_error(e)?;
1415                interp_ok(Scalar::from_target_usize(0, this))
1416            }
1417        }
1418    }
1419    fn mkstemp(&mut self, template_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1420        use rand::seq::IndexedRandom;
1421
1422        // POSIX defines the template string.
1423        const TEMPFILE_TEMPLATE_STR: &str = "XXXXXX";
1424
1425        let this = self.eval_context_mut();
1426        this.assert_target_os_is_unix("mkstemp");
1427
1428        // POSIX defines the maximum number of attempts before failure.
1429        //
1430        // `mkstemp()` relies on `tmpnam()` which in turn relies on `TMP_MAX`.
1431        // POSIX says this about `TMP_MAX`:
1432        // * Minimum number of unique filenames generated by `tmpnam()`.
1433        // * Maximum number of times an application can call `tmpnam()` reliably.
1434        //   * The value of `TMP_MAX` is at least 25.
1435        //   * On XSI-conformant systems, the value of `TMP_MAX` is at least 10000.
1436        // See <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/stdio.h.html>.
1437        let max_attempts = this.eval_libc_u32("TMP_MAX");
1438
1439        // Get the raw bytes from the template -- as a byte slice, this is a string in the target
1440        // (and the target is unix, so a byte slice is the right representation).
1441        let template_ptr = this.read_pointer(template_op)?;
1442        let mut template = this.eval_context_ref().read_c_str(template_ptr)?.to_owned();
1443        let template_bytes = template.as_mut_slice();
1444
1445        // Reject if isolation is enabled.
1446        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1447            this.reject_in_isolation("`mkstemp`", reject_with)?;
1448            return this.set_last_error_and_return_i32(LibcError("EACCES"));
1449        }
1450
1451        // Get the bytes of the suffix we expect in _target_ encoding.
1452        let suffix_bytes = TEMPFILE_TEMPLATE_STR.as_bytes();
1453
1454        // At this point we have one `&[u8]` that represents the template and one `&[u8]`
1455        // that represents the expected suffix.
1456
1457        // Now we figure out the index of the slice we expect to contain the suffix.
1458        let start_pos = template_bytes.len().saturating_sub(suffix_bytes.len());
1459        let end_pos = template_bytes.len();
1460        let last_six_char_bytes = &template_bytes[start_pos..end_pos];
1461
1462        // If we don't find the suffix, it is an error.
1463        if last_six_char_bytes != suffix_bytes {
1464            return this.set_last_error_and_return_i32(LibcError("EINVAL"));
1465        }
1466
1467        // At this point we know we have 6 ASCII 'X' characters as a suffix.
1468
1469        // From <https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/sysdeps/posix/tempname.c#L175>
1470        const SUBSTITUTIONS: &[char; 62] = &[
1471            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
1472            'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
1473            'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
1474            'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
1475        ];
1476
1477        // The file is opened with specific options, which Rust does not expose in a portable way.
1478        // So we use specific APIs depending on the host OS.
1479        let mut fopts = OpenOptions::new();
1480        fopts.read(true).write(true).create_new(true);
1481
1482        #[cfg(unix)]
1483        {
1484            use std::os::unix::fs::OpenOptionsExt;
1485            // Do not allow others to read or modify this file.
1486            fopts.mode(0o600);
1487            fopts.custom_flags(libc::O_EXCL);
1488        }
1489        #[cfg(windows)]
1490        {
1491            use std::os::windows::fs::OpenOptionsExt;
1492            // Do not allow others to read or modify this file.
1493            fopts.share_mode(0);
1494        }
1495
1496        // If the generated file already exists, we will try again `max_attempts` many times.
1497        for _ in 0..max_attempts {
1498            let rng = this.machine.rng.get_mut();
1499
1500            // Generate a random unique suffix.
1501            let unique_suffix = SUBSTITUTIONS.choose_multiple(rng, 6).collect::<String>();
1502
1503            // Replace the template string with the random string.
1504            template_bytes[start_pos..end_pos].copy_from_slice(unique_suffix.as_bytes());
1505
1506            // Write the modified template back to the passed in pointer to maintain POSIX semantics.
1507            this.write_bytes_ptr(template_ptr, template_bytes.iter().copied())?;
1508
1509            // To actually open the file, turn this into a host OsString.
1510            let p = bytes_to_os_str(template_bytes)?.to_os_string();
1511
1512            let possibly_unique = std::env::temp_dir().join::<PathBuf>(p.into());
1513
1514            let file = fopts.open(possibly_unique);
1515
1516            match file {
1517                Ok(f) => {
1518                    let fd = this.machine.fds.insert_new(FileHandle { file: f, writable: true });
1519                    return interp_ok(Scalar::from_i32(fd));
1520                }
1521                Err(e) =>
1522                    match e.kind() {
1523                        // If the random file already exists, keep trying.
1524                        ErrorKind::AlreadyExists => continue,
1525                        // Any other errors are returned to the caller.
1526                        _ => {
1527                            // "On error, -1 is returned, and errno is set to
1528                            // indicate the error"
1529                            return this.set_last_error_and_return_i32(e);
1530                        }
1531                    },
1532            }
1533        }
1534
1535        // We ran out of attempts to create the file, return an error.
1536        this.set_last_error_and_return_i32(LibcError("EEXIST"))
1537    }
1538}
1539
1540/// Extracts the number of seconds and nanoseconds elapsed between `time` and the unix epoch when
1541/// `time` is Ok. Returns `None` if `time` is an error. Fails if `time` happens before the unix
1542/// epoch.
1543fn extract_sec_and_nsec<'tcx>(
1544    time: std::io::Result<SystemTime>,
1545) -> InterpResult<'tcx, Option<(u64, u32)>> {
1546    match time.ok() {
1547        Some(time) => {
1548            let duration = system_time_to_duration(&time)?;
1549            interp_ok(Some((duration.as_secs(), duration.subsec_nanos())))
1550        }
1551        None => interp_ok(None),
1552    }
1553}
1554
1555/// Stores a file's metadata in order to avoid code duplication in the different metadata related
1556/// shims.
1557struct FileMetadata {
1558    mode: Scalar,
1559    size: u64,
1560    created: Option<(u64, u32)>,
1561    accessed: Option<(u64, u32)>,
1562    modified: Option<(u64, u32)>,
1563    dev: u64,
1564    uid: u32,
1565    gid: u32,
1566}
1567
1568impl FileMetadata {
1569    fn from_path<'tcx>(
1570        ecx: &mut MiriInterpCx<'tcx>,
1571        path: &Path,
1572        follow_symlink: bool,
1573    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1574        let metadata =
1575            if follow_symlink { std::fs::metadata(path) } else { std::fs::symlink_metadata(path) };
1576
1577        FileMetadata::from_meta(ecx, metadata)
1578    }
1579
1580    fn from_fd_num<'tcx>(
1581        ecx: &mut MiriInterpCx<'tcx>,
1582        fd_num: i32,
1583    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1584        let Some(fd) = ecx.machine.fds.get(fd_num) else {
1585            return interp_ok(Err(LibcError("EBADF")));
1586        };
1587
1588        let metadata = fd.metadata()?;
1589        drop(fd);
1590        FileMetadata::from_meta(ecx, metadata)
1591    }
1592
1593    fn from_meta<'tcx>(
1594        ecx: &mut MiriInterpCx<'tcx>,
1595        metadata: Result<std::fs::Metadata, std::io::Error>,
1596    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1597        let metadata = match metadata {
1598            Ok(metadata) => metadata,
1599            Err(e) => {
1600                return interp_ok(Err(e.into()));
1601            }
1602        };
1603
1604        let file_type = metadata.file_type();
1605
1606        let mode_name = if file_type.is_file() {
1607            "S_IFREG"
1608        } else if file_type.is_dir() {
1609            "S_IFDIR"
1610        } else {
1611            "S_IFLNK"
1612        };
1613
1614        let mode = ecx.eval_libc(mode_name);
1615
1616        let size = metadata.len();
1617
1618        let created = extract_sec_and_nsec(metadata.created())?;
1619        let accessed = extract_sec_and_nsec(metadata.accessed())?;
1620        let modified = extract_sec_and_nsec(metadata.modified())?;
1621
1622        // FIXME: Provide more fields using platform specific methods.
1623
1624        cfg_select! {
1625            unix => {
1626                use std::os::unix::fs::MetadataExt;
1627                let dev = metadata.dev();
1628                let uid = metadata.uid();
1629                let gid = metadata.gid();
1630            }
1631            _ => {
1632                let dev = 0;
1633                let uid = 0;
1634                let gid = 0;
1635            }
1636        }
1637
1638        interp_ok(Ok(FileMetadata { mode, size, created, accessed, modified, dev, uid, gid }))
1639    }
1640}