From 0a08f6551a9c4d0b113f0bfaea0d2a4faf80a56e Mon Sep 17 00:00:00 2001 From: Max Makarov Date: Wed, 3 Jun 2026 13:11:56 +0000 Subject: [PATCH] vmm: clean up a stale API socket under a lock before bind When Cloud Hypervisor crashed or was killed, the API socket file was left on disk, so the next start failed with EADDRINUSE ("Address already in use") and the VMM could not restart. This affects any environment where the socket directory survives across restarts (systemd services, Kubernetes emptyDir volumes, and so on). Before binding the path-based API socket, take an exclusive lock on a sidecar ".lock" file using the block crate's OFD-lock helper. Holding it proves no other instance is bound to this path, so a stale socket left by a crashed run can be removed safely and race-free. If the lock is already held, fail with a clear "API socket is already in use" error instead of clobbering the live instance. The lock is held for the process lifetime and released by the kernel on exit or crash. The fd-based (socket-activation) path is left unchanged. This implements the lock-file approach suggested by @DemiMarie. Fixes: #7784 Signed-off-by: Max Makarov Assisted-by: Claude:claude-opus-4-8 [Claude Code] --- vmm/src/api/http/mod.rs | 39 +++++++++++++++++++++++++++++++++++++-- vmm/src/lib.rs | 4 ++++ 2 files changed, 41 insertions(+), 2 deletions(-) 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),