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