mirror of
https://github.com/rust-lang/rust.git
synced 2026-05-17 05:25:37 +03:00
Merge pull request #5014 from RalfJung/permissions
support chmod and fchmod on unix hosts
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user