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