mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
Use CamelCase as per the existing definitions (which allows the removal of the serde transformation) Signed-off-by: Rob Bradford <rbradford@meta.com>
255 lines
8.5 KiB
Rust
255 lines
8.5 KiB
Rust
// 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]: <https://apenwarr.ca/log/20101213>.
|
|
|
|
use std::fmt::Debug;
|
|
use std::io;
|
|
use std::os::fd::{AsRawFd, RawFd};
|
|
use std::str::FromStr;
|
|
|
|
use thiserror::Error;
|
|
|
|
/// Errors that can happen when working with file locks.
|
|
#[derive(Error, Debug)]
|
|
pub enum LockError {
|
|
/// The file is already locked.
|
|
///
|
|
/// A call to [`get_lock_state`] can help to identify the reason.
|
|
#[error("The file is already locked")]
|
|
AlreadyLocked,
|
|
/// IO error.
|
|
#[error("The lock state could not be checked or set")]
|
|
Io(#[source] io::Error),
|
|
}
|
|
|
|
/// 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}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The granularity of the advisory lock.
|
|
///
|
|
/// The granularity has significant implications in typical cloud deployments
|
|
/// with network storage. The Linux kernel will sync advisory locks to network
|
|
/// file systems, but these backends may have different policies and handle
|
|
/// locks differently. For example, Netapp speaks a NFS API but will treat
|
|
/// advisory OFD locks for the whole file as mandatory locks, whereas byte-range
|
|
/// locks for the whole file will remain advisory [0].
|
|
///
|
|
/// As it is a valid use case to prevent multiple CHV instances from accessing
|
|
/// the same disk but disk management software (e.g., Cinder in OpenStack)
|
|
/// should be able to snapshot disks while VMs are running, we need special
|
|
/// control over the lock granularity. Therefore, it is a valid use case to lock
|
|
/// the whole byte range of a disk image without technically locking the whole
|
|
/// file - to get the best of both worlds.
|
|
///
|
|
/// [0] https://kb.netapp.com/on-prem/ontap/da/NAS/NAS-KBs/How_is_Mandatory_Locking_supported_for_NFSv4_on_ONTAP_9
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub enum LockGranularity {
|
|
WholeFile,
|
|
ByteRange(u64 /* from, inclusive */, u64 /* len */),
|
|
}
|
|
|
|
impl LockGranularity {
|
|
const fn l_start(self) -> u64 {
|
|
match self {
|
|
LockGranularity::WholeFile => 0,
|
|
LockGranularity::ByteRange(start, _) => start,
|
|
}
|
|
}
|
|
|
|
const fn l_len(self) -> u64 {
|
|
match self {
|
|
LockGranularity::WholeFile => 0, /* EOF */
|
|
LockGranularity::ByteRange(_, len) => len,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// User-facing choice for the lock granularity.
|
|
///
|
|
/// This allows external management software to create snapshots of the disk
|
|
/// image. Without a byte-range lock, some NFS implementations may treat the
|
|
/// entire file as exclusively locked and prevent such operations (e.g. NetApp).
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
|
pub enum LockGranularityChoice {
|
|
/// Byte-range lock covering [0, size).
|
|
#[default]
|
|
ByteRange,
|
|
/// Whole-file lock (l_start=0, l_len=0) - original OFD whole-file lock behavior.
|
|
Full,
|
|
}
|
|
|
|
/// Error returned when parsing a [`LockGranularityChoice`] from a string.
|
|
#[derive(Error, Debug)]
|
|
#[error("Invalid lock granularity value: {0}, expected 'byte-range' or 'full'")]
|
|
pub struct LockGranularityParseError(String);
|
|
|
|
impl FromStr for LockGranularityChoice {
|
|
type Err = LockGranularityParseError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s {
|
|
"byte-range" => Ok(LockGranularityChoice::ByteRange),
|
|
"full" => Ok(LockGranularityChoice::Full),
|
|
_ => Err(LockGranularityParseError(s.to_owned())),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns a [`struct@libc::flock`] structure for the whole file.
|
|
const fn get_flock(lock_type: LockType, granularity: LockGranularity) -> 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: granularity.l_start() as libc::c_long,
|
|
l_len: granularity.l_len() as libc::c_long,
|
|
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`]
|
|
/// - `granularity`: The [`LockGranularity`].
|
|
pub fn try_acquire_lock<Fd: AsRawFd>(
|
|
file: &Fd,
|
|
lock_type: LockType,
|
|
granularity: LockGranularity,
|
|
) -> Result<(), LockError> {
|
|
let flock = get_flock(lock_type, granularity);
|
|
|
|
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:
|
|
// <https://man7.org/linux/man-pages/man2/fcntl.2.html>
|
|
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`].
|
|
/// - `granularity`: The [`LockGranularity`].
|
|
pub fn clear_lock<Fd: AsRawFd>(file: &Fd, granularity: LockGranularity) -> Result<(), LockError> {
|
|
try_acquire_lock(file, LockType::Unlock, granularity)
|
|
}
|
|
|
|
/// Returns the current lock state using [`fcntl`] with respect to the given
|
|
/// parameters.
|
|
///
|
|
/// # Parameters
|
|
/// - `file`: The file for which to get the lock state.
|
|
/// - `granularity`: The [`LockGranularity`].
|
|
pub fn get_lock_state<Fd: AsRawFd>(
|
|
file: &Fd,
|
|
granularity: LockGranularity,
|
|
) -> Result<LockState, LockError> {
|
|
let mut flock = get_flock(LockType::Write, granularity);
|
|
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}"),
|
|
}
|
|
}
|