use std::collections::HashMap;
use std::num::NonZeroU64;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, RwLock};
use std::time::Duration;
use chrono::{DateTime, Utc};
use fc_sdk::VmBuilder;
use fc_sdk::types::{BootSource, Drive, NetworkInterface, Vsock};
use nix::unistd::{Gid, Uid, chown};
use tokio::sync::broadcast;
use tracing::{debug, error, info, warn};
use uuid::Uuid;
use crate::boot_proto::KernelIpParam;
use crate::config::VmmConfig;
use crate::error::{Result, VmmError};
use crate::network::{NetworkAllocation, NetworkManager};
use crate::snapshot::SnapshotCatalog;
use crate::snapshot_cow::{CowHandle, CowManager};
use crate::spawn::{spawn_direct, spawn_jailer};
use crate::vsock::{self, ExecInputMsg, OutputChunk, StartCommand};
mod boot;
mod checkpoint;
mod cleanup;
mod lifecycle;
mod reconcile;
mod types;
mod workload;
pub use types::{
CheckpointInfo, CheckpointSummary, RestoreSandboxSpec, SandboxEvent, SandboxId, SandboxInfo,
SandboxInstance, SandboxMountSpec, SandboxNetworkInfo, SandboxNetworkSpec, SandboxSpec,
SandboxState, SandboxSummary,
};
const EVENT_CHANNEL_CAPACITY: usize = 256;
pub(crate) type InstanceMap = Arc<RwLock<HashMap<SandboxId, Arc<Mutex<SandboxInstance>>>>>;
pub struct SandboxManager {
instances: Arc<RwLock<HashMap<SandboxId, Arc<Mutex<SandboxInstance>>>>>,
network: Arc<NetworkManager>,
snapshots: Arc<SnapshotCatalog>,
config: Arc<VmmConfig>,
events_tx: broadcast::Sender<SandboxEvent>,
cow_manager: Arc<CowManager>,
reconcile_done: tokio::sync::watch::Receiver<bool>,
}
impl SandboxManager {
pub fn new(config: VmmConfig) -> Result<Self> {
let network = Arc::new(NetworkManager::new(
&config.network.cidr,
&config.network.gateway,
config.network.dns.clone(),
)?);
let snapshots = Arc::new(SnapshotCatalog::new(&config.firecracker.data_dir));
let (events_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
let cow_manager = Arc::new(
CowManager::new(&config.firecracker.data_dir)
.map_err(|e| VmmError::Config(format!("CowManager init: {e}")))?,
);
if let Some(ref jc) = config.firecracker.jailer {
let base = jc.chroot_base_dir.as_deref().unwrap_or("/srv/jailer");
std::fs::create_dir_all(base).map_err(VmmError::Io)?;
}
let config = Arc::new(config);
let (reconcile_tx, reconcile_done) = tokio::sync::watch::channel(false);
if tokio::runtime::Handle::try_current().is_ok() {
let config = Arc::clone(&config);
let network = Arc::clone(&network);
let cow_manager = Arc::clone(&cow_manager);
tokio::spawn(async move {
reconcile::sweep_orphans(&config, &network, &cow_manager).await;
let _ = reconcile_tx.send(true);
});
} else {
let _ = reconcile_tx.send(true);
}
Ok(Self {
instances: Arc::new(RwLock::new(HashMap::new())),
network,
snapshots,
config,
events_tx,
cow_manager,
reconcile_done,
})
}
pub(super) async fn await_reconcile(&self) {
let mut rx = self.reconcile_done.clone();
let _ = rx.wait_for(|&done| done).await;
}
}
pub(super) fn validate_id(kind: &str, id: &str) -> Result<()> {
if id.is_empty() {
return Err(VmmError::Config(format!("{kind} must not be empty")));
}
if !id
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
{
return Err(VmmError::Config(format!(
"invalid {kind} {id:?}: only ASCII letters, digits, '-' and '_' are allowed"
)));
}
Ok(())
}
pub(super) fn reserve_id(
instances: &InstanceMap,
id: &SandboxId,
placeholder: SandboxInstance,
) -> Result<IdReservation> {
let mut map = instances.write().unwrap();
if map.contains_key(id) {
return Err(VmmError::AlreadyExists(id.clone()));
}
map.insert(id.clone(), Arc::new(Mutex::new(placeholder)));
Ok(IdReservation {
instances: Arc::clone(instances),
id: id.clone(),
committed: false,
})
}
pub(super) struct IdReservation {
instances: InstanceMap,
id: SandboxId,
committed: bool,
}
impl IdReservation {
pub(super) fn instance(&self) -> Arc<Mutex<SandboxInstance>> {
self.instances
.read()
.unwrap()
.get(&self.id)
.cloned()
.expect("reserved instance is present until commit/drop")
}
pub(super) fn commit(mut self) {
self.committed = true;
}
}
impl Drop for IdReservation {
fn drop(&mut self) {
if !self.committed {
self.instances.write().unwrap().remove(&self.id);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn placeholder(id: &str) -> SandboxInstance {
SandboxInstance::new(
id.to_owned(),
SandboxSpec::default(),
None,
PathBuf::from("/tmp/x"),
)
}
#[test]
fn reserve_id_rejects_a_concurrent_duplicate() {
let instances: InstanceMap = Arc::new(RwLock::new(HashMap::new()));
let first = reserve_id(&instances, &"dup".to_owned(), placeholder("dup")).unwrap();
assert!(matches!(
reserve_id(&instances, &"dup".to_owned(), placeholder("dup")),
Err(VmmError::AlreadyExists(_))
));
first.commit();
assert!(instances.read().unwrap().contains_key("dup"));
}
#[test]
fn validate_id_accepts_safe_ids_and_rejects_traversal() {
for ok in ["sandbox1", "a-b_c", "0f3e9d16-1234", "A_B-9"] {
assert!(validate_id("id", ok).is_ok(), "{ok} should be valid");
}
for bad in ["", "..", ".", "a/b", "a\\b", "a b", "a.b", "a\0b", "../etc"] {
assert!(
validate_id("id", bad).is_err(),
"{bad:?} should be rejected"
);
}
}
#[test]
fn dropped_reservation_unwinds_the_placeholder() {
let instances: InstanceMap = Arc::new(RwLock::new(HashMap::new()));
{
let _r = reserve_id(&instances, &"tmp".to_owned(), placeholder("tmp")).unwrap();
assert!(instances.read().unwrap().contains_key("tmp"));
}
assert!(!instances.read().unwrap().contains_key("tmp"));
let again = reserve_id(&instances, &"tmp".to_owned(), placeholder("tmp"));
assert!(again.is_ok());
}
}