1//! File and file system access23use std::borrow::Cow;4use std::ffi::OsString;5use std::fs::{6 self, DirBuilder, File, FileType, OpenOptions, TryLockError, read_dir, remove_dir, remove_file,7 rename,8};9use std::io::{self, ErrorKind, Read, Seek, SeekFrom, Write};10use std::path::{self, Path, PathBuf};11use std::time::SystemTime;1213use rustc_abi::Size;14use rustc_data_structures::either::Either;15use rustc_data_structures::fx::FxHashMap;16use rustc_target::spec::Os;1718use self::shims::time::system_time_to_duration;19use crate::shims::files::FileHandle;20use crate::shims::os_str::bytes_to_os_str;21use crate::shims::sig::check_min_vararg_count;22use crate::shims::unix::fd::{FlockOp, UnixFileDescription};23use crate::*;2425/// An open directory, tracked by DirHandler.26#[derive(Debug)]27struct OpenDir {28 /// The "special" entries that must still be yielded by the iterator.29 /// Used for `.` and `..`.30 special_entries: Vec<&'static str>,31 /// The directory reader on the host.32 read_dir: fs::ReadDir,33 /// The most recent entry returned by readdir().34 /// Will be freed by the next call.35 entry: Option<Pointer>,36}3738impl OpenDir {39 fn new(read_dir: fs::ReadDir) -> Self {40 Self { special_entries: vec!["..", "."], read_dir, entry: None }41 }4243 fn next_host_entry(&mut self) -> Option<io::Result<Either<fs::DirEntry, &'static str>>> {44 if let Some(special) = self.special_entries.pop() {45 return Some(Ok(Either::Right(special)));46 }47 let entry = self.read_dir.next()?;48 Some(entry.map(Either::Left))49 }50}5152#[derive(Debug)]53struct DirEntry {54 name: OsString,55 ino: u64,56 d_type: i32,57}5859impl UnixFileDescription for FileHandle {60 fn pread<'tcx>(61 &self,62 communicate_allowed: bool,63 offset: u64,64 ptr: Pointer,65 len: usize,66 ecx: &mut MiriInterpCx<'tcx>,67 finish: DynMachineCallback<'tcx, Result<usize, IoError>>,68 ) -> InterpResult<'tcx> {69 assert!(communicate_allowed, "isolation should have prevented even opening a file");70 let mut bytes = vec![0; len];71 // Emulates pread using seek + read + 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 mut f = || {76 let cursor_pos = file.stream_position()?;77 file.seek(SeekFrom::Start(offset))?;78 let res = file.read(&mut bytes);79 // Attempt to restore cursor position even if the read has failed80 file.seek(SeekFrom::Start(cursor_pos))81 .expect("failed to restore file position, this shouldn't be possible");82 res83 };84 let result = match f() {85 Ok(read_size) => {86 // If reading to `bytes` did not fail, we write those bytes to the buffer.87 // Crucially, if fewer than `bytes.len()` bytes were read, only write88 // that much into the output buffer!89 ecx.write_bytes_ptr(ptr, bytes[..read_size].iter().copied())?;90 Ok(read_size)91 }92 Err(e) => Err(IoError::HostError(e)),93 };94 finish.call(ecx, result)95 }9697 fn pwrite<'tcx>(98 &self,99 communicate_allowed: bool,100 ptr: Pointer,101 len: usize,102 offset: u64,103 ecx: &mut MiriInterpCx<'tcx>,104 finish: DynMachineCallback<'tcx, Result<usize, IoError>>,105 ) -> InterpResult<'tcx> {106 assert!(communicate_allowed, "isolation should have prevented even opening a file");107 // Emulates pwrite using seek + write + seek to restore cursor position.108 // Correctness of this emulation relies on sequential nature of Miri execution.109 // The closure is used to emulate `try` block, since we "bubble" `io::Error` using `?`.110 let file = &mut &self.file;111 let bytes = ecx.read_bytes_ptr_strip_provenance(ptr, Size::from_bytes(len))?;112 let mut f = || {113 let cursor_pos = file.stream_position()?;114 file.seek(SeekFrom::Start(offset))?;115 let res = file.write(bytes);116 // Attempt to restore cursor position even if the write has failed117 file.seek(SeekFrom::Start(cursor_pos))118 .expect("failed to restore file position, this shouldn't be possible");119 res120 };121 let result = f();122 finish.call(ecx, result.map_err(IoError::HostError))123 }124125 fn flock<'tcx>(126 &self,127 communicate_allowed: bool,128 op: FlockOp,129 ) -> InterpResult<'tcx, io::Result<()>> {130 assert!(communicate_allowed, "isolation should have prevented even opening a file");131132 use FlockOp::*;133 // We must not block the interpreter loop, so we always `try_lock`.134 let (res, nonblocking) = match op {135 SharedLock { nonblocking } => (self.file.try_lock_shared(), nonblocking),136 ExclusiveLock { nonblocking } => (self.file.try_lock(), nonblocking),137 Unlock => {138 return interp_ok(self.file.unlock());139 }140 };141142 match res {143 Ok(()) => interp_ok(Ok(())),144 Err(TryLockError::Error(err)) => interp_ok(Err(err)),145 Err(TryLockError::WouldBlock) =>146 if nonblocking {147 interp_ok(Err(ErrorKind::WouldBlock.into()))148 } else {149 throw_unsup_format!("blocking `flock` is not currently supported");150 },151 }152 }153}154155/// The table of open directories.156/// Curiously, Unix/POSIX does not unify this into the "file descriptor" concept... everything157/// is a file, except a directory is not?158#[derive(Debug)]159pub struct DirTable {160 /// Directory iterators used to emulate libc "directory streams", as used in opendir, readdir,161 /// and closedir.162 ///163 /// When opendir is called, a directory iterator is created on the host for the target164 /// directory, and an entry is stored in this hash map, indexed by an ID which represents165 /// the directory stream. When readdir is called, the directory stream ID is used to look up166 /// the corresponding ReadDir iterator from this map, and information from the next167 /// directory entry is returned. When closedir is called, the ReadDir iterator is removed from168 /// the map.169 streams: FxHashMap<u64, OpenDir>,170 /// ID number to be used by the next call to opendir171 next_id: u64,172}173174impl DirTable {175 #[expect(clippy::arithmetic_side_effects)]176 fn insert_new(&mut self, read_dir: fs::ReadDir) -> u64 {177 let id = self.next_id;178 self.next_id += 1;179 self.streams.try_insert(id, OpenDir::new(read_dir)).unwrap();180 id181 }182}183184impl Default for DirTable {185 fn default() -> DirTable {186 DirTable {187 streams: FxHashMap::default(),188 // Skip 0 as an ID, because it looks like a null pointer to libc189 next_id: 1,190 }191 }192}193194impl VisitProvenance for DirTable {195 fn visit_provenance(&self, visit: &mut VisitWith<'_>) {196 let DirTable { streams, next_id: _ } = self;197198 for dir in streams.values() {199 dir.entry.visit_provenance(visit);200 }201 }202}203204fn maybe_sync_file(205 file: &File,206 writable: bool,207 operation: fn(&File) -> std::io::Result<()>,208) -> std::io::Result<i32> {209 if !writable && cfg!(windows) {210 // sync_all() and sync_data() will return an error on Windows hosts if the file is not opened211 // for writing. (FlushFileBuffers requires that the file handle have the212 // GENERIC_WRITE right)213 Ok(0i32)214 } else {215 let result = operation(file);216 result.map(|_| 0i32)217 }218}219220impl<'tcx> EvalContextExtPrivate<'tcx> for crate::MiriInterpCx<'tcx> {}221trait EvalContextExtPrivate<'tcx>: crate::MiriInterpCxExt<'tcx> {222 fn write_stat_buf(223 &mut self,224 metadata: FileMetadata,225 buf_op: &OpTy<'tcx>,226 ) -> InterpResult<'tcx, i32> {227 let this = self.eval_context_mut();228229 let (access_sec, access_nsec) = metadata.accessed.unwrap_or((0, 0));230 let (created_sec, created_nsec) = metadata.created.unwrap_or((0, 0));231 let (modified_sec, modified_nsec) = metadata.modified.unwrap_or((0, 0));232233 // We do *not* use `deref_pointer_as` here since determining the right pointee type234 // is highly non-trivial: it depends on which exact alias of the function was invoked235 // (e.g. `fstat` vs `fstat64`), and then on FreeBSD it also depends on the ABI level236 // which can be different between the libc used by std and the libc used by everyone else.237 let buf = this.deref_pointer(buf_op)?;238239 // `libc::S_IF*` constants are of type `mode_t`, which varies in width across targets240 // (`u16` on macOS, `u32` on Linux). Read the scalar using `mode_t`'s size on the target.241 let mode_t_size = this.libc_ty_layout("mode_t").size;242 let mode: u32 = metadata.mode.to_uint(mode_t_size)?.try_into().unwrap();243244 this.write_int_fields_named(245 &[246 ("st_dev", metadata.dev.unwrap_or(0).into()),247 ("st_mode", mode.into()),248 ("st_nlink", metadata.nlink.unwrap_or(0).into()),249 ("st_ino", metadata.ino.unwrap_or(0).into()),250 ("st_uid", metadata.uid.unwrap_or(0).into()),251 ("st_gid", metadata.gid.unwrap_or(0).into()),252 ("st_rdev", 0),253 ("st_atime", access_sec.into()),254 ("st_atime_nsec", access_nsec.into()),255 ("st_mtime", modified_sec.into()),256 ("st_mtime_nsec", modified_nsec.into()),257 ("st_ctime", 0),258 ("st_ctime_nsec", 0),259 ("st_size", metadata.size.into()),260 ("st_blocks", metadata.blocks.unwrap_or(0).into()),261 ("st_blksize", metadata.blksize.unwrap_or(0).into()),262 ],263 &buf,264 )?;265266 if matches!(&this.tcx.sess.target.os, Os::MacOs | Os::FreeBsd) {267 this.write_int_fields_named(268 &[269 ("st_birthtime", created_sec.into()),270 ("st_birthtime_nsec", created_nsec.into()),271 ("st_flags", 0),272 ("st_gen", 0),273 ],274 &buf,275 )?;276 }277278 if matches!(&this.tcx.sess.target.os, Os::Solaris | Os::Illumos) {279 let st_fstype = this.project_field_named(&buf, "st_fstype")?;280 // This is an array; write 0 into first element so that it encodes the empty string.281 this.write_int(0, &this.project_index(&st_fstype, 0)?)?;282 }283284 interp_ok(0)285 }286287 fn file_type_to_d_type(&self, file_type: std::io::Result<FileType>) -> InterpResult<'tcx, i32> {288 #[cfg(unix)]289 use std::os::unix::fs::FileTypeExt;290291 let this = self.eval_context_ref();292 match file_type {293 Ok(file_type) => {294 match () {295 _ if file_type.is_dir() => interp_ok(this.eval_libc("DT_DIR").to_u8()?.into()),296 _ if file_type.is_file() => interp_ok(this.eval_libc("DT_REG").to_u8()?.into()),297 _ if file_type.is_symlink() =>298 interp_ok(this.eval_libc("DT_LNK").to_u8()?.into()),299 // Certain file types are only supported when the host is a Unix system.300 #[cfg(unix)]301 _ if file_type.is_block_device() =>302 interp_ok(this.eval_libc("DT_BLK").to_u8()?.into()),303 #[cfg(unix)]304 _ if file_type.is_char_device() =>305 interp_ok(this.eval_libc("DT_CHR").to_u8()?.into()),306 #[cfg(unix)]307 _ if file_type.is_fifo() =>308 interp_ok(this.eval_libc("DT_FIFO").to_u8()?.into()),309 #[cfg(unix)]310 _ if file_type.is_socket() =>311 interp_ok(this.eval_libc("DT_SOCK").to_u8()?.into()),312 // Fallback313 _ => interp_ok(this.eval_libc("DT_UNKNOWN").to_u8()?.into()),314 }315 }316 Err(_) => {317 // Fallback on error318 interp_ok(this.eval_libc("DT_UNKNOWN").to_u8()?.into())319 }320 }321 }322323 fn dir_entry_fields(324 &self,325 entry: Either<fs::DirEntry, &'static str>,326 ) -> InterpResult<'tcx, DirEntry> {327 let this = self.eval_context_ref();328 interp_ok(match entry {329 Either::Left(dir_entry) => {330 DirEntry {331 name: dir_entry.file_name(),332 d_type: this.file_type_to_d_type(dir_entry.file_type())?,333 // If the host is a Unix system, fill in the inode number with its real value.334 // If not, use 0 as a fallback value.335 #[cfg(unix)]336 ino: std::os::unix::fs::DirEntryExt::ino(&dir_entry),337 #[cfg(not(unix))]338 ino: 0u64,339 }340 }341 Either::Right(special) =>342 DirEntry {343 name: special.into(),344 d_type: this.eval_libc("DT_DIR").to_u8()?.into(),345 ino: 0,346 },347 })348 }349}350351impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}352pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {353 fn open(354 &mut self,355 path_raw: &OpTy<'tcx>,356 flag: &OpTy<'tcx>,357 varargs: &[OpTy<'tcx>],358 ) -> InterpResult<'tcx, Scalar> {359 let this = self.eval_context_mut();360361 let path_raw = this.read_pointer(path_raw)?;362 let flag = this.read_scalar(flag)?.to_i32()?;363364 let path = this.read_path_from_c_str(path_raw)?;365 // Files in `/proc` won't work properly.366 if matches!(this.tcx.sess.target.os, Os::Linux | Os::Android | Os::Illumos | Os::Solaris)367 && path::absolute(&path).is_ok_and(|path| path.starts_with("/proc"))368 {369 this.machine.emit_diagnostic(NonHaltingDiagnostic::FileInProcOpened);370 }371372 // We will "subtract" supported flags from this and at the end check that no bits are left.373 let mut flag = flag;374375 let mut options = OpenOptions::new();376377 let o_rdonly = this.eval_libc_i32("O_RDONLY");378 let o_wronly = this.eval_libc_i32("O_WRONLY");379 let o_rdwr = this.eval_libc_i32("O_RDWR");380 // The first two bits of the flag correspond to the access mode in linux, macOS and381 // windows. We need to check that in fact the access mode flags for the current target382 // only use these two bits, otherwise we are in an unsupported target and should error.383 if (o_rdonly | o_wronly | o_rdwr) & !0b11 != 0 {384 throw_unsup_format!("access mode flags on this target are unsupported");385 }386 let mut writable = true;387388 // Now we check the access mode389 let access_mode = flag & 0b11;390 flag &= !access_mode;391392 if access_mode == o_rdonly {393 writable = false;394 options.read(true);395 } else if access_mode == o_wronly {396 options.write(true);397 } else if access_mode == o_rdwr {398 options.read(true).write(true);399 } else {400 throw_unsup_format!("unsupported access mode {:#x}", access_mode);401 }402403 let o_append = this.eval_libc_i32("O_APPEND");404 if flag & o_append == o_append {405 flag &= !o_append;406 options.append(true);407 }408 let o_trunc = this.eval_libc_i32("O_TRUNC");409 if flag & o_trunc == o_trunc {410 flag &= !o_trunc;411 options.truncate(true);412 }413 let o_creat = this.eval_libc_i32("O_CREAT");414 if flag & o_creat == o_creat {415 flag &= !o_creat;416 // Get the mode. On macOS, the argument type `mode_t` is actually `u16`, but417 // C integer promotion rules mean that on the ABI level, it gets passed as `u32`418 // (see https://github.com/rust-lang/rust/issues/71915).419 let [mode] = check_min_vararg_count("open(pathname, O_CREAT, ...)", varargs)?;420 let mode = this.read_scalar(mode)?.to_u32()?;421422 #[cfg(unix)]423 {424 // Support all modes on UNIX host425 use std::os::unix::fs::OpenOptionsExt;426 options.mode(mode);427 }428 #[cfg(not(unix))]429 {430 // Only support default mode for non-UNIX (i.e. Windows) host431 if mode != 0o666 {432 throw_unsup_format!(433 "non-default mode 0o{:o} is not supported on non-Unix hosts",434 mode435 );436 }437 }438439 let o_excl = this.eval_libc_i32("O_EXCL");440 if flag & o_excl == o_excl {441 flag &= !o_excl;442 options.create_new(true);443 } else {444 options.create(true);445 }446 }447 let o_cloexec = this.eval_libc_i32("O_CLOEXEC");448 if flag & o_cloexec == o_cloexec {449 flag &= !o_cloexec;450 // We do not need to do anything for this flag because `std` already sets it.451 // (Technically we do not support *not* setting this flag, but we ignore that.)452 }453 if this.tcx.sess.target.os == Os::Linux {454 let o_tmpfile = this.eval_libc_i32("O_TMPFILE");455 if flag & o_tmpfile == o_tmpfile {456 // if the flag contains `O_TMPFILE` then we return a graceful error457 return this.set_last_error_and_return_i32(LibcError("EOPNOTSUPP"));458 }459 }460461 let o_nofollow = this.eval_libc_i32("O_NOFOLLOW");462 if flag & o_nofollow == o_nofollow {463 flag &= !o_nofollow;464 #[cfg(unix)]465 {466 use std::os::unix::fs::OpenOptionsExt;467 options.custom_flags(libc::O_NOFOLLOW);468 }469 // Strictly speaking, this emulation is not equivalent to the O_NOFOLLOW flag behavior:470 // the path could change between us checking it here and the later call to `open`.471 // But it's good enough for Miri purposes.472 #[cfg(not(unix))]473 {474 // O_NOFOLLOW only fails when the trailing component is a symlink;475 // the entire rest of the path can still contain symlinks.476 if path.is_symlink() {477 return this.set_last_error_and_return_i32(LibcError("ELOOP"));478 }479 }480 }481482 // If `flag` has any bits left set, those are not supported.483 if flag != 0 {484 throw_unsup_format!("unsupported flags {:#x}", flag);485 }486487 // Reject if isolation is enabled.488 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {489 this.reject_in_isolation("`open`", reject_with)?;490 return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);491 }492493 let fd = options494 .open(path)495 .map(|file| this.machine.fds.insert_new(FileHandle { file, writable }));496497 interp_ok(Scalar::from_i32(this.try_unwrap_io_result(fd)?))498 }499500 fn lseek(501 &mut self,502 fd_num: i32,503 offset: i128,504 whence: i32,505 dest: &MPlaceTy<'tcx>,506 ) -> InterpResult<'tcx> {507 let this = self.eval_context_mut();508509 // Isolation check is done via `FileDescription` trait.510511 let seek_from = if whence == this.eval_libc_i32("SEEK_SET") {512 if offset < 0 {513 // Negative offsets return `EINVAL`.514 return this.set_last_error_and_return(LibcError("EINVAL"), dest);515 } else {516 SeekFrom::Start(u64::try_from(offset).unwrap())517 }518 } else if whence == this.eval_libc_i32("SEEK_CUR") {519 SeekFrom::Current(i64::try_from(offset).unwrap())520 } else if whence == this.eval_libc_i32("SEEK_END") {521 SeekFrom::End(i64::try_from(offset).unwrap())522 } else {523 return this.set_last_error_and_return(LibcError("EINVAL"), dest);524 };525526 let communicate = this.machine.communicate();527528 let Some(fd) = this.machine.fds.get(fd_num) else {529 return this.set_last_error_and_return(LibcError("EBADF"), dest);530 };531 let result = fd.seek(communicate, seek_from)?.map(|offset| i64::try_from(offset).unwrap());532 drop(fd);533534 let result = this.try_unwrap_io_result(result)?;535 this.write_int(result, dest)?;536 interp_ok(())537 }538539 fn unlink(&mut self, path_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {540 let this = self.eval_context_mut();541542 let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;543544 // Reject if isolation is enabled.545 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {546 this.reject_in_isolation("`unlink`", reject_with)?;547 return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);548 }549550 let result = remove_file(path).map(|_| 0);551 interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))552 }553554 fn symlink(555 &mut self,556 target_op: &OpTy<'tcx>,557 linkpath_op: &OpTy<'tcx>,558 ) -> InterpResult<'tcx, Scalar> {559 #[cfg(unix)]560 fn create_link(src: &Path, dst: &Path) -> std::io::Result<()> {561 std::os::unix::fs::symlink(src, dst)562 }563564 #[cfg(windows)]565 fn create_link(src: &Path, dst: &Path) -> std::io::Result<()> {566 use std::os::windows::fs;567 if src.is_dir() { fs::symlink_dir(src, dst) } else { fs::symlink_file(src, dst) }568 }569570 let this = self.eval_context_mut();571 let target = this.read_path_from_c_str(this.read_pointer(target_op)?)?;572 let linkpath = this.read_path_from_c_str(this.read_pointer(linkpath_op)?)?;573574 // Reject if isolation is enabled.575 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {576 this.reject_in_isolation("`symlink`", reject_with)?;577 return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);578 }579580 let result = create_link(&target, &linkpath).map(|_| 0);581 interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))582 }583584 fn stat(&mut self, path_op: &OpTy<'tcx>, buf_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {585 let this = self.eval_context_mut();586587 if !matches!(588 &this.tcx.sess.target.os,589 Os::MacOs | Os::FreeBsd | Os::Solaris | Os::Illumos | Os::Android | Os::Linux590 ) {591 panic!("`stat` should not be called on {}", this.tcx.sess.target.os);592 }593594 let path_scalar = this.read_pointer(path_op)?;595 let path = this.read_path_from_c_str(path_scalar)?.into_owned();596597 // Reject if isolation is enabled.598 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {599 this.reject_in_isolation("`stat`", reject_with)?;600 return this.set_last_error_and_return_i32(LibcError("EACCES"));601 }602603 // `stat` always follows symlinks.604 let metadata = match FileMetadata::from_path(this, &path, true)? {605 Ok(metadata) => metadata,606 Err(err) => return this.set_last_error_and_return_i32(err),607 };608609 interp_ok(Scalar::from_i32(this.write_stat_buf(metadata, buf_op)?))610 }611612 // `lstat` is used to get symlink metadata.613 fn lstat(&mut self, path_op: &OpTy<'tcx>, buf_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {614 let this = self.eval_context_mut();615616 if !matches!(617 &this.tcx.sess.target.os,618 Os::MacOs | Os::FreeBsd | Os::Solaris | Os::Illumos | Os::Android | Os::Linux619 ) {620 panic!("`lstat` should not be called on {}", this.tcx.sess.target.os);621 }622623 let path_scalar = this.read_pointer(path_op)?;624 let path = this.read_path_from_c_str(path_scalar)?.into_owned();625626 // Reject if isolation is enabled.627 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {628 this.reject_in_isolation("`lstat`", reject_with)?;629 return this.set_last_error_and_return_i32(LibcError("EACCES"));630 }631632 let metadata = match FileMetadata::from_path(this, &path, false)? {633 Ok(metadata) => metadata,634 Err(err) => return this.set_last_error_and_return_i32(err),635 };636637 interp_ok(Scalar::from_i32(this.write_stat_buf(metadata, buf_op)?))638 }639640 fn fstat(&mut self, fd_op: &OpTy<'tcx>, buf_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {641 let this = self.eval_context_mut();642643 if !matches!(644 &this.tcx.sess.target.os,645 Os::MacOs | Os::FreeBsd | Os::Solaris | Os::Illumos | Os::Linux | Os::Android646 ) {647 panic!("`fstat` should not be called on {}", this.tcx.sess.target.os);648 }649650 let fd = this.read_scalar(fd_op)?.to_i32()?;651652 // Reject if isolation is enabled.653 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {654 this.reject_in_isolation("`fstat`", reject_with)?;655 // Set error code as "EBADF" (bad fd)656 return this.set_last_error_and_return_i32(LibcError("EBADF"));657 }658659 let metadata = match FileMetadata::from_fd_num(this, fd)? {660 Ok(metadata) => metadata,661 Err(err) => return this.set_last_error_and_return_i32(err),662 };663 interp_ok(Scalar::from_i32(this.write_stat_buf(metadata, buf_op)?))664 }665666 fn linux_statx(667 &mut self,668 dirfd_op: &OpTy<'tcx>, // Should be an `int`669 pathname_op: &OpTy<'tcx>, // Should be a `const char *`670 flags_op: &OpTy<'tcx>, // Should be an `int`671 mask_op: &OpTy<'tcx>, // Should be an `unsigned int`672 statxbuf_op: &OpTy<'tcx>, // Should be a `struct statx *`673 ) -> InterpResult<'tcx, Scalar> {674 let this = self.eval_context_mut();675676 this.assert_target_os(Os::Linux, "statx");677678 let dirfd = this.read_scalar(dirfd_op)?.to_i32()?;679 let pathname_ptr = this.read_pointer(pathname_op)?;680 let flags = this.read_scalar(flags_op)?.to_i32()?;681 let _mask = this.read_scalar(mask_op)?.to_u32()?;682 let statxbuf_ptr = this.read_pointer(statxbuf_op)?;683684 // If the statxbuf or pathname pointers are null, the function fails with `EFAULT`.685 if this.ptr_is_null(statxbuf_ptr)? || this.ptr_is_null(pathname_ptr)? {686 return this.set_last_error_and_return_i32(LibcError("EFAULT"));687 }688689 let statxbuf = this.deref_pointer_as(statxbuf_op, this.libc_ty_layout("statx"))?;690691 let path = this.read_path_from_c_str(pathname_ptr)?.into_owned();692 // See <https://github.com/rust-lang/rust/pull/79196> for a discussion of argument sizes.693 let at_empty_path = this.eval_libc_i32("AT_EMPTY_PATH");694 let empty_path_flag = flags & at_empty_path == at_empty_path;695 // We only support:696 // * interpreting `path` as an absolute directory,697 // * interpreting `path` as a path relative to `dirfd` when the latter is `AT_FDCWD`, or698 // * interpreting `dirfd` as any file descriptor when `path` is empty and AT_EMPTY_PATH is699 // set.700 // Other behaviors cannot be tested from `libstd` and thus are not implemented. If you701 // found this error, please open an issue reporting it.702 if !(path.is_absolute()703 || dirfd == this.eval_libc_i32("AT_FDCWD")704 || (path.as_os_str().is_empty() && empty_path_flag))705 {706 throw_unsup_format!(707 "using statx is only supported with absolute paths, relative paths with the file \708 descriptor `AT_FDCWD`, and empty paths with the `AT_EMPTY_PATH` flag set and any \709 file descriptor"710 )711 }712713 // Reject if isolation is enabled.714 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {715 this.reject_in_isolation("`statx`", reject_with)?;716 let ecode = if path.is_absolute() || dirfd == this.eval_libc_i32("AT_FDCWD") {717 // since `path` is provided, either absolute or718 // relative to CWD, `EACCES` is the most relevant.719 LibcError("EACCES")720 } else {721 // `dirfd` is set to target file, and `path` is empty722 // (or we would have hit the `throw_unsup_format`723 // above). `EACCES` would violate the spec.724 assert!(empty_path_flag);725 LibcError("EBADF")726 };727 return this.set_last_error_and_return_i32(ecode);728 }729730 // If the `AT_SYMLINK_NOFOLLOW` flag is set, we query the file's metadata without following731 // symbolic links.732 let follow_symlink = flags & this.eval_libc_i32("AT_SYMLINK_NOFOLLOW") == 0;733734 // If the path is empty, and the AT_EMPTY_PATH flag is set, we query the open file735 // represented by dirfd, whether it's a directory or otherwise.736 let metadata = if path.as_os_str().is_empty() && empty_path_flag {737 FileMetadata::from_fd_num(this, dirfd)?738 } else {739 FileMetadata::from_path(this, &path, follow_symlink)?740 };741 let metadata = match metadata {742 Ok(metadata) => metadata,743 Err(err) => return this.set_last_error_and_return_i32(err),744 };745746 // The `_mask_op` parameter specifies the file information that the caller requested.747 // However, `statx` is allowed to return information that was not requested or to not748 // return information that was requested. This `mask` represents the information we can749 // actually provide for any target.750 let mut mask = this.eval_libc_u32("STATX_TYPE") | this.eval_libc_u32("STATX_SIZE");751752 // Check which pieces of metadata we acquired, and set the appropriate flags in the mask.753 if metadata.ino.is_some() {754 mask |= this.eval_libc_u32("STATX_INO");755 }756 if metadata.nlink.is_some() {757 mask |= this.eval_libc_u32("STATX_NLINK");758 }759 if metadata.uid.is_some() {760 mask |= this.eval_libc_u32("STATX_UID");761 }762 if metadata.gid.is_some() {763 mask |= this.eval_libc_u32("STATX_GID");764 }765 if metadata.blocks.is_some() {766 mask |= this.eval_libc_u32("STATX_BLOCKS");767 }768769 // `statx.stx_mode` is `__u16`. `libc::S_IF*` are of type `mode_t`, which varies in770 // width across targets (`u16` on macOS, `u32` on Linux). Read using `mode_t`'s size.771 let mode_t_size = this.libc_ty_layout("mode_t").size;772 let mode: u16 = metadata773 .mode774 .to_uint(mode_t_size)?775 .try_into()776 .unwrap_or_else(|_| bug!("libc contains bad value for constant"));777778 // We need to set the corresponding bits of `mask` if the access, creation and modification779 // times were available. Otherwise we let them be zero.780 let (access_sec, access_nsec) = metadata781 .accessed782 .map(|tup| {783 mask |= this.eval_libc_u32("STATX_ATIME");784 interp_ok(tup)785 })786 .unwrap_or_else(|| interp_ok((0, 0)))?;787788 let (created_sec, created_nsec) = metadata789 .created790 .map(|tup| {791 mask |= this.eval_libc_u32("STATX_BTIME");792 interp_ok(tup)793 })794 .unwrap_or_else(|| interp_ok((0, 0)))?;795796 let (modified_sec, modified_nsec) = metadata797 .modified798 .map(|tup| {799 mask |= this.eval_libc_u32("STATX_MTIME");800 interp_ok(tup)801 })802 .unwrap_or_else(|| interp_ok((0, 0)))?;803804 // Now we write everything to `statxbuf`. We write a zero for the unavailable fields.805 this.write_int_fields_named(806 &[807 ("stx_mask", mask.into()),808 ("stx_blksize", metadata.blksize.unwrap_or(0).into()),809 ("stx_attributes", 0),810 ("stx_nlink", metadata.nlink.unwrap_or(0).into()),811 ("stx_uid", metadata.uid.unwrap_or(0).into()),812 ("stx_gid", metadata.gid.unwrap_or(0).into()),813 ("stx_mode", mode.into()),814 ("stx_ino", metadata.ino.unwrap_or(0).into()),815 ("stx_size", metadata.size.into()),816 ("stx_blocks", metadata.blocks.unwrap_or(0).into()),817 ("stx_attributes_mask", 0),818 ("stx_rdev_major", 0),819 ("stx_rdev_minor", 0),820 ("stx_dev_major", 0),821 ("stx_dev_minor", 0),822 ],823 &statxbuf,824 )?;825 #[rustfmt::skip]826 this.write_int_fields_named(827 &[828 ("tv_sec", access_sec.into()),829 ("tv_nsec", access_nsec.into()),830 ],831 &this.project_field_named(&statxbuf, "stx_atime")?,832 )?;833 #[rustfmt::skip]834 this.write_int_fields_named(835 &[836 ("tv_sec", created_sec.into()),837 ("tv_nsec", created_nsec.into()),838 ],839 &this.project_field_named(&statxbuf, "stx_btime")?,840 )?;841 #[rustfmt::skip]842 this.write_int_fields_named(843 &[844 ("tv_sec", 0.into()),845 ("tv_nsec", 0.into()),846 ],847 &this.project_field_named(&statxbuf, "stx_ctime")?,848 )?;849 #[rustfmt::skip]850 this.write_int_fields_named(851 &[852 ("tv_sec", modified_sec.into()),853 ("tv_nsec", modified_nsec.into()),854 ],855 &this.project_field_named(&statxbuf, "stx_mtime")?,856 )?;857858 interp_ok(Scalar::from_i32(0))859 }860861 fn rename(862 &mut self,863 oldpath_op: &OpTy<'tcx>,864 newpath_op: &OpTy<'tcx>,865 ) -> InterpResult<'tcx, Scalar> {866 let this = self.eval_context_mut();867868 let oldpath_ptr = this.read_pointer(oldpath_op)?;869 let newpath_ptr = this.read_pointer(newpath_op)?;870871 if this.ptr_is_null(oldpath_ptr)? || this.ptr_is_null(newpath_ptr)? {872 return this.set_last_error_and_return_i32(LibcError("EFAULT"));873 }874875 let oldpath = this.read_path_from_c_str(oldpath_ptr)?;876 let newpath = this.read_path_from_c_str(newpath_ptr)?;877878 // Reject if isolation is enabled.879 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {880 this.reject_in_isolation("`rename`", reject_with)?;881 return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);882 }883884 let result = rename(oldpath, newpath).map(|_| 0);885886 interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))887 }888889 fn mkdir(&mut self, path_op: &OpTy<'tcx>, mode_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {890 let this = self.eval_context_mut();891892 #[cfg_attr(not(unix), allow(unused_variables))]893 let mode = if matches!(&this.tcx.sess.target.os, Os::MacOs | Os::FreeBsd) {894 u32::from(this.read_scalar(mode_op)?.to_u16()?)895 } else {896 this.read_scalar(mode_op)?.to_u32()?897 };898899 let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;900901 // Reject if isolation is enabled.902 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {903 this.reject_in_isolation("`mkdir`", reject_with)?;904 return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);905 }906907 #[cfg_attr(not(unix), allow(unused_mut))]908 let mut builder = DirBuilder::new();909910 // If the host supports it, forward on the mode of the directory911 // (i.e. permission bits and the sticky bit)912 #[cfg(unix)]913 {914 use std::os::unix::fs::DirBuilderExt;915 builder.mode(mode);916 }917918 let result = builder.create(path).map(|_| 0i32);919920 interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))921 }922923 fn rmdir(&mut self, path_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {924 let this = self.eval_context_mut();925926 let path = this.read_path_from_c_str(this.read_pointer(path_op)?)?;927928 // Reject if isolation is enabled.929 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {930 this.reject_in_isolation("`rmdir`", reject_with)?;931 return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);932 }933934 let result = remove_dir(path).map(|_| 0i32);935936 interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))937 }938939 fn opendir(&mut self, name_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {940 let this = self.eval_context_mut();941942 let name = this.read_path_from_c_str(this.read_pointer(name_op)?)?;943944 // Reject if isolation is enabled.945 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {946 this.reject_in_isolation("`opendir`", reject_with)?;947 this.set_last_error(LibcError("EACCES"))?;948 return interp_ok(Scalar::null_ptr(this));949 }950951 let result = read_dir(name);952953 match result {954 Ok(dir_iter) => {955 let id = this.machine.dirs.insert_new(dir_iter);956957 // The libc API for opendir says that this method returns a pointer to an opaque958 // structure, but we are returning an ID number. Thus, pass it as a scalar of959 // pointer width.960 interp_ok(Scalar::from_target_usize(id, this))961 }962 Err(e) => {963 this.set_last_error(e)?;964 interp_ok(Scalar::null_ptr(this))965 }966 }967 }968969 fn readdir(&mut self, dirp_op: &OpTy<'tcx>, dest: &MPlaceTy<'tcx>) -> InterpResult<'tcx> {970 let this = self.eval_context_mut();971972 if !matches!(973 &this.tcx.sess.target.os,974 Os::Linux | Os::Android | Os::Solaris | Os::Illumos | Os::FreeBsd975 ) {976 panic!("`readdir` should not be called on {}", this.tcx.sess.target.os);977 }978979 let dirp = this.read_target_usize(dirp_op)?;980981 // Reject if isolation is enabled.982 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {983 this.reject_in_isolation("`readdir`", reject_with)?;984 this.set_last_error(LibcError("EBADF"))?;985 this.write_null(dest)?;986 return interp_ok(());987 }988989 let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| {990 err_ub_format!("the DIR pointer passed to `readdir` did not come from opendir")991 })?;992993 let entry = match open_dir.next_host_entry() {994 Some(Ok(dir_entry)) => {995 let dir_entry = this.dir_entry_fields(dir_entry)?;996997 // Write the directory entry into a newly allocated buffer.998 // The name is written with write_bytes, while the rest of the999 // dirent64 (or dirent) struct is written using write_int_fields.10001001 // For reference:1002 // On Linux:1003 // pub struct dirent64 {1004 // pub d_ino: ino64_t,1005 // pub d_off: off64_t,1006 // pub d_reclen: c_ushort,1007 // pub d_type: c_uchar,1008 // pub d_name: [c_char; 256],1009 // }1010 //1011 // On Solaris:1012 // pub struct dirent {1013 // pub d_ino: ino64_t,1014 // pub d_off: off64_t,1015 // pub d_reclen: c_ushort,1016 // pub d_name: [c_char; 3],1017 // }1018 //1019 // On FreeBSD:1020 // pub struct dirent {1021 // pub d_fileno: uint32_t,1022 // pub d_reclen: uint16_t,1023 // pub d_type: uint8_t,1024 // pub d_namlen: uint8_t,1025 // pub d_name: [c_char; 256],1026 // }10271028 // We just use the pointee type here since determining the right pointee type1029 // independently is highly non-trivial: it depends on which exact alias of the1030 // function was invoked (e.g. `fstat` vs `fstat64`), and then on FreeBSD it also1031 // depends on the ABI level which can be different between the libc used by std and1032 // the libc used by everyone else.1033 let dirent_ty = dest.layout.ty.builtin_deref(true).unwrap();1034 let dirent_layout = this.layout_of(dirent_ty)?;1035 let fields = &dirent_layout.fields;1036 let d_name_offset = fields.offset(fields.count().strict_sub(1)).bytes();10371038 // Determine the size of the buffer we have to allocate.1039 let mut name = dir_entry.name; // not a Path as there are no separators!1040 name.push("\0"); // Add a NUL terminator1041 let name_bytes = name.as_encoded_bytes();1042 let name_len = u64::try_from(name_bytes.len()).unwrap();1043 let size = d_name_offset.strict_add(name_len);10441045 let entry = this.allocate_ptr(1046 Size::from_bytes(size),1047 dirent_layout.align.abi,1048 MiriMemoryKind::Runtime.into(),1049 AllocInit::Uninit,1050 )?;1051 let entry = this.ptr_to_mplace(entry.into(), dirent_layout);10521053 // Write the name.1054 // The name is not a normal field, we already computed the offset above.1055 let name_ptr = entry.ptr().wrapping_offset(Size::from_bytes(d_name_offset), this);1056 this.write_bytes_ptr(name_ptr, name_bytes.iter().copied())?;10571058 // Write common fields.1059 let ino_name =1060 if this.tcx.sess.target.os == Os::FreeBsd { "d_fileno" } else { "d_ino" };1061 this.write_int_fields_named(1062 &[(ino_name, dir_entry.ino.into()), ("d_reclen", size.into())],1063 &entry,1064 )?;10651066 // Write "optional" fields.1067 if let Some(d_off) = this.try_project_field_named(&entry, "d_off")? {1068 this.write_null(&d_off)?;1069 }1070 if let Some(d_namlen) = this.try_project_field_named(&entry, "d_namlen")? {1071 this.write_int(name_len.strict_sub(1), &d_namlen)?;1072 }1073 if let Some(d_type) = this.try_project_field_named(&entry, "d_type")? {1074 this.write_int(dir_entry.d_type, &d_type)?;1075 }10761077 Some(entry.ptr())1078 }1079 None => {1080 // end of stream: return NULL1081 None1082 }1083 Some(Err(e)) => {1084 this.set_last_error(e)?;1085 None1086 }1087 };10881089 let open_dir = this.machine.dirs.streams.get_mut(&dirp).unwrap();1090 let old_entry = std::mem::replace(&mut open_dir.entry, entry);1091 if let Some(old_entry) = old_entry {1092 this.deallocate_ptr(old_entry, None, MiriMemoryKind::Runtime.into())?;1093 }10941095 this.write_pointer(entry.unwrap_or_else(Pointer::null), dest)?;1096 interp_ok(())1097 }10981099 fn macos_readdir_r(1100 &mut self,1101 dirp_op: &OpTy<'tcx>,1102 entry_op: &OpTy<'tcx>,1103 result_op: &OpTy<'tcx>,1104 ) -> InterpResult<'tcx, Scalar> {1105 let this = self.eval_context_mut();11061107 this.assert_target_os(Os::MacOs, "readdir_r");11081109 let dirp = this.read_target_usize(dirp_op)?;1110 let result_place = this.deref_pointer_as(result_op, this.machine.layouts.mut_raw_ptr)?;11111112 // Reject if isolation is enabled.1113 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {1114 this.reject_in_isolation("`readdir_r`", reject_with)?;1115 // Return error code, do *not* set `errno`.1116 return interp_ok(this.eval_libc("EBADF"));1117 }11181119 let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| {1120 err_unsup_format!("the DIR pointer passed to readdir_r did not come from opendir")1121 })?;1122 interp_ok(match open_dir.next_host_entry() {1123 Some(Ok(dir_entry)) => {1124 let dir_entry = this.dir_entry_fields(dir_entry)?;1125 // Write into entry, write pointer to result, return 0 on success.1126 // The name is written with write_os_str_to_c_str, while the rest of the1127 // dirent struct is written using write_int_fields.11281129 // For reference, on macOS this looks like:1130 // pub struct dirent {1131 // pub d_ino: u64,1132 // pub d_seekoff: u64,1133 // pub d_reclen: u16,1134 // pub d_namlen: u16,1135 // pub d_type: u8,1136 // pub d_name: [c_char; 1024],1137 // }11381139 let entry_place = this.deref_pointer_as(entry_op, this.libc_ty_layout("dirent"))?;11401141 // Write the name.1142 let name_place = this.project_field_named(&entry_place, "d_name")?;1143 let (name_fits, file_name_buf_len) = this.write_os_str_to_c_str(1144 &dir_entry.name,1145 name_place.ptr(),1146 name_place.layout.size.bytes(),1147 )?;1148 if !name_fits {1149 throw_unsup_format!(1150 "a directory entry had a name too large to fit in libc::dirent"1151 );1152 }11531154 // Write the other fields.1155 this.write_int_fields_named(1156 &[1157 ("d_reclen", entry_place.layout.size.bytes().into()),1158 ("d_namlen", file_name_buf_len.strict_sub(1).into()),1159 ("d_type", dir_entry.d_type.into()),1160 ("d_ino", dir_entry.ino.into()),1161 ("d_seekoff", 0),1162 ],1163 &entry_place,1164 )?;1165 this.write_scalar(this.read_scalar(entry_op)?, &result_place)?;11661167 Scalar::from_i32(0)1168 }1169 None => {1170 // end of stream: return 0, assign *result=NULL1171 this.write_null(&result_place)?;1172 Scalar::from_i32(0)1173 }1174 Some(Err(e)) => {1175 // return positive error number on error (do *not* set last error)1176 this.io_error_to_errnum(e)?1177 }1178 })1179 }11801181 fn closedir(&mut self, dirp_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {1182 let this = self.eval_context_mut();11831184 let dirp = this.read_target_usize(dirp_op)?;11851186 // Reject if isolation is enabled.1187 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {1188 this.reject_in_isolation("`closedir`", reject_with)?;1189 return this.set_last_error_and_return_i32(LibcError("EBADF"));1190 }11911192 let Some(mut open_dir) = this.machine.dirs.streams.remove(&dirp) else {1193 return this.set_last_error_and_return_i32(LibcError("EBADF"));1194 };1195 if let Some(entry) = open_dir.entry.take() {1196 this.deallocate_ptr(entry, None, MiriMemoryKind::Runtime.into())?;1197 }1198 // We drop the `open_dir`, which will close the host dir handle.1199 drop(open_dir);12001201 interp_ok(Scalar::from_i32(0))1202 }12031204 fn ftruncate64(&mut self, fd_num: i32, length: i128) -> InterpResult<'tcx, Scalar> {1205 let this = self.eval_context_mut();12061207 // Reject if isolation is enabled.1208 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {1209 this.reject_in_isolation("`ftruncate64`", reject_with)?;1210 // Set error code as "EBADF" (bad fd)1211 return this.set_last_error_and_return_i32(LibcError("EBADF"));1212 }12131214 let Some(fd) = this.machine.fds.get(fd_num) else {1215 return this.set_last_error_and_return_i32(LibcError("EBADF"));1216 };12171218 let Some(file) = fd.downcast::<FileHandle>() else {1219 // The docs say that EINVAL is returned when the FD "does not reference a regular file1220 // or a POSIX shared memory object" (and we don't support shmem objects).1221 return interp_ok(this.eval_libc("EINVAL"));1222 };12231224 if file.writable {1225 if let Ok(length) = length.try_into() {1226 let result = file.file.set_len(length);1227 let result = this.try_unwrap_io_result(result.map(|_| 0i32))?;1228 interp_ok(Scalar::from_i32(result))1229 } else {1230 this.set_last_error_and_return_i32(LibcError("EINVAL"))1231 }1232 } else {1233 // The file is not writable1234 this.set_last_error_and_return_i32(LibcError("EINVAL"))1235 }1236 }12371238 /// NOTE: According to the man page of `possix_fallocate`, it returns the error code instead1239 /// of setting `errno`.1240 fn posix_fallocate(1241 &mut self,1242 fd_num: i32,1243 offset: i64,1244 len: i64,1245 ) -> InterpResult<'tcx, Scalar> {1246 let this = self.eval_context_mut();12471248 // Reject if isolation is enabled.1249 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {1250 this.reject_in_isolation("`posix_fallocate`", reject_with)?;1251 // Return error code "EBADF" (bad fd).1252 return interp_ok(this.eval_libc("EBADF"));1253 }12541255 // EINVAL is returned when: "offset was less than 0, or len was less than or equal to 0".1256 if offset < 0 || len <= 0 {1257 return interp_ok(this.eval_libc("EINVAL"));1258 }12591260 // Get the file handle.1261 let Some(fd) = this.machine.fds.get(fd_num) else {1262 return interp_ok(this.eval_libc("EBADF"));1263 };1264 let Some(file) = fd.downcast::<FileHandle>() else {1265 // Man page specifies to return ENODEV if `fd` is not a regular file.1266 return interp_ok(this.eval_libc("ENODEV"));1267 };12681269 if !file.writable {1270 // The file is not writable.1271 return interp_ok(this.eval_libc("EBADF"));1272 }12731274 let current_size = match file.file.metadata() {1275 Ok(metadata) => metadata.len(),1276 Err(err) => return this.io_error_to_errnum(err),1277 };1278 // Checked i64 addition, to ensure the result does not exceed the max file size.1279 let new_size = match offset.checked_add(len) {1280 // `new_size` is definitely non-negative, so we can cast to `u64`.1281 Some(new_size) => u64::try_from(new_size).unwrap(),1282 None => return interp_ok(this.eval_libc("EFBIG")), // new size too big1283 };1284 // If the size of the file is less than offset+size, then the file is increased to this size;1285 // otherwise the file size is left unchanged.1286 if current_size < new_size {1287 interp_ok(match file.file.set_len(new_size) {1288 Ok(()) => Scalar::from_i32(0),1289 Err(e) => this.io_error_to_errnum(e)?,1290 })1291 } else {1292 interp_ok(Scalar::from_i32(0))1293 }1294 }12951296 fn fsync(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {1297 // On macOS, `fsync` (unlike `fcntl(F_FULLFSYNC)`) does not wait for the1298 // underlying disk to finish writing. In the interest of host compatibility,1299 // we conservatively implement this with `sync_all`, which1300 // *does* wait for the disk.13011302 let this = self.eval_context_mut();13031304 let fd = this.read_scalar(fd_op)?.to_i32()?;13051306 // Reject if isolation is enabled.1307 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {1308 this.reject_in_isolation("`fsync`", reject_with)?;1309 // Set error code as "EBADF" (bad fd)1310 return this.set_last_error_and_return_i32(LibcError("EBADF"));1311 }13121313 self.ffullsync_fd(fd)1314 }13151316 fn ffullsync_fd(&mut self, fd_num: i32) -> InterpResult<'tcx, Scalar> {1317 let this = self.eval_context_mut();1318 let Some(fd) = this.machine.fds.get(fd_num) else {1319 return this.set_last_error_and_return_i32(LibcError("EBADF"));1320 };1321 // Only regular files support synchronization.1322 let file = fd.downcast::<FileHandle>().ok_or_else(|| {1323 err_unsup_format!("`fsync` is only supported on file-backed file descriptors")1324 })?;1325 let io_result = maybe_sync_file(&file.file, file.writable, File::sync_all);1326 interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))1327 }13281329 fn fdatasync(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {1330 let this = self.eval_context_mut();13311332 let fd = this.read_scalar(fd_op)?.to_i32()?;13331334 // Reject if isolation is enabled.1335 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {1336 this.reject_in_isolation("`fdatasync`", reject_with)?;1337 // Set error code as "EBADF" (bad fd)1338 return this.set_last_error_and_return_i32(LibcError("EBADF"));1339 }13401341 let Some(fd) = this.machine.fds.get(fd) else {1342 return this.set_last_error_and_return_i32(LibcError("EBADF"));1343 };1344 // Only regular files support synchronization.1345 let file = fd.downcast::<FileHandle>().ok_or_else(|| {1346 err_unsup_format!("`fdatasync` is only supported on file-backed file descriptors")1347 })?;1348 let io_result = maybe_sync_file(&file.file, file.writable, File::sync_data);1349 interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))1350 }13511352 fn sync_file_range(1353 &mut self,1354 fd_op: &OpTy<'tcx>,1355 offset_op: &OpTy<'tcx>,1356 nbytes_op: &OpTy<'tcx>,1357 flags_op: &OpTy<'tcx>,1358 ) -> InterpResult<'tcx, Scalar> {1359 let this = self.eval_context_mut();13601361 let fd = this.read_scalar(fd_op)?.to_i32()?;1362 let offset = this.read_scalar(offset_op)?.to_i64()?;1363 let nbytes = this.read_scalar(nbytes_op)?.to_i64()?;1364 let flags = this.read_scalar(flags_op)?.to_i32()?;13651366 if offset < 0 || nbytes < 0 {1367 return this.set_last_error_and_return_i32(LibcError("EINVAL"));1368 }1369 let allowed_flags = this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_BEFORE")1370 | this.eval_libc_i32("SYNC_FILE_RANGE_WRITE")1371 | this.eval_libc_i32("SYNC_FILE_RANGE_WAIT_AFTER");1372 if flags & allowed_flags != flags {1373 return this.set_last_error_and_return_i32(LibcError("EINVAL"));1374 }13751376 // Reject if isolation is enabled.1377 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {1378 this.reject_in_isolation("`sync_file_range`", reject_with)?;1379 // Set error code as "EBADF" (bad fd)1380 return this.set_last_error_and_return_i32(LibcError("EBADF"));1381 }13821383 let Some(fd) = this.machine.fds.get(fd) else {1384 return this.set_last_error_and_return_i32(LibcError("EBADF"));1385 };1386 // Only regular files support synchronization.1387 let file = fd.downcast::<FileHandle>().ok_or_else(|| {1388 err_unsup_format!("`sync_data_range` is only supported on file-backed file descriptors")1389 })?;1390 let io_result = maybe_sync_file(&file.file, file.writable, File::sync_data);1391 interp_ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))1392 }13931394 fn readlink(1395 &mut self,1396 pathname_op: &OpTy<'tcx>,1397 buf_op: &OpTy<'tcx>,1398 bufsize_op: &OpTy<'tcx>,1399 ) -> InterpResult<'tcx, i64> {1400 let this = self.eval_context_mut();14011402 let pathname = this.read_path_from_c_str(this.read_pointer(pathname_op)?)?;1403 let buf = this.read_pointer(buf_op)?;1404 let bufsize = this.read_target_usize(bufsize_op)?;14051406 // Reject if isolation is enabled.1407 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {1408 this.reject_in_isolation("`readlink`", reject_with)?;1409 this.set_last_error(LibcError("EACCES"))?;1410 return interp_ok(-1);1411 }14121413 let result = std::fs::read_link(pathname);1414 match result {1415 Ok(resolved) => {1416 // 'readlink' truncates the resolved path if the provided buffer is not large1417 // enough, and does *not* add a null terminator. That means we cannot use the usual1418 // `write_path_to_c_str` and have to re-implement parts of it ourselves.1419 let resolved = this.convert_path(1420 Cow::Borrowed(resolved.as_ref()),1421 crate::shims::os_str::PathConversion::HostToTarget,1422 );1423 let mut path_bytes = resolved.as_encoded_bytes();1424 let bufsize: usize = bufsize.try_into().unwrap();1425 if path_bytes.len() > bufsize {1426 path_bytes = &path_bytes[..bufsize]1427 }1428 this.write_bytes_ptr(buf, path_bytes.iter().copied())?;1429 interp_ok(path_bytes.len().try_into().unwrap())1430 }1431 Err(e) => {1432 this.set_last_error(e)?;1433 interp_ok(-1)1434 }1435 }1436 }14371438 fn isatty(&mut self, miri_fd: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {1439 let this = self.eval_context_mut();1440 // "returns 1 if fd is an open file descriptor referring to a terminal;1441 // otherwise 0 is returned, and errno is set to indicate the error"1442 let fd = this.read_scalar(miri_fd)?.to_i32()?;1443 let error = if let Some(fd) = this.machine.fds.get(fd) {1444 if fd.is_tty(this.machine.communicate()) {1445 return interp_ok(Scalar::from_i32(1));1446 } else {1447 LibcError("ENOTTY")1448 }1449 } else {1450 // FD does not exist1451 LibcError("EBADF")1452 };1453 this.set_last_error(error)?;1454 interp_ok(Scalar::from_i32(0))1455 }14561457 fn realpath(1458 &mut self,1459 path_op: &OpTy<'tcx>,1460 processed_path_op: &OpTy<'tcx>,1461 ) -> InterpResult<'tcx, Scalar> {1462 let this = self.eval_context_mut();1463 this.assert_target_os_is_unix("realpath");14641465 let pathname = this.read_path_from_c_str(this.read_pointer(path_op)?)?;1466 let processed_ptr = this.read_pointer(processed_path_op)?;14671468 // Reject if isolation is enabled.1469 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {1470 this.reject_in_isolation("`realpath`", reject_with)?;1471 this.set_last_error(LibcError("EACCES"))?;1472 return interp_ok(Scalar::from_target_usize(0, this));1473 }14741475 let result = std::fs::canonicalize(pathname);1476 match result {1477 Ok(resolved) => {1478 let path_max = this1479 .eval_libc_i32("PATH_MAX")1480 .try_into()1481 .expect("PATH_MAX does not fit in u64");1482 let dest = if this.ptr_is_null(processed_ptr)? {1483 // POSIX says behavior when passing a null pointer is implementation-defined,1484 // but GNU/linux, freebsd, netbsd, bionic/android, and macos all treat a null pointer1485 // similarly to:1486 //1487 // "If resolved_path is specified as NULL, then realpath() uses1488 // malloc(3) to allocate a buffer of up to PATH_MAX bytes to hold1489 // the resolved pathname, and returns a pointer to this buffer. The1490 // caller should deallocate this buffer using free(3)."1491 // <https://man7.org/linux/man-pages/man3/realpath.3.html>1492 this.alloc_path_as_c_str(&resolved, MiriMemoryKind::C.into())?1493 } else {1494 let (wrote_path, _) =1495 this.write_path_to_c_str(&resolved, processed_ptr, path_max)?;14961497 if !wrote_path {1498 // Note that we do not explicitly handle `FILENAME_MAX`1499 // (different from `PATH_MAX` above) as it is Linux-specific and1500 // seems like a bit of a mess anyway: <https://eklitzke.org/path-max-is-tricky>.1501 this.set_last_error(LibcError("ENAMETOOLONG"))?;1502 return interp_ok(Scalar::from_target_usize(0, this));1503 }1504 processed_ptr1505 };15061507 interp_ok(Scalar::from_maybe_pointer(dest, this))1508 }1509 Err(e) => {1510 this.set_last_error(e)?;1511 interp_ok(Scalar::from_target_usize(0, this))1512 }1513 }1514 }1515 fn mkstemp(&mut self, template_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {1516 use rand::seq::IndexedRandom;15171518 // POSIX defines the template string.1519 const TEMPFILE_TEMPLATE_STR: &str = "XXXXXX";15201521 let this = self.eval_context_mut();1522 this.assert_target_os_is_unix("mkstemp");15231524 // POSIX defines the maximum number of attempts before failure.1525 //1526 // `mkstemp()` relies on `tmpnam()` which in turn relies on `TMP_MAX`.1527 // POSIX says this about `TMP_MAX`:1528 // * Minimum number of unique filenames generated by `tmpnam()`.1529 // * Maximum number of times an application can call `tmpnam()` reliably.1530 // * The value of `TMP_MAX` is at least 25.1531 // * On XSI-conformant systems, the value of `TMP_MAX` is at least 10000.1532 // See <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/stdio.h.html>.1533 let max_attempts = this.eval_libc_u32("TMP_MAX");15341535 // Get the raw bytes from the template -- as a byte slice, this is a string in the target1536 // (and the target is unix, so a byte slice is the right representation).1537 let template_ptr = this.read_pointer(template_op)?;1538 let mut template = this.eval_context_ref().read_c_str(template_ptr)?.to_owned();1539 let template_bytes = template.as_mut_slice();15401541 // Reject if isolation is enabled.1542 if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {1543 this.reject_in_isolation("`mkstemp`", reject_with)?;1544 return this.set_last_error_and_return_i32(LibcError("EACCES"));1545 }15461547 // Get the bytes of the suffix we expect in _target_ encoding.1548 let suffix_bytes = TEMPFILE_TEMPLATE_STR.as_bytes();15491550 // At this point we have one `&[u8]` that represents the template and one `&[u8]`1551 // that represents the expected suffix.15521553 // Now we figure out the index of the slice we expect to contain the suffix.1554 let start_pos = template_bytes.len().saturating_sub(suffix_bytes.len());1555 let end_pos = template_bytes.len();1556 let last_six_char_bytes = &template_bytes[start_pos..end_pos];15571558 // If we don't find the suffix, it is an error.1559 if last_six_char_bytes != suffix_bytes {1560 return this.set_last_error_and_return_i32(LibcError("EINVAL"));1561 }15621563 // At this point we know we have 6 ASCII 'X' characters as a suffix.15641565 // From <https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/sysdeps/posix/tempname.c#L175>1566 const SUBSTITUTIONS: &[char; 62] = &[1567 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',1568 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',1569 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',1570 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',1571 ];15721573 // The file is opened with specific options, which Rust does not expose in a portable way.1574 // So we use specific APIs depending on the host OS.1575 let mut fopts = OpenOptions::new();1576 fopts.read(true).write(true).create_new(true);15771578 #[cfg(unix)]1579 {1580 use std::os::unix::fs::OpenOptionsExt;1581 // Do not allow others to read or modify this file.1582 fopts.mode(0o600);1583 fopts.custom_flags(libc::O_EXCL);1584 }1585 #[cfg(windows)]1586 {1587 use std::os::windows::fs::OpenOptionsExt;1588 // Do not allow others to read or modify this file.1589 fopts.share_mode(0);1590 }15911592 // If the generated file already exists, we will try again `max_attempts` many times.1593 for _ in 0..max_attempts {1594 let rng = this.machine.rng.get_mut();15951596 // Generate a random unique suffix.1597 let unique_suffix = SUBSTITUTIONS.choose_multiple(rng, 6).collect::<String>();15981599 // Replace the template string with the random string.1600 template_bytes[start_pos..end_pos].copy_from_slice(unique_suffix.as_bytes());16011602 // Write the modified template back to the passed in pointer to maintain POSIX semantics.1603 this.write_bytes_ptr(template_ptr, template_bytes.iter().copied())?;16041605 // To actually open the file, turn this into a host OsString.1606 let p = bytes_to_os_str(template_bytes)?.to_os_string();16071608 let possibly_unique = std::env::temp_dir().join::<PathBuf>(p.into());16091610 let file = fopts.open(possibly_unique);16111612 match file {1613 Ok(f) => {1614 let fd = this.machine.fds.insert_new(FileHandle { file: f, writable: true });1615 return interp_ok(Scalar::from_i32(fd));1616 }1617 Err(e) =>1618 match e.kind() {1619 // If the random file already exists, keep trying.1620 ErrorKind::AlreadyExists => continue,1621 // Any other errors are returned to the caller.1622 _ => {1623 // "On error, -1 is returned, and errno is set to1624 // indicate the error"1625 return this.set_last_error_and_return_i32(e);1626 }1627 },1628 }1629 }16301631 // We ran out of attempts to create the file, return an error.1632 this.set_last_error_and_return_i32(LibcError("EEXIST"))1633 }1634}16351636/// Extracts the number of seconds and nanoseconds elapsed between `time` and the unix epoch when1637/// `time` is Ok. Returns `None` if `time` is an error. Fails if `time` happens before the unix1638/// epoch.1639fn extract_sec_and_nsec<'tcx>(1640 time: std::io::Result<SystemTime>,1641) -> InterpResult<'tcx, Option<(u64, u32)>> {1642 match time.ok() {1643 Some(time) => {1644 let duration = system_time_to_duration(&time)?;1645 interp_ok(Some((duration.as_secs(), duration.subsec_nanos())))1646 }1647 None => interp_ok(None),1648 }1649}16501651fn file_type_to_mode_name(file_type: std::fs::FileType) -> &'static str {1652 #[cfg(unix)]1653 use std::os::unix::fs::FileTypeExt;16541655 if file_type.is_file() {1656 "S_IFREG"1657 } else if file_type.is_dir() {1658 "S_IFDIR"1659 } else if file_type.is_symlink() {1660 "S_IFLNK"1661 } else {1662 // Certain file types are only available when the host is a Unix system.1663 #[cfg(unix)]1664 {1665 if file_type.is_socket() {1666 return "S_IFSOCK";1667 } else if file_type.is_fifo() {1668 return "S_IFIFO";1669 } else if file_type.is_char_device() {1670 return "S_IFCHR";1671 } else if file_type.is_block_device() {1672 return "S_IFBLK";1673 }1674 }1675 "S_IFREG"1676 }1677}16781679/// Stores a file's metadata in order to avoid code duplication in the different metadata related1680/// shims.1681///1682/// Some fields are host/platform-specific. `None` means that Miri does not have a real value for1683/// this field, for example because the metadata is synthetic or because the host platform does not1684/// expose it. `statx` must only advertise the corresponding `STATX_*` bit when the field is `Some`;1685/// legacy `stat` writes zero for `None` to preserve the old fallback behavior.1686struct FileMetadata {1687 mode: Scalar,1688 size: u64,1689 created: Option<(u64, u32)>,1690 accessed: Option<(u64, u32)>,1691 modified: Option<(u64, u32)>,1692 dev: Option<u64>,1693 ino: Option<u64>,1694 nlink: Option<u64>,1695 uid: Option<u32>,1696 gid: Option<u32>,1697 blksize: Option<u64>,1698 blocks: Option<u64>,1699}17001701impl FileMetadata {1702 fn from_path<'tcx>(1703 ecx: &mut MiriInterpCx<'tcx>,1704 path: &Path,1705 follow_symlink: bool,1706 ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {1707 let metadata =1708 if follow_symlink { std::fs::metadata(path) } else { std::fs::symlink_metadata(path) };17091710 FileMetadata::from_meta(ecx, metadata)1711 }17121713 fn from_fd_num<'tcx>(1714 ecx: &mut MiriInterpCx<'tcx>,1715 fd_num: i32,1716 ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {1717 let Some(fd) = ecx.machine.fds.get(fd_num) else {1718 return interp_ok(Err(LibcError("EBADF")));1719 };1720 match fd.metadata()? {1721 Either::Left(host) => Self::from_meta(ecx, host),1722 Either::Right(name) => Self::synthetic(ecx, name),1723 }1724 }17251726 fn synthetic<'tcx>(1727 ecx: &mut MiriInterpCx<'tcx>,1728 mode_name: &str,1729 ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {1730 let mode = ecx.eval_libc(mode_name);1731 interp_ok(Ok(FileMetadata {1732 mode,1733 size: 0,1734 created: None,1735 accessed: None,1736 modified: None,1737 dev: None,1738 uid: None,1739 gid: None,1740 blksize: None,1741 blocks: None,1742 ino: None,1743 nlink: None,1744 }))1745 }17461747 fn from_meta<'tcx>(1748 ecx: &mut MiriInterpCx<'tcx>,1749 metadata: Result<std::fs::Metadata, std::io::Error>,1750 ) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {1751 let metadata = match metadata {1752 Ok(metadata) => metadata,1753 Err(e) => {1754 return interp_ok(Err(e.into()));1755 }1756 };17571758 let file_type = metadata.file_type();1759 let mode = ecx.eval_libc(file_type_to_mode_name(file_type));17601761 let size = metadata.len();17621763 let created = extract_sec_and_nsec(metadata.created())?;1764 let accessed = extract_sec_and_nsec(metadata.accessed())?;1765 let modified = extract_sec_and_nsec(metadata.modified())?;17661767 // FIXME: Provide more fields using platform specific methods.17681769 cfg_select! {1770 unix => {1771 use std::os::unix::fs::MetadataExt;1772 let dev = metadata.dev();1773 let ino = metadata.ino();1774 let nlink = metadata.nlink();1775 let uid = metadata.uid();1776 let gid = metadata.gid();1777 let blksize = metadata.blksize();1778 let blocks = metadata.blocks();17791780 interp_ok(Ok(FileMetadata {1781 mode,1782 size,1783 created,1784 accessed,1785 modified,1786 dev: Some(dev),1787 ino: Some(ino),1788 nlink: Some(nlink),1789 uid: Some(uid),1790 gid: Some(gid),1791 blksize: Some(blksize),1792 blocks: Some(blocks),1793 }))1794 }1795 _ => interp_ok(Ok(FileMetadata {1796 mode,1797 size,1798 created,1799 accessed,1800 modified,1801 dev: None,1802 ino: None,1803 nlink: None,1804 uid: None,1805 gid: None,1806 blksize: None,1807 blocks: None,1808 })),1809 }1810 }1811}