Merge pull request #5014 from RalfJung/permissions

support chmod and fchmod on unix hosts
This commit is contained in:
Ralf Jung
2026-05-09 20:42:55 +00:00
committed by GitHub
7 changed files with 226 additions and 40 deletions
+21 -1
View File
@@ -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),
+88 -39
View File
@@ -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;
@@ -236,15 +233,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()),
@@ -346,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> {}
@@ -547,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)?))
}
@@ -766,15 +769,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 +799,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()),
@@ -858,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::<FileHandle>() 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>,
@@ -881,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)?))
}
@@ -931,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)?))
}
@@ -948,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) => {
@@ -1684,7 +1719,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 +1764,9 @@ fn synthetic<'tcx>(
mode_name: &str,
) -> InterpResult<'tcx, Result<FileMetadata, IoError>> {
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 +1796,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 +1809,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 +1819,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 +1836,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,
}))
},
}
}
}
@@ -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::<libc::stat>::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);
}
@@ -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);
@@ -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);
}
@@ -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));
}
+1
View File
@@ -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);