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