Skip to main content

miri/shims/unix/
fs.rs

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