diff --git a/Cargo.lock b/Cargo.lock index 417ecb09b3..eecb158d70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -563,6 +563,7 @@ dependencies = [ "smallvec", "tempfile", "ui_test", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 976bd08086..e12f3f9012 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,12 +9,12 @@ default-run = "miri" edition = "2021" [lib] -test = true # we have unit tests +test = true # we have unit tests doctest = false # but no doc tests [[bin]] name = "miri" -test = false # we have no unit tests +test = false # we have no unit tests doctest = false # and no doc tests [dependencies] @@ -42,6 +42,13 @@ libc = "0.2" libffi = "3.2.0" libloading = "0.8" +[target.'cfg(target_family = "windows")'.dependencies] +windows-sys = { version = "0.52", features = [ + "Win32_Foundation", + "Win32_System_IO", + "Win32_Storage_FileSystem", +] } + [dev-dependencies] colored = "2" ui_test = "0.21.1" diff --git a/src/shims/unix/fd.rs b/src/shims/unix/fd.rs index 8fb046b5e6..b06129b2c3 100644 --- a/src/shims/unix/fd.rs +++ b/src/shims/unix/fd.rs @@ -53,6 +53,14 @@ pub trait FileDescription: std::fmt::Debug + Any { throw_unsup_format!("cannot close {}", self.name()); } + fn flock<'tcx>( + &self, + _communicate_allowed: bool, + _op: FlockOp, + ) -> InterpResult<'tcx, io::Result<()>> { + throw_unsup_format!("cannot flock {}", self.name()); + } + fn is_tty(&self, _communicate_allowed: bool) -> bool { // Most FDs are not tty's and the consequence of a wrong `false` are minor, // so we use a default impl here. @@ -299,6 +307,40 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { Ok(new_fd) } + fn flock(&mut self, fd: i32, op: i32) -> InterpResult<'tcx, Scalar> { + let this = self.eval_context_mut(); + let Some(file_descriptor) = this.machine.fds.get(fd) else { + return Ok(Scalar::from_i32(this.fd_not_found()?)); + }; + + // We need to check that there aren't unsupported options in `op`. + let lock_sh = this.eval_libc_i32("LOCK_SH"); + let lock_ex = this.eval_libc_i32("LOCK_EX"); + let lock_nb = this.eval_libc_i32("LOCK_NB"); + let lock_un = this.eval_libc_i32("LOCK_UN"); + + use FlockOp::*; + let parsed_op = if op == lock_sh { + SharedLock { nonblocking: false } + } else if op == lock_sh | lock_nb { + SharedLock { nonblocking: true } + } else if op == lock_ex { + ExclusiveLock { nonblocking: false } + } else if op == lock_ex | lock_nb { + ExclusiveLock { nonblocking: true } + } else if op == lock_un { + Unlock + } else { + throw_unsup_format!("unsupported flags {:#x}", op); + }; + + let result = file_descriptor.flock(this.machine.communicate(), parsed_op)?; + drop(file_descriptor); + // return `0` if flock is successful + let result = result.map(|()| 0i32); + Ok(Scalar::from_i32(this.try_unwrap_io_result(result)?)) + } + fn fcntl(&mut self, args: &[OpTy<'tcx>]) -> InterpResult<'tcx, i32> { let this = self.eval_context_mut(); @@ -464,3 +506,9 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { this.try_unwrap_io_result(result) } } + +pub(crate) enum FlockOp { + SharedLock { nonblocking: bool }, + ExclusiveLock { nonblocking: bool }, + Unlock, +} diff --git a/src/shims/unix/foreign_items.rs b/src/shims/unix/foreign_items.rs index 3a18d62203..095fb8608e 100644 --- a/src/shims/unix/foreign_items.rs +++ b/src/shims/unix/foreign_items.rs @@ -130,6 +130,13 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { let result = this.dup2(old_fd, new_fd)?; this.write_scalar(Scalar::from_i32(result), dest)?; } + "flock" => { + let [fd, op] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?; + let fd = this.read_scalar(fd)?.to_i32()?; + let op = this.read_scalar(op)?.to_i32()?; + let result = this.flock(fd, op)?; + this.write_scalar(result, dest)?; + } // File and file system access "open" | "open64" => { diff --git a/src/shims/unix/fs.rs b/src/shims/unix/fs.rs index e34aa5c09d..d1b07f8807 100644 --- a/src/shims/unix/fs.rs +++ b/src/shims/unix/fs.rs @@ -16,7 +16,7 @@ use crate::shims::unix::*; use crate::*; use shims::time::system_time_to_duration; -use self::fd::FileDescriptor; +use self::fd::{FileDescriptor, FlockOp}; #[derive(Debug)] struct FileHandle { @@ -81,6 +81,89 @@ impl FileDescription for FileHandle { } } + fn flock<'tcx>( + &self, + communicate_allowed: bool, + op: FlockOp, + ) -> InterpResult<'tcx, io::Result<()>> { + assert!(communicate_allowed, "isolation should have prevented even opening a file"); + #[cfg(target_family = "unix")] + { + use std::os::fd::AsRawFd; + + // Transform `op` into host's flags + use FlockOp::*; + let host_op = match op { + SharedLock { nonblocking: false } => libc::LOCK_SH, + SharedLock { nonblocking: true } => libc::LOCK_SH | libc::LOCK_NB, + ExclusiveLock { nonblocking: false } => libc::LOCK_EX, + ExclusiveLock { nonblocking: true } => libc::LOCK_EX | libc::LOCK_NB, + Unlock => libc::LOCK_UN, + }; + + let fd = self.file.as_raw_fd(); + let ret = unsafe { libc::flock(fd, host_op) }; + let res = match ret { + 0 => Ok(()), + -1 => Err(io::Error::last_os_error()), + ret => panic!("Unexpected return value from flock: {ret}"), + }; + Ok(res) + } + + #[cfg(target_family = "windows")] + { + use std::os::windows::io::AsRawHandle; + use windows_sys::Win32::{ + Foundation::{ERROR_IO_PENDING, ERROR_LOCK_VIOLATION, FALSE, HANDLE, TRUE}, + Storage::FileSystem::{ + LockFileEx, UnlockFile, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY, + }, + }; + let handle = self.file.as_raw_handle() as HANDLE; + + use FlockOp::*; + let mut lock_nb = false; + let ret = match op { + SharedLock { nonblocking } | ExclusiveLock { nonblocking } => { + lock_nb = nonblocking; + let mut flags = 0; + if nonblocking { + flags |= LOCKFILE_FAIL_IMMEDIATELY; + } + if let ExclusiveLock { .. } = op { + flags |= LOCKFILE_EXCLUSIVE_LOCK; + } + unsafe { LockFileEx(handle, flags, 0, !0, !0, &mut std::mem::zeroed()) } + } + Unlock => unsafe { UnlockFile(handle, 0, 0, !0, !0) }, + }; + + let res = match ret { + TRUE => Ok(()), + FALSE => { + let mut err = io::Error::last_os_error(); + let code: u32 = err.raw_os_error().unwrap().try_into().unwrap(); + // Replace error with a custom WouldBlock error, which later will be mapped + // in the `helpers` module + if lock_nb && matches!(code, ERROR_IO_PENDING | ERROR_LOCK_VIOLATION) { + let desc = format!("LockFileEx wouldblock error: {err}"); + err = io::Error::new(io::ErrorKind::WouldBlock, desc); + } + Err(err) + } + _ => panic!("Unexpected return value: {ret}"), + }; + Ok(res) + } + + #[cfg(not(any(target_family = "unix", target_family = "windows")))] + { + let _ = op; + throw_unsup_format!("flock is supported only on UNIX and Windows hosts"); + } + } + fn is_tty(&self, communicate_allowed: bool) -> bool { communicate_allowed && self.file.is_terminal() } diff --git a/tests/pass-dep/libc/libc-fs-flock.rs b/tests/pass-dep/libc/libc-fs-flock.rs new file mode 100644 index 0000000000..c1b3b8f575 --- /dev/null +++ b/tests/pass-dep/libc/libc-fs-flock.rs @@ -0,0 +1,71 @@ +// Flock tests are separate since they don't in general work on a Windows host. +//@ignore-target-windows: File handling is not implemented yet +//@compile-flags: -Zmiri-disable-isolation + +use std::{fs::File, io::Error, os::fd::AsRawFd}; + +#[path = "../../utils/mod.rs"] +mod utils; + +fn main() { + let bytes = b"Hello, World!\n"; + let path = utils::prepare_with_content("miri_test_fs_shared_lock.txt", bytes); + + let files: Vec = (0..3).map(|_| File::open(&path).unwrap()).collect(); + + // Test that we can apply many shared locks + for file in files.iter() { + let fd = file.as_raw_fd(); + let ret = unsafe { libc::flock(fd, libc::LOCK_SH) }; + if ret != 0 { + panic!("flock error: {}", Error::last_os_error()); + } + } + + // Test that shared lock prevents exclusive lock + { + let fd = files[0].as_raw_fd(); + let ret = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) }; + assert_eq!(ret, -1); + let err = Error::last_os_error().raw_os_error().unwrap(); + assert_eq!(err, libc::EWOULDBLOCK); + } + + // Unlock shared lock + for file in files.iter() { + let fd = file.as_raw_fd(); + let ret = unsafe { libc::flock(fd, libc::LOCK_UN) }; + if ret != 0 { + panic!("flock error: {}", Error::last_os_error()); + } + } + + // Take exclusive lock + { + let fd = files[0].as_raw_fd(); + let ret = unsafe { libc::flock(fd, libc::LOCK_EX) }; + assert_eq!(ret, 0); + } + + // Test that shared lock prevents exclusive and shared locks + { + let fd = files[1].as_raw_fd(); + let ret = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) }; + assert_eq!(ret, -1); + let err = Error::last_os_error().raw_os_error().unwrap(); + assert_eq!(err, libc::EWOULDBLOCK); + + let fd = files[2].as_raw_fd(); + let ret = unsafe { libc::flock(fd, libc::LOCK_SH | libc::LOCK_NB) }; + assert_eq!(ret, -1); + let err = Error::last_os_error().raw_os_error().unwrap(); + assert_eq!(err, libc::EWOULDBLOCK); + } + + // Unlock exclusive lock + { + let fd = files[0].as_raw_fd(); + let ret = unsafe { libc::flock(fd, libc::LOCK_UN) }; + assert_eq!(ret, 0); + } +}