miri/shims/unix/
fs.rs

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