From ea6eaab341b66720f868f10b8905b497a1c40762 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Sat, 9 May 2026 16:42:37 +0200 Subject: [PATCH 1/2] fstat: expose permissions in 'mode' field --- src/tools/miri/src/shims/unix/fs.rs | 62 +++++++++---------- src/tools/miri/tests/pass-dep/libc/libc-fs.rs | 3 + .../pass-dep/libc/libc-fstat-non-file.rs | 2 + 3 files changed, 36 insertions(+), 31 deletions(-) diff --git a/src/tools/miri/src/shims/unix/fs.rs b/src/tools/miri/src/shims/unix/fs.rs index b1d39513065c..fcd0cb261742 100644 --- a/src/tools/miri/src/shims/unix/fs.rs +++ b/src/tools/miri/src/shims/unix/fs.rs @@ -236,15 +236,10 @@ fn write_stat_buf( // which can be different between the libc used by std and the libc used by everyone else. let buf = this.deref_pointer(buf_op)?; - // `libc::S_IF*` constants are of type `mode_t`, which varies in width across targets - // (`u16` on macOS, `u32` on Linux). Read the scalar using `mode_t`'s size on the target. - let mode_t_size = this.libc_ty_layout("mode_t").size; - let mode: u32 = metadata.mode.to_uint(mode_t_size)?.try_into().unwrap(); - this.write_int_fields_named( &[ ("st_dev", metadata.dev.unwrap_or(0).into()), - ("st_mode", mode.into()), + ("st_mode", metadata.mode.into()), ("st_nlink", metadata.nlink.unwrap_or(0).into()), ("st_ino", metadata.ino.unwrap_or(0).into()), ("st_uid", metadata.uid.unwrap_or(0).into()), @@ -766,15 +761,6 @@ fn linux_statx( mask |= this.eval_libc_u32("STATX_BLOCKS"); } - // `statx.stx_mode` is `__u16`. `libc::S_IF*` are of type `mode_t`, which varies in - // width across targets (`u16` on macOS, `u32` on Linux). Read using `mode_t`'s size. - let mode_t_size = this.libc_ty_layout("mode_t").size; - let mode: u16 = metadata - .mode - .to_uint(mode_t_size)? - .try_into() - .unwrap_or_else(|_| bug!("libc contains bad value for constant")); - // We need to set the corresponding bits of `mask` if the access, creation and modification // times were available. Otherwise we let them be zero. let (access_sec, access_nsec) = metadata @@ -805,12 +791,12 @@ fn linux_statx( this.write_int_fields_named( &[ ("stx_mask", mask.into()), + ("stx_mode", metadata.mode.into()), ("stx_blksize", metadata.blksize.unwrap_or(0).into()), ("stx_attributes", 0), ("stx_nlink", metadata.nlink.unwrap_or(0).into()), ("stx_uid", metadata.uid.unwrap_or(0).into()), ("stx_gid", metadata.gid.unwrap_or(0).into()), - ("stx_mode", mode.into()), ("stx_ino", metadata.ino.unwrap_or(0).into()), ("stx_size", metadata.size.into()), ("stx_blocks", metadata.blocks.unwrap_or(0).into()), @@ -1684,7 +1670,8 @@ fn file_type_to_mode_name(file_type: std::fs::FileType) -> &'static str { /// expose it. `statx` must only advertise the corresponding `STATX_*` bit when the field is `Some`; /// legacy `stat` writes zero for `None` to preserve the old fallback behavior. struct FileMetadata { - mode: Scalar, + /// This holds both the file type (dir, regular, symlink, ...) and permissions. + mode: u32, size: u64, created: Option<(u64, u32)>, accessed: Option<(u64, u32)>, @@ -1728,6 +1715,9 @@ fn synthetic<'tcx>( mode_name: &str, ) -> InterpResult<'tcx, Result> { let mode = ecx.eval_libc(mode_name); + let mode: u32 = mode.to_uint(ecx.libc_ty_layout("mode_t").size)?.try_into().unwrap(); + // We observed 0x777 on sockets and 0x600 on pipes... + let mode = mode | 0o666; interp_ok(Ok(FileMetadata { mode, size: 0, @@ -1757,6 +1747,7 @@ fn from_meta<'tcx>( let file_type = metadata.file_type(); let mode = ecx.eval_libc(file_type_to_mode_name(file_type)); + let mut mode = mode.to_uint(ecx.libc_ty_layout("mode_t").size)?.try_into().unwrap(); let size = metadata.len(); @@ -1769,6 +1760,8 @@ fn from_meta<'tcx>( cfg_select! { unix => { use std::os::unix::fs::MetadataExt; + use std::os::unix::fs::PermissionsExt; + let dev = metadata.dev(); let ino = metadata.ino(); let nlink = metadata.nlink(); @@ -1777,6 +1770,8 @@ fn from_meta<'tcx>( let blksize = metadata.blksize(); let blocks = metadata.blocks(); + mode |= metadata.permissions().mode(); + interp_ok(Ok(FileMetadata { mode, size, @@ -1792,20 +1787,25 @@ fn from_meta<'tcx>( blocks: Some(blocks), })) } - _ => interp_ok(Ok(FileMetadata { - mode, - size, - created, - accessed, - modified, - dev: None, - ino: None, - nlink: None, - uid: None, - gid: None, - blksize: None, - blocks: None, - })), + _ => { + // Emulate "everyone can read" or "everyone can read and write". + mode |= if metadata.permissions().readonly() { 0o111 } else { 0o333 }; + + interp_ok(Ok(FileMetadata { + mode, + size, + created, + accessed, + modified, + dev: None, + ino: None, + nlink: None, + uid: None, + gid: None, + blksize: None, + blocks: None, + })) + }, } } } diff --git a/src/tools/miri/tests/pass-dep/libc/libc-fs.rs b/src/tools/miri/tests/pass-dep/libc/libc-fs.rs index 23f2bf66f7d3..a388943f922f 100644 --- a/src/tools/miri/tests/pass-dep/libc/libc-fs.rs +++ b/src/tools/miri/tests/pass-dep/libc/libc-fs.rs @@ -605,6 +605,7 @@ fn test_fstat() { assert_eq!(stat.st_size, 5); assert_eq!(stat.st_mode & libc::S_IFMT, libc::S_IFREG); + assert_ne!(stat.st_mode & !libc::S_IFMT, 0, "some permission should be set"); // Check that all fields are initialized. check_stat_fields(stat); @@ -625,6 +626,7 @@ fn test_stat() { assert_eq!(stat.st_size, 5); assert_eq!(stat.st_mode & libc::S_IFMT, libc::S_IFREG); + assert_ne!(stat.st_mode & !libc::S_IFMT, 0, "some permission should be set"); // Check that all fields are initialized. check_stat_fields(stat); @@ -648,6 +650,7 @@ fn test_lstat() { let stat = unsafe { stat.assume_init_ref() }; assert_eq!(stat.st_mode & libc::S_IFMT, libc::S_IFLNK); + assert_ne!(stat.st_mode & !libc::S_IFMT, 0, "some permission should be set"); // Check that all fields are initialized. check_stat_fields(stat); diff --git a/src/tools/miri/tests/pass-dep/libc/libc-fstat-non-file.rs b/src/tools/miri/tests/pass-dep/libc/libc-fstat-non-file.rs index c29c8ceaa1df..cf848f190331 100644 --- a/src/tools/miri/tests/pass-dep/libc/libc-fstat-non-file.rs +++ b/src/tools/miri/tests/pass-dep/libc/libc-fstat-non-file.rs @@ -55,6 +55,7 @@ fn test_fstat_socketpair() { libc::S_IFSOCK, "socketpair should have S_IFSOCK mode" ); + assert_ne!(stat.st_mode & !libc::S_IFMT, 0, "socketpair should have permissions"); assert_eq!(stat.st_size, 0, "socketpair should have size 0"); assert_stat_fields_are_accessible(stat); } @@ -72,6 +73,7 @@ fn test_fstat_pipe() { let mut buf = MaybeUninit::uninit(); let stat = do_fstat(*fd, &mut buf); assert_eq!(stat.st_mode & libc::S_IFMT, libc::S_IFIFO, "pipe should have S_IFIFO mode"); + assert_ne!(stat.st_mode & !libc::S_IFMT, 0, "pipe should have permissions"); assert_eq!(stat.st_size, 0, "pipe should have size 0"); assert_stat_fields_are_accessible(stat); } From 9c426442177112e829e1c9fa17fac3244edd8427 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Sat, 9 May 2026 17:19:34 +0200 Subject: [PATCH 2/2] support chmod and fchmod on unix hosts --- .../miri/src/shims/unix/foreign_items.rs | 22 ++++++- src/tools/miri/src/shims/unix/fs.rs | 65 ++++++++++++++++--- .../pass-dep/libc/libc-fs-permissions.rs | 50 ++++++++++++++ .../miri/tests/pass/shims/fs-permissions.rs | 61 +++++++++++++++++ src/tools/miri/tests/pass/shims/fs.rs | 1 + 5 files changed, 190 insertions(+), 9 deletions(-) create mode 100644 src/tools/miri/tests/pass-dep/libc/libc-fs-permissions.rs create mode 100644 src/tools/miri/tests/pass/shims/fs-permissions.rs diff --git a/src/tools/miri/src/shims/unix/foreign_items.rs b/src/tools/miri/src/shims/unix/foreign_items.rs index 0766352bab20..748987360be9 100644 --- a/src/tools/miri/src/shims/unix/foreign_items.rs +++ b/src/tools/miri/src/shims/unix/foreign_items.rs @@ -362,6 +362,26 @@ fn emulate_foreign_item_inner( let result = this.stat(path, buf)?; this.write_scalar(result, dest)?; } + "chmod" => { + let [path, mode] = this.check_shim_sig( + shim_sig!(extern "C" fn(*const _, libc::mode_t) -> i32), + link_name, + abi, + args, + )?; + let result = this.chmod(path, mode)?; + this.write_scalar(result, dest)?; + } + "fchmod" => { + let [fd, mode] = this.check_shim_sig( + shim_sig!(extern "C" fn(i32, libc::mode_t) -> i32), + link_name, + abi, + args, + )?; + let result = this.fchmod(fd, mode)?; + this.write_scalar(result, dest)?; + } "rename" => { // FIXME: This does not have a direct test (#3179). let [oldpath, newpath] = this.check_shim_sig( @@ -536,7 +556,7 @@ fn emulate_foreign_item_inner( this.write_scalar(result, dest)?; } - // Unnamed sockets and pipes + // Sockets and pipes "socketpair" => { let [domain, type_, protocol, sv] = this.check_shim_sig( shim_sig!(extern "C" fn(i32, i32, i32, *mut _) -> i32), diff --git a/src/tools/miri/src/shims/unix/fs.rs b/src/tools/miri/src/shims/unix/fs.rs index fcd0cb261742..8df54616b562 100644 --- a/src/tools/miri/src/shims/unix/fs.rs +++ b/src/tools/miri/src/shims/unix/fs.rs @@ -2,10 +2,7 @@ use std::borrow::Cow; use std::ffi::OsString; -use std::fs::{ - self, DirBuilder, File, FileType, OpenOptions, TryLockError, read_dir, remove_dir, remove_file, - rename, -}; +use std::fs::{self, DirBuilder, File, FileType, OpenOptions, TryLockError}; use std::io::{self, ErrorKind, Read, Seek, SeekFrom, Write}; use std::path::{self, Path, PathBuf}; use std::time::SystemTime; @@ -341,6 +338,17 @@ fn dir_entry_fields( }, }) } + + #[cfg(unix)] + fn host_permissions_from_mode(&self, mode: u32) -> InterpResult<'tcx, fs::Permissions> { + use std::os::unix::fs::PermissionsExt; + interp_ok(fs::Permissions::from_mode(mode)) + } + + #[cfg(not(unix))] + fn host_permissions_from_mode(&self, _mode: u32) -> InterpResult<'tcx, fs::Permissions> { + throw_unsup_format!("setting file permissions is only supported on Unix hosts") + } } impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {} @@ -542,7 +550,7 @@ fn unlink(&mut self, path_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> { return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied); } - let result = remove_file(path).map(|_| 0); + let result = fs::remove_file(path).map(|_| 0); interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?)) } @@ -844,6 +852,47 @@ fn linux_statx( interp_ok(Scalar::from_i32(0)) } + fn chmod(&mut self, path_op: &OpTy<'tcx>, mode_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> { + let this = self.eval_context_mut(); + + let path_ptr = this.read_pointer(path_op)?; + let mode = this.read_scalar(mode_op)?.to_uint(this.libc_ty_layout("mode_t").size)?; + + if this.ptr_is_null(path_ptr)? { + return this.set_last_error_and_return_i32(LibcError("EFAULT")); + } + let path = this.read_path_from_c_str(path_ptr)?; + + let permissions = this.host_permissions_from_mode(mode.try_into().unwrap())?; + if let Err(err) = fs::set_permissions(path, permissions) { + return this.set_last_error_and_return_i32(IoError::HostError(err)); + } + + interp_ok(Scalar::from_i32(0)) + } + + fn fchmod(&mut self, fd_op: &OpTy<'tcx>, mode_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> { + let this = self.eval_context_mut(); + + let fd_num = this.read_scalar(fd_op)?.to_i32()?; + let mode = this.read_scalar(mode_op)?.to_uint(this.libc_ty_layout("mode_t").size)?; + + let Some(fd) = this.machine.fds.get(fd_num) else { + return this.set_last_error_and_return_i32(LibcError("EBADF")); + }; + let Some(file) = fd.downcast::() else { + // The docs don't talk about what happens for non-regular files... + throw_unsup_format!("`fchmod` is only supported on regular files") + }; + + let permissions = this.host_permissions_from_mode(mode.try_into().unwrap())?; + if let Err(err) = file.file.set_permissions(permissions) { + return this.set_last_error_and_return_i32(IoError::HostError(err)); + } + + interp_ok(Scalar::from_i32(0)) + } + fn rename( &mut self, oldpath_op: &OpTy<'tcx>, @@ -867,7 +916,7 @@ fn rename( return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied); } - let result = rename(oldpath, newpath).map(|_| 0); + let result = fs::rename(oldpath, newpath).map(|_| 0); interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?)) } @@ -917,7 +966,7 @@ fn rmdir(&mut self, path_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> { return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied); } - let result = remove_dir(path).map(|_| 0i32); + let result = fs::remove_dir(path).map(|_| 0i32); interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?)) } @@ -934,7 +983,7 @@ fn opendir(&mut self, name_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> { return interp_ok(Scalar::null_ptr(this)); } - let result = read_dir(name); + let result = fs::read_dir(name); match result { Ok(dir_iter) => { diff --git a/src/tools/miri/tests/pass-dep/libc/libc-fs-permissions.rs b/src/tools/miri/tests/pass-dep/libc/libc-fs-permissions.rs new file mode 100644 index 000000000000..f03e1ad8641d --- /dev/null +++ b/src/tools/miri/tests/pass-dep/libc/libc-fs-permissions.rs @@ -0,0 +1,50 @@ +//@ignore-target: windows # no libc +//@ignore-host: windows # needs unix PermissionExt +//@compile-flags: -Zmiri-disable-isolation + +#![feature(io_error_more)] +#![feature(io_error_uncategorized)] + +use std::ffi::{CStr, CString}; +use std::mem::MaybeUninit; +use std::os::unix::ffi::OsStrExt; + +#[path = "../../utils/mod.rs"] +mod utils; + +#[path = "../../utils/libc.rs"] +mod libc_utils; +use libc_utils::{errno_check, errno_result}; + +fn main() { + test_chmod(); + test_fchmod(); +} + +#[track_caller] +fn getmod(path: &CStr) -> u32 { + let mut stat = MaybeUninit::::uninit(); + unsafe { errno_check(libc::stat(path.as_ptr(), stat.as_mut_ptr())) }; + u32::from(unsafe { stat.assume_init_ref().st_mode & !libc::S_IFMT }) +} + +fn test_chmod() { + let path = utils::prepare_with_content("miri_test_libc_chmod.txt", b"abcdef"); + let c_path = CString::new(path.as_os_str().as_bytes()).expect("CString::new failed"); + + unsafe { errno_check(libc::chmod(c_path.as_ptr(), 0o777)) }; + assert_eq!(getmod(&c_path), 0o777); + unsafe { errno_check(libc::chmod(c_path.as_ptr(), 0o610)) }; + assert_eq!(getmod(&c_path), 0o610); +} + +fn test_fchmod() { + let path = utils::prepare_with_content("miri_test_libc_chmod.txt", b"abcdef"); + let c_path = CString::new(path.as_os_str().as_bytes()).expect("CString::new failed"); + + let fd = unsafe { errno_result(libc::open(c_path.as_ptr(), libc::O_RDONLY)).unwrap() }; + unsafe { errno_check(libc::fchmod(fd, 0o777)) }; + assert_eq!(getmod(&c_path), 0o777); + unsafe { errno_check(libc::fchmod(fd, 0o610)) }; + assert_eq!(getmod(&c_path), 0o610); +} diff --git a/src/tools/miri/tests/pass/shims/fs-permissions.rs b/src/tools/miri/tests/pass/shims/fs-permissions.rs new file mode 100644 index 000000000000..51b371e83ba5 --- /dev/null +++ b/src/tools/miri/tests/pass/shims/fs-permissions.rs @@ -0,0 +1,61 @@ +//@compile-flags: -Zmiri-disable-isolation +//@ignore-target: windows # shim not supported +//@ignore-host: windows # needs unix PermissionExt + +use std::fs::{self, File}; + +#[path = "../../utils/mod.rs"] +mod utils; + +macro_rules! check { + ($e:expr) => { + match $e { + Ok(t) => t, + Err(e) => panic!("{} failed with: {e}", stringify!($e)), + } + }; +} + +fn main() { + chmod_works(); + fchmod_works(); +} + +fn chmod_works() { + let tmpdir = utils::tmp(); + let file = tmpdir.join("miri_test_fs_set_permissions.txt"); + + check!(File::create(&file)); + let attr = check!(fs::metadata(&file)); + assert!(!attr.permissions().readonly()); + let mut p = attr.permissions(); + p.set_readonly(true); + check!(fs::set_permissions(&file, p.clone())); + let attr = check!(fs::metadata(&file)); + assert!(attr.permissions().readonly()); + + match fs::set_permissions(&tmpdir.join("foo"), p.clone()) { + Ok(..) => panic!("wanted an error"), + Err(..) => {} + } + + p.set_readonly(false); + check!(fs::set_permissions(&file, p)); +} + +fn fchmod_works() { + let tmpdir = utils::tmp(); + let path = tmpdir.join("miri_test_file_set_permissions.txt"); + + let file = check!(File::create(&path)); + let attr = check!(fs::metadata(&path)); + assert!(!attr.permissions().readonly()); + let mut p = attr.permissions(); + p.set_readonly(true); + check!(file.set_permissions(p.clone())); + let attr = check!(fs::metadata(&path)); + assert!(attr.permissions().readonly()); + + p.set_readonly(false); + check!(file.set_permissions(p)); +} diff --git a/src/tools/miri/tests/pass/shims/fs.rs b/src/tools/miri/tests/pass/shims/fs.rs index e6c15c81d9fd..e0da9f63876f 100644 --- a/src/tools/miri/tests/pass/shims/fs.rs +++ b/src/tools/miri/tests/pass/shims/fs.rs @@ -54,6 +54,7 @@ fn test_file() { // Test creating, writing and closing a file (closing is tested when `file` is dropped). let mut file = File::create(&path).unwrap(); + assert!(!file.metadata().unwrap().permissions().readonly()); // new file shouldn't be read-only // Writing 0 bytes should not change the file contents. file.write(&mut []).unwrap(); assert_eq!(file.metadata().unwrap().len(), 0);