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 macos_fbsd_solarish_stat(
531        &mut self,
532        path_op: &OpTy<'tcx>,
533        buf_op: &OpTy<'tcx>,
534    ) -> InterpResult<'tcx, Scalar> {
535        let this = self.eval_context_mut();
536
537        if !matches!(&this.tcx.sess.target.os, Os::MacOs | Os::FreeBsd | Os::Solaris | Os::Illumos)
538        {
539            panic!("`macos_fbsd_solaris_stat` should not be called on {}", this.tcx.sess.target.os);
540        }
541
542        let path_scalar = this.read_pointer(path_op)?;
543        let path = this.read_path_from_c_str(path_scalar)?.into_owned();
544
545        // Reject if isolation is enabled.
546        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
547            this.reject_in_isolation("`stat`", reject_with)?;
548            return this.set_last_error_and_return_i32(LibcError("EACCES"));
549        }
550
551        // `stat` always follows symlinks.
552        let metadata = match FileMetadata::from_path(this, &path, true)? {
553            Ok(metadata) => metadata,
554            Err(err) => return this.set_last_error_and_return_i32(err),
555        };
556
557        interp_ok(Scalar::from_i32(this.write_stat_buf(metadata, buf_op)?))
558    }
559
560    // `lstat` is used to get symlink metadata.
561    fn macos_fbsd_solarish_lstat(
562        &mut self,
563        path_op: &OpTy<'tcx>,
564        buf_op: &OpTy<'tcx>,
565    ) -> InterpResult<'tcx, Scalar> {
566        let this = self.eval_context_mut();
567
568        if !matches!(&this.tcx.sess.target.os, Os::MacOs | Os::FreeBsd | Os::Solaris | Os::Illumos)
569        {
570            panic!(
571                "`macos_fbsd_solaris_lstat` should not be called on {}",
572                this.tcx.sess.target.os
573            );
574        }
575
576        let path_scalar = this.read_pointer(path_op)?;
577        let path = this.read_path_from_c_str(path_scalar)?.into_owned();
578
579        // Reject if isolation is enabled.
580        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
581            this.reject_in_isolation("`lstat`", reject_with)?;
582            return this.set_last_error_and_return_i32(LibcError("EACCES"));
583        }
584
585        let metadata = match FileMetadata::from_path(this, &path, false)? {
586            Ok(metadata) => metadata,
587            Err(err) => return this.set_last_error_and_return_i32(err),
588        };
589
590        interp_ok(Scalar::from_i32(this.write_stat_buf(metadata, buf_op)?))
591    }
592
593    fn fstat(&mut self, fd_op: &OpTy<'tcx>, buf_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
594        let this = self.eval_context_mut();
595
596        if !matches!(
597            &this.tcx.sess.target.os,
598            Os::MacOs | Os::FreeBsd | Os::Solaris | Os::Illumos | Os::Linux
599        ) {
600            panic!("`fstat` should not be called on {}", this.tcx.sess.target.os);
601        }
602
603        let fd = this.read_scalar(fd_op)?.to_i32()?;
604
605        // Reject if isolation is enabled.
606        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
607            this.reject_in_isolation("`fstat`", reject_with)?;
608            // Set error code as "EBADF" (bad fd)
609            return this.set_last_error_and_return_i32(LibcError("EBADF"));
610        }
611
612        let metadata = match FileMetadata::from_fd_num(this, fd)? {
613            Ok(metadata) => metadata,
614            Err(err) => return this.set_last_error_and_return_i32(err),
615        };
616        interp_ok(Scalar::from_i32(this.write_stat_buf(metadata, buf_op)?))
617    }
618
619    fn linux_statx(
620        &mut self,
621        dirfd_op: &OpTy<'tcx>,    // Should be an `int`
622        pathname_op: &OpTy<'tcx>, // Should be a `const char *`
623        flags_op: &OpTy<'tcx>,    // Should be an `int`
624        mask_op: &OpTy<'tcx>,     // Should be an `unsigned int`
625        statxbuf_op: &OpTy<'tcx>, // Should be a `struct statx *`
626    ) -> InterpResult<'tcx, Scalar> {
627        let this = self.eval_context_mut();
628
629        this.assert_target_os(Os::Linux, "statx");
630
631        let dirfd = this.read_scalar(dirfd_op)?.to_i32()?;
632        let pathname_ptr = this.read_pointer(pathname_op)?;
633        let flags = this.read_scalar(flags_op)?.to_i32()?;
634        let _mask = this.read_scalar(mask_op)?.to_u32()?;
635        let statxbuf_ptr = this.read_pointer(statxbuf_op)?;
636
637        // If the statxbuf or pathname pointers are null, the function fails with `EFAULT`.
638        if this.ptr_is_null(statxbuf_ptr)? || this.ptr_is_null(pathname_ptr)? {
639            return this.set_last_error_and_return_i32(LibcError("EFAULT"));
640        }
641
642        let statxbuf = this.deref_pointer_as(statxbuf_op, this.libc_ty_layout("statx"))?;
643
644        let path = this.read_path_from_c_str(pathname_ptr)?.into_owned();
645        // See <https://github.com/rust-lang/rust/pull/79196> for a discussion of argument sizes.
646        let at_empty_path = this.eval_libc_i32("AT_EMPTY_PATH");
647        let empty_path_flag = flags & at_empty_path == at_empty_path;
648        // We only support:
649        // * interpreting `path` as an absolute directory,
650        // * interpreting `path` as a path relative to `dirfd` when the latter is `AT_FDCWD`, or
651        // * interpreting `dirfd` as any file descriptor when `path` is empty and AT_EMPTY_PATH is
652        // set.
653        // Other behaviors cannot be tested from `libstd` and thus are not implemented. If you
654        // found this error, please open an issue reporting it.
655        if !(path.is_absolute()
656            || dirfd == this.eval_libc_i32("AT_FDCWD")
657            || (path.as_os_str().is_empty() && empty_path_flag))
658        {
659            throw_unsup_format!(
660                "using statx is only supported with absolute paths, relative paths with the file \
661                descriptor `AT_FDCWD`, and empty paths with the `AT_EMPTY_PATH` flag set and any \
662                file descriptor"
663            )
664        }
665
666        // Reject if isolation is enabled.
667        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
668            this.reject_in_isolation("`statx`", reject_with)?;
669            let ecode = if path.is_absolute() || dirfd == this.eval_libc_i32("AT_FDCWD") {
670                // since `path` is provided, either absolute or
671                // relative to CWD, `EACCES` is the most relevant.
672                LibcError("EACCES")
673            } else {
674                // `dirfd` is set to target file, and `path` is empty
675                // (or we would have hit the `throw_unsup_format`
676                // above). `EACCES` would violate the spec.
677                assert!(empty_path_flag);
678                LibcError("EBADF")
679            };
680            return this.set_last_error_and_return_i32(ecode);
681        }
682
683        // the `_mask_op` parameter specifies the file information that the caller requested.
684        // However `statx` is allowed to return information that was not requested or to not
685        // return information that was requested. This `mask` represents the information we can
686        // actually provide for any target.
687        let mut mask = this.eval_libc_u32("STATX_TYPE") | this.eval_libc_u32("STATX_SIZE");
688
689        // If the `AT_SYMLINK_NOFOLLOW` flag is set, we query the file's metadata without following
690        // symbolic links.
691        let follow_symlink = flags & this.eval_libc_i32("AT_SYMLINK_NOFOLLOW") == 0;
692
693        // If the path is empty, and the AT_EMPTY_PATH flag is set, we query the open file
694        // represented by dirfd, whether it's a directory or otherwise.
695        let metadata = if path.as_os_str().is_empty() && empty_path_flag {
696            FileMetadata::from_fd_num(this, dirfd)?
697        } else {
698            FileMetadata::from_path(this, &path, follow_symlink)?
699        };
700        let metadata = match metadata {
701            Ok(metadata) => metadata,
702            Err(err) => return this.set_last_error_and_return_i32(err),
703        };
704
705        // The `mode` field specifies the type of the file and the permissions over the file for
706        // the owner, its group and other users. Given that we can only provide the file type
707        // without using platform specific methods, we only set the bits corresponding to the file
708        // type. This should be an `__u16` but `libc` provides its values as `u32`.
709        let mode: u16 = metadata
710            .mode
711            .to_u32()?
712            .try_into()
713            .unwrap_or_else(|_| bug!("libc contains bad value for constant"));
714
715        // We need to set the corresponding bits of `mask` if the access, creation and modification
716        // times were available. Otherwise we let them be zero.
717        let (access_sec, access_nsec) = metadata
718            .accessed
719            .map(|tup| {
720                mask |= this.eval_libc_u32("STATX_ATIME");
721                interp_ok(tup)
722            })
723            .unwrap_or_else(|| interp_ok((0, 0)))?;
724
725        let (created_sec, created_nsec) = metadata
726            .created
727            .map(|tup| {
728                mask |= this.eval_libc_u32("STATX_BTIME");
729                interp_ok(tup)
730            })
731            .unwrap_or_else(|| interp_ok((0, 0)))?;
732
733        let (modified_sec, modified_nsec) = metadata
734            .modified
735            .map(|tup| {
736                mask |= this.eval_libc_u32("STATX_MTIME");
737                interp_ok(tup)
738            })
739            .unwrap_or_else(|| interp_ok((0, 0)))?;
740
741        // Now we write everything to `statxbuf`. We write a zero for the unavailable fields.
742        this.write_int_fields_named(
743            &[
744                ("stx_mask", mask.into()),
745                ("stx_blksize", 0),
746                ("stx_attributes", 0),
747                ("stx_nlink", 0),
748                ("stx_uid", 0),
749                ("stx_gid", 0),
750                ("stx_mode", mode.into()),
751                ("stx_ino", 0),
752                ("stx_size", metadata.size.into()),
753                ("stx_blocks", 0),
754                ("stx_attributes_mask", 0),
755                ("stx_rdev_major", 0),
756                ("stx_rdev_minor", 0),
757                ("stx_dev_major", 0),
758                ("stx_dev_minor", 0),
759            ],
760            &statxbuf,
761        )?;
762        #[rustfmt::skip]
763        this.write_int_fields_named(
764            &[
765                ("tv_sec", access_sec.into()),
766                ("tv_nsec", access_nsec.into()),
767            ],
768            &this.project_field_named(&statxbuf, "stx_atime")?,
769        )?;
770        #[rustfmt::skip]
771        this.write_int_fields_named(
772            &[
773                ("tv_sec", created_sec.into()),
774                ("tv_nsec", created_nsec.into()),
775            ],
776            &this.project_field_named(&statxbuf, "stx_btime")?,
777        )?;
778        #[rustfmt::skip]
779        this.write_int_fields_named(
780            &[
781                ("tv_sec", 0.into()),
782                ("tv_nsec", 0.into()),
783            ],
784            &this.project_field_named(&statxbuf, "stx_ctime")?,
785        )?;
786        #[rustfmt::skip]
787        this.write_int_fields_named(
788            &[
789                ("tv_sec", modified_sec.into()),
790                ("tv_nsec", modified_nsec.into()),
791            ],
792            &this.project_field_named(&statxbuf, "stx_mtime")?,
793        )?;
794
795        interp_ok(Scalar::from_i32(0))
796    }
797
798    fn rename(
799        &mut self,
800        oldpath_op: &OpTy<'tcx>,
801        newpath_op: &OpTy<'tcx>,
802    ) -> InterpResult<'tcx, Scalar> {
803        let this = self.eval_context_mut();
804
805        let oldpath_ptr = this.read_pointer(oldpath_op)?;
806        let newpath_ptr = this.read_pointer(newpath_op)?;
807
808        if this.ptr_is_null(oldpath_ptr)? || this.ptr_is_null(newpath_ptr)? {
809            return this.set_last_error_and_return_i32(LibcError("EFAULT"));
810        }
811
812        let oldpath = this.read_path_from_c_str(oldpath_ptr)?;
813        let newpath = this.read_path_from_c_str(newpath_ptr)?;
814
815        // Reject if isolation is enabled.
816        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
817            this.reject_in_isolation("`rename`", reject_with)?;
818            return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);
819        }
820
821        let result = rename(oldpath, newpath).map(|_| 0);
822
823        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
824    }
825
826    fn mkdir(&mut self, path_op: &OpTy<'tcx>, mode_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
827        let this = self.eval_context_mut();
828
829        #[cfg_attr(not(unix), allow(unused_variables))]
830        let mode = if matches!(&this.tcx.sess.target.os, Os::MacOs | Os::FreeBsd) {
831            u32::from(this.read_scalar(mode_op)?.to_u16()?)
832        } else {
833            this.read_scalar(mode_op)?.to_u32()?
834        };
835
836        let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
837
838        // Reject if isolation is enabled.
839        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
840            this.reject_in_isolation("`mkdir`", reject_with)?;
841            return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);
842        }
843
844        #[cfg_attr(not(unix), allow(unused_mut))]
845        let mut builder = DirBuilder::new();
846
847        // If the host supports it, forward on the mode of the directory
848        // (i.e. permission bits and the sticky bit)
849        #[cfg(unix)]
850        {
851            use std::os::unix::fs::DirBuilderExt;
852            builder.mode(mode);
853        }
854
855        let result = builder.create(path).map(|_| 0i32);
856
857        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
858    }
859
860    fn rmdir(&mut self, path_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
861        let this = self.eval_context_mut();
862
863        let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
864
865        // Reject if isolation is enabled.
866        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
867            this.reject_in_isolation("`rmdir`", reject_with)?;
868            return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);
869        }
870
871        let result = remove_dir(path).map(|_| 0i32);
872
873        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
874    }
875
876    fn opendir(&mut self, name_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
877        let this = self.eval_context_mut();
878
879        let name = this.read_path_from_c_str(this.read_pointer(name_op)?)?;
880
881        // Reject if isolation is enabled.
882        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
883            this.reject_in_isolation("`opendir`", reject_with)?;
884            this.set_last_error(LibcError("EACCES"))?;
885            return interp_ok(Scalar::null_ptr(this));
886        }
887
888        let result = read_dir(name);
889
890        match result {
891            Ok(dir_iter) => {
892                let id = this.machine.dirs.insert_new(dir_iter);
893
894                // The libc API for opendir says that this method returns a pointer to an opaque
895                // structure, but we are returning an ID number. Thus, pass it as a scalar of
896                // pointer width.
897                interp_ok(Scalar::from_target_usize(id, this))
898            }
899            Err(e) => {
900                this.set_last_error(e)?;
901                interp_ok(Scalar::null_ptr(this))
902            }
903        }
904    }
905
906    fn readdir64(&mut self, dirent_type: &str, dirp_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
907        let this = self.eval_context_mut();
908
909        if !matches!(&this.tcx.sess.target.os, Os::Linux | Os::Solaris | Os::Illumos | Os::FreeBsd)
910        {
911            panic!("`linux_solaris_readdir64` should not be called on {}", this.tcx.sess.target.os);
912        }
913
914        let dirp = this.read_target_usize(dirp_op)?;
915
916        // Reject if isolation is enabled.
917        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
918            this.reject_in_isolation("`readdir`", reject_with)?;
919            this.set_last_error(LibcError("EBADF"))?;
920            return interp_ok(Scalar::null_ptr(this));
921        }
922
923        let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| {
924            err_unsup_format!("the DIR pointer passed to readdir64 did not come from opendir")
925        })?;
926
927        let entry = match open_dir.read_dir.next() {
928            Some(Ok(dir_entry)) => {
929                // If the host is a Unix system, fill in the inode number with its real value.
930                // If not, use 0 as a fallback value.
931                #[cfg(unix)]
932                let ino = std::os::unix::fs::DirEntryExt::ino(&dir_entry);
933                #[cfg(not(unix))]
934                let ino = 0u64;
935
936                // Write the directory entry into a newly allocated buffer.
937                // The name is written with write_bytes, while the rest of the
938                // dirent64 (or dirent) struct is written using write_int_fields.
939
940                // For reference:
941                // On Linux:
942                // pub struct dirent64 {
943                //     pub d_ino: ino64_t,
944                //     pub d_off: off64_t,
945                //     pub d_reclen: c_ushort,
946                //     pub d_type: c_uchar,
947                //     pub d_name: [c_char; 256],
948                // }
949                //
950                // On Solaris:
951                // pub struct dirent {
952                //     pub d_ino: ino64_t,
953                //     pub d_off: off64_t,
954                //     pub d_reclen: c_ushort,
955                //     pub d_name: [c_char; 3],
956                // }
957                //
958                // On FreeBSD:
959                // pub struct dirent{
960                //     pub d_fileno: uint32_t,
961                //     pub d_reclen: uint16_t,
962                //     pub d_type: uint8_t,
963                //     pub d_namlen: uint8_t,
964                //     pub d_name: [c_char; 256]
965                // }
966
967                let mut name = dir_entry.file_name(); // not a Path as there are no separators!
968                name.push("\0"); // Add a NUL terminator
969                let name_bytes = name.as_encoded_bytes();
970                let name_len = u64::try_from(name_bytes.len()).unwrap();
971
972                let dirent_layout = this.libc_ty_layout(dirent_type);
973                let fields = &dirent_layout.fields;
974                let last_field = fields.count().strict_sub(1);
975                let d_name_offset = fields.offset(last_field).bytes();
976                let size = d_name_offset.strict_add(name_len);
977
978                let entry = this.allocate_ptr(
979                    Size::from_bytes(size),
980                    dirent_layout.align.abi,
981                    MiriMemoryKind::Runtime.into(),
982                    AllocInit::Uninit,
983                )?;
984                let entry = this.ptr_to_mplace(entry.into(), dirent_layout);
985
986                // Write common fields
987                let ino_name =
988                    if this.tcx.sess.target.os == Os::FreeBsd { "d_fileno" } else { "d_ino" };
989                this.write_int_fields_named(
990                    &[(ino_name, ino.into()), ("d_reclen", size.into())],
991                    &entry,
992                )?;
993
994                // Write "optional" fields.
995                if let Some(d_off) = this.try_project_field_named(&entry, "d_off")? {
996                    this.write_null(&d_off)?;
997                }
998
999                if let Some(d_namlen) = this.try_project_field_named(&entry, "d_namlen")? {
1000                    this.write_int(name_len.strict_sub(1), &d_namlen)?;
1001                }
1002
1003                let file_type = this.file_type_to_d_type(dir_entry.file_type())?;
1004                if let Some(d_type) = this.try_project_field_named(&entry, "d_type")? {
1005                    this.write_int(file_type, &d_type)?;
1006                }
1007
1008                // The name is not a normal field, we already computed the offset above.
1009                let name_ptr = entry.ptr().wrapping_offset(Size::from_bytes(d_name_offset), this);
1010                this.write_bytes_ptr(name_ptr, name_bytes.iter().copied())?;
1011
1012                Some(entry.ptr())
1013            }
1014            None => {
1015                // end of stream: return NULL
1016                None
1017            }
1018            Some(Err(e)) => {
1019                this.set_last_error(e)?;
1020                None
1021            }
1022        };
1023
1024        let open_dir = this.machine.dirs.streams.get_mut(&dirp).unwrap();
1025        let old_entry = std::mem::replace(&mut open_dir.entry, entry);
1026        if let Some(old_entry) = old_entry {
1027            this.deallocate_ptr(old_entry, None, MiriMemoryKind::Runtime.into())?;
1028        }
1029
1030        interp_ok(Scalar::from_maybe_pointer(entry.unwrap_or_else(Pointer::null), this))
1031    }
1032
1033    fn macos_readdir_r(
1034        &mut self,
1035        dirp_op: &OpTy<'tcx>,
1036        entry_op: &OpTy<'tcx>,
1037        result_op: &OpTy<'tcx>,
1038    ) -> InterpResult<'tcx, Scalar> {
1039        let this = self.eval_context_mut();
1040
1041        this.assert_target_os(Os::MacOs, "readdir_r");
1042
1043        let dirp = this.read_target_usize(dirp_op)?;
1044        let result_place = this.deref_pointer_as(result_op, this.machine.layouts.mut_raw_ptr)?;
1045
1046        // Reject if isolation is enabled.
1047        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1048            this.reject_in_isolation("`readdir_r`", reject_with)?;
1049            // Return error code, do *not* set `errno`.
1050            return interp_ok(this.eval_libc("EBADF"));
1051        }
1052
1053        let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| {
1054            err_unsup_format!("the DIR pointer passed to readdir_r did not come from opendir")
1055        })?;
1056        interp_ok(match open_dir.read_dir.next() {
1057            Some(Ok(dir_entry)) => {
1058                // Write into entry, write pointer to result, return 0 on success.
1059                // The name is written with write_os_str_to_c_str, while the rest of the
1060                // dirent struct is written using write_int_fields.
1061
1062                // For reference, on macOS this looks like:
1063                // pub struct dirent {
1064                //     pub d_ino: u64,
1065                //     pub d_seekoff: u64,
1066                //     pub d_reclen: u16,
1067                //     pub d_namlen: u16,
1068                //     pub d_type: u8,
1069                //     pub d_name: [c_char; 1024],
1070                // }
1071
1072                let entry_place = this.deref_pointer_as(entry_op, this.libc_ty_layout("dirent"))?;
1073                let name_place = this.project_field_named(&entry_place, "d_name")?;
1074
1075                let file_name = dir_entry.file_name(); // not a Path as there are no separators!
1076                let (name_fits, file_name_buf_len) = this.write_os_str_to_c_str(
1077                    &file_name,
1078                    name_place.ptr(),
1079                    name_place.layout.size.bytes(),
1080                )?;
1081                let file_name_len = file_name_buf_len.strict_sub(1);
1082                if !name_fits {
1083                    throw_unsup_format!(
1084                        "a directory entry had a name too large to fit in libc::dirent"
1085                    );
1086                }
1087
1088                // If the host is a Unix system, fill in the inode number with its real value.
1089                // If not, use 0 as a fallback value.
1090                #[cfg(unix)]
1091                let ino = std::os::unix::fs::DirEntryExt::ino(&dir_entry);
1092                #[cfg(not(unix))]
1093                let ino = 0u64;
1094
1095                let file_type = this.file_type_to_d_type(dir_entry.file_type())?;
1096
1097                this.write_int_fields_named(
1098                    &[
1099                        ("d_reclen", 0),
1100                        ("d_namlen", file_name_len.into()),
1101                        ("d_type", file_type.into()),
1102                        ("d_ino", ino.into()),
1103                        ("d_seekoff", 0),
1104                    ],
1105                    &entry_place,
1106                )?;
1107                this.write_scalar(this.read_scalar(entry_op)?, &result_place)?;
1108
1109                Scalar::from_i32(0)
1110            }
1111            None => {
1112                // end of stream: return 0, assign *result=NULL
1113                this.write_null(&result_place)?;
1114                Scalar::from_i32(0)
1115            }
1116            Some(Err(e)) => {
1117                // return positive error number on error (do *not* set last error)
1118                this.io_error_to_errnum(e)?
1119            }
1120        })
1121    }
1122
1123    fn closedir(&mut self, dirp_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1124        let this = self.eval_context_mut();
1125
1126        let dirp = this.read_target_usize(dirp_op)?;
1127
1128        // Reject if isolation is enabled.
1129        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1130            this.reject_in_isolation("`closedir`", reject_with)?;
1131            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1132        }
1133
1134        let Some(mut open_dir) = this.machine.dirs.streams.remove(&dirp) else {
1135            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1136        };
1137        if let Some(entry) = open_dir.entry.take() {
1138            this.deallocate_ptr(entry, None, MiriMemoryKind::Runtime.into())?;
1139        }
1140        // We drop the `open_dir`, which will close the host dir handle.
1141        drop(open_dir);
1142
1143        interp_ok(Scalar::from_i32(0))
1144    }
1145
1146    fn ftruncate64(&mut self, fd_num: i32, length: i128) -> InterpResult<'tcx, Scalar> {
1147        let this = self.eval_context_mut();
1148
1149        // Reject if isolation is enabled.
1150        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1151            this.reject_in_isolation("`ftruncate64`", reject_with)?;
1152            // Set error code as "EBADF" (bad fd)
1153            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1154        }
1155
1156        let Some(fd) = this.machine.fds.get(fd_num) else {
1157            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1158        };
1159
1160        // FIXME: Support ftruncate64 for all FDs
1161        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1162            err_unsup_format!("`ftruncate64` is only supported on file-backed file descriptors")
1163        })?;
1164
1165        if file.writable {
1166            if let Ok(length) = length.try_into() {
1167                let result = file.file.set_len(length);
1168                let result = this.try_unwrap_io_result(result.map(|_| 0i32))?;
1169                interp_ok(Scalar::from_i32(result))
1170            } else {
1171                this.set_last_error_and_return_i32(LibcError("EINVAL"))
1172            }
1173        } else {
1174            // The file is not writable
1175            this.set_last_error_and_return_i32(LibcError("EINVAL"))
1176        }
1177    }
1178
1179    /// NOTE: According to the man page of `possix_fallocate`, it returns the error code instead
1180    /// of setting `errno`.
1181    fn posix_fallocate(
1182        &mut self,
1183        fd_num: i32,
1184        offset: i64,
1185        len: i64,
1186    ) -> InterpResult<'tcx, Scalar> {
1187        let this = self.eval_context_mut();
1188
1189        // Reject if isolation is enabled.
1190        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1191            this.reject_in_isolation("`posix_fallocate`", reject_with)?;
1192            // Return error code "EBADF" (bad fd).
1193            return interp_ok(this.eval_libc("EBADF"));
1194        }
1195
1196        // EINVAL is returned when: "offset was less than 0, or len was less than or equal to 0".
1197        if offset < 0 || len <= 0 {
1198            return interp_ok(this.eval_libc("EINVAL"));
1199        }
1200
1201        // Get the file handle.
1202        let Some(fd) = this.machine.fds.get(fd_num) else {
1203            return interp_ok(this.eval_libc("EBADF"));
1204        };
1205        let file = match fd.downcast::<FileHandle>() {
1206            Some(file_handle) => file_handle,
1207            // Man page specifies to return ENODEV if `fd` is not a regular file.
1208            None => return interp_ok(this.eval_libc("ENODEV")),
1209        };
1210
1211        if !file.writable {
1212            // The file is not writable.
1213            return interp_ok(this.eval_libc("EBADF"));
1214        }
1215
1216        let current_size = match file.file.metadata() {
1217            Ok(metadata) => metadata.len(),
1218            Err(err) => return this.io_error_to_errnum(err),
1219        };
1220        // Checked i64 addition, to ensure the result does not exceed the max file size.
1221        let new_size = match offset.checked_add(len) {
1222            // `new_size` is definitely non-negative, so we can cast to `u64`.
1223            Some(new_size) => u64::try_from(new_size).unwrap(),
1224            None => return interp_ok(this.eval_libc("EFBIG")), // new size too big
1225        };
1226        // If the size of the file is less than offset+size, then the file is increased to this size;
1227        // otherwise the file size is left unchanged.
1228        if current_size < new_size {
1229            interp_ok(match file.file.set_len(new_size) {
1230                Ok(()) => Scalar::from_i32(0),
1231                Err(e) => this.io_error_to_errnum(e)?,
1232            })
1233        } else {
1234            interp_ok(Scalar::from_i32(0))
1235        }
1236    }
1237
1238    fn fsync(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1239        // On macOS, `fsync` (unlike `fcntl(F_FULLFSYNC)`) does not wait for the
1240        // underlying disk to finish writing. In the interest of host compatibility,
1241        // we conservatively implement this with `sync_all`, which
1242        // *does* wait for the disk.
1243
1244        let this = self.eval_context_mut();
1245
1246        let fd = this.read_scalar(fd_op)?.to_i32()?;
1247
1248        // Reject if isolation is enabled.
1249        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1250            this.reject_in_isolation("`fsync`", reject_with)?;
1251            // Set error code as "EBADF" (bad fd)
1252            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1253        }
1254
1255        self.ffullsync_fd(fd)
1256    }
1257
1258    fn ffullsync_fd(&mut self, fd_num: i32) -> InterpResult<'tcx, Scalar> {
1259        let this = self.eval_context_mut();
1260        let Some(fd) = this.machine.fds.get(fd_num) else {
1261            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1262        };
1263        // Only regular files support synchronization.
1264        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1265            err_unsup_format!("`fsync` is only supported on file-backed file descriptors")
1266        })?;
1267        let io_result = maybe_sync_file(&file.file, file.writable, File::sync_all);
1268        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1269    }
1270
1271    fn fdatasync(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1272        let this = self.eval_context_mut();
1273
1274        let fd = this.read_scalar(fd_op)?.to_i32()?;
1275
1276        // Reject if isolation is enabled.
1277        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1278            this.reject_in_isolation("`fdatasync`", reject_with)?;
1279            // Set error code as "EBADF" (bad fd)
1280            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1281        }
1282
1283        let Some(fd) = this.machine.fds.get(fd) else {
1284            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1285        };
1286        // Only regular files support synchronization.
1287        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1288            err_unsup_format!("`fdatasync` is only supported on file-backed file descriptors")
1289        })?;
1290        let io_result = maybe_sync_file(&file.file, file.writable, File::sync_data);
1291        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1292    }
1293
1294    fn sync_file_range(
1295        &mut self,
1296        fd_op: &OpTy<'tcx>,
1297        offset_op: &OpTy<'tcx>,
1298        nbytes_op: &OpTy<'tcx>,
1299        flags_op: &OpTy<'tcx>,
1300    ) -> InterpResult<'tcx, Scalar> {
1301        let this = self.eval_context_mut();
1302
1303        let fd = this.read_scalar(fd_op)?.to_i32()?;
1304        let offset = this.read_scalar(offset_op)?.to_i64()?;
1305        let nbytes = this.read_scalar(nbytes_op)?.to_i64()?;
1306        let flags = this.read_scalar(flags_op)?.to_i32()?;
1307
1308        if offset < 0 || nbytes < 0 {
1309            return this.set_last_error_and_return_i32(LibcError("EINVAL"));
1310        }
1311        let allowed_flags = this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_BEFORE")
1312            | this.eval_libc_i32("SYNC_FILE_RANGE_WRITE")
1313            | this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_AFTER");
1314        if flags & allowed_flags != flags {
1315            return this.set_last_error_and_return_i32(LibcError("EINVAL"));
1316        }
1317
1318        // Reject if isolation is enabled.
1319        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1320            this.reject_in_isolation("`sync_file_range`", reject_with)?;
1321            // Set error code as "EBADF" (bad fd)
1322            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1323        }
1324
1325        let Some(fd) = this.machine.fds.get(fd) else {
1326            return this.set_last_error_and_return_i32(LibcError("EBADF"));
1327        };
1328        // Only regular files support synchronization.
1329        let file = fd.downcast::<FileHandle>().ok_or_else(|| {
1330            err_unsup_format!("`sync_data_range` is only supported on file-backed file descriptors")
1331        })?;
1332        let io_result = maybe_sync_file(&file.file, file.writable, File::sync_data);
1333        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
1334    }
1335
1336    fn readlink(
1337        &mut self,
1338        pathname_op: &OpTy<'tcx>,
1339        buf_op: &OpTy<'tcx>,
1340        bufsize_op: &OpTy<'tcx>,
1341    ) -> InterpResult<'tcx, i64> {
1342        let this = self.eval_context_mut();
1343
1344        let pathname = this.read_path_from_c_str(this.read_pointer(pathname_op)?)?;
1345        let buf = this.read_pointer(buf_op)?;
1346        let bufsize = this.read_target_usize(bufsize_op)?;
1347
1348        // Reject if isolation is enabled.
1349        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1350            this.reject_in_isolation("`readlink`", reject_with)?;
1351            this.set_last_error(LibcError("EACCES"))?;
1352            return interp_ok(-1);
1353        }
1354
1355        let result = std::fs::read_link(pathname);
1356        match result {
1357            Ok(resolved) => {
1358                // 'readlink' truncates the resolved path if the provided buffer is not large
1359                // enough, and does *not* add a null terminator. That means we cannot use the usual
1360                // `write_path_to_c_str` and have to re-implement parts of it ourselves.
1361                let resolved = this.convert_path(
1362                    Cow::Borrowed(resolved.as_ref()),
1363                    crate::shims::os_str::PathConversion::HostToTarget,
1364                );
1365                let mut path_bytes = resolved.as_encoded_bytes();
1366                let bufsize: usize = bufsize.try_into().unwrap();
1367                if path_bytes.len() > bufsize {
1368                    path_bytes = &path_bytes[..bufsize]
1369                }
1370                this.write_bytes_ptr(buf, path_bytes.iter().copied())?;
1371                interp_ok(path_bytes.len().try_into().unwrap())
1372            }
1373            Err(e) => {
1374                this.set_last_error(e)?;
1375                interp_ok(-1)
1376            }
1377        }
1378    }
1379
1380    fn isatty(&mut self, miri_fd: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1381        let this = self.eval_context_mut();
1382        // "returns 1 if fd is an open file descriptor referring to a terminal;
1383        // otherwise 0 is returned, and errno is set to indicate the error"
1384        let fd = this.read_scalar(miri_fd)?.to_i32()?;
1385        let error = if let Some(fd) = this.machine.fds.get(fd) {
1386            if fd.is_tty(this.machine.communicate()) {
1387                return interp_ok(Scalar::from_i32(1));
1388            } else {
1389                LibcError("ENOTTY")
1390            }
1391        } else {
1392            // FD does not exist
1393            LibcError("EBADF")
1394        };
1395        this.set_last_error(error)?;
1396        interp_ok(Scalar::from_i32(0))
1397    }
1398
1399    fn realpath(
1400        &mut self,
1401        path_op: &OpTy<'tcx>,
1402        processed_path_op: &OpTy<'tcx>,
1403    ) -> InterpResult<'tcx, Scalar> {
1404        let this = self.eval_context_mut();
1405        this.assert_target_os_is_unix("realpath");
1406
1407        let pathname = this.read_path_from_c_str(this.read_pointer(path_op)?)?;
1408        let processed_ptr = this.read_pointer(processed_path_op)?;
1409
1410        // Reject if isolation is enabled.
1411        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1412            this.reject_in_isolation("`realpath`", reject_with)?;
1413            this.set_last_error(LibcError("EACCES"))?;
1414            return interp_ok(Scalar::from_target_usize(0, this));
1415        }
1416
1417        let result = std::fs::canonicalize(pathname);
1418        match result {
1419            Ok(resolved) => {
1420                let path_max = this
1421                    .eval_libc_i32("PATH_MAX")
1422                    .try_into()
1423                    .expect("PATH_MAX does not fit in u64");
1424                let dest = if this.ptr_is_null(processed_ptr)? {
1425                    // POSIX says behavior when passing a null pointer is implementation-defined,
1426                    // but GNU/linux, freebsd, netbsd, bionic/android, and macos all treat a null pointer
1427                    // similarly to:
1428                    //
1429                    // "If resolved_path is specified as NULL, then realpath() uses
1430                    // malloc(3) to allocate a buffer of up to PATH_MAX bytes to hold
1431                    // the resolved pathname, and returns a pointer to this buffer.  The
1432                    // caller should deallocate this buffer using free(3)."
1433                    // <https://man7.org/linux/man-pages/man3/realpath.3.html>
1434                    this.alloc_path_as_c_str(&resolved, MiriMemoryKind::C.into())?
1435                } else {
1436                    let (wrote_path, _) =
1437                        this.write_path_to_c_str(&resolved, processed_ptr, path_max)?;
1438
1439                    if !wrote_path {
1440                        // Note that we do not explicitly handle `FILENAME_MAX`
1441                        // (different from `PATH_MAX` above) as it is Linux-specific and
1442                        // seems like a bit of a mess anyway: <https://eklitzke.org/path-max-is-tricky>.
1443                        this.set_last_error(LibcError("ENAMETOOLONG"))?;
1444                        return interp_ok(Scalar::from_target_usize(0, this));
1445                    }
1446                    processed_ptr
1447                };
1448
1449                interp_ok(Scalar::from_maybe_pointer(dest, this))
1450            }
1451            Err(e) => {
1452                this.set_last_error(e)?;
1453                interp_ok(Scalar::from_target_usize(0, this))
1454            }
1455        }
1456    }
1457    fn mkstemp(&mut self, template_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
1458        use rand::seq::IndexedRandom;
1459
1460        // POSIX defines the template string.
1461        const TEMPFILE_TEMPLATE_STR: &str = "XXXXXX";
1462
1463        let this = self.eval_context_mut();
1464        this.assert_target_os_is_unix("mkstemp");
1465
1466        // POSIX defines the maximum number of attempts before failure.
1467        //
1468        // `mkstemp()` relies on `tmpnam()` which in turn relies on `TMP_MAX`.
1469        // POSIX says this about `TMP_MAX`:
1470        // * Minimum number of unique filenames generated by `tmpnam()`.
1471        // * Maximum number of times an application can call `tmpnam()` reliably.
1472        //   * The value of `TMP_MAX` is at least 25.
1473        //   * On XSI-conformant systems, the value of `TMP_MAX` is at least 10000.
1474        // See <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/stdio.h.html>.
1475        let max_attempts = this.eval_libc_u32("TMP_MAX");
1476
1477        // Get the raw bytes from the template -- as a byte slice, this is a string in the target
1478        // (and the target is unix, so a byte slice is the right representation).
1479        let template_ptr = this.read_pointer(template_op)?;
1480        let mut template = this.eval_context_ref().read_c_str(template_ptr)?.to_owned();
1481        let template_bytes = template.as_mut_slice();
1482
1483        // Reject if isolation is enabled.
1484        if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
1485            this.reject_in_isolation("`mkstemp`", reject_with)?;
1486            return this.set_last_error_and_return_i32(LibcError("EACCES"));
1487        }
1488
1489        // Get the bytes of the suffix we expect in _target_ encoding.
1490        let suffix_bytes = TEMPFILE_TEMPLATE_STR.as_bytes();
1491
1492        // At this point we have one `&[u8]` that represents the template and one `&[u8]`
1493        // that represents the expected suffix.
1494
1495        // Now we figure out the index of the slice we expect to contain the suffix.
1496        let start_pos = template_bytes.len().saturating_sub(suffix_bytes.len());
1497        let end_pos = template_bytes.len();
1498        let last_six_char_bytes = &template_bytes[start_pos..end_pos];
1499
1500        // If we don't find the suffix, it is an error.
1501        if last_six_char_bytes != suffix_bytes {
1502            return this.set_last_error_and_return_i32(LibcError("EINVAL"));
1503        }
1504
1505        // At this point we know we have 6 ASCII 'X' characters as a suffix.
1506
1507        // From <https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/sysdeps/posix/tempname.c#L175>
1508        const SUBSTITUTIONS: &[char; 62] = &[
1509            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
1510            'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
1511            'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
1512            'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
1513        ];
1514
1515        // The file is opened with specific options, which Rust does not expose in a portable way.
1516        // So we use specific APIs depending on the host OS.
1517        let mut fopts = OpenOptions::new();
1518        fopts.read(true).write(true).create_new(true);
1519
1520        #[cfg(unix)]
1521        {
1522            use std::os::unix::fs::OpenOptionsExt;
1523            // Do not allow others to read or modify this file.
1524            fopts.mode(0o600);
1525            fopts.custom_flags(libc::O_EXCL);
1526        }
1527        #[cfg(windows)]
1528        {
1529            use std::os::windows::fs::OpenOptionsExt;
1530            // Do not allow others to read or modify this file.
1531            fopts.share_mode(0);
1532        }
1533
1534        // If the generated file already exists, we will try again `max_attempts` many times.
1535        for _ in 0..max_attempts {
1536            let rng = this.machine.rng.get_mut();
1537
1538            // Generate a random unique suffix.
1539            let unique_suffix = SUBSTITUTIONS.choose_multiple(rng, 6).collect::<String>();
1540
1541            // Replace the template string with the random string.
1542            template_bytes[start_pos..end_pos].copy_from_slice(unique_suffix.as_bytes());
1543
1544            // Write the modified template back to the passed in pointer to maintain POSIX semantics.
1545            this.write_bytes_ptr(template_ptr, template_bytes.iter().copied())?;
1546
1547            // To actually open the file, turn this into a host OsString.
1548            let p = bytes_to_os_str(template_bytes)?.to_os_string();
1549
1550            let possibly_unique = std::env::temp_dir().join::<PathBuf>(p.into());
1551
1552            let file = fopts.open(possibly_unique);
1553
1554            match file {
1555                Ok(f) => {
1556                    let fd = this.machine.fds.insert_new(FileHandle { file: f, writable: true });
1557                    return interp_ok(Scalar::from_i32(fd));
1558                }
1559                Err(e) =>
1560                    match e.kind() {
1561                        // If the random file already exists, keep trying.
1562                        ErrorKind::AlreadyExists => continue,
1563                        // Any other errors are returned to the caller.
1564                        _ => {
1565                            // "On error, -1 is returned, and errno is set to
1566                            // indicate the error"
1567                            return this.set_last_error_and_return_i32(e);
1568                        }
1569                    },
1570            }
1571        }
1572
1573        // We ran out of attempts to create the file, return an error.
1574        this.set_last_error_and_return_i32(LibcError("EEXIST"))
1575    }
1576}
1577
1578/// Extracts the number of seconds and nanoseconds elapsed between `time` and the unix epoch when
1579/// `time` is Ok. Returns `None` if `time` is an error. Fails if `time` happens before the unix
1580/// epoch.
1581fn extract_sec_and_nsec<'tcx>(
1582    time: std::io::Result<SystemTime>,
1583) -> InterpResult<'tcx, Option<(u64, u32)>> {
1584    match time.ok() {
1585        Some(time) => {
1586            let duration = system_time_to_duration(&time)?;
1587            interp_ok(Some((duration.as_secs(), duration.subsec_nanos())))
1588        }
1589        None => interp_ok(None),
1590    }
1591}
1592
1593/// Stores a file's metadata in order to avoid code duplication in the different metadata related
1594/// shims.
1595struct FileMetadata {
1596    mode: Scalar,
1597    size: u64,
1598    created: Option<(u64, u32)>,
1599    accessed: Option<(u64, u32)>,
1600    modified: Option<(u64, u32)>,
1601    dev: u64,
1602    uid: u32,
1603    gid: u32,
1604}
1605
1606impl FileMetadata {
1607    fn from_path<'tcx>(
1608        ecx: &mut MiriInterpCx<'tcx>,
1609        path: &Path,
1610        follow_symlink: bool,
1611    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1612        let metadata =
1613            if follow_symlink { std::fs::metadata(path) } else { std::fs::symlink_metadata(path) };
1614
1615        FileMetadata::from_meta(ecx, metadata)
1616    }
1617
1618    fn from_fd_num<'tcx>(
1619        ecx: &mut MiriInterpCx<'tcx>,
1620        fd_num: i32,
1621    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1622        let Some(fd) = ecx.machine.fds.get(fd_num) else {
1623            return interp_ok(Err(LibcError("EBADF")));
1624        };
1625
1626        let metadata = fd.metadata()?;
1627        drop(fd);
1628        FileMetadata::from_meta(ecx, metadata)
1629    }
1630
1631    fn from_meta<'tcx>(
1632        ecx: &mut MiriInterpCx<'tcx>,
1633        metadata: Result<std::fs::Metadata, std::io::Error>,
1634    ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
1635        let metadata = match metadata {
1636            Ok(metadata) => metadata,
1637            Err(e) => {
1638                return interp_ok(Err(e.into()));
1639            }
1640        };
1641
1642        let file_type = metadata.file_type();
1643
1644        let mode_name = if file_type.is_file() {
1645            "S_IFREG"
1646        } else if file_type.is_dir() {
1647            "S_IFDIR"
1648        } else {
1649            "S_IFLNK"
1650        };
1651
1652        let mode = ecx.eval_libc(mode_name);
1653
1654        let size = metadata.len();
1655
1656        let created = extract_sec_and_nsec(metadata.created())?;
1657        let accessed = extract_sec_and_nsec(metadata.accessed())?;
1658        let modified = extract_sec_and_nsec(metadata.modified())?;
1659
1660        // FIXME: Provide more fields using platform specific methods.
1661
1662        cfg_select! {
1663            unix => {
1664                use std::os::unix::fs::MetadataExt;
1665                let dev = metadata.dev();
1666                let uid = metadata.uid();
1667                let gid = metadata.gid();
1668            }
1669            _ => {
1670                let dev = 0;
1671                let uid = 0;
1672                let gid = 0;
1673            }
1674        }
1675
1676        interp_ok(Ok(FileMetadata { mode, size, created, accessed, modified, dev, uid, gid }))
1677    }
1678}