diff --git a/vmm/src/api/http/mod.rs b/vmm/src/api/http/mod.rs index 8ca8dd1f6..9175cb8a0 100644 --- a/vmm/src/api/http/mod.rs +++ b/vmm/src/api/http/mod.rs @@ -5,15 +5,16 @@ use std::collections::BTreeMap; use std::error::Error; -use std::fs::File; +use std::fs::{File, OpenOptions}; use std::os::unix::io::{IntoRawFd, RawFd}; use std::os::unix::net::UnixListener; use std::panic::AssertUnwindSafe; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::LazyLock; use std::sync::mpsc::Sender; use std::thread; +use block::fcntl::{LockError, LockGranularity, LockType, try_acquire_lock}; use log::{error, info}; use micro_http::{ Body, HttpServer, MediaType, Method, Request, Response, ServerError, StatusCode, Version, @@ -342,6 +343,7 @@ fn start_http_thread( seccomp_action: &SeccompAction, exit_evt: EventFd, landlock_enable: bool, + api_socket_lock: Option, ) -> Result { // Retrieve seccomp filter for API thread let api_seccomp_filter = get_seccomp_filter(seccomp_action, Thread::HttpApi, None) @@ -357,6 +359,9 @@ fn start_http_thread( let thread = thread::Builder::new() .name("http-server".to_string()) .spawn(move || { + // Keep the API socket lock (if any) alive for the server thread. + let _api_socket_lock = api_socket_lock; + // Apply seccomp filter for API thread. if !api_seccomp_filter.is_empty() { apply_filter(&api_seccomp_filter) @@ -416,6 +421,28 @@ fn start_http_thread( Ok((thread, api_shutdown_fd)) } +/// Acquires an exclusive lock for the socket path. +/// +/// This prevents opening (and potentially deleting) a socket that is in active +/// use by another Cloud Hypervisor instance. +fn acquire_api_socket_lock(socket_path: &Path) -> Result { + let mut lock_path = socket_path.to_path_buf().into_os_string(); + lock_path.push(".lock"); + + let lock = OpenOptions::new() + .create(true) + .truncate(false) + .write(true) + .open(&lock_path) + .map_err(VmmError::CreateApiServerSocket)?; + + match try_acquire_lock(&lock, LockType::Write, LockGranularity::WholeFile) { + Ok(()) => Ok(lock), + Err(LockError::AlreadyLocked) => Err(VmmError::ApiSocketInUse(socket_path.to_path_buf())), + Err(LockError::Io(e)) => Err(VmmError::CreateApiServerSocket(e)), + } +} + pub fn start_http_path_thread( path: &str, api_notifier: EventFd, @@ -425,6 +452,12 @@ pub fn start_http_path_thread( landlock_enable: bool, ) -> Result { let socket_path = PathBuf::from(path); + + let lock = acquire_api_socket_lock(&socket_path)?; + // We hold the lock, so any socket at this path is stale from a crashed + // run: remove it before bind. Ignore errors (it is usually not present). + let _ = std::fs::remove_file(&socket_path); + let socket_fd = UnixListener::bind(socket_path).map_err(VmmError::CreateApiServerSocket)?; // SAFETY: Valid FD just opened let server = unsafe { HttpServer::new_from_fd(socket_fd.into_raw_fd()) } @@ -437,6 +470,7 @@ pub fn start_http_path_thread( seccomp_action, exit_evt, landlock_enable, + Some(lock), ) } @@ -457,6 +491,7 @@ pub fn start_http_fd_thread( seccomp_action, exit_evt, landlock_enable, + None, ) } diff --git a/vmm/src/lib.rs b/vmm/src/lib.rs index 1dde00537..378dafc4e 100644 --- a/vmm/src/lib.rs +++ b/vmm/src/lib.rs @@ -201,6 +201,10 @@ pub enum Error { #[error("Error creation API server's socket")] CreateApiServerSocket(#[source] io::Error), + /// The API server socket is already in use by another running instance + #[error("API socket {0:?} is already in use by another running instance")] + ApiSocketInUse(std::path::PathBuf), + #[cfg(feature = "guest_debug")] #[error("Failed to start the GDB thread")] GdbThreadSpawn(#[source] io::Error),