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 "<socket>.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 <maxpain@linux.com>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
This commit is contained in:
Max Makarov
2026-06-03 13:11:56 +00:00
committed by Rob Bradford
parent 6219613bce
commit 0a08f6551a
2 changed files with 41 additions and 2 deletions

View File

@@ -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<File>,
) -> Result<HttpApiHandle> {
// 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<File> {
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<HttpApiHandle> {
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,
)
}

View File

@@ -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),