From 71a36e0c695c0bcfa168b5c8029d29ca9159eb0e Mon Sep 17 00:00:00 2001 From: Philipp Schuster Date: Fri, 9 May 2025 13:57:40 +0200 Subject: [PATCH] block: add fcntl module for locking This is a prerequisite for the next steps. Signed-off-by: Philipp Schuster On-behalf-of: SAP philipp.schuster@sap.com --- block/src/fcntl.rs | 183 +++++++++++++++++++++++++++++++++++++++++++++ block/src/lib.rs | 1 + 2 files changed, 184 insertions(+) create mode 100644 block/src/fcntl.rs diff --git a/block/src/fcntl.rs b/block/src/fcntl.rs new file mode 100644 index 000000000..100b8e1f7 --- /dev/null +++ b/block/src/fcntl.rs @@ -0,0 +1,183 @@ +// Copyright © 2025 Cyberus Technology GmbH +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! Helpers for advisory file locking. +//! +//! Under the hood, the implementation uses OFD locks for the entire file, +//! as described in [[0]]. The advantage over `F_SETLKW` (currently used by +//! Rust std: `File::try_lock()`) is that only the very last `close()` on a +//! file descriptor releases the lock. This prevents mistakes and unexpected +//! behavior. +//! +//! [0]: . + +use std::error::Error; +use std::fmt::{Debug, Display, Formatter}; +use std::io; +use std::os::fd::{AsRawFd, RawFd}; + +/// Errors that can happen when working with file locks. +#[derive(Debug)] +pub enum LockError { + /// The file is already locked. + /// + /// A call to [`get_lock_state`] can help to identify the reason. + AlreadyLocked, + /// IO error. + Io(io::Error), +} + +impl Display for LockError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + LockError::AlreadyLocked => f.write_str("The file is already locked"), + LockError::Io(e) => { + write!(f, "{e}") + } + } + } +} + +impl Error for LockError {} + +/// Commands for use with [`fcntl`]. +#[allow(non_camel_case_types)] +enum FcntlArg<'a> { + /// Set an OFD lock from the given lock description. + F_OFD_SETLK(&'a libc::flock), + /// Get the first OFD lock for the given lock description. + F_OFD_GETLK(&'a mut libc::flock), +} + +/// Wrapper for [`libc::fcntl`] that properly sets the function arguments. +fn fcntl(fd: RawFd, arg: FcntlArg) -> libc::c_int { + // SAFETY: We use a valid FD. + unsafe { + match arg { + FcntlArg::F_OFD_SETLK(flock) => libc::fcntl(fd, libc::F_OFD_SETLK, flock), + FcntlArg::F_OFD_GETLK(flock) => libc::fcntl(fd, libc::F_OFD_GETLK, flock), + } + } +} + +/// Describes the type of lock you want to set. +#[derive(Clone, Copy, Debug)] +pub enum LockType { + /// Clear a lock. + Unlock, + /// Set a write lock (exclusive). + Write, + /// Set a read lock (shared). + Read, +} + +impl LockType { + pub const fn to_libc_val(self) -> libc::c_int { + match self { + Self::Unlock => libc::F_UNLCK as libc::c_int, + Self::Write => libc::F_WRLCK as libc::c_int, + Self::Read => libc::F_RDLCK as libc::c_int, + } + } +} + +/// Describes the current state of a lock. +#[derive(Debug)] +pub enum LockState { + /// No lock set. + Unlocked, + /// Locked for reading (non-exclusive). + SharedRead, + /// Locked for writing (exclusive mode). + ExclusiveWrite, +} + +impl LockState { + fn new(value: libc::c_int) -> Self { + const F_UNLCK: libc::c_int = libc::F_UNLCK as libc::c_int; + const F_WRLCK: libc::c_int = libc::F_WRLCK as libc::c_int; + const F_RDLCK: libc::c_int = libc::F_RDLCK as libc::c_int; + match value { + F_UNLCK => Self::Unlocked, + F_WRLCK => Self::ExclusiveWrite, + F_RDLCK => Self::SharedRead, + // This is so unlikely that we want to avoid the complexity of + // coping with this error case. Can only fail if either Linux + // is broken or memory is messed up. + other => panic!("Unexpected lock state: {other}"), + } + } +} + +/// Returns a [`struct@libc::flock`] structure for the whole file. +const fn get_flock(lock_type: LockType) -> libc::flock { + libc::flock { + l_type: lock_type.to_libc_val() as libc::c_short, + l_whence: libc::SEEK_SET as libc::c_short, + l_start: 0, + l_len: 0, /* EOF */ + l_pid: 0, /* filled by callee */ + } +} + +/// Tries to acquire a lock using [`fcntl`] with respect to the given +/// parameters. +/// +/// Please note that `fcntl()` OFD locks are **advisory locks**, which do not +/// prevent to `open()` a file if a lock is already placed. +/// +/// # Parameters +/// - `file`: The file to acquire a lock for [`LockType`]. The file's state will +/// be logically mutated, but not technically. +/// - `lock_type`: The [`LockType`] +pub fn try_acquire_lock(file: Fd, lock_type: LockType) -> Result<(), LockError> { + let flock = get_flock(lock_type); + + let res = fcntl(file.as_raw_fd(), FcntlArg::F_OFD_SETLK(&flock)); + match res { + 0 => Ok(()), + -1 => { + let io_error = io::Error::last_os_error(); + let errno = io_error.raw_os_error().unwrap(); + match errno { + // See man page for error code: + // + libc::EAGAIN | libc::EACCES => Err(LockError::AlreadyLocked), + _ => Err(LockError::Io(io_error)), + } + } + val => panic!("Unexpected return value from fcntl(): {val}"), + } +} + +/// Clears a lock. +/// +/// # Parameters +/// - `file`: The file to clear all locks for [`LockType`]. +pub fn clear_lock(file: Fd) -> Result<(), LockError> { + try_acquire_lock(file, LockType::Unlock) +} + +/// Returns the current lock state using [`fcntl`] with respect to the given +/// parameters. +/// +/// # Parameters +/// - `file`: The file for which to get the lock state. +pub fn get_lock_state(file: Fd) -> Result { + let mut flock = get_flock(LockType::Write); + let res = fcntl(file.as_raw_fd(), FcntlArg::F_OFD_GETLK(&mut flock)); + match res { + 0 => { + let state = flock.l_type as libc::c_int; + let state = LockState::new(state); + Ok(state) + } + -1 => { + let io_error = io::Error::last_os_error(); + Err(LockError::Io(io_error)) + } + val => panic!("Unexpected return value from fcntl(): {val}"), + } +} diff --git a/block/src/lib.rs b/block/src/lib.rs index 81036ab63..e831502be 100644 --- a/block/src/lib.rs +++ b/block/src/lib.rs @@ -12,6 +12,7 @@ extern crate log; pub mod async_io; +pub mod fcntl; pub mod fixed_vhd; #[cfg(feature = "io_uring")] /// Enabled with the `"io_uring"` feature